@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,408 @@
1
+ <script setup>
2
+ import { computed, ref } from 'vue';
3
+ import DomDropdown from '../dropdown/DomDropdown.vue';
4
+ import DomMenu from '../menu/DomMenu.vue';
5
+ import DomMenuItem from '../menu/DomMenuItem.vue';
6
+ import DomMenuLabel from '../menu/DomMenuLabel.vue';
7
+ import DomMenuSeparator from '../menu/DomMenuSeparator.vue';
8
+ import DomAvatar from '../../visual/avatar/DomAvatar.vue';
9
+ import DomThemeSwitcher from '../theme-switcher/DomThemeSwitcher.vue';
10
+ import DomProfileMenuItemContent from './DomProfileMenuItemContent.vue';
11
+
12
+ defineOptions({
13
+ __doc: {
14
+ name: 'Profile menu',
15
+ tag: '<DomProfileMenu>',
16
+ description: 'A signed-in account trigger with avatar, email, and app profile actions.',
17
+ playground: {
18
+ initial: {
19
+ name: 'Mina Patel',
20
+ email: 'mina@northstar.app',
21
+ initials: 'MP',
22
+ showName: false,
23
+ },
24
+ },
25
+ slots: [
26
+ { name: 'trigger', payload: '{ name, email, initials }', description: 'Replace the avatar and identity text inside the dropdown trigger.' },
27
+ { name: 'item', payload: '{ item, label, value }', description: 'Replace the markup for selectable data-driven menu items. Useful for resolving item.component or item.componentName to a custom row component.' },
28
+ { name: 'item-label', payload: '{ item, label, value }', description: 'Replace the markup for non-selectable data-driven label/static rows.' },
29
+ ],
30
+ events: [
31
+ { name: 'select', payload: '({ value, item })', description: 'Fired when an action item is chosen.' },
32
+ { name: 'change', payload: '({ value, checked, item })', description: 'Fired when a checkbox or radio menu item changes.' },
33
+ { name: 'update:themeMode', payload: '(mode: "dark" | "light" | "system")', description: 'Fired when the embedded theme switcher changes.' },
34
+ { name: 'profile', payload: '({ value, item })', description: 'Fired when the profile action is chosen.' },
35
+ { name: 'onboarding', payload: '({ value, item })', description: 'Fired when the onboarding action is chosen.' },
36
+ { name: 'theme', payload: '({ value, item, mode })', description: 'Fired when the theme mode changes, or when a plain theme item is chosen.' },
37
+ { name: 'logout', payload: '({ value, item })', description: 'Fired when the logout action is chosen.' },
38
+ ],
39
+ },
40
+ });
41
+
42
+ const profileItem = { value: 'profile', label: 'Profile', description: 'Account details and preferences', icon: 'profile', href: '#profile' };
43
+ const onboardingItem = { value: 'onboarding', label: 'Onboarding', description: 'Resume setup checklist', icon: 'onboarding' };
44
+ const logoutItem = { value: 'logout', label: 'Log out', description: 'End the current session', icon: 'logout', tone: 'danger' };
45
+ const onboardingTaskItems = [
46
+ { value: 'add-website', label: 'Add website', type: 'task', href: '#add-website', completed: true },
47
+ { value: 'audience-competitors', label: 'Audience and competitors', type: 'task', href: '#audience-competitors', completed: false },
48
+ { value: 'integrate-blog', label: 'Integrate blog', type: 'task', href: '#integrate-blog', completed: false },
49
+ { value: 'connect-search-console', label: 'Connect Google search console', type: 'task', href: '#connect-search-console', completed: false },
50
+ ];
51
+
52
+ const iconPaths = {
53
+ profile: [
54
+ 'M20 21a8 8 0 0 0-16 0',
55
+ 'M12 13a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
56
+ ],
57
+ onboarding: [
58
+ 'M9 11l3 3L22 4',
59
+ 'M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11',
60
+ ],
61
+ theme: [
62
+ 'M12 3a9 9 0 1 0 9 9 6 6 0 0 1-9-9z',
63
+ ],
64
+ logout: [
65
+ 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4',
66
+ 'M16 17l5-5-5-5',
67
+ 'M21 12H9',
68
+ ],
69
+ };
70
+
71
+ const props = defineProps({
72
+ name: {
73
+ type: String,
74
+ default: '',
75
+ _edit: { description: 'User display name used for avatar fallback and optional trigger text.' },
76
+ },
77
+ email: {
78
+ type: String,
79
+ default: '',
80
+ _edit: { description: 'Signed-in email shown in the trigger by default.' },
81
+ },
82
+ avatar: {
83
+ type: String,
84
+ default: '',
85
+ _edit: { description: 'Avatar image URL.' },
86
+ },
87
+ initials: {
88
+ type: String,
89
+ default: '',
90
+ _edit: { description: 'Explicit initials for the avatar fallback.' },
91
+ },
92
+ triggerLabel: {
93
+ type: String,
94
+ default: '',
95
+ _edit: { description: 'Fallback trigger text when no email or name is available.' },
96
+ },
97
+ showName: {
98
+ type: Boolean,
99
+ default: false,
100
+ _edit: { description: 'Show the name in the trigger. When email is also shown, it appears as secondary text.' },
101
+ },
102
+ showEmail: {
103
+ type: Boolean,
104
+ default: true,
105
+ _edit: { description: 'Show the email address in the trigger.' },
106
+ },
107
+ avatarSize: {
108
+ type: String,
109
+ default: 'sm',
110
+ _edit: { options: ['xs', 'sm', 'md'], description: 'Avatar size inside the trigger.' },
111
+ },
112
+ themeMode: {
113
+ type: String,
114
+ default: null,
115
+ _edit: { options: ['dark', 'light', 'system'], description: 'Optional controlled theme mode for the embedded theme switcher. Omit it to use the stored DOM Studio preference.' },
116
+ },
117
+ themeControl: {
118
+ type: Boolean,
119
+ default: true,
120
+ _edit: { description: 'Render a segmented dark / light / system control as a profile row accessory.' },
121
+ },
122
+ items: {
123
+ type: Array,
124
+ default: null,
125
+ _edit: {
126
+ component: 'DomJsonListInput',
127
+ description: 'Optional data-driven menu rows. Use separator: true, type: "label" for static rows, children for submenus, shortcut for right-side commands, accessorySlot for custom right-side content, and componentName as a marker for the #item slot.',
128
+ props: {
129
+ compact: true,
130
+ addLabel: '+ Add action',
131
+ schema: [
132
+ { key: 'label', label: 'Label', placeholder: 'Profile', default: (index) => ['Profile', 'Onboarding', 'Log out'][index] || `Action ${index + 1}` },
133
+ { key: 'value', label: 'Value', placeholder: 'profile', default: (index) => ['profile', 'onboarding', 'logout'][index] || `action-${index + 1}` },
134
+ { key: 'href', label: 'Href', placeholder: '/settings/profile' },
135
+ { key: 'description', label: 'Description', placeholder: 'Account details and preferences' },
136
+ { key: 'icon', label: 'Icon', placeholder: 'profile' },
137
+ { key: 'shortcut', label: 'Shortcut', placeholder: '⌘K' },
138
+ { key: 'accessorySlot', label: 'Accessory slot', placeholder: 'theme-switcher' },
139
+ { key: 'componentName', label: 'Component', placeholder: 'usage' },
140
+ { key: 'type', label: 'Type', placeholder: 'label' },
141
+ { key: 'tone', label: 'Tone', placeholder: 'danger' },
142
+ ],
143
+ },
144
+ },
145
+ },
146
+ align: {
147
+ type: String,
148
+ default: 'right',
149
+ _edit: { options: ['left', 'right'], description: 'Where the menu opens relative to the trigger.' },
150
+ },
151
+ placement: {
152
+ type: String,
153
+ default: 'bottom',
154
+ _edit: { options: ['bottom', 'top', 'right', 'left'], description: 'Preferred side before collision handling.' },
155
+ },
156
+ width: {
157
+ type: String,
158
+ default: 'min-w-[18rem]',
159
+ _edit: { description: 'Tailwind width utility for the dropdown panel.' },
160
+ },
161
+ collisionPadding: {
162
+ type: Number,
163
+ default: 8,
164
+ _edit: { description: 'Viewport padding used when the menu flips or shifts.' },
165
+ },
166
+ floatingMode: {
167
+ type: String,
168
+ default: 'viewport',
169
+ _edit: { options: ['viewport', 'anchor'], description: 'viewport keeps the menu inside the browser; anchor keeps it attached while scrolling.' },
170
+ },
171
+ lockScroll: {
172
+ type: Boolean,
173
+ default: false,
174
+ _edit: { description: 'Lock browser scrolling while the dropdown is open.' },
175
+ },
176
+ });
177
+
178
+ const emit = defineEmits(['select', 'change', 'update:themeMode', 'profile', 'onboarding', 'theme', 'logout']);
179
+ const dropdown = ref(null);
180
+ const actionEvents = new Set(['profile', 'onboarding', 'theme', 'logout']);
181
+
182
+ const hasCustomItems = computed(() => Array.isArray(props.items) && props.items.length > 0);
183
+ const accountSummary = computed(() => props.email || props.name || '');
184
+ const defaultMenuItems = computed(() => cleanSeparators([
185
+ ...(accountSummary.value ? [{ type: 'label', value: 'account-summary', label: 'Logged in as', description: accountSummary.value }, { separator: true }] : []),
186
+ profileItem,
187
+ { ...onboardingItem, children: onboardingTaskItems },
188
+ { separator: true },
189
+ logoutItem,
190
+ ]));
191
+ const panelItems = computed(() => {
192
+ if (!Array.isArray(props.items)) return [];
193
+ return cleanSeparators(props.items.map((item) => normalizeMenuItem(item)).filter(Boolean));
194
+ });
195
+ const activeMenuItems = computed(() => hasCustomItems.value ? panelItems.value : defaultMenuItems.value);
196
+
197
+ const avatarName = computed(() => props.name || props.email || props.triggerLabel || 'Account');
198
+ const triggerPrimary = computed(() => {
199
+ if (props.showName && props.name) return props.name;
200
+ if (props.showEmail && props.email) return props.email;
201
+ return props.triggerLabel || props.email || props.name || 'Account';
202
+ });
203
+ const triggerSecondary = computed(() => {
204
+ if (!props.showName || !props.showEmail || !props.name || !props.email) return '';
205
+ return props.email;
206
+ });
207
+
208
+ function labelOf(item) {
209
+ if (item && typeof item === 'object') return item.label ?? item.value;
210
+ return item;
211
+ }
212
+
213
+ function itemValueOf(item) {
214
+ if (item && typeof item === 'object') return item.value ?? item.label;
215
+ return item;
216
+ }
217
+
218
+ function isSeparator(item) {
219
+ return Boolean(item && typeof item === 'object' && item.separator);
220
+ }
221
+
222
+ function isThemeItem(item) {
223
+ return props.themeControl && String(itemValueOf(item)) === 'theme';
224
+ }
225
+
226
+ function normalizeMenuItem(item) {
227
+ if (isSeparator(item)) return item;
228
+ if (!item || typeof item !== 'object') return item;
229
+
230
+ const value = String(itemValueOf(item));
231
+ const next = { ...item };
232
+ if (props.themeControl && value === 'theme') next.accessorySlot = next.accessorySlot || 'theme-switcher';
233
+ if (Array.isArray(next.children) && next.children.length) {
234
+ next.children = cleanSeparators(next.children.map((child) => normalizeMenuItem(child)).filter(Boolean));
235
+ }
236
+ return next;
237
+ }
238
+
239
+ function cleanSeparators(items) {
240
+ return items.reduce((result, item) => {
241
+ if (!isSeparator(item)) return [...result, item];
242
+ if (!result.length || isSeparator(result[result.length - 1])) return result;
243
+ return [...result, item];
244
+ }, []).filter((item, index, list) => !isSeparator(item) || index < list.length - 1);
245
+ }
246
+
247
+ function itemForValue(value, items = activeMenuItems.value) {
248
+ for (const item of items) {
249
+ if (isSeparator(item)) continue;
250
+ if (String(itemValueOf(item)) === String(value)) return item;
251
+ if (Array.isArray(item?.children)) {
252
+ const child = itemForValue(value, item.children);
253
+ if (child) return child;
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+
259
+ function itemFromMenuEvent(event) {
260
+ if (event.item && !event.item.nodeType) return event.item;
261
+ return itemForValue(event.value);
262
+ }
263
+
264
+ function iconFor(item) {
265
+ if (!item || typeof item !== 'object') return [];
266
+ return iconPaths[item.icon || item.value] || [];
267
+ }
268
+
269
+ function onThemeModeUpdate(mode) {
270
+ emit('update:themeMode', mode);
271
+ }
272
+
273
+ function onThemeChange(event) {
274
+ const item = itemForValue('theme') || itemForValue('profile');
275
+ emit('theme', { value: 'theme', item, mode: event.mode, theme: event.theme });
276
+ }
277
+
278
+ function onMenuChange(event) {
279
+ const item = itemFromMenuEvent(event);
280
+ const payload = { ...event, item };
281
+ emit('change', payload);
282
+ }
283
+
284
+ function onMenuSelect(event) {
285
+ const item = itemFromMenuEvent(event);
286
+ if (isThemeItem(item)) return;
287
+ emitSelection(event.value, item);
288
+ }
289
+
290
+ function emitSelection(value, item = itemForValue(value)) {
291
+ const payload = { value, item };
292
+ emit('select', payload);
293
+ if (actionEvents.has(String(value))) emit(String(value), payload);
294
+ }
295
+
296
+ function open() {
297
+ dropdown.value?.open?.();
298
+ }
299
+
300
+ function close() {
301
+ dropdown.value?.close?.();
302
+ }
303
+
304
+ function toggle() {
305
+ dropdown.value?.toggle?.();
306
+ }
307
+
308
+ defineExpose({
309
+ open,
310
+ close,
311
+ toggle,
312
+ });
313
+ </script>
314
+
315
+ <template>
316
+ <DomDropdown
317
+ ref="dropdown"
318
+ :items="panelItems"
319
+ :align="align"
320
+ :placement="placement"
321
+ :collision-padding="collisionPadding"
322
+ :floating-mode="floatingMode"
323
+ :lock-scroll="lockScroll"
324
+ :width="width"
325
+ :auto-focus-panel="false"
326
+ panel-type="dialog"
327
+ >
328
+ <template #trigger>
329
+ <slot name="trigger" :name="name" :email="email" :initials="initials">
330
+ <span class="flex min-w-0 items-center gap-2">
331
+ <DomAvatar :src="avatar" :name="avatarName" :initials="initials" :alt="name ? `${name} avatar` : ''" :size="avatarSize" />
332
+ <span class="grid min-w-0 text-left leading-tight">
333
+ <span class="truncate" :class="triggerSecondary ? 'text-[13px] font-semibold' : 'text-sm'">{{ triggerPrimary }}</span>
334
+ <span v-if="triggerSecondary" class="truncate text-xs font-normal text-muted-fg">{{ triggerSecondary }}</span>
335
+ </span>
336
+ </span>
337
+ </slot>
338
+ </template>
339
+
340
+ <DomMenu v-if="hasCustomItems" :items="panelItems" @select="onMenuSelect" @change="onMenuChange">
341
+ <template #item="{ item, label, value }">
342
+ <slot name="item" :item="item" :label="label" :value="value">
343
+ <DomProfileMenuItemContent :item="item" :label="label" :paths="iconFor(item)" />
344
+ </slot>
345
+ </template>
346
+
347
+ <template #item-label="{ item, label, value }">
348
+ <slot name="item-label" :item="item" :label="label" :value="value">
349
+ <span class="block text-xs font-medium text-muted-fg">{{ label }}</span>
350
+ <span v-if="item?.description" class="mt-0.5 block truncate text-sm font-medium text-canvas-fg">
351
+ {{ item.description }}
352
+ </span>
353
+ </slot>
354
+ </template>
355
+
356
+ <template #theme-switcher>
357
+ <DomThemeSwitcher
358
+ :model-value="themeMode"
359
+ :show-label="false"
360
+ size="icon-sm"
361
+ @update:model-value="onThemeModeUpdate"
362
+ @change="onThemeChange"
363
+ />
364
+ </template>
365
+ </DomMenu>
366
+
367
+ <DomMenu v-else @select="onMenuSelect" @change="onMenuChange">
368
+ <DomMenuLabel v-if="accountSummary">
369
+ <span class="block text-[11px] font-medium uppercase text-muted-fg">Logged in as</span>
370
+ <span class="mt-0.5 block truncate text-sm font-medium text-canvas-fg">{{ accountSummary }}</span>
371
+ </DomMenuLabel>
372
+ <DomMenuSeparator v-if="accountSummary" />
373
+
374
+ <DomMenuItem :value="profileItem.value" :href="profileItem.href">
375
+ <DomProfileMenuItemContent :item="profileItem" :label="profileItem.label" :paths="iconFor(profileItem)" />
376
+ <template v-if="themeControl" #accessory>
377
+ <DomThemeSwitcher
378
+ :model-value="themeMode"
379
+ :show-label="false"
380
+ size="icon-sm"
381
+ @update:model-value="onThemeModeUpdate"
382
+ @change="onThemeChange"
383
+ />
384
+ </template>
385
+ </DomMenuItem>
386
+
387
+ <DomMenuItem :value="onboardingItem.value">
388
+ <DomProfileMenuItemContent :item="onboardingItem" :label="onboardingItem.label" :paths="iconFor(onboardingItem)" />
389
+ <template #submenu>
390
+ <DomMenuItem
391
+ v-for="task in onboardingTaskItems"
392
+ :key="task.value"
393
+ :value="task.value"
394
+ :href="task.href"
395
+ >
396
+ <DomProfileMenuItemContent :item="task" :label="task.label" />
397
+ </DomMenuItem>
398
+ </template>
399
+ </DomMenuItem>
400
+
401
+ <DomMenuSeparator />
402
+
403
+ <DomMenuItem :value="logoutItem.value" :tone="logoutItem.tone">
404
+ <DomProfileMenuItemContent :item="logoutItem" :label="logoutItem.label" :paths="iconFor(logoutItem)" />
405
+ </DomMenuItem>
406
+ </DomMenu>
407
+ </DomDropdown>
408
+ </template>
@@ -0,0 +1,45 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ item: {
6
+ type: Object,
7
+ default: () => ({}),
8
+ },
9
+ label: {
10
+ type: String,
11
+ default: '',
12
+ },
13
+ paths: {
14
+ type: Array,
15
+ default: () => [],
16
+ },
17
+ });
18
+
19
+ const isTask = computed(() => props.item?.type === 'task' || props.item?.task === true || props.item?.completed !== undefined);
20
+ const isComplete = computed(() => Boolean(props.item?.completed));
21
+ </script>
22
+
23
+ <template>
24
+ <span class="flex min-w-0 items-center gap-3">
25
+ <span v-if="isTask" class="grid size-4 shrink-0 place-items-center rounded-full border text-primary" :class="isComplete ? 'border-primary bg-primary text-primary-fg' : 'border-border bg-canvas text-muted-fg'" aria-hidden="true">
26
+ <svg v-if="isComplete" viewBox="0 0 16 16" class="size-3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.2l2.7 2.7 6.3-6.6" /></svg>
27
+ </span>
28
+ <span v-else class="grid size-8 shrink-0 place-items-center rounded-full bg-secondary text-muted-fg" :class="item?.tone === 'danger' ? 'bg-destructive/10 text-destructive' : ''">
29
+ <svg v-if="paths.length" viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
30
+ <path v-for="path in paths" :key="path" :d="path" />
31
+ </svg>
32
+ <span v-else class="size-1.5 rounded-full bg-current opacity-70" aria-hidden="true" />
33
+ </span>
34
+ <span class="min-w-0 flex-1">
35
+ <span class="block truncate font-medium" :class="isComplete ? 'text-muted-fg line-through decoration-border decoration-2' : ''">{{ label }}</span>
36
+ <span
37
+ v-if="item?.description"
38
+ class="mt-0.5 block truncate text-xs"
39
+ :class="item?.tone === 'danger' ? 'text-destructive/80' : 'text-muted-fg'"
40
+ >
41
+ {{ item.description }}
42
+ </span>
43
+ </span>
44
+ </span>
45
+ </template>