@growgroup/visual-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. package/src/vendor/firebase-functions.ts +17 -0
@@ -0,0 +1,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
+ }