@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.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. 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
+ }