@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,515 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useComponentInstances Hook
|
|
5
|
+
*
|
|
6
|
+
* Manages component instances on a page, including loading from Firestore,
|
|
7
|
+
* creating/updating/deleting instances, and syncing with the DOM.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useState, useRef } from 'react';
|
|
11
|
+
import type {
|
|
12
|
+
ComponentInstance,
|
|
13
|
+
ComponentOverride,
|
|
14
|
+
MasterComponent,
|
|
15
|
+
} from '../../types/editor-components';
|
|
16
|
+
import {
|
|
17
|
+
getPageComponentInstances,
|
|
18
|
+
saveComponentInstance,
|
|
19
|
+
updateComponentInstance,
|
|
20
|
+
deleteComponentInstance,
|
|
21
|
+
getMasterComponent,
|
|
22
|
+
} from '../../lib/firebase/editor-components';
|
|
23
|
+
import {
|
|
24
|
+
renderInstance,
|
|
25
|
+
updateInstanceElement,
|
|
26
|
+
getInstanceIdFromElement,
|
|
27
|
+
getMasterIdFromElement,
|
|
28
|
+
} from '../utils/component-renderer';
|
|
29
|
+
import { generateElementId } from '../utils/dom-utils';
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Types
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
export interface UseComponentInstancesOptions {
|
|
36
|
+
/** Auto-load instances on mount */
|
|
37
|
+
autoLoad?: boolean;
|
|
38
|
+
/** Callback when instances change */
|
|
39
|
+
onInstancesChange?: (instances: ComponentInstance[]) => void;
|
|
40
|
+
/** Callback when an error occurs */
|
|
41
|
+
onError?: (error: Error) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UseComponentInstancesReturn {
|
|
45
|
+
/** Array of component instances on the page */
|
|
46
|
+
instances: ComponentInstance[];
|
|
47
|
+
/** Whether instances are currently loading */
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
/** Error if any occurred during operations */
|
|
50
|
+
error: Error | null;
|
|
51
|
+
/** Create a new component instance */
|
|
52
|
+
createInstance: (
|
|
53
|
+
masterComponentId: string,
|
|
54
|
+
position: { x: number; y: number },
|
|
55
|
+
variantId?: string
|
|
56
|
+
) => Promise<ComponentInstance>;
|
|
57
|
+
/** Delete a component instance */
|
|
58
|
+
deleteInstance: (instanceId: string) => Promise<void>;
|
|
59
|
+
/** Update a component instance */
|
|
60
|
+
updateInstance: (
|
|
61
|
+
instanceId: string,
|
|
62
|
+
updates: Partial<ComponentInstance>
|
|
63
|
+
) => Promise<void>;
|
|
64
|
+
/** Render an instance to the DOM */
|
|
65
|
+
renderInstanceToDOM: (
|
|
66
|
+
instance: ComponentInstance,
|
|
67
|
+
container: HTMLElement
|
|
68
|
+
) => Promise<HTMLElement | null>;
|
|
69
|
+
/** Refresh instances from Firestore */
|
|
70
|
+
refreshInstances: () => Promise<void>;
|
|
71
|
+
/** Get an instance by ID */
|
|
72
|
+
getInstance: (instanceId: string) => ComponentInstance | undefined;
|
|
73
|
+
/** Get an instance by DOM element ID */
|
|
74
|
+
getInstanceByDomId: (domElementId: string) => ComponentInstance | undefined;
|
|
75
|
+
/** Add an override to an instance */
|
|
76
|
+
addOverride: (
|
|
77
|
+
instanceId: string,
|
|
78
|
+
override: Omit<ComponentOverride, 'id'>
|
|
79
|
+
) => Promise<void>;
|
|
80
|
+
/** Remove an override from an instance */
|
|
81
|
+
removeOverride: (instanceId: string, overrideId: string) => Promise<void>;
|
|
82
|
+
/** Reset all overrides on an instance */
|
|
83
|
+
resetAllOverrides: (instanceId: string) => Promise<void>;
|
|
84
|
+
/** Change the variant of an instance */
|
|
85
|
+
changeVariant: (instanceId: string, variantId: string) => Promise<void>;
|
|
86
|
+
/** Detach an instance from its master (makes it independent) */
|
|
87
|
+
detachInstance: (instanceId: string) => Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================
|
|
91
|
+
// Hook Implementation
|
|
92
|
+
// ============================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hook for managing component instances on a page.
|
|
96
|
+
*
|
|
97
|
+
* @param websiteId - The website ID
|
|
98
|
+
* @param pageId - The page ID
|
|
99
|
+
* @param options - Optional configuration
|
|
100
|
+
* @returns Instance management functions and state
|
|
101
|
+
*/
|
|
102
|
+
export function useComponentInstances(
|
|
103
|
+
websiteId: string,
|
|
104
|
+
pageId: string,
|
|
105
|
+
options: UseComponentInstancesOptions = {}
|
|
106
|
+
): UseComponentInstancesReturn {
|
|
107
|
+
const { autoLoad = true, onInstancesChange, onError } = options;
|
|
108
|
+
|
|
109
|
+
// State
|
|
110
|
+
const [instances, setInstances] = useState<ComponentInstance[]>([]);
|
|
111
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
112
|
+
const [error, setError] = useState<Error | null>(null);
|
|
113
|
+
|
|
114
|
+
// Cache for master components
|
|
115
|
+
const masterComponentsCache = useRef<Map<string, MasterComponent>>(new Map());
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// Helper Functions
|
|
119
|
+
// ============================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a master component, using cache if available.
|
|
123
|
+
*/
|
|
124
|
+
const fetchMasterComponent = useCallback(
|
|
125
|
+
async (masterComponentId: string): Promise<MasterComponent | null> => {
|
|
126
|
+
// Check cache first
|
|
127
|
+
const cached = masterComponentsCache.current.get(masterComponentId);
|
|
128
|
+
if (cached) {
|
|
129
|
+
return cached;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const master = await getMasterComponent(websiteId, masterComponentId);
|
|
134
|
+
if (master) {
|
|
135
|
+
masterComponentsCache.current.set(masterComponentId, master);
|
|
136
|
+
}
|
|
137
|
+
return master;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error(
|
|
140
|
+
`[useComponentInstances] Error fetching master component: ${masterComponentId}`,
|
|
141
|
+
err
|
|
142
|
+
);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[websiteId]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle errors consistently.
|
|
151
|
+
*/
|
|
152
|
+
const handleError = useCallback(
|
|
153
|
+
(err: unknown, context: string) => {
|
|
154
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
155
|
+
console.error(`[useComponentInstances] ${context}:`, error);
|
|
156
|
+
setError(error);
|
|
157
|
+
onError?.(error);
|
|
158
|
+
},
|
|
159
|
+
[onError]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Update instances state and notify listeners.
|
|
164
|
+
*/
|
|
165
|
+
const updateInstancesState = useCallback(
|
|
166
|
+
(newInstances: ComponentInstance[]) => {
|
|
167
|
+
setInstances(newInstances);
|
|
168
|
+
onInstancesChange?.(newInstances);
|
|
169
|
+
},
|
|
170
|
+
[onInstancesChange]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// Core Operations
|
|
175
|
+
// ============================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load instances from Firestore.
|
|
179
|
+
*/
|
|
180
|
+
const refreshInstances = useCallback(async () => {
|
|
181
|
+
if (!websiteId || !pageId) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setIsLoading(true);
|
|
186
|
+
setError(null);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const loadedInstances = await getPageComponentInstances(websiteId, pageId);
|
|
190
|
+
updateInstancesState(loadedInstances);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
handleError(err, 'Error loading instances');
|
|
193
|
+
} finally {
|
|
194
|
+
setIsLoading(false);
|
|
195
|
+
}
|
|
196
|
+
}, [websiteId, pageId, updateInstancesState, handleError]);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create a new component instance.
|
|
200
|
+
*/
|
|
201
|
+
const createInstance = useCallback(
|
|
202
|
+
async (
|
|
203
|
+
masterComponentId: string,
|
|
204
|
+
position: { x: number; y: number },
|
|
205
|
+
variantId?: string
|
|
206
|
+
): Promise<ComponentInstance> => {
|
|
207
|
+
// Fetch master component to get default variant
|
|
208
|
+
const master = await fetchMasterComponent(masterComponentId);
|
|
209
|
+
if (!master) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Master component not found: ${masterComponentId}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const now = new Date().toISOString();
|
|
216
|
+
const instanceId = generateElementId('inst');
|
|
217
|
+
const domElementId = generateElementId('dom');
|
|
218
|
+
|
|
219
|
+
const newInstance: ComponentInstance = {
|
|
220
|
+
id: instanceId,
|
|
221
|
+
masterComponentId,
|
|
222
|
+
variantId: variantId || master.defaultVariantId,
|
|
223
|
+
domElementId,
|
|
224
|
+
overrides: [],
|
|
225
|
+
propertyValues: {},
|
|
226
|
+
position,
|
|
227
|
+
pageId,
|
|
228
|
+
createdAt: now,
|
|
229
|
+
updatedAt: now,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Set default property values from exposed properties
|
|
233
|
+
for (const prop of master.exposedProperties) {
|
|
234
|
+
newInstance.propertyValues[prop.id] = prop.defaultValue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Save to Firestore
|
|
239
|
+
await saveComponentInstance(websiteId, pageId, newInstance);
|
|
240
|
+
|
|
241
|
+
// Update local state
|
|
242
|
+
const newInstances = [...instances, newInstance];
|
|
243
|
+
updateInstancesState(newInstances);
|
|
244
|
+
|
|
245
|
+
return newInstance;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
handleError(err, 'Error creating instance');
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[
|
|
252
|
+
websiteId,
|
|
253
|
+
pageId,
|
|
254
|
+
instances,
|
|
255
|
+
fetchMasterComponent,
|
|
256
|
+
updateInstancesState,
|
|
257
|
+
handleError,
|
|
258
|
+
]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Delete a component instance.
|
|
263
|
+
*/
|
|
264
|
+
const deleteInstanceFn = useCallback(
|
|
265
|
+
async (instanceId: string): Promise<void> => {
|
|
266
|
+
try {
|
|
267
|
+
// Delete from Firestore
|
|
268
|
+
await deleteComponentInstance(websiteId, pageId, instanceId);
|
|
269
|
+
|
|
270
|
+
// Update local state
|
|
271
|
+
const newInstances = instances.filter((i) => i.id !== instanceId);
|
|
272
|
+
updateInstancesState(newInstances);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
handleError(err, 'Error deleting instance');
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
[websiteId, pageId, instances, updateInstancesState, handleError]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Update a component instance.
|
|
283
|
+
*/
|
|
284
|
+
const updateInstanceFn = useCallback(
|
|
285
|
+
async (
|
|
286
|
+
instanceId: string,
|
|
287
|
+
updates: Partial<ComponentInstance>
|
|
288
|
+
): Promise<void> => {
|
|
289
|
+
try {
|
|
290
|
+
// Update in Firestore
|
|
291
|
+
await updateComponentInstance(websiteId, pageId, instanceId, updates);
|
|
292
|
+
|
|
293
|
+
// Update local state
|
|
294
|
+
const newInstances = instances.map((i) =>
|
|
295
|
+
i.id === instanceId
|
|
296
|
+
? { ...i, ...updates, updatedAt: new Date().toISOString() }
|
|
297
|
+
: i
|
|
298
|
+
);
|
|
299
|
+
updateInstancesState(newInstances);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
handleError(err, 'Error updating instance');
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
[websiteId, pageId, instances, updateInstancesState, handleError]
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Render an instance to the DOM.
|
|
310
|
+
*/
|
|
311
|
+
const renderInstanceToDOM = useCallback(
|
|
312
|
+
async (
|
|
313
|
+
instance: ComponentInstance,
|
|
314
|
+
container: HTMLElement
|
|
315
|
+
): Promise<HTMLElement | null> => {
|
|
316
|
+
try {
|
|
317
|
+
const master = await fetchMasterComponent(instance.masterComponentId);
|
|
318
|
+
if (!master) {
|
|
319
|
+
console.error(
|
|
320
|
+
`[useComponentInstances] Master component not found: ${instance.masterComponentId}`
|
|
321
|
+
);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Get the document from the container
|
|
326
|
+
const doc = container.ownerDocument;
|
|
327
|
+
|
|
328
|
+
// Render the instance
|
|
329
|
+
const el = renderInstance(master, instance, doc);
|
|
330
|
+
|
|
331
|
+
// Append to container
|
|
332
|
+
container.appendChild(el);
|
|
333
|
+
|
|
334
|
+
return el;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
handleError(err, 'Error rendering instance to DOM');
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
[fetchMasterComponent, handleError]
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get an instance by ID.
|
|
345
|
+
*/
|
|
346
|
+
const getInstance = useCallback(
|
|
347
|
+
(instanceId: string): ComponentInstance | undefined => {
|
|
348
|
+
return instances.find((i) => i.id === instanceId);
|
|
349
|
+
},
|
|
350
|
+
[instances]
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get an instance by DOM element ID.
|
|
355
|
+
*/
|
|
356
|
+
const getInstanceByDomId = useCallback(
|
|
357
|
+
(domElementId: string): ComponentInstance | undefined => {
|
|
358
|
+
return instances.find((i) => i.domElementId === domElementId);
|
|
359
|
+
},
|
|
360
|
+
[instances]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// ============================================================
|
|
364
|
+
// Override Operations
|
|
365
|
+
// ============================================================
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Add an override to an instance.
|
|
369
|
+
*/
|
|
370
|
+
const addOverride = useCallback(
|
|
371
|
+
async (
|
|
372
|
+
instanceId: string,
|
|
373
|
+
override: Omit<ComponentOverride, 'id'>
|
|
374
|
+
): Promise<void> => {
|
|
375
|
+
const instance = instances.find((i) => i.id === instanceId);
|
|
376
|
+
if (!instance) {
|
|
377
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const newOverride: ComponentOverride = {
|
|
381
|
+
...override,
|
|
382
|
+
id: generateElementId('ovr'),
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Check if there's an existing override for the same target and type
|
|
386
|
+
const existingIndex = instance.overrides.findIndex(
|
|
387
|
+
(o) =>
|
|
388
|
+
(o.targetElementId === newOverride.targetElementId ||
|
|
389
|
+
o.elementPath === newOverride.elementPath) &&
|
|
390
|
+
o.type === newOverride.type
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
let newOverrides: ComponentOverride[];
|
|
394
|
+
if (existingIndex >= 0) {
|
|
395
|
+
// Replace existing override
|
|
396
|
+
newOverrides = [...instance.overrides];
|
|
397
|
+
newOverrides[existingIndex] = newOverride;
|
|
398
|
+
} else {
|
|
399
|
+
// Add new override
|
|
400
|
+
newOverrides = [...instance.overrides, newOverride];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await updateInstanceFn(instanceId, { overrides: newOverrides });
|
|
404
|
+
},
|
|
405
|
+
[instances, updateInstanceFn]
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Remove an override from an instance.
|
|
410
|
+
*/
|
|
411
|
+
const removeOverride = useCallback(
|
|
412
|
+
async (instanceId: string, overrideId: string): Promise<void> => {
|
|
413
|
+
const instance = instances.find((i) => i.id === instanceId);
|
|
414
|
+
if (!instance) {
|
|
415
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const newOverrides = instance.overrides.filter((o) => o.id !== overrideId);
|
|
419
|
+
await updateInstanceFn(instanceId, { overrides: newOverrides });
|
|
420
|
+
},
|
|
421
|
+
[instances, updateInstanceFn]
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Reset all overrides on an instance.
|
|
426
|
+
*/
|
|
427
|
+
const resetAllOverrides = useCallback(
|
|
428
|
+
async (instanceId: string): Promise<void> => {
|
|
429
|
+
await updateInstanceFn(instanceId, { overrides: [] });
|
|
430
|
+
},
|
|
431
|
+
[updateInstanceFn]
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Change the variant of an instance.
|
|
436
|
+
*/
|
|
437
|
+
const changeVariant = useCallback(
|
|
438
|
+
async (instanceId: string, variantId: string): Promise<void> => {
|
|
439
|
+
const instance = instances.find((i) => i.id === instanceId);
|
|
440
|
+
if (!instance) {
|
|
441
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fetch master to validate variant exists
|
|
445
|
+
const master = await fetchMasterComponent(instance.masterComponentId);
|
|
446
|
+
if (!master) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Master component not found: ${instance.masterComponentId}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const variant = master.variants.find((v) => v.id === variantId);
|
|
453
|
+
if (!variant) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`Variant not found: ${variantId} in master ${master.id}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await updateInstanceFn(instanceId, { variantId });
|
|
460
|
+
},
|
|
461
|
+
[instances, fetchMasterComponent, updateInstanceFn]
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Detach an instance from its master.
|
|
466
|
+
*/
|
|
467
|
+
const detachInstance = useCallback(
|
|
468
|
+
async (instanceId: string): Promise<void> => {
|
|
469
|
+
await updateInstanceFn(instanceId, { isDetached: true });
|
|
470
|
+
},
|
|
471
|
+
[updateInstanceFn]
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// ============================================================
|
|
475
|
+
// Effects
|
|
476
|
+
// ============================================================
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Auto-load instances on mount if enabled.
|
|
480
|
+
*/
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (autoLoad && websiteId && pageId) {
|
|
483
|
+
refreshInstances();
|
|
484
|
+
}
|
|
485
|
+
}, [autoLoad, websiteId, pageId, refreshInstances]);
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Clear cache when websiteId changes.
|
|
489
|
+
*/
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
masterComponentsCache.current.clear();
|
|
492
|
+
}, [websiteId]);
|
|
493
|
+
|
|
494
|
+
// ============================================================
|
|
495
|
+
// Return
|
|
496
|
+
// ============================================================
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
instances,
|
|
500
|
+
isLoading,
|
|
501
|
+
error,
|
|
502
|
+
createInstance,
|
|
503
|
+
deleteInstance: deleteInstanceFn,
|
|
504
|
+
updateInstance: updateInstanceFn,
|
|
505
|
+
renderInstanceToDOM,
|
|
506
|
+
refreshInstances,
|
|
507
|
+
getInstance,
|
|
508
|
+
getInstanceByDomId,
|
|
509
|
+
addOverride,
|
|
510
|
+
removeOverride,
|
|
511
|
+
resetAllOverrides,
|
|
512
|
+
changeVariant,
|
|
513
|
+
detachInstance,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* コンテキストメニュー処理フック
|
|
3
|
+
* - 右クリックイベントの処理
|
|
4
|
+
* - 座標変換(iframe → 親ウィンドウ)
|
|
5
|
+
* - 要素の自動選択
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useRef, useEffect, MutableRefObject } from 'react';
|
|
9
|
+
import { useEditorContext } from '../EditorContext';
|
|
10
|
+
import { updateSelectionBox } from '../utils/dom-utils';
|
|
11
|
+
import { extractElementInfo } from '../utils/style-utils';
|
|
12
|
+
import { SLIDE_WIDTH, SLIDE_HEIGHT, WEBPAGE_WIDTH } from '../constants';
|
|
13
|
+
|
|
14
|
+
interface UseContextMenuHandlerOptions {
|
|
15
|
+
/** 編集可能要素を取得する関数 */
|
|
16
|
+
getEditableElement: (target: EventTarget | null, iframeDoc: Document) => HTMLElement | null;
|
|
17
|
+
/** コンテンツ高さのref */
|
|
18
|
+
contentHeightRef: MutableRefObject<number>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UseContextMenuHandlerReturn {
|
|
22
|
+
/**
|
|
23
|
+
* iframe ドキュメントにコンテキストメニューリスナーをセットアップ
|
|
24
|
+
* @returns クリーンアップ関数
|
|
25
|
+
*/
|
|
26
|
+
setupContextMenuListener: (iframeDoc: Document) => () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* コンテキストメニュー処理フック
|
|
31
|
+
*/
|
|
32
|
+
export function useContextMenuHandler(
|
|
33
|
+
options: UseContextMenuHandlerOptions
|
|
34
|
+
): UseContextMenuHandlerReturn {
|
|
35
|
+
const {
|
|
36
|
+
iframeRef,
|
|
37
|
+
editorMode,
|
|
38
|
+
selectedElementIds,
|
|
39
|
+
setSelectedElementIds,
|
|
40
|
+
} = useEditorContext();
|
|
41
|
+
|
|
42
|
+
const { getEditableElement, contentHeightRef } = options;
|
|
43
|
+
|
|
44
|
+
// クロージャ問題回避のためのref
|
|
45
|
+
const editorModeRef = useRef(editorMode);
|
|
46
|
+
const selectedElementIdsRef = useRef(selectedElementIds);
|
|
47
|
+
const setSelectedElementIdsRef = useRef(setSelectedElementIds);
|
|
48
|
+
|
|
49
|
+
// refを最新値に同期
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
editorModeRef.current = editorMode;
|
|
52
|
+
}, [editorMode]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
selectedElementIdsRef.current = selectedElementIds;
|
|
56
|
+
setSelectedElementIdsRef.current = setSelectedElementIds;
|
|
57
|
+
}, [selectedElementIds, setSelectedElementIds]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 要素情報を親ウィンドウに送信
|
|
61
|
+
*/
|
|
62
|
+
const sendElementInfo = useCallback((element: HTMLElement, iframeDoc: Document) => {
|
|
63
|
+
const info = extractElementInfo(element, iframeDoc);
|
|
64
|
+
if (info) {
|
|
65
|
+
window.postMessage({ type: 'ELEMENT_SELECTED', element: info }, '*');
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const setupContextMenuListener = useCallback(
|
|
70
|
+
(iframeDoc: Document) => {
|
|
71
|
+
const iframe = iframeRef.current;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* コンテキストメニューイベントハンドラ
|
|
75
|
+
*/
|
|
76
|
+
const handleContextMenu = (e: MouseEvent) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
|
|
79
|
+
// iframe内のクリック位置を親ウィンドウの座標系に変換
|
|
80
|
+
const iframeRect = iframe?.getBoundingClientRect();
|
|
81
|
+
if (!iframeRect) return;
|
|
82
|
+
|
|
83
|
+
// クリック位置のiframe内での比率(0〜1)を計算
|
|
84
|
+
const currentWidth =
|
|
85
|
+
editorModeRef.current === 'webpage' ? WEBPAGE_WIDTH : SLIDE_WIDTH;
|
|
86
|
+
const currentHeight =
|
|
87
|
+
editorModeRef.current === 'webpage'
|
|
88
|
+
? contentHeightRef.current
|
|
89
|
+
: SLIDE_HEIGHT;
|
|
90
|
+
const proportionX = e.clientX / currentWidth;
|
|
91
|
+
const proportionY = e.clientY / currentHeight;
|
|
92
|
+
|
|
93
|
+
// 比率をスケール後のビジュアルサイズに適用
|
|
94
|
+
const x = iframeRect.left + iframeRect.width * proportionX;
|
|
95
|
+
const y = iframeRect.top + iframeRect.height * proportionY;
|
|
96
|
+
|
|
97
|
+
// 要素がクリックされた場合は選択
|
|
98
|
+
const element = getEditableElement(e.target, iframeDoc);
|
|
99
|
+
if (element) {
|
|
100
|
+
const elementId = element.getAttribute('data-element-id') || '';
|
|
101
|
+
const currentIds = selectedElementIdsRef.current;
|
|
102
|
+
|
|
103
|
+
// Check if clicked element is inside any currently selected element
|
|
104
|
+
// If so, keep the current selection (don't change to child element)
|
|
105
|
+
const clickedTarget = e.target as HTMLElement;
|
|
106
|
+
const isInsideSelectedElement = currentIds.some((selectedId) => {
|
|
107
|
+
const selectedElement = iframeDoc.querySelector(
|
|
108
|
+
`[data-element-id="${selectedId}"]`
|
|
109
|
+
);
|
|
110
|
+
return selectedElement?.contains(clickedTarget);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// まだ選択されていない場合かつ選択要素の内部でない場合のみ選択
|
|
114
|
+
if (
|
|
115
|
+
!isInsideSelectedElement &&
|
|
116
|
+
!currentIds.includes(elementId) &&
|
|
117
|
+
!element.classList.contains('selected')
|
|
118
|
+
) {
|
|
119
|
+
iframeDoc
|
|
120
|
+
.querySelectorAll('.selected')
|
|
121
|
+
.forEach((sel) => sel.classList.remove('selected'));
|
|
122
|
+
iframeDoc
|
|
123
|
+
.querySelectorAll('.selection-box')
|
|
124
|
+
.forEach((box) => box.remove());
|
|
125
|
+
element.classList.add('selected');
|
|
126
|
+
updateSelectionBox(iframeDoc, element);
|
|
127
|
+
setSelectedElementIdsRef.current([elementId]);
|
|
128
|
+
sendElementInfo(element, iframeDoc);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 親ウィンドウにコンテキストメニューイベントを送信
|
|
133
|
+
window.postMessage(
|
|
134
|
+
{
|
|
135
|
+
type: 'IFRAME_CONTEXT_MENU',
|
|
136
|
+
clientX: x,
|
|
137
|
+
clientY: y,
|
|
138
|
+
},
|
|
139
|
+
'*'
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// イベントリスナーを登録
|
|
144
|
+
iframeDoc.addEventListener('contextmenu', handleContextMenu);
|
|
145
|
+
|
|
146
|
+
// クリーンアップ関数を返す
|
|
147
|
+
return () => {
|
|
148
|
+
iframeDoc.removeEventListener('contextmenu', handleContextMenu);
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
[iframeRef, getEditableElement, contentHeightRef, sendElementInfo]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
setupContextMenuListener,
|
|
156
|
+
};
|
|
157
|
+
}
|