@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,149 @@
1
+ <script setup>
2
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: String, default: '' },
6
+ tabs: { type: Array, required: true }, // [{ key, label }]
7
+ variant: {
8
+ type: String,
9
+ default: 'pill',
10
+ _edit: { options: ['pill', 'flat', 'page'], description: 'Visual treatment for the tab list.' },
11
+ },
12
+ /** Stretch tabs + active panel to fill a flex parent (studio / inspector shell). */
13
+ fill: { type: Boolean, default: false },
14
+ });
15
+ const emit = defineEmits(['update:modelValue']);
16
+
17
+ const root = ref(null);
18
+ const activeValue = computed(() => props.modelValue || props.tabs[0]?.key || '');
19
+ const tablistClass = computed(() => {
20
+ if (props.variant === 'page') {
21
+ return [
22
+ 'relative flex min-w-0 shrink-0 overflow-x-auto bg-secondary/55 px-2 pt-1',
23
+ 'before:pointer-events-none before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-border before:content-[\'\']',
24
+ ].join(' ');
25
+ }
26
+ if (props.variant === 'flat') return 'flex min-w-0 shrink-0 gap-6 overflow-x-auto border-b border-border';
27
+ return 'inline-flex shrink-0 gap-1 rounded-full border border-border bg-secondary p-1';
28
+ });
29
+ const tabButtonClass = computed(() => {
30
+ if (props.variant === 'page') {
31
+ return [
32
+ 'relative -mb-px flex h-9 min-w-0 max-w-56 items-center gap-2 rounded-t-md border border-transparent border-b-border px-3 text-left text-sm font-medium text-muted-fg transition',
33
+ 'hover:border-x-border hover:border-t-border hover:bg-secondary hover:text-canvas-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60',
34
+ 'data-[active]:z-10 data-[active]:border-x-border data-[active]:border-t-border data-[active]:border-b-canvas data-[active]:bg-canvas data-[active]:text-canvas-fg',
35
+ 'data-[active]:after:pointer-events-none data-[active]:after:absolute data-[active]:after:inset-x-px data-[active]:after:-bottom-px data-[active]:after:h-1 data-[active]:after:bg-canvas data-[active]:after:content-[\'\']',
36
+ ].join(' ');
37
+ }
38
+ if (props.variant === 'flat') {
39
+ return [
40
+ 'relative -mb-px flex h-10 shrink-0 items-center border-b-2 border-transparent px-1 text-sm font-medium text-muted-fg transition',
41
+ 'hover:text-canvas-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60',
42
+ 'data-[active]:border-primary data-[active]:text-canvas-fg',
43
+ ].join(' ');
44
+ }
45
+ return 'rounded-full px-4 py-1.5 text-sm font-medium text-muted-fg transition focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 data-[active]:bg-canvas data-[active]:text-canvas-fg data-[active]:shadow-sm';
46
+ });
47
+ const panelClass = computed(() => {
48
+ if (props.fill) return props.variant === 'page'
49
+ ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas'
50
+ : 'mt-3 flex min-h-0 flex-1 flex-col overflow-hidden';
51
+ return props.variant === 'page' ? 'mt-0 bg-canvas' : 'mt-6';
52
+ });
53
+
54
+ onMounted(mountTabs);
55
+ onUnmounted(removeTabsListeners);
56
+ watch(getModelValue, syncModelValue);
57
+
58
+ /**
59
+ * Load the headless tabs behavior and subscribe to changes from this tab host.
60
+ *
61
+ * @returns {Promise<void>} Resolves after the headless element is registered.
62
+ */
63
+ async function mountTabs() {
64
+ await import('../../lib/headless/tabs.js');
65
+ root.value?.addEventListener('dom:change', onTabChange);
66
+ }
67
+
68
+ /**
69
+ * Remove the host-level change listener when the Vue component unmounts.
70
+ *
71
+ * @returns {void}
72
+ */
73
+ function removeTabsListeners() {
74
+ root.value?.removeEventListener('dom:change', onTabChange);
75
+ }
76
+
77
+ /**
78
+ * Return the externally controlled tab value for Vue's watcher.
79
+ *
80
+ * @returns {string} Controlled active tab value.
81
+ */
82
+ function getModelValue() {
83
+ return props.modelValue;
84
+ }
85
+
86
+ /**
87
+ * Commit only changes dispatched by the tab host itself.
88
+ *
89
+ * Nested DOM Studio controls can also emit bubbling `dom:change` events. Those
90
+ * events must not reset the active tab.
91
+ *
92
+ * @param {CustomEvent<{ value?: string }>} event Headless change event.
93
+ * @returns {void}
94
+ */
95
+ function onTabChange(event) {
96
+ if (event.target !== root.value) return;
97
+ const value = event.detail?.value;
98
+ let isKnownTab = false;
99
+ for (const tab of props.tabs) {
100
+ if (tab.key === value) {
101
+ isKnownTab = true;
102
+ break;
103
+ }
104
+ }
105
+ if (!isKnownTab) return;
106
+ emit('update:modelValue', value);
107
+ }
108
+
109
+ /**
110
+ * Keep the headless tab host aligned with the controlled Vue model.
111
+ *
112
+ * @param {string} value Active tab value.
113
+ * @returns {void}
114
+ */
115
+ function syncModelValue(value) {
116
+ if (root.value && value && root.value.value !== value) root.value.value = value;
117
+ }
118
+ </script>
119
+
120
+ <template>
121
+ <dom-tabs
122
+ ref="root"
123
+ :value="modelValue || tabs[0]?.key"
124
+ :class="fill ? 'flex min-h-0 flex-1 flex-col' : 'block'"
125
+ >
126
+ <div role="tablist" :class="tablistClass">
127
+ <button
128
+ v-for="t in tabs"
129
+ :key="t.key"
130
+ :data-tab="t.key"
131
+ :class="tabButtonClass"
132
+ >
133
+ <slot name="tab" :tab="t" :active="activeValue === t.key">
134
+ <span class="truncate">{{ t.label }}</span>
135
+ </slot>
136
+ </button>
137
+ </div>
138
+ <div :class="panelClass">
139
+ <div
140
+ v-for="t in tabs"
141
+ :key="t.key"
142
+ :data-panel="t.key"
143
+ :class="fill && 'flex min-h-0 flex-1 flex-col overflow-hidden'"
144
+ >
145
+ <slot :name="t.key" />
146
+ </div>
147
+ </div>
148
+ </dom-tabs>
149
+ </template>
@@ -0,0 +1,226 @@
1
+ <script setup>
2
+ import { computed, onMounted, ref, watch } from 'vue';
3
+ import DomToggleButtonGroup from '../../forms/toggle-button-group/DomToggleButtonGroup.vue';
4
+ import { normalizeThemeMode, useTheme } from '../../lib/vue/useTheme.js';
5
+
6
+ defineOptions({
7
+ __doc: {
8
+ name: 'Theme switcher',
9
+ tag: '<DomThemeSwitcher>',
10
+ description: 'A DOM Studio light, dark, and system theme preference control.',
11
+ events: [
12
+ { name: 'update:modelValue', payload: '(mode: "light" | "dark" | "system")', description: 'Emitted when the stored theme mode changes.' },
13
+ { name: 'change', payload: '({ mode, theme })', description: 'Emitted with the selected preference mode and resolved light or dark theme.' },
14
+ ],
15
+ playground: {
16
+ initial: {
17
+ showLabel: true,
18
+ size: 'icon-sm',
19
+ },
20
+ },
21
+ },
22
+ });
23
+
24
+ const compactThemeModeOptions = [
25
+ {
26
+ value: 'dark',
27
+ label: 'Dark',
28
+ tooltip: 'dark mode theme',
29
+ paths: ['M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'],
30
+ },
31
+ {
32
+ value: 'light',
33
+ label: 'Light',
34
+ tooltip: 'light mode theme',
35
+ paths: [
36
+ 'M12 2v2',
37
+ 'M12 20v2',
38
+ 'M4.93 4.93l1.41 1.41',
39
+ 'M17.66 17.66l1.41 1.41',
40
+ 'M2 12h2',
41
+ 'M20 12h2',
42
+ 'M4.93 19.07l1.41-1.41',
43
+ 'M17.66 6.34l1.41-1.41',
44
+ 'M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
45
+ ],
46
+ },
47
+ {
48
+ value: 'system',
49
+ label: 'System',
50
+ tooltip: 'Auto: use the theme of the system',
51
+ paths: [
52
+ 'M3 6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6z',
53
+ 'M8 21h8',
54
+ 'M12 17v4',
55
+ ],
56
+ },
57
+ ];
58
+ const appearanceThemeModeOptions = [
59
+ compactThemeModeOptions[1],
60
+ compactThemeModeOptions[0],
61
+ compactThemeModeOptions[2],
62
+ ];
63
+
64
+ const props = defineProps({
65
+ modelValue: {
66
+ type: String,
67
+ default: null,
68
+ _edit: { options: ['light', 'dark', 'system'], description: 'Optional controlled theme mode. Omit this to use the stored DOM Studio preference.' },
69
+ },
70
+ label: {
71
+ type: String,
72
+ default: 'Theme mode',
73
+ _edit: { description: 'Accessible label for the segmented control.' },
74
+ },
75
+ showLabel: {
76
+ type: Boolean,
77
+ default: true,
78
+ _edit: { description: 'Show the current preference label beside the segmented control.' },
79
+ },
80
+ size: {
81
+ type: String,
82
+ default: 'icon-sm',
83
+ _edit: { options: ['icon-sm', 'icon-md', 'sm', 'md', 'lg'], description: 'Button size for the segmented control.' },
84
+ },
85
+ variant: {
86
+ type: String,
87
+ default: 'default',
88
+ _edit: { options: ['default', 'appearance'], description: 'Use appearance for a larger account-settings presentation.' },
89
+ },
90
+ title: {
91
+ type: String,
92
+ default: 'Appearance settings',
93
+ _edit: { description: 'Heading shown by the appearance variant. Pass an empty string to hide it.' },
94
+ },
95
+ description: {
96
+ type: String,
97
+ default: 'Update the appearance settings for your account',
98
+ _edit: { description: 'Supporting text shown by the appearance variant. Pass an empty string to hide it.' },
99
+ },
100
+ });
101
+
102
+ const emit = defineEmits(['update:modelValue', 'change']);
103
+ const { mode, resolvedTheme, setMode, initTheme } = useTheme();
104
+ const mounted = ref(false);
105
+
106
+ const isAppearanceVariant = computed(() => props.variant === 'appearance');
107
+ const hasControlledMode = computed(() => props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '');
108
+ const themeModeOptions = computed(() => isAppearanceVariant.value ? appearanceThemeModeOptions : compactThemeModeOptions);
109
+ const selectedMode = computed({
110
+ get() {
111
+ return mode.value;
112
+ },
113
+ set(value) {
114
+ commitMode(value);
115
+ },
116
+ });
117
+ const currentLabel = computed(() => {
118
+ return compactThemeModeOptions.find((option) => option.value === mode.value)?.label || 'System';
119
+ });
120
+ const displayLabel = computed(() => mounted.value ? currentLabel.value : 'System');
121
+ const controlSize = computed(() => isAppearanceVariant.value && props.size === 'icon-sm' ? 'md' : props.size);
122
+ const placeholderSizeClass = computed(() => ({
123
+ sm: 'min-h-8 px-3 py-1.5 text-xs',
124
+ md: 'min-h-10 px-4 py-2 text-sm',
125
+ lg: 'min-h-11 px-5 py-2.5 text-base',
126
+ 'icon-sm': 'size-8 p-0 text-xs',
127
+ 'icon-md': 'size-10 p-0 text-sm',
128
+ }[controlSize.value] || 'size-8 p-0 text-xs'));
129
+
130
+ onMounted(() => {
131
+ initTheme();
132
+ if (hasControlledMode.value) setMode(props.modelValue);
133
+ mounted.value = true;
134
+ });
135
+
136
+ watch(() => props.modelValue, (value) => {
137
+ if (!mounted.value || !hasControlledMode.value) return;
138
+ setMode(value);
139
+ });
140
+
141
+ /**
142
+ * Persists the selected theme mode and emits the public change events.
143
+ *
144
+ * @param {unknown} value Requested theme mode.
145
+ */
146
+ function commitMode(value) {
147
+ const next = normalizeThemeMode(value);
148
+ const theme = setMode(next);
149
+ emit('update:modelValue', next);
150
+ emit('change', { mode: next, theme });
151
+ }
152
+ </script>
153
+
154
+ <template>
155
+ <div
156
+ :class="isAppearanceVariant ? 'grid w-full max-w-lg gap-4' : (showLabel ? 'flex w-full items-center justify-between gap-3' : 'flex w-fit items-center gap-3')"
157
+ >
158
+ <div v-if="isAppearanceVariant && (title || description)" class="grid gap-1">
159
+ <h2 v-if="title" class="text-2xl font-semibold leading-tight text-canvas-fg">
160
+ {{ title }}
161
+ </h2>
162
+ <p v-if="description" class="text-sm leading-6 text-muted-fg">
163
+ {{ description }}
164
+ </p>
165
+ </div>
166
+
167
+ <span v-if="!isAppearanceVariant && showLabel" class="min-w-0 truncate text-sm font-medium text-canvas-fg">
168
+ {{ displayLabel }} theme
169
+ </span>
170
+
171
+ <DomToggleButtonGroup
172
+ v-if="mounted"
173
+ :key="selectedMode"
174
+ v-model="selectedMode"
175
+ :options="themeModeOptions"
176
+ :label="label"
177
+ :size="controlSize"
178
+ :variant="isAppearanceVariant ? 'switch' : 'default'"
179
+ :chrome="false"
180
+ >
181
+ <template #option="{ option }">
182
+ <svg
183
+ viewBox="0 0 24 24"
184
+ class="size-4"
185
+ fill="none"
186
+ stroke="currentColor"
187
+ stroke-width="1.8"
188
+ stroke-linecap="round"
189
+ stroke-linejoin="round"
190
+ aria-hidden="true"
191
+ >
192
+ <path v-for="path in option.paths" :key="path" :d="path" />
193
+ </svg>
194
+ <span v-if="isAppearanceVariant">{{ option.label }}</span>
195
+ <span v-else class="sr-only">{{ option.label }}</span>
196
+ </template>
197
+ </DomToggleButtonGroup>
198
+
199
+ <div
200
+ v-else
201
+ class="inline-flex w-fit overflow-hidden rounded-xl"
202
+ :class="isAppearanceVariant ? 'items-center gap-0.5 bg-secondary/80 p-0.5 shadow-inner ring-1 ring-border/30' : 'skin-control p-0'"
203
+ aria-hidden="true"
204
+ >
205
+ <span
206
+ v-for="option in themeModeOptions"
207
+ :key="option.value"
208
+ class="relative inline-flex items-center justify-center gap-2 text-muted-fg first:border-l-0 first:rounded-l-xl last:rounded-r-xl"
209
+ :class="[isAppearanceVariant ? 'rounded-lg' : 'border-l border-border/80', placeholderSizeClass]"
210
+ >
211
+ <svg
212
+ viewBox="0 0 24 24"
213
+ class="size-4"
214
+ fill="none"
215
+ stroke="currentColor"
216
+ stroke-width="1.8"
217
+ stroke-linecap="round"
218
+ stroke-linejoin="round"
219
+ >
220
+ <path v-for="path in option.paths" :key="path" :d="path" />
221
+ </svg>
222
+ <span v-if="isAppearanceVariant">{{ option.label }}</span>
223
+ </span>
224
+ </div>
225
+ </div>
226
+ </template>
@@ -0,0 +1,52 @@
1
+ <script setup>
2
+ defineOptions({
3
+ __doc: {
4
+ hidden: true,
5
+ studio: { hidden: true },
6
+ },
7
+ });
8
+
9
+ const props = defineProps({
10
+ title: {
11
+ type: String,
12
+ default: '',
13
+ },
14
+ description: {
15
+ type: String,
16
+ default: '',
17
+ },
18
+ tone: {
19
+ type: String,
20
+ default: 'default',
21
+ },
22
+ dismissible: {
23
+ type: Boolean,
24
+ default: true,
25
+ },
26
+ });
27
+ const emit = defineEmits(['dismiss']);
28
+
29
+ const accentClass = (tone) => ({
30
+ default: 'bg-primary',
31
+ success: 'bg-success',
32
+ danger: 'bg-destructive',
33
+ warning: 'bg-warning',
34
+ }[tone] || 'bg-primary');
35
+ </script>
36
+
37
+ <template>
38
+ <div class="flex items-start gap-3">
39
+ <span class="mt-1 size-2.5 shrink-0 rounded-full" :class="accentClass(tone)"></span>
40
+ <div class="min-w-0 flex-1">
41
+ <p v-if="title" class="text-sm font-semibold">{{ title }}</p>
42
+ <p v-if="description" class="mt-1 text-sm text-muted-fg">{{ description }}</p>
43
+ </div>
44
+ <button
45
+ v-if="props.dismissible"
46
+ type="button"
47
+ aria-label="Dismiss notification"
48
+ class="-mr-1 -mt-1 rounded-full px-2 text-lg leading-none text-muted-fg hover:bg-accent hover:text-accent-fg"
49
+ @click="emit('dismiss')"
50
+ >×</button>
51
+ </div>
52
+ </template>