@growgroup/visual-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,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
|
+
}
|