diff --git a/demo/partials/lists.html b/demo/partials/lists.html index 22e5832..1af4b7f 100644 --- a/demo/partials/lists.html +++ b/demo/partials/lists.html @@ -51,4 +51,88 @@ ` }).mount("#vue-action-list-app"); + +

Definition list

+
+
+
Item
+
Basic interface entity.
+
+
+
Group
+
Logical grouping of elements.
+
+
+
Flow
+
Set of actions performed sequentially.
+
+
+ +

Vue definition list

+
+ + +

Navigation list

+ + +

Vue navigation list

+
+ diff --git a/demo/partials/progress.html b/demo/partials/progress.html index 5d8a547..25f5196 100644 --- a/demo/partials/progress.html +++ b/demo/partials/progress.html @@ -58,6 +58,38 @@ }).mount("#vue-progress-app"); +

Staged progress

+
+
+ Queued + Build + Deploy + Verify +
+
+ +

Vue staged progress

+
+ +

Usage meter

diff --git a/docs/component-coverage.md b/docs/component-coverage.md index 506e332..90055b6 100644 --- a/docs/component-coverage.md +++ b/docs/component-coverage.md @@ -37,5 +37,19 @@ | Tooltip | done | GnTooltip | partial | | Dropdown | done | GnDropdown, GnDropdownItem | partial | | NavigationShell | done | GnNavigationShell, GnNavigationItem | partial | +| DescriptionList | done | GnDescriptionList | partial | +| DefinitionList | done | GnDefinitionList | partial | +| List | done | GnList, GnListItem | partial | +| NavList | done | GnNavList | partial | +| Steps | done | GnSteps, GnStep | partial | +| ProgressStages | done | GnProgressStages | partial | +| Breadcrumbs | done | GnBreadcrumbs, GnBreadcrumbItem | partial | +| Divider | done | GnDivider | partial | +| Popover | done | GnPopover | partial | +| Pagination | done | GnPagination | partial | +| Timeline | done | GnTimeline | partial | +| FileUpload | done | GnFileUpload, GnFileAttachment | partial | +| Repeater | done | GnRepeater | partial | +| UsageMeter | done | GnUsageMeter | partial | Статусы: `—` не начато, `WIP` в работе, `done` готово, `redesigned` перерисован в новом стиле. diff --git a/src/scss/components/_lists.scss b/src/scss/components/_lists.scss index 2f07442..1226e6c 100644 --- a/src/scss/components/_lists.scss +++ b/src/scss/components/_lists.scss @@ -137,3 +137,105 @@ } } } + +// Definition list — term/value pairs inside a .list container. +.list-definition { + .list-row { + display: grid; + grid-template-columns: minmax(140px, 0.36fr) minmax(0, 1fr); + gap: $space-4; + padding: $space-3 $space-4; + border-bottom: $border-width-base $border-style-base rgba($neutral-900, 0.06); + + &:last-child { + border-bottom: 0; + } + } + + .list-term { + margin: 0; + color: $neutral-600; + font-size: 13px; + font-weight: $font-weight-semibold; + line-height: 1.5; + text-transform: uppercase; + } + + .list-desc { + min-width: 0; + margin: 0; + color: $neutral-900; + font-size: 15px; + font-weight: $font-weight-medium; + line-height: 1.5; + } +} + +@media (max-width: 640px) { + .list-definition { + .list-row { + grid-template-columns: 1fr; + gap: $space-1; + } + } +} + +// Navigation / interactive list. +.list-nav { + .list-item { + padding: 0; + + &.list-item-active .list-action { + background-color: rgba($mint-500, 0.12); + color: $neutral-900; + } + + &.list-item-disabled .list-action { + opacity: 0.6; + cursor: not-allowed; + } + } + + .list-action { + display: flex; + align-items: center; + justify-content: space-between; + gap: $space-3; + width: 100%; + padding: $space-3 $space-4; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + text-decoration: none; + cursor: pointer; + transition: background-color $motion-base $motion-ease; + + &:hover:not(:disabled) { + background-color: rgba($neutral-900, 0.04); + } + + &:focus-visible { + @include focus_ring; + outline-offset: -2px; + } + } + + .list-label { + display: inline-flex; + align-items: center; + gap: $space-2; + + .ph { + color: $neutral-600; + } + } + + .list-meta { + margin-left: auto; + color: $neutral-500; + font-size: 12px; + font-weight: $font-weight-semibold; + } +} diff --git a/src/scss/components/_progress.scss b/src/scss/components/_progress.scss index 88126c5..4d751d8 100644 --- a/src/scss/components/_progress.scss +++ b/src/scss/components/_progress.scss @@ -86,6 +86,56 @@ } } +// Staged progress +.progress-stages { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: $space-2; + font-size: 13px; + font-weight: $font-weight-semibold; + line-height: 1; + + .progress-stage { + display: inline-flex; + align-items: center; + gap: $space-2; + padding: $space-2 $space-3; + border-radius: $radius-md; + background-color: $neutral-100; + color: $neutral-500; + transition: background-color $motion-base $motion-ease, color $motion-base $motion-ease; + + &::before { + content: ""; + width: 8px; + height: 8px; + border-radius: $radius-full; + background-color: currentColor; + opacity: 0.5; + } + + &.progress-stage-complete { + background-color: rgba($success-text, 0.12); + color: $success-text; + + &::before { + opacity: 1; + } + } + + &.progress-stage-current { + background-color: rgba($mint-600, 0.12); + color: $mint-700; + + &::before { + opacity: 1; + box-shadow: 0 0 0 3px rgba($mint-600, 0.2); + } + } + } +} + /* Usage meter */ .usage-meter { width: 100%; diff --git a/src/vue/components/GnDefinitionList.js b/src/vue/components/GnDefinitionList.js new file mode 100644 index 0000000..51a741b --- /dev/null +++ b/src/vue/components/GnDefinitionList.js @@ -0,0 +1,27 @@ +/** + * GnDefinitionList — term/value pairs rendered as a definition list. + * + * API-compatible with gnexus-ui-kit GnDefinitionList. + */ + +import { defineComponent, h } from "vue"; +import { cx } from "../utils.js"; + +export default defineComponent({ + name: "GnDefinitionList", + inheritAttrs: false, + 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 || item.key), + h("dd", { class: "list-desc" }, slots[item.key]?.({ item }) || item.description || item.value) + ]))); + } +}); diff --git a/src/vue/components/GnNavList.js b/src/vue/components/GnNavList.js new file mode 100644 index 0000000..253f1e1 --- /dev/null +++ b/src/vue/components/GnNavList.js @@ -0,0 +1,91 @@ +/** + * GnNavList — interactive navigation list with optional vue-router support. + * + * API-compatible with gnexus-ui-kit GnNavList. + */ + +import { computed, defineComponent, h } from "vue"; +import { cx, iconNode } from "../utils.js"; +import { tryUseRouter, tryUseRoute, isRouteActive } from "../composables/useVueRouter.js"; + +export default defineComponent({ + name: "GnNavList", + inheritAttrs: false, + props: { + items: { type: Array, default: () => [] }, + activeMatch: { type: String, default: "prefix" } + }, + emits: ["select"], + setup(props, { attrs, emit, slots }) { + const router = tryUseRouter(); + const route = tryUseRoute(); + const hasRouter = Boolean(router && route); + + const resolveHref = to => { + if (!to) { + return undefined; + } + + if (typeof to === "string") { + return to; + } + + if (to.path) { + return to.path; + } + + return undefined; + }; + + const navItems = computed(() => props.items.map(item => { + const hasTo = Boolean(item.to); + const resolvedHref = hasTo + ? (hasRouter ? router.resolve(item.to).href : resolveHref(item.to)) + : item.href; + const isActive = hasTo && hasRouter + ? isRouteActive(route, item.to, props.activeMatch) + : Boolean(item.active); + + return { + ...item, + resolvedHref, + isActive, + hasTo + }; + })); + + return () => h("ul", { + ...attrs, + class: cx("list", "list-nav", attrs.class) + }, navItems.value.map(item => h("li", { + class: cx("list-item", { "list-item-active": item.isActive, "list-item-disabled": item.disabled }) + }, [ + h(item.resolvedHref ? "a" : "button", { + class: "list-action", + href: item.resolvedHref, + type: item.resolvedHref ? undefined : "button", + disabled: item.disabled || undefined, + onClick: event => { + if (item.disabled) { + event.preventDefault(); + return; + } + + if (item.hasTo && hasRouter) { + event.preventDefault(); + router.push(item.to); + } + + item.onSelect?.(item, event); + emit("select", item); + } + }, [ + h("span", { class: "list-label" }, [ + iconNode(item.icon), + slots.label?.({ item }) || item.label + ]), + (item.meta || slots.meta) && h("span", { class: "list-meta" }, slots.meta?.({ item }) || item.meta) + ]) + ]))); + } +}); diff --git a/src/vue/components/GnProgressStages.js b/src/vue/components/GnProgressStages.js new file mode 100644 index 0000000..ea4901c --- /dev/null +++ b/src/vue/components/GnProgressStages.js @@ -0,0 +1,27 @@ +/** + * GnProgressStages — horizontal staged progress indicator. + * + * API-compatible with gnexus-ui-kit GnProgressStages. + */ + +import { defineComponent, h } from "vue"; +import { cx } from "../utils.js"; + +export default defineComponent({ + name: "GnProgressStages", + inheritAttrs: false, + props: { + items: { type: Array, default: () => [] } + }, + setup(props, { attrs }) { + return () => h("div", { + ...attrs, + class: cx("progress-stages", attrs.class) + }, props.items.map(item => h("span", { + class: cx("progress-stage", { + "progress-stage-complete": item.status === "complete", + "progress-stage-current": item.status === "current" + }) + }, item.label || item))); + } +}); diff --git a/src/vue/index.js b/src/vue/index.js index 42c19a9..c2ee38c 100644 --- a/src/vue/index.js +++ b/src/vue/index.js @@ -40,12 +40,12 @@ export { default as GnTableRow } from "./components/GnTableRow.js"; export { default as GnTableHeader } from "./components/GnTableHeader.js"; export { default as GnDescriptionList } from "./components/GnDescriptionList.js"; -export { default as GnDefinitionList } from "./components/GnDescriptionList.js"; +export { default as GnDefinitionList } from "./components/GnDefinitionList.js"; export { default as GnList } from "./components/GnList.js"; -export { default as GnNavList } from "./components/GnList.js"; +export { default as GnNavList } from "./components/GnNavList.js"; export { default as GnSteps } from "./components/GnSteps.js"; export { default as GnStep } from "./components/GnStep.js"; -export { default as GnProgressStages } from "./components/GnSteps.js"; +export { default as GnProgressStages } from "./components/GnProgressStages.js"; export { default as GnListItem } from "./components/GnListItem.js"; export { default as GnEmptyState } from "./components/GnEmptyState.js"; export { default as GnPageHeader } from "./components/GnPageHeader.js"; diff --git a/src/vue/plugin.js b/src/vue/plugin.js index a94d040..e56082a 100644 --- a/src/vue/plugin.js +++ b/src/vue/plugin.js @@ -40,7 +40,9 @@ import GnTableRow from "./components/GnTableRow.js"; import GnTableHeader from "./components/GnTableHeader.js"; import GnDescriptionList from "./components/GnDescriptionList.js"; +import GnDefinitionList from "./components/GnDefinitionList.js"; import GnList from "./components/GnList.js"; +import GnNavList from "./components/GnNavList.js"; import GnListItem from "./components/GnListItem.js"; import GnEmptyState from "./components/GnEmptyState.js"; import GnPageHeader from "./components/GnPageHeader.js"; @@ -56,6 +58,7 @@ import GnAccordion from "./components/GnAccordion.js"; import GnSteps from "./components/GnSteps.js"; import GnStep from "./components/GnStep.js"; +import GnProgressStages from "./components/GnProgressStages.js"; import GnDivider from "./components/GnDivider.js"; import GnFileUpload from "./components/GnFileUpload.js"; import GnFileAttachment from "./components/GnFileAttachment.js"; @@ -113,9 +116,9 @@ GnTableRow, GnTableHeader, GnDescriptionList, - GnDefinitionList: GnDescriptionList, + GnDefinitionList, GnList, - GnNavList: GnList, + GnNavList, GnListItem, GnEmptyState, GnPageHeader, @@ -131,7 +134,7 @@ GnAccordion, GnSteps, GnStep, - GnProgressStages: GnSteps, + GnProgressStages, GnDivider, GnFileUpload, GnFileAttachment,