@growgroup/visual-editor 0.1.6 → 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 +157 -43
- package/src/editor/components/EditorCanvas.tsx +1 -1
- 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/useComponentEditMode.ts +95 -1
- package/src/editor/hooks/useIframeSetup.ts +10 -0
- 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 +5 -4
- 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, useLayoutEffect, 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,
|
|
@@ -1091,7 +1099,35 @@ function FrontendVisualEditorInner({
|
|
|
1091
1099
|
try {
|
|
1092
1100
|
const { componentId } = JSON.parse(componentData);
|
|
1093
1101
|
console.log('[FrontendVisualEditor] Parsed componentId:', componentId);
|
|
1094
|
-
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) {
|
|
1095
1131
|
// インスタンスを作成
|
|
1096
1132
|
const instance = createInstance(componentId, undefined, contentId);
|
|
1097
1133
|
console.log('[FrontendVisualEditor] Created instance:', instance);
|
|
@@ -1144,7 +1180,7 @@ function FrontendVisualEditorInner({
|
|
|
1144
1180
|
|
|
1145
1181
|
window.addEventListener('message', handleMessage);
|
|
1146
1182
|
return () => window.removeEventListener('message', handleMessage);
|
|
1147
|
-
}, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds]);
|
|
1183
|
+
}, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds, partsMode, materializePartInstance, editorMode]);
|
|
1148
1184
|
|
|
1149
1185
|
// ドラッグ&ドロップハンドラ
|
|
1150
1186
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
@@ -1221,6 +1257,38 @@ function FrontendVisualEditorInner({
|
|
|
1221
1257
|
y = (e.clientY - iframeRect.top) / dropScale;
|
|
1222
1258
|
}
|
|
1223
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
|
+
|
|
1224
1292
|
// インスタンスを作成(バリアントIDが指定されていれば使用)
|
|
1225
1293
|
// Pass position directly to createInstance to avoid React state batching race condition
|
|
1226
1294
|
console.log('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
|
|
@@ -1298,7 +1366,7 @@ function FrontendVisualEditorInner({
|
|
|
1298
1366
|
}
|
|
1299
1367
|
}
|
|
1300
1368
|
}
|
|
1301
|
-
}, [containerRef, uploadFromFiles, createInstance, getMasterComponent, setSelectedElement, contentId, pushHistory, iframeRef, zoom]);
|
|
1369
|
+
}, [containerRef, uploadFromFiles, createInstance, getMasterComponent, setSelectedElement, contentId, pushHistory, iframeRef, zoom, partsMode, materializePartInstance, editorMode, notifyIframeChange, setSelectedElementIds]);
|
|
1302
1370
|
|
|
1303
1371
|
// クリップボードからのペースト処理(画像、HTML、Excel、Word、SVG)
|
|
1304
1372
|
// 同じpasteイベントを二度処理しないための記録。
|
|
@@ -1825,18 +1893,22 @@ function FrontendVisualEditorInner({
|
|
|
1825
1893
|
// ================= UIモード(Figma風 / PowerPoint風) =================
|
|
1826
1894
|
// ベース(キャンバス・選択・保存・書き戻し)は完全共通で、切り替わるのは殻だけ。
|
|
1827
1895
|
// 選択はlocalStorageに記憶する
|
|
1828
|
-
const isPpt = uiMode === 'ppt' && !isMultiPageCanvas;
|
|
1896
|
+
const isPpt = uiMode === 'ppt' && editorMode === 'slide' && !isMultiPageCanvas;
|
|
1829
1897
|
|
|
1830
1898
|
return (
|
|
1899
|
+
<EditorAppearanceContext.Provider value={pptTheme}>
|
|
1831
1900
|
<div
|
|
1901
|
+
data-editor-theme={pptTheme}
|
|
1902
|
+
data-editor-mode={editorMode}
|
|
1832
1903
|
className={cn(
|
|
1833
1904
|
// [移植時の修正] gg-editor-skin でビューアと同じ配色に揃える(editor-skin.css)
|
|
1834
|
-
"gg-editor-skin flex flex-col",
|
|
1905
|
+
"gg-editor-skin gg-editor-ui flex flex-col",
|
|
1835
1906
|
isMultiPageCanvas ? "absolute inset-0 z-40" : "fixed inset-0 z-50"
|
|
1836
1907
|
)}
|
|
1837
1908
|
data-frontend-visual-editor="true"
|
|
1838
|
-
style={{
|
|
1909
|
+
style={{ backgroundColor: 'var(--ed-bg)' }}
|
|
1839
1910
|
>
|
|
1911
|
+
<CanvasAppearance theme={pptTheme} />
|
|
1840
1912
|
{/* ヘッダー(UIモードで切り替え) */}
|
|
1841
1913
|
{isPpt ? (
|
|
1842
1914
|
<>
|
|
@@ -1913,6 +1985,12 @@ function FrontendVisualEditorInner({
|
|
|
1913
1985
|
</>
|
|
1914
1986
|
) : (
|
|
1915
1987
|
<EditorHeader
|
|
1988
|
+
theme={pptTheme}
|
|
1989
|
+
onToggleTheme={togglePptTheme}
|
|
1990
|
+
layersOpen={layersOpen}
|
|
1991
|
+
onToggleLayers={() => setLayersOpen((v) => !v)}
|
|
1992
|
+
guidesOpen={guidesOpen}
|
|
1993
|
+
onToggleGuides={() => setGuidesOpen((v) => !v)}
|
|
1916
1994
|
comments={{
|
|
1917
1995
|
open: pptCommentsOpen,
|
|
1918
1996
|
toggle: () =>
|
|
@@ -1929,13 +2007,13 @@ function FrontendVisualEditorInner({
|
|
|
1929
2007
|
saveStatus={saveStatus}
|
|
1930
2008
|
isCanvasEditing={isMultiPageCanvas}
|
|
1931
2009
|
onImport={() => setIsImportDialogOpen(true)}
|
|
1932
|
-
onCssEdit={handleOpenCssEditor}
|
|
2010
|
+
onCssEdit={can('apiFetch') ? handleOpenCssEditor : undefined}
|
|
1933
2011
|
hasCss={!!importedCss}
|
|
1934
|
-
onJsEdit={handleOpenJsEditor}
|
|
2012
|
+
onJsEdit={can('apiFetch') ? handleOpenJsEditor : undefined}
|
|
1935
2013
|
hasJs={!!importedJs}
|
|
1936
|
-
onPageSettings={handleOpenPageSettings}
|
|
2014
|
+
onPageSettings={can('apiFetch') ? handleOpenPageSettings : undefined}
|
|
1937
2015
|
hasPageSettings={!!(pageSettings.title || pageSettings.description || pageSettings.ogp?.image)}
|
|
1938
|
-
onExport={parentId && contentId ? handleExport : undefined}
|
|
2016
|
+
onExport={can('apiFetch') && parentId && contentId ? handleExport : undefined}
|
|
1939
2017
|
contextNumber={Number(currentContentId ?? contentId) || undefined}
|
|
1940
2018
|
contextTitle={contentList.find((c) => c.id === (currentContentId ?? contentId))?.title}
|
|
1941
2019
|
headerExtra={headerExtra}
|
|
@@ -1951,7 +2029,9 @@ function FrontendVisualEditorInner({
|
|
|
1951
2029
|
) : isMultiPageCanvas ? (
|
|
1952
2030
|
<EditorLayerPanel />
|
|
1953
2031
|
) : (
|
|
1954
|
-
<
|
|
2032
|
+
<div className={layersOpen && !isComponentPanelOpen ? "flex min-h-0" : "hidden"}>
|
|
2033
|
+
<LeftPanel page={Number(currentContentId ?? contentId) || 1} pagesSlot={pagesPanel} />
|
|
2034
|
+
</div>
|
|
1955
2035
|
)}
|
|
1956
2036
|
|
|
1957
2037
|
{/* コンポーネントパネル(左側、レイヤーパネルの隣) */}
|
|
@@ -1989,10 +2069,10 @@ function FrontendVisualEditorInner({
|
|
|
1989
2069
|
<>
|
|
1990
2070
|
<EditorCanvas />
|
|
1991
2071
|
{/* ブレイクポイントガイド(webpageモード用) */}
|
|
1992
|
-
<BreakpointGuides
|
|
2072
|
+
{guidesOpen && <BreakpointGuides
|
|
1993
2073
|
containerWidth={canvasAreaSize.width}
|
|
1994
2074
|
containerHeight={canvasAreaSize.height}
|
|
1995
|
-
/>
|
|
2075
|
+
/>}
|
|
1996
2076
|
</>
|
|
1997
2077
|
)}
|
|
1998
2078
|
|
|
@@ -2046,9 +2126,9 @@ function FrontendVisualEditorInner({
|
|
|
2046
2126
|
onBringToFront={selectedElement ? bringToFront : undefined}
|
|
2047
2127
|
onSendToBack={selectedElement ? sendToBack : undefined}
|
|
2048
2128
|
onImageUpload={() => openFilePicker({ x: 100, y: 100 })}
|
|
2049
|
-
onOpenMediaLibrary={() => setIsMediaLibraryOpen(true)}
|
|
2129
|
+
onOpenMediaLibrary={can('apiFetch') ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2050
2130
|
isMediaReplaceMode={isImageSelected}
|
|
2051
|
-
onAiRegenerate={selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2131
|
+
onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2052
2132
|
onOpenVariables={() => setIsVariablesPanelOpen(true)}
|
|
2053
2133
|
hasVariables={hasVariables}
|
|
2054
2134
|
onOpenComponents={() => setIsComponentPanelOpen(true)}
|
|
@@ -2089,8 +2169,8 @@ function FrontendVisualEditorInner({
|
|
|
2089
2169
|
{/* 右パネルは同時に1つだけ出す。
|
|
2090
2170
|
コメントを開いている間はプロパティ(詳細編集)を引っ込め、閉じると戻る。
|
|
2091
2171
|
2つ並ぶと「いまどちらを操作しているのか」が分からなくなるため */}
|
|
2092
|
-
{!isPpt && !pptCommentsOpen && <EditorPropertyPanel />}
|
|
2093
|
-
{!isMultiPageCanvas && (
|
|
2172
|
+
{!isPpt && !pptCommentsOpen && (editorMode === 'slide' || selectedElement || selectedElementIds.length > 0 || activeTool === 'scale') && <EditorPropertyPanel />}
|
|
2173
|
+
{!isMultiPageCanvas && can('commentAction') && (
|
|
2094
2174
|
<PptCommentMarkers
|
|
2095
2175
|
page={Number(currentContentId ?? contentId) || 1}
|
|
2096
2176
|
onOpenThread={(id) => {
|
|
@@ -2102,23 +2182,25 @@ function FrontendVisualEditorInner({
|
|
|
2102
2182
|
{isPpt && pptFormatPaneOpen && (
|
|
2103
2183
|
<PptFormatPane theme={pptTheme} onClose={() => setPptFormatPaneOpen(false)} />
|
|
2104
2184
|
)}
|
|
2105
|
-
{!isMultiPageCanvas && pptCommentsOpen && (
|
|
2185
|
+
{!isMultiPageCanvas && can('commentAction') && (pptCommentsOpen || commentsMounted) && (
|
|
2186
|
+
<div className={pptCommentsOpen ? 'flex min-h-0' : 'hidden'}>
|
|
2106
2187
|
<PptCommentsPanel
|
|
2107
2188
|
page={Number(currentContentId ?? contentId) || 1}
|
|
2108
|
-
theme={
|
|
2189
|
+
theme={pptTheme}
|
|
2109
2190
|
onClose={() => setPptCommentsOpen(false)}
|
|
2110
2191
|
focusSignal={pptCommentFocus}
|
|
2111
2192
|
activeThreadId={pptActiveThread}
|
|
2112
2193
|
onActiveThread={setPptActiveThread}
|
|
2113
2194
|
/>
|
|
2195
|
+
</div>
|
|
2114
2196
|
)}
|
|
2115
2197
|
</div>
|
|
2116
2198
|
|
|
2117
2199
|
{/* ノート欄(トークスクリプト)。PowerPoint風・Figma風の両方に出す。
|
|
2118
2200
|
Figma風はダーク配色で固定(スキンと馴染む) */}
|
|
2119
2201
|
{/* ノート欄は発表原稿。Webページには無い概念なので出さない */}
|
|
2120
|
-
{!isMultiPageCanvas && editorMode !== 'webpage' && (
|
|
2121
|
-
<PptNotes page={Number(currentContentId ?? contentId) || 1} theme={
|
|
2202
|
+
{!isMultiPageCanvas && editorMode !== 'webpage' && can('apiFetch') && (
|
|
2203
|
+
<PptNotes page={Number(currentContentId ?? contentId) || 1} theme={pptTheme} />
|
|
2122
2204
|
)}
|
|
2123
2205
|
|
|
2124
2206
|
{/* フッター(Figma風) / ステータスバー(PowerPoint風) */}
|
|
@@ -2138,13 +2220,18 @@ function FrontendVisualEditorInner({
|
|
|
2138
2220
|
hasStyleInClipboard={hasStyleInClipboard()}
|
|
2139
2221
|
isComponentInstance={isComponentInstance}
|
|
2140
2222
|
hasOverrides={hasOverrides}
|
|
2223
|
+
partsMode={partsMode}
|
|
2224
|
+
isPartInstance={isPartInstance}
|
|
2225
|
+
partLabel={partLabel}
|
|
2226
|
+
onDetachPart={handleDetachPart}
|
|
2227
|
+
onUpdatePart={can('savePart') ? handleUpdatePart : undefined}
|
|
2141
2228
|
onCopy={selectedElement ? copyElements : undefined}
|
|
2142
2229
|
onCut={selectedElement ? cutElements : undefined}
|
|
2143
2230
|
onPaste={pasteElements}
|
|
2144
2231
|
onDelete={selectedElement ? deleteElement : undefined}
|
|
2145
2232
|
onDuplicate={selectedElement ? duplicateElement : undefined}
|
|
2146
2233
|
onReplaceImageFromFile={isImageSelected ? replaceImageFromFile : undefined}
|
|
2147
|
-
onReplaceImageFromLibrary={isImageSelected ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2234
|
+
onReplaceImageFromLibrary={can('apiFetch') && isImageSelected ? () => setIsMediaLibraryOpen(true) : undefined}
|
|
2148
2235
|
onReplaceImageFromClipboard={isImageSelected ? () => void replaceImageFromClipboard() : undefined}
|
|
2149
2236
|
onCopyStyle={selectedElement ? copyStyle : undefined}
|
|
2150
2237
|
onPasteStyle={selectedElement ? pasteStyle : undefined}
|
|
@@ -2161,7 +2248,7 @@ function FrontendVisualEditorInner({
|
|
|
2161
2248
|
onDeleteRow={tableActions?.deleteRow}
|
|
2162
2249
|
onDeleteColumn={tableActions?.deleteColumn}
|
|
2163
2250
|
onEditLink={selectedElement ? handleEditLink : undefined}
|
|
2164
|
-
onAiRegenerate={selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2251
|
+
onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
|
|
2165
2252
|
onEditHtml={selectedElement ? handleEditHtml : undefined}
|
|
2166
2253
|
onCreateComponent={handleCreateComponent}
|
|
2167
2254
|
onGoToMainComponent={handleGoToMainComponent}
|
|
@@ -2181,6 +2268,7 @@ function FrontendVisualEditorInner({
|
|
|
2181
2268
|
/>
|
|
2182
2269
|
|
|
2183
2270
|
{/* HTML編集ダイアログ */}
|
|
2271
|
+
{htmlEditorState.isOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2184
2272
|
<HtmlEditorDialog
|
|
2185
2273
|
isOpen={htmlEditorState.isOpen}
|
|
2186
2274
|
onClose={() => {
|
|
@@ -2193,8 +2281,10 @@ function FrontendVisualEditorInner({
|
|
|
2193
2281
|
onSave={handleSaveHtml}
|
|
2194
2282
|
initialHtml={htmlEditorState.initialHtml}
|
|
2195
2283
|
/>
|
|
2284
|
+
</Suspense>}
|
|
2196
2285
|
|
|
2197
2286
|
{/* HTMLインポートダイアログ */}
|
|
2287
|
+
{isImportDialogOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2198
2288
|
<HtmlImportDialog
|
|
2199
2289
|
isOpen={isImportDialogOpen}
|
|
2200
2290
|
onClose={() => {
|
|
@@ -2208,8 +2298,10 @@ function FrontendVisualEditorInner({
|
|
|
2208
2298
|
presentationId={parentId}
|
|
2209
2299
|
slideId={contentId}
|
|
2210
2300
|
/>
|
|
2301
|
+
</Suspense>}
|
|
2211
2302
|
|
|
2212
2303
|
{/* CSS編集ダイアログ */}
|
|
2304
|
+
{isCssEditorOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2213
2305
|
<CssEditorDialog
|
|
2214
2306
|
isOpen={isCssEditorOpen}
|
|
2215
2307
|
onClose={() => {
|
|
@@ -2223,8 +2315,10 @@ function FrontendVisualEditorInner({
|
|
|
2223
2315
|
onSave={handleSaveCss}
|
|
2224
2316
|
onClear={handleClearCss}
|
|
2225
2317
|
/>
|
|
2318
|
+
</Suspense>}
|
|
2226
2319
|
|
|
2227
2320
|
{/* JS編集ダイアログ */}
|
|
2321
|
+
{isJsEditorOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2228
2322
|
<JsEditorDialog
|
|
2229
2323
|
isOpen={isJsEditorOpen}
|
|
2230
2324
|
onClose={() => {
|
|
@@ -2238,8 +2332,10 @@ function FrontendVisualEditorInner({
|
|
|
2238
2332
|
onSave={handleSaveJs}
|
|
2239
2333
|
onClear={handleClearJs}
|
|
2240
2334
|
/>
|
|
2335
|
+
</Suspense>}
|
|
2241
2336
|
|
|
2242
2337
|
{/* ページ設定ダイアログ */}
|
|
2338
|
+
{isPageSettingsOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2243
2339
|
<PageSettingsDialog
|
|
2244
2340
|
isOpen={isPageSettingsOpen}
|
|
2245
2341
|
onClose={() => {
|
|
@@ -2254,8 +2350,10 @@ function FrontendVisualEditorInner({
|
|
|
2254
2350
|
onSave={handleSavePageSettings}
|
|
2255
2351
|
onUploadImage={handleUploadOgpImage}
|
|
2256
2352
|
/>
|
|
2353
|
+
</Suspense>}
|
|
2257
2354
|
|
|
2258
2355
|
{/* メディアライブラリ */}
|
|
2356
|
+
{isMediaLibraryOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2259
2357
|
<MediaLibraryDialog
|
|
2260
2358
|
isOpen={isMediaLibraryOpen}
|
|
2261
2359
|
onClose={() => {
|
|
@@ -2269,6 +2367,7 @@ function FrontendVisualEditorInner({
|
|
|
2269
2367
|
mode={isImageSelected ? 'replace' : 'insert'}
|
|
2270
2368
|
currentSrc={selectedImageSrc}
|
|
2271
2369
|
/>
|
|
2370
|
+
</Suspense>}
|
|
2272
2371
|
|
|
2273
2372
|
{/* CSS変数パネル */}
|
|
2274
2373
|
<VariablesPanel
|
|
@@ -2293,7 +2392,7 @@ function FrontendVisualEditorInner({
|
|
|
2293
2392
|
<Dialog open={createComponentDialogOpen} onOpenChange={setCreateComponentDialogOpen}>
|
|
2294
2393
|
<DialogContent className="bg-[#2c2c2c] border-[#444444] text-white">
|
|
2295
2394
|
<DialogHeader>
|
|
2296
|
-
<DialogTitle
|
|
2395
|
+
<DialogTitle>{partsMode ? '部品として保存' : 'コンポーネントを作成'}</DialogTitle>
|
|
2297
2396
|
</DialogHeader>
|
|
2298
2397
|
<div className="space-y-4 py-4">
|
|
2299
2398
|
<div className="space-y-2">
|
|
@@ -2334,6 +2433,7 @@ function FrontendVisualEditorInner({
|
|
|
2334
2433
|
</Dialog>
|
|
2335
2434
|
|
|
2336
2435
|
{/* マスターコンポーネントエディタ */}
|
|
2436
|
+
{isMasterEditorOpen && <Suspense fallback={<div role="status" className="absolute bottom-16 left-4 rounded bg-[#2c2c2c] p-3">読み込み中…</div>}>
|
|
2337
2437
|
<MasterComponentEditor
|
|
2338
2438
|
master={editingMasterComponent}
|
|
2339
2439
|
open={isMasterEditorOpen}
|
|
@@ -2351,6 +2451,7 @@ function FrontendVisualEditorInner({
|
|
|
2351
2451
|
name: c.name,
|
|
2352
2452
|
}))}
|
|
2353
2453
|
/>
|
|
2454
|
+
</Suspense>}
|
|
2354
2455
|
|
|
2355
2456
|
{/* レイアウトモード切替ヒントトースト。
|
|
2356
2457
|
Webページでは絶対配置に倒さないので、それを勧めるこの導線も出さない */}
|
|
@@ -2417,6 +2518,7 @@ function FrontendVisualEditorInner({
|
|
|
2417
2518
|
</div>
|
|
2418
2519
|
)}
|
|
2419
2520
|
</div>
|
|
2521
|
+
</EditorAppearanceContext.Provider>
|
|
2420
2522
|
);
|
|
2421
2523
|
}
|
|
2422
2524
|
|
|
@@ -2462,9 +2564,11 @@ export function FrontendVisualEditor({
|
|
|
2462
2564
|
const [initialVariables, setInitialVariables] = useState<CSSVariableDefinition[]>([]);
|
|
2463
2565
|
const [isVariablesLoaded, setIsVariablesLoaded] = useState(false);
|
|
2464
2566
|
|
|
2465
|
-
// CSS変数をロード(webpageモード・slide
|
|
2567
|
+
// CSS変数をロード(webpageモード・slideモード両対応)。
|
|
2568
|
+
// 利用側が io.loadVariables を渡していればそれ(ファイル等)から、無ければブラウザ内(localStorage)から
|
|
2466
2569
|
useEffect(() => {
|
|
2467
|
-
|
|
2570
|
+
const fromIo = io().loadVariables;
|
|
2571
|
+
if (!effectiveParentId && !fromIo) {
|
|
2468
2572
|
setIsVariablesLoaded(true);
|
|
2469
2573
|
return;
|
|
2470
2574
|
}
|
|
@@ -2473,9 +2577,11 @@ export function FrontendVisualEditor({
|
|
|
2473
2577
|
try {
|
|
2474
2578
|
// editorModeに応じてスコープを決定
|
|
2475
2579
|
const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
|
|
2476
|
-
const variables =
|
|
2580
|
+
const variables = fromIo
|
|
2581
|
+
? await fromIo()
|
|
2582
|
+
: await getCSSVariablesList(effectiveParentId!, scope);
|
|
2477
2583
|
setInitialVariables(variables);
|
|
2478
|
-
console.log(`[FrontendVisualEditor] Loaded CSS variables (${scope}):`, variables.length);
|
|
2584
|
+
console.log(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
|
|
2479
2585
|
} catch (error) {
|
|
2480
2586
|
console.error('[FrontendVisualEditor] Failed to load CSS variables:', error);
|
|
2481
2587
|
} finally {
|
|
@@ -2488,6 +2594,12 @@ export function FrontendVisualEditor({
|
|
|
2488
2594
|
|
|
2489
2595
|
// CSS変数を保存するコールバック
|
|
2490
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
|
+
}
|
|
2491
2603
|
if (!effectiveParentId) {
|
|
2492
2604
|
throw new Error('Parent ID is required to save CSS variables');
|
|
2493
2605
|
}
|
|
@@ -2499,6 +2611,8 @@ export function FrontendVisualEditor({
|
|
|
2499
2611
|
|
|
2500
2612
|
// CSS変数をロードするコールバック
|
|
2501
2613
|
const handleLoadVariables = useCallback(async (resourceId: string) => {
|
|
2614
|
+
const fromIo = io().loadVariables;
|
|
2615
|
+
if (fromIo) return fromIo();
|
|
2502
2616
|
// editorModeに応じてスコープを決定
|
|
2503
2617
|
const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
|
|
2504
2618
|
return getCSSVariablesList(resourceId, scope);
|
|
@@ -978,7 +978,7 @@ export function EditorCanvas() {
|
|
|
978
978
|
ref={containerRef}
|
|
979
979
|
className="h-full w-full"
|
|
980
980
|
style={{
|
|
981
|
-
backgroundColor: isInMultiPageMode ? "transparent" : "
|
|
981
|
+
backgroundColor: isInMultiPageMode ? "transparent" : "var(--ed-bg)",
|
|
982
982
|
}}
|
|
983
983
|
>
|
|
984
984
|
<iframe
|