@getdom/studio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { markRaw } from 'vue';
|
|
2
|
+
import { getRuntimeComponentRecords } from '../componentRuntimeRecords.js';
|
|
3
|
+
import { getComponentProps, getStudioMeta, inspectComponentRecord } from '../componentInspector.js';
|
|
4
|
+
|
|
5
|
+
const GROUP_ORDER = ['DOM Studio', 'Forms', 'Visual', 'HTML', 'Content'];
|
|
6
|
+
|
|
7
|
+
const SAMPLE_PROPS = {
|
|
8
|
+
DomAccordion: {
|
|
9
|
+
items: [
|
|
10
|
+
{ title: 'Section A', content: 'Content for the first section.' },
|
|
11
|
+
{ title: 'Section B', content: 'Content for the second section.' },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
DomAutocomplete: {
|
|
15
|
+
placeholder: 'Search...',
|
|
16
|
+
options: [
|
|
17
|
+
{ label: 'Apple', value: 'apple' },
|
|
18
|
+
{ label: 'Banana', value: 'banana' },
|
|
19
|
+
{ label: 'Cherry', value: 'cherry' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
DomCalendar: {
|
|
23
|
+
label: 'Date',
|
|
24
|
+
description: 'Pick a date.',
|
|
25
|
+
modelValue: '2026-06-18',
|
|
26
|
+
initialMonth: 6,
|
|
27
|
+
initialYear: 2026,
|
|
28
|
+
},
|
|
29
|
+
DomCard: {
|
|
30
|
+
class: 'w-full max-w-sm',
|
|
31
|
+
},
|
|
32
|
+
DomCheckbox: {
|
|
33
|
+
modelValue: true,
|
|
34
|
+
label: 'Product updates',
|
|
35
|
+
},
|
|
36
|
+
DomCodeInput: {
|
|
37
|
+
label: 'Code',
|
|
38
|
+
lang: 'json',
|
|
39
|
+
modelValue: '{\n\t"name": "DOM Studio"\n}',
|
|
40
|
+
rows: 8,
|
|
41
|
+
},
|
|
42
|
+
DomColorInput: {
|
|
43
|
+
label: 'Accent',
|
|
44
|
+
modelValue: '#0ea5e9',
|
|
45
|
+
},
|
|
46
|
+
DomCombobox: {
|
|
47
|
+
placeholder: 'Search...',
|
|
48
|
+
options: [
|
|
49
|
+
{ label: 'Apple', value: 'apple' },
|
|
50
|
+
{ label: 'Banana', value: 'banana' },
|
|
51
|
+
{ label: 'Cherry', value: 'cherry' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
DomTagCombobox: {
|
|
55
|
+
label: 'Classes',
|
|
56
|
+
modelValue: ['bg-white', 'text-sm'],
|
|
57
|
+
allowCustom: true,
|
|
58
|
+
options: [
|
|
59
|
+
{ label: 'bg-white', value: 'bg-white' },
|
|
60
|
+
{ label: 'text-sm', value: 'text-sm' },
|
|
61
|
+
{ label: 'rounded-xl', value: 'rounded-xl' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
DomClassToggleInput: {
|
|
65
|
+
label: 'Classes',
|
|
66
|
+
modelValue: 'flex min-w-0 text-2xl',
|
|
67
|
+
inactiveValues: ['md:h-full'],
|
|
68
|
+
options: [
|
|
69
|
+
'flex',
|
|
70
|
+
'min-w-0',
|
|
71
|
+
'text-2xl',
|
|
72
|
+
'md:h-full',
|
|
73
|
+
'md:overflow-y-auto',
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
DomCommandPalette: {
|
|
77
|
+
commands: [
|
|
78
|
+
{ label: 'Open dashboard', value: 'dashboard' },
|
|
79
|
+
{ label: 'Create project', value: 'project' },
|
|
80
|
+
{ label: 'Invite teammate', value: 'invite' },
|
|
81
|
+
],
|
|
82
|
+
placeholder: 'Search commands...',
|
|
83
|
+
},
|
|
84
|
+
DomDropdown: {
|
|
85
|
+
items: [
|
|
86
|
+
{ label: 'First option', value: 'a' },
|
|
87
|
+
{ label: 'Second option', value: 'b' },
|
|
88
|
+
{ label: 'Third option', value: 'c' },
|
|
89
|
+
],
|
|
90
|
+
label: 'Choose...',
|
|
91
|
+
align: 'left',
|
|
92
|
+
},
|
|
93
|
+
DomJsonInput: {
|
|
94
|
+
label: 'JSON',
|
|
95
|
+
description: 'Valid JSON emits parsed data.',
|
|
96
|
+
modelValue: { name: 'DOM Studio', editable: true },
|
|
97
|
+
rows: 8,
|
|
98
|
+
},
|
|
99
|
+
DomJsonListInput: {
|
|
100
|
+
label: 'Items',
|
|
101
|
+
addLabel: '+ Add item',
|
|
102
|
+
modelValue: [
|
|
103
|
+
{ label: 'Item 1', value: 'one' },
|
|
104
|
+
{ label: 'Item 2', value: 'two' },
|
|
105
|
+
],
|
|
106
|
+
schema: [
|
|
107
|
+
{ key: 'label', label: 'Label', placeholder: 'Item label', default: (index) => `Item ${index + 1}` },
|
|
108
|
+
{ key: 'value', label: 'Value', placeholder: 'item-value', default: (index) => `item-${index + 1}` },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
DomListbox: {
|
|
112
|
+
modelValue: 'medium',
|
|
113
|
+
options: [
|
|
114
|
+
{ label: 'Small', value: 'small' },
|
|
115
|
+
{ label: 'Medium', value: 'medium' },
|
|
116
|
+
{ label: 'Large', value: 'large' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
DomMenu: {
|
|
120
|
+
items: [
|
|
121
|
+
{ label: 'Rename', value: 'rename' },
|
|
122
|
+
{ label: 'Duplicate', value: 'duplicate' },
|
|
123
|
+
{ label: 'Delete', value: 'delete', tone: 'danger' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
DomNativeSelect: {
|
|
127
|
+
label: 'Workspace',
|
|
128
|
+
options: [
|
|
129
|
+
{ label: 'Design system', value: 'design' },
|
|
130
|
+
{ label: 'Marketing site', value: 'marketing' },
|
|
131
|
+
],
|
|
132
|
+
modelValue: 'design',
|
|
133
|
+
},
|
|
134
|
+
DomNumberInput: {
|
|
135
|
+
label: 'Quantity',
|
|
136
|
+
modelValue: 0,
|
|
137
|
+
},
|
|
138
|
+
DomPasswordInput: {
|
|
139
|
+
label: 'Password',
|
|
140
|
+
description: 'Use a strong password.',
|
|
141
|
+
modelValue: 'correct-horse',
|
|
142
|
+
},
|
|
143
|
+
DomRadioGroup: {
|
|
144
|
+
label: 'Plan',
|
|
145
|
+
modelValue: 'team',
|
|
146
|
+
options: [
|
|
147
|
+
{ label: 'Solo', value: 'solo' },
|
|
148
|
+
{ label: 'Team', value: 'team' },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
DomSelectInput: {
|
|
152
|
+
label: 'Pick one',
|
|
153
|
+
options: ['One', 'Two', 'Three'],
|
|
154
|
+
modelValue: 'One',
|
|
155
|
+
},
|
|
156
|
+
DomTabs: {
|
|
157
|
+
tabs: [
|
|
158
|
+
{ key: 'overview', label: 'Overview' },
|
|
159
|
+
{ key: 'settings', label: 'Settings' },
|
|
160
|
+
],
|
|
161
|
+
modelValue: 'overview',
|
|
162
|
+
},
|
|
163
|
+
DomTextInput: {
|
|
164
|
+
label: 'Label',
|
|
165
|
+
placeholder: 'Type here...',
|
|
166
|
+
},
|
|
167
|
+
DomTextareaInput: {
|
|
168
|
+
label: 'Label',
|
|
169
|
+
placeholder: 'Tell us more...',
|
|
170
|
+
rows: 3,
|
|
171
|
+
},
|
|
172
|
+
DomToastStack: {
|
|
173
|
+
toasts: [
|
|
174
|
+
{ id: 'saved', title: 'Successfully saved', description: 'Your changes have been stored.', tone: 'success', duration: 0 },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
DomTooltip: {
|
|
178
|
+
text: 'Tooltip text',
|
|
179
|
+
},
|
|
180
|
+
DomToggle: {
|
|
181
|
+
modelValue: true,
|
|
182
|
+
label: 'Notifications',
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const SAMPLE_CHILDREN = {
|
|
187
|
+
DomButton: [{ id: null, text: 'Button' }],
|
|
188
|
+
DomCard: [
|
|
189
|
+
{
|
|
190
|
+
id: null,
|
|
191
|
+
component: 'h3',
|
|
192
|
+
props: { class: 'text-lg font-semibold tracking-tight' },
|
|
193
|
+
children: [{ id: null, text: 'Card title' }],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: null,
|
|
197
|
+
component: 'p',
|
|
198
|
+
props: { class: 'mt-2 text-sm leading-6 text-muted-fg' },
|
|
199
|
+
children: [{ id: null, text: 'Drop content into this visual surface.' }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
DomDialog: [{ id: null, text: 'Dialog content.' }],
|
|
203
|
+
DomDrawer: [{ id: null, text: 'Drawer content.' }],
|
|
204
|
+
DomPopover: [{ id: null, text: 'Popover content.' }],
|
|
205
|
+
DomTooltip: [{ id: null, text: 'Hover me' }],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Builds a palette entry from a discovered DOM Studio component record.
|
|
210
|
+
*
|
|
211
|
+
* @param {object} record Inspected component record.
|
|
212
|
+
* @returns {object} Component palette entry.
|
|
213
|
+
*/
|
|
214
|
+
function buildComponentEntry(record) {
|
|
215
|
+
const { exportName, component, section } = record;
|
|
216
|
+
const meta = record.studio || getStudioMeta(component);
|
|
217
|
+
const label = meta.label || record.label || labelFromExport(exportName);
|
|
218
|
+
const accepts = meta.accepts || inferAccepts(exportName, component);
|
|
219
|
+
const defaults = {
|
|
220
|
+
props: {
|
|
221
|
+
...inferDefaultProps(exportName, component, label),
|
|
222
|
+
...(meta.defaults?.props || {}),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const children = meta.defaults?.children || SAMPLE_CHILDREN[exportName];
|
|
226
|
+
if (children) defaults.children = children;
|
|
227
|
+
if (meta.defaults?.text) defaults.text = meta.defaults.text;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
id: meta.id || idFromExport(exportName),
|
|
231
|
+
label,
|
|
232
|
+
group: meta.group || groupFromSection(section),
|
|
233
|
+
component: markRaw(component),
|
|
234
|
+
componentName: exportName,
|
|
235
|
+
tag: component.__doc?.tag,
|
|
236
|
+
icon: meta.icon || record.icon || label.charAt(0),
|
|
237
|
+
accepts,
|
|
238
|
+
defaults,
|
|
239
|
+
hints: meta.hints || {},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Infers sensible starter props for a component dropped into the editor.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} exportName Component export name.
|
|
247
|
+
* @param {object} component Vue component object.
|
|
248
|
+
* @param {string} label Display label for label-like props.
|
|
249
|
+
* @returns {object} Default props for a new component node.
|
|
250
|
+
*/
|
|
251
|
+
function inferDefaultProps(exportName, component, label) {
|
|
252
|
+
const props = {};
|
|
253
|
+
for (const [key, def] of Object.entries(getComponentProps(component))) {
|
|
254
|
+
if (key === 'class' || key === 'modelModifiers') continue;
|
|
255
|
+
const value = propDefaultValue(def);
|
|
256
|
+
if (value !== undefined) props[key] = value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const componentProps = getComponentProps(component);
|
|
260
|
+
if ('label' in componentProps && props.label == null) props.label = label;
|
|
261
|
+
if ('placeholder' in componentProps && props.placeholder == null) props.placeholder = 'Type here...';
|
|
262
|
+
if ('description' in componentProps && props.description == null) props.description = '';
|
|
263
|
+
if ('modelValue' in componentProps && props.modelValue == null) {
|
|
264
|
+
props.modelValue = emptyValueForProp(componentProps.modelValue);
|
|
265
|
+
}
|
|
266
|
+
if ('options' in componentProps && !props.options) props.options = defaultOptions();
|
|
267
|
+
if ('items' in componentProps && !props.items) props.items = defaultItems(exportName);
|
|
268
|
+
if ('commands' in componentProps && !props.commands) props.commands = defaultCommands();
|
|
269
|
+
if ('tabs' in componentProps && !props.tabs) props.tabs = defaultTabs();
|
|
270
|
+
if ('toasts' in componentProps && !props.toasts) props.toasts = defaultToasts();
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
...props,
|
|
274
|
+
...(SAMPLE_PROPS[exportName] || {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reads a prop default without letting unsafe or failing factories break setup.
|
|
280
|
+
*
|
|
281
|
+
* @param {object} def Vue prop definition.
|
|
282
|
+
* @returns {unknown} Default value, or undefined when unavailable.
|
|
283
|
+
*/
|
|
284
|
+
function propDefaultValue(def) {
|
|
285
|
+
if (!def || typeof def !== 'object' || !('default' in def)) return undefined;
|
|
286
|
+
if (typeof def.default === 'function' && def.type !== Function) {
|
|
287
|
+
try {
|
|
288
|
+
return def.default();
|
|
289
|
+
} catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return def.default;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Creates an empty v-model value that matches a prop constructor.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} def Vue prop definition.
|
|
300
|
+
* @returns {unknown} Empty value for the prop type.
|
|
301
|
+
*/
|
|
302
|
+
function emptyValueForProp(def) {
|
|
303
|
+
const type = Array.isArray(def?.type) ? def.type : [def?.type];
|
|
304
|
+
if (type.includes(Boolean)) return false;
|
|
305
|
+
if (type.includes(Number)) return 0;
|
|
306
|
+
if (type.includes(Array)) return [];
|
|
307
|
+
if (type.includes(Object)) return {};
|
|
308
|
+
return '';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Provides generic option records for controls that need selectable choices.
|
|
313
|
+
*
|
|
314
|
+
* @returns {Array<object>} Default option records.
|
|
315
|
+
*/
|
|
316
|
+
function defaultOptions() {
|
|
317
|
+
return [
|
|
318
|
+
{ label: 'Small', value: 'small' },
|
|
319
|
+
{ label: 'Medium', value: 'medium' },
|
|
320
|
+
{ label: 'Large', value: 'large' },
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Provides generic item records for list-like DOM Studio components.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} exportName Component export name.
|
|
328
|
+
* @returns {Array<object>} Default item records.
|
|
329
|
+
*/
|
|
330
|
+
function defaultItems(exportName) {
|
|
331
|
+
if (exportName === 'DomAccordion') {
|
|
332
|
+
return [
|
|
333
|
+
{ title: 'Section A', content: 'Content for the first section.' },
|
|
334
|
+
{ title: 'Section B', content: 'Content for the second section.' },
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
return [
|
|
338
|
+
{ label: 'First option', value: 'a' },
|
|
339
|
+
{ label: 'Second option', value: 'b' },
|
|
340
|
+
{ label: 'Third option', value: 'c' },
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Provides generic command records for command-palette-like components.
|
|
346
|
+
*
|
|
347
|
+
* @returns {Array<object>} Default command records.
|
|
348
|
+
*/
|
|
349
|
+
function defaultCommands() {
|
|
350
|
+
return [
|
|
351
|
+
{ label: 'Open dashboard', value: 'dashboard' },
|
|
352
|
+
{ label: 'Create project', value: 'project' },
|
|
353
|
+
{ label: 'Invite teammate', value: 'invite' },
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Provides generic tab records for tab-like components.
|
|
359
|
+
*
|
|
360
|
+
* @returns {Array<object>} Default tab records.
|
|
361
|
+
*/
|
|
362
|
+
function defaultTabs() {
|
|
363
|
+
return [
|
|
364
|
+
{ key: 'overview', label: 'Overview' },
|
|
365
|
+
{ key: 'settings', label: 'Settings' },
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Provides a generic toast record for toast stack previews.
|
|
371
|
+
*
|
|
372
|
+
* @returns {Array<object>} Default toast records.
|
|
373
|
+
*/
|
|
374
|
+
function defaultToasts() {
|
|
375
|
+
return [
|
|
376
|
+
{ id: 'saved', title: 'Successfully saved', description: 'Your changes have been stored.', tone: 'success', duration: 0 },
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Infers whether a component can accept child nodes in the editor.
|
|
382
|
+
*
|
|
383
|
+
* @param {string} exportName Component export name.
|
|
384
|
+
* @param {object} component Vue component object.
|
|
385
|
+
* @returns {'children'|'none'} Editor child-acceptance mode.
|
|
386
|
+
*/
|
|
387
|
+
function inferAccepts(exportName, component) {
|
|
388
|
+
if (SAMPLE_CHILDREN[exportName]) return 'children';
|
|
389
|
+
const slots = component.__doc?.slots || [];
|
|
390
|
+
return slots.some((slot) => slot.name === '(default)' || slot.name === 'default') ? 'children' : 'none';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Maps a source section to an editor palette group.
|
|
395
|
+
*
|
|
396
|
+
* @param {string} section Component source section.
|
|
397
|
+
* @returns {string} Palette group label.
|
|
398
|
+
*/
|
|
399
|
+
function groupFromSection(section) {
|
|
400
|
+
return {
|
|
401
|
+
components: 'DOM Studio',
|
|
402
|
+
forms: 'Forms',
|
|
403
|
+
visual: 'Visual',
|
|
404
|
+
}[section] || 'DOM Studio';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Creates a stable palette id from a component export name.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} name Component export name.
|
|
411
|
+
* @returns {string} Palette id.
|
|
412
|
+
*/
|
|
413
|
+
function idFromExport(name) {
|
|
414
|
+
return name
|
|
415
|
+
.replace(/^El/, 'el')
|
|
416
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
417
|
+
.toLowerCase();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Creates a human-readable label from a component export name.
|
|
422
|
+
*
|
|
423
|
+
* @param {string} name Component export name.
|
|
424
|
+
* @returns {string} Display label.
|
|
425
|
+
*/
|
|
426
|
+
function labelFromExport(name) {
|
|
427
|
+
return name
|
|
428
|
+
.replace(/^El/, '')
|
|
429
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
430
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Sorts palette entries by group priority and then label.
|
|
435
|
+
*
|
|
436
|
+
* @param {object} a First palette entry.
|
|
437
|
+
* @param {object} b Second palette entry.
|
|
438
|
+
* @returns {number} Sort comparison result.
|
|
439
|
+
*/
|
|
440
|
+
function sortEntries(a, b) {
|
|
441
|
+
const groupDelta = GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group);
|
|
442
|
+
if (groupDelta) return groupDelta;
|
|
443
|
+
return a.label.localeCompare(b.label);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------------- HTML primitives ------------------------------------------
|
|
447
|
+
const htmlEntries = [
|
|
448
|
+
{
|
|
449
|
+
id: 'html-div',
|
|
450
|
+
label: 'Container',
|
|
451
|
+
group: 'HTML',
|
|
452
|
+
component: 'div',
|
|
453
|
+
icon: '▢',
|
|
454
|
+
accepts: 'children',
|
|
455
|
+
defaults: {
|
|
456
|
+
props: { class: 'flex flex-col gap-3 rounded-2xl border border-border bg-canvas p-6' },
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: 'html-row',
|
|
461
|
+
label: 'Row',
|
|
462
|
+
group: 'HTML',
|
|
463
|
+
component: 'div',
|
|
464
|
+
icon: '⇆',
|
|
465
|
+
accepts: 'children',
|
|
466
|
+
defaults: { props: { class: 'flex items-center gap-3' } },
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: 'html-heading',
|
|
470
|
+
label: 'Heading',
|
|
471
|
+
group: 'HTML',
|
|
472
|
+
component: 'h3',
|
|
473
|
+
icon: 'H',
|
|
474
|
+
accepts: 'children',
|
|
475
|
+
defaults: {
|
|
476
|
+
props: { class: 'text-2xl font-bold tracking-tight text-canvas-fg' },
|
|
477
|
+
children: [{ id: null, text: 'Heading' }],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: 'html-paragraph',
|
|
482
|
+
label: 'Paragraph',
|
|
483
|
+
group: 'HTML',
|
|
484
|
+
component: 'p',
|
|
485
|
+
icon: '¶',
|
|
486
|
+
accepts: 'children',
|
|
487
|
+
defaults: {
|
|
488
|
+
props: { class: 'text-sm leading-relaxed text-muted-fg' },
|
|
489
|
+
children: [{ id: null, text: 'Body copy lives here.' }],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: 'html-span',
|
|
494
|
+
label: 'Span',
|
|
495
|
+
group: 'HTML',
|
|
496
|
+
component: 'span',
|
|
497
|
+
icon: '⎯',
|
|
498
|
+
accepts: 'children',
|
|
499
|
+
defaults: {
|
|
500
|
+
props: {},
|
|
501
|
+
children: [{ id: null, text: 'inline' }],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: 'html-badge',
|
|
506
|
+
label: 'Badge',
|
|
507
|
+
group: 'HTML',
|
|
508
|
+
component: 'span',
|
|
509
|
+
icon: '▣',
|
|
510
|
+
accepts: 'children',
|
|
511
|
+
defaults: {
|
|
512
|
+
props: { class: 'inline-flex rounded-full bg-primary px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary-fg' },
|
|
513
|
+
children: [{ id: null, text: 'New' }],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 'html-img',
|
|
518
|
+
label: 'Image',
|
|
519
|
+
group: 'HTML',
|
|
520
|
+
component: 'img',
|
|
521
|
+
icon: '◧',
|
|
522
|
+
accepts: 'none',
|
|
523
|
+
defaults: { props: { src: 'https://placehold.co/240x160', alt: '', class: 'rounded-xl' } },
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: 'html-text',
|
|
527
|
+
label: 'Text',
|
|
528
|
+
group: 'Content',
|
|
529
|
+
component: null,
|
|
530
|
+
icon: 'T',
|
|
531
|
+
accepts: 'none',
|
|
532
|
+
defaults: { text: 'Edit me' },
|
|
533
|
+
},
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
const discoveredEntries = getRuntimeComponentRecords()
|
|
537
|
+
.map(inspectComponentRecord)
|
|
538
|
+
.filter((record) => !record.studioHidden)
|
|
539
|
+
.map(buildComponentEntry)
|
|
540
|
+
.sort(sortEntries);
|
|
541
|
+
const entries = [...discoveredEntries, ...htmlEntries];
|
|
542
|
+
|
|
543
|
+
export const componentRegistry = entries;
|
|
544
|
+
export const groupedRegistry = entries.reduce((acc, entry) => {
|
|
545
|
+
(acc[entry.group] ||= []).push(entry);
|
|
546
|
+
return acc;
|
|
547
|
+
}, {});
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Finds the palette entry for a rendered component, component name, or node type.
|
|
551
|
+
*
|
|
552
|
+
* @param {object|string|null} component Component object, component name, native tag, or null text node.
|
|
553
|
+
* @returns {object|undefined|null} Matching palette entry.
|
|
554
|
+
*/
|
|
555
|
+
export function lookupEntry(component) {
|
|
556
|
+
if (component == null) return entries.find((e) => e.id === 'html-text');
|
|
557
|
+
const direct = entries.find((e) => e.component === component);
|
|
558
|
+
if (direct) return direct;
|
|
559
|
+
if (typeof component === 'string') return lookupByName(component);
|
|
560
|
+
return lookupByName(component?.name || component?.__name || component?.__doc?.tag || component?.__doc?.name);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Finds a palette entry by its stable type id.
|
|
565
|
+
*
|
|
566
|
+
* @param {string} typeId Palette type id.
|
|
567
|
+
* @returns {object|undefined} Matching palette entry.
|
|
568
|
+
*/
|
|
569
|
+
export function lookupById(typeId) {
|
|
570
|
+
return entries.find((e) => e.id === typeId);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Finds a palette entry by any normalized component or display name.
|
|
575
|
+
*
|
|
576
|
+
* @param {string} name Component name, tag, id, or display label.
|
|
577
|
+
* @returns {object|null} Matching palette entry.
|
|
578
|
+
*/
|
|
579
|
+
export function lookupByName(name) {
|
|
580
|
+
if (!name) return null;
|
|
581
|
+
const normalized = normalizeName(name);
|
|
582
|
+
return entries.find((entry) => {
|
|
583
|
+
const names = [
|
|
584
|
+
entry.id,
|
|
585
|
+
entry.label,
|
|
586
|
+
entry.componentName,
|
|
587
|
+
entry.tag,
|
|
588
|
+
typeof entry.component === 'string' ? entry.component : null,
|
|
589
|
+
entry.component?.name,
|
|
590
|
+
entry.component?.__name,
|
|
591
|
+
entry.component?.__doc?.tag,
|
|
592
|
+
entry.component?.__doc?.name,
|
|
593
|
+
];
|
|
594
|
+
return names.some((candidate) => normalizeName(candidate) === normalized);
|
|
595
|
+
}) || null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Normalizes component names, tags, and labels for fuzzy registry lookup.
|
|
600
|
+
*
|
|
601
|
+
* @param {string} name Source name.
|
|
602
|
+
* @returns {string} Normalized lookup key.
|
|
603
|
+
*/
|
|
604
|
+
function normalizeName(name) {
|
|
605
|
+
return String(name || '')
|
|
606
|
+
.replace(/[<>]/g, '')
|
|
607
|
+
.replace(/^El/, 'el')
|
|
608
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
609
|
+
.toLowerCase()
|
|
610
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
611
|
+
.replace(/^-|-$/g, '');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Determines whether an inspector node is a pure text node.
|
|
616
|
+
*
|
|
617
|
+
* @param {object} node Inspector node.
|
|
618
|
+
* @returns {boolean} True when the node carries text content instead of a component.
|
|
619
|
+
*/
|
|
620
|
+
export function isTextNode(node) {
|
|
621
|
+
return !!(node && node.component == null && node.text != null);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Determines whether an inspector node can contain child nodes.
|
|
626
|
+
*
|
|
627
|
+
* @param {object} node Inspector node.
|
|
628
|
+
* @returns {boolean} True when children can be inserted.
|
|
629
|
+
*/
|
|
630
|
+
export function canHaveChildren(node) {
|
|
631
|
+
if (!node) return false;
|
|
632
|
+
if (isTextNode(node)) return false;
|
|
633
|
+
const entry = lookupEntry(node.component);
|
|
634
|
+
if (entry) return entry.accepts !== 'none';
|
|
635
|
+
if (typeof node.component === 'string') {
|
|
636
|
+
return !['area','base','br','col','embed','hr','img','input','link','meta','source','track','wbr'].includes(node.component);
|
|
637
|
+
}
|
|
638
|
+
return false;
|
|
639
|
+
}
|