@growgroup/visual-editor 0.1.5 → 0.2.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/README.md +59 -2
- package/dist/editor.css +1 -1
- package/package.json +7 -3
- package/src/components/ui/alert-dialog.tsx +18 -4
- package/src/components/ui/context-menu.tsx +18 -4
- package/src/components/ui/dialog.tsx +19 -5
- package/src/components/ui/dropdown-menu.tsx +18 -4
- package/src/components/ui/popover.tsx +10 -2
- package/src/components/ui/select.tsx +10 -2
- package/src/components/ui/tooltip.tsx +10 -2
- package/src/editor/EditorToolbar.tsx +10 -20
- package/src/editor/FrontendVisualEditor.tsx +189 -43
- package/src/editor/components/EditorCanvas.tsx +8 -3
- package/src/editor/components/EditorContextMenu.tsx +53 -71
- package/src/editor/components/EditorFooter.tsx +36 -70
- package/src/editor/components/EditorHeader.tsx +37 -20
- package/src/editor/components/EditorLayerPanel.tsx +7 -9
- package/src/editor/components/EditorPropertyPanel.tsx +84 -8
- package/src/editor/components/LayerTreeItem.tsx +28 -5
- package/src/editor/components/component-panel/ComponentPanel.tsx +4 -3
- package/src/editor/components/ppt/PptChrome.tsx +156 -344
- package/src/editor/components/ppt/PptComments.tsx +135 -87
- package/src/editor/components/ppt/PptDesignProposals.tsx +21 -26
- package/src/editor/components/ppt/PptNotes.tsx +15 -15
- package/src/editor/components/shell/CanvasAppearance.tsx +19 -0
- package/src/editor/components/shell/ConfirmDialog.tsx +15 -0
- package/src/editor/components/shell/EditorTopBar.tsx +29 -68
- package/src/editor/components/shell/LeftPanel.tsx +20 -94
- package/src/editor/components/shell/PagesPanel.tsx +4 -2
- package/src/editor/contexts/EditorAppearanceContext.tsx +44 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +278 -16
- package/src/editor/editor-skin.css +2 -122
- package/src/editor/hooks/useCanvasControls.ts +3 -2
- package/src/editor/hooks/useComponentEditMode.ts +95 -1
- package/src/editor/hooks/useDragResize.ts +86 -6
- package/src/editor/hooks/useDrawingMode.ts +4 -2
- package/src/editor/hooks/useElementActions.ts +60 -1
- package/src/editor/hooks/useElementSelection.ts +35 -8
- package/src/editor/hooks/useIframeSetup.ts +10 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +12 -1
- package/src/editor/hooks/useMediaLibrary.ts +4 -5
- package/src/editor/hooks/useResizablePanel.ts +40 -11
- package/src/editor/parts.ts +321 -0
- package/src/editor/utils/dom-utils.ts +135 -15
- package/src/editor/utils/flex-reorder.ts +268 -43
- package/src/editor/utils/geometry.ts +4 -3
- package/src/editor/utils/html-utils.ts +10 -5
- package/src/hooks/useEditorShortcuts.ts +39 -2
- package/src/index.ts +25 -0
- package/src/io.ts +54 -0
- package/src/styles/skin.css +159 -0
- package/src/types/editor-components.ts +23 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 汎用的なHTML編集エディタとして、スライド、ページ、コンポーネント等で利用可能
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
import { useEditorShortcuts } from '../hooks/useEditorShortcuts';
|
|
16
16
|
import {
|
|
17
17
|
editorCancelDragRef,
|
|
@@ -32,7 +32,10 @@ import { useAuth } from '../components/auth/AuthProvider';
|
|
|
32
32
|
import { findSlideRoot, findInsertionParent } from './utils/slide-root';
|
|
33
33
|
import { registerAutoSaveFlush } from './autosave';
|
|
34
34
|
import { getCleanHtml } from './utils/html-utils';
|
|
35
|
-
import './
|
|
35
|
+
import { EditorAppearanceContext, useEditorTheme } from './contexts/EditorAppearanceContext';
|
|
36
|
+
import { CanvasAppearance } from './components/shell/CanvasAppearance';
|
|
37
|
+
import { can, io } from '../io';
|
|
38
|
+
import { insertIntoFlow } from './parts';
|
|
36
39
|
import type { Slide } from '../types/slide';
|
|
37
40
|
import {
|
|
38
41
|
useEditorMessages,
|
|
@@ -55,16 +58,9 @@ import {
|
|
|
55
58
|
EditorFooter,
|
|
56
59
|
EditorContextMenu,
|
|
57
60
|
AiPromptPopover,
|
|
58
|
-
HtmlEditorDialog,
|
|
59
|
-
HtmlImportDialog,
|
|
60
|
-
CssEditorDialog,
|
|
61
|
-
JsEditorDialog,
|
|
62
|
-
PageSettingsDialog,
|
|
63
|
-
MediaLibraryDialog,
|
|
64
61
|
EditorCanvas,
|
|
65
62
|
VariablesPanel,
|
|
66
63
|
ComponentPanel,
|
|
67
|
-
MasterComponentEditor,
|
|
68
64
|
} from './components';
|
|
69
65
|
import { useEditorVariables, useEditorComponents } from './EditorContext';
|
|
70
66
|
import type { CSSVariableDefinition } from '../types/css-variables';
|
|
@@ -102,6 +98,13 @@ import { cn } from '../lib/utils';
|
|
|
102
98
|
* 打鍵やドラッグのたびに保存(=原本TSXへの書き戻しジョブ)が走ることはない。
|
|
103
99
|
*/
|
|
104
100
|
const AUTO_SAVE_DELAY_MS = 2000;
|
|
101
|
+
const MasterComponentEditor = lazy(() => import('./components/MasterComponentEditor').then((m) => ({ default: m.MasterComponentEditor })));
|
|
102
|
+
const MediaLibraryDialog = lazy(() => import('./components/MediaLibraryDialog').then((m) => ({ default: m.MediaLibraryDialog })));
|
|
103
|
+
const PageSettingsDialog = lazy(() => import('./components/PageSettingsDialog').then((m) => ({ default: m.PageSettingsDialog })));
|
|
104
|
+
const JsEditorDialog = lazy(() => import('./components/JsEditorDialog').then((m) => ({ default: m.JsEditorDialog })));
|
|
105
|
+
const CssEditorDialog = lazy(() => import('./components/CssEditorDialog').then((m) => ({ default: m.CssEditorDialog })));
|
|
106
|
+
const HtmlImportDialog = lazy(() => import('./components/HtmlImportDialog').then((m) => ({ default: m.HtmlImportDialog })));
|
|
107
|
+
const HtmlEditorDialog = lazy(() => import('./components/HtmlEditorDialog').then((m) => ({ default: m.HtmlEditorDialog })));
|
|
105
108
|
|
|
106
109
|
/**
|
|
107
110
|
* 保存の状態表示。エディタの殻(Figma風ヘッダー / PowerPoint風タイトルバー)で共通に使う。
|
|
@@ -285,6 +288,8 @@ function FrontendVisualEditorInner({
|
|
|
285
288
|
componentLibrary,
|
|
286
289
|
createInstance,
|
|
287
290
|
getMasterComponent,
|
|
291
|
+
partsMode,
|
|
292
|
+
materializePartInstance,
|
|
288
293
|
} = useEditorComponents();
|
|
289
294
|
|
|
290
295
|
const { getIdToken } = useAuth();
|
|
@@ -332,15 +337,14 @@ function FrontendVisualEditorInner({
|
|
|
332
337
|
try { localStorage.setItem('gg-editor:ui-mode', mode); } catch { /* 記憶できなくても動作は継続 */ }
|
|
333
338
|
};
|
|
334
339
|
// PowerPoint風UIのテーマ(OS設定に追従・切替は記憶)とサムネイル検索
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
setPptTheme(next);
|
|
339
|
-
try { localStorage.setItem('gg-editor:ppt-theme', next); } catch { /* 続行 */ }
|
|
340
|
-
};
|
|
340
|
+
const { theme: pptTheme, toggleTheme: togglePptTheme } = useEditorTheme();
|
|
341
|
+
const [layersOpen, setLayersOpen] = useState(editorMode !== 'webpage' || !!pagesPanel);
|
|
342
|
+
const [guidesOpen, setGuidesOpen] = useState(false);
|
|
341
343
|
const [pptSearch, setPptSearch] = useState('');
|
|
342
344
|
// コメントパネル(PowerPoint風UIのみ)。フォーカス合図はカウンタで送る
|
|
343
345
|
const [pptCommentsOpen, setPptCommentsOpen] = useState(false);
|
|
346
|
+
const [commentsMounted, setCommentsMounted] = useState(false);
|
|
347
|
+
useEffect(() => { if (pptCommentsOpen) setCommentsMounted(true); }, [pptCommentsOpen]);
|
|
344
348
|
// PowerPoint風UIの右ペイン「図の書式設定」(影・反射・光彩・ぼかし)
|
|
345
349
|
const [pptFormatPaneOpen, setPptFormatPaneOpen] = useState(false);
|
|
346
350
|
const [pptCommentFocus, setPptCommentFocus] = useState(0);
|
|
@@ -534,6 +538,10 @@ function FrontendVisualEditorInner({
|
|
|
534
538
|
selectedElementInstance,
|
|
535
539
|
isComponentInstance,
|
|
536
540
|
hasOverrides,
|
|
541
|
+
isPartInstance,
|
|
542
|
+
partLabel,
|
|
543
|
+
handleDetachPart,
|
|
544
|
+
handleUpdatePart,
|
|
537
545
|
handleCreateComponent,
|
|
538
546
|
handleConfirmCreateComponent,
|
|
539
547
|
handleEditMasterComponent,
|
|
@@ -817,6 +825,17 @@ function FrontendVisualEditorInner({
|
|
|
817
825
|
/** ページ切替(サムネイル・新しいスライド)の直前に、殻から保存を呼べるようにする */
|
|
818
826
|
useEffect(() => registerAutoSaveFlush(() => saveIfDirtyRef.current()), []);
|
|
819
827
|
|
|
828
|
+
/**
|
|
829
|
+
* この編集画面が消えるとき(ページ切替で key が変わる・閉じる)、保存待ちの変更を捨てない。
|
|
830
|
+
*
|
|
831
|
+
* 自動保存は2秒のデバウンス。編集して2秒以内にページを切り替えると、タイマーごと
|
|
832
|
+
* 捨てられて編集が消えていた(構成ラフで実測: 見出しを直して隣のページへ移ると失われる)。
|
|
833
|
+
* useLayoutEffect の後始末は iframe がまだ DOM にある間に走るので、ここで
|
|
834
|
+
* 本文を読み取って保存を始める。onSave はこの画面のもの(古い閉包)なので、
|
|
835
|
+
* 切替先ではなく編集していたページへ正しく保存される。
|
|
836
|
+
*/
|
|
837
|
+
useLayoutEffect(() => () => { void saveIfDirtyRef.current(); }, []);
|
|
838
|
+
|
|
820
839
|
/** ページが変わったら「保存済みの本文」の記憶も切り替える */
|
|
821
840
|
useEffect(() => {
|
|
822
841
|
savedPayloadRef.current = null;
|
|
@@ -1080,7 +1099,35 @@ function FrontendVisualEditorInner({
|
|
|
1080
1099
|
try {
|
|
1081
1100
|
const { componentId } = JSON.parse(componentData);
|
|
1082
1101
|
console.log('[FrontendVisualEditor] Parsed componentId:', componentId);
|
|
1083
|
-
if (componentId) {
|
|
1102
|
+
if (componentId && partsMode) {
|
|
1103
|
+
// 部品モード: 実体化してフローへ(iframe 内の座標がそのまま来る)
|
|
1104
|
+
const iframeDoc = iframeRef.current?.contentDocument;
|
|
1105
|
+
const element = iframeDoc ? materializePartInstance(componentId, iframeDoc) : null;
|
|
1106
|
+
if (iframeDoc && element) {
|
|
1107
|
+
if (editorMode === 'webpage') {
|
|
1108
|
+
insertIntoFlow(iframeDoc, element, x || 0, y || 0);
|
|
1109
|
+
} else {
|
|
1110
|
+
element.style.position = 'absolute';
|
|
1111
|
+
element.style.left = `${x || 100}px`;
|
|
1112
|
+
element.style.top = `${y || 100}px`;
|
|
1113
|
+
findInsertionParent(iframeDoc).appendChild(element);
|
|
1114
|
+
}
|
|
1115
|
+
if (!element.getAttribute('data-element-id')) {
|
|
1116
|
+
element.setAttribute('data-editable', 'true');
|
|
1117
|
+
element.setAttribute('data-element-id', `el-${Date.now()}-part`);
|
|
1118
|
+
}
|
|
1119
|
+
notifyIframeChange(true);
|
|
1120
|
+
const { extractElementInfo } = await import('./utils/style-utils');
|
|
1121
|
+
const elementInfo = extractElementInfo(element, iframeDoc);
|
|
1122
|
+
if (elementInfo) {
|
|
1123
|
+
setSelectedElement(elementInfo);
|
|
1124
|
+
setSelectedElementIds([element.getAttribute('data-element-id')!]);
|
|
1125
|
+
element.classList.add('selected');
|
|
1126
|
+
}
|
|
1127
|
+
} else {
|
|
1128
|
+
console.error('[FrontendVisualEditor] 部品が見つかりません:', componentId);
|
|
1129
|
+
}
|
|
1130
|
+
} else if (componentId) {
|
|
1084
1131
|
// インスタンスを作成
|
|
1085
1132
|
const instance = createInstance(componentId, undefined, contentId);
|
|
1086
1133
|
console.log('[FrontendVisualEditor] Created instance:', instance);
|
|
@@ -1133,7 +1180,7 @@ function FrontendVisualEditorInner({
|
|
|
1133
1180
|
|
|
1134
1181
|
window.addEventListener('message', handleMessage);
|
|
1135
1182
|
return () => window.removeEventListener('message', handleMessage);
|
|
1136
|
-
}, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds]);
|
|
1183
|
+
}, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds, partsMode, materializePartInstance, editorMode]);
|
|
1137
1184
|
|
|
1138
1185
|
// ドラッグ&ドロップハンドラ
|
|
1139
1186
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
@@ -1210,6 +1257,38 @@ function FrontendVisualEditorInner({
|
|
|
1210
1257
|
y = (e.clientY - iframeRect.top) / dropScale;
|
|
1211
1258
|
}
|
|
1212
1259
|
|
|
1260
|
+
// 部品モード: 定義を実体化してそのまま置く(JSON のインスタンスは作らない)。
|
|
1261
|
+
// webpage はフローに差し込み、slide は従来どおり絶対配置
|
|
1262
|
+
if (partsMode) {
|
|
1263
|
+
const element = materializePartInstance(componentId, iframeDoc);
|
|
1264
|
+
if (!element) {
|
|
1265
|
+
console.error('[FrontendVisualEditor] 部品が見つかりません:', componentId);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (editorMode === 'webpage') {
|
|
1269
|
+
insertIntoFlow(iframeDoc, element, e.clientX - iframeRect.left, e.clientY - iframeRect.top);
|
|
1270
|
+
} else {
|
|
1271
|
+
element.style.position = 'absolute';
|
|
1272
|
+
element.style.left = `${x}px`;
|
|
1273
|
+
element.style.top = `${y}px`;
|
|
1274
|
+
(slideRoot ?? iframeDoc.body).appendChild(element);
|
|
1275
|
+
}
|
|
1276
|
+
// MutationObserver が id を付けるのは非同期なので、選択できるよう先に付ける
|
|
1277
|
+
if (!element.getAttribute('data-element-id')) {
|
|
1278
|
+
element.setAttribute('data-editable', 'true');
|
|
1279
|
+
element.setAttribute('data-element-id', `el-${Date.now()}-part`);
|
|
1280
|
+
}
|
|
1281
|
+
notifyIframeChange(true);
|
|
1282
|
+
const { extractElementInfo } = await import('./utils/style-utils');
|
|
1283
|
+
const elementInfo = extractElementInfo(element, iframeDoc);
|
|
1284
|
+
if (elementInfo) {
|
|
1285
|
+
setSelectedElement(elementInfo);
|
|
1286
|
+
setSelectedElementIds([element.getAttribute('data-element-id')!]);
|
|
1287
|
+
element.classList.add('selected');
|
|
1288
|
+
}
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1213
1292
|
// インスタンスを作成(バリアントIDが指定されていれば使用)
|
|
1214
1293
|
// Pass position directly to createInstance to avoid React state batching race condition
|
|
1215
1294
|
console.log('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
|
|
@@ -1287,7 +1366,7 @@ function FrontendVisualEditorInner({
|
|
|
1287
1366
|
}
|
|
1288
1367
|
}
|
|
1289
1368
|
}
|
|
1290
|
-
}, [containerRef, uploadFromFiles, createInstance, getMasterComponent, setSelectedElement, contentId, pushHistory, iframeRef, zoom]);
|
|
1369
|
+
}, [containerRef, uploadFromFiles, createInstance, getMasterComponent, setSelectedElement, contentId, pushHistory, iframeRef, zoom, partsMode, materializePartInstance, editorMode, notifyIframeChange, setSelectedElementIds]);
|
|
1291
1370
|
|
|
1292
1371
|
// クリップボードからのペースト処理(画像、HTML、Excel、Word、SVG)
|
|
1293
1372
|
// 同じpasteイベントを二度処理しないための記録。
|
|
@@ -1814,18 +1893,22 @@ function FrontendVisualEditorInner({
|
|
|
1814
1893
|
// ================= UIモード(Figma風 / PowerPoint風) =================
|
|
1815
1894
|
// ベース(キャンバス・選択・保存・書き戻し)は完全共通で、切り替わるのは殻だけ。
|
|
1816
1895
|
// 選択はlocalStorageに記憶する
|
|
1817
|
-
const isPpt = uiMode === 'ppt' && !isMultiPageCanvas;
|
|
1896
|
+
const isPpt = uiMode === 'ppt' && editorMode === 'slide' && !isMultiPageCanvas;
|
|
1818
1897
|
|
|
1819
1898
|
return (
|
|
1899
|
+
<EditorAppearanceContext.Provider value={pptTheme}>
|
|
1820
1900
|
<div
|
|
1901
|
+
data-editor-theme={pptTheme}
|
|
1902
|
+
data-editor-mode={editorMode}
|
|
1821
1903
|
className={cn(
|
|
1822
1904
|
// [移植時の修正] gg-editor-skin でビューアと同じ配色に揃える(editor-skin.css)
|
|
1823
|
-
"gg-editor-skin flex flex-col",
|
|
1905
|
+
"gg-editor-skin gg-editor-ui flex flex-col",
|
|
1824
1906
|
isMultiPageCanvas ? "absolute inset-0 z-40" : "fixed inset-0 z-50"
|
|
1825
1907
|
)}
|
|
1826
1908
|
data-frontend-visual-editor="true"
|
|
1827
|
-
style={{
|
|
1909
|
+
style={{ backgroundColor: 'var(--ed-bg)' }}
|
|
1828
1910
|
>
|
|
1911
|
+
<CanvasAppearance theme={pptTheme} />
|
|
1829
1912
|
{/* ヘッダー(UIモードで切り替え) */}
|
|
1830
1913
|
{isPpt ? (
|
|
1831
1914
|
<>
|
|
@@ -1902,6 +1985,12 @@ function FrontendVisualEditorInner({
|
|
|
1902
1985
|
</>
|
|
1903
1986
|
) : (
|
|
1904
1987
|
<EditorHeader
|
|
1988
|
+
theme={pptTheme}
|
|
1989
|
+
onToggleTheme={togglePptTheme}
|
|
1990
|
+
layersOpen={layersOpen}
|
|
1991
|
+
onToggleLayers={() => setLayersOpen((v) => !v)}
|
|
1992
|
+
guidesOpen={guidesOpen}
|
|
1993
|
+
onToggleGuides={() => setGuidesOpen((v) => !v)}
|
|
1905
1994
|
comments={{
|
|
1906
1995
|
open: pptCommentsOpen,
|
|
1907
1996
|
toggle: () =>
|
|
@@ -1918,13 +2007,13 @@ function FrontendVisualEditorInner({
|
|
|
1918
2007
|
saveStatus={saveStatus}
|
|
1919
2008
|
isCanvasEditing={isMultiPageCanvas}
|
|
1920
2009
|
onImport={() => setIsImportDialogOpen(true)}
|
|
1921
|
-
onCssEdit={handleOpenCssEditor}
|
|
2010
|
+
onCssEdit={can('apiFetch') ? handleOpenCssEditor : undefined}
|
|
1922
2011
|
hasCss={!!importedCss}
|
|
1923
|
-
onJsEdit={handleOpenJsEditor}
|
|
2012
|
+
onJsEdit={can('apiFetch') ? handleOpenJsEditor : undefined}
|
|
1924
2013
|
hasJs={!!importedJs}
|
|
1925
|
-
onPageSettings={handleOpenPageSettings}
|
|
2014
|
+
onPageSettings={can('apiFetch') ? handleOpenPageSettings : undefined}
|
|
1926
2015
|
hasPageSettings={!!(pageSettings.title || pageSettings.description || pageSettings.ogp?.image)}
|
|
1927
|
-
onExport={parentId && contentId ? handleExport : undefined}
|
|
2016
|
+
onExport={can('apiFetch') && parentId && contentId ? handleExport : undefined}
|
|
1928
2017
|
contextNumber={Number(currentContentId ?? contentId) || undefined}
|
|
1929
2018
|
contextTitle={contentList.find((c) => c.id === (currentContentId ?? contentId))?.title}
|
|
1930
2019
|
headerExtra={headerExtra}
|
|
@@ -1940,7 +2029,9 @@ function FrontendVisualEditorInner({
|
|
|
1940
2029
|
) : isMultiPageCanvas ? (
|
|
1941
2030
|
<EditorLayerPanel />
|
|
1942
2031
|
) : (
|
|
1943
|
-
<
|
|
2032
|
+
<div className={layersOpen && !isComponentPanelOpen ? "flex min-h-0" : "hidden"}>
|
|
2033
|
+
<LeftPanel page={Number(currentContentId ?? contentId) || 1} pagesSlot={pagesPanel} />
|
|
2034
|
+
</div>
|
|
1944
2035
|
)}
|
|
1945
2036
|
|
|
1946
2037
|
{/* コンポーネントパネル(左側、レイヤーパネルの隣) */}
|
|
@@ -1978,10 +2069,10 @@ function FrontendVisualEditorInner({
|
|
|
1978
2069
|
<>
|
|
1979
2070
|
<EditorCanvas />
|
|
1980
2071
|
{/* ブレイクポイントガイド(webpageモード用) */}
|
|
1981
|
-
<BreakpointGuides
|
|
2072
|
+
{guidesOpen && <BreakpointGuides
|
|
1982
2073
|
containerWidth={canvasAreaSize.width}
|
|
1983
2074
|
containerHeight={canvasAreaSize.height}
|
|
1984
|
-
/>
|
|
2075
|
+
/>}
|
|
1985
2076
|
</>
|
|
1986
2077
|
)}
|
|
1987
2078
|
|
|
@@ -2035,9 +2126,9 @@ function FrontendVisualEditorInner({
|
|
|
2035
2126
|
onBringToFront={selectedElement ? bringToFront : undefined}
|
|
2036
2127
|
onSendToBack={selectedElement ? sendToBack : undefined}
|
|
2037
2128
|
onImageUpload={() => openFilePicker({ x: 100, y: 100 })}
|
|
2038
|
-
onOpenMediaLibrary={() => setIsMediaLibraryOpen(true)}
|
|
2129
|
+
onOpenMediaLibrary={can('apiFetch') ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2039
2130
|
isMediaReplaceMode={isImageSelected}
|
|
2040
|
-
onAiRegenerate={selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2131
|
+
onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2041
2132
|
onOpenVariables={() => setIsVariablesPanelOpen(true)}
|
|
2042
2133
|
hasVariables={hasVariables}
|
|
2043
2134
|
onOpenComponents={() => setIsComponentPanelOpen(true)}
|
|
@@ -2078,8 +2169,8 @@ function FrontendVisualEditorInner({
|
|
|
2078
2169
|
{/* 右パネルは同時に1つだけ出す。
|
|
2079
2170
|
コメントを開いている間はプロパティ(詳細編集)を引っ込め、閉じると戻る。
|
|
2080
2171
|
2つ並ぶと「いまどちらを操作しているのか」が分からなくなるため */}
|
|
2081
|
-
{!isPpt && !pptCommentsOpen && <EditorPropertyPanel />}
|
|
2082
|
-
{!isMultiPageCanvas && (
|
|
2172
|
+
{!isPpt && !pptCommentsOpen && (editorMode === 'slide' || selectedElement || selectedElementIds.length > 0 || activeTool === 'scale') && <EditorPropertyPanel />}
|
|
2173
|
+
{!isMultiPageCanvas && can('commentAction') && (
|
|
2083
2174
|
<PptCommentMarkers
|
|
2084
2175
|
page={Number(currentContentId ?? contentId) || 1}
|
|
2085
2176
|
onOpenThread={(id) => {
|
|
@@ -2091,23 +2182,25 @@ function FrontendVisualEditorInner({
|
|
|
2091
2182
|
{isPpt && pptFormatPaneOpen && (
|
|
2092
2183
|
<PptFormatPane theme={pptTheme} onClose={() => setPptFormatPaneOpen(false)} />
|
|
2093
2184
|
)}
|
|
2094
|
-
{!isMultiPageCanvas && pptCommentsOpen && (
|
|
2185
|
+
{!isMultiPageCanvas && can('commentAction') && (pptCommentsOpen || commentsMounted) && (
|
|
2186
|
+
<div className={pptCommentsOpen ? 'flex min-h-0' : 'hidden'}>
|
|
2095
2187
|
<PptCommentsPanel
|
|
2096
2188
|
page={Number(currentContentId ?? contentId) || 1}
|
|
2097
|
-
theme={
|
|
2189
|
+
theme={pptTheme}
|
|
2098
2190
|
onClose={() => setPptCommentsOpen(false)}
|
|
2099
2191
|
focusSignal={pptCommentFocus}
|
|
2100
2192
|
activeThreadId={pptActiveThread}
|
|
2101
2193
|
onActiveThread={setPptActiveThread}
|
|
2102
2194
|
/>
|
|
2195
|
+
</div>
|
|
2103
2196
|
)}
|
|
2104
2197
|
</div>
|
|
2105
2198
|
|
|
2106
2199
|
{/* ノート欄(トークスクリプト)。PowerPoint風・Figma風の両方に出す。
|
|
2107
2200
|
Figma風はダーク配色で固定(スキンと馴染む) */}
|
|
2108
2201
|
{/* ノート欄は発表原稿。Webページには無い概念なので出さない */}
|
|
2109
|
-
{!isMultiPageCanvas && editorMode !== 'webpage' && (
|
|
2110
|
-
<PptNotes page={Number(currentContentId ?? contentId) || 1} theme={
|
|
2202
|
+
{!isMultiPageCanvas && editorMode !== 'webpage' && can('apiFetch') && (
|
|
2203
|
+
<PptNotes page={Number(currentContentId ?? contentId) || 1} theme={pptTheme} />
|
|
2111
2204
|
)}
|
|
2112
2205
|
|
|
2113
2206
|
{/* フッター(Figma風) / ステータスバー(PowerPoint風) */}
|
|
@@ -2127,13 +2220,18 @@ function FrontendVisualEditorInner({
|
|
|
2127
2220
|
hasStyleInClipboard={hasStyleInClipboard()}
|
|
2128
2221
|
isComponentInstance={isComponentInstance}
|
|
2129
2222
|
hasOverrides={hasOverrides}
|
|
2223
|
+
partsMode={partsMode}
|
|
2224
|
+
isPartInstance={isPartInstance}
|
|
2225
|
+
partLabel={partLabel}
|
|
2226
|
+
onDetachPart={handleDetachPart}
|
|
2227
|
+
onUpdatePart={can('savePart') ? handleUpdatePart : undefined}
|
|
2130
2228
|
onCopy={selectedElement ? copyElements : undefined}
|
|
2131
2229
|
onCut={selectedElement ? cutElements : undefined}
|
|
2132
2230
|
onPaste={pasteElements}
|
|
2133
2231
|
onDelete={selectedElement ? deleteElement : undefined}
|
|
2134
2232
|
onDuplicate={selectedElement ? duplicateElement : undefined}
|
|
2135
2233
|
onReplaceImageFromFile={isImageSelected ? replaceImageFromFile : undefined}
|
|
2136
|
-
onReplaceImageFromLibrary={isImageSelected ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2234
|
+
onReplaceImageFromLibrary={can('apiFetch') && isImageSelected ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2137
2235
|
onReplaceImageFromClipboard={isImageSelected ? () => void replaceImageFromClipboard() : undefined}
|
|
2138
2236
|
onCopyStyle={selectedElement ? copyStyle : undefined}
|
|
2139
2237
|
onPasteStyle={selectedElement ? pasteStyle : undefined}
|
|
@@ -2150,7 +2248,7 @@ function FrontendVisualEditorInner({
|
|
|
2150
2248
|
onDeleteRow={tableActions?.deleteRow}
|
|
2151
2249
|
onDeleteColumn={tableActions?.deleteColumn}
|
|
2152
2250
|
onEditLink={selectedElement ? handleEditLink : undefined}
|
|
2153
|
-
onAiRegenerate={selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2251
|
+
onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2154
2252
|
onEditHtml={selectedElement ? handleEditHtml : undefined}
|
|
2155
2253
|
onCreateComponent={handleCreateComponent}
|
|
2156
2254
|
onGoToMainComponent={handleGoToMainComponent}
|
|
@@ -2170,6 +2268,7 @@ function FrontendVisualEditorInner({
|
|
|
2170
2268
|
/>
|
|
2171
2269
|
|
|
2172
2270
|
{/* HTML編集ダイアログ */}
|
|
2271
|
+
{htmlEditorState.isOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2173
2272
|
<HtmlEditorDialog
|
|
2174
2273
|
isOpen={htmlEditorState.isOpen}
|
|
2175
2274
|
onClose={() => {
|
|
@@ -2182,8 +2281,10 @@ function FrontendVisualEditorInner({
|
|
|
2182
2281
|
onSave={handleSaveHtml}
|
|
2183
2282
|
initialHtml={htmlEditorState.initialHtml}
|
|
2184
2283
|
/>
|
|
2284
|
+
</Suspense>}
|
|
2185
2285
|
|
|
2186
2286
|
{/* HTMLインポートダイアログ */}
|
|
2287
|
+
{isImportDialogOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2187
2288
|
<HtmlImportDialog
|
|
2188
2289
|
isOpen={isImportDialogOpen}
|
|
2189
2290
|
onClose={() => {
|
|
@@ -2197,8 +2298,10 @@ function FrontendVisualEditorInner({
|
|
|
2197
2298
|
presentationId={parentId}
|
|
2198
2299
|
slideId={contentId}
|
|
2199
2300
|
/>
|
|
2301
|
+
</Suspense>}
|
|
2200
2302
|
|
|
2201
2303
|
{/* CSS編集ダイアログ */}
|
|
2304
|
+
{isCssEditorOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2202
2305
|
<CssEditorDialog
|
|
2203
2306
|
isOpen={isCssEditorOpen}
|
|
2204
2307
|
onClose={() => {
|
|
@@ -2212,8 +2315,10 @@ function FrontendVisualEditorInner({
|
|
|
2212
2315
|
onSave={handleSaveCss}
|
|
2213
2316
|
onClear={handleClearCss}
|
|
2214
2317
|
/>
|
|
2318
|
+
</Suspense>}
|
|
2215
2319
|
|
|
2216
2320
|
{/* JS編集ダイアログ */}
|
|
2321
|
+
{isJsEditorOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2217
2322
|
<JsEditorDialog
|
|
2218
2323
|
isOpen={isJsEditorOpen}
|
|
2219
2324
|
onClose={() => {
|
|
@@ -2227,8 +2332,10 @@ function FrontendVisualEditorInner({
|
|
|
2227
2332
|
onSave={handleSaveJs}
|
|
2228
2333
|
onClear={handleClearJs}
|
|
2229
2334
|
/>
|
|
2335
|
+
</Suspense>}
|
|
2230
2336
|
|
|
2231
2337
|
{/* ページ設定ダイアログ */}
|
|
2338
|
+
{isPageSettingsOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2232
2339
|
<PageSettingsDialog
|
|
2233
2340
|
isOpen={isPageSettingsOpen}
|
|
2234
2341
|
onClose={() => {
|
|
@@ -2243,8 +2350,10 @@ function FrontendVisualEditorInner({
|
|
|
2243
2350
|
onSave={handleSavePageSettings}
|
|
2244
2351
|
onUploadImage={handleUploadOgpImage}
|
|
2245
2352
|
/>
|
|
2353
|
+
</Suspense>}
|
|
2246
2354
|
|
|
2247
2355
|
{/* メディアライブラリ */}
|
|
2356
|
+
{isMediaLibraryOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2248
2357
|
<MediaLibraryDialog
|
|
2249
2358
|
isOpen={isMediaLibraryOpen}
|
|
2250
2359
|
onClose={() => {
|
|
@@ -2258,6 +2367,7 @@ function FrontendVisualEditorInner({
|
|
|
2258
2367
|
mode={isImageSelected ? 'replace' : 'insert'}
|
|
2259
2368
|
currentSrc={selectedImageSrc}
|
|
2260
2369
|
/>
|
|
2370
|
+
</Suspense>}
|
|
2261
2371
|
|
|
2262
2372
|
{/* CSS変数パネル */}
|
|
2263
2373
|
<VariablesPanel
|
|
@@ -2282,7 +2392,7 @@ function FrontendVisualEditorInner({
|
|
|
2282
2392
|
<Dialog open={createComponentDialogOpen} onOpenChange={setCreateComponentDialogOpen}>
|
|
2283
2393
|
<DialogContent className="bg-[#2c2c2c] border-[#444444] text-white">
|
|
2284
2394
|
<DialogHeader>
|
|
2285
|
-
<DialogTitle
|
|
2395
|
+
<DialogTitle>{partsMode ? '部品として保存' : 'コンポーネントを作成'}</DialogTitle>
|
|
2286
2396
|
</DialogHeader>
|
|
2287
2397
|
<div className="space-y-4 py-4">
|
|
2288
2398
|
<div className="space-y-2">
|
|
@@ -2323,6 +2433,7 @@ function FrontendVisualEditorInner({
|
|
|
2323
2433
|
</Dialog>
|
|
2324
2434
|
|
|
2325
2435
|
{/* マスターコンポーネントエディタ */}
|
|
2436
|
+
{isMasterEditorOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2326
2437
|
<MasterComponentEditor
|
|
2327
2438
|
master={editingMasterComponent}
|
|
2328
2439
|
open={isMasterEditorOpen}
|
|
@@ -2340,6 +2451,7 @@ function FrontendVisualEditorInner({
|
|
|
2340
2451
|
name: c.name,
|
|
2341
2452
|
}))}
|
|
2342
2453
|
/>
|
|
2454
|
+
</Suspense>}
|
|
2343
2455
|
|
|
2344
2456
|
{/* レイアウトモード切替ヒントトースト。
|
|
2345
2457
|
Webページでは絶対配置に倒さないので、それを勧めるこの導線も出さない */}
|
|
@@ -2384,7 +2496,29 @@ function FrontendVisualEditorInner({
|
|
|
2384
2496
|
</div>
|
|
2385
2497
|
</div>
|
|
2386
2498
|
)}
|
|
2499
|
+
|
|
2500
|
+
{/* Webページ用の案内。
|
|
2501
|
+
Webページでは「絶対配置モードに切り替える」を勧めない(版面が固定pxに固まる)ので、
|
|
2502
|
+
代わりに逃げ道だけを伝える。sonner の Toaster はアプリ側に無く toast() が
|
|
2503
|
+
表示されないため、既存のこの帯を流用している */}
|
|
2504
|
+
{showLayoutHint && editorMode === 'webpage' && (
|
|
2505
|
+
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-[100] animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
2506
|
+
<div className="bg-black/90 text-white px-4 py-3 rounded-lg shadow-xl flex items-center gap-4">
|
|
2507
|
+
<span className="text-sm">
|
|
2508
|
+
並べ替える相手がありません。Cmd/Ctrl+ドラッグで自由配置、ダブルクリックで中に入れます
|
|
2509
|
+
</span>
|
|
2510
|
+
<button
|
|
2511
|
+
onClick={() => setShowLayoutHint(false)}
|
|
2512
|
+
className="text-gray-400 hover:text-white text-lg leading-none ml-1"
|
|
2513
|
+
aria-label="閉じる"
|
|
2514
|
+
>
|
|
2515
|
+
×
|
|
2516
|
+
</button>
|
|
2517
|
+
</div>
|
|
2518
|
+
</div>
|
|
2519
|
+
)}
|
|
2387
2520
|
</div>
|
|
2521
|
+
</EditorAppearanceContext.Provider>
|
|
2388
2522
|
);
|
|
2389
2523
|
}
|
|
2390
2524
|
|
|
@@ -2430,9 +2564,11 @@ export function FrontendVisualEditor({
|
|
|
2430
2564
|
const [initialVariables, setInitialVariables] = useState<CSSVariableDefinition[]>([]);
|
|
2431
2565
|
const [isVariablesLoaded, setIsVariablesLoaded] = useState(false);
|
|
2432
2566
|
|
|
2433
|
-
// CSS変数をロード(webpageモード・slide
|
|
2567
|
+
// CSS変数をロード(webpageモード・slideモード両対応)。
|
|
2568
|
+
// 利用側が io.loadVariables を渡していればそれ(ファイル等)から、無ければブラウザ内(localStorage)から
|
|
2434
2569
|
useEffect(() => {
|
|
2435
|
-
|
|
2570
|
+
const fromIo = io().loadVariables;
|
|
2571
|
+
if (!effectiveParentId && !fromIo) {
|
|
2436
2572
|
setIsVariablesLoaded(true);
|
|
2437
2573
|
return;
|
|
2438
2574
|
}
|
|
@@ -2441,9 +2577,11 @@ export function FrontendVisualEditor({
|
|
|
2441
2577
|
try {
|
|
2442
2578
|
// editorModeに応じてスコープを決定
|
|
2443
2579
|
const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
|
|
2444
|
-
const variables =
|
|
2580
|
+
const variables = fromIo
|
|
2581
|
+
? await fromIo()
|
|
2582
|
+
: await getCSSVariablesList(effectiveParentId!, scope);
|
|
2445
2583
|
setInitialVariables(variables);
|
|
2446
|
-
console.log(`[FrontendVisualEditor] Loaded CSS variables (${scope}):`, variables.length);
|
|
2584
|
+
console.log(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
|
|
2447
2585
|
} catch (error) {
|
|
2448
2586
|
console.error('[FrontendVisualEditor] Failed to load CSS variables:', error);
|
|
2449
2587
|
} finally {
|
|
@@ -2456,6 +2594,12 @@ export function FrontendVisualEditor({
|
|
|
2456
2594
|
|
|
2457
2595
|
// CSS変数を保存するコールバック
|
|
2458
2596
|
const handleSaveVariables = useCallback(async (variables: CSSVariableDefinition[]) => {
|
|
2597
|
+
const toIo = io().saveVariables;
|
|
2598
|
+
if (toIo) {
|
|
2599
|
+
await toIo(variables);
|
|
2600
|
+
console.log('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2459
2603
|
if (!effectiveParentId) {
|
|
2460
2604
|
throw new Error('Parent ID is required to save CSS variables');
|
|
2461
2605
|
}
|
|
@@ -2467,6 +2611,8 @@ export function FrontendVisualEditor({
|
|
|
2467
2611
|
|
|
2468
2612
|
// CSS変数をロードするコールバック
|
|
2469
2613
|
const handleLoadVariables = useCallback(async (resourceId: string) => {
|
|
2614
|
+
const fromIo = io().loadVariables;
|
|
2615
|
+
if (fromIo) return fromIo();
|
|
2470
2616
|
// editorModeに応じてスコープを決定
|
|
2471
2617
|
const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
|
|
2472
2618
|
return getCSSVariablesList(resourceId, scope);
|
|
@@ -318,14 +318,19 @@ export function EditorCanvas() {
|
|
|
318
318
|
if (!container) return;
|
|
319
319
|
|
|
320
320
|
const scrollLeft = (container.scrollWidth - container.clientWidth) / 2;
|
|
321
|
-
|
|
321
|
+
// webpage は上端固定(紙面は #canvas-scroll-area で flex-start)。
|
|
322
|
+
// 初期表示もページの先頭から見えるべきで、縦の中央へ飛ばさない
|
|
323
|
+
const scrollTop =
|
|
324
|
+
editorMode === "webpage"
|
|
325
|
+
? 0
|
|
326
|
+
: (container.scrollHeight - container.clientHeight) / 2;
|
|
322
327
|
|
|
323
328
|
container.scrollTo({
|
|
324
329
|
left: scrollLeft,
|
|
325
330
|
top: scrollTop,
|
|
326
331
|
behavior: "instant",
|
|
327
332
|
});
|
|
328
|
-
}, [containerRef]);
|
|
333
|
+
}, [containerRef, editorMode]);
|
|
329
334
|
|
|
330
335
|
useEffect(() => {
|
|
331
336
|
if (isInMultiPageMode) return; // マルチページモードではキャンバスがスクロールを管理
|
|
@@ -973,7 +978,7 @@ export function EditorCanvas() {
|
|
|
973
978
|
ref={containerRef}
|
|
974
979
|
className="h-full w-full"
|
|
975
980
|
style={{
|
|
976
|
-
backgroundColor: isInMultiPageMode ? "transparent" : "
|
|
981
|
+
backgroundColor: isInMultiPageMode ? "transparent" : "var(--ed-bg)",
|
|
977
982
|
}}
|
|
978
983
|
>
|
|
979
984
|
<iframe
|