@growgroup/visual-editor 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 +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Renderer Utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions to render ComponentElement structures to actual DOM elements.
|
|
5
|
+
* Supports rendering master components, variants, and instances with overrides.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ComponentElement,
|
|
10
|
+
ComponentInstance,
|
|
11
|
+
ComponentOverride,
|
|
12
|
+
MasterComponent,
|
|
13
|
+
OverrideType,
|
|
14
|
+
OverrideImageValue,
|
|
15
|
+
} from '../../types/editor-components';
|
|
16
|
+
import { generateElementId } from './dom-utils';
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Constants
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Data attributes used for component instance tracking
|
|
24
|
+
*/
|
|
25
|
+
const DATA_ATTRIBUTES = {
|
|
26
|
+
ELEMENT_ID: 'data-element-id',
|
|
27
|
+
COMPONENT_INSTANCE: 'data-component-instance',
|
|
28
|
+
COMPONENT_MASTER: 'data-component-master',
|
|
29
|
+
VARIANT_ID: 'data-variant-id',
|
|
30
|
+
EDITABLE: 'data-editable',
|
|
31
|
+
OVERRIDABLE: 'data-overridable',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Self-closing HTML tags that should not have children
|
|
36
|
+
*/
|
|
37
|
+
const SELF_CLOSING_TAGS = new Set([
|
|
38
|
+
'img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base',
|
|
39
|
+
'col', 'embed', 'param', 'source', 'track', 'wbr',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// ============================================================
|
|
43
|
+
// Core Rendering Functions
|
|
44
|
+
// ============================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a DOM element from a ComponentElement structure.
|
|
48
|
+
* Recursively creates all child elements.
|
|
49
|
+
*
|
|
50
|
+
* @param element - The ComponentElement to render
|
|
51
|
+
* @param doc - The Document to create elements in (defaults to window.document)
|
|
52
|
+
* @returns The created HTMLElement
|
|
53
|
+
*/
|
|
54
|
+
export function createDOMElement(
|
|
55
|
+
element: ComponentElement,
|
|
56
|
+
doc: Document = document
|
|
57
|
+
): HTMLElement {
|
|
58
|
+
// Debug logging
|
|
59
|
+
console.log('[createDOMElement] Creating:', {
|
|
60
|
+
tagName: element.tagName,
|
|
61
|
+
className: element.className,
|
|
62
|
+
hasTextContent: !!element.textContent,
|
|
63
|
+
textContentPreview: element.textContent?.substring(0, 50),
|
|
64
|
+
childrenCount: element.children.length,
|
|
65
|
+
stylesCount: Object.keys(element.styles).length,
|
|
66
|
+
attributesCount: Object.keys(element.attributes).length,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create the base element
|
|
70
|
+
// For SVG elements, use createElementNS
|
|
71
|
+
const SVG_TAGS = new Set(['svg', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse', 'g', 'defs', 'use', 'text', 'tspan', 'image', 'clipPath', 'mask', 'pattern', 'linearGradient', 'radialGradient', 'stop', 'filter', 'feBlend', 'feColorMatrix', 'feGaussianBlur', 'feOffset', 'feMerge', 'feMergeNode']);
|
|
72
|
+
const isSvgElement = SVG_TAGS.has(element.tagName.toLowerCase());
|
|
73
|
+
|
|
74
|
+
const el = isSvgElement
|
|
75
|
+
? doc.createElementNS('http://www.w3.org/2000/svg', element.tagName) as unknown as HTMLElement
|
|
76
|
+
: doc.createElement(element.tagName);
|
|
77
|
+
|
|
78
|
+
// Set element ID
|
|
79
|
+
el.setAttribute(DATA_ATTRIBUTES.ELEMENT_ID, element.id);
|
|
80
|
+
|
|
81
|
+
// Apply class names
|
|
82
|
+
if (element.className) {
|
|
83
|
+
// For SVG elements, use setAttribute instead of className property
|
|
84
|
+
if (isSvgElement) {
|
|
85
|
+
el.setAttribute('class', element.className);
|
|
86
|
+
} else {
|
|
87
|
+
el.className = element.className;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Apply HTML attributes
|
|
92
|
+
for (const [key, value] of Object.entries(element.attributes)) {
|
|
93
|
+
if (value !== undefined && value !== null) {
|
|
94
|
+
el.setAttribute(key, value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply inline styles
|
|
99
|
+
for (const [property, value] of Object.entries(element.styles)) {
|
|
100
|
+
if (value !== undefined && value !== null) {
|
|
101
|
+
// Convert camelCase to kebab-case for CSS property names
|
|
102
|
+
const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
103
|
+
el.style.setProperty(cssProperty, value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle content: innerHTML takes precedence, then textContent (if set), then children
|
|
108
|
+
// Note: textContent being set explicitly (e.g., via override) takes precedence over children
|
|
109
|
+
if (!SELF_CLOSING_TAGS.has(element.tagName.toLowerCase())) {
|
|
110
|
+
if (element.innerHTML !== undefined) {
|
|
111
|
+
// Use innerHTML for mixed content (text + elements)
|
|
112
|
+
el.innerHTML = element.innerHTML;
|
|
113
|
+
console.log('[createDOMElement] Using innerHTML for mixed content');
|
|
114
|
+
} else if (element.textContent !== undefined) {
|
|
115
|
+
// textContent is explicitly set (possibly via override) - use it
|
|
116
|
+
// This takes precedence over children, which is important for text overrides
|
|
117
|
+
el.textContent = element.textContent;
|
|
118
|
+
if (element.children.length > 0) {
|
|
119
|
+
console.log('[createDOMElement] textContent override replacing children structure');
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Recursively create and append children
|
|
123
|
+
for (const child of element.children) {
|
|
124
|
+
const childEl = createDOMElement(child, doc);
|
|
125
|
+
el.appendChild(childEl);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Set display name as data attribute if provided
|
|
131
|
+
if (element.displayName) {
|
|
132
|
+
el.setAttribute('data-display-name', element.displayName);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Set locked/hidden states
|
|
136
|
+
if (element.locked) {
|
|
137
|
+
el.setAttribute('data-locked', 'true');
|
|
138
|
+
}
|
|
139
|
+
if (element.hidden) {
|
|
140
|
+
el.style.display = 'none';
|
|
141
|
+
el.setAttribute('data-hidden', 'true');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Mark as editable
|
|
145
|
+
el.setAttribute(DATA_ATTRIBUTES.EDITABLE, 'true');
|
|
146
|
+
|
|
147
|
+
// Store overridable properties as data attribute
|
|
148
|
+
if (element.overridable) {
|
|
149
|
+
const overridableTypes = Object.entries(element.overridable)
|
|
150
|
+
.filter(([, enabled]) => enabled)
|
|
151
|
+
.map(([type]) => type)
|
|
152
|
+
.join(',');
|
|
153
|
+
if (overridableTypes) {
|
|
154
|
+
el.setAttribute(DATA_ATTRIBUTES.OVERRIDABLE, overridableTypes);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log('[createDOMElement] Created element:', el.tagName, 'with innerHTML length:', el.innerHTML.length);
|
|
159
|
+
|
|
160
|
+
return el;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render a resolved ComponentElement to an HTMLElement.
|
|
165
|
+
* This is the main entry point for rendering a component structure.
|
|
166
|
+
*
|
|
167
|
+
* @param element - The ComponentElement structure to render
|
|
168
|
+
* @param doc - The Document to create elements in (defaults to window.document)
|
|
169
|
+
* @returns The rendered HTMLElement
|
|
170
|
+
*/
|
|
171
|
+
export function renderComponentElement(
|
|
172
|
+
element: ComponentElement,
|
|
173
|
+
doc: Document = document
|
|
174
|
+
): HTMLElement {
|
|
175
|
+
return createDOMElement(element, doc);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Apply instance tracking data attributes to an element.
|
|
180
|
+
*
|
|
181
|
+
* @param el - The HTMLElement to modify
|
|
182
|
+
* @param instance - The ComponentInstance being rendered
|
|
183
|
+
* @param masterId - The master component ID
|
|
184
|
+
*/
|
|
185
|
+
function applyInstanceAttributes(
|
|
186
|
+
el: HTMLElement,
|
|
187
|
+
instance: ComponentInstance,
|
|
188
|
+
masterId: string
|
|
189
|
+
): void {
|
|
190
|
+
el.setAttribute(DATA_ATTRIBUTES.COMPONENT_INSTANCE, instance.id);
|
|
191
|
+
el.setAttribute(DATA_ATTRIBUTES.COMPONENT_MASTER, masterId);
|
|
192
|
+
el.setAttribute(DATA_ATTRIBUTES.VARIANT_ID, instance.variantId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Find an element in a ComponentElement tree by its ID.
|
|
197
|
+
*
|
|
198
|
+
* @param root - The root ComponentElement to search in
|
|
199
|
+
* @param elementId - The ID to search for
|
|
200
|
+
* @returns The found ComponentElement or null
|
|
201
|
+
*/
|
|
202
|
+
function findElementById(
|
|
203
|
+
root: ComponentElement,
|
|
204
|
+
elementId: string
|
|
205
|
+
): ComponentElement | null {
|
|
206
|
+
if (root.id === elementId) {
|
|
207
|
+
return root;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const child of root.children) {
|
|
211
|
+
const found = findElementById(child, elementId);
|
|
212
|
+
if (found) return found;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Find an element in a ComponentElement tree by path.
|
|
220
|
+
*
|
|
221
|
+
* @param root - The root ComponentElement to search in
|
|
222
|
+
* @param path - The path string (e.g., "children.0.children.1" or "root")
|
|
223
|
+
* @returns The found ComponentElement or null
|
|
224
|
+
*/
|
|
225
|
+
function findElementByPath(
|
|
226
|
+
root: ComponentElement,
|
|
227
|
+
path: string
|
|
228
|
+
): ComponentElement | null {
|
|
229
|
+
if (path === 'root') {
|
|
230
|
+
return root;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const segments = path.split('.');
|
|
234
|
+
let current: ComponentElement | undefined = root;
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < segments.length; i += 2) {
|
|
237
|
+
if (segments[i] !== 'children' || !current) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const index = parseInt(segments[i + 1], 10);
|
|
241
|
+
if (isNaN(index) || index < 0 || index >= current.children.length) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
current = current.children[index];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return current || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Deep clone a ComponentElement structure, optionally preserving IDs.
|
|
252
|
+
*
|
|
253
|
+
* @param element - The element to clone
|
|
254
|
+
* @param preserveIds - If true, preserve original IDs (for override resolution). If false, generate new IDs.
|
|
255
|
+
* @param idPrefix - Optional prefix for generated IDs (only used when preserveIds is false)
|
|
256
|
+
* @returns A deep clone of the element
|
|
257
|
+
*/
|
|
258
|
+
function deepCloneElement(
|
|
259
|
+
element: ComponentElement,
|
|
260
|
+
preserveIds: boolean = false,
|
|
261
|
+
idPrefix: string = 'inst'
|
|
262
|
+
): ComponentElement {
|
|
263
|
+
return {
|
|
264
|
+
...element,
|
|
265
|
+
id: preserveIds ? element.id : generateElementId(idPrefix),
|
|
266
|
+
attributes: { ...element.attributes },
|
|
267
|
+
styles: { ...element.styles },
|
|
268
|
+
overridable: { ...element.overridable },
|
|
269
|
+
children: element.children.map((child) => deepCloneElement(child, preserveIds, idPrefix)),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Apply a single override to a ComponentElement.
|
|
275
|
+
*
|
|
276
|
+
* @param element - The element to modify (will be mutated)
|
|
277
|
+
* @param override - The override to apply
|
|
278
|
+
*/
|
|
279
|
+
function applyOverride(
|
|
280
|
+
element: ComponentElement,
|
|
281
|
+
override: ComponentOverride
|
|
282
|
+
): void {
|
|
283
|
+
switch (override.type) {
|
|
284
|
+
case 'text':
|
|
285
|
+
if (typeof override.value === 'string') {
|
|
286
|
+
element.textContent = override.value;
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case 'fill':
|
|
291
|
+
if (typeof override.value === 'string') {
|
|
292
|
+
element.styles.backgroundColor = override.value;
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'stroke':
|
|
297
|
+
if (typeof override.value === 'string') {
|
|
298
|
+
element.styles.borderColor = override.value;
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'visibility':
|
|
303
|
+
if (typeof override.value === 'boolean') {
|
|
304
|
+
element.hidden = !override.value;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
case 'image':
|
|
309
|
+
if (isImageOverrideValue(override.value)) {
|
|
310
|
+
element.attributes.src = override.value.url;
|
|
311
|
+
if (override.value.alt) {
|
|
312
|
+
element.attributes.alt = override.value.alt;
|
|
313
|
+
}
|
|
314
|
+
if (override.value.objectFit) {
|
|
315
|
+
element.styles.objectFit = override.value.objectFit;
|
|
316
|
+
}
|
|
317
|
+
if (override.value.objectPosition) {
|
|
318
|
+
element.styles.objectPosition = override.value.objectPosition;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
|
|
323
|
+
case 'style':
|
|
324
|
+
if (typeof override.value === 'object' && !Array.isArray(override.value)) {
|
|
325
|
+
Object.assign(element.styles, override.value as Record<string, string>);
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'attribute':
|
|
330
|
+
if (typeof override.value === 'object' && !Array.isArray(override.value)) {
|
|
331
|
+
Object.assign(element.attributes, override.value as Record<string, string>);
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'children':
|
|
336
|
+
if (Array.isArray(override.value)) {
|
|
337
|
+
element.children = override.value as ComponentElement[];
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
|
|
341
|
+
case 'instanceSwap':
|
|
342
|
+
// Instance swap is handled at a higher level
|
|
343
|
+
// The value contains componentId and optionally variantId
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Type guard for OverrideImageValue
|
|
350
|
+
*/
|
|
351
|
+
function isImageOverrideValue(value: unknown): value is OverrideImageValue {
|
|
352
|
+
return (
|
|
353
|
+
typeof value === 'object' &&
|
|
354
|
+
value !== null &&
|
|
355
|
+
'url' in value &&
|
|
356
|
+
typeof (value as OverrideImageValue).url === 'string'
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Resolve a component instance to a ComponentElement by applying all overrides.
|
|
362
|
+
*
|
|
363
|
+
* @param master - The master component definition
|
|
364
|
+
* @param instance - The component instance with overrides
|
|
365
|
+
* @returns A resolved ComponentElement with all overrides applied
|
|
366
|
+
*/
|
|
367
|
+
export function resolveInstance(
|
|
368
|
+
master: MasterComponent,
|
|
369
|
+
instance: ComponentInstance
|
|
370
|
+
): ComponentElement {
|
|
371
|
+
// Find the variant
|
|
372
|
+
const variant = master.variants.find((v) => v.id === instance.variantId);
|
|
373
|
+
if (!variant) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Variant "${instance.variantId}" not found in master component "${master.id}"`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Deep clone the variant's root element, preserving IDs for override resolution
|
|
380
|
+
const resolved = deepCloneElement(variant.rootElement, true);
|
|
381
|
+
|
|
382
|
+
// Apply overrides
|
|
383
|
+
for (const override of instance.overrides) {
|
|
384
|
+
// Find target element
|
|
385
|
+
let targetElement: ComponentElement | null = null;
|
|
386
|
+
|
|
387
|
+
if (override.targetElementId) {
|
|
388
|
+
targetElement = findElementById(resolved, override.targetElementId);
|
|
389
|
+
} else if (override.elementPath) {
|
|
390
|
+
targetElement = findElementByPath(resolved, override.elementPath);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (targetElement) {
|
|
394
|
+
applyOverride(targetElement, override);
|
|
395
|
+
} else {
|
|
396
|
+
console.warn(
|
|
397
|
+
`[resolveInstance] Target element not found for override: ${
|
|
398
|
+
override.targetElementId || override.elementPath
|
|
399
|
+
}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return resolved;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Render a component instance to an HTML element.
|
|
409
|
+
*
|
|
410
|
+
* @param master - The master component definition
|
|
411
|
+
* @param instance - The component instance to render
|
|
412
|
+
* @param doc - The Document to create elements in (defaults to window.document)
|
|
413
|
+
* @returns The rendered HTMLElement
|
|
414
|
+
*/
|
|
415
|
+
export function renderInstance(
|
|
416
|
+
master: MasterComponent,
|
|
417
|
+
instance: ComponentInstance,
|
|
418
|
+
doc: Document = document
|
|
419
|
+
): HTMLElement {
|
|
420
|
+
console.log('[renderInstance] Starting render:', {
|
|
421
|
+
masterId: master.id,
|
|
422
|
+
masterName: master.name,
|
|
423
|
+
instanceId: instance.id,
|
|
424
|
+
variantId: instance.variantId,
|
|
425
|
+
variantsCount: master.variants.length,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Log the master component's root element structure
|
|
429
|
+
const defaultVariant = master.variants.find(v => v.id === instance.variantId);
|
|
430
|
+
if (defaultVariant) {
|
|
431
|
+
console.log('[renderInstance] Variant rootElement:', {
|
|
432
|
+
tagName: defaultVariant.rootElement.tagName,
|
|
433
|
+
className: defaultVariant.rootElement.className,
|
|
434
|
+
hasTextContent: !!defaultVariant.rootElement.textContent,
|
|
435
|
+
childrenCount: defaultVariant.rootElement.children.length,
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
console.error('[renderInstance] Variant not found:', instance.variantId);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Resolve the instance to a ComponentElement
|
|
442
|
+
const resolved = resolveInstance(master, instance);
|
|
443
|
+
console.log('[renderInstance] Resolved element:', {
|
|
444
|
+
tagName: resolved.tagName,
|
|
445
|
+
className: resolved.className,
|
|
446
|
+
hasTextContent: !!resolved.textContent,
|
|
447
|
+
childrenCount: resolved.children.length,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Create the DOM element
|
|
451
|
+
const el = createDOMElement(resolved, doc);
|
|
452
|
+
|
|
453
|
+
// Apply instance tracking attributes to the root element
|
|
454
|
+
applyInstanceAttributes(el, instance, master.id);
|
|
455
|
+
|
|
456
|
+
// Set the DOM element ID
|
|
457
|
+
el.setAttribute(DATA_ATTRIBUTES.ELEMENT_ID, instance.domElementId);
|
|
458
|
+
|
|
459
|
+
// Apply position if specified
|
|
460
|
+
if (instance.position) {
|
|
461
|
+
el.style.position = 'absolute';
|
|
462
|
+
el.style.left = `${instance.position.x}px`;
|
|
463
|
+
el.style.top = `${instance.position.y}px`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Apply size override if specified
|
|
467
|
+
if (instance.size) {
|
|
468
|
+
el.style.width = `${instance.size.width}px`;
|
|
469
|
+
el.style.height = `${instance.size.height}px`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return el;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============================================================
|
|
476
|
+
// DOM Update Functions
|
|
477
|
+
// ============================================================
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Update an existing DOM element to reflect instance changes.
|
|
481
|
+
* This is more efficient than re-rendering when only overrides change.
|
|
482
|
+
*
|
|
483
|
+
* @param el - The existing DOM element
|
|
484
|
+
* @param master - The master component definition
|
|
485
|
+
* @param instance - The updated component instance
|
|
486
|
+
*/
|
|
487
|
+
export function updateInstanceElement(
|
|
488
|
+
el: HTMLElement,
|
|
489
|
+
master: MasterComponent,
|
|
490
|
+
instance: ComponentInstance
|
|
491
|
+
): void {
|
|
492
|
+
// Update variant ID attribute
|
|
493
|
+
el.setAttribute(DATA_ATTRIBUTES.VARIANT_ID, instance.variantId);
|
|
494
|
+
|
|
495
|
+
// Find the variant
|
|
496
|
+
const variant = master.variants.find((v) => v.id === instance.variantId);
|
|
497
|
+
if (!variant) {
|
|
498
|
+
console.error(
|
|
499
|
+
`[updateInstanceElement] Variant "${instance.variantId}" not found`
|
|
500
|
+
);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Apply each override to the DOM
|
|
505
|
+
for (const override of instance.overrides) {
|
|
506
|
+
const targetId = override.targetElementId;
|
|
507
|
+
if (!targetId) continue;
|
|
508
|
+
|
|
509
|
+
// Find the target element in the DOM
|
|
510
|
+
const targetEl = el.querySelector(
|
|
511
|
+
`[${DATA_ATTRIBUTES.ELEMENT_ID}="${targetId}"]`
|
|
512
|
+
) as HTMLElement | null;
|
|
513
|
+
|
|
514
|
+
if (!targetEl) {
|
|
515
|
+
// Check if it's the root element itself
|
|
516
|
+
if (el.getAttribute(DATA_ATTRIBUTES.ELEMENT_ID) === targetId) {
|
|
517
|
+
applyOverrideToDOM(el, override);
|
|
518
|
+
} else {
|
|
519
|
+
console.warn(
|
|
520
|
+
`[updateInstanceElement] Target element not found: ${targetId}`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
applyOverrideToDOM(targetEl, override);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Update position if changed
|
|
530
|
+
if (instance.position) {
|
|
531
|
+
el.style.position = 'absolute';
|
|
532
|
+
el.style.left = `${instance.position.x}px`;
|
|
533
|
+
el.style.top = `${instance.position.y}px`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Update size if changed
|
|
537
|
+
if (instance.size) {
|
|
538
|
+
el.style.width = `${instance.size.width}px`;
|
|
539
|
+
el.style.height = `${instance.size.height}px`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Apply an override directly to a DOM element.
|
|
545
|
+
*
|
|
546
|
+
* @param el - The target DOM element
|
|
547
|
+
* @param override - The override to apply
|
|
548
|
+
*/
|
|
549
|
+
function applyOverrideToDOM(el: HTMLElement, override: ComponentOverride): void {
|
|
550
|
+
switch (override.type) {
|
|
551
|
+
case 'text':
|
|
552
|
+
if (typeof override.value === 'string') {
|
|
553
|
+
el.textContent = override.value;
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
|
|
557
|
+
case 'fill':
|
|
558
|
+
if (typeof override.value === 'string') {
|
|
559
|
+
el.style.backgroundColor = override.value;
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
|
|
563
|
+
case 'stroke':
|
|
564
|
+
if (typeof override.value === 'string') {
|
|
565
|
+
el.style.borderColor = override.value;
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
|
|
569
|
+
case 'visibility':
|
|
570
|
+
if (typeof override.value === 'boolean') {
|
|
571
|
+
el.style.display = override.value ? '' : 'none';
|
|
572
|
+
if (!override.value) {
|
|
573
|
+
el.setAttribute('data-hidden', 'true');
|
|
574
|
+
} else {
|
|
575
|
+
el.removeAttribute('data-hidden');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
case 'image':
|
|
581
|
+
if (isImageOverrideValue(override.value)) {
|
|
582
|
+
el.setAttribute('src', override.value.url);
|
|
583
|
+
if (override.value.alt) {
|
|
584
|
+
el.setAttribute('alt', override.value.alt);
|
|
585
|
+
}
|
|
586
|
+
if (override.value.objectFit) {
|
|
587
|
+
el.style.objectFit = override.value.objectFit;
|
|
588
|
+
}
|
|
589
|
+
if (override.value.objectPosition) {
|
|
590
|
+
el.style.objectPosition = override.value.objectPosition;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
|
|
595
|
+
case 'style':
|
|
596
|
+
if (typeof override.value === 'object' && !Array.isArray(override.value)) {
|
|
597
|
+
const styles = override.value as Record<string, string>;
|
|
598
|
+
for (const [property, value] of Object.entries(styles)) {
|
|
599
|
+
const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
600
|
+
el.style.setProperty(cssProperty, value);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
|
|
605
|
+
case 'attribute':
|
|
606
|
+
if (typeof override.value === 'object' && !Array.isArray(override.value)) {
|
|
607
|
+
const attrs = override.value as Record<string, string>;
|
|
608
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
609
|
+
el.setAttribute(key, value);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
|
|
614
|
+
case 'children':
|
|
615
|
+
// Children replacement requires re-rendering
|
|
616
|
+
console.warn(
|
|
617
|
+
'[applyOverrideToDOM] Children override requires full re-render'
|
|
618
|
+
);
|
|
619
|
+
break;
|
|
620
|
+
|
|
621
|
+
case 'instanceSwap':
|
|
622
|
+
// Instance swap requires higher-level handling
|
|
623
|
+
console.warn(
|
|
624
|
+
'[applyOverrideToDOM] Instance swap override requires higher-level handling'
|
|
625
|
+
);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ============================================================
|
|
631
|
+
// Utility Functions
|
|
632
|
+
// ============================================================
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Extract instance ID from a DOM element.
|
|
636
|
+
*
|
|
637
|
+
* @param el - The DOM element
|
|
638
|
+
* @returns The instance ID or null if not found
|
|
639
|
+
*/
|
|
640
|
+
export function getInstanceIdFromElement(el: HTMLElement): string | null {
|
|
641
|
+
return el.getAttribute(DATA_ATTRIBUTES.COMPONENT_INSTANCE);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Extract master component ID from a DOM element.
|
|
646
|
+
*
|
|
647
|
+
* @param el - The DOM element
|
|
648
|
+
* @returns The master component ID or null if not found
|
|
649
|
+
*/
|
|
650
|
+
export function getMasterIdFromElement(el: HTMLElement): string | null {
|
|
651
|
+
return el.getAttribute(DATA_ATTRIBUTES.COMPONENT_MASTER);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Extract variant ID from a DOM element.
|
|
656
|
+
*
|
|
657
|
+
* @param el - The DOM element
|
|
658
|
+
* @returns The variant ID or null if not found
|
|
659
|
+
*/
|
|
660
|
+
export function getVariantIdFromElement(el: HTMLElement): string | null {
|
|
661
|
+
return el.getAttribute(DATA_ATTRIBUTES.VARIANT_ID);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Check if an element is a component instance.
|
|
666
|
+
*
|
|
667
|
+
* @param el - The DOM element to check
|
|
668
|
+
* @returns True if the element is a component instance
|
|
669
|
+
*/
|
|
670
|
+
export function isComponentInstance(el: HTMLElement): boolean {
|
|
671
|
+
return el.hasAttribute(DATA_ATTRIBUTES.COMPONENT_INSTANCE);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Find all component instances in a document.
|
|
676
|
+
*
|
|
677
|
+
* @param doc - The Document to search in
|
|
678
|
+
* @returns An array of elements that are component instances
|
|
679
|
+
*/
|
|
680
|
+
export function findAllComponentInstances(doc: Document): HTMLElement[] {
|
|
681
|
+
return Array.from(
|
|
682
|
+
doc.querySelectorAll(`[${DATA_ATTRIBUTES.COMPONENT_INSTANCE}]`)
|
|
683
|
+
) as HTMLElement[];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Find all instances of a specific master component in a document.
|
|
688
|
+
*
|
|
689
|
+
* @param doc - The Document to search in
|
|
690
|
+
* @param masterId - The master component ID to search for
|
|
691
|
+
* @returns An array of elements that are instances of the specified master
|
|
692
|
+
*/
|
|
693
|
+
export function findInstancesByMaster(
|
|
694
|
+
doc: Document,
|
|
695
|
+
masterId: string
|
|
696
|
+
): HTMLElement[] {
|
|
697
|
+
return Array.from(
|
|
698
|
+
doc.querySelectorAll(
|
|
699
|
+
`[${DATA_ATTRIBUTES.COMPONENT_MASTER}="${masterId}"]`
|
|
700
|
+
)
|
|
701
|
+
) as HTMLElement[];
|
|
702
|
+
}
|