@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,134 @@
|
|
|
1
|
+
// Round-trippable serialization for the inspector tree.
|
|
2
|
+
//
|
|
3
|
+
// Spec nodes carry a live Vue component reference, which can't go into JSON.
|
|
4
|
+
// We replace the reference with the matching `typeId` from the component
|
|
5
|
+
// registry, and resolve it back on load. The result is a plain JSON object
|
|
6
|
+
// you can save to a DB / send to a server and rehydrate later via DomRenderer.
|
|
7
|
+
|
|
8
|
+
import { componentRegistry, isTextNode } from './componentRegistry.js';
|
|
9
|
+
import { makeReactiveSpec, uid } from './useInspector.js';
|
|
10
|
+
|
|
11
|
+
function lookupByComponent(component) {
|
|
12
|
+
return componentRegistry.find((e) => e.component === component);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function lookupById(typeId) {
|
|
16
|
+
return componentRegistry.find((e) => e.id === typeId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function serializeTree(node) {
|
|
20
|
+
if (!node) return null;
|
|
21
|
+
if (isTextNode(node)) {
|
|
22
|
+
return { id: node.id, type: 'html-text', text: node.text };
|
|
23
|
+
}
|
|
24
|
+
const typeId = node.typeId || lookupByComponent(node.component)?.id;
|
|
25
|
+
const out = {
|
|
26
|
+
id: node.id,
|
|
27
|
+
type: typeId || (typeof node.component === 'string' ? node.component : 'unknown'),
|
|
28
|
+
props: { ...(node.props || {}) },
|
|
29
|
+
};
|
|
30
|
+
if (node.children?.length) out.children = node.children.map(serializeTree);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function deserializeTree(data) {
|
|
35
|
+
if (!data) return null;
|
|
36
|
+
return makeReactiveSpec(deserializeNode(data));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deserializeNode(data) {
|
|
40
|
+
// Pure text node.
|
|
41
|
+
if (data.type === 'html-text' || (data.text != null && !data.type)) {
|
|
42
|
+
return {
|
|
43
|
+
id: data.id || uid('t'),
|
|
44
|
+
text: data.text ?? '',
|
|
45
|
+
typeId: 'html-text',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const entry = lookupById(data.type);
|
|
49
|
+
if (!entry && !data.type) throw new Error('Node is missing a `type` field.');
|
|
50
|
+
return {
|
|
51
|
+
id: data.id || uid('n'),
|
|
52
|
+
label: entry?.label || data.type,
|
|
53
|
+
component: entry?.component ?? data.type, // fall back to raw tag string
|
|
54
|
+
typeId: entry?.id || data.type,
|
|
55
|
+
props: data.props || {},
|
|
56
|
+
children: (data.children || []).map(deserializeNode),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// HTML output — formatted, copy-paste-ready
|
|
62
|
+
|
|
63
|
+
const VOID = new Set([
|
|
64
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
65
|
+
'link', 'meta', 'source', 'track', 'wbr',
|
|
66
|
+
]);
|
|
67
|
+
const STRIP_ATTRS = new Set([
|
|
68
|
+
'data-node-id', 'data-selected', 'data-drop-target-id', 'data-v-app',
|
|
69
|
+
]);
|
|
70
|
+
const STRIP_CLASSES = new Set([
|
|
71
|
+
'dom-stage-node', 'dom-text-node',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export function elementToHtml(el, depth = 0) {
|
|
75
|
+
if (!el) return '';
|
|
76
|
+
const indent = '\t'.repeat(depth);
|
|
77
|
+
|
|
78
|
+
if (el.nodeType === 3 /* text */) {
|
|
79
|
+
const t = el.textContent.replace(/\s+/g, ' ').trim();
|
|
80
|
+
return t ? indent + escapeText(t) : '';
|
|
81
|
+
}
|
|
82
|
+
if (el.nodeType !== 1) return '';
|
|
83
|
+
|
|
84
|
+
// Unwrap the dom-text-node span — emit its text content directly.
|
|
85
|
+
if (el.classList?.contains('dom-text-node')) {
|
|
86
|
+
const t = el.textContent.replace(/\s+/g, ' ').trim();
|
|
87
|
+
return t ? indent + escapeText(t) : '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tag = el.tagName.toLowerCase();
|
|
91
|
+
const attrs = [];
|
|
92
|
+
for (const a of el.attributes) {
|
|
93
|
+
if (STRIP_ATTRS.has(a.name)) continue;
|
|
94
|
+
if (a.name.startsWith('data-v-')) continue;
|
|
95
|
+
if (a.name === 'class') {
|
|
96
|
+
const cls = a.value
|
|
97
|
+
.split(/\s+/)
|
|
98
|
+
.filter((c) => c && !STRIP_CLASSES.has(c))
|
|
99
|
+
.join(' ');
|
|
100
|
+
if (cls) attrs.push(`class="${escapeAttr(cls)}"`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (a.value === '') attrs.push(a.name);
|
|
104
|
+
else attrs.push(`${a.name}="${escapeAttr(a.value)}"`);
|
|
105
|
+
}
|
|
106
|
+
const open = `<${tag}${attrs.length ? ' ' + attrs.join(' ') : ''}`;
|
|
107
|
+
if (VOID.has(tag)) return indent + open + ' />';
|
|
108
|
+
|
|
109
|
+
const parts = [];
|
|
110
|
+
for (const c of el.childNodes) {
|
|
111
|
+
const part = elementToHtml(c, depth + 1);
|
|
112
|
+
if (part) parts.push(part);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (parts.length === 0) return indent + open + `></${tag}>`;
|
|
116
|
+
if (
|
|
117
|
+
el.childNodes.length === 1 &&
|
|
118
|
+
el.firstChild.nodeType === 3 &&
|
|
119
|
+
parts[0].trim().length < 60
|
|
120
|
+
) {
|
|
121
|
+
return indent + open + '>' + parts[0].trim() + `</${tag}>`;
|
|
122
|
+
}
|
|
123
|
+
return indent + open + '>\n' + parts.join('\n') + '\n' + indent + `</${tag}>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function escapeAttr(s) { return String(s).replace(/"/g, '"'); }
|
|
127
|
+
function escapeText(s) { return String(s).replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c])); }
|
|
128
|
+
|
|
129
|
+
export function stageToHtml(stageEl) {
|
|
130
|
+
if (!stageEl) return '';
|
|
131
|
+
const root = stageEl.querySelector('[data-node-id]');
|
|
132
|
+
if (!root) return '';
|
|
133
|
+
return elementToHtml(root, 0);
|
|
134
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { provide, inject, reactive } from 'vue';
|
|
2
|
+
import { lookupEntry, lookupById, isTextNode } from './componentRegistry.js';
|
|
3
|
+
|
|
4
|
+
const KEY = Symbol('domStudio.inspector');
|
|
5
|
+
|
|
6
|
+
let _uid = 0;
|
|
7
|
+
export function uid(prefix = 'n') {
|
|
8
|
+
_uid += 1;
|
|
9
|
+
return `${prefix}-${_uid.toString(36)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Schema inference
|
|
14
|
+
//
|
|
15
|
+
// Inline hints live on the component's prop definition under the `_edit` key:
|
|
16
|
+
//
|
|
17
|
+
// const props = defineProps({
|
|
18
|
+
// items: {
|
|
19
|
+
// type: Array,
|
|
20
|
+
// required: true,
|
|
21
|
+
// _edit: { component: 'DomJsonListInput', props: { label: 'Items', compact: true } },
|
|
22
|
+
// },
|
|
23
|
+
// });
|
|
24
|
+
//
|
|
25
|
+
// The `component` is a component name (string) so production output (e.g.
|
|
26
|
+
// DomRenderer) never pulls form components into the bundle. `props` are passed
|
|
27
|
+
// to that editor component, mirroring Vue's { component, props } shape.
|
|
28
|
+
|
|
29
|
+
function componentProps(component) {
|
|
30
|
+
if (!component || typeof component === 'string') return {};
|
|
31
|
+
const p = component.props;
|
|
32
|
+
if (!p) return {};
|
|
33
|
+
if (Array.isArray(p)) return Object.fromEntries(p.map((k) => [k, {}]));
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Default editor for a Vue prop type when no `_edit` hint is given.
|
|
38
|
+
function defaultEditorForProp(def) {
|
|
39
|
+
if (!def) return 'DomTextInput';
|
|
40
|
+
const t = def?.type ?? def;
|
|
41
|
+
const types = Array.isArray(t) ? t : [t];
|
|
42
|
+
if (types.includes(Boolean)) return 'DomToggle';
|
|
43
|
+
if (types.includes(String)) return 'DomTextInput';
|
|
44
|
+
if (types.includes(Number)) return 'DomNumberInput';
|
|
45
|
+
if (types.includes(Array) || types.includes(Object) || types.includes(Function)) return null;
|
|
46
|
+
return 'DomTextInput';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveComponent(hint, props, def) {
|
|
50
|
+
if (hint.component) return hint.component;
|
|
51
|
+
if (hint.options || hint.enum || props.options || props.enum) return 'DomSelectInput';
|
|
52
|
+
return defaultEditorForProp(def);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normaliseEdit(hint = {}, def) {
|
|
56
|
+
const props = { ...(hint.props || {}) };
|
|
57
|
+
for (const [key, value] of Object.entries(hint)) {
|
|
58
|
+
if (['component', 'props', 'label', 'description', 'group'].includes(key)) continue;
|
|
59
|
+
if (!(key in props)) props[key] = value;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
component: resolveComponent(hint, props, def),
|
|
63
|
+
label: hint.label ?? props.label,
|
|
64
|
+
description: hint.description ?? props.description,
|
|
65
|
+
group: hint.group ?? props.group,
|
|
66
|
+
props,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function inferSchema(node) {
|
|
71
|
+
if (isTextNode(node)) {
|
|
72
|
+
return [{ key: 'text', target: 'text', component: 'DomTextareaInput', label: 'Text', props: { rows: 3 } }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fields = [];
|
|
76
|
+
const entry = lookupEntry(node.component);
|
|
77
|
+
const hints = entry?.hints || {};
|
|
78
|
+
|
|
79
|
+
const propDefs = componentProps(node.component);
|
|
80
|
+
for (const [key, def] of Object.entries(propDefs)) {
|
|
81
|
+
if (key === 'class' || key === 'modelModifiers' || key.startsWith('_')) continue;
|
|
82
|
+
|
|
83
|
+
// Per-prop hints — `_edit` on the prop definition is the canonical
|
|
84
|
+
// place (lives with the component, shows up in source). Registry
|
|
85
|
+
// hints stack on top so the studio can override per-component.
|
|
86
|
+
const inlineEdit = def && typeof def === 'object' && def._edit;
|
|
87
|
+
const merged = { ...(inlineEdit || {}), ...(hints[key] || {}) };
|
|
88
|
+
const edit = normaliseEdit(merged, def);
|
|
89
|
+
if (!edit.component) continue;
|
|
90
|
+
|
|
91
|
+
fields.push({
|
|
92
|
+
key,
|
|
93
|
+
label: edit.label || prettify(key),
|
|
94
|
+
description: edit.description,
|
|
95
|
+
group: edit.group,
|
|
96
|
+
component: edit.component,
|
|
97
|
+
props: edit.props,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!fields.some((f) => f.key === 'class')) {
|
|
102
|
+
fields.push({
|
|
103
|
+
key: 'class',
|
|
104
|
+
label: 'Tailwind classes',
|
|
105
|
+
component: 'DomTextareaInput',
|
|
106
|
+
props: { rows: 3 },
|
|
107
|
+
description: 'Any utility classes — applied to the rendered element.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return fields;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function prettify(s) {
|
|
115
|
+
return s
|
|
116
|
+
.replace(/([A-Z])/g, ' $1')
|
|
117
|
+
.replace(/^./, (c) => c.toUpperCase())
|
|
118
|
+
.replace(/Model Value/, 'Value');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Reactive tree
|
|
123
|
+
|
|
124
|
+
export function makeReactiveSpec(node) {
|
|
125
|
+
if (!node) return null;
|
|
126
|
+
|
|
127
|
+
// Text node — { id, text, [typeId] }
|
|
128
|
+
if (node.component == null && node.text != null) {
|
|
129
|
+
const next = reactive({
|
|
130
|
+
id: node.id || uid('t'),
|
|
131
|
+
label: node.label || 'Text',
|
|
132
|
+
typeId: 'html-text',
|
|
133
|
+
component: null,
|
|
134
|
+
props: reactive({}),
|
|
135
|
+
text: node.text,
|
|
136
|
+
schema: null,
|
|
137
|
+
children: [],
|
|
138
|
+
});
|
|
139
|
+
next.schema = node.schema ?? inferSchema(next);
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Element node. Convert `text` shorthand into a single text child.
|
|
144
|
+
let rawChildren = node.children;
|
|
145
|
+
if ((!rawChildren || rawChildren.length === 0) && node.text != null) {
|
|
146
|
+
rawChildren = [{ text: node.text }];
|
|
147
|
+
}
|
|
148
|
+
rawChildren = rawChildren || [];
|
|
149
|
+
|
|
150
|
+
const typeId = node.typeId || lookupEntry(node.component)?.id || null;
|
|
151
|
+
const next = reactive({
|
|
152
|
+
id: node.id || uid(),
|
|
153
|
+
label: node.label || lookupEntry(node.component)?.label || 'Node',
|
|
154
|
+
typeId,
|
|
155
|
+
component: node.component,
|
|
156
|
+
props: reactive({ ...(node.props || {}) }),
|
|
157
|
+
text: null,
|
|
158
|
+
schema: null,
|
|
159
|
+
children: rawChildren.map(makeReactiveSpec),
|
|
160
|
+
});
|
|
161
|
+
next.schema = node.schema ?? inferSchema(next);
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Build a new reactive spec from a palette entry.
|
|
166
|
+
export function specFromEntry(entry) {
|
|
167
|
+
const d = entry.defaults || {};
|
|
168
|
+
return makeReactiveSpec({
|
|
169
|
+
id: uid(entry.id),
|
|
170
|
+
label: entry.label,
|
|
171
|
+
typeId: entry.id,
|
|
172
|
+
component: entry.component,
|
|
173
|
+
props: { ...(d.props || {}) },
|
|
174
|
+
text: d.text,
|
|
175
|
+
children: (d.children || []).map((c) => ({ ...c })),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Tree operations
|
|
181
|
+
|
|
182
|
+
export function flattenTree(node, depth = 0, out = []) {
|
|
183
|
+
if (!node) return out;
|
|
184
|
+
out.push({ node, depth });
|
|
185
|
+
(node.children || []).forEach((c) => flattenTree(c, depth + 1, out));
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function findNode(tree, id) {
|
|
190
|
+
if (!tree) return null;
|
|
191
|
+
if (tree.id === id) return tree;
|
|
192
|
+
for (const c of tree.children || []) {
|
|
193
|
+
const f = findNode(c, id);
|
|
194
|
+
if (f) return f;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function findParent(tree, id) {
|
|
200
|
+
for (const c of tree.children || []) {
|
|
201
|
+
if (c.id === id) return tree;
|
|
202
|
+
const p = findParent(c, id);
|
|
203
|
+
if (p) return p;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function removeNode(tree, id) {
|
|
209
|
+
const parent = findParent(tree, id);
|
|
210
|
+
if (!parent) return false;
|
|
211
|
+
parent.children = parent.children.filter((c) => c.id !== id);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function moveNode(tree, id, dir) {
|
|
216
|
+
const parent = findParent(tree, id);
|
|
217
|
+
if (!parent) return;
|
|
218
|
+
const i = parent.children.findIndex((c) => c.id === id);
|
|
219
|
+
const j = i + dir;
|
|
220
|
+
if (j < 0 || j >= parent.children.length) return;
|
|
221
|
+
const arr = parent.children;
|
|
222
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
223
|
+
parent.children = [...arr];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function insertChild(parent, child, index) {
|
|
227
|
+
if (!parent) return;
|
|
228
|
+
const i = index == null ? parent.children.length : index;
|
|
229
|
+
parent.children = [
|
|
230
|
+
...parent.children.slice(0, i),
|
|
231
|
+
child,
|
|
232
|
+
...parent.children.slice(i),
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function duplicateNode(tree, id) {
|
|
237
|
+
const parent = findParent(tree, id);
|
|
238
|
+
const node = findNode(tree, id);
|
|
239
|
+
if (!parent || !node) return null;
|
|
240
|
+
const copy = makeReactiveSpec(JSON.parse(JSON.stringify(stripNonSerializable(node))));
|
|
241
|
+
restoreComponentRefs(copy, node);
|
|
242
|
+
parent.children = [...parent.children, copy];
|
|
243
|
+
return copy;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stripNonSerializable(node) {
|
|
247
|
+
return {
|
|
248
|
+
id: uid(node.id),
|
|
249
|
+
label: node.label,
|
|
250
|
+
props: node.props,
|
|
251
|
+
text: node.text,
|
|
252
|
+
children: (node.children || []).map(stripNonSerializable),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function restoreComponentRefs(target, source) {
|
|
257
|
+
target.component = source.component;
|
|
258
|
+
target.typeId = source.typeId;
|
|
259
|
+
(target.children || []).forEach((c, i) => {
|
|
260
|
+
const src = source.children?.[i];
|
|
261
|
+
if (src) restoreComponentRefs(c, src);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Composition API helpers
|
|
267
|
+
|
|
268
|
+
export function provideInspector(initial = {}) {
|
|
269
|
+
const state = reactive({
|
|
270
|
+
selectedId: initial.selectedId ?? null,
|
|
271
|
+
// `pickMode` on (the default) → clicks select; hovers show an outline.
|
|
272
|
+
// off → clicks pass through to the underlying component (open the
|
|
273
|
+
// dropdown, focus the input, etc.) so authors can poke at real behaviour.
|
|
274
|
+
pickMode: initial.pickMode ?? true,
|
|
275
|
+
dragEntry: null,
|
|
276
|
+
});
|
|
277
|
+
provide(KEY, state);
|
|
278
|
+
return state;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function useInspector() {
|
|
282
|
+
return inject(KEY, null);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function getFieldValue(node, field) {
|
|
286
|
+
if (field.target === 'text') return node.text;
|
|
287
|
+
return node.props[field.key];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function setFieldValue(node, field, value) {
|
|
291
|
+
if (field.target === 'text') node.text = value;
|
|
292
|
+
else node.props[field.key] = value;
|
|
293
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
items: {
|
|
6
|
+
type: Array,
|
|
7
|
+
required: true, // [{ title, content }]
|
|
8
|
+
_edit: { description: 'Each entry renders one collapsible section.' },
|
|
9
|
+
},
|
|
10
|
+
multiple: {
|
|
11
|
+
type: Boolean,
|
|
12
|
+
default: false,
|
|
13
|
+
_edit: { description: 'Allow multiple accordion tabs to be open at once.' },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
onMounted(async () => { await import('../../lib/headless/accordion.js'); });
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<dom-accordion :multiple="multiple || null" class="block divide-y divide-border rounded-2xl border border-border bg-canvas w-full">
|
|
22
|
+
<dom-accordion-item v-for="(item, i) in items" :key="i" class="block">
|
|
23
|
+
<button
|
|
24
|
+
slot="header"
|
|
25
|
+
class="flex w-full items-center justify-between px-5 py-4 text-left text-sm font-medium text-canvas-fg outline-none transition hover:bg-secondary focus-visible:bg-secondary"
|
|
26
|
+
>
|
|
27
|
+
<span>{{ item.title }}</span>
|
|
28
|
+
<svg viewBox="0 0 20 20" class="size-4 transition aria-expanded-rotate" fill="none">
|
|
29
|
+
<path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
<div slot="content" class="px-5 pb-5 text-sm leading-relaxed text-muted-fg">{{ item.content }}</div>
|
|
33
|
+
</dom-accordion-item>
|
|
34
|
+
</dom-accordion>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style>
|
|
38
|
+
dom-accordion-item[open] [slot="header"] svg { transform: rotate(180deg); }
|
|
39
|
+
</style>
|