@growgroup/visual-editor 0.3.1 → 0.4.1
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 +6 -1
- package/dist/editor.css +1 -1
- package/package.json +1 -1
- package/src/editor/EditorContext.tsx +3 -2
- package/src/editor/EditorToolbar.tsx +8 -2
- package/src/editor/FrontendVisualEditor.tsx +123 -44
- package/src/editor/components/EditorCanvas.tsx +73 -15
- package/src/editor/components/EditorFooter.tsx +2 -0
- package/src/editor/components/EditorHeader.tsx +3 -2
- package/src/editor/components/FigmaColorPicker.tsx +2 -1
- package/src/editor/components/JsEditorDialog.tsx +1 -0
- package/src/editor/components/PageSettingsDialog.tsx +1 -0
- package/src/editor/components/comments/CommentBoard.tsx +9 -6
- package/src/editor/components/multi-page/CanvasFrameDecor.tsx +172 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +179 -77
- package/src/editor/components/multi-page/PageFramePreview.tsx +99 -21
- package/src/editor/components/multi-page/index.ts +1 -0
- package/src/editor/components/ppt/PptChrome.tsx +6 -2
- package/src/editor/components/ppt/PptComments.tsx +411 -71
- package/src/editor/constants.ts +16 -1
- package/src/editor/contexts/EditorArtboardContext.tsx +8 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +31 -27
- package/src/editor/contexts/EditorUIStateContext.tsx +4 -3
- package/src/editor/contexts/EditorVariablesContext.tsx +5 -4
- package/src/editor/contexts/MultiPageCanvasContext.tsx +194 -26
- package/src/editor/hooks/useAiReplace.ts +4 -3
- package/src/editor/hooks/useBrowserZoomPrevention.ts +2 -1
- package/src/editor/hooks/useComponentEditMode.ts +27 -26
- package/src/editor/hooks/useEditorMessages.ts +3 -2
- package/src/editor/hooks/useElementActions.ts +27 -26
- package/src/editor/hooks/useElementSelection.ts +19 -16
- package/src/editor/hooks/useFocusManagement.ts +7 -6
- package/src/editor/hooks/useGoogleFonts.ts +3 -2
- package/src/editor/hooks/useIframeInitializer.ts +6 -3
- package/src/editor/hooks/useIframeSetup.ts +4 -3
- package/src/editor/hooks/useImageUpload.ts +4 -3
- package/src/editor/hooks/useMarqueeSelection.ts +2 -1
- package/src/editor/hooks/usePageSettingsManager.ts +24 -23
- package/src/editor/hooks/useRichPaste.ts +11 -10
- package/src/editor/parts.ts +7 -25
- package/src/editor/utils/component-renderer.ts +8 -7
- package/src/editor/utils/component-sync.ts +16 -15
- package/src/editor/utils/debug.ts +52 -0
- package/src/editor/utils/dom-utils.ts +43 -11
- package/src/editor/utils/drop-target.ts +135 -0
- package/src/editor/utils/eyedropper.ts +3 -1
- package/src/editor/utils/figma-export.ts +6 -5
- package/src/editor/utils/figma-kiwi-decoder.ts +28 -27
- package/src/editor/utils/figma-kiwi-encoder.ts +9 -8
- package/src/editor/utils/figma-paste.ts +57 -56
- package/src/editor/utils/flex-utils.ts +12 -10
- package/src/editor/utils/override-apply.ts +3 -2
- package/src/editor/utils/override-detection.ts +11 -10
- package/src/editor/utils/tailwind-utils.ts +4 -3
- package/src/io.ts +22 -1
- package/src/lib/firebase/css-variables.ts +4 -3
- package/src/lib/firebase/storage.ts +3 -2
- package/src/styles/skin.css +9 -4
- package/src/types/editor.ts +6 -2
- package/src/version.ts +1 -1
|
@@ -38,6 +38,14 @@ export interface ContentListItem {
|
|
|
38
38
|
parentId?: string | null;
|
|
39
39
|
/** 編集しない表示(ページそのもの・ビューア)の URL。渡すとフレーム名と一覧に「別タブで開く」が付く */
|
|
40
40
|
href?: string;
|
|
41
|
+
/**
|
|
42
|
+
* ページ全体を写した画像(PNG / JPEG)の URL。幅は 480px 以上、縦横比はページと同じ。
|
|
43
|
+
* マルチフレームのキャンバスが大きく縮小されている間(倍率 0.5 未満)は、見るだけの紙面を
|
|
44
|
+
* srcdoc の iframe ではなくこの画像で描く(26 枚の iframe をラスタライズし直さずに済む)。
|
|
45
|
+
* 内容が変わったら URL も変える(`&v=…`)。変わると紙面が読み直す。
|
|
46
|
+
* 渡さないページ・画像が読めなかったページは、今までどおり iframe で描く
|
|
47
|
+
*/
|
|
48
|
+
thumbnail?: string;
|
|
41
49
|
/**
|
|
42
50
|
* 本文の版。利用側でファイルが外から変わったとき(部品の一括反映・CLI・他の人の編集)に
|
|
43
51
|
* 数を進めると、キャンバスの見るだけの紙面がそのページを読み直す。編集中のページには効かない
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { io } from '../../io';
|
|
47
47
|
import type { EditorPartDef, EditorPartsLibrary } from '../../io';
|
|
48
48
|
import { useMultiPageCanvasOptional } from './MultiPageCanvasContext';
|
|
49
|
+
import { debugLog, isDebugEnabled } from '../utils/debug';
|
|
49
50
|
import {
|
|
50
51
|
materializePart,
|
|
51
52
|
partDefFromElement,
|
|
@@ -211,7 +212,7 @@ export function htmlElementToComponentElement(element: HTMLElement, isRoot: bool
|
|
|
211
212
|
if (key && value) {
|
|
212
213
|
// Skip position-related styles for root element
|
|
213
214
|
if (isRoot && POSITION_STYLES.has(key)) {
|
|
214
|
-
|
|
215
|
+
debugLog(`[htmlElementToComponentElement] Skipping position style for root: ${key}`);
|
|
215
216
|
continue;
|
|
216
217
|
}
|
|
217
218
|
// Convert kebab-case to camelCase
|
|
@@ -238,7 +239,7 @@ export function htmlElementToComponentElement(element: HTMLElement, isRoot: bool
|
|
|
238
239
|
if (hasMixedContent) {
|
|
239
240
|
// Mixed content: use innerHTML to preserve all content
|
|
240
241
|
innerHTML = element.innerHTML;
|
|
241
|
-
|
|
242
|
+
debugLog('[htmlElementToComponentElement] Mixed content detected, using innerHTML');
|
|
242
243
|
} else if (hasElementChildren) {
|
|
243
244
|
// Only element children: recursively process (not root)
|
|
244
245
|
for (const child of Array.from(element.children)) {
|
|
@@ -261,17 +262,20 @@ export function htmlElementToComponentElement(element: HTMLElement, isRoot: bool
|
|
|
261
262
|
overridable: { ...defaultOverridable },
|
|
262
263
|
};
|
|
263
264
|
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
265
|
+
// 要素 1 つにつき 1 行出る。部品 89 個 + 26 ページで 2,000 件を超えるので、
|
|
266
|
+
// 出さないときは引数のオブジェクトを組み立てること自体をやめる
|
|
267
|
+
if (isDebugEnabled()) {
|
|
268
|
+
debugLog('[htmlElementToComponentElement] Converted:', {
|
|
269
|
+
tagName: result.tagName,
|
|
270
|
+
className: result.className,
|
|
271
|
+
hasTextContent: !!result.textContent,
|
|
272
|
+
hasInnerHTML: !!result.innerHTML,
|
|
273
|
+
textContentPreview: result.textContent?.substring(0, 50),
|
|
274
|
+
innerHTMLPreview: result.innerHTML?.substring(0, 50),
|
|
275
|
+
childrenCount: result.children.length,
|
|
276
|
+
stylesCount: Object.keys(result.styles).length,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
275
279
|
|
|
276
280
|
return result;
|
|
277
281
|
}
|
|
@@ -586,7 +590,7 @@ export function EditorComponentsProvider({
|
|
|
586
590
|
setIsLoadingComponents(true);
|
|
587
591
|
setError(null);
|
|
588
592
|
try {
|
|
589
|
-
|
|
593
|
+
debugLog(
|
|
590
594
|
`[EditorComponentsContext] Loading components for website: ${targetWebsiteId}`
|
|
591
595
|
);
|
|
592
596
|
|
|
@@ -610,13 +614,13 @@ export function EditorComponentsProvider({
|
|
|
610
614
|
setMasterComponents(componentsMap);
|
|
611
615
|
setComponentLibrary(categoriesFromParts(lib, componentsMap));
|
|
612
616
|
setComponentInstances(new Map());
|
|
613
|
-
|
|
617
|
+
debugLog(`[EditorComponentsContext] Loaded ${defs.size} parts`);
|
|
614
618
|
return;
|
|
615
619
|
}
|
|
616
620
|
|
|
617
621
|
// Load master components from Firestore
|
|
618
622
|
const components = await getAllMasterComponents(targetWebsiteId);
|
|
619
|
-
|
|
623
|
+
debugLog(`[EditorComponentsContext] Loaded ${components.length} components`);
|
|
620
624
|
|
|
621
625
|
// Create Map from components
|
|
622
626
|
const componentsMap = new Map<string, MasterComponent>();
|
|
@@ -689,9 +693,9 @@ export function EditorComponentsProvider({
|
|
|
689
693
|
|
|
690
694
|
const loadInstances = async () => {
|
|
691
695
|
try {
|
|
692
|
-
|
|
696
|
+
debugLog(`[EditorComponentsContext] Loading instances for page: ${pageId}`);
|
|
693
697
|
const instances = await getPageComponentInstances(websiteId, pageId);
|
|
694
|
-
|
|
698
|
+
debugLog(`[EditorComponentsContext] Loaded ${instances.length} instances`);
|
|
695
699
|
|
|
696
700
|
// Create Map from instances (keyed by domElementId)
|
|
697
701
|
const instancesMap = new Map<string, ComponentInstance>();
|
|
@@ -766,7 +770,7 @@ export function EditorComponentsProvider({
|
|
|
766
770
|
});
|
|
767
771
|
}
|
|
768
772
|
|
|
769
|
-
|
|
773
|
+
debugLog(
|
|
770
774
|
`[EditorComponentsContext] Created master component: ${name} (${id})`
|
|
771
775
|
);
|
|
772
776
|
return masterComponent;
|
|
@@ -996,7 +1000,7 @@ export function EditorComponentsProvider({
|
|
|
996
1000
|
});
|
|
997
1001
|
}
|
|
998
1002
|
|
|
999
|
-
|
|
1003
|
+
debugLog(`[EditorComponentsContext] Deleted master component: ${id}`);
|
|
1000
1004
|
}, [websiteId, partsMode]);
|
|
1001
1005
|
|
|
1002
1006
|
const getMasterComponent = useCallback(
|
|
@@ -1069,7 +1073,7 @@ export function EditorComponentsProvider({
|
|
|
1069
1073
|
});
|
|
1070
1074
|
}
|
|
1071
1075
|
|
|
1072
|
-
|
|
1076
|
+
debugLog(
|
|
1073
1077
|
`[EditorComponentsContext] Created instance: ${id} of master ${masterComponentId}`
|
|
1074
1078
|
);
|
|
1075
1079
|
return instance;
|
|
@@ -1157,7 +1161,7 @@ export function EditorComponentsProvider({
|
|
|
1157
1161
|
return next;
|
|
1158
1162
|
});
|
|
1159
1163
|
|
|
1160
|
-
|
|
1164
|
+
debugLog(`[EditorComponentsContext] Deleted instance: ${instanceId}`);
|
|
1161
1165
|
}, [websiteId, pageId]);
|
|
1162
1166
|
|
|
1163
1167
|
const getInstanceByDomId = useCallback(
|
|
@@ -1219,7 +1223,7 @@ export function EditorComponentsProvider({
|
|
|
1219
1223
|
return next;
|
|
1220
1224
|
});
|
|
1221
1225
|
|
|
1222
|
-
|
|
1226
|
+
debugLog(
|
|
1223
1227
|
`[EditorComponentsContext] Added override to instance: ${instanceId}`
|
|
1224
1228
|
);
|
|
1225
1229
|
},
|
|
@@ -1269,7 +1273,7 @@ export function EditorComponentsProvider({
|
|
|
1269
1273
|
return next;
|
|
1270
1274
|
});
|
|
1271
1275
|
|
|
1272
|
-
|
|
1276
|
+
debugLog(
|
|
1273
1277
|
`[EditorComponentsContext] Removed override ${overrideId} from instance: ${instanceId}`
|
|
1274
1278
|
);
|
|
1275
1279
|
},
|
|
@@ -1321,7 +1325,7 @@ export function EditorComponentsProvider({
|
|
|
1321
1325
|
return next;
|
|
1322
1326
|
});
|
|
1323
1327
|
|
|
1324
|
-
|
|
1328
|
+
debugLog(
|
|
1325
1329
|
`[EditorComponentsContext] Reset all overrides for instance: ${instanceId}`
|
|
1326
1330
|
);
|
|
1327
1331
|
}, [websiteId, pageId]);
|
|
@@ -1394,7 +1398,7 @@ export function EditorComponentsProvider({
|
|
|
1394
1398
|
return next;
|
|
1395
1399
|
});
|
|
1396
1400
|
|
|
1397
|
-
|
|
1401
|
+
debugLog(
|
|
1398
1402
|
`[EditorComponentsContext] Changed variant for instance ${instanceId} to ${variantId}`
|
|
1399
1403
|
);
|
|
1400
1404
|
},
|
|
@@ -1458,7 +1462,7 @@ export function EditorComponentsProvider({
|
|
|
1458
1462
|
});
|
|
1459
1463
|
}
|
|
1460
1464
|
|
|
1461
|
-
|
|
1465
|
+
debugLog(
|
|
1462
1466
|
`[EditorComponentsContext] Detached instance: ${instanceId}`
|
|
1463
1467
|
);
|
|
1464
1468
|
return htmlElement;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
19
19
|
import type { DOMTreeNode, PanelSections, MarqueeState } from '../types';
|
|
20
20
|
import { DEFAULT_PANEL_SECTIONS } from '../constants';
|
|
21
|
+
import { debugLog } from '../utils/debug';
|
|
21
22
|
|
|
22
23
|
export interface EditorUIStateContextValue {
|
|
23
24
|
// DOMツリー
|
|
@@ -81,7 +82,7 @@ export function EditorUIStateProvider({
|
|
|
81
82
|
const restoreFocus = useCallback(() => {
|
|
82
83
|
const iframeDoc = getIframeDoc();
|
|
83
84
|
if (!iframeDoc) {
|
|
84
|
-
|
|
85
|
+
debugLog('[EditorUIStateContext] restoreFocus: iframe doc not available');
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -89,14 +90,14 @@ export function EditorUIStateProvider({
|
|
|
89
90
|
const editingElement = iframeDoc.querySelector('[contenteditable="true"]') as HTMLElement;
|
|
90
91
|
if (editingElement) {
|
|
91
92
|
editingElement.focus();
|
|
92
|
-
|
|
93
|
+
debugLog('[EditorUIStateContext] restoreFocus: focused editing element');
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
// iframeのbodyにフォーカスを設定
|
|
97
98
|
if (iframeDoc.body) {
|
|
98
99
|
iframeDoc.body.focus();
|
|
99
|
-
|
|
100
|
+
debugLog('[EditorUIStateContext] restoreFocus: focused iframe body');
|
|
100
101
|
}
|
|
101
102
|
}, [getIframeDoc]);
|
|
102
103
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from 'react';
|
|
25
|
+
import { debugLog } from '../utils/debug';
|
|
25
26
|
import type {
|
|
26
27
|
CSSVariableDefinition,
|
|
27
28
|
CSSVariableCategory,
|
|
@@ -264,7 +265,7 @@ export function EditorVariablesProvider({
|
|
|
264
265
|
await onSave(variables);
|
|
265
266
|
savedVariablesRef.current = [...variables];
|
|
266
267
|
setHasChanges(false);
|
|
267
|
-
|
|
268
|
+
debugLog('[EditorVariablesContext] Variables saved successfully');
|
|
268
269
|
} catch (error) {
|
|
269
270
|
console.error('[EditorVariablesContext] Failed to save variables:', error);
|
|
270
271
|
throw error;
|
|
@@ -286,7 +287,7 @@ export function EditorVariablesProvider({
|
|
|
286
287
|
setVariablesState(loaded);
|
|
287
288
|
savedVariablesRef.current = [...loaded];
|
|
288
289
|
setHasChanges(false);
|
|
289
|
-
|
|
290
|
+
debugLog('[EditorVariablesContext] Variables loaded:', loaded.length);
|
|
290
291
|
} catch (error) {
|
|
291
292
|
console.error('[EditorVariablesContext] Failed to load variables:', error);
|
|
292
293
|
throw error;
|
|
@@ -299,7 +300,7 @@ export function EditorVariablesProvider({
|
|
|
299
300
|
const syncFromTheme = useCallback((themeVariables: CSSVariableDefinition[]) => {
|
|
300
301
|
setVariablesState(themeVariables);
|
|
301
302
|
markAsChanged();
|
|
302
|
-
|
|
303
|
+
debugLog('[EditorVariablesContext] Synced from theme:', themeVariables.length, 'variables');
|
|
303
304
|
}, [markAsChanged]);
|
|
304
305
|
|
|
305
306
|
// バルク設定
|
|
@@ -327,7 +328,7 @@ export function EditorVariablesProvider({
|
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
styleEl.textContent = cssString;
|
|
330
|
-
|
|
331
|
+
debugLog('[EditorVariablesContext] Injected CSS variables to iframe');
|
|
331
332
|
}, [cssString]);
|
|
332
333
|
|
|
333
334
|
// Context値
|
|
@@ -56,12 +56,44 @@ export const MAX_ZOOM = 4;
|
|
|
56
56
|
const ZOOM_STEP = 1.25;
|
|
57
57
|
/** 全体表示・ページ表示のときの余白(px、画面上) */
|
|
58
58
|
const FIT_PADDING = 72;
|
|
59
|
+
/** 画面の縁からこれだけ内側にあれば「見えている」とする(px、画面上) */
|
|
60
|
+
const REVEAL_MARGIN = 48;
|
|
61
|
+
/**
|
|
62
|
+
* ホスト発の切替で「そのページが見えている」と認めるフレームの画面上の幅(px)。
|
|
63
|
+
* 全体表示(2%)だと 1920 の紙面が 38px の点にしかならず、画面には入っていても
|
|
64
|
+
* どこへ移ったのか分からない。これを下回るならページを画面に合わせて寄る
|
|
65
|
+
*/
|
|
66
|
+
const FOCUS_MIN_WIDTH = 400;
|
|
59
67
|
/** 上と左の定規の太さ(px)。全体表示の余白に足す */
|
|
60
68
|
export const RULER_SIZE = 20;
|
|
61
69
|
/** ビューのアニメーション(ms) */
|
|
62
70
|
const VIEW_ANIMATION_MS = 240;
|
|
63
71
|
/** 見るだけの紙面を同時に読む枚数 */
|
|
64
72
|
const PREVIEW_PARALLEL = 4;
|
|
73
|
+
/**
|
|
74
|
+
* 見るだけの紙面を画像で描く倍率の **上限の最大値**。これ以上に拡大されていれば、
|
|
75
|
+
* どんなに解像度の高い画像でも iframe で描く(画像は文字を選べない・検索できないため)。
|
|
76
|
+
* 実際の上限はこれより小さくなることがある ── getPreviewImageZoomCap() を見ること
|
|
77
|
+
*/
|
|
78
|
+
export const PREVIEW_IMAGE_ZOOM = 0.5;
|
|
79
|
+
/** 画像 → iframe に戻す倍率(上限の最大値のとき)。上限に対する比は下の HYSTERESIS */
|
|
80
|
+
export const PREVIEW_IMAGE_ZOOM_OUT = 0.4;
|
|
81
|
+
/**
|
|
82
|
+
* 画像を引き伸ばしてよい上限。1.25 = 画面の画素に対して 25% まで。
|
|
83
|
+
* これを超えると Retina(DPR 2)で文字がにじんで見える(実測: 960px の画像を
|
|
84
|
+
* 1766 device px の枠に出すと 1.84 倍で、同じ倍率の iframe と並べて粗さが分かる)
|
|
85
|
+
*/
|
|
86
|
+
export const PREVIEW_IMAGE_MAX_STRETCH = 1.25;
|
|
87
|
+
/**
|
|
88
|
+
* 画像の実寸がまだ 1 枚も分からないときに仮定する「画像の幅 ÷ 紙面の幅」。
|
|
89
|
+
* 0.5 = 1920 の紙面に 960px の画像(殻の既定)
|
|
90
|
+
*/
|
|
91
|
+
const PREVIEW_IMAGE_PRESUMED_RATIO = 0.5;
|
|
92
|
+
/**
|
|
93
|
+
* 上限に対する「画像に落とす」側の比(ヒステリシス)。
|
|
94
|
+
* 画像 → iframe は上限以上、iframe → 画像は上限 × これ未満。境目で行き来しないため
|
|
95
|
+
*/
|
|
96
|
+
export const PREVIEW_IMAGE_ZOOM_HYSTERESIS = PREVIEW_IMAGE_ZOOM_OUT / PREVIEW_IMAGE_ZOOM;
|
|
65
97
|
|
|
66
98
|
/** フレームどうしの間隔(紙面の座標系、等倍のとき)。フレーム名を出すかの判定にも使う */
|
|
67
99
|
export const FRAME_GAP: Record<EditorMode, { x: number; y: number }> = {
|
|
@@ -99,6 +131,8 @@ export interface PageFrame {
|
|
|
99
131
|
depth: number;
|
|
100
132
|
/** 編集しない表示の URL(contentList[].href) */
|
|
101
133
|
href?: string;
|
|
134
|
+
/** ページ全体を写した画像の URL(contentList[].thumbnail)。大きく縮小した間だけ紙面の代わりに出す */
|
|
135
|
+
thumbnail?: string;
|
|
102
136
|
/** 利用側が進める本文の版(contentList[].revision)。変わったら読み直す */
|
|
103
137
|
revision: number;
|
|
104
138
|
/**
|
|
@@ -217,6 +251,19 @@ export interface MultiPageCanvasContextValue {
|
|
|
217
251
|
*/
|
|
218
252
|
requestPreviewSlot: (id: string, grant: () => void) => void;
|
|
219
253
|
releasePreviewSlot: (id: string) => void;
|
|
254
|
+
|
|
255
|
+
// ---- 紙面の画像(contentList[].thumbnail)
|
|
256
|
+
/**
|
|
257
|
+
* 紙面を画像で描いてよい倍率の上限。画像の解像度と devicePixelRatio から決める
|
|
258
|
+
* (= min(PREVIEW_IMAGE_ZOOM, 画像の幅 × 1.25 ÷ (紙面の幅 × DPR)))。
|
|
259
|
+
* 上限はキャンバス全体で 1 つで、報告された中でいちばん粗い画像に合わせる。
|
|
260
|
+
* DPR は呼ぶたびに読む(別のディスプレイへ移すと変わる)
|
|
261
|
+
*/
|
|
262
|
+
getPreviewImageZoomCap: () => number;
|
|
263
|
+
/** 画像の実寸(naturalWidth)を報告する。PageFramePreview が onLoad で呼ぶ */
|
|
264
|
+
reportThumbnailWidth: (id: string, naturalWidth: number) => void;
|
|
265
|
+
/** 上限が変わった回数。React 側が判定をやり直すきっかけに使う */
|
|
266
|
+
previewImageCapVersion: number;
|
|
220
267
|
}
|
|
221
268
|
|
|
222
269
|
// ============================================================
|
|
@@ -244,6 +291,17 @@ export function useCanvasViewState(): CanvasViewState {
|
|
|
244
291
|
return useSyncExternalStore(viewStore.subscribe, viewStore.get, viewStore.get);
|
|
245
292
|
}
|
|
246
293
|
|
|
294
|
+
/**
|
|
295
|
+
* 編集中のページだけを購読する。
|
|
296
|
+
* useCanvasViewState() だと倍率・位置が変わるたびに描き直る(ホイール 1 段ごとに
|
|
297
|
+
* キャンバス全体が React を通る)。編集中のページしか要らない側はこちらを使う
|
|
298
|
+
*/
|
|
299
|
+
export function useCanvasActivePageId(): string | null {
|
|
300
|
+
const { viewStore } = useMultiPageCanvas();
|
|
301
|
+
const getSnapshot = useCallback(() => viewStore.get().activePageId, [viewStore]);
|
|
302
|
+
return useSyncExternalStore(viewStore.subscribe, getSnapshot, getSnapshot);
|
|
303
|
+
}
|
|
304
|
+
|
|
247
305
|
const NULL_VIEW_STORE: CanvasViewStore = {
|
|
248
306
|
get: () => NULL_VIEW,
|
|
249
307
|
set: () => undefined,
|
|
@@ -487,6 +545,7 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
487
545
|
const depth = depths.get(content.id) ?? 0;
|
|
488
546
|
const revision = content.revision ?? 0;
|
|
489
547
|
const href = content.href;
|
|
548
|
+
const thumbnail = content.thumbnail;
|
|
490
549
|
if (existing) {
|
|
491
550
|
const title = content.title || existing.title;
|
|
492
551
|
const width = frameWidth;
|
|
@@ -499,9 +558,9 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
499
558
|
if (
|
|
500
559
|
title !== existing.title || html !== existing.html || width !== existing.size.width ||
|
|
501
560
|
parentId !== existing.parentId || depth !== existing.depth || href !== existing.href ||
|
|
502
|
-
revision !== existing.revision || stale !== existing.stale
|
|
561
|
+
revision !== existing.revision || stale !== existing.stale || thumbnail !== existing.thumbnail
|
|
503
562
|
) changed = true;
|
|
504
|
-
next.set(content.id, { ...existing, title, html, stale, parentId, depth, href, revision, size: { width, height: existing.size.height } });
|
|
563
|
+
next.set(content.id, { ...existing, title, html, stale, parentId, depth, href, thumbnail, revision, size: { width, height: existing.size.height } });
|
|
505
564
|
} else {
|
|
506
565
|
changed = true;
|
|
507
566
|
next.set(content.id, {
|
|
@@ -516,6 +575,7 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
516
575
|
parentId,
|
|
517
576
|
depth,
|
|
518
577
|
href,
|
|
578
|
+
thumbnail,
|
|
519
579
|
revision,
|
|
520
580
|
stale: false,
|
|
521
581
|
});
|
|
@@ -611,15 +671,6 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
611
671
|
[updatePageFrame, viewStore],
|
|
612
672
|
);
|
|
613
673
|
|
|
614
|
-
// 生きているページは利用側(currentContentId)と揃える。
|
|
615
|
-
// 本文が読めて currentContentId が変わったときに初めてエディタが移る
|
|
616
|
-
// (クリックの時点では移さない。移すと前のページの本文が新しい枠に見えてしまう)
|
|
617
|
-
useEffect(() => {
|
|
618
|
-
if (!enabled) return;
|
|
619
|
-
const id = artboard.currentContentId ?? artboard.contentList[0]?.id ?? null;
|
|
620
|
-
viewStore.set((prev) => (prev.activePageId === id ? prev : { ...prev, activePageId: id }));
|
|
621
|
-
}, [enabled, artboard.currentContentId, artboard.contentList, viewStore]);
|
|
622
|
-
|
|
623
674
|
// 見えている範囲の保存(変化が止まってから)
|
|
624
675
|
useEffect(() => {
|
|
625
676
|
if (!storageKey || typeof localStorage === 'undefined') return;
|
|
@@ -786,32 +837,107 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
786
837
|
[setView],
|
|
787
838
|
);
|
|
788
839
|
|
|
789
|
-
|
|
790
|
-
|
|
840
|
+
/**
|
|
841
|
+
* フレームが今どこに、どれだけの大きさで画面に出ているか(容器の座標)。
|
|
842
|
+
* 「見えているか」「寄る必要があるか」を revealPage と focusPage が同じ式で判定するため
|
|
843
|
+
*/
|
|
844
|
+
const frameScreenRect = useCallback(
|
|
845
|
+
(id: string) => {
|
|
791
846
|
const page = pagesRef.current.find((p) => p.id === id);
|
|
792
|
-
if (!page) return;
|
|
793
|
-
const
|
|
847
|
+
if (!page) return null;
|
|
848
|
+
const container = containerSize();
|
|
794
849
|
const { canvasZoom: z, canvasOffset: o } = viewStore.get();
|
|
795
850
|
const inset = rulersRef.current ? RULER_SIZE : 0;
|
|
796
851
|
const left = o.x + page.position.x * z;
|
|
797
852
|
const top = o.y + page.position.y * z;
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
|
|
853
|
+
const width = page.size.width * z;
|
|
854
|
+
const height = page.size.height * z;
|
|
855
|
+
return {
|
|
856
|
+
page,
|
|
857
|
+
container,
|
|
858
|
+
inset,
|
|
859
|
+
zoom: z,
|
|
860
|
+
left,
|
|
861
|
+
top,
|
|
862
|
+
width,
|
|
863
|
+
height,
|
|
864
|
+
// 一部でも画面に入っていれば「見えている」
|
|
865
|
+
visible:
|
|
866
|
+
left + width > inset + REVEAL_MARGIN &&
|
|
867
|
+
left < container.width - REVEAL_MARGIN &&
|
|
868
|
+
top + height > inset + REVEAL_MARGIN &&
|
|
869
|
+
top < container.height - REVEAL_MARGIN,
|
|
870
|
+
};
|
|
871
|
+
},
|
|
872
|
+
[viewStore],
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
const revealPage = useCallback<MultiPageCanvasContextValue['revealPage']>(
|
|
876
|
+
(id) => {
|
|
877
|
+
const r = frameScreenRect(id);
|
|
801
878
|
// 一部でも見えていればそのまま。見えていなければ、そのフレームの左上を画面へ寄せる
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const w = page.size.width * z;
|
|
805
|
-
const h = page.size.height * z;
|
|
879
|
+
if (!r || r.visible) return;
|
|
880
|
+
const { page, container, inset, zoom: z, width: w, height: h } = r;
|
|
806
881
|
const offset = {
|
|
807
|
-
x: w <= width - inset ? inset + (width - inset - w) / 2 - page.position.x * z : inset +
|
|
808
|
-
y: h <= height - inset ? inset + (height - inset - h) / 2 - page.position.y * z : inset +
|
|
882
|
+
x: w <= container.width - inset ? inset + (container.width - inset - w) / 2 - page.position.x * z : inset + REVEAL_MARGIN - page.position.x * z,
|
|
883
|
+
y: h <= container.height - inset ? inset + (container.height - inset - h) / 2 - page.position.y * z : inset + REVEAL_MARGIN - page.position.y * z,
|
|
809
884
|
};
|
|
810
885
|
setView({ zoom: z, offset }, { animate: true });
|
|
811
886
|
},
|
|
812
|
-
[
|
|
887
|
+
[frameScreenRect, setView],
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* ホスト発(利用側のレールなど)でページが変わったときに視点を寄せる。
|
|
892
|
+
* 倍率はできるだけ変えない ── ただし「画面に入ってはいるが点にしか見えない」ときは
|
|
893
|
+
* 寄せても何も起きていないように見えるので、そのときだけページに合わせる
|
|
894
|
+
*/
|
|
895
|
+
const focusPage = useCallback(
|
|
896
|
+
(id: string) => {
|
|
897
|
+
const r = frameScreenRect(id);
|
|
898
|
+
if (!r) return;
|
|
899
|
+
if (r.width < FOCUS_MIN_WIDTH) {
|
|
900
|
+
zoomToPage(id, { animate: true });
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (!r.visible) revealPage(id);
|
|
904
|
+
},
|
|
905
|
+
[frameScreenRect, revealPage, zoomToPage],
|
|
813
906
|
);
|
|
814
907
|
|
|
908
|
+
/**
|
|
909
|
+
* 直前の activatePage(フレームのクリック・左パネル)の宛先。
|
|
910
|
+
* 下の同期 effect が「エディタ発かホスト発か」を見分けるのに使う。
|
|
911
|
+
* state(activatingPageId)ではなく ref で持つのは、切替が終わって state が戻るのと
|
|
912
|
+
* 同期 effect が走るのが同じ描画にまとまるため(state では読んだときに既に null)。
|
|
913
|
+
* 消すのは effect が使ったときだけ ── A → B と続けて押したときは A の後始末で消さない
|
|
914
|
+
*/
|
|
915
|
+
const editorInitiatedPageIdRef = useRef<string | null>(null);
|
|
916
|
+
|
|
917
|
+
// 生きているページは利用側(currentContentId)と揃える。
|
|
918
|
+
// 本文が読めて currentContentId が変わったときに初めてエディタが移る
|
|
919
|
+
// (クリックの時点では移さない。移すと前のページの本文が新しい枠に見えてしまう)。
|
|
920
|
+
// ホスト発(利用側がレールで contentId を変えた)のときは視点も寄せる ── フレームの
|
|
921
|
+
// クリックと違って画面のどこにも手掛かりが無く、26 ページの木では移り先がほぼ画面外
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
if (!enabled) return;
|
|
924
|
+
const id = artboard.currentContentId ?? artboard.contentList[0]?.id ?? null;
|
|
925
|
+
let prevId: string | null | undefined;
|
|
926
|
+
viewStore.set((prev) => {
|
|
927
|
+
if (prev.activePageId === id) return prev;
|
|
928
|
+
prevId = prev.activePageId;
|
|
929
|
+
return { ...prev, activePageId: id };
|
|
930
|
+
});
|
|
931
|
+
if (prevId === undefined || !id) return; // 変わっていない
|
|
932
|
+
if (prevId === null) return; // 最初の 1 ページが決まっただけ。初回の視点は MultiPageCanvasView が決める
|
|
933
|
+
if (editorInitiatedPageIdRef.current === id) {
|
|
934
|
+
// エディタ発(activatePage)。あちらで既に寄せてあるので二重に動かさない
|
|
935
|
+
editorInitiatedPageIdRef.current = null;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
focusPage(id);
|
|
939
|
+
}, [enabled, artboard.currentContentId, artboard.contentList, viewStore, focusPage]);
|
|
940
|
+
|
|
815
941
|
// ---- 本文の読み込み
|
|
816
942
|
const loadingRef = useRef(new Set<string>());
|
|
817
943
|
const ensurePageHtml = useCallback<MultiPageCanvasContextValue['ensurePageHtml']>(
|
|
@@ -869,13 +995,21 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
869
995
|
}
|
|
870
996
|
const notify = onContentChangeRef.current;
|
|
871
997
|
if (!notify) return false;
|
|
998
|
+
// エディタ発だと印す。利用側の contentId が変わって同期 effect が走るころには
|
|
999
|
+
// この切替は終わっているので、state ではなく ref で残す
|
|
1000
|
+
editorInitiatedPageIdRef.current = id;
|
|
872
1001
|
setActivatingPageId(id);
|
|
873
1002
|
if (options?.reveal) revealPage(id);
|
|
1003
|
+
let moved = false;
|
|
874
1004
|
try {
|
|
875
1005
|
const result = await (notify as (contentId: string) => unknown)(id);
|
|
876
|
-
|
|
1006
|
+
moved = result !== false;
|
|
1007
|
+
return moved;
|
|
877
1008
|
} finally {
|
|
878
1009
|
setActivatingPageId((v) => (v === id ? null : v));
|
|
1010
|
+
// 移れなかったら印を消す(残すと、あとで同じページへホスト発で移るときに視点が動かない)。
|
|
1011
|
+
// 別のページへ移ったあとなら消さない ── その印はそちらの切替のもの
|
|
1012
|
+
if (!moved && editorInitiatedPageIdRef.current === id) editorInitiatedPageIdRef.current = null;
|
|
879
1013
|
}
|
|
880
1014
|
},
|
|
881
1015
|
[revealPage, viewStore],
|
|
@@ -930,6 +1064,36 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
930
1064
|
[pumpPreviewSlots],
|
|
931
1065
|
);
|
|
932
1066
|
|
|
1067
|
+
// ---- 紙面の画像の解像度(画像を出してよい倍率の上限を決める)
|
|
1068
|
+
// 引き伸ばしすぎるとにじむ。上限はキャンバス全体で 1 つにし、いちばん粗い画像に合わせる
|
|
1069
|
+
const thumbnailWidthsRef = useRef(new Map<string, number>());
|
|
1070
|
+
const thumbnailRatioRef = useRef<number | null>(null);
|
|
1071
|
+
const [previewImageCapVersion, setPreviewImageCapVersion] = useState(0);
|
|
1072
|
+
|
|
1073
|
+
const reportThumbnailWidth = useCallback<MultiPageCanvasContextValue['reportThumbnailWidth']>((id, naturalWidth) => {
|
|
1074
|
+
if (!Number.isFinite(naturalWidth) || naturalWidth <= 0) return;
|
|
1075
|
+
if (thumbnailWidthsRef.current.get(id) === naturalWidth) return;
|
|
1076
|
+
thumbnailWidthsRef.current.set(id, naturalWidth);
|
|
1077
|
+
let min = Infinity;
|
|
1078
|
+
thumbnailWidthsRef.current.forEach((width, key) => {
|
|
1079
|
+
const pageWidth = framesMapRef.current.get(key)?.size.width;
|
|
1080
|
+
if (!pageWidth || pageWidth <= 0) return;
|
|
1081
|
+
min = Math.min(min, width / pageWidth);
|
|
1082
|
+
});
|
|
1083
|
+
const ratio = Number.isFinite(min) ? min : null;
|
|
1084
|
+
if (ratio === thumbnailRatioRef.current) return;
|
|
1085
|
+
thumbnailRatioRef.current = ratio;
|
|
1086
|
+
// 上限が動いたので、React 側(画像にするかの判定)にやり直させる
|
|
1087
|
+
setPreviewImageCapVersion((v) => v + 1);
|
|
1088
|
+
}, []);
|
|
1089
|
+
|
|
1090
|
+
const getPreviewImageZoomCap = useCallback<MultiPageCanvasContextValue['getPreviewImageZoomCap']>(() => {
|
|
1091
|
+
// 別のディスプレイへ移すと変わるので、そのつど読む
|
|
1092
|
+
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
|
|
1093
|
+
const ratio = thumbnailRatioRef.current ?? PREVIEW_IMAGE_PRESUMED_RATIO;
|
|
1094
|
+
return Math.min(PREVIEW_IMAGE_ZOOM, (ratio * PREVIEW_IMAGE_MAX_STRETCH) / dpr);
|
|
1095
|
+
}, []);
|
|
1096
|
+
|
|
933
1097
|
const value = useMemo<MultiPageCanvasContextValue>(
|
|
934
1098
|
() => ({
|
|
935
1099
|
isEnabled: enabled,
|
|
@@ -968,12 +1132,16 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
968
1132
|
toggleRulers,
|
|
969
1133
|
requestPreviewSlot,
|
|
970
1134
|
releasePreviewSlot,
|
|
1135
|
+
getPreviewImageZoomCap,
|
|
1136
|
+
reportThumbnailWidth,
|
|
1137
|
+
previewImageCapVersion,
|
|
971
1138
|
}),
|
|
972
1139
|
[
|
|
973
1140
|
enabled, editorMode, pages, bounds, layout, getPage, updatePageFrame, setPageHtml, invalidatePages, artboard.documentAttributes,
|
|
974
1141
|
setPageHeight, ensurePageHtml, previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
|
|
975
1142
|
zoomToActual, revealPage, activatePage, activatingPageId, registerContainer, isInteracting, markInteracting,
|
|
976
1143
|
persisted, rulersVisible, toggleRulers, requestPreviewSlot, releasePreviewSlot,
|
|
1144
|
+
getPreviewImageZoomCap, reportThumbnailWidth, previewImageCapVersion,
|
|
977
1145
|
],
|
|
978
1146
|
);
|
|
979
1147
|
|
|
@@ -13,6 +13,7 @@ import { createAuthApi } from '../../lib/api/auth-fetch';
|
|
|
13
13
|
import { convertSingleElementToAbsolute, generateElementId } from '../utils/dom-utils';
|
|
14
14
|
import type { SlideAgentRequest, SlideAgentResponse, AttachedFile as SlideAttachedFile } from '../../lib/agent/slide-agent/types';
|
|
15
15
|
import type { WebsiteAgentRequest, WebsiteAgentResponse, AttachedFile as WebsiteAttachedFile } from '../../lib/agent/website-agent/types';
|
|
16
|
+
import { debugLog } from '../utils/debug';
|
|
16
17
|
|
|
17
18
|
// AttachedFile型を共通化(両方同じ構造)
|
|
18
19
|
export type AttachedFile = SlideAttachedFile;
|
|
@@ -176,7 +177,7 @@ export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps =
|
|
|
176
177
|
const convertChildrenToAbsolute = async () => {
|
|
177
178
|
// オートレイアウトモードの場合は変換をスキップ
|
|
178
179
|
if (layoutMode === 'auto') {
|
|
179
|
-
|
|
180
|
+
debugLog('[useAiReplace] Auto layout mode - skipping absolute positioning conversion for children');
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -196,7 +197,7 @@ export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps =
|
|
|
196
197
|
convertSingleElementToAbsolute(iframeDoc, htmlChild);
|
|
197
198
|
});
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
debugLog('[useAiReplace] Converted', editableChildren.length, 'child elements to absolute positioning');
|
|
200
201
|
|
|
201
202
|
// 変更を再度通知
|
|
202
203
|
notifyIframeChange();
|
|
@@ -263,7 +264,7 @@ export function useAiReplace({ presentationId, websiteId }: UseAiReplaceProps =
|
|
|
263
264
|
// editorModeに基づいてAPI呼び出しを分岐
|
|
264
265
|
const isWebpageMode = editorMode === 'webpage';
|
|
265
266
|
|
|
266
|
-
|
|
267
|
+
debugLog('[useAiReplace] API呼び出し:', {
|
|
267
268
|
editorMode,
|
|
268
269
|
apiEndpoint: isWebpageMode ? '/api/website-agent' : '/api/slide-agent',
|
|
269
270
|
promptLength: prompt.length,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import { debugLog } from '../utils/debug';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* ブラウザのネイティブズーム(ピンチ、Ctrl+ホイール、キーボードショートカット等)を防止するフック
|
|
@@ -67,7 +68,7 @@ export function useBrowserZoomPrevention(): void {
|
|
|
67
68
|
// e.key ではなく e.code を使用して物理キーを判定する
|
|
68
69
|
// preventDefault のみでブラウザのデフォルト動作を止め、自前のハンドラーは動くようにする
|
|
69
70
|
if (isCmdOrCtrl && e.altKey && (e.code === 'KeyC' || e.code === 'KeyV')) {
|
|
70
|
-
|
|
71
|
+
debugLog('[preventBrowserShortcuts] Preventing browser default for Cmd/Ctrl+Alt+C/V', {
|
|
71
72
|
key: e.key,
|
|
72
73
|
code: e.code,
|
|
73
74
|
});
|