@growgroup/visual-editor 0.3.0 → 0.4.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.
Files changed (65) hide show
  1. package/README.md +10 -1
  2. package/dist/editor.css +1 -1
  3. package/package.json +3 -2
  4. package/src/editor/EditorContext.tsx +11 -2
  5. package/src/editor/EditorToolbar.tsx +8 -2
  6. package/src/editor/FrontendVisualEditor.tsx +128 -45
  7. package/src/editor/components/EditorCanvas.tsx +87 -15
  8. package/src/editor/components/EditorContextMenu.tsx +12 -3
  9. package/src/editor/components/EditorFooter.tsx +2 -0
  10. package/src/editor/components/EditorHeader.tsx +3 -2
  11. package/src/editor/components/EditorLayerPanel.tsx +28 -2
  12. package/src/editor/components/EditorPropertyPanel.tsx +54 -25
  13. package/src/editor/components/FigmaColorPicker.tsx +2 -1
  14. package/src/editor/components/JsEditorDialog.tsx +1 -0
  15. package/src/editor/components/PageSettingsDialog.tsx +1 -0
  16. package/src/editor/components/comments/CommentBoard.tsx +9 -6
  17. package/src/editor/components/multi-page/CanvasFrameDecor.tsx +172 -0
  18. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +204 -58
  19. package/src/editor/components/multi-page/PageFramePreview.tsx +131 -22
  20. package/src/editor/components/multi-page/index.ts +1 -0
  21. package/src/editor/components/ppt/PptChrome.tsx +6 -2
  22. package/src/editor/components/ppt/PptComments.tsx +411 -71
  23. package/src/editor/constants.ts +44 -1
  24. package/src/editor/contexts/EditorArtboardContext.tsx +60 -1
  25. package/src/editor/contexts/EditorComponentsContext.tsx +48 -31
  26. package/src/editor/contexts/EditorUIStateContext.tsx +4 -3
  27. package/src/editor/contexts/EditorVariablesContext.tsx +5 -4
  28. package/src/editor/contexts/MultiPageCanvasContext.tsx +211 -34
  29. package/src/editor/hooks/useAiReplace.ts +4 -3
  30. package/src/editor/hooks/useBrowserZoomPrevention.ts +2 -1
  31. package/src/editor/hooks/useComponentEditMode.ts +27 -26
  32. package/src/editor/hooks/useEditorMessages.ts +3 -2
  33. package/src/editor/hooks/useElementActions.ts +27 -26
  34. package/src/editor/hooks/useElementSelection.ts +19 -16
  35. package/src/editor/hooks/useFocusManagement.ts +7 -6
  36. package/src/editor/hooks/useGoogleFonts.ts +3 -2
  37. package/src/editor/hooks/useIframeInitializer.ts +6 -3
  38. package/src/editor/hooks/useIframeSetup.ts +4 -3
  39. package/src/editor/hooks/useImageUpload.ts +4 -3
  40. package/src/editor/hooks/useMarqueeSelection.ts +2 -1
  41. package/src/editor/hooks/usePageSettingsManager.ts +24 -23
  42. package/src/editor/hooks/useRichPaste.ts +11 -10
  43. package/src/editor/parts.ts +7 -25
  44. package/src/editor/utils/component-renderer.ts +8 -7
  45. package/src/editor/utils/component-sync.ts +16 -15
  46. package/src/editor/utils/debug.ts +52 -0
  47. package/src/editor/utils/dom-utils.ts +27 -10
  48. package/src/editor/utils/drop-target.ts +135 -0
  49. package/src/editor/utils/eyedropper.ts +3 -1
  50. package/src/editor/utils/figma-export.ts +6 -5
  51. package/src/editor/utils/figma-kiwi-decoder.ts +28 -27
  52. package/src/editor/utils/figma-kiwi-encoder.ts +9 -8
  53. package/src/editor/utils/figma-paste.ts +57 -56
  54. package/src/editor/utils/flex-utils.ts +12 -10
  55. package/src/editor/utils/html-utils.ts +19 -6
  56. package/src/editor/utils/override-apply.ts +3 -2
  57. package/src/editor/utils/override-detection.ts +11 -10
  58. package/src/editor/utils/tailwind-utils.ts +4 -3
  59. package/src/index.ts +5 -0
  60. package/src/io.ts +22 -1
  61. package/src/lib/firebase/css-variables.ts +4 -3
  62. package/src/lib/firebase/storage.ts +3 -2
  63. package/src/styles/skin.css +63 -4
  64. package/src/types/editor.ts +6 -2
  65. package/src/version.ts +9 -0
@@ -34,7 +34,7 @@ import React, {
34
34
  useState,
35
35
  useSyncExternalStore,
36
36
  } from 'react';
37
- import { useEditorArtboard } from './EditorArtboardContext';
37
+ import { useEditorArtboard, computeContentDepths, type DocumentAttributes } from './EditorArtboardContext';
38
38
  import { useEditorTool } from './EditorToolContext';
39
39
  import { useEditorView } from './EditorViewContext';
