@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,987 @@
1
+ "use client";
2
+
3
+ import { useEffect, useCallback, useMemo, useRef, useState } from "react";
4
+ import { useEditorContext } from "../EditorContext";
5
+ import { useCanvasControls } from "../hooks/useCanvasControls";
6
+ import { useElementActions } from "../hooks/useElementActions";
7
+ import { useTouchGestures } from "../hooks/useTouchGestures";
8
+ import { useKeyboardShortcuts, editorCancelDragRef } from "../hooks/useKeyboardShortcuts";
9
+ import { useElementSelection } from "../hooks/useElementSelection";
10
+ import { useMarqueeSelection } from "../hooks/useMarqueeSelection";
11
+ import { useDragResize } from "../hooks/useDragResize";
12
+ import { useContextMenuHandler } from "../hooks/useContextMenuHandler";
13
+ import { useIframeSetup } from "../hooks/useIframeSetup";
14
+ import { useFocusManagement } from "../hooks/useFocusManagement";
15
+ import {
16
+ SLIDE_WIDTH,
17
+ SLIDE_HEIGHT,
18
+ WEBPAGE_MIN_HEIGHT,
19
+ } from "../constants";
20
+ import {
21
+ applyCanvasZoomDom,
22
+ convertToAbsolutePositioning,
23
+ stampBaselines,
24
+ buildDomTree,
25
+ getArtboardContent,
26
+ refreshSelectionOverlay,
27
+ } from "../utils/dom-utils";
28
+ import { generateEditableHtml } from "../utils/html-utils";
29
+ import { setupInlineFormatToolbar } from "../utils/inline-format";
30
+ import { recalculateViewportUnits } from "../utils/viewport-utils";
31
+ import { useMultiPageCanvasOptional } from "../contexts/MultiPageCanvasContext";
32
+ import type {
33
+ DOMTreeNode,
34
+ DragState,
35
+ ResizeState,
36
+ EditorTool,
37
+ MarqueeState,
38
+ } from "../types";
39
+
40
+ /**
41
+ * 初期ドラッグ状態
42
+ */
43
+ const INITIAL_DRAG_STATE: DragState = {
44
+ element: null,
45
+ elements: [],
46
+ startX: 0,
47
+ startY: 0,
48
+ origLeft: 0,
49
+ origTop: 0,
50
+ origPositions: [],
51
+ isDragging: false,
52
+ hasMoved: false,
53
+ // Flex reorder mode (auto-layout)
54
+ flexReorderMode: false,
55
+ flexParent: null,
56
+ originalIndex: -1,
57
+ targetIndex: -1,
58
+ // Enhanced auto-layout drag (hierarchy change support)
59
+ autoLayoutDragMode: false,
60
+ dragGhost: null,
61
+ originalParent: null,
62
+ currentDropTarget: null,
63
+ dropPosition: null,
64
+ dropIndex: -1,
65
+ };
66
+
67
+ /**
68
+ * 初期リサイズ状態
69
+ */
70
+ const INITIAL_RESIZE_STATE: ResizeState = {
71
+ isResizing: false,
72
+ isRotating: false,
73
+ element: null,
74
+ elements: [],
75
+ handle: "",
76
+ startX: 0,
77
+ startY: 0,
78
+ origLeft: 0,
79
+ origTop: 0,
80
+ origWidth: 0,
81
+ origHeight: 0,
82
+ origRadius: 0,
83
+ selectionBounds: null,
84
+ origElementStates: [],
85
+ origScaleX: 1,
86
+ origScaleY: 1,
87
+ rotation: 0,
88
+ rotationStartAngle: 0,
89
+ centerX: 0,
90
+ centerY: 0,
91
+ };
92
+
93
+ /**
94
+ * iframeをラップするキャンバスコンポーネント
95
+ * iframeの初期化とイベントハンドリングを担う
96
+ *
97
+ * キャンバス操作:
98
+ * - Ctrl/Cmd + ホイール: ズーム
99
+ * - ホイール: パン(縦)
100
+ * - Shift + ホイール: パン(横)
101
+ * - ピンチ: ズーム
102
+ * - スペース + ドラッグ: パン
103
+ */
104
+ export function EditorCanvas() {
105
+ const {
106
+ iframeRef,
107
+ containerRef,
108
+ zoom,
109
+ originalHtml,
110
+ sourceHtml,
111
+ activeTool,
112
+ getIframeDoc,
113
+ setFitZoom,
114
+ setZoom,
115
+ setDomTree,
116
+ setExpandedNodes,
117
+ setHtml,
118
+ setOriginalHtml,
119
+ clearHistory,
120
+ layoutMode,
121
+ editorMode,
122
+ setShowLayoutHint,
123
+ viewportWidth,
124
+ setIframeReady,
125
+ } = useEditorContext();
126
+
127
+ // マルチページモード判定
128
+ const multiPageCanvas = useMultiPageCanvasOptional();
129
+ const isInMultiPageMode = !!multiPageCanvas?.isEnabled;
130
+
131
+ // Figmaライクなキャンバス操作
132
+ useCanvasControls();
133
+
134
+ // グループ化/グループ解除/スタイルコピー&ペースト/Figmaエクスポート
135
+ const { groupElements, ungroupElements, copyStyle, pasteStyle, copyToFigma } = useElementActions();
136
+
137
+ // ========== State Refs ==========
138
+ // ドラッグ・リサイズ状態(ミュータブル)
139
+ const dragStateRef = useRef<DragState>({ ...INITIAL_DRAG_STATE });
140
+ const resizeStateRef = useRef<ResizeState>({ ...INITIAL_RESIZE_STATE });
141
+
142
+ // マーキー選択用refs
143
+ const marqueeStartPendingRef = useRef(false);
144
+ const marqueeClickTargetRef = useRef<HTMLElement | null>(null);
145
+ // Shift+マーキー(既存選択への加算)フラグ。mousedown で決めて mouseup で使う
146
+ const marqueeAdditiveRef = useRef(false);
147
+ // マーキーの矩形。React state だと mousedown → 最初の mousemove の間に
148
+ // 反映が間に合わず、始点が (0,0) のまま読まれて矩形がずれる(速いドラッグで再現)。
149
+ // 判定に使う値は必ずこの ref(同期書き込み)を正とする。
150
+ const marqueeGeomRef = useRef<MarqueeState>({
151
+ isActive: false,
152
+ startX: 0,
153
+ startY: 0,
154
+ currentX: 0,
155
+ currentY: 0,
156
+ });
157
+
158
+ // クロージャ問題回避のためのref
159
+ const activeToolRef = useRef<EditorTool>(activeTool);
160
+ const layoutModeRef = useRef<"absolute" | "auto">(layoutMode);
161
+ const setShowLayoutHintRef = useRef(setShowLayoutHint);
162
+ const setDomTreeRef = useRef(setDomTree);
163
+ const setExpandedNodesRef = useRef(setExpandedNodes);
164
+ const setHtmlRef = useRef(setHtml);
165
+ const clearHistoryRef = useRef(clearHistory);
166
+ const editorModeRef = useRef(editorMode);
167
+
168
+ // Webページモード用のコンテンツ高さ追跡
169
+ const [contentHeight, setContentHeight] = useState(WEBPAGE_MIN_HEIGHT);
170
+ const contentHeightRef = useRef(contentHeight);
171
+
172
+ // イベントリスナーのクリーンアップ関数を保存するref
173
+ // これにより、iframeがリロードされた際に古いリスナーを確実に削除できる
174
+ const cleanupFunctionsRef = useRef<(() => void)[]>([]);
175
+
176
+ // refを最新値に同期
177
+ useEffect(() => {
178
+ activeToolRef.current = activeTool;
179
+ layoutModeRef.current = layoutMode;
180
+ setShowLayoutHintRef.current = setShowLayoutHint;
181
+ setDomTreeRef.current = setDomTree;
182
+ setExpandedNodesRef.current = setExpandedNodes;
183
+ setHtmlRef.current = setHtml;
184
+ clearHistoryRef.current = clearHistory;
185
+ editorModeRef.current = editorMode;
186
+ contentHeightRef.current = contentHeight;
187
+ }, [
188
+ activeTool,
189
+ layoutMode,
190
+ setShowLayoutHint,
191
+ setDomTree,
192
+ setExpandedNodes,
193
+ setHtml,
194
+ clearHistory,
195
+ editorMode,
196
+ contentHeight,
197
+ ]);
198
+
199
+ // ========== Hooks ==========
200
+
201
+ // フォーカス管理(ショートカットが効かなくなる問題対策)
202
+ const { setupFocusRecovery } = useFocusManagement();
203
+
204
+ // タッチジェスチャー(ピンチズーム、ホイールズーム)
205
+ const { setupTouchGestureListeners } = useTouchGestures();
206
+
207
+ // キーボードショートカット
208
+ const { setupKeyboardShortcuts } = useKeyboardShortcuts({
209
+ groupElements,
210
+ ungroupElements,
211
+ copyStyle,
212
+ pasteStyle,
213
+ copyToFigma,
214
+ });
215
+
216
+ // 要素選択
217
+ const {
218
+ getEditableElement,
219
+ setupSelectionListeners,
220
+ resolveHoverTarget,
221
+ handleSelectionMouseUp,
222
+ } = useElementSelection({
223
+ dragStateRef,
224
+ resizeStateRef,
225
+ marqueeStartPendingRef,
226
+ marqueeClickTargetRef,
227
+ layoutModeRef,
228
+ // 群移動できないケース(オートレイアウト中のフロー要素)でヒントを出すため
229
+ setShowLayoutHintRef,
230
+ marqueeAdditiveRef,
231
+ marqueeGeomRef,
232
+ });
233
+
234
+ // マーキー選択
235
+ const {
236
+ marqueeBoxRef: _marqueeBoxRef, // used internally by hook
237
+ handleMarqueeMouseMove,
238
+ handleMarqueeMouseUp,
239
+ initMarqueeBox,
240
+ } = useMarqueeSelection({
241
+ marqueeStartPendingRef,
242
+ marqueeClickTargetRef,
243
+ marqueeAdditiveRef,
244
+ marqueeGeomRef,
245
+ });
246
+
247
+ // ドラッグ・リサイズ
248
+ const {
249
+ handleDragResizeMouseMove,
250
+ handleDragResizeMouseUp,
251
+ cancelDrag,
252
+ sendElementInfo: _sendElementInfo, // used internally by hooks
253
+ } = useDragResize({
254
+ dragStateRef,
255
+ resizeStateRef,
256
+ activeToolRef,
257
+ layoutModeRef,
258
+ setShowLayoutHintRef,
259
+ });
260
+
261
+ // ドラッグ中断(Escape)はキーボードのディスパッチャ側から呼ばれる。
262
+ // ドラッグ状態はこのコンポーネントのrefが持っているため、モジュールレベルの
263
+ // レジストリ経由で最新の cancelDrag を渡す(keydownの経路を1本に保つための橋渡し)
264
+ editorCancelDragRef.current = cancelDrag;
265
+
266
+ // コンテキストメニュー
267
+ const { setupContextMenuListener } = useContextMenuHandler({
268
+ getEditableElement,
269
+ contentHeightRef,
270
+ });
271
+
272
+ // iframe初期化
273
+ const { initializeIframeDocument, setupMutationObserver, setupTextHoverListener } = useIframeSetup();
274
+
275
+ // ========== Canvas Dimensions ==========
276
+ // webpageモードではviewportWidth(ブレイクポイント)を使用
277
+ const canvasWidth = editorMode === "webpage" ? viewportWidth : SLIDE_WIDTH;
278
+ const canvasHeight =
279
+ editorMode === "webpage" ? contentHeight : SLIDE_HEIGHT;
280
+
281
+ // Webページモードでコンテンツ高さを更新する関数
282
+ const updateContentHeightFromIframe = useCallback(() => {
283
+ if (editorModeRef.current !== "webpage") return;
284
+
285
+ const iframeDoc = getIframeDoc();
286
+ if (!iframeDoc) return;
287
+
288
+ const artboard = iframeDoc.getElementById("artboard");
289
+ const targetElement = artboard || iframeDoc.body;
290
+
291
+ const elements = targetElement.querySelectorAll("*");
292
+ let maxBottom = 0;
293
+
294
+ elements.forEach((el) => {
295
+ const rect = (el as HTMLElement).getBoundingClientRect();
296
+ const bottom = rect.bottom;
297
+ if (bottom > maxBottom) {
298
+ maxBottom = bottom;
299
+ }
300
+ });
301
+
302
+ const scrollHeight = targetElement.scrollHeight;
303
+ const computedHeight = Math.max(scrollHeight, maxBottom);
304
+ const newHeight = Math.max(WEBPAGE_MIN_HEIGHT, computedHeight + 100);
305
+
306
+ setContentHeight(newHeight);
307
+ contentHeightRef.current = newHeight;
308
+ }, [getIframeDoc]);
309
+
310
+ const updateContentHeightRef = useRef(updateContentHeightFromIframe);
311
+ useEffect(() => {
312
+ updateContentHeightRef.current = updateContentHeightFromIframe;
313
+ }, [updateContentHeightFromIframe]);
314
+
315
+ // ========== Center Slide ==========
316
+ const centerSlide = useCallback(() => {
317
+ const container = containerRef.current;
318
+ if (!container) return;
319
+
320
+ const scrollLeft = (container.scrollWidth - container.clientWidth) / 2;
321
+ const scrollTop = (container.scrollHeight - container.clientHeight) / 2;
322
+
323
+ container.scrollTo({
324
+ left: scrollLeft,
325
+ top: scrollTop,
326
+ behavior: "instant",
327
+ });
328
+ }, [containerRef]);
329
+
330
+ useEffect(() => {
331
+ if (isInMultiPageMode) return; // マルチページモードではキャンバスがスクロールを管理
332
+ if (containerRef.current) {
333
+ centerSlide();
334
+ requestAnimationFrame(centerSlide);
335
+ setTimeout(centerSlide, 50);
336
+ setTimeout(centerSlide, 150);
337
+ setTimeout(centerSlide, 300);
338
+ }
339
+ }, [centerSlide, isInMultiPageMode]);
340
+
341
+ // iframeロード後にも中央配置を実行
342
+ useEffect(() => {
343
+ if (isInMultiPageMode) return; // マルチページモードではスキップ
344
+ const iframe = iframeRef.current;
345
+ if (!iframe) return;
346
+
347
+ const handleLoad = () => {
348
+ requestAnimationFrame(centerSlide);
349
+ setTimeout(centerSlide, 100);
350
+ };
351
+
352
+ iframe.addEventListener("load", handleLoad);
353
+ return () => iframe.removeEventListener("load", handleLoad);
354
+ }, [iframeRef, centerSlide, isInMultiPageMode]);
355
+
356
+ // ========== Fit Zoom Calculation ==========
357
+ const calculateFitZoom = useCallback(() => {
358
+ const container = containerRef.current;
359
+ if (!container) return 100;
360
+
361
+ const containerRect = container.getBoundingClientRect();
362
+ const padding = 64;
363
+ const availableWidth = containerRect.width - padding;
364
+ const availableHeight = containerRect.height - padding;
365
+
366
+ const scaleX = availableWidth / canvasWidth;
367
+ const scaleY = availableHeight / canvasHeight;
368
+ const fitScale = Math.min(scaleX, scaleY);
369
+
370
+ return Math.floor(fitScale * 100);
371
+ }, [containerRef, canvasWidth, canvasHeight]);
372
+
373
+ const initialZoomSetRef = useRef(false);
374
+
375
+ useEffect(() => {
376
+ // マルチページモードではキャンバスズームが全体を制御
377
+ // エディタズームは100%固定(MultiPageCanvasViewで設定)
378
+ if (isInMultiPageMode) return;
379
+
380
+ const updateFitZoomValue = () => {
381
+ const newFitZoom = calculateFitZoom();
382
+ setFitZoom(newFitZoom);
383
+
384
+ if (!initialZoomSetRef.current) {
385
+ setZoom(newFitZoom);
386
+ initialZoomSetRef.current = true;
387
+ }
388
+ };
389
+
390
+ const timer = setTimeout(updateFitZoomValue, 100);
391
+ window.addEventListener("resize", updateFitZoomValue);
392
+ return () => {
393
+ clearTimeout(timer);
394
+ window.removeEventListener("resize", updateFitZoomValue);
395
+ };
396
+ }, [calculateFitZoom, setFitZoom, setZoom, isInMultiPageMode]);
397
+
398
+ // ========== iframe HTML ==========
399
+ // iframe のロードハンドラ(deps=[])から最新のズーム倍率を読むための ref。
400
+ // ページ切替では zoom 状態が変わらず、ズーム反映の effect が走らないため、
401
+ // 読み込み完了時にこの値で塗り直す(でないと新しい文書が倍率1で巨大に出る)
402
+ const zoomRef = useRef(zoom);
403
+ useEffect(() => {
404
+ zoomRef.current = zoom;
405
+ }, [zoom]);
406
+
407
+ const iframeHtml = useMemo(
408
+ // sourceHtml(ページ切替でのみ変わる)から組む。originalHtml は
409
+ // initLayout が変更判定の基準として書き換えるため、ここに使うとループする
410
+ () => generateEditableHtml(sourceHtml, editorMode),
411
+ [sourceHtml, editorMode]
412
+ );
413
+
414
+ // ========== iframe Load Handler ==========
415
+ const handleIframeLoad = useCallback(() => {
416
+ const iframe = iframeRef.current;
417
+ if (!iframe) return;
418
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
419
+ if (!iframeDoc) return;
420
+
421
+ console.log("[Canvas] iframe loaded, initializing...");
422
+
423
+ // 最初のペイントより先に現在の倍率を当ててから見せる。
424
+ // initLayout はフォント待ち等で数百msかかるため、その後に適用すると
425
+ // 「等倍の巨大な一瞬 → 縮む」のフラッシュがノイズになる
426
+ applyCanvasZoomDom(iframeDoc, zoomRef.current);
427
+ const wrapperEl = iframeDoc.getElementById("artboard-wrapper");
428
+ if (wrapperEl) wrapperEl.style.visibility = "visible";
429
+
430
+ // 重要: 新しいリスナーを追加する前に、既存のクリーンアップを実行
431
+ // これにより、iframeがリロードされたり状態が変わるたびに
432
+ // 古いリスナーを削除せずに新しいリスナーが追加される問題を防ぐ
433
+ if (cleanupFunctionsRef.current.length > 0) {
434
+ console.log("[Canvas] Cleaning up", cleanupFunctionsRef.current.length, "previous listeners");
435
+ cleanupFunctionsRef.current.forEach(cleanup => {
436
+ try {
437
+ cleanup();
438
+ } catch (e) {
439
+ console.error("[Canvas] Cleanup error:", e);
440
+ }
441
+ });
442
+ cleanupFunctionsRef.current = [];
443
+ }
444
+
445
+ // 1. iframe ドキュメントの初期化(スタイル注入、data-editable属性付与)
446
+ initializeIframeDocument(iframeDoc);
447
+
448
+ // 2. キャンバス構造の確認
449
+ const artboard = iframeDoc.getElementById("artboard");
450
+ if (!artboard) {
451
+ console.error("[Canvas] #artboard not found");
452
+ return;
453
+ }
454
+
455
+ // 3. イベントリスナーのセットアップ(ローカル配列に収集後、refに保存)
456
+ const cleanupFunctions: (() => void)[] = [];
457
+
458
+ // タッチジェスチャー
459
+ cleanupFunctions.push(setupTouchGestureListeners(iframeDoc));
460
+
461
+ // キーボードショートカット
462
+ cleanupFunctions.push(setupKeyboardShortcuts(iframeDoc));
463
+
464
+ // フォーカス自動復元(ショートカットが効かなくなる問題対策)
465
+ cleanupFunctions.push(setupFocusRecovery());
466
+
467
+ // 選択リスナー(mousedown, dblclick)
468
+ cleanupFunctions.push(setupSelectionListeners(iframeDoc));
469
+ // テキスト編集中の範囲選択に、マーカー・太字のツールバーを出す
470
+ cleanupFunctions.push(setupInlineFormatToolbar(iframeDoc));
471
+
472
+ // コンテキストメニュー
473
+ cleanupFunctions.push(setupContextMenuListener(iframeDoc));
474
+
475
+ // テキスト要素ホバー(Figmaスタイルの下線表示 + クリック結果の予告輪郭)
476
+ // 予告の対象は選択側とまったく同じ判別器で決める
477
+ cleanupFunctions.push(setupTextHoverListener(iframeDoc, resolveHoverTarget));
478
+
479
+ // 4. マウス移動・アップのグローバルハンドラ
480
+ const handleMouseMove = (e: MouseEvent) => {
481
+ // マーキー選択を先に処理
482
+ if (handleMarqueeMouseMove(e, iframeDoc)) return;
483
+ // ドラッグ・リサイズを処理
484
+ handleDragResizeMouseMove(e, iframeDoc);
485
+ };
486
+
487
+ const handleMouseUp = (e: MouseEvent) => {
488
+ // マーキー選択を先に処理
489
+ if (!handleMarqueeMouseUp(iframeDoc)) {
490
+ // ドラッグ・リサイズを処理
491
+ handleDragResizeMouseUp(iframeDoc);
492
+ }
493
+ // 選択の確定(Shiftトグル / 群→単独の畳み込み)は
494
+ // ドラッグ確定処理の *後* に行う。先に行うと、群ドラッグの終了処理が
495
+ // 古い選択セットを見て枠と情報パネルを取り違える。
496
+ handleSelectionMouseUp(e, iframeDoc);
497
+ };
498
+
499
+ iframeDoc.addEventListener("mousemove", handleMouseMove);
500
+ iframeDoc.addEventListener("mouseup", handleMouseUp);
501
+ cleanupFunctions.push(() => {
502
+ iframeDoc.removeEventListener("mousemove", handleMouseMove);
503
+ iframeDoc.removeEventListener("mouseup", handleMouseUp);
504
+ });
505
+
506
+ // 4-b. iframe の外で離した場合の確定処理
507
+ //
508
+ // [なぜ親ウィンドウにも張るか]
509
+ // iframe 内で発生したマウスイベントは親ウィンドウへは伝播しない。
510
+ // 従来は mousemove/mouseup が iframeDoc にしか無かったため、
511
+ // ドラッグの終点がプロパティパネル等の iframe 外に出ると mouseup が
512
+ // 届かず、確定処理が走らないまま選択が飛んでいた。
513
+ //
514
+ // 親のイベント座標は「親ビューポート基準」だが、この先の処理
515
+ // (マーキー矩形 vs getBoundingClientRect、dragState.startX)はすべて
516
+ // 「iframe ビューポート基準」なので、iframe の矩形分だけ平行移動して渡す。
517
+ // iframe 自体は等倍(scale は iframe 内の #artboard-wrapper に掛かる)なので
518
+ // 平行移動だけで正しく一致する。
519
+ const toIframeCoords = (e: MouseEvent): MouseEvent => {
520
+ const frameEl = iframeRef.current;
521
+ if (!frameEl) return e;
522
+ const r = frameEl.getBoundingClientRect();
523
+ return new MouseEvent(e.type, {
524
+ clientX: e.clientX - r.left,
525
+ clientY: e.clientY - r.top,
526
+ screenX: e.screenX,
527
+ screenY: e.screenY,
528
+ button: e.button,
529
+ buttons: e.buttons,
530
+ shiftKey: e.shiftKey,
531
+ altKey: e.altKey,
532
+ ctrlKey: e.ctrlKey,
533
+ metaKey: e.metaKey,
534
+ });
535
+ };
536
+
537
+ const handleWindowMouseMove = (e: MouseEvent) => {
538
+ // 何も掴んでいなければ無視(通常のマウス移動でコストを払わない)
539
+ if (
540
+ !dragStateRef.current.isDragging &&
541
+ !resizeStateRef.current.isResizing &&
542
+ !resizeStateRef.current.isRotating &&
543
+ !marqueeStartPendingRef.current
544
+ ) {
545
+ return;
546
+ }
547
+ handleMouseMove(toIframeCoords(e));
548
+ };
549
+
550
+ const handleWindowMouseUp = (e: MouseEvent) => {
551
+ handleMouseUp(toIframeCoords(e));
552
+ };
553
+
554
+ window.addEventListener("mousemove", handleWindowMouseMove);
555
+ window.addEventListener("mouseup", handleWindowMouseUp);
556
+ cleanupFunctions.push(() => {
557
+ window.removeEventListener("mousemove", handleWindowMouseMove);
558
+ window.removeEventListener("mouseup", handleWindowMouseUp);
559
+ });
560
+
561
+ // 5. 背景クリック後のフォーカス復帰
562
+ //
563
+ // [変更] 選択解除そのものは mousedown の判別器(useElementSelection)へ移した。
564
+ // ここで解除していた頃は
565
+ // - e.target === body 限定なので #artboard に覆われて発火しない
566
+ // - マーキーで選択を確定した直後の click で選択を消してしまう
567
+ // という2つの食い違いがあった。ここではフォーカス復帰だけを担う。
568
+ const handleBackgroundClick = (e: MouseEvent) => {
569
+ if (
570
+ e.target === iframeDoc.body ||
571
+ e.target === iframeDoc.documentElement
572
+ ) {
573
+ // フォーカスをiframeとbodyに確実に維持してショートカットが引き続き機能するようにする
574
+ requestAnimationFrame(() => {
575
+ // まずiframe要素自体にフォーカス
576
+ if (iframe) iframe.focus();
577
+ if (iframeDoc.body) {
578
+ iframeDoc.body.focus();
579
+ console.log('[Canvas] Focus restored after background click');
580
+ }
581
+ });
582
+ }
583
+ };
584
+ iframeDoc.addEventListener("click", handleBackgroundClick);
585
+ cleanupFunctions.push(() =>
586
+ iframeDoc.removeEventListener("click", handleBackgroundClick)
587
+ );
588
+
589
+ // 6. テキスト編集終了(フォーカスアウト)
590
+ const handleFocusOut = (e: FocusEvent) => {
591
+ const element = getEditableElement(e.target, iframeDoc);
592
+ if (element && element.getAttribute("contenteditable") === "true") {
593
+ element.classList.remove("editing");
594
+ element.removeAttribute("contenteditable");
595
+ window.postMessage(
596
+ {
597
+ type: "SLIDE_CONTENT_CHANGED",
598
+ html: getArtboardContent(iframeDoc),
599
+ },
600
+ "*"
601
+ );
602
+ }
603
+ };
604
+ iframeDoc.addEventListener("focusout", handleFocusOut);
605
+ cleanupFunctions.push(() =>
606
+ iframeDoc.removeEventListener("focusout", handleFocusOut)
607
+ );
608
+
609
+ // 7. ドラッグ&ドロップ(画像アップロード用)
610
+ let dragCounter = 0;
611
+
612
+ const handleDragEnter = (e: DragEvent) => {
613
+ e.preventDefault();
614
+ e.stopPropagation();
615
+ dragCounter++;
616
+ if (e.dataTransfer?.types.includes("Files")) {
617
+ window.parent.postMessage({ type: "IFRAME_DRAG_ENTER" }, "*");
618
+ }
619
+ };
620
+
621
+ const handleDragOver = (e: DragEvent) => {
622
+ e.preventDefault();
623
+ e.stopPropagation();
624
+ if (e.dataTransfer?.types.includes("Files")) {
625
+ e.dataTransfer.dropEffect = "copy";
626
+ }
627
+ };
628
+
629
+ const handleDragLeave = (e: DragEvent) => {
630
+ e.preventDefault();
631
+ e.stopPropagation();
632
+ dragCounter--;
633
+ if (dragCounter === 0) {
634
+ window.parent.postMessage({ type: "IFRAME_DRAG_LEAVE" }, "*");
635
+ }
636
+ };
637
+
638
+ const handleDrop = (e: DragEvent) => {
639
+ e.preventDefault();
640
+ e.stopPropagation();
641
+ dragCounter = 0;
642
+
643
+ // コンポーネントのドロップをチェックして親ウィンドウに転送
644
+ const componentData = e.dataTransfer?.getData('application/x-editor-component');
645
+ if (componentData) {
646
+ console.log('[EditorCanvas] Component drop detected, forwarding to parent:', componentData);
647
+ window.parent.postMessage(
648
+ {
649
+ type: "IFRAME_COMPONENT_DROP",
650
+ componentData,
651
+ x: e.clientX,
652
+ y: e.clientY,
653
+ },
654
+ "*"
655
+ );
656
+ return;
657
+ }
658
+
659
+ const files = e.dataTransfer?.files;
660
+ if (files && files.length > 0) {
661
+ window.parent.postMessage(
662
+ {
663
+ type: "IFRAME_DROP",
664
+ files: Array.from(files).map((file) => ({
665
+ name: file.name,
666
+ type: file.type,
667
+ size: file.size,
668
+ })),
669
+ x: e.clientX,
670
+ y: e.clientY,
671
+ },
672
+ "*"
673
+ );
674
+
675
+ Array.from(files).forEach((file, index) => {
676
+ if (file.type.startsWith("image/")) {
677
+ const reader = new FileReader();
678
+ reader.onload = () => {
679
+ window.parent.postMessage(
680
+ {
681
+ type: "IFRAME_DROP_FILE_DATA",
682
+ index,
683
+ fileName: file.name,
684
+ fileType: file.type,
685
+ dataUrl: reader.result,
686
+ x: e.clientX,
687
+ y: e.clientY,
688
+ },
689
+ "*"
690
+ );
691
+ };
692
+ reader.readAsDataURL(file);
693
+ }
694
+ });
695
+ }
696
+ };
697
+
698
+ iframeDoc.addEventListener("dragenter", handleDragEnter);
699
+ iframeDoc.addEventListener("dragover", handleDragOver);
700
+ iframeDoc.addEventListener("dragleave", handleDragLeave);
701
+ iframeDoc.addEventListener("drop", handleDrop);
702
+ cleanupFunctions.push(() => {
703
+ iframeDoc.removeEventListener("dragenter", handleDragEnter);
704
+ iframeDoc.removeEventListener("dragover", handleDragOver);
705
+ iframeDoc.removeEventListener("dragleave", handleDragLeave);
706
+ iframeDoc.removeEventListener("drop", handleDrop);
707
+ });
708
+
709
+ // [修正] ここにあった「iframe内のpasteを親へpostMessageして画像を挿入する」
710
+ // ブリッジは削除した。FrontendVisualEditor が iframe の document にも
711
+ // 直接 paste リスナーを張っており、2系統が同時に走るため1回の貼り付けで
712
+ // 画像が2枚入っていた。ペーストの処理は FrontendVisualEditor の1本に集約する。
713
+
714
+ console.log("[Canvas] Drag/drop handlers registered");
715
+
716
+ // 9. レイアウトモードに応じた初期化とDOMツリー構築
717
+ const initLayout = async () => {
718
+ try {
719
+ if (iframeDoc.fonts && iframeDoc.fonts.ready) {
720
+ await iframeDoc.fonts.ready;
721
+ }
722
+ // 画像も待つ。ロード前に採寸すると、高さ0の画像の分だけ後続要素の座標が
723
+ // ずれた状態で絶対配置に固定される(「編集に入った瞬間に崩れる」の原因)。
724
+ // fonts.ready は画像を待たない
725
+ await Promise.all(
726
+ Array.from(iframeDoc.images)
727
+ .filter((img) => !img.complete)
728
+ .map(
729
+ (img) =>
730
+ new Promise<void>((resolve) => {
731
+ img.addEventListener('load', () => resolve(), { once: true });
732
+ img.addEventListener('error', () => resolve(), { once: true });
733
+ }),
734
+ ),
735
+ );
736
+ await new Promise((resolve) => setTimeout(resolve, 50));
737
+ // レイアウトが完全に確定してから採寸する(Tailwindの遅延適用対策)。
738
+ // 注意: タブが背面にあると Chrome は requestAnimationFrame を止めるため、
739
+ // rAF だけを待つと初期化が永久に走らない(変換もベースラインも無いまま
740
+ // 編集が始まってしまう)。タイムアウトとの競争にして必ず先へ進む
741
+ const win = iframeDoc.defaultView;
742
+ if (win) {
743
+ await Promise.race([
744
+ new Promise((r) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => r(null)))),
745
+ new Promise((r) => setTimeout(r, 300)),
746
+ ]);
747
+ }
748
+
749
+ // [開いた時に一括で絶対配置へ倒す]
750
+ // 1要素ずつ倒す遅延変換は、倒した瞬間にその要素が流れから抜けて
751
+ // 未選択の兄弟が詰め上がり、版面が動く(選択・リサイズで実害が出た)。
752
+ // 一括変換は「親を基準化 → offsetLeft/offsetTop を全要素まとめて採寸 → 書き込み」
753
+ // の順で行うので、変換の前後で見た目は変わらない(dom-utils側で担保)。
754
+ //
755
+ // かつて「一括変換すると崩れる」が起きたのは、採寸が rect÷ズーム倍率で
756
+ // 端数がずれていたため。offsetベースに直してから再有効化した。
757
+ // 絶対配置へ倒すのはスライド(固定キャンバス)のための最適化。
758
+ // Webページは流し込み(フロー)レイアウトで、可変パディングや flex カラムが
759
+ // そのまま次工程の入力になる。倒すと版面が固定ピクセルに固まり、
760
+ // 別の幅で崩れ、デザインツールへの取り込みで意味を失う。
761
+ if (editorModeRef.current !== "webpage") {
762
+ const converted = convertToAbsolutePositioning(iframeDoc);
763
+ console.log('[Canvas] 絶対配置へ一括変換:', converted, '要素');
764
+ }
765
+
766
+ // ここまでが「開いただけ」の姿。以後の変化=ユーザーの編集、と
767
+ // 切り分けるための指紋を全要素に刻む(保存時の変換ノイズ巻き戻しに使う)
768
+ const stamped = stampBaselines(iframeDoc);
769
+ console.log('[Canvas] 書き戻し用ベースライン:', stamped, '要素');
770
+
771
+ const artboardEl = iframeDoc.getElementById("artboard");
772
+ const tree = buildDomTree(iframeDoc, artboardEl || undefined);
773
+ console.log("[Canvas] Built DOM tree with", tree.length, "root nodes");
774
+
775
+ setDomTreeRef.current(tree);
776
+ const firstLevelIds = new Set<string>(
777
+ tree.map((n: DOMTreeNode) => n.id)
778
+ );
779
+ setExpandedNodesRef.current(firstLevelIds);
780
+
781
+ const initializedHtml = artboardEl
782
+ ? artboardEl.innerHTML
783
+ : iframeDoc.body.innerHTML;
784
+ clearHistoryRef.current();
785
+ setHtmlRef.current(initializedHtml);
786
+ // 変換後の姿を「変更なし」の基準にする。これをしないと開いただけで
787
+ // 未保存扱いになり、サムネイル移動のたびに確認ダイアログが出る
788
+ setOriginalHtml(initializedHtml);
789
+ console.log(
790
+ "[Canvas] History reset with initialized HTML from artboard"
791
+ );
792
+
793
+ // マーキーボックスを作成
794
+ initMarqueeBox(iframeDoc);
795
+ } catch (err) {
796
+ console.error("[Canvas] Error during initialization:", err);
797
+ }
798
+ };
799
+
800
+ initLayout().then(() => {
801
+ // ページ切替直後の文書へ現在の倍率を適用(上記 zoomRef のコメント参照)
802
+ applyCanvasZoomDom(iframeDoc, zoomRef.current);
803
+ const artboardEl = iframeDoc.getElementById("artboard");
804
+ if (editorModeRef.current === "webpage" && artboardEl) {
805
+ updateContentHeightRef.current();
806
+
807
+ const cleanupMutation = setupMutationObserver(iframeDoc);
808
+ cleanupFunctions.push(cleanupMutation);
809
+
810
+ const resizeObserver = new ResizeObserver(() => {
811
+ updateContentHeightRef.current();
812
+ });
813
+ resizeObserver.observe(artboardEl);
814
+ cleanupFunctions.push(() => resizeObserver.disconnect());
815
+
816
+ console.log(
817
+ "[Canvas] Webpage mode: content height tracking initialized on artboard"
818
+ );
819
+ }
820
+ });
821
+
822
+ // 重要: クリーンアップ関数をrefに保存して、
823
+ // 次のiframeロード時やアンマウント時にクリーンアップできるようにする
824
+ cleanupFunctionsRef.current = cleanupFunctions;
825
+ console.log("[Canvas] Registered", cleanupFunctions.length, "cleanup functions");
826
+
827
+ // iframeの読み込み完了をマーク(CSS変数注入等のトリガー用)
828
+ setIframeReady(true);
829
+ console.log("[Canvas] iframe ready, setIframeReady(true) called");
830
+
831
+ // eslint-disable-next-line react-hooks/exhaustive-deps
832
+ }, []);
833
+
834
+ // iframeのsrcを設定。
835
+ // 依存に iframeHtml(=originalHtml由来)を含めることで、ページ切替(サムネイル/URL)で
836
+ // コンテンツが差し替わったときに**iframeだけ**を作り直す。殻(ヘッダー・パネル・
837
+ // サムネイル)は残るので、切替のたびに画面全体がリロードされたようには見えない
838
+ useEffect(() => {
839
+ const iframe = iframeRef.current;
840
+ if (!iframe) return;
841
+
842
+ iframe.addEventListener("load", handleIframeLoad);
843
+ const blob = new Blob([iframeHtml], { type: "text/html" });
844
+ iframe.src = URL.createObjectURL(blob);
845
+
846
+ return () => {
847
+ iframe.removeEventListener("load", handleIframeLoad);
848
+ if (iframe.src.startsWith("blob:")) {
849
+ URL.revokeObjectURL(iframe.src);
850
+ }
851
+
852
+ // 重要: アンマウント時にすべてのイベントリスナーをクリーンアップ
853
+ // これにより、メモリリークとゴーストリスナーを防ぐ
854
+ if (cleanupFunctionsRef.current.length > 0) {
855
+ console.log("[Canvas] Unmounting: cleaning up", cleanupFunctionsRef.current.length, "listeners");
856
+ cleanupFunctionsRef.current.forEach(cleanup => {
857
+ try {
858
+ cleanup();
859
+ } catch (e) {
860
+ console.error("[Canvas] Cleanup error during unmount:", e);
861
+ }
862
+ });
863
+ cleanupFunctionsRef.current = [];
864
+ }
865
+ };
866
+ // eslint-disable-next-line react-hooks/exhaustive-deps
867
+ }, [iframeHtml]);
868
+
869
+ // モード変更をiframeに直接適用
870
+ useEffect(() => {
871
+ const iframeDoc = getIframeDoc();
872
+ if (iframeDoc?.body) {
873
+ iframeDoc.body.classList.remove(
874
+ "move-mode",
875
+ "draw-mode",
876
+ "text-mode",
877
+ "tool-rectangle",
878
+ "tool-ellipse",
879
+ "tool-line",
880
+ "tool-arrow",
881
+ "tool-pen",
882
+ "tool-pencil",
883
+ "tool-text",
884
+ "tool-frame",
885
+ "tool-shape",
886
+ "tool-eraser"
887
+ );
888
+
889
+ if (activeTool === "move") {
890
+ iframeDoc.body.classList.add("move-mode");
891
+ } else if (
892
+ [
893
+ "rectangle",
894
+ "ellipse",
895
+ "line",
896
+ "arrow",
897
+ "pen",
898
+ "pencil",
899
+ "frame",
900
+ "shape",
901
+ ].includes(activeTool)
902
+ ) {
903
+ iframeDoc.body.classList.add("draw-mode", `tool-${activeTool}`);
904
+ } else if (activeTool === "text") {
905
+ iframeDoc.body.classList.add("text-mode", "tool-text");
906
+ }
907
+ }
908
+ }, [activeTool, getIframeDoc]);
909
+
910
+ // ズーム状態を iframe 内の #artboard-wrapper に適用
911
+ useEffect(() => {
912
+ const iframeDoc = getIframeDoc();
913
+ if (!iframeDoc) return;
914
+
915
+ const artboardWrapper = iframeDoc.getElementById("artboard-wrapper");
916
+ const canvasScrollArea = iframeDoc.getElementById("canvas-scroll-area");
917
+ const canvasContainer = iframeDoc.getElementById("canvas-container");
918
+
919
+ if (!artboardWrapper || !canvasScrollArea || !canvasContainer) return;
920
+
921
+ const currentScale = zoom / 100;
922
+ // 反映は共通関数(ホイールの即時経路と同じもの)へ寄せる。
923
+ // 二重実装にすると片方だけ補正式がずれる事故が起きる
924
+ applyCanvasZoomDom(iframeDoc, zoom);
925
+
926
+ requestAnimationFrame(() => {
927
+ // ズーム変更で枠の座標基準が変わるので選択セット全体から作り直す
928
+ // (群バウンディングボックスもここで再計算される)
929
+ refreshSelectionOverlay(iframeDoc);
930
+ });
931
+
932
+ console.log(
933
+ `[Canvas] Applied zoom: ${zoom}%, wrapper transform: scale(${currentScale})`
934
+ );
935
+ }, [zoom, editorMode, contentHeight, viewportWidth, getIframeDoc]);
936
+
937
+ // ビューポート幅変更時にiframe内のartboard幅を更新
938
+ useEffect(() => {
939
+ if (editorMode !== "webpage") return;
940
+
941
+ const iframeDoc = getIframeDoc();
942
+ if (!iframeDoc) return;
943
+
944
+ const artboard = iframeDoc.getElementById("artboard");
945
+ const artboardWrapper = iframeDoc.getElementById("artboard-wrapper");
946
+
947
+ if (artboard) {
948
+ artboard.style.width = `${viewportWidth}px`;
949
+ }
950
+ if (artboardWrapper) {
951
+ artboardWrapper.style.width = `${viewportWidth}px`;
952
+ }
953
+
954
+ // fitZoomを再計算
955
+ const newFitZoom = calculateFitZoom();
956
+ setFitZoom(newFitZoom);
957
+
958
+ // viewport単位を使用している要素を再計算
959
+ // キャンバス/アートボードの設計寸法を基準に計算
960
+ // slideモード: SLIDE_WIDTH x SLIDE_HEIGHT
961
+ // webpageモード: viewportWidth x アートボードの高さ
962
+ const canvasWidth = editorMode === "webpage" ? viewportWidth : SLIDE_WIDTH;
963
+ const canvasHeight = editorMode === "webpage"
964
+ ? (artboard?.scrollHeight || SLIDE_HEIGHT)
965
+ : SLIDE_HEIGHT;
966
+ recalculateViewportUnits(iframeDoc, canvasWidth, canvasHeight);
967
+
968
+ console.log(`[Canvas] Viewport width changed to ${viewportWidth}px, recalculated viewport units with canvas dimensions: ${canvasWidth}x${canvasHeight}`);
969
+ }, [viewportWidth, editorMode, getIframeDoc, calculateFitZoom, setFitZoom]);
970
+
971
+ return (
972
+ <div
973
+ ref={containerRef}
974
+ className="h-full w-full"
975
+ style={{
976
+ backgroundColor: isInMultiPageMode ? "transparent" : "#1a1a1a",
977
+ }}
978
+ >
979
+ <iframe
980
+ ref={iframeRef}
981
+ className="w-full h-full border-0"
982
+ title={editorMode === "webpage" ? "Webpage Editor" : "Slide Editor"}
983
+ sandbox="allow-same-origin allow-scripts"
984
+ />
985
+ </div>
986
+ );
987
+ }