@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,388 @@
1
+ /**
2
+ * マーキー選択(範囲選択)処理フック
3
+ * - ドラッグによる複数要素の範囲選択
4
+ * - 閾値ベースのアクティベーション
5
+ * - リアルタイムホバープレビュー
6
+ * - Ctrl/Cmd+クリックでのリーフ選択
7
+ */
8
+
9
+ import { useCallback, useRef, useEffect, MutableRefObject } from 'react';
10
+ import { useEditorContext } from '../EditorContext';
11
+ import {
12
+ refreshSelectionOverlay,
13
+ createMarqueeBox,
14
+ updateMarqueeBox,
15
+ hideMarqueeBox,
16
+ getMarqueeBounds,
17
+ } from '../utils/dom-utils';
18
+ import { extractElementInfo } from '../utils/style-utils';
19
+ import { MARQUEE_DRAG_THRESHOLD } from '../constants';
20
+ import type { MarqueeState } from '../types';
21
+
22
+ interface UseMarqueeSelectionOptions {
23
+ /** マーキー選択の開始保留フラグ */
24
+ marqueeStartPendingRef: MutableRefObject<boolean>;
25
+ /** マーキークリック対象の要素 */
26
+ marqueeClickTargetRef: MutableRefObject<HTMLElement | null>;
27
+ /** Shift+マーキー(既存選択への加算)かどうか */
28
+ marqueeAdditiveRef?: MutableRefObject<boolean>;
29
+ /**
30
+ * マーキー矩形の同期的な保持先(mousedown 側と共有)。
31
+ * React state は反映が1レンダ遅れるため、速いドラッグでは
32
+ * 始点が (0,0) のまま読まれて矩形がずれる/確定処理が走らない。
33
+ */
34
+ marqueeGeomRef?: MutableRefObject<MarqueeState>;
35
+ }
36
+
37
+ /**
38
+ * マーキー矩形と「交差」する要素のIDを集める
39
+ *
40
+ * [なぜ完全包含をやめたか]
41
+ * dom-utils の findElementsInMarquee は完全包含判定なので、
42
+ * 要素をまたぐ帯(横一直線のドラッグ)では何も選ばれなかった。
43
+ * Figma と同じく交差判定にする。
44
+ * ※ dom-utils は他の修正項目と共有しているため、ここにローカル実装を置く。
45
+ */
46
+ function findElementsIntersectingMarquee(
47
+ iframeDoc: Document,
48
+ marquee: { left: number; top: number; right: number; bottom: number }
49
+ ): HTMLElement[] {
50
+ const result: HTMLElement[] = [];
51
+ iframeDoc.querySelectorAll<HTMLElement>('[data-element-id]').forEach((el) => {
52
+ if (
53
+ el.classList.contains('selection-box') ||
54
+ el.classList.contains('marquee-selection-box') ||
55
+ el.classList.contains('drawing-preview') ||
56
+ el.closest('.selection-box')
57
+ ) {
58
+ return;
59
+ }
60
+ const rect = el.getBoundingClientRect();
61
+ // 面積0の要素(非表示・空ラッパー)は拾わない
62
+ if (rect.width <= 0 || rect.height <= 0) return;
63
+ const intersects =
64
+ rect.left < marquee.right &&
65
+ rect.right > marquee.left &&
66
+ rect.top < marquee.bottom &&
67
+ rect.bottom > marquee.top;
68
+ if (intersects) result.push(el);
69
+ });
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * 選択結果を「最上位ノードのみ」に畳む
75
+ *
76
+ * [なぜ必要か]
77
+ * 交差判定にすると、カード1枚を囲んだだけでその中の子孫まで全部入り
78
+ * (実測40件・ハンドル520個)、群バウンディングボックスも群ドラッグも破綻する。
79
+ * 祖先が入っているなら子孫は落とす。
80
+ * ※ dom-utils の collapseToParentIfAllChildrenSelected は
81
+ * 「全ての子が選択されたときだけ親に畳む」別のルールなので使わない。
82
+ */
83
+ function collapseToTopMost(elements: HTMLElement[]): HTMLElement[] {
84
+ return elements.filter(
85
+ (el) => !elements.some((other) => other !== el && other.contains(el))
86
+ );
87
+ }
88
+
89
+ interface UseMarqueeSelectionReturn {
90
+ /** マーキーボックスのref */
91
+ marqueeBoxRef: MutableRefObject<HTMLDivElement | null>;
92
+
93
+ /**
94
+ * マーキー選択のマウス移動処理
95
+ * @returns true if marquee was handled, false otherwise
96
+ */
97
+ handleMarqueeMouseMove: (e: MouseEvent, iframeDoc: Document) => boolean;
98
+
99
+ /**
100
+ * マーキー選択のマウスアップ処理
101
+ * @returns true if marquee was handled, false otherwise
102
+ */
103
+ handleMarqueeMouseUp: (iframeDoc: Document) => boolean;
104
+
105
+ /**
106
+ * マーキーボックスを初期化(iframeロード時に呼ぶ)
107
+ */
108
+ initMarqueeBox: (iframeDoc: Document) => void;
109
+ }
110
+
111
+ /**
112
+ * マーキー選択処理フック
113
+ */
114
+ export function useMarqueeSelection(
115
+ options: UseMarqueeSelectionOptions
116
+ ): UseMarqueeSelectionReturn {
117
+ const {
118
+ marqueeState,
119
+ setMarqueeState,
120
+ setSelectedElementIds,
121
+ } = useEditorContext();
122
+
123
+ const {
124
+ marqueeStartPendingRef,
125
+ marqueeClickTargetRef,
126
+ marqueeAdditiveRef,
127
+ marqueeGeomRef,
128
+ } = options;
129
+
130
+ // マーキーボックスのref
131
+ const marqueeBoxRef = useRef<HTMLDivElement | null>(null);
132
+
133
+ // クロージャ問題回避のためのref
134
+ const fallbackGeomRef = useRef<MarqueeState>(marqueeState);
135
+ // 判定に使う矩形は同期 ref を正とする(共有 ref が無い場合のみ state 追従にフォールバック)
136
+ const marqueeStateRef = marqueeGeomRef ?? fallbackGeomRef;
137
+ const setMarqueeStateRef = useRef(setMarqueeState);
138
+ const setSelectedElementIdsRef = useRef(setSelectedElementIds);
139
+
140
+ // refを最新値に同期(フォールバック用。共有 ref を使うときは同期書き込みが優先される)
141
+ useEffect(() => {
142
+ if (!marqueeGeomRef) fallbackGeomRef.current = marqueeState;
143
+ }, [marqueeState, marqueeGeomRef]);
144
+
145
+ useEffect(() => {
146
+ setMarqueeStateRef.current = setMarqueeState;
147
+ setSelectedElementIdsRef.current = setSelectedElementIds;
148
+ }, [setMarqueeState, setSelectedElementIds]);
149
+
150
+ /**
151
+ * 要素情報を親ウィンドウに送信
152
+ */
153
+ const sendElementInfo = useCallback((element: HTMLElement, iframeDoc: Document) => {
154
+ const info = extractElementInfo(element, iframeDoc);
155
+ if (info) {
156
+ window.postMessage({ type: 'ELEMENT_SELECTED', element: info }, '*');
157
+ }
158
+ }, []);
159
+
160
+ /**
161
+ * マーキーボックスを初期化
162
+ */
163
+ const initMarqueeBox = useCallback((iframeDoc: Document) => {
164
+ if (!marqueeBoxRef.current) {
165
+ marqueeBoxRef.current = createMarqueeBox(iframeDoc);
166
+ }
167
+ }, []);
168
+
169
+ /**
170
+ * マーキー選択のマウス移動処理
171
+ */
172
+ const handleMarqueeMouseMove = useCallback(
173
+ (e: MouseEvent, iframeDoc: Document): boolean => {
174
+ const currentMarqueeState = marqueeStateRef.current;
175
+ if (!marqueeStartPendingRef.current && !currentMarqueeState.isActive) {
176
+ return false;
177
+ }
178
+
179
+ const dx = Math.abs(e.clientX - currentMarqueeState.startX);
180
+ const dy = Math.abs(e.clientY - currentMarqueeState.startY);
181
+
182
+ // ペンディング状態で閾値超え → アクティブに
183
+ if (
184
+ marqueeStartPendingRef.current &&
185
+ (dx > MARQUEE_DRAG_THRESHOLD || dy > MARQUEE_DRAG_THRESHOLD)
186
+ ) {
187
+ marqueeStartPendingRef.current = false;
188
+ // 同期 ref を先に立てる。ここが React state だけだと、
189
+ // 反映前に mouseup が来て「マーキーではなくクリック」と誤判定される
190
+ currentMarqueeState.isActive = true;
191
+ setMarqueeStateRef.current((prev) => ({ ...prev, isActive: true }));
192
+ // マーキー中はホバー予告を止める(輪郭が踊るのを防ぐ)
193
+ iframeDoc.body.classList.add('marquee-active');
194
+ }
195
+
196
+ const isNowActive =
197
+ !marqueeStartPendingRef.current &&
198
+ (currentMarqueeState.isActive ||
199
+ dx > MARQUEE_DRAG_THRESHOLD ||
200
+ dy > MARQUEE_DRAG_THRESHOLD);
201
+
202
+ if (isNowActive) {
203
+ // 状態を更新(同期 ref → React state の順)
204
+ currentMarqueeState.isActive = true;
205
+ currentMarqueeState.currentX = e.clientX;
206
+ currentMarqueeState.currentY = e.clientY;
207
+ setMarqueeStateRef.current((prev) => ({
208
+ ...prev,
209
+ isActive: true,
210
+ currentX: e.clientX,
211
+ currentY: e.clientY,
212
+ }));
213
+
214
+ // マーキーボックスを更新
215
+ if (marqueeBoxRef.current) {
216
+ updateMarqueeBox(
217
+ marqueeBoxRef.current,
218
+ currentMarqueeState.startX,
219
+ currentMarqueeState.startY,
220
+ e.clientX,
221
+ e.clientY
222
+ );
223
+ }
224
+
225
+ // リアルタイムで選択状態をプレビュー
226
+ // 交差 → 最上位のみに畳む、で mouseup 時とまったく同じ結果を出す
227
+ const bounds = getMarqueeBounds({
228
+ isActive: true,
229
+ startX: currentMarqueeState.startX,
230
+ startY: currentMarqueeState.startY,
231
+ currentX: e.clientX,
232
+ currentY: e.clientY,
233
+ });
234
+ const hovered = new Set(
235
+ collapseToTopMost(findElementsIntersectingMarquee(iframeDoc, bounds))
236
+ );
237
+
238
+ // 全要素のホバー状態を更新
239
+ iframeDoc.querySelectorAll('[data-element-id]').forEach((el) => {
240
+ const htmlEl = el as HTMLElement;
241
+ if (hovered.has(htmlEl)) {
242
+ htmlEl.classList.add('marquee-hover');
243
+ } else {
244
+ htmlEl.classList.remove('marquee-hover');
245
+ }
246
+ });
247
+ }
248
+
249
+ return true;
250
+ },
251
+ [marqueeStartPendingRef]
252
+ );
253
+
254
+ /**
255
+ * マーキー選択のマウスアップ処理
256
+ */
257
+ const handleMarqueeMouseUp = useCallback(
258
+ (iframeDoc: Document): boolean => {
259
+ const currentMarqueeState = marqueeStateRef.current;
260
+
261
+ // マーキー選択完了
262
+ if (currentMarqueeState.isActive) {
263
+ // ホバー状態をクリア
264
+ iframeDoc
265
+ .querySelectorAll('.marquee-hover')
266
+ .forEach((el) => el.classList.remove('marquee-hover'));
267
+ iframeDoc.body.classList.remove('marquee-active');
268
+
269
+ const additive = marqueeAdditiveRef?.current === true;
270
+ const bounds = getMarqueeBounds(currentMarqueeState);
271
+
272
+ // Shift+マーキーは既存選択への加算。
273
+ // 加算してから畳むことで、祖先と子孫が同時に残ることがない。
274
+ const existing = additive
275
+ ? Array.from(
276
+ iframeDoc.querySelectorAll<HTMLElement>(
277
+ '[data-element-id].selected'
278
+ )
279
+ )
280
+ : [];
281
+ const hit = findElementsIntersectingMarquee(iframeDoc, bounds);
282
+ const merged = collapseToTopMost(
283
+ Array.from(new Set<HTMLElement>([...existing, ...hit]))
284
+ );
285
+
286
+ // 既存の選択をクリアしてから、畳んだ結果を適用する
287
+ iframeDoc
288
+ .querySelectorAll('.selected')
289
+ .forEach((el) => el.classList.remove('selected'));
290
+ merged.forEach((el) => el.classList.add('selected'));
291
+
292
+ // 枠は選択セット全体から作り直す。
293
+ // 複数選択のときはここで群バウンディングボックスも描かれる
294
+ // (1要素ずつ updateSelectionBox すると群枠が出ない)
295
+ refreshSelectionOverlay(iframeDoc);
296
+
297
+ const selectedIds = merged
298
+ .map((el) => el.getAttribute('data-element-id') || '')
299
+ .filter(Boolean);
300
+ setSelectedElementIdsRef.current(selectedIds);
301
+
302
+ if (merged.length > 0) {
303
+ sendElementInfo(merged[0], iframeDoc);
304
+ } else {
305
+ window.postMessage({ type: 'ELEMENT_DESELECTED' }, '*');
306
+ }
307
+
308
+ console.log(
309
+ '[Canvas] Marquee selected',
310
+ selectedIds.length,
311
+ additive ? '(additive)' : '',
312
+ selectedIds
313
+ );
314
+
315
+ // マーキーボックスを非表示
316
+ if (marqueeBoxRef.current) {
317
+ hideMarqueeBox(marqueeBoxRef.current);
318
+ }
319
+
320
+ // 状態リセット
321
+ marqueeClickTargetRef.current = null;
322
+ if (marqueeAdditiveRef) marqueeAdditiveRef.current = false;
323
+ // 同期 ref も必ず畳む(残っていると次のドラッグが古い矩形を引き継ぐ)
324
+ Object.assign(marqueeStateRef.current, {
325
+ isActive: false,
326
+ startX: 0,
327
+ startY: 0,
328
+ currentX: 0,
329
+ currentY: 0,
330
+ });
331
+ setMarqueeStateRef.current({
332
+ isActive: false,
333
+ startX: 0,
334
+ startY: 0,
335
+ currentX: 0,
336
+ currentY: 0,
337
+ });
338
+ marqueeStartPendingRef.current = false;
339
+ return true;
340
+ }
341
+
342
+ // 閾値未満で離した = 空白の「クリック」。
343
+ // 選択解除は mousedown 時点で済んでいる(Figma と同じ)ので、
344
+ // ここでは保留状態を畳むだけ。
345
+ if (marqueeStartPendingRef.current) {
346
+ marqueeStartPendingRef.current = false;
347
+ iframeDoc
348
+ .querySelectorAll('.marquee-hover')
349
+ .forEach((el) => el.classList.remove('marquee-hover'));
350
+ iframeDoc.body.classList.remove('marquee-active');
351
+
352
+ marqueeClickTargetRef.current = null;
353
+ if (marqueeAdditiveRef) marqueeAdditiveRef.current = false;
354
+ // 同期 ref も必ず畳む(残っていると次のドラッグが古い矩形を引き継ぐ)
355
+ Object.assign(marqueeStateRef.current, {
356
+ isActive: false,
357
+ startX: 0,
358
+ startY: 0,
359
+ currentX: 0,
360
+ currentY: 0,
361
+ });
362
+ setMarqueeStateRef.current({
363
+ isActive: false,
364
+ startX: 0,
365
+ startY: 0,
366
+ currentX: 0,
367
+ currentY: 0,
368
+ });
369
+ return true;
370
+ }
371
+
372
+ return false;
373
+ },
374
+ [
375
+ marqueeStartPendingRef,
376
+ marqueeClickTargetRef,
377
+ marqueeAdditiveRef,
378
+ sendElementInfo,
379
+ ]
380
+ );
381
+
382
+ return {
383
+ marqueeBoxRef,
384
+ handleMarqueeMouseMove,
385
+ handleMarqueeMouseUp,
386
+ initMarqueeBox,
387
+ };
388
+ }
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * メディアライブラリ(生成済み画像・イラスト)を取得するHook
5
+ *
6
+ * `GET /__media` は開発サーバー(vite-plugin-slide-io)が返す静的カタログで、
7
+ * セッション中に内容が変わるのはアップロード時のみ。
8
+ * 381件を毎回取り直すのは無駄なのでモジュールスコープでキャッシュし、
9
+ * 同時オープン時のリクエスト重複も in-flight promise で防ぐ。
10
+ */
11
+
12
+ import { useCallback, useEffect, useState } from 'react';
13
+
14
+ export interface MediaItem {
15
+ /** そのまま <img src> に使えるパス(例: /media/assets/seo-ranking.png) */
16
+ url: string;
17
+ /** ファイル名ベースのID */
18
+ name: string;
19
+ /** object / concept / deco / background / photo / illust / template / upload */
20
+ category: string;
21
+ /** 日本語名 */
22
+ label: string;
23
+ /** 日本語タグ */
24
+ tags: string[];
25
+ /** 想定用途(assets系のみ) */
26
+ usage?: string;
27
+ }
28
+
29
+ interface MediaResponse {
30
+ items?: MediaItem[];
31
+ }
32
+
33
+ let cachedItems: MediaItem[] | null = null;
34
+ let inFlight: Promise<MediaItem[]> | null = null;
35
+
36
+ function normalizeItem(raw: unknown): MediaItem | null {
37
+ if (!raw || typeof raw !== 'object') return null;
38
+ const item = raw as Partial<MediaItem>;
39
+ if (typeof item.url !== 'string' || !item.url) return null;
40
+ return {
41
+ url: item.url,
42
+ name: typeof item.name === 'string' ? item.name : item.url.split('/').pop() || item.url,
43
+ category: typeof item.category === 'string' ? item.category : 'other',
44
+ label: typeof item.label === 'string' && item.label ? item.label : (item.name ?? item.url),
45
+ tags: Array.isArray(item.tags) ? item.tags.filter((t): t is string => typeof t === 'string') : [],
46
+ usage: typeof item.usage === 'string' ? item.usage : undefined,
47
+ };
48
+ }
49
+
50
+ async function fetchMediaItems(): Promise<MediaItem[]> {
51
+ const res = await fetch('/__media');
52
+ if (!res.ok) {
53
+ throw new Error(`メディア一覧の取得に失敗しました (HTTP ${res.status})`);
54
+ }
55
+ const data = (await res.json()) as MediaResponse;
56
+ const items = Array.isArray(data.items) ? data.items : [];
57
+ return items.map(normalizeItem).filter((i): i is MediaItem => i !== null);
58
+ }
59
+
60
+ /**
61
+ * カタログのURL(`/media/...`)をエディタのiframeで解決できる形へ変換する。
62
+ *
63
+ * エディタのキャンバスは blob: URL の iframe なのでルート相対パスが解決できない。
64
+ * `src/lib/slide-html.ts` の toEditorUrls / fromEditorUrls と同じ規約に合わせ、
65
+ * 挿入時は絶対URLにしておく(保存時に fromEditorUrls が相対へ戻す)。
66
+ */
67
+ export function toEditorMediaUrl(url: string): string {
68
+ if (typeof window === 'undefined') return url;
69
+ if (!url.startsWith('/media/')) return url;
70
+ return `${window.location.origin}${url}`;
71
+ }
72
+
73
+ /** アップロード後などにキャッシュを破棄する */
74
+ export function invalidateMediaLibraryCache() {
75
+ cachedItems = null;
76
+ inFlight = null;
77
+ }
78
+
79
+ interface UseMediaLibraryOptions {
80
+ /** true の間だけ取得する(ダイアログが開いたときに初回ロード) */
81
+ enabled?: boolean;
82
+ }
83
+
84
+ export function useMediaLibrary({ enabled = true }: UseMediaLibraryOptions = {}) {
85
+ const [items, setItems] = useState<MediaItem[]>(() => cachedItems ?? []);
86
+ const [isLoading, setIsLoading] = useState(false);
87
+ const [error, setError] = useState<string | null>(null);
88
+
89
+ const load = useCallback(async (force = false) => {
90
+ if (!force && cachedItems) {
91
+ setItems(cachedItems);
92
+ return;
93
+ }
94
+ if (force) invalidateMediaLibraryCache();
95
+
96
+ setIsLoading(true);
97
+ setError(null);
98
+ try {
99
+ if (!inFlight) inFlight = fetchMediaItems();
100
+ const result = await inFlight;
101
+ cachedItems = result;
102
+ setItems(result);
103
+ } catch (e) {
104
+ inFlight = null;
105
+ const message = e instanceof Error ? e.message : 'メディア一覧の取得に失敗しました';
106
+ setError(message);
107
+ console.error('[useMediaLibrary] load failed:', e);
108
+ } finally {
109
+ setIsLoading(false);
110
+ }
111
+ }, []);
112
+
113
+ useEffect(() => {
114
+ if (!enabled) return;
115
+ if (cachedItems) {
116
+ setItems(cachedItems);
117
+ return;
118
+ }
119
+ void load();
120
+ }, [enabled, load]);
121
+
122
+ return {
123
+ items,
124
+ isLoading,
125
+ error,
126
+ /** 再取得(アップロード直後など) */
127
+ reload: useCallback(() => load(true), [load]),
128
+ };
129
+ }