@xleddyl/443-components-vue 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CATALOG.md +195 -180
  2. package/README.md +107 -23
  3. package/dist/components/data/Tag.vue +2 -0
  4. package/dist/components/data/Tag.vue.d.ts +2 -0
  5. package/dist/components/data/TagList.vue +239 -0
  6. package/dist/components/data/TagList.vue.d.ts +53 -0
  7. package/dist/components/data/index.d.ts +1 -0
  8. package/dist/components/data/index.mjs +1 -0
  9. package/dist/components/data/types.d.ts +15 -0
  10. package/dist/components/forms/Combobox.vue +33 -4
  11. package/dist/components/forms/Combobox.vue.d.ts +3 -1
  12. package/dist/components/forms/NumberInput.vue.d.ts +1 -1
  13. package/dist/components/forms/PinInput.vue.d.ts +2 -2
  14. package/dist/components/forms/types.d.ts +2 -0
  15. package/dist/components/layout/AppShell.vue +14 -2
  16. package/dist/components/layout/AppShell.vue.d.ts +3 -1
  17. package/dist/components/layout/Sidebar.vue +8 -3
  18. package/dist/components/layout/Sidebar.vue.d.ts +6 -1
  19. package/dist/components/layout/Stepper.vue +148 -0
  20. package/dist/components/layout/Stepper.vue.d.ts +420 -0
  21. package/dist/components/layout/index.d.ts +1 -0
  22. package/dist/components/layout/index.mjs +1 -0
  23. package/dist/components/layout/types.d.ts +13 -0
  24. package/dist/components/overlays/Toaster.vue +10 -0
  25. package/dist/components/overlays/Toaster.vue.d.ts +1 -1
  26. package/dist/components/templates/DashboardLayout.vue +13 -1
  27. package/dist/components/templates/DashboardLayout.vue.d.ts +7 -2
  28. package/dist/components/templates/SettingsModal.vue +95 -4
  29. package/dist/components/templates/SettingsModal.vue.d.ts +6 -1
  30. package/dist/components/templates/types.d.ts +4 -1
  31. package/dist/composables/data.d.ts +1 -0
  32. package/dist/composables/data.mjs +1 -0
  33. package/dist/composables/layout.d.ts +1 -0
  34. package/dist/composables/layout.mjs +1 -0
  35. package/dist/composables/useRouteState.d.ts +13 -0
  36. package/dist/composables/useRouteState.mjs +37 -0
  37. package/dist/composables/useSortable.d.ts +38 -0
  38. package/dist/composables/useSortable.mjs +215 -0
  39. package/dist/grid/DataGrid.vue +7 -1
  40. package/dist/grid/DataGrid.vue.d.ts +8 -1
  41. package/dist/grid/GridToolbar.vue +57 -2
  42. package/dist/grid/GridToolbar.vue.d.ts +13 -2
  43. package/dist/grid/cells/BadgeCell.vue +90 -18
  44. package/dist/grid/cells/BadgeCell.vue.d.ts +14 -14
  45. package/dist/grid/data-grid.css +57 -0
  46. package/dist/grid/spreadsheet.mjs +8 -4
  47. package/dist/grid/types.d.ts +3 -0
  48. package/dist/icons/builtin.d.ts +2 -0
  49. package/dist/icons/builtin.mjs +2 -0
  50. package/dist/internal/data/sortable.d.ts +5 -0
  51. package/dist/internal/data/sortable.mjs +23 -0
  52. package/dist/internal/layout/query.d.ts +6 -0
  53. package/dist/internal/layout/query.mjs +64 -0
  54. package/dist/internal/layout/route.d.ts +9 -2
  55. package/dist/internal/overlays/avoid.d.ts +26 -0
  56. package/dist/internal/overlays/avoid.mjs +134 -0
  57. package/dist/locale/de.mjs +24 -0
  58. package/dist/locale/en.mjs +24 -0
  59. package/dist/locale/it.mjs +24 -0
  60. package/dist/locale/types.d.ts +24 -0
  61. package/dist/nuxt/module.mjs +14 -0
  62. package/dist/nuxt/runtime/layouts/dashboard.vue +9 -1
  63. package/dist/styles/data/tag-list.css +93 -0
  64. package/dist/styles/data.css +1 -0
  65. package/dist/styles/forced-colors.css +10 -0
  66. package/dist/styles/layout/app-shell.css +25 -6
  67. package/dist/styles/layout/sidebar.css +11 -2
  68. package/dist/styles/layout/stepper.css +244 -0
  69. package/dist/styles/layout.css +1 -0
  70. package/dist/styles/overlays/toast.css +17 -12
  71. package/dist/styles/templates/settings-modal.css +5 -1
  72. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ import type { Tone } from '../core/types';
