@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,397 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI要素置換Hook
|
|
5
|
+
* 選択した要素をAI生成したHTMLで置換する
|
|
6
|
+
* editorModeに基づいて slide-agent または website-agent API を使用
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback } from 'react';
|
|
10
|
+
import { useEditorContext } from '../EditorContext';
|
|
11
|
+
import { useAuth } from '../../components/auth/AuthProvider';
|
|
12
|
+
import { createAuthApi } from '../../lib/api/auth-fetch';
|
|
13
|
+
import { convertSingleElementToAbsolute, generateElementId } from '../utils/dom-utils';
|
|
14
|
+
import type { SlideAgentRequest, SlideAgentResponse, AttachedFile as SlideAttachedFile } from '../../lib/agent/slide-agent/types';
|
|
15
|
+
import type { WebsiteAgentRequest, WebsiteAgentResponse, AttachedFile as WebsiteAttachedFile } from '../../lib/agent/website-agent/types';
|
|
16
|
+
|
|
17
|
+
// AttachedFile型を共通化(両方同じ構造)
|
|
18
|
+
export type AttachedFile = SlideAttachedFile;
|
|
19
|
+
|
|
20
|
+
interface UseAiReplaceProps {
|
|
21
|
+
/** スライドモード用: プレゼンテーションID */
|
|
22
|
+
presentationId?: string;
|
|
23
|
+
/** Webページモード用: WebサイトID */
|
|
24
|
+
websiteId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* AI要素置換フック
|
|
29
|
+
* editorModeに基づいてslide-agentまたはwebsite-agentを使い分ける
|
|
30
|
+
*/
|
|
31
|
+
export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps = {}) {
|
|
32
|
+
const { getIdToken, user } = useAuth();
|
|
33
|
+
const {
|
|
34
|
+
selectedElement,
|
|
35
|
+
getIframeDoc,
|
|
36
|
+
notifyIframeChange,
|
|
37
|
+
setSelectedElement,
|
|
38
|
+
layoutMode,
|
|
39
|
+
editorMode,
|
|
40
|
+
} = useEditorContext();
|
|
41
|
+
|
|
42
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 選択要素のHTMLを取得
|
|
47
|
+
*/
|
|
48
|
+
const getSelectedElementHtml = useCallback((): string | null => {
|
|
49
|
+
if (!selectedElement) return null;
|
|
50
|
+
|
|
51
|
+
const iframeDoc = getIframeDoc();
|
|
52
|
+
if (!iframeDoc) return null;
|
|
53
|
+
|
|
54
|
+
const element = iframeDoc.querySelector(
|
|
55
|
+
`[data-element-id="${selectedElement.id}"]`
|
|
56
|
+
) as HTMLElement | null;
|
|
57
|
+
|
|
58
|
+
if (!element) return null;
|
|
59
|
+
|
|
60
|
+
// 選択ボックス等のUI要素を除いたHTMLを取得
|
|
61
|
+
const clone = element.cloneNode(true) as HTMLElement;
|
|
62
|
+
clone.classList.remove('selected');
|
|
63
|
+
clone.querySelectorAll('.selection-box, .resize-handle').forEach(el => el.remove());
|
|
64
|
+
|
|
65
|
+
return clone.outerHTML;
|
|
66
|
+
}, [selectedElement, getIframeDoc]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 現在のスライドHTML全体を取得
|
|
70
|
+
* キャンバス構造(#canvas-container等)ではなく、#artboard内のコンテンツのみを返す
|
|
71
|
+
*/
|
|
72
|
+
const getContentHtml = useCallback((): string => {
|
|
73
|
+
const iframeDoc = getIframeDoc();
|
|
74
|
+
if (!iframeDoc) return '';
|
|
75
|
+
|
|
76
|
+
// #artboard内のHTMLをクリーンに取得(キャンバス構造は含めない)
|
|
77
|
+
const artboard = iframeDoc.getElementById('artboard');
|
|
78
|
+
const sourceElement = artboard || iframeDoc.body;
|
|
79
|
+
const clone = sourceElement.cloneNode(true) as HTMLElement;
|
|
80
|
+
|
|
81
|
+
// UI要素を除去
|
|
82
|
+
clone.querySelectorAll('.selection-box, .resize-handle, [data-editor-ui]').forEach(el => el.remove());
|
|
83
|
+
|
|
84
|
+
return clone.innerHTML;
|
|
85
|
+
}, [getIframeDoc]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 要素を新しいHTMLで置換
|
|
89
|
+
* 新しい要素には data-editable, data-element-id を付与し、
|
|
90
|
+
* 絶対位置に変換する
|
|
91
|
+
*/
|
|
92
|
+
const replaceElement = useCallback((newHtml: string) => {
|
|
93
|
+
if (!selectedElement) return false;
|
|
94
|
+
|
|
95
|
+
const iframeDoc = getIframeDoc();
|
|
96
|
+
if (!iframeDoc) return false;
|
|
97
|
+
|
|
98
|
+
const element = iframeDoc.querySelector(
|
|
99
|
+
`[data-element-id="${selectedElement.id}"]`
|
|
100
|
+
) as HTMLElement | null;
|
|
101
|
+
|
|
102
|
+
if (!element) return false;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// 元の要素の位置とサイズを保持
|
|
106
|
+
const originalStyle = {
|
|
107
|
+
position: element.style.position || 'absolute',
|
|
108
|
+
left: element.style.left,
|
|
109
|
+
top: element.style.top,
|
|
110
|
+
width: element.style.width,
|
|
111
|
+
height: element.style.height,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// 新しいHTML要素を作成
|
|
115
|
+
const temp = iframeDoc.createElement('div');
|
|
116
|
+
temp.innerHTML = newHtml.trim();
|
|
117
|
+
const newElement = temp.firstElementChild as HTMLElement;
|
|
118
|
+
|
|
119
|
+
if (!newElement) {
|
|
120
|
+
console.error('[useAiReplace] Failed to create new element from HTML');
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ルート要素にdata属性を設定
|
|
125
|
+
newElement.setAttribute('data-editable', 'true');
|
|
126
|
+
newElement.setAttribute('data-element-id', selectedElement.id);
|
|
127
|
+
// 出所の刻印(data-gg-src)を引き継ぐ。AIの出力には残らないため、
|
|
128
|
+
// 移植しないとこの要素だけ原本TSXへの書き戻しが効かなくなる
|
|
129
|
+
const ggSrc = element.getAttribute('data-gg-src');
|
|
130
|
+
if (ggSrc && !newElement.getAttribute('data-gg-src')) {
|
|
131
|
+
newElement.setAttribute('data-gg-src', ggSrc);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 子要素にもdata-editable, data-element-idを付与(ブロック要素のみ)
|
|
135
|
+
const inlineTags = new Set([
|
|
136
|
+
'SPAN', 'A', 'STRONG', 'EM', 'B', 'I', 'U', 'S', 'MARK', 'CODE',
|
|
137
|
+
'SMALL', 'SUB', 'SUP', 'ABBR', 'BR', 'WBR', 'LABEL'
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const processChildren = (parent: HTMLElement) => {
|
|
141
|
+
Array.from(parent.children).forEach((child) => {
|
|
142
|
+
const htmlChild = child as HTMLElement;
|
|
143
|
+
|
|
144
|
+
// インライン要素とSCRIPT/STYLEはスキップ
|
|
145
|
+
if (inlineTags.has(htmlChild.tagName) ||
|
|
146
|
+
htmlChild.tagName === 'SCRIPT' ||
|
|
147
|
+
htmlChild.tagName === 'STYLE') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// まだdata-element-idがない場合は付与
|
|
152
|
+
if (!htmlChild.getAttribute('data-element-id')) {
|
|
153
|
+
htmlChild.setAttribute('data-editable', 'true');
|
|
154
|
+
htmlChild.setAttribute('data-element-id', generateElementId());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 再帰的に子要素を処理
|
|
158
|
+
processChildren(htmlChild);
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
processChildren(newElement);
|
|
163
|
+
|
|
164
|
+
// 元の位置・サイズを適用(置換前に設定)
|
|
165
|
+
newElement.style.position = originalStyle.position;
|
|
166
|
+
if (originalStyle.left) newElement.style.left = originalStyle.left;
|
|
167
|
+
if (originalStyle.top) newElement.style.top = originalStyle.top;
|
|
168
|
+
if (originalStyle.width) newElement.style.width = originalStyle.width;
|
|
169
|
+
if (originalStyle.height) newElement.style.height = originalStyle.height;
|
|
170
|
+
|
|
171
|
+
// 要素を置換
|
|
172
|
+
element.parentNode?.replaceChild(newElement, element);
|
|
173
|
+
|
|
174
|
+
// フォントの読み込みを待ってから絶対位置に変換
|
|
175
|
+
// (フォントによってサイズが変わる可能性があるため)
|
|
176
|
+
const convertChildrenToAbsolute = async () => {
|
|
177
|
+
// オートレイアウトモードの場合は変換をスキップ
|
|
178
|
+
if (layoutMode === 'auto') {
|
|
179
|
+
console.log('[useAiReplace] Auto layout mode - skipping absolute positioning conversion for children');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// フォントの読み込みを待つ
|
|
185
|
+
if (iframeDoc.fonts?.ready) {
|
|
186
|
+
await iframeDoc.fonts.ready;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 少し待ってからレイアウトを確定
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
191
|
+
|
|
192
|
+
// 子要素を絶対配置に変換
|
|
193
|
+
const editableChildren = newElement.querySelectorAll('[data-editable="true"]');
|
|
194
|
+
editableChildren.forEach((child) => {
|
|
195
|
+
const htmlChild = child as HTMLElement;
|
|
196
|
+
convertSingleElementToAbsolute(iframeDoc, htmlChild);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
console.log('[useAiReplace] Converted', editableChildren.length, 'child elements to absolute positioning');
|
|
200
|
+
|
|
201
|
+
// 変更を再度通知
|
|
202
|
+
notifyIframeChange();
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error('[useAiReplace] Error converting children to absolute:', err);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// 非同期で変換を実行
|
|
209
|
+
convertChildrenToAbsolute();
|
|
210
|
+
|
|
211
|
+
// 選択ボックスを更新
|
|
212
|
+
const selectionBox = iframeDoc.querySelector(
|
|
213
|
+
`.selection-box[data-for-element="${selectedElement.id}"]`
|
|
214
|
+
);
|
|
215
|
+
if (selectionBox) {
|
|
216
|
+
selectionBox.remove();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 変更を通知
|
|
220
|
+
notifyIframeChange();
|
|
221
|
+
|
|
222
|
+
// 選択状態をクリア(新しい要素は選択し直す必要がある)
|
|
223
|
+
setSelectedElement(null);
|
|
224
|
+
|
|
225
|
+
return true;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('[useAiReplace] Replace element error:', err);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}, [selectedElement, getIframeDoc, notifyIframeChange, setSelectedElement]);
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* AIで要素を生成して置換
|
|
234
|
+
* editorModeに基づいてslide-agentまたはwebsite-agent APIを使用
|
|
235
|
+
* @param prompt ユーザーの指示
|
|
236
|
+
* @param attachedFiles 添付ファイル(画像、PDF等)
|
|
237
|
+
*/
|
|
238
|
+
const generateAndReplace = useCallback(async (
|
|
239
|
+
prompt: string,
|
|
240
|
+
attachedFiles?: AttachedFile[],
|
|
241
|
+
engine?: 'codex' | 'claude',
|
|
242
|
+
): Promise<void> => {
|
|
243
|
+
if (!selectedElement) {
|
|
244
|
+
throw new Error('要素が選択されていません');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!user) {
|
|
248
|
+
throw new Error('ログインが必要です');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const selectedElementHtml = getSelectedElementHtml();
|
|
252
|
+
if (!selectedElementHtml) {
|
|
253
|
+
throw new Error('選択要素のHTMLを取得できません');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
setIsGenerating(true);
|
|
257
|
+
setError(null);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const api = await createAuthApi(getIdToken);
|
|
261
|
+
const contentHtml = getContentHtml();
|
|
262
|
+
|
|
263
|
+
// editorModeに基づいてAPI呼び出しを分岐
|
|
264
|
+
const isWebpageMode = editorMode === 'webpage';
|
|
265
|
+
|
|
266
|
+
console.log('[useAiReplace] API呼び出し:', {
|
|
267
|
+
editorMode,
|
|
268
|
+
apiEndpoint: isWebpageMode ? '/api/website-agent' : '/api/slide-agent',
|
|
269
|
+
promptLength: prompt.length,
|
|
270
|
+
attachedFilesCount: attachedFiles?.length ?? 0,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let generatedHtml: string | undefined;
|
|
274
|
+
|
|
275
|
+
if (isWebpageMode) {
|
|
276
|
+
// Webページモード: website-agent API を使用
|
|
277
|
+
const requestBody: WebsiteAgentRequest = {
|
|
278
|
+
prompt,
|
|
279
|
+
userId: user.id,
|
|
280
|
+
websiteId, // WebサイトIDを渡す(画像保存用)
|
|
281
|
+
options: {
|
|
282
|
+
mode: 'element_replace',
|
|
283
|
+
elementReplace: {
|
|
284
|
+
pageHtml: contentHtml,
|
|
285
|
+
selectedElementHtml,
|
|
286
|
+
selectedElementInfo: {
|
|
287
|
+
tagName: selectedElement.tagName,
|
|
288
|
+
width: selectedElement.width,
|
|
289
|
+
height: selectedElement.height,
|
|
290
|
+
position: {
|
|
291
|
+
x: selectedElement.x,
|
|
292
|
+
y: selectedElement.y,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
attachedFiles: attachedFiles as WebsiteAttachedFile[],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const response: WebsiteAgentResponse = await api.post(
|
|
301
|
+
'/api/website-agent',
|
|
302
|
+
requestBody
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (!response.success) {
|
|
306
|
+
throw new Error(response.error || '生成に失敗しました');
|
|
307
|
+
}
|
|
308
|
+
generatedHtml = response.generatedHtml;
|
|
309
|
+
} else {
|
|
310
|
+
// スライドモード: slide-agent API を使用
|
|
311
|
+
const requestBody: SlideAgentRequest & { engine?: string } = {
|
|
312
|
+
prompt,
|
|
313
|
+
engine,
|
|
314
|
+
userId: user.id,
|
|
315
|
+
presentationId, // プレゼンテーションIDを渡す(画像保存用)
|
|
316
|
+
options: {
|
|
317
|
+
mode: 'element_replace',
|
|
318
|
+
elementReplace: {
|
|
319
|
+
slideHtml: contentHtml,
|
|
320
|
+
selectedElementHtml,
|
|
321
|
+
selectedElementInfo: {
|
|
322
|
+
tagName: selectedElement.tagName,
|
|
323
|
+
width: selectedElement.width,
|
|
324
|
+
height: selectedElement.height,
|
|
325
|
+
position: {
|
|
326
|
+
x: selectedElement.x,
|
|
327
|
+
y: selectedElement.y,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
attachedFiles: attachedFiles as SlideAttachedFile[],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const response: SlideAgentResponse = await api.post(
|
|
336
|
+
'/api/slide-agent',
|
|
337
|
+
requestBody
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (!response.success) {
|
|
341
|
+
throw new Error(response.error || '生成に失敗しました');
|
|
342
|
+
}
|
|
343
|
+
generatedHtml = response.generatedHtml;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!generatedHtml) {
|
|
347
|
+
throw new Error('HTMLが生成されませんでした');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 要素を置換
|
|
351
|
+
const success = replaceElement(generatedHtml);
|
|
352
|
+
if (!success) {
|
|
353
|
+
throw new Error('要素の置換に失敗しました');
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const message = err instanceof Error ? err.message : '生成に失敗しました';
|
|
357
|
+
setError(message);
|
|
358
|
+
throw err;
|
|
359
|
+
} finally {
|
|
360
|
+
setIsGenerating(false);
|
|
361
|
+
}
|
|
362
|
+
}, [
|
|
363
|
+
selectedElement,
|
|
364
|
+
user,
|
|
365
|
+
getSelectedElementHtml,
|
|
366
|
+
getContentHtml,
|
|
367
|
+
replaceElement,
|
|
368
|
+
getIdToken,
|
|
369
|
+
editorMode,
|
|
370
|
+
presentationId,
|
|
371
|
+
websiteId,
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 選択要素の簡易情報を取得
|
|
376
|
+
*/
|
|
377
|
+
const getSelectedElementInfo = useCallback((): string | undefined => {
|
|
378
|
+
if (!selectedElement) return undefined;
|
|
379
|
+
|
|
380
|
+
const { tagName, text, width, height } = selectedElement;
|
|
381
|
+
const safeText = text || '';
|
|
382
|
+
const truncatedText = safeText.length > 20 ? safeText.substring(0, 20) + '...' : safeText;
|
|
383
|
+
|
|
384
|
+
return `<${tagName.toLowerCase()}> ${truncatedText} (${Math.round(width || 0)}×${Math.round(height || 0)})`.trim();
|
|
385
|
+
}, [selectedElement]);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
generateAndReplace,
|
|
389
|
+
replaceElement,
|
|
390
|
+
isGenerating,
|
|
391
|
+
error,
|
|
392
|
+
hasSelection: !!selectedElement,
|
|
393
|
+
getSelectedElementInfo,
|
|
394
|
+
/** 現在のエディタモード(どのエージェントを使用するかを示す) */
|
|
395
|
+
editorMode,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Alt(Option)ホバーの距離メジャー(Figmaと同じ操作)。
|
|
5
|
+
*
|
|
6
|
+
* 要素を選択した状態でAltを押しながらカーソルを動かすと、
|
|
7
|
+
* カーソル下の要素との距離を赤い線で表示する(measure-distance.ts)。
|
|
8
|
+
* - 選択要素自身(またはその内側)の上では親要素と測る(Figmaの余白表示に相当)
|
|
9
|
+
* - Altを離す・カーソルが外へ出る・選択が消えると消える
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect } from 'react';
|
|
13
|
+
import { useEditorContext } from '../EditorContext';
|
|
14
|
+
import { drawMeasure, clearMeasure } from '../utils/measure-distance';
|
|
15
|
+
|
|
16
|
+
export function useAltMeasure() {
|
|
17
|
+
const { getIframeDoc, iframeReady } = useEditorContext();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const doc = getIframeDoc();
|
|
21
|
+
if (!doc) return;
|
|
22
|
+
|
|
23
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
24
|
+
if (!e.altKey) {
|
|
25
|
+
clearMeasure(doc);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// 選択はReact状態でなくDOMを見る(伝播ラグと依存の張り直しを避ける)
|
|
29
|
+
const sel = doc.querySelector<HTMLElement>('.selected');
|
|
30
|
+
if (!sel || doc.body.classList.contains('gg-cropping')) {
|
|
31
|
+
clearMeasure(doc);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let target =
|
|
35
|
+
(e.target as HTMLElement)?.closest?.<HTMLElement>('[data-element-id], [data-editable="true"]') ?? null;
|
|
36
|
+
// 選択要素の上(または内側)では親と測る = 余白の確認
|
|
37
|
+
if (!target || target === sel || sel.contains(target)) {
|
|
38
|
+
target =
|
|
39
|
+
(sel.parentElement?.closest<HTMLElement>('[data-element-id], [data-editable="true"]') as HTMLElement) ??
|
|
40
|
+
doc.getElementById('artboard');
|
|
41
|
+
}
|
|
42
|
+
if (!target || target === sel) {
|
|
43
|
+
clearMeasure(doc);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
drawMeasure(doc, sel, target);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === 'Alt') clearMeasure(doc);
|
|
51
|
+
};
|
|
52
|
+
const onLeaveOrBlur = () => clearMeasure(doc);
|
|
53
|
+
|
|
54
|
+
doc.addEventListener('mousemove', onMouseMove);
|
|
55
|
+
doc.addEventListener('keyup', onKeyUp);
|
|
56
|
+
doc.addEventListener('mouseleave', onLeaveOrBlur);
|
|
57
|
+
doc.defaultView?.addEventListener('blur', onLeaveOrBlur);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
clearMeasure(doc);
|
|
61
|
+
doc.removeEventListener('mousemove', onMouseMove);
|
|
62
|
+
doc.removeEventListener('keyup', onKeyUp);
|
|
63
|
+
doc.removeEventListener('mouseleave', onLeaveOrBlur);
|
|
64
|
+
doc.defaultView?.removeEventListener('blur', onLeaveOrBlur);
|
|
65
|
+
};
|
|
66
|
+
}, [getIframeDoc, iframeReady]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ブラウザのネイティブズーム(ピンチ、Ctrl+ホイール、キーボードショートカット等)を防止するフック
|
|
5
|
+
* エディタ画面全体で独自のズーム制御を行うために使用
|
|
6
|
+
*/
|
|
7
|
+
export function useBrowserZoomPrevention(): void {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
// body と html にスタイルを適用してブラウザのピンチズームを完全にブロック
|
|
10
|
+
const originalBodyOverflow = document.body.style.overflow;
|
|
11
|
+
const originalBodyTouchAction = document.body.style.touchAction;
|
|
12
|
+
const originalHtmlTouchAction = document.documentElement.style.touchAction;
|
|
13
|
+
const originalHtmlOverflow = document.documentElement.style.overflow;
|
|
14
|
+
|
|
15
|
+
document.body.style.overflow = 'hidden';
|
|
16
|
+
document.body.style.touchAction = 'none';
|
|
17
|
+
document.documentElement.style.touchAction = 'none';
|
|
18
|
+
document.documentElement.style.overflow = 'hidden';
|
|
19
|
+
|
|
20
|
+
// Add custom attribute to trigger CSS protections from globals.css
|
|
21
|
+
document.body.setAttribute('data-editor-active', 'true');
|
|
22
|
+
|
|
23
|
+
// Chrome macOSのピンチズーム(wheelイベント + ctrlKey)を防止
|
|
24
|
+
// 注: stopPropagation/stopImmediatePropagationは呼ばない
|
|
25
|
+
// → キャンバスのズームハンドラにイベントが到達する必要がある
|
|
26
|
+
const preventBrowserZoom = (e: WheelEvent) => {
|
|
27
|
+
// ctrlKey または metaKey が押されている場合(ピンチジェスチャー含む)
|
|
28
|
+
if (e.ctrlKey || e.metaKey) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Safari/WebKitのジェスチャーイベントを防止
|
|
34
|
+
const preventGesture = (e: Event) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
e.stopImmediatePropagation();
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// タッチイベントでの2本指ピンチを防止
|
|
42
|
+
const preventTouchZoom = (e: TouchEvent) => {
|
|
43
|
+
if (e.touches.length > 1) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// キーボードショートカットでのブラウザデフォルト動作を防止
|
|
50
|
+
// - Ctrl/Cmd + +/-/0: ズーム
|
|
51
|
+
// - Ctrl/Cmd + Alt + C: Chrome DevTools (開発者ツールのJavaScriptコンソール)
|
|
52
|
+
// - Ctrl/Cmd + Alt + V: 一部ブラウザのショートカット
|
|
53
|
+
// 注意: stopImmediatePropagation は使わない(自前のハンドラーも止まってしまうため)
|
|
54
|
+
const preventBrowserShortcuts = (e: KeyboardEvent) => {
|
|
55
|
+
const isCmdOrCtrl = e.ctrlKey || e.metaKey;
|
|
56
|
+
const key = e.key.toLowerCase();
|
|
57
|
+
|
|
58
|
+
// ズーム防止
|
|
59
|
+
if (isCmdOrCtrl && (key === '+' || key === '-' || key === '=' || key === '0')) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// スタイルコピー/ペースト用のショートカット(DevTools起動を防止)
|
|
65
|
+
// Cmd/Ctrl + Alt + C または Cmd/Ctrl + Alt + V
|
|
66
|
+
// 注意: macOSではOption+CやOption+Vが特殊文字(ç, √)を生成するため、
|
|
67
|
+
// e.key ではなく e.code を使用して物理キーを判定する
|
|
68
|
+
// preventDefault のみでブラウザのデフォルト動作を止め、自前のハンドラーは動くようにする
|
|
69
|
+
if (isCmdOrCtrl && e.altKey && (e.code === 'KeyC' || e.code === 'KeyV')) {
|
|
70
|
+
console.log('[preventBrowserShortcuts] Preventing browser default for Cmd/Ctrl+Alt+C/V', {
|
|
71
|
+
key: e.key,
|
|
72
|
+
code: e.code,
|
|
73
|
+
});
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// document と window 両方でキャプチャフェーズで登録(最優先)
|
|
80
|
+
document.addEventListener('wheel', preventBrowserZoom, { passive: false, capture: true });
|
|
81
|
+
window.addEventListener('wheel', preventBrowserZoom, { passive: false, capture: true });
|
|
82
|
+
|
|
83
|
+
// ジェスチャーイベント(Safari)
|
|
84
|
+
document.addEventListener('gesturestart', preventGesture, { passive: false, capture: true });
|
|
85
|
+
document.addEventListener('gesturechange', preventGesture, { passive: false, capture: true });
|
|
86
|
+
document.addEventListener('gestureend', preventGesture, { passive: false, capture: true });
|
|
87
|
+
window.addEventListener('gesturestart', preventGesture, { passive: false, capture: true });
|
|
88
|
+
window.addEventListener('gesturechange', preventGesture, { passive: false, capture: true });
|
|
89
|
+
window.addEventListener('gestureend', preventGesture, { passive: false, capture: true });
|
|
90
|
+
|
|
91
|
+
// タッチイベント
|
|
92
|
+
document.addEventListener('touchstart', preventTouchZoom, { passive: false, capture: true });
|
|
93
|
+
document.addEventListener('touchmove', preventTouchZoom, { passive: false, capture: true });
|
|
94
|
+
|
|
95
|
+
// キーボードショートカット防止(ズーム、DevTools等)
|
|
96
|
+
document.addEventListener('keydown', preventBrowserShortcuts, { passive: false, capture: true });
|
|
97
|
+
window.addEventListener('keydown', preventBrowserShortcuts, { passive: false, capture: true });
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
// スタイルを復元
|
|
101
|
+
document.body.style.overflow = originalBodyOverflow;
|
|
102
|
+
document.body.style.touchAction = originalBodyTouchAction;
|
|
103
|
+
document.documentElement.style.touchAction = originalHtmlTouchAction;
|
|
104
|
+
document.documentElement.style.overflow = originalHtmlOverflow;
|
|
105
|
+
document.body.removeAttribute('data-editor-active');
|
|
106
|
+
|
|
107
|
+
document.removeEventListener('wheel', preventBrowserZoom, { capture: true });
|
|
108
|
+
window.removeEventListener('wheel', preventBrowserZoom, { capture: true });
|
|
109
|
+
document.removeEventListener('gesturestart', preventGesture, { capture: true });
|
|
110
|
+
document.removeEventListener('gesturechange', preventGesture, { capture: true });
|
|
111
|
+
document.removeEventListener('gestureend', preventGesture, { capture: true });
|
|
112
|
+
window.removeEventListener('gesturestart', preventGesture, { capture: true });
|
|
113
|
+
window.removeEventListener('gesturechange', preventGesture, { capture: true });
|
|
114
|
+
window.removeEventListener('gestureend', preventGesture, { capture: true });
|
|
115
|
+
document.removeEventListener('touchstart', preventTouchZoom, { capture: true });
|
|
116
|
+
document.removeEventListener('touchmove', preventTouchZoom, { capture: true });
|
|
117
|
+
document.removeEventListener('keydown', preventBrowserShortcuts, { capture: true });
|
|
118
|
+
window.removeEventListener('keydown', preventBrowserShortcuts, { capture: true });
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
}
|