@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,323 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ブレイクポイントガイド
5
+ * キャンバス上にブレイクポイントの縦線を表示し、クリックで幅を切り替える
6
+ * Studioのような直感的なインタラクション
7
+ *
8
+ * iframe内にガイド要素を直接注入することで:
9
+ * - スクロールに自然に追従
10
+ * - コンテンツの背面に表示
11
+ */
12
+
13
+ import { useEffect, useCallback, useRef } from 'react';
14
+ import { useEditorContext } from '../EditorContext';
15
+ import { BREAKPOINT_PRESETS } from '../constants';
16
+
17
+ // ガイドコンテナのID
18
+ const GUIDES_CONTAINER_ID = 'breakpoint-guides-container';
19
+
20
+ interface BreakpointGuidesProps {
21
+ /** キャンバスコンテナの幅(未使用だが互換性のため残す) */
22
+ containerWidth: number;
23
+ /** キャンバスコンテナの高さ(未使用だが互換性のため残す) */
24
+ containerHeight: number;
25
+ }
26
+
27
+ /**
28
+ * ブレイクポイントガイドコンポーネント
29
+ * webpageモード専用
30
+ *
31
+ * iframe内にガイド要素を注入して、スクロールに追従し、コンテンツの背面に表示
32
+ */
33
+ export function BreakpointGuides({ containerWidth, containerHeight }: BreakpointGuidesProps) {
34
+ const { viewportWidth, setViewportWidth, zoom, editorMode, iframeRef } = useEditorContext();
35
+ const hoveredBreakpointRef = useRef<string | null>(null);
36
+
37
+ // ガイド要素を作成・更新する関数
38
+ const updateGuides = useCallback(() => {
39
+ const iframe = iframeRef.current;
40
+ if (!iframe) return;
41
+
42
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
43
+ if (!iframeDoc) return;
44
+
45
+ const canvasScrollArea = iframeDoc.getElementById('canvas-scroll-area');
46
+ if (!canvasScrollArea) return;
47
+
48
+ // 既存のガイドコンテナを取得または作成
49
+ let guidesContainer = iframeDoc.getElementById(GUIDES_CONTAINER_ID);
50
+ if (!guidesContainer) {
51
+ guidesContainer = iframeDoc.createElement('div');
52
+ guidesContainer.id = GUIDES_CONTAINER_ID;
53
+ guidesContainer.style.cssText = `
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ right: 0;
58
+ bottom: 0;
59
+ pointer-events: none;
60
+ z-index: 0;
61
+ `;
62
+ // canvas-scroll-areaの最初の子として挿入(artboard-wrapperより前)
63
+ canvasScrollArea.insertBefore(guidesContainer, canvasScrollArea.firstChild);
64
+ }
65
+
66
+ const scale = zoom / 100;
67
+
68
+ // artboard-wrapperの中心位置を取得
69
+ const artboardWrapper = iframeDoc.getElementById('artboard-wrapper');
70
+ if (!artboardWrapper) return;
71
+
72
+ // artboard-wrapperのスタイルから位置を計算
73
+ const wrapperRect = artboardWrapper.getBoundingClientRect();
74
+ const scrollAreaRect = canvasScrollArea.getBoundingClientRect();
75
+
76
+ // scroll-area内でのartboard中心位置
77
+ const scrollLeft = canvasScrollArea.scrollLeft;
78
+ const scrollTop = canvasScrollArea.scrollTop;
79
+ const centerX = (wrapperRect.left - scrollAreaRect.left + scrollLeft) + (wrapperRect.width / 2);
80
+
81
+ // ガイドのHTML生成
82
+ const guideLines: string[] = [];
83
+
84
+ // 全てのブレイクポイントのガイドラインを生成(現在のビューポート幅以外)
85
+ BREAKPOINT_PRESETS.filter(bp => bp.width !== viewportWidth).forEach((breakpoint) => {
86
+ const isSmaller = breakpoint.width < viewportWidth;
87
+ const halfWidth = (breakpoint.width * scale) / 2;
88
+ const leftPos = centerX - halfWidth;
89
+ const rightPos = centerX + halfWidth;
90
+
91
+ // 線のスタイル
92
+ const lineColor = isSmaller ? 'rgba(107, 114, 128, 0.3)' : 'rgba(107, 114, 128, 0.5)';
93
+ const hoverColor = '#3b82f6';
94
+
95
+ // 共通のラインスタイル
96
+ const lineStyle = `
97
+ position: absolute;
98
+ top: 0;
99
+ bottom: 0;
100
+ width: 1px;
101
+ background: ${lineColor};
102
+ pointer-events: auto;
103
+ cursor: pointer;
104
+ transition: all 0.15s ease;
105
+ `;
106
+
107
+ // 左側のガイドライン
108
+ guideLines.push(`
109
+ <div
110
+ class="bp-guide-line"
111
+ data-breakpoint="${breakpoint.id}"
112
+ data-width="${breakpoint.width}"
113
+ data-side="left"
114
+ style="${lineStyle} left: ${leftPos}px;"
115
+ onmouseenter="this.style.width='3px'; this.style.background='${hoverColor}'; this.style.transform='translateX(-1px)'; this.querySelector('.bp-label').style.opacity='1';"
116
+ onmouseleave="this.style.width='1px'; this.style.background='${lineColor}'; this.style.transform='none'; this.querySelector('.bp-label').style.opacity='0';"
117
+ >
118
+ <div class="bp-label" style="
119
+ position: absolute;
120
+ top: 16px;
121
+ left: 8px;
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 6px;
125
+ padding: 4px 8px;
126
+ background: ${hoverColor};
127
+ color: white;
128
+ font-size: 12px;
129
+ border-radius: 4px;
130
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
131
+ white-space: nowrap;
132
+ opacity: 0;
133
+ transition: opacity 0.15s ease;
134
+ pointer-events: none;
135
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
136
+ ">
137
+ <span style="font-weight: 500;">${breakpoint.name}</span>
138
+ <span style="opacity: 0.75;">${breakpoint.width}px</span>
139
+ </div>
140
+ </div>
141
+ `);
142
+
143
+ // 右側のガイドライン
144
+ guideLines.push(`
145
+ <div
146
+ class="bp-guide-line"
147
+ data-breakpoint="${breakpoint.id}"
148
+ data-width="${breakpoint.width}"
149
+ data-side="right"
150
+ style="${lineStyle} left: ${rightPos}px;"
151
+ onmouseenter="this.style.width='3px'; this.style.background='${hoverColor}'; this.style.transform='translateX(-1px)'; this.querySelector('.bp-label').style.opacity='1';"
152
+ onmouseleave="this.style.width='1px'; this.style.background='${lineColor}'; this.style.transform='none'; this.querySelector('.bp-label').style.opacity='0';"
153
+ >
154
+ <div class="bp-label" style="
155
+ position: absolute;
156
+ top: 16px;
157
+ right: 8px;
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 6px;
161
+ padding: 4px 8px;
162
+ background: ${hoverColor};
163
+ color: white;
164
+ font-size: 12px;
165
+ border-radius: 4px;
166
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
167
+ white-space: nowrap;
168
+ opacity: 0;
169
+ transition: opacity 0.15s ease;
170
+ pointer-events: none;
171
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
172
+ ">
173
+ <span style="opacity: 0.75;">${breakpoint.width}px</span>
174
+ <span style="font-weight: 500;">${breakpoint.name}</span>
175
+ </div>
176
+ </div>
177
+ `);
178
+ });
179
+
180
+ // 現在のビューポート幅の境界線(緑色)
181
+ const currentHalfWidth = (viewportWidth * scale) / 2;
182
+ const currentLeftPos = centerX - currentHalfWidth;
183
+ const currentRightPos = centerX + currentHalfWidth;
184
+
185
+ guideLines.push(`
186
+ <div style="
187
+ position: absolute;
188
+ top: 0;
189
+ bottom: 0;
190
+ left: ${currentLeftPos}px;
191
+ width: 2px;
192
+ background: rgba(34, 197, 94, 0.7);
193
+ pointer-events: none;
194
+ "></div>
195
+ <div style="
196
+ position: absolute;
197
+ top: 0;
198
+ bottom: 0;
199
+ left: ${currentRightPos}px;
200
+ width: 2px;
201
+ background: rgba(34, 197, 94, 0.7);
202
+ pointer-events: none;
203
+ "></div>
204
+ `);
205
+
206
+ guidesContainer.innerHTML = guideLines.join('');
207
+
208
+ // クリックイベントを設定
209
+ const guideLineElements = guidesContainer.querySelectorAll('.bp-guide-line');
210
+ guideLineElements.forEach((el) => {
211
+ el.addEventListener('click', (e) => {
212
+ const width = parseInt((el as HTMLElement).dataset.width || '0', 10);
213
+ if (width > 0) {
214
+ setViewportWidth(width);
215
+ }
216
+ });
217
+ });
218
+ }, [iframeRef, viewportWidth, zoom, setViewportWidth]);
219
+
220
+ // ガイドを更新するタイミング
221
+ useEffect(() => {
222
+ if (editorMode !== 'webpage') return;
223
+
224
+ const iframe = iframeRef.current;
225
+ if (!iframe) return;
226
+
227
+ // iframeがロードされたら更新
228
+ const handleLoad = () => {
229
+ // 少し遅延させてDOMが準備されるのを待つ
230
+ setTimeout(updateGuides, 100);
231
+ };
232
+
233
+ iframe.addEventListener('load', handleLoad);
234
+
235
+ // 既にロード済みの場合
236
+ if (iframe.contentDocument) {
237
+ setTimeout(updateGuides, 100);
238
+ }
239
+
240
+ return () => {
241
+ iframe.removeEventListener('load', handleLoad);
242
+ };
243
+ }, [editorMode, iframeRef, updateGuides]);
244
+
245
+ // ズーム、ビューポート幅、コンテナ幅が変わったら更新
246
+ useEffect(() => {
247
+ if (editorMode !== 'webpage') return;
248
+
249
+ // 遅延させてスタイルが適用されてから更新
250
+ const timer = setTimeout(updateGuides, 50);
251
+ return () => clearTimeout(timer);
252
+ }, [zoom, viewportWidth, containerWidth, editorMode, updateGuides]);
253
+
254
+ // ブラウザリサイズ時にガイドを更新
255
+ useEffect(() => {
256
+ if (editorMode !== 'webpage') return;
257
+
258
+ const handleResize = () => {
259
+ // debounce的に少し遅延させて更新
260
+ setTimeout(updateGuides, 50);
261
+ };
262
+
263
+ // メインウィンドウのリサイズを監視
264
+ window.addEventListener('resize', handleResize);
265
+
266
+ // iframe内のリサイズも監視
267
+ const iframe = iframeRef.current;
268
+ const iframeWindow = iframe?.contentWindow;
269
+ if (iframeWindow) {
270
+ iframeWindow.addEventListener('resize', handleResize);
271
+ }
272
+
273
+ // ResizeObserverでartboard-wrapperのサイズ変更を監視
274
+ let resizeObserver: ResizeObserver | null = null;
275
+ const iframeDoc = iframe?.contentDocument;
276
+ if (iframeDoc) {
277
+ const artboardWrapper = iframeDoc.getElementById('artboard-wrapper');
278
+ const canvasScrollArea = iframeDoc.getElementById('canvas-scroll-area');
279
+
280
+ if (artboardWrapper || canvasScrollArea) {
281
+ resizeObserver = new ResizeObserver(() => {
282
+ setTimeout(updateGuides, 50);
283
+ });
284
+
285
+ if (artboardWrapper) {
286
+ resizeObserver.observe(artboardWrapper);
287
+ }
288
+ if (canvasScrollArea) {
289
+ resizeObserver.observe(canvasScrollArea);
290
+ }
291
+ }
292
+ }
293
+
294
+ return () => {
295
+ window.removeEventListener('resize', handleResize);
296
+ if (iframeWindow) {
297
+ iframeWindow.removeEventListener('resize', handleResize);
298
+ }
299
+ if (resizeObserver) {
300
+ resizeObserver.disconnect();
301
+ }
302
+ };
303
+ }, [editorMode, iframeRef, updateGuides]);
304
+
305
+ // クリーンアップ: コンポーネントがアンマウントされたらガイドを削除
306
+ useEffect(() => {
307
+ return () => {
308
+ const iframe = iframeRef.current;
309
+ if (!iframe) return;
310
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
311
+ if (!iframeDoc) return;
312
+ const container = iframeDoc.getElementById(GUIDES_CONTAINER_ID);
313
+ if (container) {
314
+ container.remove();
315
+ }
316
+ };
317
+ }, [iframeRef]);
318
+
319
+ // このコンポーネント自体は何もレンダリングしない(iframe内にガイドを注入するため)
320
+ return null;
321
+ }
322
+
323
+ export default BreakpointGuides;
@@ -0,0 +1,180 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ブレイクポイントセレクター
5
+ * Webページモードでレスポンシブプレビュー用のビューポート幅を切り替える
6
+ */
7
+
8
+ import { useState, useEffect } from 'react';
9
+ import { Popover, PopoverTrigger, PopoverContent } from '../../components/ui/popover';
10
+ import { Button } from '../../components/ui/button';
11
+ import { Input } from '../../components/ui/input';
12
+ import { Monitor, Laptop, Tablet, Smartphone } from 'lucide-react';
13
+ import { useEditorContext } from '../EditorContext';
14
+ import { BREAKPOINT_PRESETS, type BreakpointIcon } from '../constants';
15
+
16
+ // ビューポート幅の制限
17
+ const MIN_VIEWPORT_WIDTH = 320;
18
+ const MAX_VIEWPORT_WIDTH = 1920;
19
+
20
+ // アイコンマッピング
21
+ const ICON_MAP: Record<BreakpointIcon, React.ComponentType<{ className?: string }>> = {
22
+ 'monitor': Monitor,
23
+ 'laptop': Laptop,
24
+ 'tablet': Tablet,
25
+ 'mobile': Smartphone,
26
+ 'mobile-small': Smartphone,
27
+ };
28
+
29
+ /**
30
+ * ブレイクポイントセレクターコンポーネント
31
+ * webpageモード専用(slideモードでは非表示)
32
+ */
33
+ export function BreakpointSelector() {
34
+ const { viewportWidth, setViewportWidth, editorMode, restoreFocus } = useEditorContext();
35
+ const [isOpen, setIsOpen] = useState(false);
36
+ const [customWidth, setCustomWidth] = useState(viewportWidth.toString());
37
+
38
+ // viewportWidthが外部から変更された場合にcustomWidthを同期
39
+ useEffect(() => {
40
+ setCustomWidth(viewportWidth.toString());
41
+ }, [viewportWidth]);
42
+
43
+ // webpageモード以外では表示しない
44
+ if (editorMode !== 'webpage') return null;
45
+
46
+ // 現在のプリセットを特定(カスタム値の場合はnull)
47
+ const activePreset = BREAKPOINT_PRESETS.find(p => p.width === viewportWidth);
48
+
49
+ // カスタム値かどうかを判定
50
+ const isCustomWidth = !activePreset;
51
+
52
+ // トリガーボタン用のアイコン(カスタム値の場合はMonitor)
53
+ const TriggerIcon = activePreset ? ICON_MAP[activePreset.icon] : Monitor;
54
+
55
+ const handlePresetSelect = (width: number) => {
56
+ setViewportWidth(width);
57
+ setCustomWidth(width.toString());
58
+ };
59
+
60
+ const handleCustomWidthChange = (value: string) => {
61
+ setCustomWidth(value);
62
+ };
63
+
64
+ const handleCustomWidthBlur = () => {
65
+ const width = parseInt(customWidth, 10);
66
+ if (!isNaN(width)) {
67
+ const clampedWidth = Math.max(MIN_VIEWPORT_WIDTH, Math.min(MAX_VIEWPORT_WIDTH, width));
68
+ setViewportWidth(clampedWidth);
69
+ setCustomWidth(clampedWidth.toString());
70
+ } else {
71
+ // 無効な値の場合は現在のviewportWidthに戻す
72
+ setCustomWidth(viewportWidth.toString());
73
+ }
74
+ };
75
+
76
+ const handleCustomWidthKeyDown = (e: React.KeyboardEvent) => {
77
+ if (e.key === 'Enter') {
78
+ handleCustomWidthBlur();
79
+ }
80
+ };
81
+
82
+ const handleDone = () => {
83
+ setIsOpen(false);
84
+ };
85
+
86
+ const handleOpenChange = (open: boolean) => {
87
+ setIsOpen(open);
88
+ // ポップオーバーが閉じた時にフォーカスを復元
89
+ if (!open) {
90
+ requestAnimationFrame(() => {
91
+ restoreFocus();
92
+ });
93
+ }
94
+ };
95
+
96
+ return (
97
+ <Popover open={isOpen} onOpenChange={handleOpenChange}>
98
+ <PopoverTrigger asChild>
99
+ <Button
100
+ variant="ghost"
101
+ size="sm"
102
+ className="h-7 px-2 text-xs text-gray-400 hover:text-white hover:bg-[#444444] gap-1.5"
103
+ >
104
+ <TriggerIcon className="h-4 w-4" />
105
+ <span>{viewportWidth}px</span>
106
+ </Button>
107
+ </PopoverTrigger>
108
+ <PopoverContent
109
+ className="w-auto p-4 bg-[#1e1e1e] border-[#444444]"
110
+ side="top"
111
+ align="center"
112
+ >
113
+ {/* ブレイクポイントオプション */}
114
+ <div className="flex items-center gap-2 mb-4">
115
+ {BREAKPOINT_PRESETS.map((preset) => {
116
+ const Icon = ICON_MAP[preset.icon];
117
+ const isActive = viewportWidth === preset.width;
118
+ return (
119
+ <button
120
+ key={preset.id}
121
+ onClick={() => handlePresetSelect(preset.width)}
122
+ className={`flex flex-col items-center p-3 rounded-lg transition-colors min-w-[60px]
123
+ ${isActive
124
+ ? 'bg-green-600 text-white'
125
+ : 'hover:bg-[#444444] text-gray-400 hover:text-white'
126
+ }`}
127
+ >
128
+ <Icon className={`h-6 w-6 mb-1 ${preset.icon === 'mobile-small' ? 'scale-75' : ''}`} />
129
+ <span className="text-[10px] font-medium mb-0.5">{preset.name}</span>
130
+ <span className="text-xs">{preset.width}</span>
131
+ </button>
132
+ );
133
+ })}
134
+ </div>
135
+
136
+ {/* 範囲テキスト */}
137
+ {activePreset ? (
138
+ <p className="text-xs text-gray-500 mb-4 text-center">
139
+ {activePreset.name}設定サイズ: {activePreset.minRange} ~ {activePreset.maxRange} px
140
+ </p>
141
+ ) : (
142
+ <p className="text-xs text-gray-500 mb-4 text-center">
143
+ カスタムサイズ: {viewportWidth}px
144
+ </p>
145
+ )}
146
+
147
+ {/* カスタム幅入力 */}
148
+ <div className="flex items-center gap-2 mb-4">
149
+ <label className="text-xs text-gray-400 whitespace-nowrap">カスタム幅:</label>
150
+ <div className="flex items-center gap-1">
151
+ <Input
152
+ type="number"
153
+ value={customWidth}
154
+ onChange={(e) => handleCustomWidthChange(e.target.value)}
155
+ onBlur={handleCustomWidthBlur}
156
+ onKeyDown={handleCustomWidthKeyDown}
157
+ min={MIN_VIEWPORT_WIDTH}
158
+ max={MAX_VIEWPORT_WIDTH}
159
+ className={`w-20 h-8 text-xs bg-[#2c2c2c] border-[#444444] text-white text-center
160
+ ${isCustomWidth ? 'border-green-600' : ''}`}
161
+ />
162
+ <span className="text-xs text-gray-500">px</span>
163
+ </div>
164
+ <span className="text-[10px] text-gray-600">({MIN_VIEWPORT_WIDTH}-{MAX_VIEWPORT_WIDTH})</span>
165
+ </div>
166
+
167
+ {/* 完了ボタン */}
168
+ <Button
169
+ onClick={handleDone}
170
+ className="w-full bg-[#444444] hover:bg-[#4a4a4a] text-white"
171
+ size="sm"
172
+ >
173
+ 完了
174
+ </Button>
175
+ </PopoverContent>
176
+ </Popover>
177
+ );
178
+ }
179
+
180
+ export default BreakpointSelector;