2
+ import type { BadgeSize, TagListAccept, TagListDrop, TagListItem } from './types';
3
+ declare const __VLS_export: <T extends TagListItem>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
4
+ props: import("vue").PublicProps & __VLS_PrettifyLocal<({
5
+ label?: string;
6
+ sortable?: boolean;
7
+ removable?: boolean;
8
+ moveButtons?: boolean;
9
+ accept?: TagListAccept;
10
+ dropEffect?: "copy" | "move" | "link";
11
+ tone?: Tone;
12
+ size?: BadgeSize;
13
+ disabled?: boolean;
14
+ emptyText?: string;
15
+ removeLabel?: string;
16
+ } & {
17
+ modelValue?: T[];
18
+ }) & {
19
+ onRemove?: ((item: T, index: number) => any) | undefined;
20
+ onMove?: ((move: {
21
+ item: T;
22
+ from: number;
23
+ to: number;
24
+ }) => any) | undefined;
25
+ "onUpdate:modelValue"?: ((value: T[]) => any) | undefined;
26
+ onItemDrop?: ((drop: TagListDrop) => any) | undefined;
27
+ } & Partial<import("vue").HTMLAttributes & import("vue").ReservedProps>> & (typeof globalThis extends {
28
+ __VLS_PROPS_FALLBACK: infer P;
29
+ } ? P : {});
30
+ expose: (exposed: {}) => void;
31
+ attrs: any;
32
+ slots: {
33
+ item?: (props: {
34
+ item: T;
35
+ index: number;
36
+ }) => unknown;
37
+ empty?: () => unknown;
38
+ };
39
+ emit: (((evt: "remove", item: T, index: number) => void) & ((evt: "move", move: {
40
+ item: T;
41
+ from: number;
42
+ to: number;
43
+ }) => void) & ((evt: "itemDrop", drop: TagListDrop) => void)) & ((event: "update:modelValue", value: T[]) => void);
44
+ }>) => import("vue").VNode & {
45
+ __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
46
+ };
47
+ declare const _default: typeof __VLS_export;
48
+ export default _default;
49
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
50
+ [K in keyof T]: T[K];
51
+ } : {
52
+ [K in keyof T as K]: T[K];
53
+ }) & {};
@@ -9,6 +9,7 @@ export { default as SelectionBar } from './SelectionBar.vue';
9
9
  export { default as Stat } from './Stat.vue';
10
10
  export { default as Table } from './Table.vue';
11
11
  export { default as Tag } from './Tag.vue';
12
+ export { default as TagList } from './TagList.vue';
12
13
  export { default as Timeline } from './Timeline.vue';
13
14
  export { default as Tree } from './Tree.vue';
14
15
  export type * from './types';
@@ -9,6 +9,7 @@ export { default as SelectionBar } from "./SelectionBar.vue";
9
9
  export { default as Stat } from "./Stat.vue";
10
10
  export { default as Table } from "./Table.vue";
11
11
  export { default as Tag } from "./Tag.vue";
12
+ export { default as TagList } from "./TagList.vue";
12
13
  export { default as Timeline } from "./Timeline.vue";
13
14
  export { default as Tree } from "./Tree.vue";
14
15
  export { applyTreeMove } from "../../internal/data/tree.mjs";
@@ -1,4 +1,5 @@
1
1
  import type { Tone } from '../core/types';
2
+ import type { IconName } from '../../composables/useIcons';
2
3
  import type { MenuItems } from '../overlays/types';
3
4
  export type BadgeVariant = 'soft' | 'solid' | 'outline';
4
5
  export type BadgeSize = 'sm' | 'md';
@@ -94,3 +95,17 @@ export interface FilterPillOption<T extends FilterPillValue = FilterPillValue> {
94
95
  removable?: boolean;
95
96
  }
96
97
  export type SelectionBarPlacement = 'inline' | 'sticky' | 'floating';
