diff --git a/docs/vue/ai-usage-guide.md b/docs/vue/ai-usage-guide.md index c048775..dbc682f 100644 --- a/docs/vue/ai-usage-guide.md +++ b/docs/vue/ai-usage-guide.md @@ -63,10 +63,15 @@ - loading placeholders: `GnSkeleton` - key-value metadata: `GnDescriptionList` - determinate progress: `GnProgress` +- usage/limit card: `GnUsageMeter` +- staged progress strip: `GnProgressStages` - wizard/staged process: `GnSteps` - chips/filters: `GnChip`, `GnChipGroup` - user or entity identity: `GnAvatar`, `GnIdentity`, `GnAvatarStack` - activity timeline/log: `GnTimeline`, `GnActivityLog` +- basic/definition/action lists: `GnList`, `GnDefinitionList`, `GnActionList` +- loaders: `GnLoader` +- specialized cards: `GnStatusCard`, `GnMetricCard`, `GnActionCard` - dropdown menu: `GnDropdown` - contextual panel: `GnPopover` - compact hint: `GnTooltip` @@ -106,7 +111,7 @@ ```bash npm run build -npm run build:example:vue +npm run test:vue-adapter ``` Warnings about `/assets/fonts/...` in Vite builds are expected with the current CSS contract. They mean the host app must serve GNexus UI Kit assets at `/assets`. diff --git a/docs/vue/component-map.md b/docs/vue/component-map.md index 792a267..9747aaa 100644 --- a/docs/vue/component-map.md +++ b/docs/vue/component-map.md @@ -35,6 +35,8 @@ | `GnSkeleton` | `.skeleton`, `.skeleton-stack` | Loading placeholders. | | `GnDescriptionList` | `.description-list` | Key-value metadata. | | `GnProgress` | `.progress` | Determinate progress. | +| `GnUsageMeter` | `.usage-meter` | Usage card with progress. | +| `GnProgressStages` | `.progress-stages` | Segmented staged progress. | | `GnSteps` | `.steps` | Wizard/staged process. | | `GnChip` | `.chip` | Static/selectable/removable chip. | | `GnChipGroup` | `.chip-group` | Chip layout wrapper. | @@ -43,6 +45,13 @@ | `GnAvatarStack` | `.avatar-stack` | Compact avatar group. | | `GnTimeline` | `.timeline` | Activity/event timeline. | | `GnActivityLog` | `.activity-log` | Compact log rows. | +| `GnList` | `.list` | Basic/ordered/icon list. | +| `GnDefinitionList` | `.list.list-definition` | Definition list pattern. | +| `GnActionList` | `.list.list-actions` | Action rows. | +| `GnLoader` | `.loader`, `.circle-loader` | Loading indicators. | +| `GnStatusCard` | `.card.status-card` | Status card variants. | +| `GnMetricCard` | `.card.metric-card` | Metric widget. | +| `GnActionCard` | `.card.action-card` | Action prompt card. | | `GnDropdown` | `.dropdown`, `.dropdown-menu` | Vue-native open state, outside click, Escape. | | `GnPopover` | `.popover`, `.popover-panel` | Vue-native contextual panel. | | `GnTooltip` | `.tooltip`, `.tooltip-panel` | Focus/hover tooltip wrapper. | diff --git a/docs/vue/migration-policy.md b/docs/vue/migration-policy.md index 8e34aad..30daf48 100644 --- a/docs/vue/migration-policy.md +++ b/docs/vue/migration-policy.md @@ -55,7 +55,7 @@ ```bash npm run build -npm run build:example:vue +npm run test:vue-adapter ``` Vite warnings about `/assets/fonts/...` are expected while the CSS asset contract uses absolute `/assets` paths. diff --git a/examples/vue/src/main.js b/examples/vue/src/main.js index 707883d..89a99fb 100644 --- a/examples/vue/src/main.js +++ b/examples/vue/src/main.js @@ -3,6 +3,7 @@ import "gnexus-ui-kit/dist/assets/fonts/phosphor-icons/src/css/icons.css"; import { GnButton, + GnActionCard, GnActivityLog, GnAvatar, GnBadge, @@ -14,10 +15,12 @@ GnEmptyState, GnFileUpload, GnInput, + GnMetricCard, GnModal, GnPopover, GnPageHeader, GnProgress, + GnProgressStages, GnRadioGroup, GnRange, GnSearchField, @@ -25,12 +28,14 @@ GnTooltip, GnToolbar, GnToastProvider, + GnUsageMeter, useToast } from "gnexus-ui-kit/vue"; const DemoScreen = { components: { GnButton, + GnActionCard, GnActivityLog, GnAvatar, GnBadge, @@ -42,15 +47,18 @@ GnEmptyState, GnFileUpload, GnInput, + GnMetricCard, GnModal, GnPopover, GnPageHeader, GnProgress, + GnProgressStages, GnRadioGroup, GnRange, GnSearchField, GnTabs, GnTooltip, + GnUsageMeter, }, setup() { const activeTab = ref("overview"); @@ -92,6 +100,12 @@ { label: "Eliza", value: "eliza" }, { label: "Emily", value: "emily" } ]; + const stages = [ + { label: "Created", status: "complete" }, + { label: "Review", status: "current" }, + { label: "Deploy" }, + { label: "Done" } + ]; const save = () => { toast.success({ title: "Saved", text: `${name.value} updated` }); @@ -112,6 +126,7 @@ query, rollout, modes, + stages, tabs, save }; @@ -170,6 +185,18 @@
+
+ + + + +
+
+ +
@@ -178,6 +205,13 @@
+ +
+ +
+
+ +
diff --git a/package.json b/package.json index 1515e6c..cc5f11c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "build:example:vue": "npm --prefix examples/vue run build", "build:vue": "gulp vue", "dev": "gulp serve", - "start": "gulp serve" + "start": "gulp serve", + "test:vue-adapter": "npm run build:vue && npm run build:example:vue" }, "peerDependencies": { "vue": "^3.4.0" diff --git a/src/vue/components/GnActionCard.js b/src/vue/components/GnActionCard.js new file mode 100644 index 0000000..fcb3f93 --- /dev/null +++ b/src/vue/components/GnActionCard.js @@ -0,0 +1,20 @@ +import { defineComponent, h } from "vue"; + +export default defineComponent({ + name: "GnActionCard", + props: { + kicker: { type: String, default: "" }, + title: { type: String, required: true }, + text: { type: String, default: "" } + }, + setup(props, { slots }) { + return () => h("article", { class: "card action-card" }, [ + h("div", { class: "card-content" }, [ + (props.kicker || slots.kicker) && h("span", { class: "action-card-kicker" }, slots.kicker?.() || props.kicker), + h("h3", { class: "action-card-title" }, slots.title?.() || props.title), + (props.text || slots.default) && h("p", { class: "action-card-text" }, slots.default?.() || props.text), + slots.actions && h("div", { class: "action-card-actions" }, slots.actions()) + ]) + ]); + } +}); diff --git a/src/vue/components/GnActionList.js b/src/vue/components/GnActionList.js new file mode 100644 index 0000000..5062904 --- /dev/null +++ b/src/vue/components/GnActionList.js @@ -0,0 +1,20 @@ +import { defineComponent, h } from "vue"; +import { cx } from "../utils.js"; + +export default defineComponent({ + name: "GnActionList", + props: { + items: { type: Array, default: () => [] } + }, + setup(props, { attrs, slots }) { + return () => h("ul", { ...attrs, class: cx("list list-actions", attrs.class) }, props.items.map(item => h("li", { + class: cx("list-item", item.muted && "list-item-muted") + }, [ + h("div", { class: "list-content" }, [ + h("div", { class: "list-title" }, slots.title?.({ item }) || item.title), + (item.subtitle || slots.subtitle) && h("div", { class: "list-subtitle" }, slots.subtitle?.({ item }) || item.subtitle) + ]), + slots.controls && h("div", { class: "list-controls" }, slots.controls({ item })) + ]))); + } +}); diff --git a/src/vue/components/GnDefinitionList.js b/src/vue/components/GnDefinitionList.js new file mode 100644 index 0000000..e66bc7b --- /dev/null +++ b/src/vue/components/GnDefinitionList.js @@ -0,0 +1,17 @@ +import { defineComponent, h } from "vue"; +import { cx } from "../utils.js"; + +export default defineComponent({ + name: "GnDefinitionList", + props: { + items: { type: Array, default: () => [] } + }, + setup(props, { attrs, slots }) { + return () => h("dl", { ...attrs, class: cx("list list-definition", attrs.class) }, props.items.map(item => h("div", { + class: "list-row" + }, [ + h("dt", { class: "list-term" }, item.term || item.label), + h("dd", { class: "list-desc" }, slots[item.key]?.({ item }) || item.description || item.value) + ]))); + } +}); diff --git a/src/vue/components/GnList.js b/src/vue/components/GnList.js new file mode 100644 index 0000000..e17fc46 --- /dev/null +++ b/src/vue/components/GnList.js @@ -0,0 +1,25 @@ +import { defineComponent, h } from "vue"; +import { cx, iconNode } from "../utils.js"; + +export default defineComponent({ + name: "GnList", + props: { + items: { type: Array, default: () => [] }, + ordered: { type: Boolean, default: false }, + icons: { type: Boolean, default: false } + }, + setup(props, { attrs, slots }) { + const tag = props.ordered ? "ol" : "ul"; + + return () => h(tag, { + ...attrs, + class: cx("list", { + "list-ordered": props.ordered, + "with-icons": props.icons + }, attrs.class) + }, props.items.map(item => h("li", { class: "list-item" }, [ + iconNode(item.icon), + slots.item?.({ item }) || item.label || item + ]))); + } +}); diff --git a/src/vue/components/GnLoader.js b/src/vue/components/GnLoader.js new file mode 100644 index 0000000..0025dbf --- /dev/null +++ b/src/vue/components/GnLoader.js @@ -0,0 +1,18 @@ +import { defineComponent, h } from "vue"; +import { cx, iconNode } from "../utils.js"; + +export default defineComponent({ + name: "GnLoader", + props: { + circle: { type: Boolean, default: false }, + label: { type: String, default: "Loading" } + }, + setup(props, { attrs }) { + return () => props.circle + ? h("div", { ...attrs, class: cx("circle-loader", attrs.class) }, [ + iconNode("ph-bold ph-spinner normalize"), + props.label + ]) + : h("div", { ...attrs, class: cx("loader", attrs.class), role: "status", "aria-label": props.label }); + } +}); diff --git a/src/vue/components/GnMetricCard.js b/src/vue/components/GnMetricCard.js new file mode 100644 index 0000000..f3bd776 --- /dev/null +++ b/src/vue/components/GnMetricCard.js @@ -0,0 +1,31 @@ +import { defineComponent, h } from "vue"; +import { cx, iconNode } from "../utils.js"; + +export default defineComponent({ + name: "GnMetricCard", + props: { + label: { type: String, required: true }, + value: { type: [String, Number], required: true }, + icon: { type: String, default: "ph-chart-line-up" }, + delta: { type: String, default: "" }, + negative: { type: Boolean, default: false }, + meta: { type: String, default: "" } + }, + setup(props, { attrs, slots }) { + return () => h("article", { ...attrs, class: cx("card metric-card", attrs.class) }, [ + h("div", { class: "card-content" }, [ + h("div", { class: "metric-card-header" }, [ + h("p", { class: "metric-card-label" }, slots.label?.() || props.label), + h("span", { class: "metric-card-icon" }, [iconNode(props.icon)]) + ]), + h("p", { class: "metric-card-value" }, slots.value?.() || props.value), + (props.delta || props.meta || slots.meta) && h("div", { class: "metric-card-meta" }, [ + props.delta && h("span", { + class: cx("metric-card-delta", { "metric-card-delta-negative": props.negative }) + }, props.delta), + slots.meta?.() || props.meta + ]) + ]) + ]); + } +}); diff --git a/src/vue/components/GnProgressStages.js b/src/vue/components/GnProgressStages.js new file mode 100644 index 0000000..d4ef2d3 --- /dev/null +++ b/src/vue/components/GnProgressStages.js @@ -0,0 +1,17 @@ +import { defineComponent, h } from "vue"; +import { cx } from "../utils.js"; + +export default defineComponent({ + name: "GnProgressStages", + props: { + items: { type: Array, default: () => [] } + }, + setup(props, { attrs }) { + return () => h("div", { ...attrs, class: cx("progress-stages", attrs.class) }, props.items.map(item => h("div", { + class: cx("progress-stage", { + "progress-stage-complete": item.status === "complete", + "progress-stage-current": item.status === "current" + }) + }, item.label || item))); + } +}); diff --git a/src/vue/components/GnStatusCard.js b/src/vue/components/GnStatusCard.js new file mode 100644 index 0000000..3b26707 --- /dev/null +++ b/src/vue/components/GnStatusCard.js @@ -0,0 +1,25 @@ +import { defineComponent, h } from "vue"; +import { cx, iconNode, normalizeVariant } from "../utils.js"; + +export default defineComponent({ + name: "GnStatusCard", + props: { + title: { type: String, required: true }, + text: { type: String, default: "" }, + icon: { type: String, default: "ph-stack" }, + variant: { type: String, default: "primary" } + }, + setup(props, { attrs, slots }) { + const variant = normalizeVariant(props.variant); + + return () => h("article", { ...attrs, class: cx("card status-card", `card-${variant}`, attrs.class) }, [ + h("span", { class: "card-title" }, slots.title?.() || props.title), + h("div", { class: "card-content" }, [ + h("div", { class: "status-icon-container" }, [ + h("div", { class: "status-icon" }, slots.icon?.() || [iconNode(props.icon)]) + ]), + (props.text || slots.default) && h("p", { class: "status-name" }, slots.default?.() || props.text) + ]) + ]); + } +}); diff --git a/src/vue/components/GnUsageMeter.js b/src/vue/components/GnUsageMeter.js new file mode 100644 index 0000000..03d3471 --- /dev/null +++ b/src/vue/components/GnUsageMeter.js @@ -0,0 +1,26 @@ +import { defineComponent, h } from "vue"; +import GnProgress from "./GnProgress.js"; + +export default defineComponent({ + name: "GnUsageMeter", + props: { + title: { type: String, required: true }, + value: { type: Number, required: true }, + max: { type: Number, default: 100 }, + meta: { type: String, default: "" } + }, + setup(props, { slots }) { + return () => { + const percent = Math.max(0, Math.min(100, Math.round((props.value / props.max) * 100))); + + return h("section", { class: "usage-meter" }, [ + h("h3", { class: "usage-meter-title" }, [ + slots.title?.() || props.title, + h("span", { class: "usage-meter-value" }, `${percent}%`) + ]), + h(GnProgress, { value: props.value, max: props.max }), + (props.meta || slots.meta) && h("p", { class: "usage-meter-meta" }, slots.meta?.() || props.meta) + ]); + }; + } +}); diff --git a/src/vue/index.js b/src/vue/index.js index 2e872f9..7541b3f 100644 --- a/src/vue/index.js +++ b/src/vue/index.js @@ -1,4 +1,6 @@ export { default as GnAccordion } from "./components/GnAccordion.js"; +export { default as GnActionCard } from "./components/GnActionCard.js"; +export { default as GnActionList } from "./components/GnActionList.js"; export { default as GnActivityLog } from "./components/GnActivityLog.js"; export { default as GnAlert } from "./components/GnAlert.js"; export { default as GnAvatar } from "./components/GnAvatar.js"; @@ -12,6 +14,7 @@ export { default as GnCombobox } from "./components/GnCombobox.js"; export { default as GnConfirmDialog } from "./components/GnConfirmDialog.js"; export { default as GnDescriptionList } from "./components/GnDescriptionList.js"; +export { default as GnDefinitionList } from "./components/GnDefinitionList.js"; export { default as GnDropdown } from "./components/GnDropdown.js"; export { default as GnDrawer } from "./components/GnDrawer.js"; export { default as GnEmptyState } from "./components/GnEmptyState.js"; @@ -20,6 +23,9 @@ export { default as GnIdentity } from "./components/GnIdentity.js"; export { default as GnInput } from "./components/GnInput.js"; export { default as GnInputGroup } from "./components/GnInputGroup.js"; +export { default as GnList } from "./components/GnList.js"; +export { default as GnLoader } from "./components/GnLoader.js"; +export { default as GnMetricCard } from "./components/GnMetricCard.js"; export { default as GnModal } from "./components/GnModal.js"; export { default as GnNavList } from "./components/GnNavList.js"; export { default as GnNavigationShell } from "./components/GnNavigationShell.js"; @@ -27,6 +33,7 @@ export { default as GnPagination } from "./components/GnPagination.js"; export { default as GnPopover } from "./components/GnPopover.js"; export { default as GnProgress } from "./components/GnProgress.js"; +export { default as GnProgressStages } from "./components/GnProgressStages.js"; export { default as GnRadio } from "./components/GnRadio.js"; export { default as GnRadioGroup } from "./components/GnRadioGroup.js"; export { default as GnRange } from "./components/GnRange.js"; @@ -34,6 +41,7 @@ export { default as GnSelect } from "./components/GnSelect.js"; export { default as GnSkeleton } from "./components/GnSkeleton.js"; export { default as GnSteps } from "./components/GnSteps.js"; +export { default as GnStatusCard } from "./components/GnStatusCard.js"; export { default as GnSwitch } from "./components/GnSwitch.js"; export { default as GnTable } from "./components/GnTable.js"; export { default as GnTabs } from "./components/GnTabs.js"; @@ -42,5 +50,6 @@ export { default as GnTooltip } from "./components/GnTooltip.js"; export { default as GnToolbar } from "./components/GnToolbar.js"; export { default as GnToastProvider } from "./components/GnToastProvider.js"; +export { default as GnUsageMeter } from "./components/GnUsageMeter.js"; export { useToast } from "./composables/useToast.js"; export { components, default as GnexusUiVue } from "./plugin.js"; diff --git a/src/vue/plugin.js b/src/vue/plugin.js index 5aa61c1..c11d739 100644 --- a/src/vue/plugin.js +++ b/src/vue/plugin.js @@ -1,4 +1,6 @@ import GnAccordion from "./components/GnAccordion.js"; +import GnActionCard from "./components/GnActionCard.js"; +import GnActionList from "./components/GnActionList.js"; import GnActivityLog from "./components/GnActivityLog.js"; import GnAlert from "./components/GnAlert.js"; import GnAvatar from "./components/GnAvatar.js"; @@ -12,6 +14,7 @@ import GnCombobox from "./components/GnCombobox.js"; import GnConfirmDialog from "./components/GnConfirmDialog.js"; import GnDescriptionList from "./components/GnDescriptionList.js"; +import GnDefinitionList from "./components/GnDefinitionList.js"; import GnDropdown from "./components/GnDropdown.js"; import GnDrawer from "./components/GnDrawer.js"; import GnEmptyState from "./components/GnEmptyState.js"; @@ -20,6 +23,9 @@ import GnIdentity from "./components/GnIdentity.js"; import GnInput from "./components/GnInput.js"; import GnInputGroup from "./components/GnInputGroup.js"; +import GnList from "./components/GnList.js"; +import GnLoader from "./components/GnLoader.js"; +import GnMetricCard from "./components/GnMetricCard.js"; import GnModal from "./components/GnModal.js"; import GnNavList from "./components/GnNavList.js"; import GnNavigationShell from "./components/GnNavigationShell.js"; @@ -27,6 +33,7 @@ import GnPagination from "./components/GnPagination.js"; import GnPopover from "./components/GnPopover.js"; import GnProgress from "./components/GnProgress.js"; +import GnProgressStages from "./components/GnProgressStages.js"; import GnRadio from "./components/GnRadio.js"; import GnRadioGroup from "./components/GnRadioGroup.js"; import GnRange from "./components/GnRange.js"; @@ -34,6 +41,7 @@ import GnSelect from "./components/GnSelect.js"; import GnSkeleton from "./components/GnSkeleton.js"; import GnSteps from "./components/GnSteps.js"; +import GnStatusCard from "./components/GnStatusCard.js"; import GnSwitch from "./components/GnSwitch.js"; import GnTable from "./components/GnTable.js"; import GnTabs from "./components/GnTabs.js"; @@ -42,9 +50,12 @@ import GnTooltip from "./components/GnTooltip.js"; import GnToolbar from "./components/GnToolbar.js"; import GnToastProvider from "./components/GnToastProvider.js"; +import GnUsageMeter from "./components/GnUsageMeter.js"; export const components = { GnAccordion, + GnActionCard, + GnActionList, GnActivityLog, GnAlert, GnAvatar, @@ -58,6 +69,7 @@ GnCombobox, GnConfirmDialog, GnDescriptionList, + GnDefinitionList, GnDropdown, GnDrawer, GnEmptyState, @@ -66,6 +78,9 @@ GnIdentity, GnInput, GnInputGroup, + GnList, + GnLoader, + GnMetricCard, GnModal, GnNavList, GnNavigationShell, @@ -73,6 +88,7 @@ GnPagination, GnPopover, GnProgress, + GnProgressStages, GnRadio, GnRadioGroup, GnRange, @@ -80,6 +96,7 @@ GnSelect, GnSkeleton, GnSteps, + GnStatusCard, GnSwitch, GnTable, GnTabs, @@ -87,7 +104,8 @@ GnTimeline, GnTooltip, GnToolbar, - GnToastProvider + GnToastProvider, + GnUsageMeter }; export default {