40
40
  import { SLIDE_HEIGHT, SLIDE_WIDTH, WEBPAGE_WIDTH } from '../constants';
@@ -62,6 +62,15 @@ export const RULER_SIZE = 20;
62
62
  const VIEW_ANIMATION_MS = 240;
63
63
  /** 見るだけの紙面を同時に読む枚数 */
64
64
  const PREVIEW_PARALLEL = 4;
65
+ /**
66
+ * 見るだけの紙面を画像で描く倍率のしきい値。これ未満に縮小されている間は
67
+ * srcdoc の iframe ではなく contentList[].thumbnail の画像を出す。
68
+ * 判定は操作が終わってからしか動かさない(縮小の途中で 26 枚を差し替えない)。
69
+ * 行き来でちらつかないように戻りは別のしきい値にする(ヒステリシス)
70
+ */
71
+ export const PREVIEW_IMAGE_ZOOM = 0.5;
72
+ /** 画像 → iframe に戻す倍率(PREVIEW_IMAGE_ZOOM 以上)。iframe → 画像は PREVIEW_IMAGE_ZOOM_OUT 未満 */
73
+ export const PREVIEW_IMAGE_ZOOM_OUT = 0.4;
65
74
 
66
75
  /** フレームどうしの間隔(紙面の座標系、等倍のとき)。フレーム名を出すかの判定にも使う */
