@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,613 @@
1
+ /**
2
+ * Override Detection Utilities
3
+ *
4
+ * Functions to detect changes between component instances and their master components.
5
+ * This module provides tools for comparing DOM elements with ComponentElement structures
6
+ * and identifying which properties have been overridden.
7
+ */
8
+
9
+ import type {
10
+ ComponentElement,
11
+ ComponentOverride,
12
+ OverrideType,
13
+ MasterComponent,
14
+ ComponentInstance,
15
+ OverridableProperties,
16
+ } from '../../types/editor-components';
17
+
18
+ /**
19
+ * Find an element within the ComponentElement tree by ID
20
+ */
21
+ export function findElementById(
22
+ element: ComponentElement,
23
+ targetId: string
24
+ ): ComponentElement | null {
25
+ if (element.id === targetId) {
26
+ return element;
27
+ }
28
+
29
+ for (const child of element.children) {
30
+ const found = findElementById(child, targetId);
31
+ if (found) return found;
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Find an element within the ComponentElement tree by path
39
+ * Path format: "children.0.children.1" or "root"
40
+ */
41
+ export function findElementByPath(
42
+ element: ComponentElement,
43
+ path: string
44
+ ): ComponentElement | null {
45
+ if (path === 'root' || path === '') {
46
+ return element;
47
+ }
48
+
49
+ const parts = path.split('.');
50
+ let current: ComponentElement | undefined = element;
51
+
52
+ for (let i = 0; i < parts.length; i += 2) {
53
+ if (parts[i] !== 'children' || !current) {
54
+ return null;
55
+ }
56
+
57
+ const index = parseInt(parts[i + 1], 10);
58
+ if (isNaN(index) || index < 0 || index >= current.children.length) {
59
+ return null;
60
+ }
61
+
62
+ current = current.children[index];
63
+ }
64
+
65
+ return current || null;
66
+ }
67
+
68
+ /**
69
+ * Extract text content from an HTMLElement (direct text only, excluding child elements)
70
+ */
71
+ function getDirectTextContent(element: HTMLElement): string {
72
+ let text = '';
73
+ for (const node of element.childNodes) {
74
+ if (node.nodeType === Node.TEXT_NODE) {
75
+ text += node.textContent || '';
76
+ }
77
+ }
78
+ return text.trim();
79
+ }
80
+
81
+ /**
82
+ * Extract background color from computed styles
83
+ */
84
+ function extractFillColor(computedStyle: CSSStyleDeclaration): string {
85
+ return computedStyle.backgroundColor || 'transparent';
86
+ }
87
+
88
+ /**
89
+ * Extract stroke/border information from computed styles
90
+ */
91
+ function extractStrokeInfo(computedStyle: CSSStyleDeclaration): string {
92
+ const borderWidth = computedStyle.borderWidth || '0px';
93
+ const borderStyle = computedStyle.borderStyle || 'none';
94
+ const borderColor = computedStyle.borderColor || 'transparent';
95
+
96
+ if (borderStyle === 'none' || borderWidth === '0px') {
97
+ return 'none';
98
+ }
99
+
100
+ return `${borderWidth} ${borderStyle} ${borderColor}`;
101
+ }
102
+
103
+ /**
104
+ * Check if an element is visible
105
+ */
106
+ function isElementVisible(element: HTMLElement, computedStyle: CSSStyleDeclaration): boolean {
107
+ return (
108
+ computedStyle.display !== 'none' &&
109
+ computedStyle.visibility !== 'hidden' &&
110
+ computedStyle.opacity !== '0'
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Extract image source from an HTMLElement
116
+ */
117
+ function extractImageSrc(element: HTMLElement): string | null {
118
+ if (element.tagName === 'IMG') {
119
+ return (element as HTMLImageElement).src;
120
+ }
121
+
122
+ // Check for background-image
123
+ const bgImage = element.style.backgroundImage;
124
+ if (bgImage && bgImage !== 'none') {
125
+ const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
126
+ if (urlMatch) {
127
+ return urlMatch[1];
128
+ }
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Compare a single property between instance DOM and master ComponentElement
136
+ */
137
+ function detectSinglePropertyOverride(
138
+ instanceElement: HTMLElement,
139
+ masterElement: ComponentElement,
140
+ type: OverrideType,
141
+ computedStyle: CSSStyleDeclaration
142
+ ): ComponentOverride | null {
143
+ // CRITICAL: Use the MASTER's element ID for the override target, not the instance's DOM element ID
144
+ // The override will be applied to the master's ComponentElement tree during resolveInstance,
145
+ // so the targetElementId must match an ID in the master's structure
146
+ const elementId = masterElement.id;
147
+
148
+ switch (type) {
149
+ case 'text': {
150
+ // Debug: Log text comparison attempt
151
+ console.log('[detectOverrides] Text comparison for element:', {
152
+ elementId,
153
+ masterHasInnerHTML: masterElement.innerHTML !== undefined,
154
+ masterChildrenCount: masterElement.children.length,
155
+ masterTextContent: masterElement.textContent?.substring(0, 50),
156
+ instanceTextContent: instanceElement.textContent?.substring(0, 50),
157
+ domChildrenCount: instanceElement.children.length,
158
+ });
159
+
160
+ // Skip text comparison for elements that use innerHTML (mixed content)
161
+ // because the text is embedded in the HTML structure
162
+ if (masterElement.innerHTML !== undefined) {
163
+ console.log('[detectOverrides] Skipping: element uses innerHTML');
164
+ return null;
165
+ }
166
+
167
+ // Get text content for comparison
168
+ const instanceText = instanceElement.textContent?.trim() || '';
169
+
170
+ // For leaf elements (no children), compare textContent directly
171
+ if (masterElement.children.length === 0) {
172
+ const masterText = masterElement.textContent || '';
173
+
174
+ console.log('[detectOverrides] Comparing text (leaf element):', {
175
+ elementId,
176
+ instanceText,
177
+ masterText,
178
+ areEqual: instanceText === masterText,
179
+ });
180
+
181
+ if (instanceText !== masterText) {
182
+ console.log('[detectOverrides] Text override detected:', {
183
+ elementId,
184
+ instanceText: instanceText.substring(0, 50),
185
+ masterText: masterText.substring(0, 50),
186
+ });
187
+ return {
188
+ id: `override-${elementId}-text-${Date.now()}`,
189
+ targetElementId: elementId,
190
+ type: 'text',
191
+ value: instanceText,
192
+ };
193
+ }
194
+ } else {
195
+ // Element has children - DO NOT create text override at parent level
196
+ // Text overrides should only be created at leaf elements (no children)
197
+ // Creating a parent-level text override would destroy the child structure
198
+ // when the override is applied (textContent replaces all children)
199
+ console.log('[detectOverrides] Skipping text override for element with children:', {
200
+ elementId,
201
+ childrenCount: masterElement.children.length,
202
+ reason: 'Text overrides at parent level would destroy child structure',
203
+ });
204
+ // The recursive processing will handle text overrides at child level
205
+ }
206
+ break;
207
+ }
208
+
209
+ case 'fill': {
210
+ const instanceFill = extractFillColor(computedStyle);
211
+ const masterFill = masterElement.styles.backgroundColor || 'transparent';
212
+
213
+ // Normalize transparent values for comparison
214
+ // Both 'transparent', 'rgba(0, 0, 0, 0)', and undefined represent no background
215
+ const isTransparent = (color: string): boolean => {
216
+ if (!color || color === 'transparent') return true;
217
+ // Match rgba(0, 0, 0, 0) or rgba(0,0,0,0)
218
+ if (/^rgba?\s*\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)$/.test(color)) return true;
219
+ return false;
220
+ };
221
+
222
+ // Don't create fill override if both are transparent
223
+ if (isTransparent(instanceFill) && isTransparent(masterFill)) {
224
+ break;
225
+ }
226
+
227
+ if (instanceFill !== masterFill) {
228
+ return {
229
+ id: `override-${elementId}-fill-${Date.now()}`,
230
+ targetElementId: elementId,
231
+ type: 'fill',
232
+ value: instanceFill,
233
+ };
234
+ }
235
+ break;
236
+ }
237
+
238
+ case 'stroke': {
239
+ const instanceStroke = extractStrokeInfo(computedStyle);
240
+ const masterStroke = masterElement.styles.border || 'none';
241
+
242
+ if (instanceStroke !== masterStroke) {
243
+ return {
244
+ id: `override-${elementId}-stroke-${Date.now()}`,
245
+ targetElementId: elementId,
246
+ type: 'stroke',
247
+ value: instanceStroke,
248
+ };
249
+ }
250
+ break;
251
+ }
252
+
253
+ case 'visibility': {
254
+ const instanceVisible = isElementVisible(instanceElement, computedStyle);
255
+ const masterVisible = !masterElement.hidden;
256
+
257
+ if (instanceVisible !== masterVisible) {
258
+ return {
259
+ id: `override-${elementId}-visibility-${Date.now()}`,
260
+ targetElementId: elementId,
261
+ type: 'visibility',
262
+ value: instanceVisible,
263
+ };
264
+ }
265
+ break;
266
+ }
267
+
268
+ case 'image': {
269
+ const instanceSrc = extractImageSrc(instanceElement);
270
+ const masterSrc = masterElement.tagName === 'img'
271
+ ? masterElement.attributes.src
272
+ : null;
273
+
274
+ if (instanceSrc && instanceSrc !== masterSrc) {
275
+ return {
276
+ id: `override-${elementId}-image-${Date.now()}`,
277
+ targetElementId: elementId,
278
+ type: 'image',
279
+ value: {
280
+ url: instanceSrc,
281
+ alt: instanceElement.getAttribute('alt') || undefined,
282
+ },
283
+ };
284
+ }
285
+ break;
286
+ }
287
+ }
288
+
289
+ return null;
290
+ }
291
+
292
+ /**
293
+ * Detect overrides by comparing instance DOM with master ComponentElement
294
+ * Recursively traverses the tree and compares each overridable property
295
+ */
296
+ export function detectOverrides(
297
+ instanceElement: HTMLElement,
298
+ masterElement: ComponentElement
299
+ ): ComponentOverride[] {
300
+ const overrides: ComponentOverride[] = [];
301
+ const iframeDoc = instanceElement.ownerDocument;
302
+ const defaultView = iframeDoc.defaultView;
303
+
304
+ if (!defaultView) {
305
+ console.warn('[detectOverrides] No defaultView available');
306
+ return overrides;
307
+ }
308
+
309
+ const processElement = (
310
+ domElement: HTMLElement,
311
+ componentElement: ComponentElement
312
+ ): void => {
313
+ const computedStyle = defaultView.getComputedStyle(domElement);
314
+ const overridable = componentElement.overridable;
315
+
316
+ // Check each overridable property type
317
+ const propertyTypes: Array<{ type: OverrideType; allowed: boolean }> = [
318
+ { type: 'text', allowed: overridable.text },
319
+ { type: 'fill', allowed: overridable.fill },
320
+ { type: 'stroke', allowed: overridable.stroke },
321
+ { type: 'visibility', allowed: overridable.visibility },
322
+ { type: 'image', allowed: overridable.image },
323
+ ];
324
+
325
+ for (const { type, allowed } of propertyTypes) {
326
+ if (allowed) {
327
+ const override = detectSinglePropertyOverride(
328
+ domElement,
329
+ componentElement,
330
+ type,
331
+ computedStyle
332
+ );
333
+ if (override) {
334
+ overrides.push(override);
335
+ }
336
+ }
337
+ }
338
+
339
+ // Process children recursively
340
+ // Note: Use nodeType check instead of instanceof HTMLElement because
341
+ // in iframe context, the HTMLElement constructor is from the parent window
342
+ const domChildren = Array.from(domElement.children).filter(
343
+ (child): child is HTMLElement => {
344
+ if (child.nodeType !== Node.ELEMENT_NODE) return false;
345
+ const el = child as HTMLElement;
346
+ if (el.classList?.contains('selection-box')) return false;
347
+ if (el.classList?.contains('resize-handle')) return false;
348
+ return true;
349
+ }
350
+ );
351
+
352
+ // Debug: Log child matching
353
+ if (componentElement.children.length > 0 || domChildren.length > 0) {
354
+ console.log('[detectOverrides] Child matching:', {
355
+ parentId: componentElement.id,
356
+ domChildrenCount: domChildren.length,
357
+ domChildrenIds: domChildren.map(dc => dc.getAttribute('data-element-id')),
358
+ domChildrenMasterIds: domChildren.map(dc => dc.getAttribute('data-master-element-id')),
359
+ componentChildrenCount: componentElement.children.length,
360
+ componentChildrenIds: componentElement.children.map(c => c.id),
361
+ });
362
+ }
363
+
364
+ for (const componentChild of componentElement.children) {
365
+ // Match by data-element-id first, then fall back to data-master-element-id
366
+ // (duplicated instances have regenerated data-element-id but preserve the original in data-master-element-id)
367
+ const matchingDomChild = domChildren.find(
368
+ (dc) => {
369
+ const domId = dc.getAttribute('data-element-id');
370
+ const masterRefId = dc.getAttribute('data-master-element-id');
371
+ return domId === componentChild.id || masterRefId === componentChild.id;
372
+ }
373
+ );
374
+
375
+ if (matchingDomChild) {
376
+ processElement(matchingDomChild, componentChild);
377
+ } else {
378
+ console.log('[detectOverrides] No matching DOM child for component child:', {
379
+ componentChildId: componentChild.id,
380
+ componentChildTagName: componentChild.tagName,
381
+ availableDomIds: domChildren.map(dc => dc.getAttribute('data-element-id')),
382
+ availableMasterIds: domChildren.map(dc => dc.getAttribute('data-master-element-id')),
383
+ });
384
+ }
385
+ }
386
+ };
387
+
388
+ processElement(instanceElement, masterElement);
389
+
390
+ console.log('[detectOverrides] ========== FINAL RESULT ==========');
391
+ console.log('[detectOverrides] Total overrides detected:', overrides.length);
392
+ if (overrides.length > 0) {
393
+ console.log('[detectOverrides] Detected overrides:', overrides.map(o => ({
394
+ type: o.type,
395
+ targetElementId: o.targetElementId,
396
+ value: typeof o.value === 'string' ? o.value.substring(0, 50) : o.value,
397
+ })));
398
+ }
399
+
400
+ return overrides;
401
+ }
402
+
403
+ /**
404
+ * Check if a specific property is overridden in an instance
405
+ */
406
+ export function isPropertyOverridden(
407
+ instance: ComponentInstance,
408
+ elementId: string,
409
+ overrideType: OverrideType
410
+ ): boolean {
411
+ return instance.overrides.some(
412
+ (override) =>
413
+ (override.targetElementId === elementId || override.elementPath?.includes(elementId)) &&
414
+ override.type === overrideType
415
+ );
416
+ }
417
+
418
+ /**
419
+ * Get the override value for a specific property
420
+ * Returns null if the property is not overridden
421
+ */
422
+ export function getOverrideValue(
423
+ instance: ComponentInstance,
424
+ elementId: string,
425
+ overrideType: OverrideType
426
+ ): string | boolean | null {
427
+ const override = instance.overrides.find(
428
+ (o) =>
429
+ (o.targetElementId === elementId || o.elementPath?.includes(elementId)) &&
430
+ o.type === overrideType
431
+ );
432
+
433
+ if (!override) {
434
+ return null;
435
+ }
436
+
437
+ // Handle different value types
438
+ if (typeof override.value === 'string' || typeof override.value === 'boolean') {
439
+ return override.value;
440
+ }
441
+
442
+ // For complex values, return string representation
443
+ if (typeof override.value === 'object' && 'url' in override.value) {
444
+ return (override.value as { url: string }).url;
445
+ }
446
+
447
+ return null;
448
+ }
449
+
450
+ /**
451
+ * Get all overrides for a specific element
452
+ */
453
+ export function getOverridesForElement(
454
+ instance: ComponentInstance,
455
+ elementId: string
456
+ ): ComponentOverride[] {
457
+ return instance.overrides.filter(
458
+ (override) =>
459
+ override.targetElementId === elementId ||
460
+ override.elementPath?.includes(elementId)
461
+ );
462
+ }
463
+
464
+ /**
465
+ * Compare two ComponentElements to find differences
466
+ * This is useful for comparing a modified element back to its original state
467
+ */
468
+ export function compareElements(
469
+ original: ComponentElement,
470
+ modified: ComponentElement
471
+ ): ComponentOverride[] {
472
+ const overrides: ComponentOverride[] = [];
473
+ const timestamp = Date.now();
474
+
475
+ // Compare text content
476
+ if (original.textContent !== modified.textContent && original.overridable.text) {
477
+ overrides.push({
478
+ id: `override-${original.id}-text-${timestamp}`,
479
+ targetElementId: original.id,
480
+ type: 'text',
481
+ value: modified.textContent || '',
482
+ });
483
+ }
484
+
485
+ // Compare styles
486
+ const fillChanged =
487
+ original.styles.backgroundColor !== modified.styles.backgroundColor;
488
+ if (fillChanged && original.overridable.fill) {
489
+ overrides.push({
490
+ id: `override-${original.id}-fill-${timestamp}`,
491
+ targetElementId: original.id,
492
+ type: 'fill',
493
+ value: modified.styles.backgroundColor || 'transparent',
494
+ });
495
+ }
496
+
497
+ // Compare border/stroke
498
+ const strokeChanged = original.styles.border !== modified.styles.border;
499
+ if (strokeChanged && original.overridable.stroke) {
500
+ overrides.push({
501
+ id: `override-${original.id}-stroke-${timestamp}`,
502
+ targetElementId: original.id,
503
+ type: 'stroke',
504
+ value: modified.styles.border || 'none',
505
+ });
506
+ }
507
+
508
+ // Compare visibility
509
+ if (original.hidden !== modified.hidden && original.overridable.visibility) {
510
+ overrides.push({
511
+ id: `override-${original.id}-visibility-${timestamp}`,
512
+ targetElementId: original.id,
513
+ type: 'visibility',
514
+ value: !modified.hidden,
515
+ });
516
+ }
517
+
518
+ // Compare image source (for img elements)
519
+ if (original.tagName === 'img' && original.overridable.image) {
520
+ const originalSrc = original.attributes.src;
521
+ const modifiedSrc = modified.attributes.src;
522
+
523
+ if (originalSrc !== modifiedSrc) {
524
+ overrides.push({
525
+ id: `override-${original.id}-image-${timestamp}`,
526
+ targetElementId: original.id,
527
+ type: 'image',
528
+ value: {
529
+ url: modifiedSrc || '',
530
+ alt: modified.attributes.alt,
531
+ },
532
+ });
533
+ }
534
+ }
535
+
536
+ // Recursively compare children
537
+ for (let i = 0; i < original.children.length; i++) {
538
+ const origChild = original.children[i];
539
+ const modChild = modified.children.find((c) => c.id === origChild.id);
540
+
541
+ if (modChild) {
542
+ const childOverrides = compareElements(origChild, modChild);
543
+ overrides.push(...childOverrides);
544
+ }
545
+ }
546
+
547
+ return overrides;
548
+ }
549
+
550
+ /**
551
+ * Get a summary of overrides for display purposes
552
+ */
553
+ export function getOverrideSummary(
554
+ instance: ComponentInstance
555
+ ): Array<{
556
+ elementId: string;
557
+ types: OverrideType[];
558
+ count: number;
559
+ }> {
560
+ const summaryMap = new Map<string, Set<OverrideType>>();
561
+
562
+ for (const override of instance.overrides) {
563
+ const elementId = override.targetElementId || override.elementPath || 'unknown';
564
+ // elementPath is always a string in our type definition
565
+ const key = elementId;
566
+
567
+ if (!summaryMap.has(key)) {
568
+ summaryMap.set(key, new Set());
569
+ }
570
+ summaryMap.get(key)!.add(override.type);
571
+ }
572
+
573
+ return Array.from(summaryMap.entries()).map(([elementId, types]) => ({
574
+ elementId,
575
+ types: Array.from(types),
576
+ count: types.size,
577
+ }));
578
+ }
579
+
580
+ /**
581
+ * Check if a specific element has any overrides
582
+ * This is a more specific check than the generic hasOverrides in component-sync.ts
583
+ */
584
+ export function elementHasOverrides(
585
+ instance: ComponentInstance,
586
+ elementId: string
587
+ ): boolean {
588
+ return instance.overrides.some(
589
+ (o) => o.targetElementId === elementId || o.elementPath?.includes(elementId)
590
+ );
591
+ }
592
+
593
+ /**
594
+ * Get the count of overrides for an instance
595
+ */
596
+ export function getOverrideCount(instance: ComponentInstance): number {
597
+ return instance.overrides.length;
598
+ }
599
+
600
+ /**
601
+ * Check which properties of an element are overridable based on the master component
602
+ */
603
+ export function getOverridableProperties(
604
+ master: MasterComponent,
605
+ elementId: string
606
+ ): OverridableProperties | null {
607
+ // Get the current variant's root element
608
+ const variant = master.variants.find((v) => v.id === master.defaultVariantId);
609
+ if (!variant) return null;
610
+
611
+ const element = findElementById(variant.rootElement, elementId);
612
+ return element?.overridable || null;
613
+ }