98
+ export interface TagListItem {
99
+ value: string | number;
100
+ label: string;
101
+ tone?: Tone;
102
+ icon?: IconName;
103
+ removable?: boolean;
104
+ disabled?: boolean;
105
+ }
106
+ export interface TagListDrop {
107
+ index: number;
108
+ dataTransfer: DataTransfer | null;
109
+ event: DragEvent;
110
+ }
111
+ export type TagListAccept = string | string[] | ((event: DragEvent) => boolean);
@@ -20,7 +20,7 @@
20
20
  :class="[
21
21
  `k-field-${size}`,
22
22
  {
23
- 'is-invalid': control.invalid.value,
23
+ 'is-invalid': invalid,
24
24
  'is-disabled': control.disabled.value,
25
25
  'is-readonly': readonly
26
26
  }
@@ -48,8 +48,14 @@
48
48
  v-bind="$attrs"
49
49
  :disabled="control.disabled.value || void 0"
50
50
  :aria-required="control.required.value ? 'true' : void 0"
51
- :aria-invalid="control.invalid.value ? 'true' : void 0"
52
- :aria-describedby="joinIds($attrs['aria-describedby'], control.describedBy.value)"
51
+ :aria-invalid="invalid ? 'true' : void 0"
52
+ :aria-describedby="
53
+ joinIds(
54
+ $attrs['aria-describedby'],
55
+ control.describedBy.value,
56
+ localError ? errorId : void 0
57
+ )
58
+ "
53
59
  class="k-combobox-input"
54
60
  type="text"
55
61
  role="combobox"
@@ -167,6 +173,7 @@
167
173
  <slot name="empty" :query="query">{{ emptyLabel }}</slot>
168
174
  </div>
169
175
  </div>
176
+ <FieldMessage v-if="localError" :id="errorId" :message="createError" />
170
177
  <LiveRegion :message="live.message.value" />
171
178
  <template v-if="formName">
172
179
  <input
@@ -203,6 +210,7 @@ import { normalizeSearch } from "../../internal/core/text";
203
210
  import { translateOr } from "../../locale";
204
211
  import { joinIds } from "../../internal/forms/control";
205
212
  import Icon from "../core/Icon.vue";
213
+ import FieldMessage from "../core/FieldMessage.vue";
206
214
  import Spinner from "../core/Spinner.vue";
207
215
  defineOptions({ inheritAttrs: false });
