@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.
Files changed (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,711 @@
1
+ <script setup>
2
+ import { computed, defineComponent, Fragment, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowReactive, useSlots, watch } from 'vue';
3
+ import DomAppListItem from '../list-item/DomAppListItem.vue';
4
+
5
+ const StackScreenRenderer = defineComponent({
6
+ name: 'DomAppStackScreenRenderer',
7
+ props: {
8
+ screen: { type: Object, default: null },
9
+ component: { type: [Object, Function, String], default: null },
10
+ helpers: { type: Object, required: true },
11
+ childScreens: { type: Array, default: () => [] },
12
+ loading: { type: Boolean, default: false },
13
+ error: { type: null, default: null },
14
+ loadingLabel: { type: String, default: 'Loading settings' },
15
+ emptyTitle: { type: String, default: 'Select a setting' },
16
+ emptyDescription: { type: String, default: 'Choose an item to continue.' },
17
+ },
18
+ setup(props) {
19
+ return () => {
20
+ if (props.loading) return renderLoading(props.screen, props.loadingLabel);
21
+ if (props.error) return renderError(props.error);
22
+ if (!props.screen) return renderEmpty(props.emptyTitle, props.emptyDescription);
23
+ if (props.screen.slot) return props.screen.slot({ ...props.helpers, screen: props.screen });
24
+ if (props.component) {
25
+ return h(props.component, {
26
+ ...(props.screen.props || {}),
27
+ ...props.helpers,
28
+ stack: props.helpers,
29
+ screen: props.screen,
30
+ });
31
+ }
32
+ if (props.childScreens.length) return renderGeneratedMenu(props.screen, props.childScreens, props.helpers);
33
+ return renderEmpty(props.screen.title || props.emptyTitle, props.screen.description || props.emptyDescription);
34
+ };
35
+ },
36
+ });
37
+
38
+ function renderGeneratedMenu(screen, childScreens, helpers) {
39
+ return h('div', { class: 'overflow-hidden rounded-[1.5rem] border border-border bg-canvas' }, childScreens.map((child) => {
40
+ const slots = child.icon ? {
41
+ icon: () => h('span', {
42
+ class: 'grid size-5 place-items-center',
43
+ innerHTML: child.icon,
44
+ }),
45
+ } : undefined;
46
+
47
+ return h(DomAppListItem, {
48
+ key: child.id,
49
+ label: child.title || child.label || child.id,
50
+ description: child.description || '',
51
+ meta: child.meta || '',
52
+ chevron: true,
53
+ disabled: child.disabled || undefined,
54
+ class: child.disabled ? 'opacity-50' : '',
55
+ onClick: () => {
56
+ if (!child.disabled) helpers.openChild(screen.id, child.id);
57
+ },
58
+ }, slots);
59
+ }));
60
+ }
61
+
62
+ function renderLoading(screen, fallbackLabel) {
63
+ return h('div', { class: 'grid min-h-64 place-items-center p-6 text-center' }, [
64
+ h('div', { class: 'space-y-3' }, [
65
+ h('span', {
66
+ class: 'mx-auto block size-6 animate-spin rounded-full border-2 border-border border-t-primary',
67
+ 'aria-hidden': 'true',
68
+ }),
69
+ h('p', { class: 'text-sm font-medium text-canvas-fg' }, screen?.loadingTitle || fallbackLabel),
70
+ ]),
71
+ ]);
72
+ }
73
+
74
+ function renderError(error) {
75
+ return h('div', { class: 'grid min-h-64 place-items-center p-6 text-center' }, [
76
+ h('div', { class: 'max-w-sm space-y-2 rounded-2xl border border-destructive/30 bg-destructive/10 p-5' }, [
77
+ h('p', { class: 'text-sm font-semibold text-destructive' }, 'Unable to load this setting'),
78
+ h('p', { class: 'text-xs leading-5 text-muted-fg' }, error?.message || 'The screen loader rejected.'),
79
+ ]),
80
+ ]);
81
+ }
82
+
83
+ function renderEmpty(title, description) {
84
+ return h('div', { class: 'grid min-h-64 place-items-center p-6 text-center' }, [
85
+ h('div', { class: 'max-w-sm' }, [
86
+ h('p', { class: 'text-sm font-semibold text-canvas-fg' }, title),
87
+ description ? h('p', { class: 'mt-1 text-xs leading-5 text-muted-fg' }, description) : null,
88
+ ]),
89
+ ]);
90
+ }
91
+
92
+ defineOptions({
93
+ __doc: {
94
+ name: 'App stack',
95
+ tag: '<DomAppStack>',
96
+ description: 'A settings-style screen stack that handles slide navigation, breadcrumbs, lazy screens, and responsive panel styling.',
97
+ slots: [
98
+ { name: '(default)', description: 'Optional DomAppStackScreen children used to register slot-driven screens.' },
99
+ { name: 'breadcrumb', payload: '{ crumbs, canGoBack, back, goTo, push, path }', description: 'Replace the breadcrumb and back control while keeping the stack state.' },
100
+ ],
101
+ events: [
102
+ { name: 'update:path', payload: '(path: Array<string>)', description: 'Fired when the active stack path changes.' },
103
+ { name: 'navigate', payload: '(payload: { path, previousPath, direction, screen, reason })', description: 'Fired after push, back, replace, reset, or breadcrumb navigation.' },
104
+ { name: 'load', payload: '(payload: { screen, component })', description: 'Fired when a lazy screen component resolves.' },
105
+ { name: 'error', payload: '(payload: { screen, error })', description: 'Fired when a lazy screen component rejects.' },
106
+ ],
107
+ playground: {
108
+ mobileSlot: 'content',
109
+ initial: {
110
+ root: 'settings',
111
+ screens: [
112
+ { id: 'settings', title: 'Settings', children: ['account', 'notifications'] },
113
+ { id: 'account', title: 'Account', description: 'Profile, password, and sign-in settings.' },
114
+ { id: 'notifications', title: 'Notifications', description: 'Push, email, and quiet hours.' },
115
+ ],
116
+ },
117
+ },
118
+ },
119
+ });
120
+
121
+ const props = defineProps({
122
+ path: {
123
+ type: Array,
124
+ default: null,
125
+ _edit: {
126
+ component: 'DomJsonListInput',
127
+ description: 'Controlled stack path. Use v-model:path for two-way navigation.',
128
+ },
129
+ },
130
+ root: {
131
+ type: String,
132
+ default: '',
133
+ _edit: { description: 'Root screen id. Defaults to the first registered screen.' },
134
+ },
135
+ screens: {
136
+ type: Array,
137
+ default: () => [],
138
+ _edit: {
139
+ component: 'DomJsonListInput',
140
+ description: 'Screen records: { id, title, description, children, component, props }.',
141
+ },
142
+ },
143
+ layout: {
144
+ type: String,
145
+ default: 'auto',
146
+ validator: (value) => ['auto', 'mobile', 'desktop'].includes(value),
147
+ _edit: {
148
+ options: ['auto', 'mobile', 'desktop'],
149
+ description: 'Resolve mobile or desktop styling automatically while keeping one full-width sliding stack.',
150
+ },
151
+ },
152
+ desktopBreakpoint: {
153
+ type: String,
154
+ default: '768px',
155
+ _edit: { description: 'Breakpoint used when layout is auto. Accepts a CSS length or media query.' },
156
+ },
157
+ showBreadcrumbs: {
158
+ type: Boolean,
159
+ default: true,
160
+ _edit: { description: 'Render the default breadcrumb bar, or provide the breadcrumb slot.' },
161
+ },
162
+ animate: {
163
+ type: Boolean,
164
+ default: true,
165
+ _edit: { description: 'Animate push and back transitions.' },
166
+ },
167
+ focusOnNavigate: {
168
+ type: Boolean,
169
+ default: true,
170
+ _edit: { description: 'Move focus to the active panel after navigation.' },
171
+ },
172
+ loadingLabel: {
173
+ type: String,
174
+ default: 'Loading settings',
175
+ _edit: { description: 'Fallback label shown while lazy screens load.' },
176
+ },
177
+ emptyTitle: {
178
+ type: String,
179
+ default: 'Select a setting',
180
+ _edit: { description: 'Fallback title when no screen is active.' },
181
+ },
182
+ emptyDescription: {
183
+ type: String,
184
+ default: 'Choose an item to continue.',
185
+ _edit: { description: 'Fallback description when no screen is active.' },
186
+ },
187
+ breadcrumbClass: {
188
+ type: String,
189
+ default: '',
190
+ _edit: { group: 'Styling', description: 'Classes merged onto the default breadcrumb bar.' },
191
+ },
192
+ breadcrumbItemClass: {
193
+ type: String,
194
+ default: '',
195
+ _edit: { group: 'Styling', description: 'Classes merged onto clickable breadcrumb items.' },
196
+ },
197
+ breadcrumbCurrentClass: {
198
+ type: String,
199
+ default: '',
200
+ _edit: { group: 'Styling', description: 'Classes merged onto the current breadcrumb item.' },
201
+ },
202
+ backButtonClass: {
203
+ type: String,
204
+ default: '',
205
+ _edit: { group: 'Styling', description: 'Classes merged onto the default back button.' },
206
+ },
207
+ mobilePanelClass: {
208
+ type: String,
209
+ default: '',
210
+ _edit: { group: 'Styling', description: 'Classes merged onto the sliding panel when layout resolves to mobile.' },
211
+ },
212
+ desktopContentClass: {
213
+ type: String,
214
+ default: '',
215
+ _edit: { group: 'Styling', description: 'Classes merged onto the sliding panel when layout resolves to desktop.' },
216
+ },
217
+ });
218
+
219
+ const emit = defineEmits(['update:path', 'navigate', 'load', 'error']);
220
+
221
+ const slots = useSlots();
222
+ const loadedComponents = shallowReactive(new Map());
223
+ const loadErrors = shallowReactive(new Map());
224
+ const loadingScreens = reactive(new Set());
225
+ const internalPath = ref([]);
226
+ const direction = ref('forward');
227
+ const isDesktopMedia = ref(false);
228
+ const screenPanel = ref(null);
229
+
230
+ let removeDesktopMediaListener = null;
231
+
232
+ const propScreens = computed(() => props.screens.map(normalizeScreen).filter(Boolean));
233
+ const slottedScreens = computed(() => extractSlotScreens(slots.default?.() || []));
234
+ const allScreens = computed(() => {
235
+ const records = [...propScreens.value];
236
+
237
+ for (const screen of slottedScreens.value) {
238
+ const index = records.findIndex((record) => record.id === screen.id);
239
+ if (index >= 0) records[index] = { ...records[index], ...screen };
240
+ else records.push(screen);
241
+ }
242
+
243
+ return records;
244
+ });
245
+ const screenById = computed(() => new Map(allScreens.value.map((screen) => [screen.id, screen])));
246
+ const parentById = computed(() => {
247
+ const parents = new Map();
248
+
249
+ for (const screen of allScreens.value) {
250
+ if (screen.parent) parents.set(screen.id, screen.parent);
251
+ }
252
+
253
+ for (const screen of allScreens.value) {
254
+ for (const childId of normalizeChildren(screen.children)) {
255
+ if (!parents.has(childId)) parents.set(childId, screen.id);
256
+ }
257
+ }
258
+
259
+ return parents;
260
+ });
261
+ const rootId = computed(() => props.root || allScreens.value[0]?.id || '');
262
+ const rawPath = computed(() => props.path ?? internalPath.value);
263
+ const resolvedPath = computed(() => normalizePath(rawPath.value));
264
+ const activeId = computed(() => resolvedPath.value.at(-1) || rootId.value);
265
+ const rootScreen = computed(() => screenById.value.get(rootId.value) || null);
266
+ const activeScreen = computed(() => screenById.value.get(activeId.value) || null);
267
+ const crumbs = computed(() => resolvedPath.value.map((id, index) => {
268
+ const screen = screenById.value.get(id);
269
+ return {
270
+ id,
271
+ index,
272
+ title: screen?.title || screen?.label || id,
273
+ screen,
274
+ path: resolvedPath.value.slice(0, index + 1),
275
+ };
276
+ }));
277
+ const canGoBack = computed(() => resolvedPath.value.length > 1);
278
+ const isDesktopLayout = computed(() => props.layout === 'desktop' || (props.layout === 'auto' && isDesktopMedia.value));
279
+ const transitionName = computed(() => (props.animate ? `dom-app-stack-${direction.value}` : ''));
280
+ const activeComponent = computed(() => componentForScreen(activeScreen.value));
281
+ const helpers = computed(() => ({
282
+ activeId: activeId.value,
283
+ back,
284
+ canGoBack: canGoBack.value,
285
+ crumbs: crumbs.value,
286
+ direction: direction.value,
287
+ goTo,
288
+ layout: isDesktopLayout.value ? 'desktop' : 'mobile',
289
+ openChild,
290
+ path: resolvedPath.value,
291
+ push,
292
+ replace,
293
+ reset,
294
+ }));
295
+ const breadcrumbSlotProps = computed(() => ({
296
+ ...helpers.value,
297
+ activeScreen: activeScreen.value,
298
+ rootScreen: rootScreen.value,
299
+ screens: allScreens.value,
300
+ }));
301
+
302
+ watch(rootId, (id) => {
303
+ if (id && props.path === null && !internalPath.value.length) internalPath.value = [id];
304
+ }, { immediate: true });
305
+
306
+ watch(activeScreen, (screen) => {
307
+ ensureLoaded(screen);
308
+ }, { immediate: true });
309
+
310
+ watch(() => props.path, (next, previous) => {
311
+ if (next === null) return;
312
+ direction.value = compareDirection(normalizePath(next), normalizePath(previous));
313
+ }, { deep: true });
314
+
315
+ onMounted(() => {
316
+ configureDesktopMedia();
317
+ });
318
+
319
+ watch(() => props.desktopBreakpoint, () => {
320
+ configureDesktopMedia();
321
+ });
322
+
323
+ onBeforeUnmount(() => {
324
+ removeDesktopMediaListener?.();
325
+ });
326
+
327
+ function normalizeScreen(screen) {
328
+ if (!screen || !screen.id) return null;
329
+ return {
330
+ ...screen,
331
+ id: String(screen.id),
332
+ title: screen.title || screen.label || String(screen.id),
333
+ children: normalizeChildren(screen.children),
334
+ };
335
+ }
336
+
337
+ function extractSlotScreens(vnodes) {
338
+ const screens = [];
339
+
340
+ for (const vnode of vnodes) {
341
+ if (!vnode) continue;
342
+ if (isStackScreenVNode(vnode)) {
343
+ const screen = normalizeScreen({
344
+ ...(vnode.props || {}),
345
+ disabled: vnode.props?.disabled === '' ? true : vnode.props?.disabled,
346
+ slot: vnode.children?.default,
347
+ });
348
+ if (screen) screens.push(screen);
349
+ continue;
350
+ }
351
+
352
+ if (vnode.type === Fragment && Array.isArray(vnode.children)) {
353
+ screens.push(...extractSlotScreens(vnode.children));
354
+ }
355
+ }
356
+
357
+ return screens;
358
+ }
359
+
360
+ function isStackScreenVNode(vnode) {
361
+ const type = vnode?.type;
362
+ return type?.name === 'DomAppStackScreen' || type?.__name === 'AppStackScreen';
363
+ }
364
+
365
+ function normalizeChildren(children) {
366
+ if (!Array.isArray(children)) return [];
367
+ return children
368
+ .map((child) => {
369
+ if (typeof child === 'string' || typeof child === 'number') return String(child);
370
+ return child?.id ? String(child.id) : '';
371
+ })
372
+ .filter(Boolean);
373
+ }
374
+
375
+ function normalizePath(path) {
376
+ const next = Array.isArray(path) ? path.map((item) => String(item)).filter(Boolean) : [];
377
+ if (!next.length && rootId.value) return [rootId.value];
378
+ return next;
379
+ }
380
+
381
+ function samePath(a, b) {
382
+ if (a.length !== b.length) return false;
383
+ return a.every((item, index) => item === b[index]);
384
+ }
385
+
386
+ function compareDirection(next, previous = []) {
387
+ if (next.length > previous.length && previous.every((id, index) => next[index] === id)) return 'forward';
388
+ if (next.length < previous.length && next.every((id, index) => previous[index] === id)) return 'back';
389
+ return next.length >= previous.length ? 'forward' : 'back';
390
+ }
391
+
392
+ function pathToScreen(id) {
393
+ const target = String(id || '');
394
+ if (!target) return null;
395
+ const root = rootId.value;
396
+ const path = [target];
397
+ const seen = new Set(path);
398
+ let current = target;
399
+
400
+ while (current && current !== root) {
401
+ const parent = parentById.value.get(current);
402
+ if (!parent || seen.has(parent)) return null;
403
+ path.unshift(parent);
404
+ seen.add(parent);
405
+ current = parent;
406
+ }
407
+
408
+ if (root && path[0] !== root) return null;
409
+ return path;
410
+ }
411
+
412
+ function commitPath(path, reason = 'navigate') {
413
+ const previousPath = resolvedPath.value;
414
+ const nextPath = normalizePath(path);
415
+ if (samePath(nextPath, previousPath)) return;
416
+
417
+ direction.value = compareDirection(nextPath, previousPath);
418
+ if (props.path === null) internalPath.value = nextPath;
419
+ emit('update:path', nextPath);
420
+ emit('navigate', {
421
+ direction: direction.value,
422
+ path: nextPath,
423
+ previousPath,
424
+ reason,
425
+ screen: screenById.value.get(nextPath.at(-1)) || null,
426
+ });
427
+
428
+ nextTick(focusActivePanel);
429
+ }
430
+
431
+ function push(id) {
432
+ const target = String(id || '');
433
+ if (!target) return;
434
+ const existingIndex = resolvedPath.value.indexOf(target);
435
+ if (existingIndex >= 0) {
436
+ commitPath(resolvedPath.value.slice(0, existingIndex + 1), 'push');
437
+ return;
438
+ }
439
+ commitPath(pathToScreen(target) || [...resolvedPath.value, target], 'push');
440
+ }
441
+
442
+ function replace(id) {
443
+ const target = String(id || '');
444
+ if (!target) return;
445
+ commitPath(pathToScreen(target) || [...resolvedPath.value.slice(0, -1), target], 'replace');
446
+ }
447
+
448
+ function back(steps = 1) {
449
+ const count = Math.max(1, Number(steps) || 1);
450
+ if (resolvedPath.value.length <= 1) return;
451
+ commitPath(resolvedPath.value.slice(0, Math.max(1, resolvedPath.value.length - count)), 'back');
452
+ }
453
+
454
+ function goTo(target) {
455
+ if (typeof target === 'number') {
456
+ const index = Math.max(0, Math.min(target, resolvedPath.value.length - 1));
457
+ commitPath(resolvedPath.value.slice(0, index + 1), 'breadcrumb');
458
+ return;
459
+ }
460
+
461
+ const id = String(target || '');
462
+ if (!id) return;
463
+ const existingIndex = resolvedPath.value.indexOf(id);
464
+ if (existingIndex >= 0) {
465
+ commitPath(resolvedPath.value.slice(0, existingIndex + 1), 'breadcrumb');
466
+ return;
467
+ }
468
+ commitPath(pathToScreen(id) || [...resolvedPath.value, id], 'breadcrumb');
469
+ }
470
+
471
+ function reset(id = rootId.value) {
472
+ const target = String(id || '');
473
+ commitPath(pathToScreen(target) || (target ? [target] : []), 'reset');
474
+ }
475
+
476
+ function openChild(parentId, childId) {
477
+ const parentPath = pathToScreen(parentId) || resolvedPath.value;
478
+ commitPath([...parentPath, String(childId)], 'push');
479
+ }
480
+
481
+ function childScreensFor(screen) {
482
+ if (!screen) return [];
483
+ const childIds = [
484
+ ...normalizeChildren(screen.children),
485
+ ...allScreens.value.filter((item) => item.parent === screen.id).map((item) => item.id),
486
+ ];
487
+ return [...new Set(childIds)]
488
+ .map((id) => screenById.value.get(id))
489
+ .filter(Boolean);
490
+ }
491
+
492
+ function isLazyScreenComponent(component) {
493
+ return typeof component === 'function';
494
+ }
495
+
496
+ function componentForScreen(screen) {
497
+ if (!screen) return null;
498
+ if (loadedComponents.has(screen.id)) return loadedComponents.get(screen.id);
499
+ if (screen.loader || isLazyScreenComponent(screen.component)) return null;
500
+ return screen.component || null;
501
+ }
502
+
503
+ async function ensureLoaded(screen) {
504
+ if (!screen || screen.slot || loadedComponents.has(screen.id) || loadingScreens.has(screen.id)) return;
505
+ const loader = screen.loader || (isLazyScreenComponent(screen.component) ? screen.component : null);
506
+ if (!loader) return;
507
+
508
+ loadingScreens.add(screen.id);
509
+ loadErrors.delete(screen.id);
510
+
511
+ try {
512
+ const module = await loader();
513
+ const component = module?.default || module;
514
+ loadedComponents.set(screen.id, component);
515
+ emit('load', { screen, component });
516
+ } catch (error) {
517
+ loadErrors.set(screen.id, error);
518
+ emit('error', { screen, error });
519
+ } finally {
520
+ loadingScreens.delete(screen.id);
521
+ }
522
+ }
523
+
524
+ function isScreenLoading(screen) {
525
+ return !!screen && loadingScreens.has(screen.id);
526
+ }
527
+
528
+ function screenError(screen) {
529
+ return screen ? loadErrors.get(screen.id) : null;
530
+ }
531
+
532
+ function focusActivePanel() {
533
+ if (!props.focusOnNavigate) return;
534
+ screenPanel.value?.focus?.({ preventScroll: true });
535
+ }
536
+
537
+ function mediaQueryForBreakpoint(value) {
538
+ const text = String(value || '').trim();
539
+ if (!text) return '(min-width: 768px)';
540
+ if (text.startsWith('(')) return text;
541
+ return `(min-width: ${text})`;
542
+ }
543
+
544
+ function configureDesktopMedia() {
545
+ if (typeof window === 'undefined') return;
546
+ removeDesktopMediaListener?.();
547
+
548
+ const query = window.matchMedia(mediaQueryForBreakpoint(props.desktopBreakpoint));
549
+ const update = () => {
550
+ isDesktopMedia.value = query.matches;
551
+ };
552
+
553
+ update();
554
+ if (query.addEventListener) query.addEventListener('change', update);
555
+ else query.addListener(update);
556
+ removeDesktopMediaListener = () => {
557
+ if (query.removeEventListener) query.removeEventListener('change', update);
558
+ else query.removeListener(update);
559
+ };
560
+ }
561
+ </script>
562
+
563
+ <template>
564
+ <div
565
+ data-dom-app-stack
566
+ class="flex h-full min-h-[28rem] w-full flex-col overflow-hidden bg-canvas text-canvas-fg"
567
+ >
568
+ <slot
569
+ v-if="showBreadcrumbs"
570
+ name="breadcrumb"
571
+ v-bind="breadcrumbSlotProps"
572
+ >
573
+ <nav
574
+ class="flex min-h-12 items-center gap-2 border-b border-border bg-canvas/95 px-2 backdrop-blur"
575
+ :class="breadcrumbClass"
576
+ aria-label="Settings path"
577
+ >
578
+ <button
579
+ v-if="canGoBack"
580
+ type="button"
581
+ class="inline-flex h-9 shrink-0 items-center gap-1 rounded-full px-2.5 text-sm font-medium text-canvas-fg transition hover:bg-secondary active:scale-95"
582
+ :class="backButtonClass"
583
+ @click="back()"
584
+ >
585
+ <svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
586
+ <path d="M12.7 4.3a1 1 0 0 1 0 1.4L8.42 10l4.3 4.3a1 1 0 0 1-1.42 1.4l-5-5a1 1 0 0 1 0-1.4l5-5a1 1 0 0 1 1.42 0Z" />
587
+ </svg>
588
+ <span>Back</span>
589
+ </button>
590
+
591
+ <ol class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto text-sm">
592
+ <li
593
+ v-for="(crumb, index) in crumbs"
594
+ :key="`${crumb.id}:${index}`"
595
+ class="flex min-w-0 items-center gap-1"
596
+ >
597
+ <button
598
+ v-if="index < crumbs.length - 1"
599
+ type="button"
600
+ class="max-w-36 truncate rounded-full px-2.5 py-1.5 font-medium text-muted-fg transition hover:bg-secondary hover:text-canvas-fg"
601
+ :class="breadcrumbItemClass"
602
+ @click="goTo(index)"
603
+ >{{ crumb.title }}</button>
604
+ <span
605
+ v-else
606
+ class="max-w-44 truncate rounded-full bg-secondary px-2.5 py-1.5 font-semibold text-canvas-fg"
607
+ :class="breadcrumbCurrentClass"
608
+ aria-current="page"
609
+ >{{ crumb.title }}</span>
610
+ <svg
611
+ v-if="index < crumbs.length - 1"
612
+ class="size-3 shrink-0 text-muted-fg"
613
+ viewBox="0 0 20 20"
614
+ fill="currentColor"
615
+ aria-hidden="true"
616
+ >
617
+ <path d="M7.3 4.3a1 1 0 0 1 1.4 0l5 5a1 1 0 0 1 0 1.4l-5 5a1 1 0 1 1-1.4-1.4L11.58 10 7.3 5.7a1 1 0 0 1 0-1.4Z" />
618
+ </svg>
619
+ </li>
620
+ </ol>
621
+ </nav>
622
+ </slot>
623
+
624
+ <div class="relative min-h-0 flex-1 overflow-hidden">
625
+ <Transition :name="transitionName">
626
+ <section
627
+ :key="activeId || 'empty'"
628
+ ref="screenPanel"
629
+ tabindex="-1"
630
+ data-dom-app-stack-pane
631
+ class="absolute inset-0 min-h-0 overflow-y-auto outline-none [-webkit-overflow-scrolling:touch]"
632
+ :class="[isDesktopLayout ? 'p-4 md:p-6' : 'p-4', isDesktopLayout ? desktopContentClass : mobilePanelClass]"
633
+ :aria-busy="isScreenLoading(activeScreen) ? 'true' : undefined"
634
+ >
635
+ <StackScreenRenderer
636
+ :screen="activeScreen"
637
+ :component="activeComponent"
638
+ :helpers="helpers"
639
+ :child-screens="childScreensFor(activeScreen)"
640
+ :loading="isScreenLoading(activeScreen)"
641
+ :error="screenError(activeScreen)"
642
+ :loading-label="loadingLabel"
643
+ :empty-title="emptyTitle"
644
+ :empty-description="emptyDescription"
645
+ />
646
+ </section>
647
+ </Transition>
648
+ </div>
649
+ </div>
650
+ </template>
651
+
652
+ <style scoped>
653
+ [data-dom-app-stack-pane] {
654
+ backface-visibility: hidden;
655
+ will-change: transform, opacity;
656
+ }
657
+
658
+ .dom-app-stack-forward-enter-active,
659
+ .dom-app-stack-back-enter-active {
660
+ pointer-events: none;
661
+ transition:
662
+ transform 360ms cubic-bezier(0.4, 0, 0.2, 1),
663
+ opacity 220ms ease;
664
+ }
665
+
666
+ .dom-app-stack-forward-leave-active,
667
+ .dom-app-stack-back-leave-active {
668
+ pointer-events: none;
669
+ transition:
670
+ transform 360ms cubic-bezier(0.4, 0, 0.2, 1),
671
+ opacity 140ms ease 180ms;
672
+ }
673
+
674
+ .dom-app-stack-forward-enter-from {
675
+ opacity: 0.98;
676
+ transform: translate3d(100%, 0, 0);
677
+ }
678
+
679
+ .dom-app-stack-forward-leave-to {
680
+ opacity: 0;
681
+ transform: translate3d(-24%, 0, 0);
682
+ }
683
+
684
+ .dom-app-stack-back-enter-from {
685
+ opacity: 0.72;
686
+ transform: translate3d(-24%, 0, 0);
687
+ }
688
+
689
+ .dom-app-stack-back-leave-to {
690
+ opacity: 0;
691
+ transform: translate3d(100%, 0, 0);
692
+ }
693
+
694
+ @media (prefers-reduced-motion: reduce) {
695
+ .dom-app-stack-forward-enter-active,
696
+ .dom-app-stack-forward-leave-active,
697
+ .dom-app-stack-back-enter-active,
698
+ .dom-app-stack-back-leave-active {
699
+ transition-delay: 0ms;
700
+ transition-duration: 1ms;
701
+ }
702
+
703
+ .dom-app-stack-forward-enter-from,
704
+ .dom-app-stack-forward-leave-to,
705
+ .dom-app-stack-back-enter-from,
706
+ .dom-app-stack-back-leave-to {
707
+ opacity: 1;
708
+ transform: none;
709
+ }
710
+ }
711
+ </style>