@getdom/studio 0.1.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.
- package/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineOptions({
|
|
5
|
+
__doc: {
|
|
6
|
+
name: 'Splitter panel',
|
|
7
|
+
tag: '<DomSplitterPanel>',
|
|
8
|
+
description: 'A resizable multi-pane shell for editors, inspectors, file browsers, and app-builder workspaces.',
|
|
9
|
+
icon: 'M5 4v16M19 4v16M8 6h8M8 12h8M8 18h8',
|
|
10
|
+
slots: [
|
|
11
|
+
{ name: 'start', description: 'The leading resizable pane.' },
|
|
12
|
+
{ name: 'default', description: 'The flexible central pane.' },
|
|
13
|
+
{ name: 'end', description: 'Optional trailing resizable pane.' },
|
|
14
|
+
{ name: 'start-handle', description: 'Optional content inside the leading resize handle.' },
|
|
15
|
+
{ name: 'end-handle', description: 'Optional content inside the trailing resize handle.' },
|
|
16
|
+
],
|
|
17
|
+
events: [
|
|
18
|
+
{ name: 'update:startSize', payload: '(pixels)', description: 'Emitted when the leading pane is resized.' },
|
|
19
|
+
{ name: 'update:endSize', payload: '(pixels)', description: 'Emitted when the trailing pane is resized.' },
|
|
20
|
+
{ name: 'resize', payload: '({ startSize, endSize })', description: 'Emitted after either splitter handle moves.' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const props = defineProps({
|
|
26
|
+
orientation: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: 'horizontal',
|
|
29
|
+
validator: (value) => ['horizontal', 'vertical'].includes(value),
|
|
30
|
+
_edit: {
|
|
31
|
+
description: 'Resize direction. Horizontal creates columns; vertical creates rows.',
|
|
32
|
+
options: ['horizontal', 'vertical'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
startSize: {
|
|
36
|
+
type: Number,
|
|
37
|
+
default: 280,
|
|
38
|
+
_edit: { description: 'Initial width or height of the leading pane in pixels.' },
|
|
39
|
+
},
|
|
40
|
+
endSize: {
|
|
41
|
+
type: Number,
|
|
42
|
+
default: 360,
|
|
43
|
+
_edit: { description: 'Initial width or height of the trailing pane in pixels.' },
|
|
44
|
+
},
|
|
45
|
+
minStart: {
|
|
46
|
+
type: Number,
|
|
47
|
+
default: 180,
|
|
48
|
+
_edit: { description: 'Minimum leading pane width or height.' },
|
|
49
|
+
},
|
|
50
|
+
minMain: {
|
|
51
|
+
type: Number,
|
|
52
|
+
default: 320,
|
|
53
|
+
_edit: { description: 'Minimum central pane width or height.' },
|
|
54
|
+
},
|
|
55
|
+
minEnd: {
|
|
56
|
+
type: Number,
|
|
57
|
+
default: 220,
|
|
58
|
+
_edit: { description: 'Minimum trailing pane width or height.' },
|
|
59
|
+
},
|
|
60
|
+
handleSize: {
|
|
61
|
+
type: Number,
|
|
62
|
+
default: 8,
|
|
63
|
+
_edit: { description: 'Splitter handle hit area in pixels.' },
|
|
64
|
+
},
|
|
65
|
+
handleClass: {
|
|
66
|
+
type: [String, Array, Object],
|
|
67
|
+
default: 'transition hover:bg-ring/25 focus-visible:bg-ring/35 data-[dragging=true]:bg-ring/35',
|
|
68
|
+
_edit: { description: 'Classes applied to each resize button. Use this to make handles transparent, bordered, or prominent.' },
|
|
69
|
+
},
|
|
70
|
+
activeHandleClass: {
|
|
71
|
+
type: [String, Array, Object],
|
|
72
|
+
default: '',
|
|
73
|
+
_edit: { description: 'Additional classes applied to the active resize button while dragging.' },
|
|
74
|
+
},
|
|
75
|
+
handleIndicatorClass: {
|
|
76
|
+
type: [String, Array, Object],
|
|
77
|
+
default: '',
|
|
78
|
+
_edit: { description: 'Classes applied to the small visual indicator inside each resize button.' },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const emit = defineEmits(['update:startSize', 'update:endSize', 'resize']);
|
|
83
|
+
const slots = useSlots();
|
|
84
|
+
const root = ref(null);
|
|
85
|
+
const startPx = ref(props.startSize);
|
|
86
|
+
const endPx = ref(props.endSize);
|
|
87
|
+
const dragging = ref(null);
|
|
88
|
+
let resizeObserver = null;
|
|
89
|
+
|
|
90
|
+
const hasEnd = computed(() => Boolean(slots.end));
|
|
91
|
+
const isVertical = computed(() => props.orientation === 'vertical');
|
|
92
|
+
const resizeCursor = computed(() => isVertical.value ? 'row-resize' : 'col-resize');
|
|
93
|
+
const rootClass = computed(() => [
|
|
94
|
+
'grid min-h-0',
|
|
95
|
+
isVertical.value ? 'overflow-y-auto' : 'overflow-x-auto',
|
|
96
|
+
]);
|
|
97
|
+
const handleBaseClass = computed(() => [
|
|
98
|
+
'group flex outline-none',
|
|
99
|
+
isVertical.value
|
|
100
|
+
? 'min-w-0 cursor-row-resize items-center justify-center'
|
|
101
|
+
: 'min-h-0 cursor-col-resize items-stretch justify-center',
|
|
102
|
+
]);
|
|
103
|
+
const defaultIndicatorClass = computed(() => [
|
|
104
|
+
'rounded-full bg-muted-fg/35 opacity-0 transition group-hover:opacity-100 group-data-[dragging=true]:opacity-100 group-data-[dragging=true]:bg-canvas-fg/50',
|
|
105
|
+
isVertical.value ? 'mx-auto h-0.5 w-10' : 'my-auto h-10 w-0.5',
|
|
106
|
+
]);
|
|
107
|
+
const gridStyle = computed(() => {
|
|
108
|
+
const tracks = hasEnd.value
|
|
109
|
+
? `${startPx.value}px ${props.handleSize}px minmax(${props.minMain}px, 1fr) ${props.handleSize}px ${endPx.value}px`
|
|
110
|
+
: `${startPx.value}px ${props.handleSize}px minmax(${props.minMain}px, 1fr)`;
|
|
111
|
+
const minSize = hasEnd.value
|
|
112
|
+
? `${props.minStart + props.minMain + props.minEnd + props.handleSize * 2}px`
|
|
113
|
+
: `${props.minStart + props.minMain + props.handleSize}px`;
|
|
114
|
+
|
|
115
|
+
if (isVertical.value) return {
|
|
116
|
+
gridTemplateRows: tracks,
|
|
117
|
+
minHeight: minSize,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
gridTemplateColumns: tracks,
|
|
122
|
+
minWidth: minSize,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
watch(() => props.startSize, (value) => {
|
|
127
|
+
startPx.value = clamp(value, props.minStart, maxStartSize());
|
|
128
|
+
syncSizesToBounds();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
watch(() => props.endSize, (value) => {
|
|
132
|
+
endPx.value = clamp(value, props.minEnd, maxEndSize());
|
|
133
|
+
syncSizesToBounds();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
watch(() => props.orientation, () => {
|
|
137
|
+
nextTick(syncSizesToBounds);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
nextTick(syncSizesToBounds);
|
|
142
|
+
|
|
143
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
144
|
+
resizeObserver = new ResizeObserver(syncSizesToBounds);
|
|
145
|
+
if (root.value) resizeObserver.observe(root.value);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
onBeforeUnmount(() => {
|
|
149
|
+
resizeObserver?.disconnect();
|
|
150
|
+
endResize();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function beginResize(pane, event) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
dragging.value = {
|
|
156
|
+
pane,
|
|
157
|
+
position: pointerPosition(event),
|
|
158
|
+
start: startPx.value,
|
|
159
|
+
end: endPx.value,
|
|
160
|
+
};
|
|
161
|
+
window.addEventListener('pointermove', resizeFromPointer);
|
|
162
|
+
window.addEventListener('pointerup', endResize, { once: true });
|
|
163
|
+
document.body.style.cursor = resizeCursor.value;
|
|
164
|
+
document.body.style.userSelect = 'none';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resizeFromPointer(event) {
|
|
168
|
+
if (!dragging.value) return;
|
|
169
|
+
const delta = pointerPosition(event) - dragging.value.position;
|
|
170
|
+
|
|
171
|
+
if (dragging.value.pane === 'start') {
|
|
172
|
+
updateStartSize(clamp(dragging.value.start + delta, props.minStart, maxStartSize()));
|
|
173
|
+
} else {
|
|
174
|
+
updateEndSize(clamp(dragging.value.end - delta, props.minEnd, maxEndSize()));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function endResize() {
|
|
179
|
+
window.removeEventListener('pointermove', resizeFromPointer);
|
|
180
|
+
document.body.style.cursor = '';
|
|
181
|
+
document.body.style.userSelect = '';
|
|
182
|
+
dragging.value = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function updateStartSize(value) {
|
|
186
|
+
startPx.value = value;
|
|
187
|
+
emit('update:startSize', value);
|
|
188
|
+
emit('resize', { startSize: startPx.value, endSize: endPx.value });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function updateEndSize(value) {
|
|
192
|
+
endPx.value = value;
|
|
193
|
+
emit('update:endSize', value);
|
|
194
|
+
emit('resize', { startSize: startPx.value, endSize: endPx.value });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function syncSizesToBounds() {
|
|
198
|
+
if (!root.value) return;
|
|
199
|
+
|
|
200
|
+
const nextStart = clamp(startPx.value, props.minStart, maxStartSize());
|
|
201
|
+
startPx.value = nextStart;
|
|
202
|
+
|
|
203
|
+
if (!hasEnd.value) return;
|
|
204
|
+
endPx.value = clamp(endPx.value, props.minEnd, maxEndSize());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function maxStartSize() {
|
|
208
|
+
const width = panelWidth();
|
|
209
|
+
const reserved = props.minMain + (hasEnd.value ? endPx.value + props.handleSize * 2 : props.handleSize);
|
|
210
|
+
return Math.max(props.minStart, width - reserved);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function maxEndSize() {
|
|
214
|
+
const width = panelWidth();
|
|
215
|
+
const reserved = props.minMain + startPx.value + props.handleSize * 2;
|
|
216
|
+
return Math.max(props.minEnd, width - reserved);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function panelWidth() {
|
|
220
|
+
if (isVertical.value) return root.value?.clientHeight || globalThis.innerHeight || 800;
|
|
221
|
+
return root.value?.clientWidth || globalThis.innerWidth || 1200;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pointerPosition(event) {
|
|
225
|
+
return isVertical.value ? event.clientY : event.clientX;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isDragging(pane) {
|
|
229
|
+
return dragging.value?.pane === pane;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function handleLabel(pane) {
|
|
233
|
+
if (!isVertical.value) return pane === 'start' ? 'Resize leading panel' : 'Resize trailing panel';
|
|
234
|
+
return pane === 'start' ? 'Resize upper panel' : 'Resize lower panel';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function clamp(value, min, max) {
|
|
238
|
+
return Math.min(Math.max(value, min), max);
|
|
239
|
+
}
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<template>
|
|
243
|
+
<div ref="root" :class="rootClass" :style="gridStyle">
|
|
244
|
+
<section class="min-h-0 min-w-0 overflow-hidden">
|
|
245
|
+
<slot name="start" />
|
|
246
|
+
</section>
|
|
247
|
+
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
:class="[handleBaseClass, handleClass, isDragging('start') && activeHandleClass]"
|
|
251
|
+
:aria-label="handleLabel('start')"
|
|
252
|
+
:aria-orientation="isVertical ? 'horizontal' : 'vertical'"
|
|
253
|
+
:data-dragging="isDragging('start')"
|
|
254
|
+
@pointerdown="beginResize('start', $event)"
|
|
255
|
+
>
|
|
256
|
+
<slot name="start-handle" :active="isDragging('start')" :orientation="orientation">
|
|
257
|
+
<span :class="[defaultIndicatorClass, handleIndicatorClass]"></span>
|
|
258
|
+
</slot>
|
|
259
|
+
</button>
|
|
260
|
+
|
|
261
|
+
<section class="min-h-0 min-w-0 overflow-hidden">
|
|
262
|
+
<slot />
|
|
263
|
+
</section>
|
|
264
|
+
|
|
265
|
+
<button
|
|
266
|
+
v-if="hasEnd"
|
|
267
|
+
type="button"
|
|
268
|
+
:class="[handleBaseClass, handleClass, isDragging('end') && activeHandleClass]"
|
|
269
|
+
:aria-label="handleLabel('end')"
|
|
270
|
+
:aria-orientation="isVertical ? 'horizontal' : 'vertical'"
|
|
271
|
+
:data-dragging="isDragging('end')"
|
|
272
|
+
@pointerdown="beginResize('end', $event)"
|
|
273
|
+
>
|
|
274
|
+
<slot name="end-handle" :active="isDragging('end')" :orientation="orientation">
|
|
275
|
+
<span :class="[defaultIndicatorClass, handleIndicatorClass]"></span>
|
|
276
|
+
</slot>
|
|
277
|
+
</button>
|
|
278
|
+
|
|
279
|
+
<section v-if="hasEnd" class="min-h-0 min-w-0 overflow-hidden">
|
|
280
|
+
<slot name="end" />
|
|
281
|
+
</section>
|
|
282
|
+
</div>
|
|
283
|
+
</template>
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} StepperItem
|
|
4
|
+
* @property {string|number} key Stable controlled step key.
|
|
5
|
+
* @property {string} label Human-readable step label.
|
|
6
|
+
* @property {string} [description] Optional supporting copy.
|
|
7
|
+
* @property {'complete'|'current'|'upcoming'|'error'} [status] Explicit state override.
|
|
8
|
+
* @property {boolean} [optional] Mark the step as optional.
|
|
9
|
+
* @property {boolean} [disabled] Prevent navigation to this step.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} StepperChangePayload
|
|
14
|
+
* @property {StepperItem} item Selected step record.
|
|
15
|
+
* @property {string|number} key Selected step key.
|
|
16
|
+
* @property {number} index Selected step index.
|
|
17
|
+
* @property {string|number|null} previousKey Previously active step key.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} StepperSlotPayload
|
|
22
|
+
* @property {StepperItem} item Step record.
|
|
23
|
+
* @property {string|number} key Step key.
|
|
24
|
+
* @property {number} index Step index.
|
|
25
|
+
* @property {'complete'|'current'|'upcoming'|'error'} state Resolved display state.
|
|
26
|
+
* @property {boolean} active Whether the step matches the controlled active key.
|
|
27
|
+
* @property {boolean} disabled Whether the step cannot currently be selected.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const stepperProps = {
|
|
31
|
+
items: {
|
|
32
|
+
type: Array,
|
|
33
|
+
default: () => [],
|
|
34
|
+
_edit: { typeRef: 'Array<StepperItem>', description: 'Ordered application-owned step records.' },
|
|
35
|
+
},
|
|
36
|
+
modelValue: {
|
|
37
|
+
type: [String, Number],
|
|
38
|
+
default: '',
|
|
39
|
+
_edit: { description: 'Controlled active step key. Defaults to the first enabled item.' },
|
|
40
|
+
},
|
|
41
|
+
orientation: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: 'horizontal',
|
|
44
|
+
_edit: { options: ['horizontal', 'vertical'], description: 'Arrange steps across a row or down a column.' },
|
|
45
|
+
},
|
|
46
|
+
clickable: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: true,
|
|
49
|
+
_edit: { description: 'Allow enabled step indicators and labels to change the active key.' },
|
|
50
|
+
},
|
|
51
|
+
linear: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false,
|
|
54
|
+
_edit: { description: 'Prevent navigation beyond the next incomplete step.' },
|
|
55
|
+
},
|
|
56
|
+
showDescriptions: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: true,
|
|
59
|
+
_edit: { description: 'Show supporting descriptions when supplied.' },
|
|
60
|
+
},
|
|
61
|
+
size: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: 'md',
|
|
64
|
+
_edit: { options: ['sm', 'md'], description: 'Indicator and label density.' },
|
|
65
|
+
},
|
|
66
|
+
ariaLabel: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'Progress',
|
|
69
|
+
_edit: { description: 'Accessible label for the step navigation.' },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<script setup>
|
|
75
|
+
import { computed, ref } from 'vue';
|
|
76
|
+
|
|
77
|
+
defineOptions({
|
|
78
|
+
__doc: {
|
|
79
|
+
name: 'Stepper',
|
|
80
|
+
tag: '<DomStepper>',
|
|
81
|
+
description: 'Controlled horizontal or vertical progress navigation for multi-step application workflows.',
|
|
82
|
+
icon: 'M5 12h14M6 12a2 2 0 1 0 0 .01M12 12a2 2 0 1 0 0 .01M18 12a2 2 0 1 0 0 .01',
|
|
83
|
+
events: [
|
|
84
|
+
{ name: 'update:modelValue', payload: 'string | number', description: 'Fired when an enabled step is selected.' },
|
|
85
|
+
{ name: 'change', payload: 'StepperChangePayload', typeRef: 'StepperChangePayload', description: 'Fired with the selected step, key, index, and previous key.' },
|
|
86
|
+
],
|
|
87
|
+
slots: [
|
|
88
|
+
{ name: 'step', payload: 'StepperSlotPayload', typeRef: 'StepperSlotPayload', description: 'Replace the label and description while retaining navigation and progress structure.' },
|
|
89
|
+
{ name: 'indicator', payload: 'StepperSlotPayload', typeRef: 'StepperSlotPayload', description: 'Replace the numbered, completed, or error indicator.' },
|
|
90
|
+
{ name: 'separator', payload: '{ index, previous, next }', description: 'Replace the connector between adjacent steps.' },
|
|
91
|
+
],
|
|
92
|
+
keyboard: [
|
|
93
|
+
{ keys: '← / →', action: 'Move between enabled horizontal steps.' },
|
|
94
|
+
{ keys: '↑ / ↓', action: 'Move between enabled vertical steps.' },
|
|
95
|
+
{ keys: 'Home / End', action: 'Move to the first or last enabled step.' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const props = defineProps(stepperProps);
|
|
101
|
+
const emit = defineEmits(['update:modelValue', 'change']);
|
|
102
|
+
|
|
103
|
+
const root = ref(null);
|
|
104
|
+
const normalizedItems = computed(getNormalizedItems);
|
|
105
|
+
const activeIndex = computed(getActiveIndex);
|
|
106
|
+
const rootClass = computed(getRootClass);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalizes every supplied step and its stable key.
|
|
110
|
+
*
|
|
111
|
+
* @returns {Array<StepperItem>} Safe step records.
|
|
112
|
+
*/
|
|
113
|
+
function getNormalizedItems() {
|
|
114
|
+
return (Array.isArray(props.items) ? props.items : [])
|
|
115
|
+
.filter(hasStepKey)
|
|
116
|
+
.map(normalizeStep);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reports whether a candidate step has a stable key.
|
|
121
|
+
*
|
|
122
|
+
* @param {StepperItem} item Candidate step.
|
|
123
|
+
* @returns {boolean} True when the step can be controlled.
|
|
124
|
+
*/
|
|
125
|
+
function hasStepKey(item) {
|
|
126
|
+
return Boolean(item && item.key !== null && item.key !== undefined && item.key !== '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Adds safe defaults to one step record.
|
|
131
|
+
*
|
|
132
|
+
* @param {StepperItem} item Source step.
|
|
133
|
+
* @param {number} index Source step index.
|
|
134
|
+
* @returns {StepperItem} Normalized step.
|
|
135
|
+
*/
|
|
136
|
+
function normalizeStep(item, index) {
|
|
137
|
+
return {
|
|
138
|
+
...item,
|
|
139
|
+
label: item.label || `Step ${index + 1}`,
|
|
140
|
+
description: item.description || '',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Finds the controlled active step or the first enabled step.
|
|
146
|
+
*
|
|
147
|
+
* @returns {number} Active step index or -1.
|
|
148
|
+
*/
|
|
149
|
+
function getActiveIndex() {
|
|
150
|
+
const controlledIndex = normalizedItems.value.findIndex(matchesModelValue);
|
|
151
|
+
if (controlledIndex >= 0) return controlledIndex;
|
|
152
|
+
return normalizedItems.value.findIndex(isEnabledStep);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reports whether a step matches the controlled active key.
|
|
157
|
+
*
|
|
158
|
+
* @param {StepperItem} item Candidate step.
|
|
159
|
+
* @returns {boolean} True when keys match.
|
|
160
|
+
*/
|
|
161
|
+
function matchesModelValue(item) {
|
|
162
|
+
return String(item.key) === String(props.modelValue);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reports whether a step is enabled.
|
|
167
|
+
*
|
|
168
|
+
* @param {StepperItem} item Candidate step.
|
|
169
|
+
* @returns {boolean} True when navigation is allowed.
|
|
170
|
+
*/
|
|
171
|
+
function isEnabledStep(item) {
|
|
172
|
+
return !item.disabled;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Builds orientation-aware root classes.
|
|
177
|
+
*
|
|
178
|
+
* @returns {string} Step list classes.
|
|
179
|
+
*/
|
|
180
|
+
function getRootClass() {
|
|
181
|
+
return props.orientation === 'vertical'
|
|
182
|
+
? 'flex flex-col'
|
|
183
|
+
: 'flex min-w-max items-start';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolves derived or explicitly supplied state for one step.
|
|
188
|
+
*
|
|
189
|
+
* @param {StepperItem} item Step record.
|
|
190
|
+
* @param {number} index Step index.
|
|
191
|
+
* @returns {'complete'|'current'|'upcoming'|'error'} Display state.
|
|
192
|
+
*/
|
|
193
|
+
function stepState(item, index) {
|
|
194
|
+
if (['complete', 'current', 'upcoming', 'error'].includes(item.status)) return item.status;
|
|
195
|
+
if (index < activeIndex.value) return 'complete';
|
|
196
|
+
if (index === activeIndex.value) return 'current';
|
|
197
|
+
return 'upcoming';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Reports whether a step can be selected under current navigation rules.
|
|
202
|
+
*
|
|
203
|
+
* @param {StepperItem} item Step record.
|
|
204
|
+
* @param {number} index Step index.
|
|
205
|
+
* @returns {boolean} True when the step is interactive.
|
|
206
|
+
*/
|
|
207
|
+
function canSelect(item, index) {
|
|
208
|
+
if (!props.clickable || item.disabled) return false;
|
|
209
|
+
if (!props.linear) return true;
|
|
210
|
+
return index <= activeIndex.value + 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Selects an enabled step and emits controlled state.
|
|
215
|
+
*
|
|
216
|
+
* @param {StepperItem} item Step record.
|
|
217
|
+
* @param {number} index Step index.
|
|
218
|
+
* @returns {void}
|
|
219
|
+
*/
|
|
220
|
+
function selectStep(item, index) {
|
|
221
|
+
if (!canSelect(item, index)) return;
|
|
222
|
+
const previousKey = normalizedItems.value[activeIndex.value]?.key ?? null;
|
|
223
|
+
emit('update:modelValue', item.key);
|
|
224
|
+
emit('change', { item, key: item.key, index, previousKey });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handles directional and boundary keyboard navigation.
|
|
229
|
+
*
|
|
230
|
+
* @param {KeyboardEvent} event Native keyboard event.
|
|
231
|
+
* @param {number} index Focused step index.
|
|
232
|
+
* @returns {void}
|
|
233
|
+
*/
|
|
234
|
+
function onStepKeydown(event, index) {
|
|
235
|
+
const direction = keyboardDirection(event.key);
|
|
236
|
+
if (direction === null) return;
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
const nextIndex = direction === 'first'
|
|
239
|
+
? findEnabledIndex(0, 1)
|
|
240
|
+
: direction === 'last'
|
|
241
|
+
? findEnabledIndex(normalizedItems.value.length - 1, -1)
|
|
242
|
+
: findEnabledIndex(index + direction, direction);
|
|
243
|
+
if (nextIndex < 0) return;
|
|
244
|
+
focusStep(nextIndex);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Maps supported keys to a navigation direction.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} key Keyboard key.
|
|
251
|
+
* @returns {number|'first'|'last'|null} Navigation direction.
|
|
252
|
+
*/
|
|
253
|
+
function keyboardDirection(key) {
|
|
254
|
+
if (key === 'Home') return 'first';
|
|
255
|
+
if (key === 'End') return 'last';
|
|
256
|
+
if (props.orientation === 'vertical' && key === 'ArrowUp') return -1;
|
|
257
|
+
if (props.orientation === 'vertical' && key === 'ArrowDown') return 1;
|
|
258
|
+
if (props.orientation === 'horizontal' && key === 'ArrowLeft') return -1;
|
|
259
|
+
if (props.orientation === 'horizontal' && key === 'ArrowRight') return 1;
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Finds the next enabled and selectable step in one direction.
|
|
265
|
+
*
|
|
266
|
+
* @param {number} startIndex Initial candidate index.
|
|
267
|
+
* @param {number} direction Direction increment.
|
|
268
|
+
* @returns {number} Matching index or -1.
|
|
269
|
+
*/
|
|
270
|
+
function findEnabledIndex(startIndex, direction) {
|
|
271
|
+
for (let index = startIndex; index >= 0 && index < normalizedItems.value.length; index += direction) {
|
|
272
|
+
if (canSelect(normalizedItems.value[index], index)) return index;
|
|
273
|
+
}
|
|
274
|
+
return -1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Focuses and activates one step button.
|
|
279
|
+
*
|
|
280
|
+
* @param {number} index Target step index.
|
|
281
|
+
* @returns {void}
|
|
282
|
+
*/
|
|
283
|
+
function focusStep(index) {
|
|
284
|
+
const button = root.value?.querySelector(`[data-step-index="${index}"]`);
|
|
285
|
+
button?.focus();
|
|
286
|
+
selectStep(normalizedItems.value[index], index);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Builds the public slot payload for one step.
|
|
291
|
+
*
|
|
292
|
+
* @param {StepperItem} item Step record.
|
|
293
|
+
* @param {number} index Step index.
|
|
294
|
+
* @returns {Record<string, unknown>} Step slot payload.
|
|
295
|
+
*/
|
|
296
|
+
function slotPayload(item, index) {
|
|
297
|
+
return {
|
|
298
|
+
item,
|
|
299
|
+
key: item.key,
|
|
300
|
+
index,
|
|
301
|
+
state: stepState(item, index),
|
|
302
|
+
active: index === activeIndex.value,
|
|
303
|
+
disabled: !canSelect(item, index),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Builds semantic classes for a step indicator.
|
|
309
|
+
*
|
|
310
|
+
* @param {StepperItem} item Step record.
|
|
311
|
+
* @param {number} index Step index.
|
|
312
|
+
* @returns {string} Indicator classes.
|
|
313
|
+
*/
|
|
314
|
+
function indicatorClass(item, index) {
|
|
315
|
+
const state = stepState(item, index);
|
|
316
|
+
if (state === 'complete') return 'border-primary bg-primary text-primary-fg';
|
|
317
|
+
if (state === 'current') return 'border-primary bg-canvas text-primary ring-4 ring-primary/10';
|
|
318
|
+
if (state === 'error') return 'border-destructive bg-destructive/10 text-destructive';
|
|
319
|
+
return 'border-border bg-secondary text-muted-fg';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Builds semantic classes for a connector after one step.
|
|
324
|
+
*
|
|
325
|
+
* @param {number} index Step index before the connector.
|
|
326
|
+
* @returns {string} Connector classes.
|
|
327
|
+
*/
|
|
328
|
+
function separatorClass(index) {
|
|
329
|
+
const complete = index < activeIndex.value;
|
|
330
|
+
if (props.orientation === 'vertical') {
|
|
331
|
+
return `ml-[0.9375rem] h-8 w-px ${complete ? 'bg-primary' : 'bg-border'}`;
|
|
332
|
+
}
|
|
333
|
+
return `mt-[0.9375rem] h-px min-w-6 flex-1 ${complete ? 'bg-primary' : 'bg-border'}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns the compact indicator dimension for the selected size.
|
|
338
|
+
*
|
|
339
|
+
* @returns {string} Indicator size classes.
|
|
340
|
+
*/
|
|
341
|
+
function indicatorSizeClass() {
|
|
342
|
+
return props.size === 'sm' ? 'size-7 text-xs' : 'size-8 text-sm';
|
|
343
|
+
}
|
|
344
|
+
</script>
|
|
345
|
+
|
|
346
|
+
<template>
|
|
347
|
+
<nav :aria-label="ariaLabel" class="min-w-0 overflow-x-auto">
|
|
348
|
+
<ol ref="root" :class="rootClass">
|
|
349
|
+
<template v-for="(item, index) in normalizedItems" :key="item.key">
|
|
350
|
+
<li class="flex min-w-0" :class="orientation === 'vertical' ? 'flex-col' : 'min-w-[8rem] flex-1 flex-row'">
|
|
351
|
+
<div class="flex min-w-0" :class="orientation === 'vertical' ? 'items-start gap-3' : 'flex-col items-center px-1 text-center'">
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
class="group flex min-w-0 focus-visible:outline-none"
|
|
355
|
+
:class="orientation === 'vertical' ? 'items-start gap-3 text-left' : 'flex-col items-center text-center'"
|
|
356
|
+
:data-step-index="index"
|
|
357
|
+
:aria-current="index === activeIndex ? 'step' : undefined"
|
|
358
|
+
:aria-disabled="!canSelect(item, index) ? 'true' : undefined"
|
|
359
|
+
:disabled="!canSelect(item, index)"
|
|
360
|
+
@click="selectStep(item, index)"
|
|
361
|
+
@keydown="onStepKeydown($event, index)"
|
|
362
|
+
>
|
|
363
|
+
<slot name="indicator" v-bind="slotPayload(item, index)">
|
|
364
|
+
<span class="grid shrink-0 place-items-center rounded-full border font-semibold transition group-focus-visible:ring-2 group-focus-visible:ring-ring" :class="[indicatorSizeClass(), indicatorClass(item, index)]">
|
|
365
|
+
<svg v-if="stepState(item, index) === 'complete'" viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true"><path d="m6.5 12.5 3.2 3.2 7.8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
366
|
+
<span v-else-if="stepState(item, index) === 'error'" aria-hidden="true">!</span>
|
|
367
|
+
<span v-else>{{ index + 1 }}</span>
|
|
368
|
+
</span>
|
|
369
|
+
</slot>
|
|
370
|
+
<slot name="step" v-bind="slotPayload(item, index)">
|
|
371
|
+
<span class="min-w-0" :class="orientation === 'vertical' ? 'pt-0.5' : 'mt-2'">
|
|
372
|
+
<span class="block text-sm font-semibold" :class="index === activeIndex ? 'text-canvas-fg' : 'text-muted-fg'">{{ item.label }}</span>
|
|
373
|
+
<span v-if="item.optional" class="mt-0.5 block text-[11px] text-muted-fg">Optional</span>
|
|
374
|
+
<span v-if="showDescriptions && item.description" class="mt-1 block text-xs leading-5 text-muted-fg">{{ item.description }}</span>
|
|
375
|
+
</span>
|
|
376
|
+
</slot>
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
<slot v-if="index < normalizedItems.length - 1" name="separator" :index="index" :previous="item" :next="normalizedItems[index + 1]">
|
|
380
|
+
<span aria-hidden="true" :class="separatorClass(index)"></span>
|
|
381
|
+
</slot>
|
|
382
|
+
</li>
|
|
383
|
+
</template>
|
|
384
|
+
</ol>
|
|
385
|
+
</nav>
|
|
386
|
+
</template>
|