@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,584 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * iframeの初期化を管理するHook
5
+ */
6
+
7
+ import { useEffect, useCallback, useRef } from 'react';
8
+ import { useEditorContext } from '../EditorContext';
9
+ import { EDITOR_IFRAME_STYLES, SLIDE_WIDTH, SLIDE_HEIGHT } from '../constants';
10
+ import {
11
+ convertToAbsolutePositioning,
12
+ buildDomTree,
13
+ updateSelectionBox,
14
+ removeSelectionBox,
15
+ getIframeElement,
16
+ getArtboardContent,
17
+ } from '../utils/dom-utils';
18
+ import { generateEditableHtml } from '../utils/html-utils';
19
+ import { extractElementInfo } from '../utils/style-utils';
20
+
21
+ /**
22
+ * iframeの初期化とイベントハンドラ設定を行うHook
23
+ */
24
+ export function useIframeInitializer() {
25
+ const {
26
+ iframeRef,
27
+ html,
28
+ setHtml,
29
+ isUndoRedoRef,
30
+ iframeHtmlRef,
31
+ activeTool,
32
+ setSelectedElement,
33
+ selectedElement,
34
+ pushHistory,
35
+ getIframeDoc,
36
+ editorMode,
37
+ } = useEditorContext();
38
+
39
+ // ズームフィット計算
40
+ const { containerRef, setFitZoom, setZoom, zoom } = useEditorContext();
41
+
42
+ // ズーム値をrefで追跡(イベントハンドラ内で使用するため)
43
+ const zoomRef = useRef(zoom);
44
+ useEffect(() => {
45
+ zoomRef.current = zoom;
46
+ }, [zoom]);
47
+
48
+ const calculateFitZoom = useCallback(() => {
49
+ const container = containerRef.current;
50
+ if (!container) return 100;
51
+
52
+ const containerRect = container.getBoundingClientRect();
53
+ const padding = 64;
54
+ const availableWidth = containerRect.width - padding;
55
+ const availableHeight = containerRect.height - padding;
56
+
57
+ const scaleX = availableWidth / SLIDE_WIDTH;
58
+ const scaleY = availableHeight / SLIDE_HEIGHT;
59
+ const scale = Math.min(scaleX, scaleY);
60
+
61
+ return Math.floor(scale * 100);
62
+ }, [containerRef]);
63
+
64
+ // 初期化時とウィンドウリサイズ時にフィットズームを計算
65
+ useEffect(() => {
66
+ const updateFitZoom = () => {
67
+ const newFitZoom = calculateFitZoom();
68
+ setFitZoom(newFitZoom);
69
+ setZoom(newFitZoom);
70
+ };
71
+
72
+ const timer = setTimeout(updateFitZoom, 100);
73
+ window.addEventListener('resize', updateFitZoom);
74
+ return () => {
75
+ clearTimeout(timer);
76
+ window.removeEventListener('resize', updateFitZoom);
77
+ };
78
+ }, [calculateFitZoom, setFitZoom, setZoom]);
79
+
80
+ // iframe初期化
81
+ useEffect(() => {
82
+ const iframe = iframeRef.current;
83
+ if (!iframe) {
84
+ // iframeがまだマウントされていない場合、次のレンダリングを待つ
85
+ return;
86
+ }
87
+
88
+ const handleLoad = () => {
89
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
90
+ if (!iframeDoc) return;
91
+
92
+ // スタイルを注入
93
+ const style = iframeDoc.createElement('style');
94
+ style.textContent = EDITOR_IFRAME_STYLES;
95
+ iframeDoc.head.appendChild(style);
96
+
97
+ // ドラッグ状態
98
+ let dragState = {
99
+ element: null as HTMLElement | null,
100
+ startX: 0,
101
+ startY: 0,
102
+ origLeft: 0,
103
+ origTop: 0,
104
+ isDragging: false,
105
+ hasMoved: false,
106
+ };
107
+
108
+ // リサイズ状態
109
+ let resizeState = {
110
+ isResizing: false,
111
+ isRotating: false,
112
+ element: null as HTMLElement | null,
113
+ handle: '',
114
+ startX: 0,
115
+ startY: 0,
116
+ origLeft: 0,
117
+ origTop: 0,
118
+ origWidth: 0,
119
+ origHeight: 0,
120
+ origRadius: 0,
121
+ // 回転用の追加プロパティ
122
+ rotation: 0,
123
+ rotationStartAngle: 0,
124
+ centerX: 0,
125
+ centerY: 0,
126
+ };
127
+
128
+ // 要素情報を親に送信
129
+ const sendElementInfo = (element: HTMLElement) => {
130
+ const info = extractElementInfo(element, iframeDoc);
131
+ if (info) {
132
+ window.postMessage({ type: 'ELEMENT_SELECTED', element: info }, '*');
133
+ }
134
+ };
135
+
136
+ // リサイズ/回転ハンドルのマウスダウン
137
+ iframeDoc.addEventListener('mousedown', (e) => {
138
+ const target = e.target as HTMLElement;
139
+ const handle = target.getAttribute('data-handle');
140
+ const elementId = target.getAttribute('data-element-id');
141
+
142
+ if (!handle || !elementId) return;
143
+
144
+ // data-editable="true"を追加してリサイズハンドル自体を選択しないようにする
145
+ const element = iframeDoc.querySelector(`[data-editable="true"][data-element-id="${elementId}"]`) as HTMLElement;
146
+ if (!element) return;
147
+
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+
151
+ const style = iframeDoc.defaultView?.getComputedStyle(element);
152
+ const rect = element.getBoundingClientRect();
153
+
154
+ const computedLeft = parseFloat(element.style.left) || 0;
155
+ const computedTop = parseFloat(element.style.top) || 0;
156
+
157
+ // ズームスケールを考慮(getBoundingClientRectはスケール後の値を返す)
158
+ const scale = zoomRef.current / 100;
159
+ const actualWidth = rect.width / scale;
160
+ const actualHeight = rect.height / scale;
161
+
162
+ // 回転ハンドルの場合
163
+ if (handle.startsWith('rotate-')) {
164
+ // 現在の回転角度を取得
165
+ const transform = element.style.transform || '';
166
+ const rotateMatch = transform.match(/rotate\(([^)]+)deg\)/);
167
+ const currentRotation = rotateMatch ? parseFloat(rotateMatch[1]) : 0;
168
+
169
+ // 要素の中心点を計算
170
+ const centerX = rect.left + rect.width / 2;
171
+ const centerY = rect.top + rect.height / 2;
172
+
173
+ // マウス位置から中心への初期角度を計算
174
+ const startAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * (180 / Math.PI);
175
+
176
+ resizeState = {
177
+ isResizing: false,
178
+ isRotating: true,
179
+ element,
180
+ handle,
181
+ startX: e.clientX,
182
+ startY: e.clientY,
183
+ origLeft: computedLeft,
184
+ origTop: computedTop,
185
+ origWidth: actualWidth,
186
+ origHeight: actualHeight,
187
+ origRadius: 0,
188
+ rotation: currentRotation,
189
+ rotationStartAngle: startAngle,
190
+ centerX,
191
+ centerY,
192
+ };
193
+
194
+ iframeDoc.body.classList.add('rotating');
195
+ return;
196
+ }
197
+
198
+ // 通常のリサイズハンドルの場合
199
+ resizeState = {
200
+ isResizing: true,
201
+ isRotating: false,
202
+ element,
203
+ handle,
204
+ startX: e.clientX,
205
+ startY: e.clientY,
206
+ origLeft: computedLeft,
207
+ origTop: computedTop,
208
+ origWidth: actualWidth,
209
+ origHeight: actualHeight,
210
+ origRadius: parseInt(style?.borderRadius || '0') || 0,
211
+ rotation: 0,
212
+ rotationStartAngle: 0,
213
+ centerX: 0,
214
+ centerY: 0,
215
+ };
216
+ });
217
+
218
+ // リサイズ/回転中のマウス移動
219
+ iframeDoc.addEventListener('mousemove', (e) => {
220
+ // 回転中の処理
221
+ if (resizeState.isRotating && resizeState.element) {
222
+ const element = resizeState.element;
223
+
224
+ // 現在のマウス位置から中心への角度を計算
225
+ const currentAngle = Math.atan2(
226
+ e.clientY - resizeState.centerY,
227
+ e.clientX - resizeState.centerX
228
+ ) * (180 / Math.PI);
229
+
230
+ // 回転差分を計算
231
+ const angleDelta = currentAngle - resizeState.rotationStartAngle;
232
+ const newRotation = resizeState.rotation + angleDelta;
233
+
234
+ // 既存のスケール値を維持しつつ回転を適用
235
+ const existingTransform = element.style.transform || '';
236
+ const scaleMatch = existingTransform.match(/scale\(([^)]+)\)/);
237
+ const scaleValue = scaleMatch ? scaleMatch[0] : '';
238
+
239
+ element.style.transform = `rotate(${newRotation}deg) ${scaleValue}`.trim();
240
+ updateSelectionBox(iframeDoc, element);
241
+ return;
242
+ }
243
+
244
+ if (!resizeState.isResizing || !resizeState.element) return;
245
+
246
+ const deltaX = e.clientX - resizeState.startX;
247
+ const deltaY = e.clientY - resizeState.startY;
248
+ const element = resizeState.element;
249
+ const handle = resizeState.handle;
250
+
251
+ let newLeft = resizeState.origLeft;
252
+ let newTop = resizeState.origTop;
253
+ let newWidth = resizeState.origWidth;
254
+ let newHeight = resizeState.origHeight;
255
+
256
+ if (handle === 'radius') {
257
+ const maxRadius = Math.min(resizeState.origWidth, resizeState.origHeight) / 2;
258
+ const newRadius = Math.max(0, Math.min(maxRadius, resizeState.origRadius - deltaX - deltaY));
259
+ element.style.borderRadius = `${newRadius}px`;
260
+ updateSelectionBox(iframeDoc, element);
261
+ return;
262
+ }
263
+
264
+ const minSize = 10;
265
+
266
+ switch (handle) {
267
+ case 'nw':
268
+ newWidth = Math.max(minSize, resizeState.origWidth - deltaX);
269
+ newHeight = Math.max(minSize, resizeState.origHeight - deltaY);
270
+ newLeft = resizeState.origLeft + (resizeState.origWidth - newWidth);
271
+ newTop = resizeState.origTop + (resizeState.origHeight - newHeight);
272
+ break;
273
+ case 'n':
274
+ newHeight = Math.max(minSize, resizeState.origHeight - deltaY);
275
+ newTop = resizeState.origTop + (resizeState.origHeight - newHeight);
276
+ break;
277
+ case 'ne':
278
+ newWidth = Math.max(minSize, resizeState.origWidth + deltaX);
279
+ newHeight = Math.max(minSize, resizeState.origHeight - deltaY);
280
+ newTop = resizeState.origTop + (resizeState.origHeight - newHeight);
281
+ break;
282
+ case 'e':
283
+ newWidth = Math.max(minSize, resizeState.origWidth + deltaX);
284
+ break;
285
+ case 'se':
286
+ newWidth = Math.max(minSize, resizeState.origWidth + deltaX);
287
+ newHeight = Math.max(minSize, resizeState.origHeight + deltaY);
288
+ break;
289
+ case 's':
290
+ newHeight = Math.max(minSize, resizeState.origHeight + deltaY);
291
+ break;
292
+ case 'sw':
293
+ newWidth = Math.max(minSize, resizeState.origWidth - deltaX);
294
+ newHeight = Math.max(minSize, resizeState.origHeight + deltaY);
295
+ newLeft = resizeState.origLeft + (resizeState.origWidth - newWidth);
296
+ break;
297
+ case 'w':
298
+ newWidth = Math.max(minSize, resizeState.origWidth - deltaX);
299
+ newLeft = resizeState.origLeft + (resizeState.origWidth - newWidth);
300
+ break;
301
+ }
302
+
303
+ const computedStyle = iframeDoc.defaultView?.getComputedStyle(element);
304
+ if (computedStyle?.position !== 'absolute' && computedStyle?.position !== 'fixed') {
305
+ element.style.position = 'absolute';
306
+ }
307
+
308
+ element.style.left = `${newLeft}px`;
309
+ element.style.top = `${newTop}px`;
310
+ element.style.width = `${newWidth}px`;
311
+ element.style.height = `${newHeight}px`;
312
+
313
+ updateSelectionBox(iframeDoc, element);
314
+ });
315
+
316
+ // リサイズ/回転終了
317
+ iframeDoc.addEventListener('mouseup', () => {
318
+ // 回転終了
319
+ if (resizeState.isRotating && resizeState.element) {
320
+ iframeDoc.body.classList.remove('rotating');
321
+ window.postMessage({
322
+ type: 'SLIDE_CONTENT_CHANGED',
323
+ html: getArtboardContent(iframeDoc),
324
+ }, '*');
325
+ sendElementInfo(resizeState.element);
326
+ }
327
+ // リサイズ終了
328
+ else if (resizeState.isResizing && resizeState.element) {
329
+ window.postMessage({
330
+ type: 'SLIDE_CONTENT_CHANGED',
331
+ html: getArtboardContent(iframeDoc),
332
+ }, '*');
333
+ sendElementInfo(resizeState.element);
334
+ }
335
+ resizeState = {
336
+ isResizing: false,
337
+ isRotating: false,
338
+ element: null,
339
+ handle: '',
340
+ startX: 0,
341
+ startY: 0,
342
+ origLeft: 0,
343
+ origTop: 0,
344
+ origWidth: 0,
345
+ origHeight: 0,
346
+ origRadius: 0,
347
+ rotation: 0,
348
+ rotationStartAngle: 0,
349
+ centerX: 0,
350
+ centerY: 0,
351
+ };
352
+ });
353
+
354
+ // 編集可能要素のクリック/ドラッグ
355
+ iframeDoc.querySelectorAll('[data-editable="true"]').forEach((el) => {
356
+ const element = el as HTMLElement;
357
+
358
+ element.addEventListener('mousedown', (e) => {
359
+ const bodyClasses = iframeDoc.body.classList;
360
+ if (bodyClasses.contains('draw-mode') || bodyClasses.contains('text-mode')) return;
361
+ if (element.getAttribute('contenteditable') === 'true') return;
362
+
363
+ e.preventDefault();
364
+ e.stopPropagation();
365
+
366
+ iframeDoc.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
367
+ element.classList.add('selected');
368
+
369
+ updateSelectionBox(iframeDoc, element);
370
+
371
+ let computedLeft = parseFloat(element.style.left) || 0;
372
+ let computedTop = parseFloat(element.style.top) || 0;
373
+
374
+ const computedStyle = iframeDoc.defaultView?.getComputedStyle(element);
375
+ if (computedStyle?.position !== 'absolute' && computedStyle?.position !== 'fixed') {
376
+ const rect = element.getBoundingClientRect();
377
+ const parent = element.parentElement;
378
+
379
+ if (parent && parent !== iframeDoc.body) {
380
+ const parentStyle = iframeDoc.defaultView?.getComputedStyle(parent);
381
+ if (parentStyle?.position === 'static') {
382
+ parent.style.position = 'relative';
383
+ }
384
+ const parentRect = parent.getBoundingClientRect();
385
+ const parentBorderLeft = parseFloat(parentStyle?.borderLeftWidth || '0') || 0;
386
+ const parentBorderTop = parseFloat(parentStyle?.borderTopWidth || '0') || 0;
387
+ computedLeft = rect.left - (parentRect.left + parentBorderLeft);
388
+ computedTop = rect.top - (parentRect.top + parentBorderTop);
389
+ } else {
390
+ const bodyRect = iframeDoc.body.getBoundingClientRect();
391
+ computedLeft = rect.left - bodyRect.left;
392
+ computedTop = rect.top - bodyRect.top;
393
+ }
394
+
395
+ element.style.position = 'absolute';
396
+ element.style.left = `${computedLeft}px`;
397
+ element.style.top = `${computedTop}px`;
398
+ element.style.width = `${rect.width}px`;
399
+ element.style.height = `${rect.height}px`;
400
+ element.style.margin = '0';
401
+ }
402
+
403
+ dragState = {
404
+ element,
405
+ startX: e.clientX,
406
+ startY: e.clientY,
407
+ origLeft: computedLeft,
408
+ origTop: computedTop,
409
+ isDragging: true,
410
+ hasMoved: false,
411
+ };
412
+
413
+ sendElementInfo(element);
414
+ });
415
+ });
416
+
417
+ // ドラッグ中の移動
418
+ iframeDoc.addEventListener('mousemove', (e) => {
419
+ if (!dragState.isDragging || !dragState.element || resizeState.isResizing) return;
420
+
421
+ const deltaX = e.clientX - dragState.startX;
422
+ const deltaY = e.clientY - dragState.startY;
423
+
424
+ if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
425
+ dragState.hasMoved = true;
426
+ dragState.element.classList.add('dragging');
427
+ }
428
+
429
+ const newLeft = dragState.origLeft + deltaX;
430
+ const newTop = dragState.origTop + deltaY;
431
+
432
+ dragState.element.style.left = `${newLeft}px`;
433
+ dragState.element.style.top = `${newTop}px`;
434
+
435
+ updateSelectionBox(iframeDoc, dragState.element);
436
+ });
437
+
438
+ // ドラッグ終了
439
+ iframeDoc.addEventListener('mouseup', () => {
440
+ if (dragState.isDragging && dragState.element) {
441
+ dragState.element.classList.remove('dragging');
442
+
443
+ if (dragState.hasMoved) {
444
+ window.postMessage({
445
+ type: 'SLIDE_CONTENT_CHANGED',
446
+ html: getArtboardContent(iframeDoc),
447
+ }, '*');
448
+ }
449
+ sendElementInfo(dragState.element);
450
+ }
451
+
452
+ dragState = {
453
+ element: null,
454
+ startX: 0,
455
+ startY: 0,
456
+ origLeft: 0,
457
+ origTop: 0,
458
+ isDragging: false,
459
+ hasMoved: false,
460
+ };
461
+ });
462
+
463
+ // 背景クリックで選択解除
464
+ iframeDoc.body.addEventListener('click', (e) => {
465
+ if (e.target === iframeDoc.body) {
466
+ iframeDoc.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
467
+ removeSelectionBox(iframeDoc);
468
+ window.postMessage({ type: 'ELEMENT_DESELECTED' }, '*');
469
+ }
470
+ });
471
+
472
+ // <a>タグのナビゲーションを防止(エディタ内ではリンクを開かない)
473
+ iframeDoc.addEventListener('click', (e) => {
474
+ const target = e.target as HTMLElement;
475
+ const anchor = target.closest('a');
476
+ if (anchor) {
477
+ e.preventDefault();
478
+ }
479
+ }, true); // キャプチャフェーズで処理
480
+
481
+ // DOMツリーを初期化
482
+ setTimeout(() => {
483
+ window.postMessage({
484
+ type: 'DOM_TREE_UPDATED',
485
+ tree: buildDomTree(iframeDoc),
486
+ }, '*');
487
+ }, 100);
488
+
489
+ // 絶対配置への変換(fonts.ready後)
490
+ const initAbsolutePositioning = async () => {
491
+ try {
492
+ if (iframeDoc.fonts && iframeDoc.fonts.ready) {
493
+ await iframeDoc.fonts.ready;
494
+ }
495
+ // 少し追加で待機(レイアウト安定のため)
496
+ await new Promise(resolve => setTimeout(resolve, 50));
497
+
498
+ // [開いた時に一括で絶対配置へ倒す]
499
+ // 1要素ずつ倒す方式は、倒した瞬間にその要素が流れから抜け、
500
+ // 未選択の兄弟が詰め上がって版面が動く(選択・リサイズで実害が出た)。
501
+ // 編集中に流れの中にいる要素を無くしておけば、以後どう触っても他は動かない。
502
+ // 採寸は offsetLeft/offsetTop で全要素まとめて行うので、変換の前後で見た目は変わらない。
503
+ // 絶対配置へ倒すのはスライド(固定キャンバス)のための最適化。
504
+ // Webページは流し込み(フロー)レイアウトで、可変パディングや flex カラムが
505
+ // そのまま次工程の入力になる。倒すと版面が固定ピクセルに固まり、
506
+ // 別の幅で崩れ、デザインツールへの取り込みで意味を失う。
507
+ if (editorMode !== 'webpage') {
508
+ const count = convertToAbsolutePositioning(iframeDoc);
509
+ console.log('[Editor] 絶対配置へ変換:', count, '要素');
510
+ }
511
+
512
+ // 変更を通知
513
+ window.postMessage({
514
+ type: 'SLIDE_CONTENT_CHANGED',
515
+ html: getArtboardContent(iframeDoc),
516
+ }, '*');
517
+ } catch (err) {
518
+ console.error('[Editor] Error converting to absolute positioning:', err);
519
+ }
520
+ };
521
+
522
+ initAbsolutePositioning();
523
+ };
524
+
525
+ iframe.addEventListener('load', handleLoad);
526
+
527
+ const blob = new Blob([generateEditableHtml(html, editorMode)], { type: 'text/html' });
528
+ iframe.src = URL.createObjectURL(blob);
529
+
530
+ return () => {
531
+ iframe.removeEventListener('load', handleLoad);
532
+ if (iframe.src.startsWith('blob:')) {
533
+ URL.revokeObjectURL(iframe.src);
534
+ }
535
+ };
536
+ }, []);
537
+
538
+ // Undo/Redo時のiframe同期
539
+ // 注: EditorContext.tsxにも同様の処理があるが、タイミングの問題で両方必要
540
+ // キャンバス構造を保持するため、#artboard.innerHTMLのみを更新
541
+ useEffect(() => {
542
+ if (isUndoRedoRef.current) {
543
+ isUndoRedoRef.current = false;
544
+ const iframeDoc = getIframeDoc();
545
+ if (iframeDoc && html !== iframeHtmlRef.current) {
546
+ // #artboardの中身のみを更新(キャンバス構造は保持)
547
+ const artboard = iframeDoc.getElementById('artboard');
548
+ if (artboard) {
549
+ artboard.innerHTML = html;
550
+ } else {
551
+ // フォールバック
552
+ iframeDoc.body.innerHTML = html;
553
+ }
554
+ iframeHtmlRef.current = html;
555
+ // フォーカスを復元してショートカットが引き続き機能するようにする
556
+ requestAnimationFrame(() => {
557
+ if (iframeDoc.body) {
558
+ iframeDoc.body.focus();
559
+ }
560
+ });
561
+ }
562
+ }
563
+ }, [html, getIframeDoc, isUndoRedoRef, iframeHtmlRef]);
564
+
565
+ // モード変更をiframeに直接適用
566
+ useEffect(() => {
567
+ const iframeDoc = getIframeDoc();
568
+ if (iframeDoc?.body) {
569
+ iframeDoc.body.classList.remove(
570
+ 'move-mode', 'draw-mode', 'text-mode',
571
+ 'tool-rectangle', 'tool-ellipse', 'tool-line', 'tool-arrow',
572
+ 'tool-pen', 'tool-pencil', 'tool-text', 'tool-frame'
573
+ );
574
+
575
+ if (activeTool === 'move') {
576
+ iframeDoc.body.classList.add('move-mode');
577
+ } else if (['rectangle', 'ellipse', 'line', 'arrow', 'pen', 'pencil', 'frame'].includes(activeTool)) {
578
+ iframeDoc.body.classList.add('draw-mode', `tool-${activeTool}`);
579
+ } else if (activeTool === 'text') {
580
+ iframeDoc.body.classList.add('text-mode', 'tool-text');
581
+ }
582
+ }
583
+ }, [activeTool, getIframeDoc]);
584
+ }