@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.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- 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';
|