208
216
  const props = defineProps({
@@ -213,6 +221,8 @@ const props = defineProps({
213
221
  multiple: { type: Boolean, required: false },
214
222
  hideTags: { type: Boolean, required: false },
215
223
  creatable: { type: [Boolean, Function], required: false },
224
+ createValidate: { type: Function, required: false },
225
+ createTransform: { type: Function, required: false },
216
226
  debounce: { type: Number, required: false, default: 250 },
217
227
  minQueryLength: { type: Number, required: false, default: 0 },
218
228
  maxSelected: { type: Number, required: false },
@@ -260,6 +270,8 @@ const rootRef = useTemplateRef("rootRef");
260
270
  const controlRef = useTemplateRef("controlRef");
261
271
  const panelRef = useTemplateRef("panelRef");
262
272
  const inputRef = useTemplateRef("inputRef");
273
+ const errorId = `${baseId}-create-error`;
274
+ const createError = ref();
263
275
  const editing = ref(false);
264
276
  const activeIndex = ref(-1);
265
277
  const results = shallowRef([]);
@@ -294,6 +306,13 @@ const layer = useFloatingLayer({
294
306
  onOpen: () => emit("open"),
295
307
  onClose: () => emit("close")
296
308
  });
309
+ const invalid = computed(() => control.invalid.value || Boolean(createError.value));
310
+ const localError = computed(() => Boolean(createError.value) && !control.field);
311
+ function setCreateError(message) {
312
+ if (createError.value === message) return;
313
+ createError.value = message;
314
+ control.setControlError(message);
315
+ }
297
316
  const interactive = computed(() => !control.disabled.value && !props.readonly);
298
317
  const busy = computed(() => props.loading || search.searching.value);
299
318
  const formName = computed(() => props.name ?? control.field?.name.value);
@@ -336,7 +355,9 @@ const filteredOptions = computed(() => {
336
355
  });
337
356
  const createCandidate = computed(() => {
338
357
  if (!props.creatable || busy.value) return void 0;
339
- const text = activeQuery.value;
358
+ const raw = activeQuery.value;
359
+ if (!raw) return void 0;
360
+ const text = (props.createTransform ? props.createTransform(raw) ?? "" : raw).trim();
340
361
  if (!text) return void 0;
341
362
  const needle = normalizeSearch(text);
342
363
  const exists = [...filteredOptions.value, ...known.values()].some(
@@ -423,6 +444,7 @@ function resetQuery() {
423
444
  editing.value = false;
424
445
  query.value = "";
425
446
  }
447
+ watch(query, () => setCreateError(void 0));
426
448
  function onInput(event) {
427
449
  query.value = event.target.value;
428
450
  editing.value = true;
@@ -531,6 +553,13 @@ function onKeydown(event) {
531
553
  async function choose(item) {
532
554
  if (item.option.disabled) return;
533
555
  if (item.kind === "create") {
556
+ const verdict = props.createValidate?.(item.option.label);
557
+ if (typeof verdict === "string" && verdict) {
558
+ setCreateError(verdict);
559
+ if (!control.field) live.announce(verdict);
560
+ return;
561
+ }
562
+ setCreateError(void 0);
534
563
  let created;
535
564
  try {
536
565
  created = typeof props.creatable === "function" ? await props.creatable(item.option.label) : item.option;
@@ -1,6 +1,6 @@
1
1
  import type { IconName } from '../../composables/useIcons';
2
2
  import type { FieldSize } from '../core/types';
3
- import type { ComboboxCreate, ComboboxOption, ComboboxSearch } from './types';
3
+ import type { ComboboxCreate, ComboboxCreateTransform, ComboboxCreateValidate, ComboboxOption, ComboboxSearch } from './types';
4
4
  declare const __VLS_export: <T extends string | number, M extends T | T[] | null | undefined = T | T[] | null | undefined>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
5
5
  props: import("vue").PublicProps & __VLS_PrettifyLocal<({
6
6
  options?: ComboboxOption<T>[];
@@ -10,6 +10,8 @@ declare const __VLS_export: <T extends string | number, M extends T | T[] | null
10
10
  multiple?: boolean;
11
11
  hideTags?: boolean;
12
12
  creatable?: boolean | ComboboxCreate<T>;
13
+ createValidate?: ComboboxCreateValidate;
14
+ createTransform?: ComboboxCreateTransform;
13
15
  debounce?: number;
14
16
  minQueryLength?: number;
15
17
  maxSelected?: number;
@@ -487,10 +487,10 @@ declare const __VLS_export: import("vue").DefineComponent<{
487
487
  "onUpdate:modelValue"?: ((value: number | null | undefined) => any) | undefined;
488
488
  }>, {
489
489
  size: FieldSize;
490
+ stepper: boolean;
490
491
  invalid: boolean;
491
492
  step: number;
492
493
  align: "end" | "start";
493
- stepper: boolean;
494
494
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
495
495
  declare const _default: typeof __VLS_export;
496
496
  export default _default;
@@ -243,8 +243,8 @@ declare const __VLS_export: import("vue").DefineComponent<{
243
243
  clear: () => void;
244
244
  el: Readonly<import("vue").ShallowRef<HTMLInputElement | null>>;
245
245
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
246
- "update:modelValue": (value: string | null | undefined) => any;
247
246
  complete: (value: string) => any;
247
+ "update:modelValue": (value: string | null | undefined) => any;
248
248
  }, string, import("vue").PublicProps, Readonly<{
249
249
  length?: number;
250
250
  mode?: PinInputMode;
@@ -484,8 +484,8 @@ declare const __VLS_export: import("vue").DefineComponent<{
484
484
  src?: string | undefined | undefined;
485
485
  onCancel?: ((payload: Event) => void) | undefined;
486
486
  }> & Readonly<{
487
- "onUpdate:modelValue"?: ((value: string | null | undefined) => any) | undefined;
488
487
  onComplete?: ((value: string) => any) | undefined;
488
+ "onUpdate:modelValue"?: ((value: string | null | undefined) => any) | undefined;
489
489
  }>, {
490
490
  length: number;
491
491
  size: FieldSize;
@@ -31,6 +31,8 @@ export interface ComboboxOption<T = string | number> {
31
31
  keywords?: string[];
32
32
  }
33
33
  export type ComboboxSearch<T = string | number> = (query: string, signal: AbortSignal) => Promise<ComboboxOption<T>[]> | ComboboxOption<T>[];
34
+ export type ComboboxCreateValidate = (input: string) => string | true | null | undefined | void;
35
+ export type ComboboxCreateTransform = (input: string) => string | null | undefined;
34
36
  export type ComboboxCreate<T = string | number> = (query: string) => ComboboxOption<T> | Promise<ComboboxOption<T>>;
35
37
  export interface CalendarRange {
36
38
  start: string | null;
@@ -8,8 +8,9 @@
8
8
  'is-nav-open': open,
9
9
  'is-mobile': mobile,
10
10
  'is-named': viewTransition,
11
- 'is-fill': fill
11
+ 'is-fill': contentMode === 'bleed'
12
12
  }"
13
+ :data-content="contentMode"
13
14
  @keydown.esc="onEscape"
14
15
  >
15
16
  <a
@@ -62,7 +63,16 @@
62
63
  <slot name="topbar" :open-nav="openNav" :mobile="mobile" :collapsed="collapsed" />
63
64
  </header>
64
65
  <main :id="mainId" class="k-shell-main" tabindex="-1">
65
- <div class="k-shell-content" :class="{ 'is-fluid': fluid, 'is-fill': fill }">
66
+ <div
67
+ class="k-shell-content"
68
+ :class="[
69
+ `k-shell-content-${contentMode}`,
70
+ {
71
+ 'is-fluid': fluid && contentMode === 'page',
72
+ 'is-fill': contentMode === 'bleed'
73
+ }
74
+ ]"
75
+ >
66
76
  <slot />
67
77
  </div>
68
78
  </main>
@@ -98,11 +108,13 @@ const props = defineProps({
98
108
  skipLabel: { type: String, required: false },
99
109
  fluid: { type: Boolean, required: false },
100
110
  fill: { type: Boolean, required: false },
111
+ content: { type: String, required: false },
101
112
  topbar: { type: Boolean, required: false, default: void 0 },
102
113
  storageKey: { type: [String, Boolean], required: false },
103
114
  shortcut: { type: [String, Boolean], required: false, default: false },
104
115
  viewTransition: { type: Boolean, required: false }
105
116
  });
117
+ const contentMode = computed(() => props.content ?? (props.fill ? "bleed" : "page"));
106
118
  const { t } = useLocale();
107
119
  const sidebarText = computed(() => props.sidebarLabel ?? t("shell.navigation"));
108
120
  const closeText = computed(() => props.closeLabel ?? t("shell.closeNavigation"));
@@ -1,4 +1,4 @@
1
- import type { AppShellVariant } from './types';
1
+ import type { AppShellVariant, ShellContent } from './types';
2
2
  declare function toggleCollapsed(): void;
3
3
  declare function openNav(): void;
4
4
  declare function closeNav(options?: {
@@ -33,6 +33,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
33
33
  skipLabel?: string;
34
34
  fluid?: boolean;
35
35
  fill?: boolean;
36
+ content?: ShellContent;
36
37
  topbar?: boolean;
37
38
  storageKey?: string | false;
38
39
  shortcut?: string | false;
@@ -247,6 +248,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
247
248
  skipLabel?: string;
248
249
  fluid?: boolean;
249
250
  fill?: boolean;
251
+ content?: ShellContent;
250
252
  topbar?: boolean;
251
253
  storageKey?: string | false;
252
254
  shortcut?: string | false;
@@ -1,6 +1,10 @@
1
1
  <template>
2
2
  <div class="k-sidebar">
3
- <div v-if="$slots.header || collapsible" class="k-sidebar-header">
3
+ <div
4
+ v-if="$slots.header || collapsible"
5
+ class="k-sidebar-header"
6
+ :class="{ 'is-brand-center': brandAlign === 'center' && !isCollapsed }"
7
+ >
4
8
  <div class="k-sidebar-brand">
5
9
  <slot name="header" :collapsed="isCollapsed" />
6
10
  </div>
@@ -10,7 +14,7 @@
10
14
  variant="ghost"
11
15
  tone="neutral"
12
16
  size="xs"
13
- icon="chevron-left"
17
+ :icon="isCollapsed ? 'sidebar-expand' : 'sidebar-collapse'"
14
18
  :aria-label="collapseText"
15
19
  :aria-expanded="isCollapsed ? 'false' : 'true'"
16
20
  :aria-controls="shell.sidebarId"
@@ -43,7 +47,8 @@ import Button from "../core/Button.vue";
43
47
  const props = defineProps({
44
48
  label: { type: String, required: false },
45
49
  collapsible: { type: Boolean, required: false },
46
- collapseLabel: { type: String, required: false }
50
+ collapseLabel: { type: String, required: false },
51
+ brandAlign: { type: String, required: false, default: "start" }
47
52
  });
48
53
  const { t } = useLocale();
49
54
  const navText = computed(() => props.label ?? t("shell.main"));
@@ -1,3 +1,4 @@
1
+ import type { SidebarBrandAlign } from './types';
1
2
  declare var __VLS_1: {
2
3
  collapsed: boolean;
3
4
  }, __VLS_10: {
@@ -20,6 +21,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
20
21
  label?: string;
21
22
  collapsible?: boolean;
22
23
  collapseLabel?: string;
24
+ brandAlign?: SidebarBrandAlign;
23
25
  innerHTML?: string | undefined | undefined;
24
26
  class?: import("vue").ClassValue;
25
27
  style?: import("vue").StyleValue;
@@ -216,6 +218,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
216
218
  label?: string;
217
219
  collapsible?: boolean;
218
220
  collapseLabel?: string;
221
+ brandAlign?: SidebarBrandAlign;
219
222
  innerHTML?: string | undefined | undefined;
220
223
  class?: import("vue").ClassValue;
221
224
  style?: import("vue").StyleValue;
@@ -408,7 +411,9 @@ declare const __VLS_base: import("vue").DefineComponent<{
408
411
  ref?: import("vue").VNodeRef | undefined;
409
412
  ref_for?: boolean | undefined | undefined;
410
413
  ref_key?: string | undefined | undefined;
411
- }> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
414
+ }> & Readonly<{}>, {
415
+ brandAlign: SidebarBrandAlign;
416
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
412
417
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
413
418
  declare const _default: typeof __VLS_export;
414
419
  export default _default;
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <nav
3
+ class="k-stepper"
4
+ :class="[
5
+ `k-stepper-${orientation}`,
6
+ `k-stepper-${size}`,
7
+ { 'is-compact': compact === true, 'is-auto-compact': compact === 'auto' }
8
+ ]"
9
+ :aria-label="labelText"
10
+ >
11
+ <ol ref="list" class="k-stepper-list">
12
+ <li
13
+ v-for="(entry, index) in entries"
14
+ :key="entry.step.value"
15
+ class="k-stepper-item"
16
+ :class="{
17
+ 'is-current': entry.current,
18
+ 'is-complete': entry.status === 'complete',
19
+ 'is-error': entry.status === 'error',
20
+ 'is-upcoming': entry.status === 'upcoming',
21
+ 'is-disabled': !entry.enabled
22
+ }"
23
+ :data-state="entry.status"
24
+ >
25
+ <component
26
+ :is="entry.enabled ? 'button' : 'span'"
27
+ :type="entry.enabled ? 'button' : void 0"
28
+ class="k-stepper-step"
29
+ :data-index="index"
30
+ :aria-current="entry.current ? 'step' : void 0"
31
+ :aria-disabled="!entry.enabled && !entry.current && !readonly ? 'true' : void 0"
32
+ :tabindex="entry.enabled ? index === focusIndex ? 0 : -1 : void 0"
33
+ @click="entry.enabled && select(entry.step)"
34
+ @focus="focusIndex = index"
35
+ @keydown="onKeydown($event, index)"
36
+ >
37
+ <span class="k-stepper-indicator" aria-hidden="true">
38
+ <Icon v-if="entry.status === 'error'" name="exclamation-circle" />
39
+ <Icon v-else-if="entry.status === 'complete'" name="check" />
40
+ <Icon v-else-if="entry.step.icon" :name="entry.step.icon" />
41
+ <span v-else class="k-stepper-number">{{ index + 1 }}</span>
42
+ </span>
43
+ <span class="k-stepper-text">
44
+ <span class="k-visually-hidden">{{ positionText(index) }}</span>
45
+ <span class="k-stepper-label">{{ entry.step.label }}</span>
46
+ <span v-if="entry.step.description" class="k-stepper-description">{{
47
+ entry.step.description
48
+ }}</span>
49
+ <span v-if="entry.status === 'complete'" class="k-visually-hidden">{{
50
+ completeText
51
+ }}</span>
52
+ <span v-else-if="entry.status === 'error'" class="k-visually-hidden">{{
53
+ errorText
54
+ }}</span>
55
+ </span>
56
+ </component>
57
+ <span
58
+ v-if="index < entries.length - 1"
59
+ class="k-stepper-connector"
60
+ aria-hidden="true"
61
+ />
62
+ </li>
63
+ </ol>
64
+ <p v-if="compact !== false && currentEntry" class="k-stepper-summary" aria-hidden="true">
65
+ <span class="k-stepper-summary-position">{{ positionText(currentIndex) }}</span>
66
+ <span class="k-stepper-summary-label">{{ currentEntry.step.label }}</span>
67
+ </p>
68
+ </nav>
69
+ </template>
70
+
71
+ <script setup>
72
+ import { computed, ref, useTemplateRef, watch } from "vue";
73
+ import { useLocale } from "../../composables/useLocale";
74
+ import { translateOr } from "../../locale";
75
+ import { rovingIndex } from "../../internal/core/roving";
76
+ import Icon from "../core/Icon.vue";
77
+ const props = defineProps({
78
+ steps: { type: Array, required: true },
79
+ label: { type: String, required: false },
80
+ orientation: { type: String, required: false, default: "horizontal" },
81
+ size: { type: String, required: false, default: "md" },
82
+ linear: { type: Boolean, required: false },
83
+ readonly: { type: Boolean, required: false },
84
+ compact: { type: [Boolean, String], required: false, default: "auto" },
85
+ positionLabel: { type: String, required: false },
86
+ completeLabel: { type: String, required: false },
87
+ errorLabel: { type: String, required: false }
88
+ });
89
+ const model = defineModel({ type: [String, Number] });
90
+ const emit = defineEmits(["select"]);
91
+ const { locale, t } = useLocale();
92
+ const list = useTemplateRef("list");
93
+ const labelText = computed(() => props.label ?? t("stepper.label"));
94
+ const completeText = computed(() => props.completeLabel ?? t("stepper.complete"));
95
+ const errorText = computed(() => props.errorLabel ?? t("stepper.error"));
96
+ function positionText(index) {
97
+ return translateOr(locale.value, props.positionLabel, "stepper.position", {
98
+ step: index + 1,
99
+ count: props.steps.length
100
+ });
101
+ }
102
+ const currentIndex = computed(
103
+ () => Math.max(
104
+ 0,
105
+ props.steps.findIndex((step) => step.value === model.value)
106
+ )
107
+ );
108
+ const entries = computed(
109
+ () => props.steps.map((step, index) => {
110
+ const current = index === currentIndex.value;
111
+ const status = step.state === "error" ? "error" : step.state === "complete" ? "complete" : current ? "current" : "upcoming";
112
+ const reachable = !props.linear || index <= currentIndex.value || step.state === "complete";
113
+ const enabled = !props.readonly && !step.disabled && reachable;
114
+ return { step, current, status, enabled };
115
+ })
116
+ );
117
+ const currentEntry = computed(() => entries.value[currentIndex.value]);
118
+ const focusIndex = ref(currentIndex.value);
119
+ watch(currentIndex, (index) => {
120
+ focusIndex.value = index;
121
+ });
122
+ watch(
123
+ entries,
124
+ (list2) => {
125
+ if (list2[focusIndex.value]?.enabled) return;
126
+ const fallback = list2.findIndex((entry) => entry.enabled);
127
+ if (fallback !== -1) focusIndex.value = fallback;
128
+ },
129
+ { immediate: true }
130
+ );
131
+ function select(step) {
132
+ emit("select", step);
133
+ model.value = step.value;
134
+ }
135
+ function onKeydown(event, index) {
136
+ const target = rovingIndex(
137
+ event.key,
138
+ index,
139
+ entries.value.length,
140
+ props.orientation === "vertical" ? "vertical" : "horizontal",
141
+ (candidate) => !entries.value[candidate]?.enabled
142
+ );
143
+ if (target === null) return;
144
+ event.preventDefault();
145
+ focusIndex.value = target;
146
+ list.value?.querySelector(`[data-index="${target}"]`)?.focus();
147
+ }
148
+ </script>