@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,845 @@
1
+ /**
2
+ * Component Sync Utilities
3
+ *
4
+ * Utilities for synchronizing master component changes to instances.
5
+ * Handles propagation of master updates, override validation, and instance reconciliation.
6
+ */
7
+
8
+ import type {
9
+ ComponentElement,
10
+ ComponentInstance,
11
+ ComponentOverride,
12
+ MasterComponent,
13
+ OverrideType,
14
+ } from '../../types/editor-components';
15
+ import {
16
+ renderInstance,
17
+ findInstancesByMaster,
18
+ getInstanceIdFromElement,
19
+ resolveInstance,
20
+ } from './component-renderer';
21
+ import { detectOverrides } from './override-detection';
22
+
23
+ // ============================================================
24
+ // Types
25
+ // ============================================================
26
+
27
+ /**
28
+ * Result of an override validation check.
29
+ */
30
+ export interface OverrideValidationResult {
31
+ /** Whether the override is valid */
32
+ isValid: boolean;
33
+ /** Reason for invalidity if not valid */
34
+ reason?: string;
35
+ /** Suggested action to fix the issue */
36
+ suggestion?: 'remove' | 'update' | 'keep';
37
+ }
38
+
39
+ /**
40
+ * Result of a sync operation.
41
+ */
42
+ export interface SyncResult {
43
+ /** Number of instances successfully updated */
44
+ updatedCount: number;
45
+ /** Number of instances that failed to update */
46
+ failedCount: number;
47
+ /** Instance IDs that failed to update */
48
+ failedInstanceIds: string[];
49
+ /** Errors encountered during sync */
50
+ errors: Array<{ instanceId: string; error: Error }>;
51
+ /** Instances updated with newly detected overrides (for persistence) */
52
+ updatedInstances: ComponentInstance[];
53
+ }
54
+
55
+ // ============================================================
56
+ // Override Validation
57
+ // ============================================================
58
+
59
+ /**
60
+ * Find an element in a ComponentElement tree by ID.
61
+ */
62
+ function findElementById(
63
+ root: ComponentElement,
64
+ elementId: string
65
+ ): ComponentElement | null {
66
+ if (root.id === elementId) {
67
+ return root;
68
+ }
69
+
70
+ for (const child of root.children) {
71
+ const found = findElementById(child, elementId);
72
+ if (found) return found;
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Find an element in a ComponentElement tree by path.
80
+ */
81
+ function findElementByPath(
82
+ root: ComponentElement,
83
+ path: string
84
+ ): ComponentElement | null {
85
+ if (path === 'root') {
86
+ return root;
87
+ }
88
+
89
+ const segments = path.split('.');
90
+ let current: ComponentElement | undefined = root;
91
+
92
+ for (let i = 0; i < segments.length; i += 2) {
93
+ if (segments[i] !== 'children' || !current) {
94
+ return null;
95
+ }
96
+ const index = parseInt(segments[i + 1], 10);
97
+ if (isNaN(index) || index < 0 || index >= current.children.length) {
98
+ return null;
99
+ }
100
+ current = current.children[index];
101
+ }
102
+
103
+ return current || null;
104
+ }
105
+
106
+ /**
107
+ * Check if an override type is allowed for an element based on its overridable properties.
108
+ */
109
+ function isOverrideTypeAllowed(
110
+ element: ComponentElement,
111
+ overrideType: OverrideType
112
+ ): boolean {
113
+ const { overridable } = element;
114
+
115
+ switch (overrideType) {
116
+ case 'text':
117
+ return overridable.text;
118
+ case 'fill':
119
+ return overridable.fill;
120
+ case 'stroke':
121
+ return overridable.stroke;
122
+ case 'visibility':
123
+ return overridable.visibility;
124
+ case 'image':
125
+ return overridable.image;
126
+ case 'style':
127
+ case 'attribute':
128
+ case 'children':
129
+ case 'instanceSwap':
130
+ // These override types are generally allowed
131
+ return true;
132
+ default:
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check if a single override is still valid after a master component update.
139
+ *
140
+ * @param override - The override to validate
141
+ * @param master - The updated master component
142
+ * @param variantId - The variant ID the instance is using
143
+ * @returns Validation result with details
144
+ */
145
+ export function isOverrideValid(
146
+ override: ComponentOverride,
147
+ master: MasterComponent,
148
+ variantId: string = master.defaultVariantId
149
+ ): OverrideValidationResult {
150
+ // Find the variant
151
+ const variant = master.variants.find((v) => v.id === variantId);
152
+ if (!variant) {
153
+ return {
154
+ isValid: false,
155
+ reason: `Variant "${variantId}" no longer exists in master`,
156
+ suggestion: 'remove',
157
+ };
158
+ }
159
+
160
+ // Find the target element
161
+ let targetElement: ComponentElement | null = null;
162
+
163
+ if (override.targetElementId) {
164
+ targetElement = findElementById(variant.rootElement, override.targetElementId);
165
+ if (!targetElement) {
166
+ return {
167
+ isValid: false,
168
+ reason: `Target element "${override.targetElementId}" no longer exists in master`,
169
+ suggestion: 'remove',
170
+ };
171
+ }
172
+ } else if (override.elementPath) {
173
+ targetElement = findElementByPath(variant.rootElement, override.elementPath);
174
+ if (!targetElement) {
175
+ return {
176
+ isValid: false,
177
+ reason: `Target path "${override.elementPath}" no longer valid in master`,
178
+ suggestion: 'remove',
179
+ };
180
+ }
181
+ } else {
182
+ return {
183
+ isValid: false,
184
+ reason: 'Override has no target (neither elementId nor path)',
185
+ suggestion: 'remove',
186
+ };
187
+ }
188
+
189
+ // Check if the override type is allowed
190
+ if (!isOverrideTypeAllowed(targetElement, override.type)) {
191
+ return {
192
+ isValid: false,
193
+ reason: `Override type "${override.type}" is no longer allowed on target element`,
194
+ suggestion: 'remove',
195
+ };
196
+ }
197
+
198
+ // Validate override value based on type
199
+ switch (override.type) {
200
+ case 'text':
201
+ if (typeof override.value !== 'string') {
202
+ return {
203
+ isValid: false,
204
+ reason: 'Text override value must be a string',
205
+ suggestion: 'update',
206
+ };
207
+ }
208
+ break;
209
+
210
+ case 'fill':
211
+ case 'stroke':
212
+ if (typeof override.value !== 'string') {
213
+ return {
214
+ isValid: false,
215
+ reason: 'Color override value must be a string',
216
+ suggestion: 'update',
217
+ };
218
+ }
219
+ break;
220
+
221
+ case 'visibility':
222
+ if (typeof override.value !== 'boolean') {
223
+ return {
224
+ isValid: false,
225
+ reason: 'Visibility override value must be a boolean',
226
+ suggestion: 'update',
227
+ };
228
+ }
229
+ break;
230
+
231
+ case 'image':
232
+ if (
233
+ typeof override.value !== 'object' ||
234
+ !override.value ||
235
+ !('url' in override.value)
236
+ ) {
237
+ return {
238
+ isValid: false,
239
+ reason: 'Image override value must be an object with url property',
240
+ suggestion: 'update',
241
+ };
242
+ }
243
+ break;
244
+
245
+ case 'style':
246
+ case 'attribute':
247
+ if (
248
+ typeof override.value !== 'object' ||
249
+ Array.isArray(override.value) ||
250
+ override.value === null
251
+ ) {
252
+ return {
253
+ isValid: false,
254
+ reason: `${override.type} override value must be a key-value object`,
255
+ suggestion: 'update',
256
+ };
257
+ }
258
+ break;
259
+
260
+ case 'children':
261
+ if (!Array.isArray(override.value)) {
262
+ return {
263
+ isValid: false,
264
+ reason: 'Children override value must be an array',
265
+ suggestion: 'update',
266
+ };
267
+ }
268
+ break;
269
+
270
+ case 'instanceSwap':
271
+ if (
272
+ typeof override.value !== 'object' ||
273
+ !override.value ||
274
+ !('componentId' in override.value)
275
+ ) {
276
+ return {
277
+ isValid: false,
278
+ reason: 'Instance swap override value must have componentId',
279
+ suggestion: 'update',
280
+ };
281
+ }
282
+ break;
283
+ }
284
+
285
+ return { isValid: true };
286
+ }
287
+
288
+ /**
289
+ * Filter overrides to only those compatible with the new master component.
290
+ *
291
+ * @param overrides - The overrides to filter
292
+ * @param master - The updated master component
293
+ * @param variantId - The variant ID to validate against
294
+ * @returns Array of valid overrides
295
+ */
296
+ export function filterCompatibleOverrides(
297
+ overrides: ComponentOverride[],
298
+ master: MasterComponent,
299
+ variantId: string = master.defaultVariantId
300
+ ): ComponentOverride[] {
301
+ return overrides.filter((override) => {
302
+ const result = isOverrideValid(override, master, variantId);
303
+ if (!result.isValid) {
304
+ console.warn(
305
+ `[filterCompatibleOverrides] Removing invalid override: ${result.reason}`,
306
+ override
307
+ );
308
+ }
309
+ return result.isValid;
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Validate all overrides on an instance and return detailed results.
315
+ *
316
+ * @param instance - The component instance
317
+ * @param master - The master component
318
+ * @returns Array of validation results for each override
319
+ */
320
+ export function validateInstanceOverrides(
321
+ instance: ComponentInstance,
322
+ master: MasterComponent
323
+ ): Array<{ override: ComponentOverride; result: OverrideValidationResult }> {
324
+ return instance.overrides.map((override) => ({
325
+ override,
326
+ result: isOverrideValid(override, master, instance.variantId),
327
+ }));
328
+ }
329
+
330
+ // ============================================================
331
+ // Master Change Propagation
332
+ // ============================================================
333
+
334
+ /**
335
+ * Propagate changes from a master component to all linked instances in a document.
336
+ *
337
+ * This function:
338
+ * 1. Finds all DOM elements that are instances of the master
339
+ * 2. Re-renders each instance with the updated master
340
+ * 3. Preserves valid overrides while removing incompatible ones
341
+ *
342
+ * @param master - The updated master component
343
+ * @param instances - Array of component instances to update
344
+ * @param iframeDoc - The iframe document containing the instances
345
+ * @returns Sync result with statistics
346
+ */
347
+ export function propagateMasterChanges(
348
+ master: MasterComponent,
349
+ instances: ComponentInstance[],
350
+ iframeDoc: Document
351
+ ): SyncResult {
352
+ const result: SyncResult = {
353
+ updatedCount: 0,
354
+ failedCount: 0,
355
+ failedInstanceIds: [],
356
+ errors: [],
357
+ updatedInstances: [],
358
+ };
359
+
360
+ // Find all DOM elements for this master
361
+ const domElements = findInstancesByMaster(iframeDoc, master.id);
362
+
363
+ console.log('[propagateMasterChanges] Found DOM elements for master:', {
364
+ masterId: master.id,
365
+ domElementsCount: domElements.length,
366
+ });
367
+
368
+ // Create a map for quick lookup
369
+ const domElementMap = new Map<string, HTMLElement>();
370
+ for (const el of domElements) {
371
+ const instanceId = getInstanceIdFromElement(el);
372
+ console.log('[propagateMasterChanges] DOM element instanceId:', instanceId);
373
+ if (instanceId) {
374
+ domElementMap.set(instanceId, el);
375
+ }
376
+ }
377
+
378
+ console.log('[propagateMasterChanges] Processing instances:', {
379
+ totalInstances: instances.length,
380
+ instanceIds: instances.map(i => i.id),
381
+ domMapKeys: Array.from(domElementMap.keys()),
382
+ });
383
+
384
+ // Process each instance
385
+ for (const instance of instances) {
386
+ // Skip detached instances
387
+ if (instance.isDetached) {
388
+ console.log('[propagateMasterChanges] Skipping detached instance:', instance.id);
389
+ continue;
390
+ }
391
+
392
+ // Skip instances for other masters
393
+ if (instance.masterComponentId !== master.id) {
394
+ console.log('[propagateMasterChanges] Skipping instance for other master:', instance.id);
395
+ continue;
396
+ }
397
+
398
+ try {
399
+ // Find the DOM element
400
+ const domEl = domElementMap.get(instance.id);
401
+ if (!domEl) {
402
+ // Instance not in DOM, skip
403
+ console.log('[propagateMasterChanges] DOM element not found for instance:', instance.id);
404
+ continue;
405
+ }
406
+
407
+ console.log('[propagateMasterChanges] Found DOM element for instance:', instance.id);
408
+
409
+ // Validate variant still exists
410
+ let variantId = instance.variantId;
411
+ const variant = master.variants.find((v) => v.id === variantId);
412
+ if (!variant) {
413
+ // Variant was removed, fall back to default
414
+ console.warn(
415
+ `[propagateMasterChanges] Variant "${instance.variantId}" removed, using default`
416
+ );
417
+ variantId = master.defaultVariantId;
418
+ }
419
+
420
+ // CRITICAL: Detect DOM-level overrides BEFORE re-rendering
421
+ // This captures any user edits that haven't been saved as overrides yet
422
+ const effectiveVariant = master.variants.find((v) => v.id === variantId);
423
+ let detectedOverrides: ComponentOverride[] = [];
424
+ if (effectiveVariant) {
425
+ // Debug: Log what we're comparing
426
+ console.log('[propagateMasterChanges] === Override Detection Debug ===');
427
+ console.log('[propagateMasterChanges] DOM element:', {
428
+ tagName: domEl.tagName,
429
+ elementId: domEl.getAttribute('data-element-id'),
430
+ textContent: domEl.textContent?.substring(0, 100),
431
+ innerHTML: domEl.innerHTML?.substring(0, 200),
432
+ childrenCount: domEl.children.length,
433
+ });
434
+ console.log('[propagateMasterChanges] Master rootElement:', {
435
+ tagName: effectiveVariant.rootElement.tagName,
436
+ id: effectiveVariant.rootElement.id,
437
+ textContent: effectiveVariant.rootElement.textContent?.substring(0, 100),
438
+ innerHTML: effectiveVariant.rootElement.innerHTML?.substring(0, 200),
439
+ childrenCount: effectiveVariant.rootElement.children.length,
440
+ hasChildren: effectiveVariant.rootElement.children.length > 0,
441
+ });
442
+
443
+ try {
444
+ detectedOverrides = detectOverrides(domEl, effectiveVariant.rootElement);
445
+ console.log('[propagateMasterChanges] Detected DOM overrides:', {
446
+ instanceId: instance.id,
447
+ count: detectedOverrides.length,
448
+ overrides: detectedOverrides.map(o => ({
449
+ type: o.type,
450
+ targetElementId: o.targetElementId,
451
+ value: typeof o.value === 'string' ? o.value.substring(0, 50) : o.value,
452
+ })),
453
+ });
454
+ } catch (e) {
455
+ console.warn('[propagateMasterChanges] Failed to detect DOM overrides:', e);
456
+ }
457
+ }
458
+
459
+ // Merge: existing stored overrides + newly detected DOM overrides
460
+ // Detected overrides take precedence (they are the current state)
461
+ const existingOverrides = filterCompatibleOverrides(
462
+ instance.overrides,
463
+ master,
464
+ variantId
465
+ );
466
+
467
+ // Create a map of existing overrides by target+type for merging
468
+ const overrideMap = new Map<string, ComponentOverride>();
469
+ for (const override of existingOverrides) {
470
+ const key = `${override.targetElementId || override.elementPath}-${override.type}`;
471
+ overrideMap.set(key, override);
472
+ }
473
+ // Detected overrides take precedence
474
+ for (const override of detectedOverrides) {
475
+ const key = `${override.targetElementId || override.elementPath}-${override.type}`;
476
+ overrideMap.set(key, override);
477
+ }
478
+ const mergedOverrides = Array.from(overrideMap.values());
479
+
480
+ console.log('[propagateMasterChanges] Merged overrides:', {
481
+ instanceId: instance.id,
482
+ existing: existingOverrides.length,
483
+ detected: detectedOverrides.length,
484
+ merged: mergedOverrides.length,
485
+ });
486
+
487
+ // Create a temporary instance with merged overrides for rendering
488
+ const updatedInstance: ComponentInstance = {
489
+ ...instance,
490
+ variantId,
491
+ overrides: mergedOverrides,
492
+ };
493
+
494
+ // Collect unique ID mappings from old element before replacing
495
+ // (for duplicated instances that have data-master-element-id attributes)
496
+ const idMappings = new Map<string, string>();
497
+ const collectIdMappings = (el: HTMLElement) => {
498
+ const masterRefId = el.getAttribute('data-master-element-id');
499
+ const uniqueId = el.getAttribute('data-element-id');
500
+ if (masterRefId && uniqueId) {
501
+ idMappings.set(masterRefId, uniqueId);
502
+ }
503
+ // Recursively collect from children
504
+ el.querySelectorAll('[data-master-element-id]').forEach((child) => {
505
+ const childMasterRefId = child.getAttribute('data-master-element-id');
506
+ const childUniqueId = child.getAttribute('data-element-id');
507
+ if (childMasterRefId && childUniqueId) {
508
+ idMappings.set(childMasterRefId, childUniqueId);
509
+ }
510
+ });
511
+ };
512
+ collectIdMappings(domEl);
513
+
514
+ // Re-render the instance
515
+ const newEl = renderInstance(master, updatedInstance, iframeDoc);
516
+
517
+ // Apply unique ID mappings to the new element
518
+ // This preserves unique IDs for duplicated instances
519
+ if (idMappings.size > 0) {
520
+ const applyIdMappings = (el: HTMLElement) => {
521
+ const currentId = el.getAttribute('data-element-id');
522
+ if (currentId && idMappings.has(currentId)) {
523
+ const uniqueId = idMappings.get(currentId)!;
524
+ el.setAttribute('data-master-element-id', currentId);
525
+ el.setAttribute('data-element-id', uniqueId);
526
+ }
527
+ // Recursively apply to children
528
+ el.querySelectorAll('[data-element-id]').forEach((child) => {
529
+ const childEl = child as HTMLElement;
530
+ const childCurrentId = childEl.getAttribute('data-element-id');
531
+ if (childCurrentId && idMappings.has(childCurrentId)) {
532
+ const childUniqueId = idMappings.get(childCurrentId)!;
533
+ childEl.setAttribute('data-master-element-id', childCurrentId);
534
+ childEl.setAttribute('data-element-id', childUniqueId);
535
+ }
536
+ });
537
+ };
538
+ applyIdMappings(newEl);
539
+ console.log('[propagateMasterChanges] Applied ID mappings for duplicated instance:', {
540
+ instanceId: instance.id,
541
+ mappingsCount: idMappings.size,
542
+ });
543
+ }
544
+
545
+ // Preserve position from existing DOM element
546
+ // The DOM element may have position styles that aren't in the instance data
547
+ // (e.g., user moved the element but didn't save)
548
+ const existingStyle = domEl.getAttribute('style');
549
+ if (existingStyle) {
550
+ // Extract position-related styles from existing element
551
+ const positionProps = ['position', 'left', 'top', 'right', 'bottom', 'width', 'height'];
552
+ const styleObj: Record<string, string> = {};
553
+
554
+ existingStyle.split(';').forEach(pair => {
555
+ const [prop, value] = pair.split(':').map(s => s.trim());
556
+ if (prop && value && positionProps.includes(prop)) {
557
+ styleObj[prop] = value;
558
+ }
559
+ });
560
+
561
+ // Apply preserved position styles to new element
562
+ for (const [prop, value] of Object.entries(styleObj)) {
563
+ newEl.style.setProperty(prop, value);
564
+ }
565
+
566
+ console.log('[propagateMasterChanges] Preserved position styles:', {
567
+ instanceId: instance.id,
568
+ preservedStyles: styleObj,
569
+ });
570
+ }
571
+
572
+ // Replace the old element with the new one
573
+ domEl.parentNode?.replaceChild(newEl, domEl);
574
+
575
+ // Store the updated instance for persistence (with merged overrides)
576
+ result.updatedInstances.push(updatedInstance);
577
+ result.updatedCount++;
578
+ } catch (error) {
579
+ result.failedCount++;
580
+ result.failedInstanceIds.push(instance.id);
581
+ result.errors.push({
582
+ instanceId: instance.id,
583
+ error: error instanceof Error ? error : new Error(String(error)),
584
+ });
585
+ }
586
+ }
587
+
588
+ console.log(
589
+ `[propagateMasterChanges] Updated ${result.updatedCount} instances, failed ${result.failedCount}`
590
+ );
591
+
592
+ return result;
593
+ }
594
+
595
+ /**
596
+ * Reconcile instances after a master component update.
597
+ * Returns updated instances with filtered overrides.
598
+ *
599
+ * @param master - The updated master component
600
+ * @param instances - Array of instances to reconcile
601
+ * @returns Array of reconciled instances with valid overrides
602
+ */
603
+ export function reconcileInstances(
604
+ master: MasterComponent,
605
+ instances: ComponentInstance[]
606
+ ): ComponentInstance[] {
607
+ return instances.map((instance) => {
608
+ // Skip detached instances
609
+ if (instance.isDetached) {
610
+ return instance;
611
+ }
612
+
613
+ // Skip instances for other masters
614
+ if (instance.masterComponentId !== master.id) {
615
+ return instance;
616
+ }
617
+
618
+ // Check if variant still exists
619
+ let variantId = instance.variantId;
620
+ const variant = master.variants.find((v) => v.id === variantId);
621
+ if (!variant) {
622
+ // Fall back to default variant
623
+ variantId = master.defaultVariantId;
624
+ }
625
+
626
+ // Filter overrides
627
+ const validOverrides = filterCompatibleOverrides(
628
+ instance.overrides,
629
+ master,
630
+ variantId
631
+ );
632
+
633
+ // Return updated instance if changes were made
634
+ if (
635
+ variantId !== instance.variantId ||
636
+ validOverrides.length !== instance.overrides.length
637
+ ) {
638
+ return {
639
+ ...instance,
640
+ variantId,
641
+ overrides: validOverrides,
642
+ updatedAt: new Date().toISOString(),
643
+ };
644
+ }
645
+
646
+ return instance;
647
+ });
648
+ }
649
+
650
+ // ============================================================
651
+ // Instance Comparison
652
+ // ============================================================
653
+
654
+ /**
655
+ * Check if an instance has diverged from its master (has overrides).
656
+ *
657
+ * @param instance - The instance to check
658
+ * @returns True if the instance has any overrides
659
+ */
660
+ export function hasOverrides(instance: ComponentInstance): boolean {
661
+ return instance.overrides.length > 0;
662
+ }
663
+
664
+ /**
665
+ * Get the effective property values for an instance,
666
+ * combining master defaults with instance overrides.
667
+ *
668
+ * @param instance - The component instance
669
+ * @param master - The master component
670
+ * @returns Combined property values
671
+ */
672
+ export function getEffectivePropertyValues(
673
+ instance: ComponentInstance,
674
+ master: MasterComponent
675
+ ): Record<string, string | number | boolean> {
676
+ // Start with master defaults
677
+ const defaults: Record<string, string | number | boolean> = {};
678
+ for (const prop of master.exposedProperties) {
679
+ defaults[prop.id] = prop.defaultValue;
680
+ }
681
+
682
+ // Merge with instance values
683
+ return {
684
+ ...defaults,
685
+ ...instance.propertyValues,
686
+ };
687
+ }
688
+
689
+ // ============================================================
690
+ // Batch Operations
691
+ // ============================================================
692
+
693
+ /**
694
+ * Update multiple instances in a document at once.
695
+ * More efficient than updating one at a time.
696
+ *
697
+ * @param master - The master component
698
+ * @param instances - Array of instances to update
699
+ * @param iframeDoc - The iframe document
700
+ * @param updateFn - Function to apply updates to each instance
701
+ * @returns Sync result
702
+ */
703
+ export function batchUpdateInstances(
704
+ master: MasterComponent,
705
+ instances: ComponentInstance[],
706
+ iframeDoc: Document,
707
+ updateFn: (instance: ComponentInstance) => ComponentInstance
708
+ ): SyncResult {
709
+ // Apply updates to all instances first
710
+ const updatedInstances = instances.map(updateFn);
711
+
712
+ // Then propagate to DOM
713
+ return propagateMasterChanges(master, updatedInstances, iframeDoc);
714
+ }
715
+
716
+ /**
717
+ * Find all instances that would be affected by a master change.
718
+ *
719
+ * @param masterId - The master component ID
720
+ * @param instances - All instances to search
721
+ * @returns Array of affected instances
722
+ */
723
+ export function findAffectedInstances(
724
+ masterId: string,
725
+ instances: ComponentInstance[]
726
+ ): ComponentInstance[] {
727
+ return instances.filter(
728
+ (instance) =>
729
+ instance.masterComponentId === masterId && !instance.isDetached
730
+ );
731
+ }
732
+
733
+ // ============================================================
734
+ // Override Creation Helpers
735
+ // ============================================================
736
+
737
+ /**
738
+ * Create a text override for an element.
739
+ *
740
+ * @param targetElementId - The target element ID
741
+ * @param text - The new text content
742
+ * @returns A ComponentOverride object (without id)
743
+ */
744
+ export function createTextOverride(
745
+ targetElementId: string,
746
+ text: string
747
+ ): Omit<ComponentOverride, 'id'> {
748
+ return {
749
+ targetElementId,
750
+ type: 'text',
751
+ value: text,
752
+ };
753
+ }
754
+
755
+ /**
756
+ * Create a fill (background) override for an element.
757
+ *
758
+ * @param targetElementId - The target element ID
759
+ * @param color - The new background color
760
+ * @returns A ComponentOverride object (without id)
761
+ */
762
+ export function createFillOverride(
763
+ targetElementId: string,
764
+ color: string
765
+ ): Omit<ComponentOverride, 'id'> {
766
+ return {
767
+ targetElementId,
768
+ type: 'fill',
769
+ value: color,
770
+ };
771
+ }
772
+
773
+ /**
774
+ * Create a stroke (border) override for an element.
775
+ *
776
+ * @param targetElementId - The target element ID
777
+ * @param color - The new border color
778
+ * @returns A ComponentOverride object (without id)
779
+ */
780
+ export function createStrokeOverride(
781
+ targetElementId: string,
782
+ color: string
783
+ ): Omit<ComponentOverride, 'id'> {
784
+ return {
785
+ targetElementId,
786
+ type: 'stroke',
787
+ value: color,
788
+ };
789
+ }
790
+
791
+ /**
792
+ * Create a visibility override for an element.
793
+ *
794
+ * @param targetElementId - The target element ID
795
+ * @param visible - Whether the element should be visible
796
+ * @returns A ComponentOverride object (without id)
797
+ */
798
+ export function createVisibilityOverride(
799
+ targetElementId: string,
800
+ visible: boolean
801
+ ): Omit<ComponentOverride, 'id'> {
802
+ return {
803
+ targetElementId,
804
+ type: 'visibility',
805
+ value: visible,
806
+ };
807
+ }
808
+
809
+ /**
810
+ * Create an image override for an element.
811
+ *
812
+ * @param targetElementId - The target element ID
813
+ * @param url - The new image URL
814
+ * @param alt - Optional alt text
815
+ * @returns A ComponentOverride object (without id)
816
+ */
817
+ export function createImageOverride(
818
+ targetElementId: string,
819
+ url: string,
820
+ alt?: string
821
+ ): Omit<ComponentOverride, 'id'> {
822
+ return {
823
+ targetElementId,
824
+ type: 'image',
825
+ value: { url, alt },
826
+ };
827
+ }
828
+
829
+ /**
830
+ * Create a style override for an element.
831
+ *
832
+ * @param targetElementId - The target element ID
833
+ * @param styles - The style properties to override
834
+ * @returns A ComponentOverride object (without id)
835
+ */
836
+ export function createStyleOverride(
837
+ targetElementId: string,
838
+ styles: Record<string, string>
839
+ ): Omit<ComponentOverride, 'id'> {
840
+ return {
841
+ targetElementId,
842
+ type: 'style',
843
+ value: styles,
844
+ };
845
+ }