@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,472 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * 描画ツールを管理するHook
5
+ */
6
+
7
+ import { useEffect } from 'react';
8
+ import { useEditorContext } from '../EditorContext';
9
+ import { generateElementId } from '../utils/dom-utils';
10
+ import { inkStyle } from '../utils/ink-style';
11
+ import { pendingShape, shapeStyles as shapeLibStyles } from '../utils/shape-library';
12
+
13
+ /**
14
+ * 描画ツールのイベントハンドリングを行うHook
15
+ */
16
+ export function useDrawingMode() {
17
+ const {
18
+ iframeRef,
19
+ activeTool,
20
+ getIframeDoc,
21
+ selectedElementIds,
22
+ notifyIframeChange,
23
+ lastUsedStylesRef,
24
+ iframeReady,
25
+ } = useEditorContext();
26
+
27
+ useEffect(() => {
28
+ const iframeDoc = getIframeDoc();
29
+ if (!iframeDoc) return;
30
+
31
+ const drawingTools = ['rectangle', 'ellipse', 'line', 'arrow', 'pen', 'pencil', 'frame', 'text', 'shape'];
32
+ if (!drawingTools.includes(activeTool)) return;
33
+
34
+ let isDrawing = false;
35
+ let startX = 0;
36
+ let startY = 0;
37
+ let previewElement: HTMLElement | null = null;
38
+ let penPoints: { x: number; y: number }[] = [];
39
+
40
+ // artboard相対座標を取得(ズームスケール考慮)
41
+ const getArtboardRelativeCoords = (e: MouseEvent): { x: number; y: number } => {
42
+ const artboard = iframeDoc.getElementById('artboard');
43
+ if (!artboard) {
44
+ return { x: e.pageX, y: e.pageY };
45
+ }
46
+
47
+ // artboardのビューポート座標を取得
48
+ const artboardRect = artboard.getBoundingClientRect();
49
+
50
+ // ズームスケールを取得
51
+ const artboardWrapper = iframeDoc.getElementById('artboard-wrapper');
52
+ let scale = 1;
53
+ if (artboardWrapper) {
54
+ let transform = artboardWrapper.style.transform;
55
+ if (!transform) {
56
+ const computedStyle = iframeDoc.defaultView?.getComputedStyle(artboardWrapper);
57
+ transform = computedStyle?.transform || '';
58
+ }
59
+ const scaleMatch = transform.match(/scale\(([^)]+)\)/);
60
+ if (scaleMatch) {
61
+ scale = parseFloat(scaleMatch[1]) || 1;
62
+ } else {
63
+ const matrixMatch = transform.match(/matrix\(([^,]+),/);
64
+ if (matrixMatch) {
65
+ scale = parseFloat(matrixMatch[1]) || 1;
66
+ }
67
+ }
68
+ }
69
+
70
+ // クライアント座標からartboard相対座標に変換(スケールで割る)
71
+ const x = (e.clientX - artboardRect.left) / scale;
72
+ const y = (e.clientY - artboardRect.top) / scale;
73
+
74
+ return { x, y };
75
+ };
76
+
77
+ // 線/矢印SVGを生成
78
+ const createLineSVG = (x1: number, y1: number, x2: number, y2: number, withArrow: boolean): string => {
79
+ const width = Math.abs(x2 - x1);
80
+ const height = Math.abs(y2 - y1);
81
+ const sx = x1 < x2 ? 0 : width;
82
+ const sy = y1 < y2 ? 0 : height;
83
+ const ex = x1 < x2 ? width : 0;
84
+ const ey = y1 < y2 ? height : 0;
85
+
86
+ // 最後に使用した線のスタイルを取得(常に最新値を参照)
87
+ const lineStyles = lastUsedStylesRef.current.line;
88
+ const strokeColor = lineStyles.stroke;
89
+ const strokeWidth = lineStyles.strokeWidth;
90
+
91
+ let arrowHead = '';
92
+ if (withArrow) {
93
+ const angle = Math.atan2(ey - sy, ex - sx);
94
+ const arrowLength = 12;
95
+ const arrowAngle = Math.PI / 6;
96
+ const ax1 = ex - arrowLength * Math.cos(angle - arrowAngle);
97
+ const ay1 = ey - arrowLength * Math.sin(angle - arrowAngle);
98
+ const ax2 = ex - arrowLength * Math.cos(angle + arrowAngle);
99
+ const ay2 = ey - arrowLength * Math.sin(angle + arrowAngle);
100
+ arrowHead = `<polygon points="${ex},${ey} ${ax1},${ay1} ${ax2},${ay2}" fill="${strokeColor}"/>`;
101
+ }
102
+
103
+ return `<svg width="${width}" height="${height}" style="position:absolute;left:0;top:0;overflow:visible;">
104
+ <line x1="${sx}" y1="${sy}" x2="${ex}" y2="${ey}" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>
105
+ ${arrowHead}
106
+ </svg>`;
107
+ };
108
+
109
+ // ペンSVGを生成
110
+ const createPenSVG = (points: { x: number; y: number }[]): string => {
111
+ if (points.length < 2) return '';
112
+ const minX = Math.min(...points.map(p => p.x));
113
+ const minY = Math.min(...points.map(p => p.y));
114
+ const maxX = Math.max(...points.map(p => p.x));
115
+ const maxY = Math.max(...points.map(p => p.y));
116
+ const width = maxX - minX || 1;
117
+ const height = maxY - minY || 1;
118
+
119
+ const pathData = points
120
+ .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x - minX} ${p.y - minY}`)
121
+ .join(' ');
122
+
123
+ // ペン系はインク設定(ペンギャラリー)を参照。線/矢印とは独立
124
+ return `<svg width="${width}" height="${height}" style="position:absolute;left:0;top:0;overflow:visible;">
125
+ <path d="${pathData}" stroke="${inkStyle.color}" stroke-width="${inkStyle.width}" stroke-opacity="${inkStyle.opacity}" fill="none" stroke-linecap="${inkStyle.cap}" stroke-linejoin="round"/>
126
+ </svg>`;
127
+ };
128
+
129
+ const handleMouseDown = (e: MouseEvent) => {
130
+ const bodyClasses = iframeDoc.body.classList;
131
+ if (!bodyClasses.contains('draw-mode') && !bodyClasses.contains('text-mode')) return;
132
+
133
+ e.preventDefault();
134
+ isDrawing = true;
135
+ const coords = getArtboardRelativeCoords(e);
136
+ startX = coords.x;
137
+ startY = coords.y;
138
+ penPoints = [{ x: startX, y: startY }];
139
+
140
+ // プレビュー要素を作成
141
+ previewElement = iframeDoc.createElement('div');
142
+ previewElement.className = 'drawing-preview';
143
+ previewElement.style.cssText = `
144
+ position: absolute;
145
+ left: ${startX}px;
146
+ top: ${startY}px;
147
+ width: 0;
148
+ height: 0;
149
+ `;
150
+
151
+ if (activeTool === 'shape') {
152
+ // ギャラリーで選んだ図形は、ドラッグ中もその形で見せる
153
+ const st = shapeLibStyles(pendingShape);
154
+ previewElement.style.backgroundColor = 'rgba(58,122,87,0.35)';
155
+ previewElement.style.border = '1px solid rgba(58,122,87,0.9)';
156
+ if (st.clipPath) previewElement.style.clipPath = st.clipPath;
157
+ if (st.borderRadius) previewElement.style.borderRadius = st.borderRadius;
158
+ } else if (activeTool === 'ellipse') {
159
+ previewElement.classList.add('ellipse');
160
+ } else if (activeTool === 'frame') {
161
+ previewElement.classList.add('frame');
162
+ } else if (activeTool === 'text') {
163
+ // テキストは即座に作成
164
+ isDrawing = false;
165
+ const textEl = iframeDoc.createElement('div');
166
+ const textId = generateElementId('text');
167
+ textEl.setAttribute('data-element-id', textId);
168
+ textEl.setAttribute('data-shape-type', 'text');
169
+ textEl.setAttribute('data-editable', 'true');
170
+ textEl.setAttribute('contenteditable', 'true');
171
+
172
+ // 最後に使用したテキストスタイルを適用
173
+ const textStyles = lastUsedStylesRef.current.text;
174
+ textEl.style.cssText = `
175
+ position: absolute;
176
+ left: ${startX}px;
177
+ top: ${startY}px;
178
+ min-width: 100px;
179
+ min-height: 1.5em;
180
+ font-size: ${textStyles.fontSize};
181
+ color: ${textStyles.color};
182
+ font-family: ${textStyles.fontFamily};
183
+ font-weight: ${textStyles.fontWeight};
184
+ line-height: ${textStyles.lineHeight};
185
+ letter-spacing: ${textStyles.letterSpacing};
186
+ text-align: ${textStyles.textAlign};
187
+ outline: none;
188
+ padding: 4px;
189
+ `;
190
+ textEl.textContent = 'テキストを入力';
191
+
192
+ // 挿入位置の決定(#artboard内に挿入)
193
+ const artboard = iframeDoc.getElementById('artboard') || iframeDoc.body;
194
+ let inserted = false;
195
+ if (selectedElementIds.length > 0) {
196
+ const lastSelectedId = selectedElementIds[selectedElementIds.length - 1];
197
+ const referenceNode = iframeDoc.querySelector(`[data-element-id="${lastSelectedId}"]`);
198
+ if (referenceNode && referenceNode.parentNode === artboard) {
199
+ referenceNode.insertAdjacentElement('afterend', textEl);
200
+ inserted = true;
201
+ }
202
+ }
203
+
204
+ if (!inserted) {
205
+ artboard.appendChild(textEl);
206
+ }
207
+
208
+ textEl.focus();
209
+ textEl.classList.add('selected', 'editing');
210
+
211
+ const range = iframeDoc.createRange();
212
+ range.selectNodeContents(textEl);
213
+ const selection = iframeDoc.getSelection();
214
+ selection?.removeAllRanges();
215
+ selection?.addRange(range);
216
+
217
+ notifyIframeChange(true);
218
+ window.postMessage({ type: 'TOOL_FINISHED' }, '*');
219
+ return;
220
+ }
221
+
222
+ // プレビュー要素を#artboard内に追加
223
+ const artboardForPreview = iframeDoc.getElementById('artboard') || iframeDoc.body;
224
+ artboardForPreview.appendChild(previewElement);
225
+ };
226
+
227
+ const handleMouseMove = (e: MouseEvent) => {
228
+ if (!isDrawing || !previewElement) return;
229
+
230
+ const coords = getArtboardRelativeCoords(e);
231
+ const currentX = coords.x;
232
+ const currentY = coords.y;
233
+
234
+ if (activeTool === 'pen' || activeTool === 'pencil') {
235
+ penPoints.push({ x: currentX, y: currentY });
236
+ previewElement.innerHTML = createPenSVG(penPoints);
237
+ const minX = Math.min(...penPoints.map(p => p.x));
238
+ const minY = Math.min(...penPoints.map(p => p.y));
239
+ previewElement.style.left = `${minX}px`;
240
+ previewElement.style.top = `${minY}px`;
241
+ } else if (activeTool === 'line' || activeTool === 'arrow') {
242
+ const width = Math.abs(currentX - startX);
243
+ const height = Math.abs(currentY - startY);
244
+ previewElement.style.left = `${Math.min(startX, currentX)}px`;
245
+ previewElement.style.top = `${Math.min(startY, currentY)}px`;
246
+ previewElement.style.width = `${width}px`;
247
+ previewElement.style.height = `${height}px`;
248
+ previewElement.style.border = 'none';
249
+ previewElement.style.backgroundColor = 'transparent';
250
+ previewElement.innerHTML = createLineSVG(startX, startY, currentX, currentY, activeTool === 'arrow');
251
+ } else {
252
+ const width = Math.abs(currentX - startX);
253
+ const height = Math.abs(currentY - startY);
254
+ previewElement.style.left = `${Math.min(startX, currentX)}px`;
255
+ previewElement.style.top = `${Math.min(startY, currentY)}px`;
256
+ previewElement.style.width = `${width}px`;
257
+ previewElement.style.height = `${height}px`;
258
+ }
259
+ };
260
+
261
+ const handleMouseUp = (e: MouseEvent) => {
262
+ if (!isDrawing) return;
263
+ isDrawing = false;
264
+
265
+ const coords = getArtboardRelativeCoords(e);
266
+ const currentX = coords.x;
267
+ const currentY = coords.y;
268
+
269
+ previewElement?.remove();
270
+ previewElement = null;
271
+
272
+ let width = Math.abs(currentX - startX);
273
+ let height = Math.abs(currentY - startY);
274
+ // PowerPointと同じく、ドラッグせずクリックしただけでも既定サイズで配置する。
275
+ // 「図形を選んでクリックしたのに何も起きない」を無くすため
276
+ if (activeTool !== 'pen' && activeTool !== 'pencil' && width < 5 && height < 5) {
277
+ if (activeTool === 'line' || activeTool === 'arrow') {
278
+ width = 240;
279
+ height = 0;
280
+ } else if (activeTool === 'text') {
281
+ width = 320;
282
+ height = 48;
283
+ } else if (activeTool === 'shape') {
284
+ width = pendingShape.w;
285
+ height = pendingShape.h;
286
+ } else {
287
+ width = 200;
288
+ height = 120;
289
+ }
290
+ }
291
+ if ((activeTool === 'pen' || activeTool === 'pencil') && penPoints.length < 3) {
292
+ return;
293
+ }
294
+
295
+ // 最終要素を作成
296
+ const shapeEl = iframeDoc.createElement('div');
297
+ const shapeId = generateElementId('shape');
298
+ shapeEl.setAttribute('data-element-id', shapeId);
299
+ shapeEl.setAttribute('data-shape-type', activeTool);
300
+ shapeEl.setAttribute('data-editable', 'true');
301
+
302
+ const left = Math.min(startX, startX + (currentX >= startX ? width : -width));
303
+ const top = Math.min(startY, startY + (currentY >= startY ? height : -height));
304
+
305
+ // 最後に使用した図形スタイルを取得
306
+ const shapeStyles = lastUsedStylesRef.current.shape;
307
+
308
+ if (activeTool === 'shape') {
309
+ // ギャラリーの図形。塗り/枠/影は「図形の書式」タブから後で変えられる
310
+ shapeEl.setAttribute('data-shape-type', 'shape');
311
+ shapeEl.setAttribute('data-shape-id', pendingShape.id);
312
+ shapeEl.style.position = 'absolute';
313
+ shapeEl.style.left = `${left}px`;
314
+ shapeEl.style.top = `${top}px`;
315
+ shapeEl.style.width = `${width}px`;
316
+ shapeEl.style.height = `${height}px`;
317
+ const st = shapeLibStyles(pendingShape);
318
+ for (const [k, v] of Object.entries(st)) {
319
+ (shapeEl.style as unknown as Record<string, string>)[k] = v;
320
+ }
321
+ } else if (activeTool === 'rectangle') {
322
+ shapeEl.style.cssText = `
323
+ position: absolute;
324
+ left: ${left}px;
325
+ top: ${top}px;
326
+ width: ${width}px;
327
+ height: ${height}px;
328
+ background-color: ${shapeStyles.backgroundColor};
329
+ border-radius: ${shapeStyles.borderRadius};
330
+ border: ${shapeStyles.borderWidth} ${shapeStyles.borderStyle} ${shapeStyles.borderColor};
331
+ opacity: ${shapeStyles.opacity};
332
+ `;
333
+ } else if (activeTool === 'ellipse') {
334
+ shapeEl.style.cssText = `
335
+ position: absolute;
336
+ left: ${left}px;
337
+ top: ${top}px;
338
+ width: ${width}px;
339
+ height: ${height}px;
340
+ background-color: ${shapeStyles.backgroundColor};
341
+ border-radius: 50%;
342
+ border: ${shapeStyles.borderWidth} ${shapeStyles.borderStyle} ${shapeStyles.borderColor};
343
+ opacity: ${shapeStyles.opacity};
344
+ `;
345
+ } else if (activeTool === 'frame') {
346
+ shapeEl.style.cssText = `
347
+ position: absolute;
348
+ left: ${left}px;
349
+ top: ${top}px;
350
+ width: ${width}px;
351
+ height: ${height}px;
352
+ background-color: transparent;
353
+ border: 1px dashed #9CA3AF;
354
+ `;
355
+ } else if (activeTool === 'line' || activeTool === 'arrow') {
356
+ shapeEl.style.cssText = `
357
+ position: absolute;
358
+ left: ${left}px;
359
+ top: ${top}px;
360
+ width: ${width}px;
361
+ height: ${height}px;
362
+ background-color: transparent;
363
+ `;
364
+ shapeEl.innerHTML = createLineSVG(startX, startY, currentX, currentY, activeTool === 'arrow');
365
+ } else if (activeTool === 'pen' || activeTool === 'pencil') {
366
+ const minX = Math.min(...penPoints.map(p => p.x));
367
+ const minY = Math.min(...penPoints.map(p => p.y));
368
+ const maxX = Math.max(...penPoints.map(p => p.x));
369
+ const maxY = Math.max(...penPoints.map(p => p.y));
370
+ shapeEl.style.cssText = `
371
+ position: absolute;
372
+ left: ${minX}px;
373
+ top: ${minY}px;
374
+ width: ${maxX - minX}px;
375
+ height: ${maxY - minY}px;
376
+ background-color: transparent;
377
+ `;
378
+ shapeEl.innerHTML = createPenSVG(penPoints);
379
+ }
380
+
381
+ // 挿入位置の決定(#artboard内に挿入)
382
+ const artboard = iframeDoc.getElementById('artboard') || iframeDoc.body;
383
+ let inserted = false;
384
+ if (selectedElementIds.length > 0) {
385
+ const lastSelectedId = selectedElementIds[selectedElementIds.length - 1];
386
+ const referenceNode = iframeDoc.querySelector(`[data-element-id="${lastSelectedId}"]`);
387
+ if (referenceNode && referenceNode.parentNode === artboard) {
388
+ referenceNode.insertAdjacentElement('afterend', shapeEl);
389
+ inserted = true;
390
+ }
391
+ }
392
+
393
+ if (!inserted) {
394
+ artboard.appendChild(shapeEl);
395
+ }
396
+ // ペン/鉛筆はストロークを続けて描けるようツールを維持する(Figma/PPTと同じ)。
397
+ // 図形やテキストは1回置いたら選択ツールへ戻す。
398
+ const continuous = activeTool === 'pen' || activeTool === 'pencil';
399
+ if (!continuous) shapeEl.classList.add('selected');
400
+
401
+ notifyIframeChange(true);
402
+ if (!continuous) window.postMessage({ type: 'TOOL_FINISHED' }, '*');
403
+ };
404
+
405
+ const handleKeyDown = (e: KeyboardEvent) => {
406
+ if (e.key === 'Escape') window.postMessage({ type: 'TOOL_FINISHED' }, '*');
407
+ };
408
+
409
+ iframeDoc.addEventListener('mousedown', handleMouseDown);
410
+ iframeDoc.addEventListener('mousemove', handleMouseMove);
411
+ iframeDoc.addEventListener('mouseup', handleMouseUp);
412
+ iframeDoc.addEventListener('keydown', handleKeyDown);
413
+
414
+ return () => {
415
+ iframeDoc.removeEventListener('mousedown', handleMouseDown);
416
+ iframeDoc.removeEventListener('mousemove', handleMouseMove);
417
+ iframeDoc.removeEventListener('mouseup', handleMouseUp);
418
+ iframeDoc.removeEventListener('keydown', handleKeyDown);
419
+ };
420
+ }, [activeTool, getIframeDoc, iframeRef, iframeReady]);
421
+
422
+ // 消しゴム: ペン/鉛筆ストロークをクリックまたはなぞって丸ごと消す(PowerPointと同じ挙動)。
423
+ // 選択処理より先に奪うため capture で受ける。Escで選択ツールへ戻る。
424
+ useEffect(() => {
425
+ const iframeDoc = getIframeDoc();
426
+ if (!iframeDoc || activeTool !== 'eraser') return;
427
+
428
+ let erasing = false;
429
+
430
+ const strokeAt = (e: MouseEvent): Element | null => {
431
+ const t = e.target as Element | null;
432
+ return t?.closest?.('[data-shape-type="pen"], [data-shape-type="pencil"]') ?? null;
433
+ };
434
+
435
+ const eraseAt = (e: MouseEvent) => {
436
+ const stroke = strokeAt(e);
437
+ if (stroke) {
438
+ stroke.remove();
439
+ notifyIframeChange(true);
440
+ }
441
+ };
442
+
443
+ const down = (e: MouseEvent) => {
444
+ erasing = true;
445
+ e.preventDefault();
446
+ e.stopPropagation();
447
+ eraseAt(e);
448
+ };
449
+ const move = (e: MouseEvent) => {
450
+ if (erasing) eraseAt(e);
451
+ };
452
+ const up = () => { erasing = false; };
453
+ const key = (e: KeyboardEvent) => {
454
+ if (e.key === 'Escape') window.postMessage({ type: 'TOOL_FINISHED' }, '*');
455
+ };
456
+
457
+ const prevCursor = iframeDoc.body.style.cursor;
458
+ iframeDoc.body.style.cursor = 'crosshair';
459
+ iframeDoc.addEventListener('mousedown', down, true);
460
+ iframeDoc.addEventListener('mousemove', move, true);
461
+ iframeDoc.addEventListener('mouseup', up, true);
462
+ iframeDoc.addEventListener('keydown', key, true);
463
+
464
+ return () => {
465
+ iframeDoc.body.style.cursor = prevCursor;
466
+ iframeDoc.removeEventListener('mousedown', down, true);
467
+ iframeDoc.removeEventListener('mousemove', move, true);
468
+ iframeDoc.removeEventListener('mouseup', up, true);
469
+ iframeDoc.removeEventListener('keydown', key, true);
470
+ };
471
+ }, [activeTool, getIframeDoc, notifyIframeChange, iframeReady]);
472
+ }
@@ -0,0 +1,144 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * スライド内で使用されている色を抽出するHook
5
+ */
6
+
7
+ import { useState, useEffect, useCallback, useMemo } from 'react';
8
+ import { useEditorContext } from '../EditorContext';
9
+
10
+ // RGB を HEX に変換
11
+ const rgbToHex = (rgb: string): string => {
12
+ if (!rgb || rgb === 'transparent' || rgb === 'none') return '';
13
+ if (rgb.startsWith('#')) return rgb.slice(0, 7).toLowerCase();
14
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
15
+ if (!match) return '';
16
+ const r = parseInt(match[1]).toString(16).padStart(2, '0');
17
+ const g = parseInt(match[2]).toString(16).padStart(2, '0');
18
+ const b = parseInt(match[3]).toString(16).padStart(2, '0');
19
+ return `#${r}${g}${b}`;
20
+ };
21
+
22
+ // グラデーションから色を抽出
23
+ const extractColorsFromGradient = (gradient: string): string[] => {
24
+ const colors: string[] = [];
25
+ const rgbaMatches = gradient.matchAll(/rgba?\([^)]+\)/g);
26
+ for (const match of rgbaMatches) {
27
+ const hex = rgbToHex(match[0]);
28
+ if (hex) colors.push(hex);
29
+ }
30
+ const hexMatches = gradient.matchAll(/#[0-9a-fA-F]{6}/g);
31
+ for (const match of hexMatches) {
32
+ colors.push(match[0].toLowerCase());
33
+ }
34
+ return colors;
35
+ };
36
+
37
+ export interface EditorColorsResult {
38
+ colors: string[]; // ユニークな色の配列
39
+ backgroundColors: string[]; // 背景色
40
+ textColors: string[]; // テキスト色
41
+ borderColors: string[]; // 線色
42
+ refresh: () => void; // 色を再取得
43
+ }
44
+
45
+ export function useEditorColors(): EditorColorsResult {
46
+ const { getIframeDoc } = useEditorContext();
47
+ const [colorData, setColorData] = useState<{
48
+ backgroundColors: string[];
49
+ textColors: string[];
50
+ borderColors: string[];
51
+ }>({
52
+ backgroundColors: [],
53
+ textColors: [],
54
+ borderColors: [],
55
+ });
56
+
57
+ const extractColors = useCallback(() => {
58
+ const iframeDoc = getIframeDoc();
59
+ if (!iframeDoc) return;
60
+
61
+ const backgroundColors = new Set<string>();
62
+ const textColors = new Set<string>();
63
+ const borderColors = new Set<string>();
64
+
65
+ // すべての要素をスキャン
66
+ const elements = iframeDoc.querySelectorAll('*');
67
+ elements.forEach((element) => {
68
+ const style = window.getComputedStyle(element);
69
+
70
+ // 背景色
71
+ const bgColor = style.backgroundColor;
72
+ if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
73
+ const hex = rgbToHex(bgColor);
74
+ if (hex) backgroundColors.add(hex);
75
+ }
76
+
77
+ // 背景画像(グラデーション)
78
+ const bgImage = style.backgroundImage;
79
+ if (bgImage && bgImage !== 'none') {
80
+ const gradientColors = extractColorsFromGradient(bgImage);
81
+ gradientColors.forEach(c => backgroundColors.add(c));
82
+ }
83
+
84
+ // テキスト色
85
+ const color = style.color;
86
+ if (color) {
87
+ const hex = rgbToHex(color);
88
+ if (hex) textColors.add(hex);
89
+ }
90
+
91
+ // 線色
92
+ const borderColor = style.borderColor;
93
+ if (borderColor && borderColor !== 'rgba(0, 0, 0, 0)' && borderColor !== 'transparent') {
94
+ // borderColorは複数の値が返ることがある
95
+ const colors = borderColor.split(/\s+(?=rgb)/);
96
+ colors.forEach(c => {
97
+ const hex = rgbToHex(c);
98
+ if (hex) borderColors.add(hex);
99
+ });
100
+ }
101
+ });
102
+
103
+ setColorData({
104
+ backgroundColors: Array.from(backgroundColors),
105
+ textColors: Array.from(textColors),
106
+ borderColors: Array.from(borderColors),
107
+ });
108
+ }, [getIframeDoc]);
109
+
110
+ // 初期取得
111
+ useEffect(() => {
112
+ extractColors();
113
+ }, [extractColors]);
114
+
115
+ // すべての色をまとめてユニークにする
116
+ const colors = useMemo(() => {
117
+ const allColors = new Set([
118
+ ...colorData.backgroundColors,
119
+ ...colorData.textColors,
120
+ ...colorData.borderColors,
121
+ ]);
122
+
123
+ // デフォルトの色を除外(白、黒、透明など)
124
+ const filtered = Array.from(allColors).filter(c =>
125
+ c &&
126
+ c !== '#000000' &&
127
+ c !== '#ffffff' &&
128
+ c !== '#fff' &&
129
+ c !== '#000'
130
+ );
131
+
132
+ // 使用頻度でソート(実装簡略化のためアルファベット順)
133
+ // toSorted()でイミュータブルパターンを維持
134
+ return filtered.toSorted();
135
+ }, [colorData]);
136
+
137
+ return {
138
+ colors,
139
+ backgroundColors: colorData.backgroundColors,
140
+ textColors: colorData.textColors,
141
+ borderColors: colorData.borderColors,
142
+ refresh: extractColors,
143
+ };
144
+ }