@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,163 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useEffect, useRef } from 'react';
4
+
5
+ export interface UseResizablePanelOptions {
6
+ /** 初期幅 */
7
+ initialWidth: number;
8
+ /** 最小幅 */
9
+ minWidth: number;
10
+ /** 最大幅 */
11
+ maxWidth: number;
12
+ /** リサイズ方向: 'left'=左端をドラッグ, 'right'=右端をドラッグ */
13
+ direction: 'left' | 'right';
14
+ /** ローカルストレージのキー(指定時は幅を永続化) */
15
+ storageKey?: string;
16
+ }
17
+
18
+ export interface UseResizablePanelReturn {
19
+ /** 現在の幅 */
20
+ width: number;
21
+ /** 幅を設定 */
22
+ setWidth: (width: number) => void;
23
+ /** ドラッグ中かどうか */
24
+ isDragging: boolean;
25
+ /** リサイズハンドルのprops */
26
+ resizeHandleProps: {
27
+ onMouseDown: (e: React.MouseEvent) => void;
28
+ onTouchStart: (e: React.TouchEvent) => void;
29
+ style: React.CSSProperties;
30
+ className: string;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * リサイズ可能なパネル用フック
36
+ */
37
+ export function useResizablePanel({
38
+ initialWidth,
39
+ minWidth,
40
+ maxWidth,
41
+ direction,
42
+ storageKey,
43
+ }: UseResizablePanelOptions): UseResizablePanelReturn {
44
+ // ローカルストレージから初期値を読み込み
45
+ const getInitialWidth = () => {
46
+ if (storageKey && typeof window !== 'undefined') {
47
+ const stored = localStorage.getItem(storageKey);
48
+ if (stored) {
49
+ const parsed = parseInt(stored, 10);
50
+ if (!isNaN(parsed) && parsed >= minWidth && parsed <= maxWidth) {
51
+ return parsed;
52
+ }
53
+ }
54
+ }
55
+ return initialWidth;
56
+ };
57
+
58
+ const [width, setWidthState] = useState(getInitialWidth);
59
+ const [isDragging, setIsDragging] = useState(false);
60
+ const startXRef = useRef(0);
61
+ const startWidthRef = useRef(0);
62
+
63
+ // 幅を設定(範囲内に収める)
64
+ const setWidth = useCallback((newWidth: number) => {
65
+ const clampedWidth = Math.min(maxWidth, Math.max(minWidth, newWidth));
66
+ setWidthState(clampedWidth);
67
+
68
+ // ローカルストレージに保存
69
+ if (storageKey && typeof window !== 'undefined') {
70
+ localStorage.setItem(storageKey, String(clampedWidth));
71
+ }
72
+ }, [minWidth, maxWidth, storageKey]);
73
+
74
+ // マウスダウン
75
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ setIsDragging(true);
79
+ startXRef.current = e.clientX;
80
+ startWidthRef.current = width;
81
+ }, [width]);
82
+
83
+ // タッチスタート
84
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
85
+ e.stopPropagation();
86
+ setIsDragging(true);
87
+ startXRef.current = e.touches[0].clientX;
88
+ startWidthRef.current = width;
89
+ }, [width]);
90
+
91
+ // マウス移動・アップイベント
92
+ useEffect(() => {
93
+ if (!isDragging) return;
94
+
95
+ const handleMouseMove = (e: MouseEvent) => {
96
+ const deltaX = e.clientX - startXRef.current;
97
+ // direction が 'right' なら右にドラッグすると幅が増える
98
+ // direction が 'left' なら左にドラッグすると幅が増える
99
+ const newWidth = direction === 'right'
100
+ ? startWidthRef.current + deltaX
101
+ : startWidthRef.current - deltaX;
102
+ setWidth(newWidth);
103
+ };
104
+
105
+ const handleTouchMove = (e: TouchEvent) => {
106
+ const deltaX = e.touches[0].clientX - startXRef.current;
107
+ const newWidth = direction === 'right'
108
+ ? startWidthRef.current + deltaX
109
+ : startWidthRef.current - deltaX;
110
+ setWidth(newWidth);
111
+ };
112
+
113
+ const handleMouseUp = () => {
114
+ setIsDragging(false);
115
+ };
116
+
117
+ const handleTouchEnd = () => {
118
+ setIsDragging(false);
119
+ };
120
+
121
+ // グローバルイベントリスナー
122
+ document.addEventListener('mousemove', handleMouseMove);
123
+ document.addEventListener('mouseup', handleMouseUp);
124
+ document.addEventListener('touchmove', handleTouchMove);
125
+ document.addEventListener('touchend', handleTouchEnd);
126
+
127
+ // カーソルスタイルを変更
128
+ document.body.style.cursor = 'col-resize';
129
+ document.body.style.userSelect = 'none';
130
+
131
+ return () => {
132
+ document.removeEventListener('mousemove', handleMouseMove);
133
+ document.removeEventListener('mouseup', handleMouseUp);
134
+ document.removeEventListener('touchmove', handleTouchMove);
135
+ document.removeEventListener('touchend', handleTouchEnd);
136
+ document.body.style.cursor = '';
137
+ document.body.style.userSelect = '';
138
+ };
139
+ }, [isDragging, direction, setWidth]);
140
+
141
+ // リサイズハンドルのprops
142
+ const resizeHandleProps = {
143
+ onMouseDown: handleMouseDown,
144
+ onTouchStart: handleTouchStart,
145
+ style: {
146
+ cursor: 'col-resize',
147
+ } as React.CSSProperties,
148
+ className: `
149
+ absolute top-0 ${direction === 'right' ? 'right-0' : 'left-0'}
150
+ w-1 h-full z-10
151
+ hover:bg-blue-500/50
152
+ ${isDragging ? 'bg-blue-500' : 'bg-transparent'}
153
+ transition-colors duration-150
154
+ `.trim().replace(/\s+/g, ' '),
155
+ };
156
+
157
+ return {
158
+ width,
159
+ setWidth,
160
+ isDragging,
161
+ resizeHandleProps,
162
+ };
163
+ }
@@ -0,0 +1,255 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useRichPaste.ts
5
+ *
6
+ * Hook for handling rich clipboard paste operations (Excel, Word, Browser HTML, SVG).
7
+ * Integrates with the editor's element system for proper insertion and selection.
8
+ */
9
+
10
+ import { useCallback } from 'react';
11
+ import { useEditorContext } from '../EditorContext';
12
+ import { getIframeElement, updateSelectionBox } from '../utils/dom-utils';
13
+ import { extractElementInfo } from '../utils/style-utils';
14
+ import {
15
+ detectContentType,
16
+ processClipboardContent,
17
+ ClipboardContentType,
18
+ PasteProcessResult,
19
+ } from '../utils/paste-processors';
20
+ import { fetchAndApplyFigmaImages } from '../utils/figma-paste';
21
+
22
+ /**
23
+ * Result of a rich paste operation
24
+ */
25
+ export interface RichPasteResult {
26
+ success: boolean;
27
+ elementIds: string[];
28
+ contentType: ClipboardContentType;
29
+ error?: string;
30
+ /** When true, caller should fallback to image paste handling */
31
+ shouldFallbackToImage?: boolean;
32
+ /** Number of Figma images successfully fetched and applied */
33
+ figmaImagesApplied?: number;
34
+ /** Errors from Figma image fetching */
35
+ figmaImageErrors?: string[];
36
+ }
37
+
38
+ /**
39
+ * Hook for handling rich clipboard paste operations.
40
+ *
41
+ * Features:
42
+ * - Detects clipboard content type (Excel, Word, Browser HTML, SVG, plain text)
43
+ * - Processes and sanitizes content while preserving styles
44
+ * - Inserts elements at the correct position (after selected element or at end)
45
+ * - Updates selection to newly pasted elements
46
+ *
47
+ * @returns Object containing paste utilities and handlers
48
+ */
49
+ export function useRichPaste() {
50
+ const {
51
+ selectedElement,
52
+ setSelectedElement,
53
+ setSelectedElementIds,
54
+ notifyIframeChange,
55
+ getIframeDoc,
56
+ } = useEditorContext();
57
+
58
+ /**
59
+ * Check if clipboard contains rich content that we can handle.
60
+ * Images are excluded as they're handled by useImageUpload.
61
+ */
62
+ const canHandleRichPaste = useCallback((clipboardData: DataTransfer): boolean => {
63
+ const contentType = detectContentType(clipboardData);
64
+ // We handle everything except images (handled by useImageUpload) and unknown
65
+ return contentType !== 'image' && contentType !== 'unknown';
66
+ }, []);
67
+
68
+ /**
69
+ * Get the content type without processing
70
+ */
71
+ const getContentType = useCallback((clipboardData: DataTransfer): ClipboardContentType => {
72
+ return detectContentType(clipboardData);
73
+ }, []);
74
+
75
+ /**
76
+ * Paste rich content from clipboard into the editor.
77
+ *
78
+ * @param clipboardData - DataTransfer from paste event
79
+ * @param forceContentType - Optional content type to force (overrides detection)
80
+ * @returns Result of the paste operation
81
+ */
82
+ const pasteRichContent = useCallback(async (
83
+ clipboardData: DataTransfer,
84
+ forceContentType?: ClipboardContentType
85
+ ): Promise<RichPasteResult> => {
86
+ const iframeDoc = getIframeDoc();
87
+ if (!iframeDoc) {
88
+ return {
89
+ success: false,
90
+ elementIds: [],
91
+ contentType: 'unknown',
92
+ error: 'No iframe document available',
93
+ };
94
+ }
95
+
96
+ // Detect content type (or use forced type)
97
+ const contentType = forceContentType || detectContentType(clipboardData);
98
+ console.log('[useRichPaste] Detected content type:', contentType, forceContentType ? '(forced)' : '');
99
+
100
+ // Skip images (handled by useImageUpload)
101
+ if (contentType === 'image') {
102
+ return {
103
+ success: false,
104
+ elementIds: [],
105
+ contentType,
106
+ error: 'Images should be handled by useImageUpload',
107
+ };
108
+ }
109
+
110
+ // Process clipboard content
111
+ const result: PasteProcessResult = processClipboardContent(clipboardData, iframeDoc, contentType);
112
+
113
+ if (!result.success || result.elements.length === 0) {
114
+ console.warn('[useRichPaste] Failed to process content:', result.error);
115
+ return {
116
+ success: false,
117
+ elementIds: [],
118
+ contentType: result.contentType,
119
+ error: result.error || 'Failed to process clipboard content',
120
+ shouldFallbackToImage: result.shouldFallbackToImage,
121
+ };
122
+ }
123
+
124
+ // Clear existing selection boxes
125
+ iframeDoc.querySelectorAll('.selection-box').forEach(box => box.remove());
126
+ iframeDoc.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
127
+
128
+ // Determine insertion point
129
+ let insertAfterElement: HTMLElement | null = null;
130
+ if (selectedElement) {
131
+ insertAfterElement = getIframeElement(iframeDoc, selectedElement.id);
132
+ }
133
+
134
+ // Insert elements
135
+ const pastedIds: string[] = [];
136
+ const pastedElements: HTMLElement[] = [];
137
+
138
+ result.elements.forEach((el) => {
139
+ const elementId = el.getAttribute('data-element-id');
140
+ if (!elementId) {
141
+ console.warn('[useRichPaste] Element missing data-element-id, skipping');
142
+ return;
143
+ }
144
+
145
+ // Insert at appropriate position
146
+ if (insertAfterElement && insertAfterElement.parentNode) {
147
+ insertAfterElement.parentNode.insertBefore(el, insertAfterElement.nextSibling);
148
+ insertAfterElement = el; // Update for next element
149
+ } else {
150
+ // Find artboard or use body
151
+ const artboard = iframeDoc.querySelector('[data-element-id="artboard"]') || iframeDoc.body;
152
+ artboard.appendChild(el);
153
+ }
154
+
155
+ pastedIds.push(elementId);
156
+ pastedElements.push(el);
157
+ });
158
+
159
+ // Notify change
160
+ notifyIframeChange();
161
+
162
+ // Update selection to pasted elements
163
+ if (pastedIds.length > 0) {
164
+ setSelectedElementIds(pastedIds);
165
+ pastedElements.forEach((el) => {
166
+ el.classList.add('selected');
167
+ updateSelectionBox(iframeDoc, el, true);
168
+ });
169
+
170
+ // Set primary selected element
171
+ if (pastedElements.length > 0) {
172
+ const info = extractElementInfo(pastedElements[0], iframeDoc);
173
+ if (info) {
174
+ setSelectedElement(info);
175
+ }
176
+ }
177
+ }
178
+
179
+ console.log('[useRichPaste] Successfully pasted', pastedIds.length, 'elements');
180
+
181
+ // For Figma content, fetch and apply images asynchronously
182
+ let figmaImagesApplied: number | undefined;
183
+ let figmaImageErrors: string[] | undefined;
184
+
185
+ const hasImageHashes = result.figmaImageHashes && result.figmaImageHashes.length > 0;
186
+ const hasImageNodeIds = result.figmaImageNodeIds && result.figmaImageNodeIds.length > 0;
187
+
188
+ if (
189
+ result.contentType === 'html-figma' &&
190
+ result.figmaFileKey &&
191
+ (hasImageHashes || hasImageNodeIds)
192
+ ) {
193
+ console.log('[useRichPaste] Fetching Figma images...',
194
+ hasImageHashes ? `${result.figmaImageHashes!.length} hashes` : 'no hashes',
195
+ hasImageNodeIds ? `${result.figmaImageNodeIds!.length} nodeIds` : 'no nodeIds',
196
+ );
197
+
198
+ // Find the root element to search within
199
+ const rootElement = pastedElements[0]?.parentElement || iframeDoc.body;
200
+
201
+ const imageResult = await fetchAndApplyFigmaImages(
202
+ result.figmaFileKey,
203
+ result.figmaImageHashes || [],
204
+ rootElement,
205
+ result.figmaImageNodeIds
206
+ );
207
+
208
+ figmaImagesApplied = imageResult.applied;
209
+ figmaImageErrors = imageResult.errors.length > 0 ? imageResult.errors : undefined;
210
+
211
+ if (imageResult.applied > 0) {
212
+ // Notify change again since images were applied
213
+ notifyIframeChange();
214
+ console.log(`[useRichPaste] Applied ${imageResult.applied} Figma images`);
215
+ }
216
+ }
217
+
218
+ return {
219
+ success: true,
220
+ elementIds: pastedIds,
221
+ contentType: result.contentType,
222
+ figmaImagesApplied,
223
+ figmaImageErrors,
224
+ };
225
+ }, [getIframeDoc, selectedElement, setSelectedElement, setSelectedElementIds, notifyIframeChange]);
226
+
227
+ /**
228
+ * Debug helper: Log clipboard contents
229
+ */
230
+ const debugClipboard = useCallback((clipboardData: DataTransfer): void => {
231
+ console.log('[useRichPaste] === Clipboard Debug ===');
232
+ console.log('Available types:', Array.from(clipboardData.types));
233
+
234
+ clipboardData.types.forEach((type) => {
235
+ const data = clipboardData.getData(type);
236
+ console.log(`[${type}]:`, data.substring(0, 500) + (data.length > 500 ? '...' : ''));
237
+ });
238
+
239
+ console.log('Items:', Array.from(clipboardData.items).map(item => ({
240
+ kind: item.kind,
241
+ type: item.type,
242
+ })));
243
+
244
+ console.log('Detected type:', detectContentType(clipboardData));
245
+ console.log('[useRichPaste] === End Debug ===');
246
+ }, []);
247
+
248
+ return {
249
+ canHandleRichPaste,
250
+ getContentType,
251
+ pasteRichContent,
252
+ debugClipboard,
253
+ detectContentType,
254
+ };
255
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * キャンバス(iframe)側のホイール/タッチジェスチャー処理フック
3
+ * - Ctrl/Cmd + ホイール → 親の useCanvasControls にズームを依頼
4
+ * - Shift + ホイール → 横スクロール(パン)
5
+ * - ピンチズーム(2本指)→ 親の useCanvasControls にズームを依頼
6
+ * - Safari の gesture イベントの抑止
7
+ *
8
+ * [移植時の修正]
9
+ * 以前はここで iframe ビューポート座標を親ウィンドウ座標に変換してから
10
+ * postMessage し、受け手の useCanvasControls が再び iframe 座標へ戻していた。
11
+ * 往復で2回とも iframeRef の実測に依存するうえ、どちらの座標系の値なのかが
12
+ * 名前から読み取れず、ズーム中心のずれの温床になっていた。
13
+ * 現在は **iframe ビューポート座標のまま** 送る(受け手もその前提)。
14
+ */
15
+
16
+ import { useCallback, useRef } from 'react';
17
+ import { useEditorContext } from '../EditorContext';
18
+
19
+ /**
20
+ * 2点間の距離を計算
21
+ */
22
+ function getTouchDistance(touches: TouchList): number {
23
+ if (touches.length < 2) return 0;
24
+ const dx = touches[0].clientX - touches[1].clientX;
25
+ const dy = touches[0].clientY - touches[1].clientY;
26
+ return Math.sqrt(dx * dx + dy * dy);
27
+ }
28
+
29
+ /**
30
+ * 2点の中心座標を計算
31
+ */
32
+ function getTouchCenter(touches: TouchList): { x: number; y: number } {
33
+ if (touches.length < 2) {
34
+ return { x: touches[0].clientX, y: touches[0].clientY };
35
+ }
36
+ return {
37
+ x: (touches[0].clientX + touches[1].clientX) / 2,
38
+ y: (touches[0].clientY + touches[1].clientY) / 2,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * ホイールの delta を CSS ピクセル相当に正規化する。
44
+ * deltaMode は 0=px / 1=行 / 2=ページ。デバイスによって単位が違うので揃える。
45
+ */
46
+ function normalizeDelta(value: number, deltaMode: number): number {
47
+ if (deltaMode === 1) return value * 16;
48
+ if (deltaMode === 2) return value * 100;
49
+ return value;
50
+ }
51
+
52
+ interface UseTouchGesturesReturn {
53
+ /**
54
+ * iframe ドキュメントにホイール/タッチジェスチャーリスナーをセットアップ
55
+ * @returns クリーンアップ関数
56
+ */
57
+ setupTouchGestureListeners: (iframeDoc: Document) => () => void;
58
+ }
59
+
60
+ /**
61
+ * ホイール/タッチジェスチャー処理フック
62
+ *
63
+ * iframe内のイベントを処理し、ズームは親ウィンドウの useCanvasControls に
64
+ * postMessage で転送する(ズーム倍率の一元管理は親側の state が持つため)。
65
+ */
66
+ export function useTouchGestures(): UseTouchGesturesReturn {
67
+ useEditorContext();
68
+
69
+ // ピンチズーム用の状態(クロージャ問題を避けるためrefを使用)
70
+ const lastTouchDistanceRef = useRef<number | null>(null);
71
+
72
+ const setupTouchGestureListeners = useCallback((iframeDoc: Document) => {
73
+ // タッチアクションを無効化(ブラウザのデフォルトズームを防止)
74
+ iframeDoc.documentElement.style.touchAction = 'none';
75
+ iframeDoc.body.style.touchAction = 'none';
76
+
77
+ /**
78
+ * ホイール処理
79
+ * - Ctrl/Cmd + ホイール: ズーム(親に転送)
80
+ * - Shift + ホイール: 横スクロール
81
+ * - それ以外: #canvas-container のネイティブスクロールに任せる
82
+ */
83
+ const handleWheel = (e: WheelEvent) => {
84
+ if (e.ctrlKey || e.metaKey) {
85
+ e.preventDefault();
86
+ e.stopPropagation();
87
+ e.stopImmediatePropagation();
88
+
89
+ // 座標は iframe ビューポート基準のまま送る
90
+ window.postMessage(
91
+ {
92
+ type: 'IFRAME_WHEEL_EVENT',
93
+ deltaY: normalizeDelta(e.deltaY, e.deltaMode),
94
+ clientX: e.clientX,
95
+ clientY: e.clientY,
96
+ ctrlKey: e.ctrlKey,
97
+ metaKey: e.metaKey,
98
+ },
99
+ '*'
100
+ );
101
+ return;
102
+ }
103
+
104
+ // Shift + ホイールで横スクロール。
105
+ // 合成イベントや一部デバイスではブラウザが縦→横の読み替えをしてくれないので、
106
+ // deltaX が来ていないときだけ自前で横に流す(「無反応」を作らないため)。
107
+ if (e.shiftKey && e.deltaX === 0 && e.deltaY !== 0) {
108
+ const container = iframeDoc.getElementById('canvas-container');
109
+ if (container) {
110
+ e.preventDefault();
111
+ container.scrollLeft += normalizeDelta(e.deltaY, e.deltaMode);
112
+ }
113
+ }
114
+ };
115
+
116
+ /**
117
+ * Safari gestureイベントを防止
118
+ */
119
+ const handleGesture = (e: Event) => {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ };
123
+
124
+ /**
125
+ * 2本指タッチ開始時の処理
126
+ */
127
+ const handleTouchStart = (e: TouchEvent) => {
128
+ if (e.touches.length >= 2) {
129
+ e.preventDefault();
130
+ lastTouchDistanceRef.current = getTouchDistance(e.touches);
131
+ }
132
+ };
133
+
134
+ /**
135
+ * 2本指タッチ移動時の処理(ピンチズーム)
136
+ */
137
+ const handleTouchMove = (e: TouchEvent) => {
138
+ if (e.touches.length >= 2) {
139
+ e.preventDefault();
140
+ if (lastTouchDistanceRef.current !== null) {
141
+ const currentDistance = getTouchDistance(e.touches);
142
+ const center = getTouchCenter(e.touches);
143
+
144
+ // 座標は iframe ビューポート基準のまま送る
145
+ window.postMessage(
146
+ {
147
+ type: 'IFRAME_PINCH_EVENT',
148
+ previousDistance: lastTouchDistanceRef.current,
149
+ currentDistance,
150
+ centerX: center.x,
151
+ centerY: center.y,
152
+ },
153
+ '*'
154
+ );
155
+
156
+ lastTouchDistanceRef.current = currentDistance;
157
+ }
158
+ }
159
+ };
160
+
161
+ /**
162
+ * タッチ終了時の処理
163
+ */
164
+ const handleTouchEnd = (e: TouchEvent) => {
165
+ if (e.touches.length < 2) {
166
+ lastTouchDistanceRef.current = null;
167
+ }
168
+ };
169
+
170
+ // イベントリスナーを登録
171
+ const options = { passive: false, capture: true };
172
+ iframeDoc.addEventListener('wheel', handleWheel, options);
173
+ iframeDoc.addEventListener('gesturestart', handleGesture, options);
174
+ iframeDoc.addEventListener('gesturechange', handleGesture, options);
175
+ iframeDoc.addEventListener('gestureend', handleGesture, options);
176
+ iframeDoc.addEventListener('touchstart', handleTouchStart, options);
177
+ iframeDoc.addEventListener('touchmove', handleTouchMove, options);
178
+ iframeDoc.addEventListener('touchend', handleTouchEnd, options);
179
+ iframeDoc.addEventListener('touchcancel', handleTouchEnd, options);
180
+
181
+ // クリーンアップ関数を返す
182
+ return () => {
183
+ iframeDoc.removeEventListener('wheel', handleWheel, options);
184
+ iframeDoc.removeEventListener('gesturestart', handleGesture, options);
185
+ iframeDoc.removeEventListener('gesturechange', handleGesture, options);
186
+ iframeDoc.removeEventListener('gestureend', handleGesture, options);
187
+ iframeDoc.removeEventListener('touchstart', handleTouchStart, options);
188
+ iframeDoc.removeEventListener('touchmove', handleTouchMove, options);
189
+ iframeDoc.removeEventListener('touchend', handleTouchEnd, options);
190
+ iframeDoc.removeEventListener('touchcancel', handleTouchEnd, options);
191
+ };
192
+ }, []);
193
+
194
+ return {
195
+ setupTouchGestureListeners,
196
+ };
197
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * FrontendVisualEditor - 汎用フロントエンドビジュアルエディタ
3
+ * スライド、ページ、コンポーネントなど様々なコンテンツの編集に使用可能
4
+ */
5
+
6
+ // メインエディタコンポーネント
7
+ export { FrontendVisualEditor, FrontendVisualEditor as SlideVisualEditor } from './FrontendVisualEditor';
8
+ export type { FrontendVisualEditorProps } from './FrontendVisualEditor';
9
+
10
+ // コンテキスト
11
+ export { EditorProvider, useEditorContext } from './EditorContext';
12
+ export type { ContentListItem, ContentListItem as SlideListItem } from './EditorContext';
13
+
14
+ // コンポーネント
15
+ export * from './components';
16
+
17
+ // フック
18
+ export * from './hooks';
19
+
20
+ // ユーティリティ
21
+ export * from './utils';
22
+
23
+ // 型定義
24
+ export * from './types';
25
+
26
+ // 定数
27
+ export * from './constants';