@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,1320 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EditorComponentsContext
|
|
5
|
+
*
|
|
6
|
+
* Manages the component system for the frontend editor.
|
|
7
|
+
* Handles master components, instances, overrides, and variants.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Master component CRUD operations
|
|
11
|
+
* - Instance management with DOM element ID mapping
|
|
12
|
+
* - Override system for instance customization
|
|
13
|
+
* - Variant switching
|
|
14
|
+
* - Instance resolution (master + overrides -> final element)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, {
|
|
18
|
+
createContext,
|
|
19
|
+
useContext,
|
|
20
|
+
useState,
|
|
21
|
+
useCallback,
|
|
22
|
+
useMemo,
|
|
23
|
+
useEffect,
|
|
24
|
+
useRef,
|
|
25
|
+
} from 'react';
|
|
26
|
+
import type {
|
|
27
|
+
MasterComponent,
|
|
28
|
+
ComponentInstance,
|
|
29
|
+
ComponentLibraryCategory,
|
|
30
|
+
ComponentOverride,
|
|
31
|
+
ComponentElement,
|
|
32
|
+
ComponentVariant,
|
|
33
|
+
EditorComponentsContextValue,
|
|
34
|
+
OverridableProperties,
|
|
35
|
+
} from '../../types/editor-components';
|
|
36
|
+
import {
|
|
37
|
+
saveMasterComponent as saveToFirestore,
|
|
38
|
+
getAllMasterComponents,
|
|
39
|
+
deleteMasterComponent as deleteFromFirestore,
|
|
40
|
+
updateMasterComponent as updateMasterComponentInFirestore,
|
|
41
|
+
saveComponentInstance,
|
|
42
|
+
updateComponentInstance,
|
|
43
|
+
deleteComponentInstance,
|
|
44
|
+
getPageComponentInstances,
|
|
45
|
+
} from '../../lib/firebase/editor-components';
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// Context Creation
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
const EditorComponentsContext =
|
|
52
|
+
createContext<EditorComponentsContextValue | null>(null);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hook to access the EditorComponentsContext.
|
|
56
|
+
* Must be used within an EditorComponentsProvider.
|
|
57
|
+
*/
|
|
58
|
+
export function useEditorComponents(): EditorComponentsContextValue {
|
|
59
|
+
const context = useContext(EditorComponentsContext);
|
|
60
|
+
if (!context) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'useEditorComponents must be used within EditorComponentsProvider'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return context;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================
|
|
69
|
+
// Helper Functions
|
|
70
|
+
// ============================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a unique ID for components and instances.
|
|
74
|
+
*/
|
|
75
|
+
function generateId(): string {
|
|
76
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Default overridable properties for component elements.
|
|
81
|
+
*/
|
|
82
|
+
const defaultOverridable: OverridableProperties = {
|
|
83
|
+
text: true,
|
|
84
|
+
fill: true,
|
|
85
|
+
stroke: true,
|
|
86
|
+
visibility: true,
|
|
87
|
+
image: true,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Position-related CSS properties that should be excluded from component root elements.
|
|
92
|
+
* These are applied by instance placement instead.
|
|
93
|
+
*/
|
|
94
|
+
const POSITION_STYLES = new Set([
|
|
95
|
+
'position', 'left', 'top', 'right', 'bottom',
|
|
96
|
+
'transform', 'zIndex', 'z-index'
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert an HTMLElement to a ComponentElement structure.
|
|
101
|
+
* Exported for use in component editing mode.
|
|
102
|
+
*
|
|
103
|
+
* Handles three cases for content:
|
|
104
|
+
* 1. Element has only text content (no child elements) -> uses textContent
|
|
105
|
+
* 2. Element has only child elements (no text nodes) -> uses children array
|
|
106
|
+
* 3. Element has mixed content (text + elements) -> uses innerHTML
|
|
107
|
+
*
|
|
108
|
+
* @param element - The HTML element to convert
|
|
109
|
+
* @param isRoot - Whether this is the root element (default: true for first call)
|
|
110
|
+
*/
|
|
111
|
+
export function htmlElementToComponentElement(element: HTMLElement, isRoot: boolean = true): ComponentElement {
|
|
112
|
+
// Preserve existing component element ID if present (for edit mode), otherwise generate new
|
|
113
|
+
const existingId = element.getAttribute('data-element-id');
|
|
114
|
+
const id = existingId || generateId();
|
|
115
|
+
|
|
116
|
+
// Extract class name using getAttribute to handle SVG elements correctly
|
|
117
|
+
// (element.className returns SVGAnimatedString for SVG elements)
|
|
118
|
+
const className = element.getAttribute('class') || '';
|
|
119
|
+
|
|
120
|
+
// Extract attributes (excluding class and style which are handled separately)
|
|
121
|
+
const attributes: Record<string, string> = {};
|
|
122
|
+
for (const attr of Array.from(element.attributes)) {
|
|
123
|
+
if (attr.name !== 'style' && attr.name !== 'class' && attr.name !== 'id' && !attr.name.startsWith('data-element') && !attr.name.startsWith('data-editable')) {
|
|
124
|
+
attributes[attr.name] = attr.value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Extract computed styles (only explicitly set ones)
|
|
129
|
+
// For root element: exclude position-related styles (applied by instance placement)
|
|
130
|
+
const styles: Record<string, string> = {};
|
|
131
|
+
const inlineStyle = element.getAttribute('style');
|
|
132
|
+
if (inlineStyle) {
|
|
133
|
+
const stylePairs = inlineStyle.split(';').filter((s) => s.trim());
|
|
134
|
+
for (const pair of stylePairs) {
|
|
135
|
+
// Handle styles with colons in values (like url(data:...) or https://...)
|
|
136
|
+
const colonIndex = pair.indexOf(':');
|
|
137
|
+
if (colonIndex === -1) continue;
|
|
138
|
+
const key = pair.slice(0, colonIndex).trim();
|
|
139
|
+
const value = pair.slice(colonIndex + 1).trim();
|
|
140
|
+
if (key && value) {
|
|
141
|
+
// Skip position-related styles for root element
|
|
142
|
+
if (isRoot && POSITION_STYLES.has(key)) {
|
|
143
|
+
console.log(`[htmlElementToComponentElement] Skipping position style for root: ${key}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Convert kebab-case to camelCase
|
|
147
|
+
const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
|
|
148
|
+
letter.toUpperCase()
|
|
149
|
+
);
|
|
150
|
+
styles[camelKey] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check for mixed content (text nodes + element nodes)
|
|
156
|
+
const hasElementChildren = element.children.length > 0;
|
|
157
|
+
const childNodes = Array.from(element.childNodes);
|
|
158
|
+
const hasTextNodes = childNodes.some(
|
|
159
|
+
node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim()
|
|
160
|
+
);
|
|
161
|
+
const hasMixedContent = hasElementChildren && hasTextNodes;
|
|
162
|
+
|
|
163
|
+
let textContent: string | undefined;
|
|
164
|
+
let innerHTML: string | undefined;
|
|
165
|
+
let children: ComponentElement[] = [];
|
|
166
|
+
|
|
167
|
+
if (hasMixedContent) {
|
|
168
|
+
// Mixed content: use innerHTML to preserve all content
|
|
169
|
+
innerHTML = element.innerHTML;
|
|
170
|
+
console.log('[htmlElementToComponentElement] Mixed content detected, using innerHTML');
|
|
171
|
+
} else if (hasElementChildren) {
|
|
172
|
+
// Only element children: recursively process (not root)
|
|
173
|
+
for (const child of Array.from(element.children)) {
|
|
174
|
+
children.push(htmlElementToComponentElement(child as HTMLElement, false));
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Only text content (or empty)
|
|
178
|
+
textContent = element.textContent?.trim() || undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result: ComponentElement = {
|
|
182
|
+
id,
|
|
183
|
+
tagName: element.tagName.toLowerCase(),
|
|
184
|
+
attributes,
|
|
185
|
+
className,
|
|
186
|
+
styles,
|
|
187
|
+
textContent,
|
|
188
|
+
innerHTML,
|
|
189
|
+
children,
|
|
190
|
+
overridable: { ...defaultOverridable },
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Debug logging
|
|
194
|
+
console.log('[htmlElementToComponentElement] Converted:', {
|
|
195
|
+
tagName: result.tagName,
|
|
196
|
+
className: result.className,
|
|
197
|
+
hasTextContent: !!result.textContent,
|
|
198
|
+
hasInnerHTML: !!result.innerHTML,
|
|
199
|
+
textContentPreview: result.textContent?.substring(0, 50),
|
|
200
|
+
innerHTMLPreview: result.innerHTML?.substring(0, 50),
|
|
201
|
+
childrenCount: result.children.length,
|
|
202
|
+
stylesCount: Object.keys(result.styles).length,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Deep clone a ComponentElement.
|
|
210
|
+
*/
|
|
211
|
+
function deepCloneElement(element: ComponentElement): ComponentElement {
|
|
212
|
+
return {
|
|
213
|
+
...element,
|
|
214
|
+
attributes: { ...element.attributes },
|
|
215
|
+
styles: { ...element.styles },
|
|
216
|
+
overridable: { ...element.overridable },
|
|
217
|
+
children: element.children.map(deepCloneElement),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get an element at a specific path within a ComponentElement tree.
|
|
223
|
+
* Path format: "children.0.children.1" or "root" for the root element.
|
|
224
|
+
*/
|
|
225
|
+
function getElementAtPath(
|
|
226
|
+
root: ComponentElement,
|
|
227
|
+
path: string
|
|
228
|
+
): ComponentElement | null {
|
|
229
|
+
if (path === 'root' || path === '') {
|
|
230
|
+
return root;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const parts = path.split('.');
|
|
234
|
+
let current: ComponentElement | undefined = root;
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
237
|
+
if (parts[i] !== 'children') {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const index = parseInt(parts[i + 1], 10);
|
|
241
|
+
if (isNaN(index) || !current?.children[index]) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
current = current.children[index];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return current || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Find element by ID in a ComponentElement tree.
|
|
252
|
+
*/
|
|
253
|
+
function findElementById(
|
|
254
|
+
root: ComponentElement,
|
|
255
|
+
targetId: string
|
|
256
|
+
): ComponentElement | null {
|
|
257
|
+
if (root.id === targetId) {
|
|
258
|
+
return root;
|
|
259
|
+
}
|
|
260
|
+
for (const child of root.children) {
|
|
261
|
+
const found = findElementById(child, targetId);
|
|
262
|
+
if (found) return found;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Apply a single override to a ComponentElement tree.
|
|
269
|
+
* Supports both path-based (elementPath) and ID-based (targetElementId) targeting.
|
|
270
|
+
*/
|
|
271
|
+
function applyOverrideToElement(
|
|
272
|
+
root: ComponentElement,
|
|
273
|
+
override: ComponentOverride
|
|
274
|
+
): void {
|
|
275
|
+
// Find target element - support both elementPath and targetElementId
|
|
276
|
+
let element: ComponentElement | null = null;
|
|
277
|
+
|
|
278
|
+
if (override.elementPath) {
|
|
279
|
+
element = getElementAtPath(root, override.elementPath);
|
|
280
|
+
} else if (override.targetElementId) {
|
|
281
|
+
element = findElementById(root, override.targetElementId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!element) {
|
|
285
|
+
console.warn(
|
|
286
|
+
`[EditorComponentsContext] Override target not found: ${override.elementPath || override.targetElementId}`
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
switch (override.type) {
|
|
292
|
+
case 'text':
|
|
293
|
+
element.textContent = override.value as string;
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'style':
|
|
297
|
+
const styleOverrides = override.value as Record<string, string>;
|
|
298
|
+
element.styles = { ...element.styles, ...styleOverrides };
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'attribute':
|
|
302
|
+
const attrOverrides = override.value as Record<string, string>;
|
|
303
|
+
element.attributes = { ...element.attributes, ...attrOverrides };
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case 'fill':
|
|
307
|
+
// Fill is a style override for background-color
|
|
308
|
+
element.styles = { ...element.styles, backgroundColor: override.value as string };
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case 'stroke':
|
|
312
|
+
// Stroke is a style override for border-color
|
|
313
|
+
element.styles = { ...element.styles, borderColor: override.value as string };
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case 'visibility':
|
|
317
|
+
if (override.value === false) {
|
|
318
|
+
element.styles = { ...element.styles, display: 'none' };
|
|
319
|
+
} else {
|
|
320
|
+
const { display, ...restStyles } = element.styles;
|
|
321
|
+
element.styles = restStyles;
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
case 'children':
|
|
326
|
+
element.children = override.value as ComponentElement[];
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'image':
|
|
330
|
+
// Image override for img elements
|
|
331
|
+
if (typeof override.value === 'object' && 'url' in override.value) {
|
|
332
|
+
element.attributes = { ...element.attributes, src: (override.value as { url: string }).url };
|
|
333
|
+
} else if (typeof override.value === 'string') {
|
|
334
|
+
element.attributes = { ...element.attributes, src: override.value };
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Convert a ComponentElement back to an HTMLElement.
|
|
342
|
+
*/
|
|
343
|
+
function componentElementToHtml(
|
|
344
|
+
element: ComponentElement,
|
|
345
|
+
doc: Document = document
|
|
346
|
+
): HTMLElement {
|
|
347
|
+
const el = doc.createElement(element.tagName);
|
|
348
|
+
|
|
349
|
+
// Set ID
|
|
350
|
+
el.id = element.id;
|
|
351
|
+
|
|
352
|
+
// Set className
|
|
353
|
+
if (element.className) {
|
|
354
|
+
el.className = element.className;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Set attributes
|
|
358
|
+
for (const [key, value] of Object.entries(element.attributes)) {
|
|
359
|
+
if (key !== 'id' && key !== 'class') {
|
|
360
|
+
el.setAttribute(key, value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Set styles
|
|
365
|
+
const styleString = Object.entries(element.styles)
|
|
366
|
+
.map(([key, value]) => {
|
|
367
|
+
// Convert camelCase to kebab-case
|
|
368
|
+
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
369
|
+
return `${kebabKey}: ${value}`;
|
|
370
|
+
})
|
|
371
|
+
.join('; ');
|
|
372
|
+
if (styleString) {
|
|
373
|
+
el.setAttribute('style', styleString);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Set text content or children
|
|
377
|
+
if (element.textContent !== undefined && element.children.length === 0) {
|
|
378
|
+
el.textContent = element.textContent;
|
|
379
|
+
} else {
|
|
380
|
+
for (const child of element.children) {
|
|
381
|
+
el.appendChild(componentElementToHtml(child, doc));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return el;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ============================================================
|
|
389
|
+
// Provider Props
|
|
390
|
+
// ============================================================
|
|
391
|
+
|
|
392
|
+
interface EditorComponentsProviderProps {
|
|
393
|
+
children: React.ReactNode;
|
|
394
|
+
websiteId?: string;
|
|
395
|
+
pageId?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ============================================================
|
|
399
|
+
// Provider Component
|
|
400
|
+
// ============================================================
|
|
401
|
+
|
|
402
|
+
export function EditorComponentsProvider({
|
|
403
|
+
children,
|
|
404
|
+
websiteId,
|
|
405
|
+
pageId,
|
|
406
|
+
}: EditorComponentsProviderProps) {
|
|
407
|
+
// State
|
|
408
|
+
const [masterComponents, setMasterComponents] = useState<
|
|
409
|
+
Map<string, MasterComponent>
|
|
410
|
+
>(new Map());
|
|
411
|
+
const [componentInstances, setComponentInstances] = useState<
|
|
412
|
+
Map<string, ComponentInstance>
|
|
413
|
+
>(new Map());
|
|
414
|
+
const [componentLibrary, setComponentLibrary] = useState<
|
|
415
|
+
ComponentLibraryCategory[]
|
|
416
|
+
>([]);
|
|
417
|
+
const [isLoadingComponents, setIsLoadingComponents] = useState(false);
|
|
418
|
+
const [selectedMasterComponentId, setSelectedMasterComponentId] = useState<
|
|
419
|
+
string | null
|
|
420
|
+
>(null);
|
|
421
|
+
const [selectedInstanceId, setSelectedInstanceId] = useState<string | null>(
|
|
422
|
+
null
|
|
423
|
+
);
|
|
424
|
+
const [error, setError] = useState<string | null>(null);
|
|
425
|
+
const [pendingNavigationTarget, setPendingNavigationTarget] = useState<string | null>(null);
|
|
426
|
+
|
|
427
|
+
// ============================================================
|
|
428
|
+
// Navigation
|
|
429
|
+
// ============================================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Request to navigate to a master component.
|
|
433
|
+
* This sets a pending navigation target that FrontendVisualEditor will observe
|
|
434
|
+
* to open the component panel and select the specified master.
|
|
435
|
+
*/
|
|
436
|
+
const navigateToMasterComponent = useCallback((masterComponentId: string) => {
|
|
437
|
+
setPendingNavigationTarget(masterComponentId);
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Clear the pending navigation request.
|
|
442
|
+
* Called by FrontendVisualEditor after handling the navigation.
|
|
443
|
+
*/
|
|
444
|
+
const clearNavigationRequest = useCallback(() => {
|
|
445
|
+
setPendingNavigationTarget(null);
|
|
446
|
+
}, []);
|
|
447
|
+
|
|
448
|
+
// ============================================================
|
|
449
|
+
// Loading
|
|
450
|
+
// ============================================================
|
|
451
|
+
|
|
452
|
+
// Default categories
|
|
453
|
+
// [移植時の修正] 汎用Web向け(Layout/Navigation/Form…)から、提案書スライドの
|
|
454
|
+
// デザイン部品(src/lib/design-parts.ts が登録する chrome2 の構成要素)に合わせたカテゴリへ変更。
|
|
455
|
+
const defaultCategories: ComponentLibraryCategory[] = useMemo(() => [
|
|
456
|
+
{
|
|
457
|
+
id: 'structure',
|
|
458
|
+
name: 'ページ構成',
|
|
459
|
+
description: 'ヘッダー・考察帯など、ページの骨格をつくる要素',
|
|
460
|
+
icon: 'layout',
|
|
461
|
+
color: '#0b6b3a',
|
|
462
|
+
order: 1,
|
|
463
|
+
componentIds: [],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: 'block',
|
|
467
|
+
name: 'ブロック',
|
|
468
|
+
description: 'パネル・キーポイントなど、情報をまとめる要素',
|
|
469
|
+
icon: 'file-text',
|
|
470
|
+
color: '#3b82f6',
|
|
471
|
+
order: 2,
|
|
472
|
+
componentIds: [],
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
id: 'data',
|
|
476
|
+
name: 'データ表現',
|
|
477
|
+
description: '数値・定義リストなど、根拠を見せる要素',
|
|
478
|
+
icon: 'bar-chart',
|
|
479
|
+
color: '#f59e0b',
|
|
480
|
+
order: 3,
|
|
481
|
+
componentIds: [],
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
id: 'label',
|
|
485
|
+
name: 'ラベル',
|
|
486
|
+
description: 'チップなどの区分ラベル',
|
|
487
|
+
icon: 'tag',
|
|
488
|
+
color: '#8b5cf6',
|
|
489
|
+
order: 4,
|
|
490
|
+
componentIds: [],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: 'media',
|
|
494
|
+
name: 'メディア',
|
|
495
|
+
description: '画像・図版',
|
|
496
|
+
icon: 'image',
|
|
497
|
+
color: '#ef4444',
|
|
498
|
+
order: 5,
|
|
499
|
+
componentIds: [],
|
|
500
|
+
},
|
|
501
|
+
], []);
|
|
502
|
+
|
|
503
|
+
const loadComponents = useCallback(async (targetWebsiteId: string) => {
|
|
504
|
+
setIsLoadingComponents(true);
|
|
505
|
+
setError(null);
|
|
506
|
+
try {
|
|
507
|
+
console.log(
|
|
508
|
+
`[EditorComponentsContext] Loading components for website: ${targetWebsiteId}`
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Load master components from Firestore
|
|
512
|
+
const components = await getAllMasterComponents(targetWebsiteId);
|
|
513
|
+
console.log(`[EditorComponentsContext] Loaded ${components.length} components`);
|
|
514
|
+
|
|
515
|
+
// Create Map from components
|
|
516
|
+
const componentsMap = new Map<string, MasterComponent>();
|
|
517
|
+
for (const component of components) {
|
|
518
|
+
componentsMap.set(component.id, component);
|
|
519
|
+
}
|
|
520
|
+
setMasterComponents(componentsMap);
|
|
521
|
+
|
|
522
|
+
// Build component library with loaded components
|
|
523
|
+
const categoryMap = new Map<string, string[]>();
|
|
524
|
+
for (const component of components) {
|
|
525
|
+
const categoryId = component.categoryId;
|
|
526
|
+
if (!categoryMap.has(categoryId)) {
|
|
527
|
+
categoryMap.set(categoryId, []);
|
|
528
|
+
}
|
|
529
|
+
categoryMap.get(categoryId)!.push(component.id);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Update categories with loaded component IDs
|
|
533
|
+
const updatedCategories = defaultCategories.map((cat) => ({
|
|
534
|
+
...cat,
|
|
535
|
+
componentIds: categoryMap.get(cat.id) || [],
|
|
536
|
+
}));
|
|
537
|
+
setComponentLibrary(updatedCategories);
|
|
538
|
+
|
|
539
|
+
setComponentInstances(new Map());
|
|
540
|
+
} catch (err) {
|
|
541
|
+
console.error('[EditorComponentsContext] Failed to load components:', err);
|
|
542
|
+
setError(err instanceof Error ? err.message : 'Failed to load components');
|
|
543
|
+
// Still set default categories even on error
|
|
544
|
+
setComponentLibrary(defaultCategories);
|
|
545
|
+
} finally {
|
|
546
|
+
setIsLoadingComponents(false);
|
|
547
|
+
}
|
|
548
|
+
}, [defaultCategories]);
|
|
549
|
+
|
|
550
|
+
// Initialize on mount or when websiteId changes
|
|
551
|
+
const loadedWebsiteIdRef = useRef<string | null>(null);
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
// Only load if websiteId exists and is different from the last loaded one
|
|
554
|
+
if (websiteId && websiteId !== loadedWebsiteIdRef.current) {
|
|
555
|
+
loadedWebsiteIdRef.current = websiteId;
|
|
556
|
+
loadComponents(websiteId);
|
|
557
|
+
} else if (!websiteId) {
|
|
558
|
+
// Reset to default categories if no websiteId
|
|
559
|
+
setComponentLibrary(defaultCategories);
|
|
560
|
+
setMasterComponents(new Map());
|
|
561
|
+
loadedWebsiteIdRef.current = null;
|
|
562
|
+
}
|
|
563
|
+
}, [websiteId, loadComponents, defaultCategories]);
|
|
564
|
+
|
|
565
|
+
// Load instances when pageId changes
|
|
566
|
+
const loadedPageIdRef = useRef<string | null>(null);
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (!websiteId || !pageId) {
|
|
569
|
+
setComponentInstances(new Map());
|
|
570
|
+
loadedPageIdRef.current = null;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Only load if pageId is different from last loaded
|
|
575
|
+
if (pageId === loadedPageIdRef.current) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
loadedPageIdRef.current = pageId;
|
|
580
|
+
|
|
581
|
+
const loadInstances = async () => {
|
|
582
|
+
try {
|
|
583
|
+
console.log(`[EditorComponentsContext] Loading instances for page: ${pageId}`);
|
|
584
|
+
const instances = await getPageComponentInstances(websiteId, pageId);
|
|
585
|
+
console.log(`[EditorComponentsContext] Loaded ${instances.length} instances`);
|
|
586
|
+
|
|
587
|
+
// Create Map from instances (keyed by domElementId)
|
|
588
|
+
const instancesMap = new Map<string, ComponentInstance>();
|
|
589
|
+
for (const instance of instances) {
|
|
590
|
+
instancesMap.set(instance.domElementId, instance);
|
|
591
|
+
}
|
|
592
|
+
setComponentInstances(instancesMap);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
console.error('[EditorComponentsContext] Failed to load instances:', err);
|
|
595
|
+
setError(err instanceof Error ? err.message : 'Failed to load instances');
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
loadInstances();
|
|
600
|
+
}, [websiteId, pageId]);
|
|
601
|
+
|
|
602
|
+
// ============================================================
|
|
603
|
+
// Master Component Operations
|
|
604
|
+
// ============================================================
|
|
605
|
+
|
|
606
|
+
const createMasterComponent = useCallback(
|
|
607
|
+
(element: HTMLElement, name: string, categoryId: string): MasterComponent => {
|
|
608
|
+
const id = generateId();
|
|
609
|
+
const now = new Date().toISOString();
|
|
610
|
+
|
|
611
|
+
// Convert HTML element to component element
|
|
612
|
+
const rootElement = htmlElementToComponentElement(element);
|
|
613
|
+
|
|
614
|
+
// Create default variant
|
|
615
|
+
const defaultVariant: ComponentVariant = {
|
|
616
|
+
id: generateId(),
|
|
617
|
+
name: 'Default',
|
|
618
|
+
description: 'Default variant',
|
|
619
|
+
rootElement,
|
|
620
|
+
isDefault: true,
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const masterComponent: MasterComponent = {
|
|
624
|
+
id,
|
|
625
|
+
name,
|
|
626
|
+
categoryId,
|
|
627
|
+
tags: [],
|
|
628
|
+
variants: [defaultVariant],
|
|
629
|
+
defaultVariantId: defaultVariant.id,
|
|
630
|
+
exposedProperties: [],
|
|
631
|
+
createdAt: now,
|
|
632
|
+
updatedAt: now,
|
|
633
|
+
websiteId: websiteId || '',
|
|
634
|
+
version: 1,
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
setMasterComponents((prev) => {
|
|
638
|
+
const next = new Map(prev);
|
|
639
|
+
next.set(id, masterComponent);
|
|
640
|
+
return next;
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Add to category
|
|
644
|
+
setComponentLibrary((prev) =>
|
|
645
|
+
prev.map((cat) =>
|
|
646
|
+
cat.id === categoryId
|
|
647
|
+
? { ...cat, componentIds: [...cat.componentIds, id] }
|
|
648
|
+
: cat
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Save to Firestore asynchronously
|
|
653
|
+
if (websiteId) {
|
|
654
|
+
saveToFirestore(websiteId, masterComponent).catch((err) => {
|
|
655
|
+
console.error('[EditorComponentsContext] Failed to save component to Firestore:', err);
|
|
656
|
+
setError('Failed to save component');
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log(
|
|
661
|
+
`[EditorComponentsContext] Created master component: ${name} (${id})`
|
|
662
|
+
);
|
|
663
|
+
return masterComponent;
|
|
664
|
+
},
|
|
665
|
+
[websiteId]
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const updateMasterComponent = useCallback(
|
|
669
|
+
(id: string, updates: Partial<MasterComponent>) => {
|
|
670
|
+
setMasterComponents((prev) => {
|
|
671
|
+
const component = prev.get(id);
|
|
672
|
+
if (!component) {
|
|
673
|
+
console.warn(
|
|
674
|
+
`[EditorComponentsContext] Master component not found: ${id}`
|
|
675
|
+
);
|
|
676
|
+
return prev;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const updatedComponent = {
|
|
680
|
+
...component,
|
|
681
|
+
...updates,
|
|
682
|
+
updatedAt: new Date().toISOString(),
|
|
683
|
+
version: component.version + 1,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const next = new Map(prev);
|
|
687
|
+
next.set(id, updatedComponent);
|
|
688
|
+
|
|
689
|
+
// Save to Firestore asynchronously
|
|
690
|
+
if (websiteId) {
|
|
691
|
+
updateMasterComponentInFirestore(websiteId, id, updates).catch((err) => {
|
|
692
|
+
console.error('[EditorComponentsContext] Failed to update component in Firestore:', err);
|
|
693
|
+
setError('Failed to save component changes');
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return next;
|
|
698
|
+
});
|
|
699
|
+
},
|
|
700
|
+
[websiteId]
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const deleteMasterComponent = useCallback((id: string) => {
|
|
704
|
+
setMasterComponents((prev) => {
|
|
705
|
+
const next = new Map(prev);
|
|
706
|
+
next.delete(id);
|
|
707
|
+
return next;
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Remove from category
|
|
711
|
+
setComponentLibrary((prev) =>
|
|
712
|
+
prev.map((cat) => ({
|
|
713
|
+
...cat,
|
|
714
|
+
componentIds: cat.componentIds.filter((cid) => cid !== id),
|
|
715
|
+
}))
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
// Delete all instances of this master
|
|
719
|
+
setComponentInstances((prev) => {
|
|
720
|
+
const next = new Map(prev);
|
|
721
|
+
for (const [instanceId, instance] of prev) {
|
|
722
|
+
if (instance.masterComponentId === id) {
|
|
723
|
+
next.delete(instanceId);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return next;
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Delete from Firestore asynchronously
|
|
730
|
+
if (websiteId) {
|
|
731
|
+
deleteFromFirestore(websiteId, id).catch((err) => {
|
|
732
|
+
console.error('[EditorComponentsContext] Failed to delete component from Firestore:', err);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
console.log(`[EditorComponentsContext] Deleted master component: ${id}`);
|
|
737
|
+
}, [websiteId]);
|
|
738
|
+
|
|
739
|
+
const getMasterComponent = useCallback(
|
|
740
|
+
(id: string): MasterComponent | null => {
|
|
741
|
+
return masterComponents.get(id) || null;
|
|
742
|
+
},
|
|
743
|
+
[masterComponents]
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// ============================================================
|
|
747
|
+
// Instance Operations
|
|
748
|
+
// ============================================================
|
|
749
|
+
|
|
750
|
+
const createInstance = useCallback(
|
|
751
|
+
(
|
|
752
|
+
masterComponentId: string,
|
|
753
|
+
variantId?: string,
|
|
754
|
+
instancePageId?: string,
|
|
755
|
+
/** Optional: pass the master component directly if it's not yet in state */
|
|
756
|
+
providedMaster?: MasterComponent,
|
|
757
|
+
/** Optional: specify domElementId (for duplication) */
|
|
758
|
+
customDomElementId?: string,
|
|
759
|
+
/** Optional: initial overrides to copy (for duplication) */
|
|
760
|
+
initialOverrides?: ComponentOverride[],
|
|
761
|
+
/** Optional: initial property values to copy (for duplication) */
|
|
762
|
+
initialPropertyValues?: Record<string, string | number | boolean>,
|
|
763
|
+
/** Optional: initial position for the instance */
|
|
764
|
+
initialPosition?: { x: number; y: number },
|
|
765
|
+
/** Optional: initial size for the instance */
|
|
766
|
+
initialSize?: { width: number; height: number }
|
|
767
|
+
): ComponentInstance => {
|
|
768
|
+
const master = providedMaster || masterComponents.get(masterComponentId);
|
|
769
|
+
if (!master) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`Master component not found: ${masterComponentId}`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const id = generateId();
|
|
776
|
+
const domElementId = customDomElementId || `component-instance-${id}`;
|
|
777
|
+
const now = new Date().toISOString();
|
|
778
|
+
const effectivePageId = instancePageId || pageId;
|
|
779
|
+
|
|
780
|
+
const instance: ComponentInstance = {
|
|
781
|
+
id,
|
|
782
|
+
masterComponentId,
|
|
783
|
+
variantId: variantId || master.defaultVariantId,
|
|
784
|
+
domElementId,
|
|
785
|
+
overrides: initialOverrides ? [...initialOverrides] : [],
|
|
786
|
+
propertyValues: initialPropertyValues ? { ...initialPropertyValues } : {},
|
|
787
|
+
isDetached: false,
|
|
788
|
+
pageId: effectivePageId,
|
|
789
|
+
position: initialPosition,
|
|
790
|
+
size: initialSize,
|
|
791
|
+
createdAt: now,
|
|
792
|
+
updatedAt: now,
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
setComponentInstances((prev) => {
|
|
796
|
+
const next = new Map(prev);
|
|
797
|
+
next.set(domElementId, instance);
|
|
798
|
+
return next;
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Save to Firestore asynchronously
|
|
802
|
+
if (websiteId && effectivePageId) {
|
|
803
|
+
saveComponentInstance(websiteId, effectivePageId, instance).catch((err) => {
|
|
804
|
+
console.error('[EditorComponentsContext] Failed to save instance to Firestore:', err);
|
|
805
|
+
setError('Failed to save component instance');
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
console.log(
|
|
810
|
+
`[EditorComponentsContext] Created instance: ${id} of master ${masterComponentId}`
|
|
811
|
+
);
|
|
812
|
+
return instance;
|
|
813
|
+
},
|
|
814
|
+
[masterComponents, websiteId, pageId]
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
const updateInstance = useCallback(
|
|
818
|
+
(instanceId: string, updates: Partial<ComponentInstance>) => {
|
|
819
|
+
setComponentInstances((prev) => {
|
|
820
|
+
// Find instance by ID (not domElementId)
|
|
821
|
+
let targetKey: string | null = null;
|
|
822
|
+
let foundInstance: ComponentInstance | null = null;
|
|
823
|
+
for (const [key, instance] of prev) {
|
|
824
|
+
if (instance.id === instanceId) {
|
|
825
|
+
targetKey = key;
|
|
826
|
+
foundInstance = instance;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (!targetKey || !foundInstance) {
|
|
832
|
+
console.warn(
|
|
833
|
+
`[EditorComponentsContext] Instance not found: ${instanceId}`
|
|
834
|
+
);
|
|
835
|
+
return prev;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const updatedInstance = {
|
|
839
|
+
...foundInstance,
|
|
840
|
+
...updates,
|
|
841
|
+
updatedAt: new Date().toISOString(),
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const next = new Map(prev);
|
|
845
|
+
next.set(targetKey, updatedInstance);
|
|
846
|
+
|
|
847
|
+
// Save to Firestore asynchronously
|
|
848
|
+
const instancePageId = updatedInstance.pageId || pageId;
|
|
849
|
+
if (websiteId && instancePageId) {
|
|
850
|
+
updateComponentInstance(websiteId, instancePageId, instanceId, updates).catch((err) => {
|
|
851
|
+
console.error('[EditorComponentsContext] Failed to update instance in Firestore:', err);
|
|
852
|
+
setError('Failed to save instance changes');
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return next;
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
[websiteId, pageId]
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
const deleteInstance = useCallback((instanceId: string) => {
|
|
863
|
+
setComponentInstances((prev) => {
|
|
864
|
+
// Find instance by ID
|
|
865
|
+
let targetKey: string | null = null;
|
|
866
|
+
let foundInstance: ComponentInstance | null = null;
|
|
867
|
+
for (const [key, instance] of prev) {
|
|
868
|
+
if (instance.id === instanceId) {
|
|
869
|
+
targetKey = key;
|
|
870
|
+
foundInstance = instance;
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (!targetKey || !foundInstance) {
|
|
876
|
+
console.warn(
|
|
877
|
+
`[EditorComponentsContext] Instance not found for deletion: ${instanceId}`
|
|
878
|
+
);
|
|
879
|
+
return prev;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const next = new Map(prev);
|
|
883
|
+
next.delete(targetKey);
|
|
884
|
+
|
|
885
|
+
// Delete from Firestore asynchronously
|
|
886
|
+
const instancePageId = foundInstance.pageId || pageId;
|
|
887
|
+
if (websiteId && instancePageId) {
|
|
888
|
+
deleteComponentInstance(websiteId, instancePageId, instanceId).catch((err) => {
|
|
889
|
+
console.error('[EditorComponentsContext] Failed to delete instance from Firestore:', err);
|
|
890
|
+
setError('Failed to delete component instance');
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return next;
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
console.log(`[EditorComponentsContext] Deleted instance: ${instanceId}`);
|
|
898
|
+
}, [websiteId, pageId]);
|
|
899
|
+
|
|
900
|
+
const getInstanceByDomId = useCallback(
|
|
901
|
+
(domElementId: string): ComponentInstance | null => {
|
|
902
|
+
return componentInstances.get(domElementId) || null;
|
|
903
|
+
},
|
|
904
|
+
[componentInstances]
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// ============================================================
|
|
908
|
+
// Override Operations
|
|
909
|
+
// ============================================================
|
|
910
|
+
|
|
911
|
+
const addOverride = useCallback(
|
|
912
|
+
(instanceId: string, override: Omit<ComponentOverride, 'id'>) => {
|
|
913
|
+
const newOverride: ComponentOverride = {
|
|
914
|
+
...override,
|
|
915
|
+
id: generateId(),
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
setComponentInstances((prev) => {
|
|
919
|
+
// Find instance by ID
|
|
920
|
+
let targetKey: string | null = null;
|
|
921
|
+
let foundInstance: ComponentInstance | null = null;
|
|
922
|
+
for (const [key, instance] of prev) {
|
|
923
|
+
if (instance.id === instanceId) {
|
|
924
|
+
targetKey = key;
|
|
925
|
+
foundInstance = instance;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (!targetKey || !foundInstance) {
|
|
931
|
+
console.warn(
|
|
932
|
+
`[EditorComponentsContext] Instance not found for override: ${instanceId}`
|
|
933
|
+
);
|
|
934
|
+
return prev;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const newOverrides = [...foundInstance.overrides, newOverride];
|
|
938
|
+
const updatedInstance = {
|
|
939
|
+
...foundInstance,
|
|
940
|
+
overrides: newOverrides,
|
|
941
|
+
updatedAt: new Date().toISOString(),
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const next = new Map(prev);
|
|
945
|
+
next.set(targetKey, updatedInstance);
|
|
946
|
+
|
|
947
|
+
// Save to Firestore asynchronously
|
|
948
|
+
const instancePageId = foundInstance.pageId || pageId;
|
|
949
|
+
if (websiteId && instancePageId) {
|
|
950
|
+
updateComponentInstance(websiteId, instancePageId, instanceId, { overrides: newOverrides }).catch((err) => {
|
|
951
|
+
console.error('[EditorComponentsContext] Failed to save override to Firestore:', err);
|
|
952
|
+
setError('Failed to save override');
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return next;
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
console.log(
|
|
960
|
+
`[EditorComponentsContext] Added override to instance: ${instanceId}`
|
|
961
|
+
);
|
|
962
|
+
},
|
|
963
|
+
[websiteId, pageId]
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
const removeOverride = useCallback(
|
|
967
|
+
(instanceId: string, overrideId: string) => {
|
|
968
|
+
setComponentInstances((prev) => {
|
|
969
|
+
// Find instance by ID
|
|
970
|
+
let targetKey: string | null = null;
|
|
971
|
+
let foundInstance: ComponentInstance | null = null;
|
|
972
|
+
for (const [key, instance] of prev) {
|
|
973
|
+
if (instance.id === instanceId) {
|
|
974
|
+
targetKey = key;
|
|
975
|
+
foundInstance = instance;
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!targetKey || !foundInstance) {
|
|
981
|
+
console.warn(
|
|
982
|
+
`[EditorComponentsContext] Instance not found for override removal: ${instanceId}`
|
|
983
|
+
);
|
|
984
|
+
return prev;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const newOverrides = foundInstance.overrides.filter((o) => o.id !== overrideId);
|
|
988
|
+
const updatedInstance = {
|
|
989
|
+
...foundInstance,
|
|
990
|
+
overrides: newOverrides,
|
|
991
|
+
updatedAt: new Date().toISOString(),
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const next = new Map(prev);
|
|
995
|
+
next.set(targetKey, updatedInstance);
|
|
996
|
+
|
|
997
|
+
// Save to Firestore asynchronously
|
|
998
|
+
const instancePageId = foundInstance.pageId || pageId;
|
|
999
|
+
if (websiteId && instancePageId) {
|
|
1000
|
+
updateComponentInstance(websiteId, instancePageId, instanceId, { overrides: newOverrides }).catch((err) => {
|
|
1001
|
+
console.error('[EditorComponentsContext] Failed to remove override in Firestore:', err);
|
|
1002
|
+
setError('Failed to remove override');
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return next;
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
console.log(
|
|
1010
|
+
`[EditorComponentsContext] Removed override ${overrideId} from instance: ${instanceId}`
|
|
1011
|
+
);
|
|
1012
|
+
},
|
|
1013
|
+
[websiteId, pageId]
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
const resetAllOverrides = useCallback((instanceId: string) => {
|
|
1017
|
+
setComponentInstances((prev) => {
|
|
1018
|
+
// Find instance by ID
|
|
1019
|
+
let targetKey: string | null = null;
|
|
1020
|
+
let foundInstance: ComponentInstance | null = null;
|
|
1021
|
+
for (const [key, instance] of prev) {
|
|
1022
|
+
if (instance.id === instanceId) {
|
|
1023
|
+
targetKey = key;
|
|
1024
|
+
foundInstance = instance;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (!targetKey || !foundInstance) {
|
|
1030
|
+
console.warn(
|
|
1031
|
+
`[EditorComponentsContext] Instance not found for reset: ${instanceId}`
|
|
1032
|
+
);
|
|
1033
|
+
return prev;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const updatedInstance = {
|
|
1037
|
+
...foundInstance,
|
|
1038
|
+
overrides: [],
|
|
1039
|
+
propertyValues: {},
|
|
1040
|
+
updatedAt: new Date().toISOString(),
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const next = new Map(prev);
|
|
1044
|
+
next.set(targetKey, updatedInstance);
|
|
1045
|
+
|
|
1046
|
+
// Save to Firestore asynchronously
|
|
1047
|
+
const instancePageId = foundInstance.pageId || pageId;
|
|
1048
|
+
if (websiteId && instancePageId) {
|
|
1049
|
+
updateComponentInstance(websiteId, instancePageId, instanceId, {
|
|
1050
|
+
overrides: [],
|
|
1051
|
+
propertyValues: {}
|
|
1052
|
+
}).catch((err) => {
|
|
1053
|
+
console.error('[EditorComponentsContext] Failed to reset overrides in Firestore:', err);
|
|
1054
|
+
setError('Failed to reset overrides');
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return next;
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
console.log(
|
|
1062
|
+
`[EditorComponentsContext] Reset all overrides for instance: ${instanceId}`
|
|
1063
|
+
);
|
|
1064
|
+
}, [websiteId, pageId]);
|
|
1065
|
+
|
|
1066
|
+
// ============================================================
|
|
1067
|
+
// Variant Operations
|
|
1068
|
+
// ============================================================
|
|
1069
|
+
|
|
1070
|
+
const changeVariant = useCallback(
|
|
1071
|
+
(instanceId: string, variantId: string) => {
|
|
1072
|
+
setComponentInstances((prev) => {
|
|
1073
|
+
// Find instance by ID
|
|
1074
|
+
let targetKey: string | null = null;
|
|
1075
|
+
let foundInstance: ComponentInstance | null = null;
|
|
1076
|
+
for (const [key, instance] of prev) {
|
|
1077
|
+
if (instance.id === instanceId) {
|
|
1078
|
+
targetKey = key;
|
|
1079
|
+
foundInstance = instance;
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (!targetKey || !foundInstance) {
|
|
1085
|
+
console.warn(
|
|
1086
|
+
`[EditorComponentsContext] Instance not found for variant change: ${instanceId}`
|
|
1087
|
+
);
|
|
1088
|
+
return prev;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Verify variant exists in master
|
|
1092
|
+
const master = masterComponents.get(foundInstance.masterComponentId);
|
|
1093
|
+
if (!master) {
|
|
1094
|
+
console.warn(
|
|
1095
|
+
`[EditorComponentsContext] Master component not found: ${foundInstance.masterComponentId}`
|
|
1096
|
+
);
|
|
1097
|
+
return prev;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const variantExists = master.variants.some((v) => v.id === variantId);
|
|
1101
|
+
if (!variantExists) {
|
|
1102
|
+
console.warn(
|
|
1103
|
+
`[EditorComponentsContext] Variant not found: ${variantId}`
|
|
1104
|
+
);
|
|
1105
|
+
return prev;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const updatedInstance = {
|
|
1109
|
+
...foundInstance,
|
|
1110
|
+
variantId,
|
|
1111
|
+
// Reset overrides when changing variants (optional, could be configurable)
|
|
1112
|
+
overrides: [],
|
|
1113
|
+
updatedAt: new Date().toISOString(),
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const next = new Map(prev);
|
|
1117
|
+
next.set(targetKey, updatedInstance);
|
|
1118
|
+
|
|
1119
|
+
// Save to Firestore asynchronously
|
|
1120
|
+
const instancePageId = foundInstance.pageId || pageId;
|
|
1121
|
+
if (websiteId && instancePageId) {
|
|
1122
|
+
updateComponentInstance(websiteId, instancePageId, instanceId, {
|
|
1123
|
+
variantId,
|
|
1124
|
+
overrides: []
|
|
1125
|
+
}).catch((err) => {
|
|
1126
|
+
console.error('[EditorComponentsContext] Failed to change variant in Firestore:', err);
|
|
1127
|
+
setError('Failed to change variant');
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return next;
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
console.log(
|
|
1135
|
+
`[EditorComponentsContext] Changed variant for instance ${instanceId} to ${variantId}`
|
|
1136
|
+
);
|
|
1137
|
+
},
|
|
1138
|
+
[masterComponents, websiteId, pageId]
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
// ============================================================
|
|
1142
|
+
// Detach Operation
|
|
1143
|
+
// ============================================================
|
|
1144
|
+
|
|
1145
|
+
const detachInstance = useCallback(
|
|
1146
|
+
(instanceId: string): HTMLElement => {
|
|
1147
|
+
// Find instance
|
|
1148
|
+
let instance: ComponentInstance | null = null;
|
|
1149
|
+
let targetKey: string | null = null;
|
|
1150
|
+
for (const [key, inst] of componentInstances) {
|
|
1151
|
+
if (inst.id === instanceId) {
|
|
1152
|
+
instance = inst;
|
|
1153
|
+
targetKey = key;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (!instance || !targetKey) {
|
|
1159
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Get master component
|
|
1163
|
+
const master = masterComponents.get(instance.masterComponentId);
|
|
1164
|
+
if (!master) {
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
`Master component not found: ${instance.masterComponentId}`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Resolve the instance to get the final element
|
|
1171
|
+
const resolved = resolveInstance(master, instance);
|
|
1172
|
+
|
|
1173
|
+
// Convert to HTMLElement
|
|
1174
|
+
const htmlElement = componentElementToHtml(resolved);
|
|
1175
|
+
|
|
1176
|
+
// Mark instance as detached
|
|
1177
|
+
setComponentInstances((prev) => {
|
|
1178
|
+
const next = new Map(prev);
|
|
1179
|
+
next.set(targetKey!, {
|
|
1180
|
+
...instance!,
|
|
1181
|
+
isDetached: true,
|
|
1182
|
+
updatedAt: new Date().toISOString(),
|
|
1183
|
+
});
|
|
1184
|
+
return next;
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// Save to Firestore asynchronously
|
|
1188
|
+
const instancePageId = instance.pageId || pageId;
|
|
1189
|
+
if (websiteId && instancePageId) {
|
|
1190
|
+
updateComponentInstance(websiteId, instancePageId, instanceId, {
|
|
1191
|
+
isDetached: true
|
|
1192
|
+
}).catch((err) => {
|
|
1193
|
+
console.error('[EditorComponentsContext] Failed to detach instance in Firestore:', err);
|
|
1194
|
+
setError('Failed to detach instance');
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
console.log(
|
|
1199
|
+
`[EditorComponentsContext] Detached instance: ${instanceId}`
|
|
1200
|
+
);
|
|
1201
|
+
return htmlElement;
|
|
1202
|
+
},
|
|
1203
|
+
[componentInstances, masterComponents, websiteId, pageId]
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
// ============================================================
|
|
1207
|
+
// Resolution
|
|
1208
|
+
// ============================================================
|
|
1209
|
+
|
|
1210
|
+
const resolveInstance = useCallback(
|
|
1211
|
+
(master: MasterComponent, instance: ComponentInstance): ComponentElement => {
|
|
1212
|
+
// Find the variant
|
|
1213
|
+
const variant = master.variants.find((v) => v.id === instance.variantId);
|
|
1214
|
+
if (!variant) {
|
|
1215
|
+
console.warn(
|
|
1216
|
+
`[EditorComponentsContext] Variant not found: ${instance.variantId}, using default`
|
|
1217
|
+
);
|
|
1218
|
+
const defaultVariant = master.variants.find(
|
|
1219
|
+
(v) => v.id === master.defaultVariantId
|
|
1220
|
+
);
|
|
1221
|
+
if (!defaultVariant) {
|
|
1222
|
+
throw new Error(
|
|
1223
|
+
`No variants found for master component: ${master.id}`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
return deepCloneElement(defaultVariant.rootElement);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Deep clone the variant's root element
|
|
1230
|
+
const resolved = deepCloneElement(variant.rootElement);
|
|
1231
|
+
|
|
1232
|
+
// Set the instance's DOM element ID
|
|
1233
|
+
resolved.id = instance.domElementId;
|
|
1234
|
+
|
|
1235
|
+
// Add data attribute to identify as component instance
|
|
1236
|
+
resolved.attributes['data-component-instance'] = instance.id;
|
|
1237
|
+
resolved.attributes['data-component-master'] = master.id;
|
|
1238
|
+
|
|
1239
|
+
// Apply all overrides
|
|
1240
|
+
for (const override of instance.overrides) {
|
|
1241
|
+
applyOverrideToElement(resolved, override);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return resolved;
|
|
1245
|
+
},
|
|
1246
|
+
[]
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
// ============================================================
|
|
1250
|
+
// Context Value
|
|
1251
|
+
// ============================================================
|
|
1252
|
+
|
|
1253
|
+
const value: EditorComponentsContextValue = useMemo(
|
|
1254
|
+
() => ({
|
|
1255
|
+
// State
|
|
1256
|
+
masterComponents,
|
|
1257
|
+
componentInstances,
|
|
1258
|
+
componentLibrary,
|
|
1259
|
+
isLoadingComponents,
|
|
1260
|
+
selectedMasterComponentId,
|
|
1261
|
+
selectedInstanceId,
|
|
1262
|
+
error,
|
|
1263
|
+
pendingNavigationTarget,
|
|
1264
|
+
|
|
1265
|
+
// Actions
|
|
1266
|
+
loadComponents,
|
|
1267
|
+
createMasterComponent,
|
|
1268
|
+
updateMasterComponent,
|
|
1269
|
+
deleteMasterComponent,
|
|
1270
|
+
getMasterComponent,
|
|
1271
|
+
createInstance,
|
|
1272
|
+
updateInstance,
|
|
1273
|
+
deleteInstance,
|
|
1274
|
+
getInstanceByDomId,
|
|
1275
|
+
addOverride,
|
|
1276
|
+
removeOverride,
|
|
1277
|
+
resetAllOverrides,
|
|
1278
|
+
changeVariant,
|
|
1279
|
+
detachInstance,
|
|
1280
|
+
resolveInstance,
|
|
1281
|
+
navigateToMasterComponent,
|
|
1282
|
+
clearNavigationRequest,
|
|
1283
|
+
}),
|
|
1284
|
+
[
|
|
1285
|
+
masterComponents,
|
|
1286
|
+
componentInstances,
|
|
1287
|
+
componentLibrary,
|
|
1288
|
+
isLoadingComponents,
|
|
1289
|
+
selectedMasterComponentId,
|
|
1290
|
+
selectedInstanceId,
|
|
1291
|
+
error,
|
|
1292
|
+
pendingNavigationTarget,
|
|
1293
|
+
loadComponents,
|
|
1294
|
+
createMasterComponent,
|
|
1295
|
+
updateMasterComponent,
|
|
1296
|
+
deleteMasterComponent,
|
|
1297
|
+
getMasterComponent,
|
|
1298
|
+
createInstance,
|
|
1299
|
+
updateInstance,
|
|
1300
|
+
deleteInstance,
|
|
1301
|
+
getInstanceByDomId,
|
|
1302
|
+
addOverride,
|
|
1303
|
+
removeOverride,
|
|
1304
|
+
resetAllOverrides,
|
|
1305
|
+
changeVariant,
|
|
1306
|
+
detachInstance,
|
|
1307
|
+
resolveInstance,
|
|
1308
|
+
navigateToMasterComponent,
|
|
1309
|
+
clearNavigationRequest,
|
|
1310
|
+
]
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
return (
|
|
1314
|
+
<EditorComponentsContext.Provider value={value}>
|
|
1315
|
+
{children}
|
|
1316
|
+
</EditorComponentsContext.Provider>
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
export default EditorComponentsContext;
|