67
76
  export const FRAME_GAP: Record<EditorMode, { x: number; y: number }> = {
@@ -82,7 +91,7 @@ const WRAP_AT: Record<EditorMode, (count: number) => number> = {
82
91
  export interface PageFrame {
83
92
  id: string;
84
93
  title: string;
85
- /** 本文。null = まだ読んでいない */
94
+ /** 本文。null = まだ読んでいない(または読み直す) */
86
95
  html: string | null;
87
96
  /** 本文を読んでいる途中 */
88
97
  loading: boolean;
@@ -93,6 +102,21 @@ export interface PageFrame {
93
102
  measured: boolean;
94
103
  /** 未保存の変更がある(生きているページだけ立つ) */
95
104
  isDirty: boolean;
105
+ /** 親ページ(contentList[].parentId。一覧に無い親は null 扱い) */
106
+ parentId: string | null;
107
+ /** 階層の深さ(ルート = 0) */
108
+ depth: number;
109
+ /** 編集しない表示の URL(contentList[].href) */
110
+ href?: string;
111
+ /** ページ全体を写した画像の URL(contentList[].thumbnail)。大きく縮小した間だけ紙面の代わりに出す */
112
+ thumbnail?: string;
113
+ /** 利用側が進める本文の版(contentList[].revision)。変わったら読み直す */
114
+ revision: number;
115
+ /**
116
+ * 本文が古い(部品の反映などで外から変わった)。読み直すときは contentList[].thumbnailHtml
117
+ * ではなく io.loadContent から取る(thumbnailHtml は利用側が最初に渡した姿のままなので)
118
+ */
119
+ stale: boolean;
96
120
  }
97
121
 
98
122
  /** レイアウト済み(位置付き)のフレーム */
@@ -101,6 +125,9 @@ export interface PageFrameLayout extends PageFrame {
101
125
  index: number;
102
126
  }
103
127
 
128
+ /** フレームの並べ方。tree = 階層(parentId)のツリー、grid = 行で折り返す */
129
+ export type CanvasLayoutKind = 'grid' | 'tree';
130
+
104
131
  export interface CanvasViewState {
105
132
  canvasOffset: { x: number; y: number };
106
133
  canvasZoom: number;
@@ -129,13 +156,19 @@ export interface MultiPageCanvasContextValue {
129
156
  // ---- ページフレーム
130
157
  pages: PageFrameLayout[];
131
158
  bounds: CanvasBounds;
159
+ /** 並べ方(contentList に階層があれば tree) */
160
+ layout: CanvasLayoutKind;
132
161
  getPage: (id: string) => PageFrameLayout | undefined;
133
- updatePageFrame: (
134
- id: string,
135
- updates: Partial<Pick<PageFrame, 'title' | 'html' | 'size' | 'isDirty' | 'measured' | 'loading' | 'error'>>,
136
- ) => void;
162
+ updatePageFrame: (id: string, updates: Partial<Omit<PageFrame, 'id'>>) => void;
137
163
  /** 本文を差し替える(保存後・読み込み後) */
138
164
  setPageHtml: (id: string, html: string) => void;
165
+ /**
166
+ * 見るだけの紙面の本文を捨てて読み直させる(部品の定義を更新して他ページが変わったとき等)。
167
+ * except のページ(編集中のページ)は触らない。読み直す手段(io.loadContent)が無ければ何もしない
168
+ */
169
+ invalidatePages: (options?: { except?: readonly (string | null | undefined)[] }) => void;
170
+ /** 文書(`<html>`)に付ける属性(利用側の documentAttributes)。紙面にも読み直さずに反映する */
171
+ documentAttributes: DocumentAttributes;
139
172
  /**
140
173
  * 高さの実測を反映する。source が 'preview' のときは、生きているページには効かない
141
174
  * (エディタの実測が正。見るだけの紙面はフォントや画像の読み込み前に測ることがある)
@@ -222,6 +255,17 @@ export function useCanvasViewState(): CanvasViewState {
222
255
  return useSyncExternalStore(viewStore.subscribe, viewStore.get, viewStore.get);
223
256
  }
224
257
 
258
+ /**
259
+ * 編集中のページだけを購読する。
260
+ * useCanvasViewState() だと倍率・位置が変わるたびに描き直る(ホイール 1 段ごとに
261
+ * キャンバス全体が React を通る)。編集中のページしか要らない側はこちらを使う
262
+ */
263
+ export function useCanvasActivePageId(): string | null {
264
+ const { viewStore } = useMultiPageCanvas();
265
+ const getSnapshot = useCallback(() => viewStore.get().activePageId, [viewStore]);
266
+ return useSyncExternalStore(viewStore.subscribe, getSnapshot, getSnapshot);
267
+ }
268
+
225
269
  const NULL_VIEW_STORE: CanvasViewStore = {
226
270
  get: () => NULL_VIEW,
227
271
  set: () => undefined,
@@ -241,16 +285,19 @@ export function useCanvasViewStateOptional(): CanvasViewState | null {
241
285
  // レイアウト
242
286
  // ============================================================
243
287
 
244
- /**
245
- * 寸法から位置を決める。行ごとに折り返し、行の高さはその行で一番高いフレームに合わせる。
246
- * 位置を状態に持たないので、高さが実測で変わっても流れ直すだけで済む
247
- */
248
- export function layoutFrames(
249
- frames: PageFrame[],
250
- editorMode: EditorMode,
251
- ): { pages: PageFrameLayout[]; bounds: CanvasBounds } {
252
- const gap = GAP[editorMode];
253
- const perRow = WRAP_AT[editorMode](frames.length);
288
+ function boundsOf(pages: PageFrameLayout[]): CanvasBounds {
289
+ return pages.length
290
+ ? {
291
+ minX: Math.min(...pages.map((p) => p.position.x)),
292
+ minY: Math.min(...pages.map((p) => p.position.y)),
293
+ maxX: Math.max(...pages.map((p) => p.position.x + p.size.width)),
294
+ maxY: Math.max(...pages.map((p) => p.position.y + p.size.height)),
295
+ }
296
+ : { minX: 0, minY: 0, maxX: 0, maxY: 0 };
297
+ }
298
+
299
+ /** 行で折り返す並べ方(階層が無いとき)。行の高さはその行で一番高いフレームに合わせる */
300
+ function layoutGrid(frames: PageFrame[], gap: { x: number; y: number }, perRow: number): PageFrameLayout[] {
254
301
  const pages: PageFrameLayout[] = [];
255
302
  let x = 0;
256
303
  let y = 0;
@@ -265,15 +312,101 @@ export function layoutFrames(
265
312
  x += frame.size.width + gap.x;
266
313
  rowHeight = Math.max(rowHeight, frame.size.height);
267
314
  });
268
- const bounds: CanvasBounds = pages.length
269
- ? {
270
- minX: Math.min(...pages.map((p) => p.position.x)),
271
- minY: Math.min(...pages.map((p) => p.position.y)),
272
- maxX: Math.max(...pages.map((p) => p.position.x + p.size.width)),
273
- maxY: Math.max(...pages.map((p) => p.position.y + p.size.height)),
274
- }
275
- : { minX: 0, minY: 0, maxX: 0, maxY: 0 };
276
- return { pages, bounds };
315
+ return pages;
316
+ }
317
+
318
+ /**
319
+ * 階層(parentId)のツリーで並べる(サイトマップの図と同じ姿)。
320
+ * - 深さごとに行を作り、行の高さはその深さで一番高いフレームに合わせる
321
+ * - 親は子の並び(部分木)の中央の上に置く。部分木の幅 = max(自分の幅, 子の部分木の幅の和 + 間隔)
322
+ * - 親が一覧に無い・循環しているものはルートとして左から順に置く
323
+ * 一覧の順(index)は変えない(フレーム名の番号・読み込みの順番待ちに使う)
324
+ */
325
+ function layoutTree(frames: PageFrame[], gap: { x: number; y: number }): PageFrameLayout[] {
326
+ const byId = new Map(frames.map((f) => [f.id, f] as const));
327
+ const children = new Map<string, PageFrame[]>();
328
+ const roots: PageFrame[] = [];
329
+ // 循環の検出: 親をたどってルートに着くか(着かないものはルート扱い)
330
+ const reachesRoot = (f: PageFrame): boolean => {
331
+ const seen = new Set<string>([f.id]);
332
+ let cur: PageFrame | undefined = f;
333
+ while (cur?.parentId && byId.has(cur.parentId)) {
334
+ if (seen.has(cur.parentId)) return false;
335
+ seen.add(cur.parentId);
336
+ cur = byId.get(cur.parentId);
337
+ }
338
+ return true;
339
+ };
340
+ frames.forEach((f) => {
341
+ const parent = f.parentId && f.parentId !== f.id && byId.has(f.parentId) && reachesRoot(f) ? f.parentId : null;
342
+ if (parent) {
343
+ const list = children.get(parent) ?? [];
344
+ list.push(f);
345
+ children.set(parent, list);
346
+ } else {
347
+ roots.push(f);
348
+ }
349
+ });
350
+ const subtreeWidth = new Map<string, number>();
351
+ const widthOf = (f: PageFrame): number => {
352
+ const cached = subtreeWidth.get(f.id);
353
+ if (cached != null) return cached;
354
+ const kids = children.get(f.id) ?? [];
355
+ const kidsWidth = kids.reduce((sum, k, i) => sum + widthOf(k) + (i > 0 ? gap.x : 0), 0);
356
+ const w = Math.max(f.size.width, kidsWidth);
357
+ subtreeWidth.set(f.id, w);
358
+ return w;
359
+ };
360
+ // 深さと行の高さ
361
+ const depthOf = new Map<string, number>();
362
+ const rowHeight: number[] = [];
363
+ const walk = (f: PageFrame, d: number) => {
364
+ depthOf.set(f.id, d);
365
+ rowHeight[d] = Math.max(rowHeight[d] ?? 0, f.size.height);
366
+ (children.get(f.id) ?? []).forEach((k) => walk(k, d + 1));
367
+ };
368
+ roots.forEach((r) => walk(r, 0));
369
+ const rowY: number[] = [];
370
+ rowHeight.forEach((h, d) => {
371
+ rowY[d] = d === 0 ? 0 : rowY[d - 1] + rowHeight[d - 1] + gap.y;
372
+ });
373
+ const position = new Map<string, { x: number; y: number }>();
374
+ const place = (f: PageFrame, x0: number) => {
375
+ const d = depthOf.get(f.id) ?? 0;
376
+ const w = widthOf(f);
377
+ position.set(f.id, { x: x0 + (w - f.size.width) / 2, y: rowY[d] ?? 0 });
378
+ const kids = children.get(f.id) ?? [];
379
+ const kidsWidth = kids.reduce((sum, k, i) => sum + widthOf(k) + (i > 0 ? gap.x : 0), 0);
380
+ let cx = x0 + (w - kidsWidth) / 2;
381
+ kids.forEach((k) => {
382
+ place(k, cx);
383
+ cx += widthOf(k) + gap.x;
384
+ });
385
+ };
386
+ let x = 0;
387
+ roots.forEach((r) => {
388
+ place(r, x);
389
+ x += widthOf(r) + gap.x;
390
+ });
391
+ return frames.map((f, index) => {
392
+ const p = position.get(f.id) ?? { x: 0, y: 0 };
393
+ return { ...f, index, depth: depthOf.get(f.id) ?? 0, position: { x: Math.round(p.x), y: Math.round(p.y) } };
394
+ });
395
+ }
396
+
397
+ /**
398
+ * 寸法から位置を決める。contentList に階層(parentId)があればツリー、無ければ行で折り返す。
399
+ * 位置を状態に持たないので、高さが実測で変わっても流れ直すだけで済む
400
+ */
401
+ export function layoutFrames(
402
+ frames: PageFrame[],
403
+ editorMode: EditorMode,
404
+ ): { pages: PageFrameLayout[]; bounds: CanvasBounds; layout: CanvasLayoutKind } {
405
+ const gap = GAP[editorMode];
406
+ const ids = new Set(frames.map((f) => f.id));
407
+ const hasHierarchy = frames.some((f) => f.parentId && f.parentId !== f.id && ids.has(f.parentId));
408
+ const pages = hasHierarchy ? layoutTree(frames, gap) : layoutGrid(frames, gap, WRAP_AT[editorMode](frames.length));
409
+ return { pages, bounds: boundsOf(pages), layout: hasHierarchy ? 'tree' : 'grid' };
277
410
  }
278
411
 
279
412
  function fitView(
@@ -361,19 +494,37 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
361
494
  framesMapRef.current = framesMap;
362
495
 
363
496
  // contentList からフレームを同期(既存の実測・本文は保つ)
497
+ const currentContentIdRef = useRef(artboard.currentContentId);
498
+ currentContentIdRef.current = artboard.currentContentId;
364
499
  useEffect(() => {
365
500
  if (!enabled) return;
501
+ const ids = new Set(artboard.contentList.map((c) => c.id));
502
+ const depths = computeContentDepths(artboard.contentList);
366
503
  setFramesMap((prev) => {
367
504
  const next = new Map<string, PageFrame>();
368
505
  let changed = prev.size !== artboard.contentList.length;
369
506
  artboard.contentList.forEach((content) => {
370
507
  const existing = prev.get(content.id);
508
+ const parentId = content.parentId && ids.has(content.parentId) && content.parentId !== content.id ? content.parentId : null;
509
+ const depth = depths.get(content.id) ?? 0;
510
+ const revision = content.revision ?? 0;
511
+ const href = content.href;
512
+ const thumbnail = content.thumbnail;
371
513
  if (existing) {
372
514
  const title = content.title || existing.title;
373
- const html = existing.html ?? content.thumbnailHtml ?? null;
374
515
  const width = frameWidth;
375
- if (title !== existing.title || html !== existing.html || width !== existing.size.width) changed = true;
376
- next.set(content.id, { ...existing, title, html, size: { width, height: existing.size.height } });
516
+ // 利用側が版を進めた = 本文が外から変わった。編集中のページは触らず(編集中の本文が正)、
517
+ // 他のページは本文を捨てて読み直させる(紙面は次の本文が来るまで前の姿のまま)
518
+ const bumped = revision !== existing.revision;
519
+ const isLive = content.id === currentContentIdRef.current;
520
+ const html = bumped && !isLive ? null : existing.html ?? content.thumbnailHtml ?? null;
521
+ const stale = bumped && !isLive ? true : existing.stale;
522
+ if (
523
+ title !== existing.title || html !== existing.html || width !== existing.size.width ||
524
+ parentId !== existing.parentId || depth !== existing.depth || href !== existing.href ||
525
+ revision !== existing.revision || stale !== existing.stale || thumbnail !== existing.thumbnail
526
+ ) changed = true;
527
+ next.set(content.id, { ...existing, title, html, stale, parentId, depth, href, thumbnail, revision, size: { width, height: existing.size.height } });
377
528
  } else {
378
529
  changed = true;
379
530
  next.set(content.id, {
@@ -385,6 +536,12 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
385
536
  size: { width: frameWidth, height: defaultHeight },
386
537
  measured: editorMode === 'slide',
387
538
  isDirty: false,
539
+ parentId,
540
+ depth,
541
+ href,
542
+ thumbnail,
543
+ revision,
544
+ stale: false,
388
545
  });
389
546
  }
390
547
  });
@@ -392,7 +549,7 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
392
549
  });
393
550
  }, [enabled, artboard.contentList, frameWidth, defaultHeight, editorMode]);
394
551
 
395
- const { pages, bounds } = useMemo(() => {
552
+ const { pages, bounds, layout } = useMemo(() => {
396
553
  const order = artboard.contentList.map((c) => c.id);
397
554
  const frames = order.map((id) => framesMap.get(id)).filter((f): f is PageFrame => !!f);
398
555
  return layoutFrames(frames, editorMode);
@@ -428,10 +585,26 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
428
585
  }, []);
429
586
 
430
587
  const setPageHtml = useCallback(
431
- (id: string, html: string) => updatePageFrame(id, { html, loading: false, error: null }),
588
+ (id: string, html: string) => updatePageFrame(id, { html, loading: false, error: null, stale: false }),
432
589
  [updatePageFrame],
433
590
  );
434
591
 
592
+ // 見るだけの紙面の本文を捨てて読み直させる(部品の反映など、外からファイルが変わったとき)
593
+ const invalidatePages = useCallback<MultiPageCanvasContextValue['invalidatePages']>((options) => {
594
+ if (!io().loadContent) return; // 読み直す手段が無い(thumbnailHtml は最初の姿のままなので読み直しにならない)
595
+ const except = new Set((options?.except ?? []).filter((id): id is string => !!id));
596
+ setFramesMap((prev) => {
597
+ let changed = false;
598
+ const next = new Map(prev);
599
+ prev.forEach((frame, id) => {
600
+ if (except.has(id) || frame.html == null) return;
601
+ next.set(id, { ...frame, html: null, loading: false, error: null, stale: true });
602
+ changed = true;
603
+ });
604
+ return changed ? next : prev;
605
+ });
606
+ }, []);
607
+
435
608
  // ---- ビュー状態(外部ストア。ズーム・パンで Context の値は変えない)
436
609
  const persisted = useMemo(() => readPersistedView(storageKey), [storageKey]);
437
610
  const [viewStore] = useState<CanvasViewStore>(() =>
@@ -669,12 +842,13 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
669
842
  async (id) => {
670
843
  const frame = framesMapRef.current.get(id);
671
844
  if (!frame || frame.html != null || loadingRef.current.has(id)) return;
845
+ const loader = io().loadContent;
846
+ // 最初は利用側が渡した本文(thumbnailHtml)で足りる。外から変わった(stale)あとは loadContent で読み直す
672
847
  const fromList = artboard.contentList.find((c) => c.id === id)?.thumbnailHtml;
673
- if (fromList != null) {
848
+ if (fromList != null && !(frame.stale && loader)) {
674
849
  setPageHtml(id, fromList);
675
850
  return;
676
851
  }
677
- const loader = io().loadContent;
678
852
  if (!loader) return;
679
853
  loadingRef.current.add(id);
680
854
  updatePageFrame(id, { loading: true, error: null });
@@ -786,9 +960,12 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
786
960
  editorMode,
787
961
  pages,
788
962
  bounds,
963
+ layout,
789
964
  getPage,
790
965
  updatePageFrame,
791
966
  setPageHtml,
967
+ invalidatePages,
968
+ documentAttributes: artboard.documentAttributes,
792
969
  setPageHeight,
793
970
  ensurePageHtml,
794
971
  previewStyles,
@@ -817,8 +994,8 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
817
994
  releasePreviewSlot,
818
995
  }),
819
996
  [
820
- enabled, editorMode, pages, bounds, getPage, updatePageFrame, setPageHtml, setPageHeight, ensurePageHtml,
821
- previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
997
+ enabled, editorMode, pages, bounds, layout, getPage, updatePageFrame, setPageHtml, invalidatePages, artboard.documentAttributes,
998
+ setPageHeight, ensurePageHtml, previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
822
999
  zoomToActual, revealPage, activatePage, activatingPageId, registerContainer, isInteracting, markInteracting,
823
1000
  persisted, rulersVisible, toggleRulers, requestPreviewSlot, releasePreviewSlot,
824
1001
  ],
@@ -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
- console.log('[useAiReplace] Auto layout mode - skipping absolute positioning conversion for children');
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
- console.log('[useAiReplace] Converted', editableChildren.length, 'child elements to absolute positioning');
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
- console.log('[useAiReplace] API呼び出し:', {
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
- console.log('[preventBrowserShortcuts] Preventing browser default for Cmd/Ctrl+Alt+C/V', {
71
+ debugLog('[preventBrowserShortcuts] Preventing browser default for Cmd/Ctrl+Alt+C/V', {
71
72
  key: e.key,
72
73
  code: e.code,
73
74
  });
@@ -7,6 +7,7 @@ import { buildDomTree } from '../utils/dom-utils';
7
7
  import { applyLockInside, detachPart, partInfoOf, unlockPartDescendants } from '../parts';
8
8
  import { toast } from 'sonner';
9
9
  import type { MasterComponent, ComponentInstance } from '../../types/editor-components';
10
+ import { debugLog } from '../utils/debug';
10
11
 
11
12
  interface UseComponentEditModeProps {
12
13
  contentId?: string;
@@ -201,7 +202,7 @@ export function useComponentEditMode({
201
202
 
202
203
  try {
203
204
  // Debug: Log the original element
204
- console.log('[handleConfirmCreateComponent] Original element:', {
205
+ debugLog('[handleConfirmCreateComponent] Original element:', {
205
206
  tagName: element.tagName,
206
207
  className: element.className,
207
208
  innerHTML: element.innerHTML.substring(0, 200),
@@ -212,7 +213,7 @@ export function useComponentEditMode({
212
213
 
213
214
  // Create the master component
214
215
  const component = createMasterComponent(element, newComponentName.trim(), newComponentCategory || 'content');
215
- console.log('[handleConfirmCreateComponent] Created component:', {
216
+ debugLog('[handleConfirmCreateComponent] Created component:', {
216
217
  id: component.id,
217
218
  name: component.name,
218
219
  variantsCount: component.variants.length,
@@ -248,7 +249,7 @@ export function useComponentEditMode({
248
249
  { x: positionX, y: positionY } // initialPosition
249
250
  );
250
251
  if (instance) {
251
- console.log('[handleConfirmCreateComponent] Created instance:', {
252
+ debugLog('[handleConfirmCreateComponent] Created instance:', {
252
253
  id: instance.id,
253
254
  domElementId: instance.domElementId,
254
255
  variantId: instance.variantId,
@@ -259,7 +260,7 @@ export function useComponentEditMode({
259
260
  const { renderInstance } = await import('../utils/component-renderer');
260
261
  const instanceElement = renderInstance(component, instance, iframeDoc);
261
262
 
262
- console.log('[handleConfirmCreateComponent] Rendered instance element:', {
263
+ debugLog('[handleConfirmCreateComponent] Rendered instance element:', {
263
264
  tagName: instanceElement.tagName,
264
265
  className: instanceElement.className,
265
266
  innerHTML: instanceElement.innerHTML.substring(0, 200),
@@ -281,7 +282,7 @@ export function useComponentEditMode({
281
282
  // Update history
282
283
  notifyIframeChange(true);
283
284
 
284
- console.log('[handleConfirmCreateComponent] Replaced element with instance:', instance.id);
285
+ debugLog('[handleConfirmCreateComponent] Replaced element with instance:', instance.id);
285
286
  }
286
287
 
287
288
  setCreateComponentDialogOpen(false);
@@ -349,11 +350,11 @@ export function useComponentEditMode({
349
350
  originalPageHtmlRef.current = artboard.innerHTML;
350
351
 
351
352
  // DEBUG: Log what instances are being saved
352
- console.log('[enterComponentEditMode] ========== SAVING PAGE HTML ==========');
353
+ debugLog('[enterComponentEditMode] ========== SAVING PAGE HTML ==========');
353
354
  const instancesToSave = artboard.querySelectorAll('[data-component-instance]');
354
- console.log('[enterComponentEditMode] Saving', instancesToSave.length, 'instances');
355
+ debugLog('[enterComponentEditMode] Saving', instancesToSave.length, 'instances');
355
356
  instancesToSave.forEach((inst, idx) => {
356
- console.log(`[enterComponentEditMode] Instance ${idx} text being saved:`, inst.textContent?.substring(0, 100));
357
+ debugLog(`[enterComponentEditMode] Instance ${idx} text being saved:`, inst.textContent?.substring(0, 100));
357
358
  });
358
359
  }
359
360
 
@@ -458,16 +459,16 @@ export function useComponentEditMode({
458
459
  const artboard = iframeDoc.getElementById('artboard');
459
460
 
460
461
  // DEBUG: Log what we're about to restore
461
- console.log('[exitComponentEditMode] ========== HTML RESTORE DEBUG ==========');
462
- console.log('[exitComponentEditMode] originalPageHtmlRef exists:', originalPageHtmlRef.current !== null);
462
+ debugLog('[exitComponentEditMode] ========== HTML RESTORE DEBUG ==========');
463
+ debugLog('[exitComponentEditMode] originalPageHtmlRef exists:', originalPageHtmlRef.current !== null);
463
464
  if (originalPageHtmlRef.current) {
464
465
  // Extract instance text from saved HTML for debugging
465
466
  const tempDiv = document.createElement('div');
466
467
  tempDiv.innerHTML = originalPageHtmlRef.current;
467
468
  const savedInstances = tempDiv.querySelectorAll('[data-component-instance]');
468
- console.log('[exitComponentEditMode] Saved HTML has', savedInstances.length, 'instances');
469
+ debugLog('[exitComponentEditMode] Saved HTML has', savedInstances.length, 'instances');
469
470
  savedInstances.forEach((inst, idx) => {
470
- console.log(`[exitComponentEditMode] Saved instance ${idx} text:`, inst.textContent?.substring(0, 100));
471
+ debugLog(`[exitComponentEditMode] Saved instance ${idx} text:`, inst.textContent?.substring(0, 100));
471
472
  });
472
473
  }
473
474
 
@@ -477,9 +478,9 @@ export function useComponentEditMode({
477
478
 
478
479
  // DEBUG: Log what's in the DOM after restore
479
480
  const restoredInstances = artboard.querySelectorAll('[data-component-instance]');
480
- console.log('[exitComponentEditMode] After restore, DOM has', restoredInstances.length, 'instances');
481
+ debugLog('[exitComponentEditMode] After restore, DOM has', restoredInstances.length, 'instances');
481
482
  restoredInstances.forEach((inst, idx) => {
482
- console.log(`[exitComponentEditMode] Restored instance ${idx} text:`, inst.textContent?.substring(0, 100));
483
+ debugLog(`[exitComponentEditMode] Restored instance ${idx} text:`, inst.textContent?.substring(0, 100));
483
484
  });
484
485
  }
485
486
 
@@ -489,8 +490,8 @@ export function useComponentEditMode({
489
490
  const instancesArray = Array.from(componentInstances.values());
490
491
 
491
492
  // Debug: Log the updated master and instances
492
- console.log('[exitComponentEditMode] ========== SYNC DEBUG ==========');
493
- console.log('[exitComponentEditMode] Updated master:', {
493
+ debugLog('[exitComponentEditMode] ========== SYNC DEBUG ==========');
494
+ debugLog('[exitComponentEditMode] Updated master:', {
494
495
  id: updatedMaster.id,
495
496
  name: updatedMaster.name,
496
497
  variantsCount: updatedMaster.variants.length,
@@ -499,7 +500,7 @@ export function useComponentEditMode({
499
500
  // Log master's text content
500
501
  const defaultVariant = updatedMaster.variants.find(v => v.id === updatedMaster.defaultVariantId);
501
502
  if (defaultVariant) {
502
- console.log('[exitComponentEditMode] Master rootElement:', {
503
+ debugLog('[exitComponentEditMode] Master rootElement:', {
503
504
  tagName: defaultVariant.rootElement.tagName,
504
505
  textContent: defaultVariant.rootElement.textContent?.substring(0, 100),
505
506
  innerHTML: defaultVariant.rootElement.innerHTML?.substring(0, 100),
@@ -507,7 +508,7 @@ export function useComponentEditMode({
507
508
  });
508
509
  }
509
510
 
510
- console.log('[exitComponentEditMode] Instances to sync:', instancesArray.map(i => ({
511
+ debugLog('[exitComponentEditMode] Instances to sync:', instancesArray.map(i => ({
511
512
  id: i.id,
512
513
  masterComponentId: i.masterComponentId,
513
514
  domElementId: i.domElementId,
@@ -519,9 +520,9 @@ export function useComponentEditMode({
519
520
  const artboardForSync = iframeDoc.getElementById('artboard');
520
521
  if (artboardForSync) {
521
522
  const instanceElements = artboardForSync.querySelectorAll(`[data-component-master="${updatedMaster.id}"]`);
522
- console.log('[exitComponentEditMode] Instance elements in DOM:', instanceElements.length);
523
+ debugLog('[exitComponentEditMode] Instance elements in DOM:', instanceElements.length);
523
524
  instanceElements.forEach((el, i) => {
524
- console.log(`[exitComponentEditMode] DOM instance ${i}:`, {
525
+ debugLog(`[exitComponentEditMode] DOM instance ${i}:`, {
525
526
  instanceId: el.getAttribute('data-component-instance'),
526
527
  masterId: el.getAttribute('data-component-master'),
527
528
  elementId: el.getAttribute('data-element-id'),
@@ -532,11 +533,11 @@ export function useComponentEditMode({
532
533
 
533
534
  // Propagate master changes to all instances in the DOM
534
535
  const syncResult = propagateMasterChanges(updatedMaster, instancesArray, iframeDoc);
535
- console.log('[exitComponentEditMode] Synced instances:', syncResult);
536
+ debugLog('[exitComponentEditMode] Synced instances:', syncResult);
536
537
 
537
538
  // Persist updated instances with their newly detected overrides
538
539
  if (syncResult.updatedInstances.length > 0) {
539
- console.log('[exitComponentEditMode] Persisting updated instances with overrides:',
540
+ debugLog('[exitComponentEditMode] Persisting updated instances with overrides:',
540
541
  syncResult.updatedInstances.map(i => ({
541
542
  id: i.id,
542
543
  overridesCount: i.overrides.length,
@@ -576,7 +577,7 @@ export function useComponentEditMode({
576
577
  // Go to main component - enters component edit mode to directly edit the master in canvas
577
578
  const handleGoToMainComponent = useCallback(() => {
578
579
  if (!selectedElementInstance) return;
579
- console.log('Go to main component:', selectedElementInstance.masterComponentId);
580
+ debugLog('Go to main component:', selectedElementInstance.masterComponentId);
580
581
  enterComponentEditMode(selectedElementInstance.masterComponentId);
581
582
  }, [selectedElementInstance, enterComponentEditMode]);
582
583
 
@@ -610,7 +611,7 @@ export function useComponentEditMode({
610
611
  }
611
612
  }
612
613
 
613
- console.log('Detached instance:', selectedElementInstance.id);
614
+ debugLog('Detached instance:', selectedElementInstance.id);
614
615
  closeContextMenu();
615
616
  toast.success('インスタンスを解除しました');
616
617
  } catch (error) {
@@ -650,7 +651,7 @@ export function useComponentEditMode({
650
651
  }
651
652
  }
652
653
 
653
- console.log('Reset overrides for instance:', selectedElementInstance.id);
654
+ debugLog('Reset overrides for instance:', selectedElementInstance.id);
654
655
  closeContextMenu();
655
656
  toast.success('オーバーライドをリセットしました');
656
657
  } catch (error) {
@@ -662,7 +663,7 @@ export function useComponentEditMode({
662
663
  // Push overrides to main (placeholder - requires more complex implementation)
663
664
  const handlePushOverridesToMain = useCallback(() => {
664
665
  if (!selectedElementInstance) return;
665
- console.log('Push overrides to main - not yet implemented');
666
+ debugLog('Push overrides to main - not yet implemented');
666
667
  closeContextMenu();
667
668
  toast.info('この機能は現在開発中です');
668
669
  }, [selectedElementInstance, closeContextMenu]);
@@ -9,6 +9,7 @@ import { useEditorContext } from '../EditorContext';
9
9
  import { buildDomTree, updateSelectionBox } from '../utils/dom-utils';
10
10
  import { extractElementInfo } from '../utils/style-utils';
11
11
  import type { DOMTreeNode } from '../types';
12
+ import { debugLog } from '../utils/debug';
12
13
 
13
14
  /**
14
15
  * postMessageの受信と処理を行うHook
@@ -55,7 +56,7 @@ export function useEditorMessages() {
55
56
 
56
57
  // DOMツリー更新
57
58
  if (event.data?.type === 'DOM_TREE_UPDATED') {
58
- console.log('[EditorMessages] DOM_TREE_UPDATED received:', event.data.tree?.length, 'nodes');
59
+ debugLog('[EditorMessages] DOM_TREE_UPDATED received:', event.data.tree?.length, 'nodes');
59
60
  if (event.data.tree && Array.isArray(event.data.tree)) {
60
61
  setDomTree(event.data.tree);
61
62
  // 初期状態で最初のレベルを展開
@@ -71,7 +72,7 @@ export function useEditorMessages() {
71
72
  const iframeDoc = getIframeDoc();
72
73
  if (iframeDoc) {
73
74
  const tree = buildDomTree(iframeDoc);
74
- console.log('[EditorMessages] Rebuilt DOM tree:', tree.length, 'nodes');
75
+ debugLog('[EditorMessages] Rebuilt DOM tree:', tree.length, 'nodes');
75
76
  setDomTree(tree);
76
77
  }
77
78
  }