@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
@@ -21,6 +21,7 @@ import {
21
21
  } from './hooks/useKeyboardShortcuts';
22
22
  import type { MoveElementResult } from './hooks/useElementActions';
23
23
  import { EditorProvider, useEditorContext, type ContentListItem, type EditorMode } from './EditorContext';
24
+ import type { DocumentAttributes } from './contexts/EditorArtboardContext';
24
25
  import { PptTitleBar, PptRibbon, PptThumbnails, PptStatusBar, initialPptTheme, PPT_PALETTES, type PptTheme } from './components/ppt/PptChrome';
25
26
  import { LeftPanel } from './components/shell/LeftPanel';
26
27
  import { useAltMeasure } from './hooks/useAltMeasure';
@@ -34,7 +35,7 @@ import { registerAutoSaveFlush, flushAutoSave, beginContentSwitch, endContentSwi
34
35
  import { getCleanHtml } from './utils/html-utils';
35
36
  import { EditorAppearanceContext, useEditorTheme } from './contexts/EditorAppearanceContext';
36
37
  import { CanvasAppearance } from './components/shell/CanvasAppearance';
37
- import { can, io } from '../io';
38
+ import { can, io, type CommentRect } from '../io';
38
39
  import { insertIntoFlow } from './parts';
39
40
  import type { Slide } from '../types/slide';
40
41
  import {
@@ -92,6 +93,8 @@ import {
92
93
  import { Input } from '../components/ui/input';
93
94
  import { Button } from '../components/ui/button';
94
95
  import { cn } from '../lib/utils';
96
+ import { clearPartDropIndicator } from './utils/drop-target';
97
+ import { debugLog } from './utils/debug';
95
98
 
96
99
  /**
97
100
  * 自動保存のデバウンス(ms)。
@@ -155,8 +158,18 @@ export interface FrontendVisualEditorProps {
155
158
  onContentChange?: (contentId: string) => void;
156
159
  /** キャンバスの表示位置(倍率・スクロール)を記憶するキー。省略時は parentId */
157
160
  canvasStorageKey?: string;
158
- /** 外部から渡すコンテンツリスト(指定時はAPI取得をスキップ) */
161
+ /**
162
+ * 外部から渡すコンテンツリスト(指定時はAPI取得をスキップ)。
163
+ * parentId を渡すとキャンバスが階層のツリーで並び、href を渡すと「別タブで開く」が付き、
164
+ * revision を進めるとそのページの紙面が読み直される(ContentListItem を参照)
165
+ */
159
166
  contentList?: ContentListItem[];
167
+ /**
168
+ * 文書(`<html>`)に付ける属性。生きているエディタと見るだけの紙面の両方に、読み直さずに反映する。
169
+ * 利用側の CSS が `html[data-…]` で切り替える表示(構成ラフの注釈カラムの表示・非表示など)に使う。
170
+ * 値が null / undefined の属性は外す
171
+ */
172
+ documentAttributes?: DocumentAttributes;
160
173
  /**
161
174
  * 左パネルの「ページ」タブに差し込む中身。
162
175
  *
@@ -363,6 +376,11 @@ function FrontendVisualEditorInner({
363
376
  const [pptFormatPaneOpen, setPptFormatPaneOpen] = useState(false);
364
377
  const [pptCommentFocus, setPptCommentFocus] = useState(0);
365
378
  const [pptActiveThread, setPptActiveThread] = useState<string | null>(null);
379
+ // コメントツール(C)で指定した範囲の下書き。投稿・解除・Esc・ページ切替で消える
380
+ const [commentDraftRect, setCommentDraftRect] = useState<CommentRect | null>(null);
381
+ const commentDraftRectRef = useRef<CommentRect | null>(null);
382
+ commentDraftRectRef.current = commentDraftRect;
383
+ useEffect(() => { setCommentDraftRect(null); }, [currentContentId]);
366
384
 
367
385
  // CSS変数パネル状態
368
386
  const [isVariablesPanelOpen, setIsVariablesPanelOpen] = useState(false);
@@ -400,7 +418,7 @@ function FrontendVisualEditorInner({
400
418
  useEffect(() => {
401
419
  // iframeが準備できていない場合は何もしない
402
420
  if (!iframeReady) {
403
- console.log('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
421
+ debugLog('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
404
422
  return;
405
423
  }
406
424
  if (!cssString) return;
@@ -422,9 +440,9 @@ function FrontendVisualEditorInner({
422
440
 
423
441
  if (styleElement.textContent !== cssString) {
424
442
  styleElement.textContent = cssString;
425
- console.log('[applyCssVariablesToIframe] Applied CSS variables:', variables.length, 'variables');
426
- console.log('[applyCssVariablesToIframe] CSS String:', cssString);
427
- console.log('[applyCssVariablesToIframe] Variables:', variables.map(v => ({ name: v.name, cssName: v.cssName, value: v.value })));
443
+ debugLog('[applyCssVariablesToIframe] Applied CSS variables:', variables.length, 'variables');
444
+ debugLog('[applyCssVariablesToIframe] CSS String:', cssString);
445
+ debugLog('[applyCssVariablesToIframe] Variables:', variables.map(v => ({ name: v.name, cssName: v.cssName, value: v.value })));
428
446
  }
429
447
  }, [cssString, variables.length, getIframeDoc, iframeReady]);
430
448
 
@@ -451,7 +469,7 @@ function FrontendVisualEditorInner({
451
469
  ? `/api/websites/${parentId}/pages/${contentId}`
452
470
  : `/api/presentations/${parentId}/slides/${contentId}`;
453
471
  await api.patch(apiPath, { layoutMode });
454
- console.log('[FrontendVisualEditor] Saved layout mode:', layoutMode);
472
+ debugLog('[FrontendVisualEditor] Saved layout mode:', layoutMode);
455
473
  } catch (error) {
456
474
  console.error('[FrontendVisualEditor] Failed to save layout mode:', error);
457
475
  }
@@ -1107,7 +1125,7 @@ function FrontendVisualEditorInner({
1107
1125
  }
1108
1126
  } else if (event.data?.type === 'IFRAME_PASTE_IMAGE') {
1109
1127
  // iframe内でペーストされた画像を処理
1110
- console.log('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
1128
+ debugLog('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
1111
1129
  const { dataUrl, fileName, fileType } = event.data;
1112
1130
  if (dataUrl && fileType?.startsWith('image/')) {
1113
1131
  try {
@@ -1122,12 +1140,14 @@ function FrontendVisualEditorInner({
1122
1140
  } else if (event.data?.type === 'IFRAME_COMPONENT_DROP') {
1123
1141
  // iframe内にドロップされたコンポーネントを処理
1124
1142
  setIsDraggingOver(false);
1143
+ // 印は挿入より先に消す。残したまま挿入すると履歴と保存HTMLに混ざる
1144
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1125
1145
  const { componentData, x, y } = event.data;
1126
- console.log('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
1146
+ debugLog('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
1127
1147
  if (componentData && contentId) {
1128
1148
  try {
1129
1149
  const { componentId } = JSON.parse(componentData);
1130
- console.log('[FrontendVisualEditor] Parsed componentId:', componentId);
1150
+ debugLog('[FrontendVisualEditor] Parsed componentId:', componentId);
1131
1151
  if (componentId && partsMode) {
1132
1152
  // 部品モード: 実体化してフローへ(iframe 内の座標がそのまま来る)
1133
1153
  const iframeDoc = iframeRef.current?.contentDocument;
@@ -1159,11 +1179,11 @@ function FrontendVisualEditorInner({
1159
1179
  } else if (componentId) {
1160
1180
  // インスタンスを作成
1161
1181
  const instance = createInstance(componentId, undefined, contentId);
1162
- console.log('[FrontendVisualEditor] Created instance:', instance);
1182
+ debugLog('[FrontendVisualEditor] Created instance:', instance);
1163
1183
  if (instance) {
1164
1184
  // マスターコンポーネントを取得
1165
1185
  const master = getMasterComponent(componentId);
1166
- console.log('[FrontendVisualEditor] Master component:', master);
1186
+ debugLog('[FrontendVisualEditor] Master component:', master);
1167
1187
  if (master && iframeRef.current) {
1168
1188
  const iframeDoc = iframeRef.current.contentDocument;
1169
1189
  if (iframeDoc) {
@@ -1191,7 +1211,7 @@ function FrontendVisualEditorInner({
1191
1211
  element.classList.add('selected');
1192
1212
  }
1193
1213
 
1194
- console.log('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
1214
+ debugLog('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
1195
1215
  }
1196
1216
  } else {
1197
1217
  console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
@@ -1212,17 +1232,24 @@ function FrontendVisualEditorInner({
1212
1232
  }, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds, partsMode, materializePartInstance, editorMode]);
1213
1233
 
1214
1234
  // ドラッグ&ドロップハンドラ
1235
+ //
1236
+ // [不具合の修正] 以前は「部品のドラッグ」と「ファイルのドラッグ」を同じ条件で扱い、
1237
+ // 部品をパネルからキャンバスへ運ぶ間じゅう「ここに画像をドロップ」の青いオーバーレイが
1238
+ // 紙面を覆っていた。落とせる場所が分からなくなるうえ、画像を入れる操作だと誤解させる。
1239
+ // 画像用のオーバーレイは Files のときだけ出し、部品は紙面の中に出す挿入位置の横棒
1240
+ // (drop-target.ts の印)で示す。
1215
1241
  const handleDragEnter = useCallback((e: React.DragEvent) => {
1216
1242
  e.preventDefault();
1217
1243
  e.stopPropagation();
1218
1244
 
1219
1245
  dragCounterRef.current++;
1220
1246
 
1221
- // コンポーネントまたはファイルがドラッグされているか確認
1222
- if (e.dataTransfer.types.includes('application/x-editor-component') ||
1223
- e.dataTransfer.types.includes('Files')) {
1247
+ if (e.dataTransfer.types.includes('Files')) {
1224
1248
  setIsDraggingOver(true);
1225
1249
  e.dataTransfer.dropEffect = 'copy';
1250
+ } else if (e.dataTransfer.types.includes('application/x-editor-component')) {
1251
+ // 落とせることは dropEffect で示すだけ。オーバーレイは出さない
1252
+ e.dataTransfer.dropEffect = 'copy';
1226
1253
  }
1227
1254
  }, []);
1228
1255
 
@@ -1230,7 +1257,6 @@ function FrontendVisualEditorInner({
1230
1257
  e.preventDefault();
1231
1258
  e.stopPropagation();
1232
1259
 
1233
- // コンポーネントまたはファイルがドラッグされているか確認
1234
1260
  if (e.dataTransfer.types.includes('application/x-editor-component') ||
1235
1261
  e.dataTransfer.types.includes('Files')) {
1236
1262
  e.dataTransfer.dropEffect = 'copy';
@@ -1240,14 +1266,17 @@ function FrontendVisualEditorInner({
1240
1266
  const handleDragLeave = useCallback((e: React.DragEvent) => {
1241
1267
  e.preventDefault();
1242
1268
  e.stopPropagation();
1243
-
1269
+
1244
1270
  dragCounterRef.current--;
1245
-
1271
+
1246
1272
  // カウンターが0になったら本当に外に出た
1247
1273
  if (dragCounterRef.current === 0) {
1248
1274
  setIsDraggingOver(false);
1275
+ // 紙面の外に出たら挿入位置の印も消す(iframe 側の dragleave が
1276
+ // 来ないまま外へ抜ける経路があるため、ここでも必ず落とす)
1277
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1249
1278
  }
1250
- }, []);
1279
+ }, [iframeRef]);
1251
1280
 
1252
1281
  const handleDrop = useCallback(async (e: React.DragEvent) => {
1253
1282
  e.preventDefault();
@@ -1256,14 +1285,16 @@ function FrontendVisualEditorInner({
1256
1285
  // ドラッグ状態をリセット
1257
1286
  dragCounterRef.current = 0;
1258
1287
  setIsDraggingOver(false);
1288
+ // 印は履歴に載る前に消す(notifyIframeChange より必ず先)
1289
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1259
1290
 
1260
1291
  // コンポーネントのドロップをチェック
1261
1292
  const componentData = e.dataTransfer.getData('application/x-editor-component');
1262
- console.log('[FrontendVisualEditor] Drop event - componentData:', componentData);
1293
+ debugLog('[FrontendVisualEditor] Drop event - componentData:', componentData);
1263
1294
  if (componentData) {
1264
1295
  try {
1265
1296
  const { componentId, variantId } = JSON.parse(componentData);
1266
- console.log('[FrontendVisualEditor] Parsed componentId:', componentId, 'variantId:', variantId, 'contentId:', contentId, 'iframeRef:', !!iframeRef.current);
1297
+ debugLog('[FrontendVisualEditor] Parsed componentId:', componentId, 'variantId:', variantId, 'contentId:', contentId, 'iframeRef:', !!iframeRef.current);
1267
1298
  if (componentId && iframeRef.current && contentId) {
1268
1299
  const iframeDoc = iframeRef.current.contentDocument;
1269
1300
  if (iframeDoc) {
@@ -1272,6 +1303,28 @@ function FrontendVisualEditorInner({
1272
1303
  // 縮小表示されている。画面座標をそのまま使うとスライド左上を原点にできないので、
1273
1304
  // スライドのルート要素の実測矩形を基準に、スライド内座標へ換算する。
1274
1305
  const iframeRect = iframeRef.current.getBoundingClientRect();
1306
+
1307
+ // [不具合の修正] いま編集していないページの紙面に落としたときに、
1308
+ // 黙って現在ページの末尾へ入れてしまうのを止める。
1309
+ //
1310
+ // キャンバス表示では、紙面の枠(`[data-page-preview]`)の中の iframe に
1311
+ // `pointer-events: none` が掛かっている。そのため別ページの紙面に落としても
1312
+ // drop はその iframe ではなく親ドキュメント側に来て、この関数が受ける。
1313
+ // ここは「現在ページの iframe」を基準に座標を出すので、別ページの上で
1314
+ // 落とすと換算結果が紙面の外になり、挿入先が見つからず末尾送りになっていた。
1315
+ // 落とした場所と違うページに黙って入るのは気づけないので、入れずに捨てる。
1316
+ // (どのページに落としたかを見て、そのページを開いてから入れるのが本筋だが、
1317
+ // それはマルチフレーム側の担当なので、ここでは誤挿入を防ぐに留める)
1318
+ const insideLiveFrame =
1319
+ e.clientX >= iframeRect.left &&
1320
+ e.clientX <= iframeRect.right &&
1321
+ e.clientY >= iframeRect.top &&
1322
+ e.clientY <= iframeRect.bottom;
1323
+ if (!insideLiveFrame) {
1324
+ console.warn('[FrontendVisualEditor] いま編集しているページの外に落とされたため、部品を入れませんでした');
1325
+ return;
1326
+ }
1327
+
1275
1328
  // 親の座標 → iframe の座標。iframe が外側の transform で縮んでいる
1276
1329
  // (マルチフレーム)ときは実測の倍率で割る。単独表示では 1
1277
1330
  const outerScale = iframeRect.width / (iframeRef.current.clientWidth || iframeRect.width) || 1;
@@ -1325,7 +1378,7 @@ function FrontendVisualEditorInner({
1325
1378
 
1326
1379
  // インスタンスを作成(バリアントIDが指定されていれば使用)
1327
1380
  // Pass position directly to createInstance to avoid React state batching race condition
1328
- console.log('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
1381
+ debugLog('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
1329
1382
  const instance = createInstance(
1330
1383
  componentId,
1331
1384
  variantId,
@@ -1336,11 +1389,11 @@ function FrontendVisualEditorInner({
1336
1389
  undefined, // initialPropertyValues
1337
1390
  { x, y } // initialPosition
1338
1391
  );
1339
- console.log('[FrontendVisualEditor] Created instance:', instance);
1392
+ debugLog('[FrontendVisualEditor] Created instance:', instance);
1340
1393
  if (instance) {
1341
1394
  // マスターコンポーネントを取得
1342
1395
  const master = getMasterComponent(componentId);
1343
- console.log('[FrontendVisualEditor] Master component:', master);
1396
+ debugLog('[FrontendVisualEditor] Master component:', master);
1344
1397
  if (master) {
1345
1398
  // インスタンスをHTMLにレンダリング (position will be applied from instance.position)
1346
1399
  const { renderInstance } = await import('./utils/component-renderer');
@@ -1363,7 +1416,7 @@ function FrontendVisualEditorInner({
1363
1416
  element.classList.add('selected');
1364
1417
  }
1365
1418
 
1366
- console.log('[FrontendVisualEditor] Component instance created:', instance.id);
1419
+ debugLog('[FrontendVisualEditor] Component instance created:', instance.id);
1367
1420
  } else {
1368
1421
  console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
1369
1422
  }
@@ -1411,7 +1464,7 @@ function FrontendVisualEditorInner({
1411
1464
  const handlePaste = useCallback(async (e: Event) => {
1412
1465
  if (handledPasteRef.current.has(e)) return;
1413
1466
  handledPasteRef.current.add(e);
1414
- console.log('[FrontendVisualEditor] handlePaste triggered');
1467
+ debugLog('[FrontendVisualEditor] handlePaste triggered');
1415
1468
  const clipboardEvent = e as ClipboardEvent;
1416
1469
 
1417
1470
  // テキスト編集中は通常のペーストを許可
@@ -1462,14 +1515,14 @@ function FrontendVisualEditorInner({
1462
1515
  }
1463
1516
 
1464
1517
  const items = Array.from(clipboardEvent.clipboardData.items);
1465
- console.log('[FrontendVisualEditor] Clipboard items:', items.map(i => ({ type: i.type, kind: i.kind })));
1518
+ debugLog('[FrontendVisualEditor] Clipboard items:', items.map(i => ({ type: i.type, kind: i.kind })));
1466
1519
 
1467
1520
  // デバッグ: クリップボードの内容を詳細ログ
1468
1521
  debugClipboard(clipboardEvent.clipboardData);
1469
1522
 
1470
1523
  // コンテンツタイプを最初に判定(Excel/Wordは画像より優先)
1471
1524
  const contentType = detectContentType(clipboardEvent.clipboardData);
1472
- console.log('[FrontendVisualEditor] Detected content type:', contentType);
1525
+ debugLog('[FrontendVisualEditor] Detected content type:', contentType);
1473
1526
 
1474
1527
  // 画像の場合(純粋な画像ペーストのみ、Excel/Wordは除外済み)
1475
1528
  if (contentType === 'image') {
@@ -1501,7 +1554,7 @@ function FrontendVisualEditorInner({
1501
1554
  }
1502
1555
  }
1503
1556
 
1504
- console.log('[FrontendVisualEditor] Calling uploadFromClipboard for image');
1557
+ debugLog('[FrontendVisualEditor] Calling uploadFromClipboard for image');
1505
1558
  await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
1506
1559
  return;
1507
1560
  }
@@ -1510,44 +1563,44 @@ function FrontendVisualEditorInner({
1510
1563
  if (canHandleRichPaste(clipboardEvent.clipboardData)) {
1511
1564
  e.preventDefault();
1512
1565
  e.stopPropagation();
1513
- console.log('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
1566
+ debugLog('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
1514
1567
  const result = await pasteRichContent(clipboardEvent.clipboardData);
1515
- console.log('[FrontendVisualEditor] Rich paste result:', result);
1568
+ debugLog('[FrontendVisualEditor] Rich paste result:', result);
1516
1569
 
1517
1570
  // Figmaなどでデコード失敗した場合は画像としてフォールバック
1518
1571
  if (result.shouldFallbackToImage) {
1519
- console.log('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
1572
+ debugLog('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
1520
1573
  const imageResult = await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
1521
1574
 
1522
1575
  // 画像もない場合はプレーンテキストとしてペースト
1523
1576
  if (!imageResult) {
1524
- console.log('[FrontendVisualEditor] No image found, falling back to plain text');
1577
+ debugLog('[FrontendVisualEditor] No image found, falling back to plain text');
1525
1578
  const plainText = clipboardEvent.clipboardData.getData('text/plain');
1526
1579
  if (plainText) {
1527
1580
  // プレーンテキストとして強制的に処理
1528
1581
  const textResult = await pasteRichContent(clipboardEvent.clipboardData, 'plain-text');
1529
- console.log('[FrontendVisualEditor] Plain text paste result:', textResult);
1582
+ debugLog('[FrontendVisualEditor] Plain text paste result:', textResult);
1530
1583
  }
1531
1584
  }
1532
1585
  }
1533
1586
  return;
1534
1587
  }
1535
1588
  } else {
1536
- console.log('[FrontendVisualEditor] No clipboardData available');
1589
+ debugLog('[FrontendVisualEditor] No clipboardData available');
1537
1590
  }
1538
1591
  }, [getIframeDoc, uploadFromClipboard, canHandleRichPaste, pasteRichContent, debugClipboard, detectContentType]);
1539
1592
 
1540
1593
  // メインドキュメントとiframe両方にペーストイベントを登録
1541
1594
  useEffect(() => {
1542
- console.log('[FrontendVisualEditor] Registering paste event listeners');
1595
+ debugLog('[FrontendVisualEditor] Registering paste event listeners');
1543
1596
 
1544
1597
  // デバッグ用:ウィンドウレベルでキャプチャフェーズでリスナー追加
1545
1598
  const debugPasteHandler = (e: ClipboardEvent) => {
1546
- console.log('[FrontendVisualEditor] Window paste event captured (capture phase)', e);
1547
- console.log('[FrontendVisualEditor] Target:', e.target);
1548
- console.log('[FrontendVisualEditor] ClipboardData:', e.clipboardData);
1599
+ debugLog('[FrontendVisualEditor] Window paste event captured (capture phase)', e);
1600
+ debugLog('[FrontendVisualEditor] Target:', e.target);
1601
+ debugLog('[FrontendVisualEditor] ClipboardData:', e.clipboardData);
1549
1602
  if (e.clipboardData) {
1550
- console.log('[FrontendVisualEditor] Items:', Array.from(e.clipboardData.items).map(i => ({ type: i.type, kind: i.kind })));
1603
+ debugLog('[FrontendVisualEditor] Items:', Array.from(e.clipboardData.items).map(i => ({ type: i.type, kind: i.kind })));
1551
1604
  }
1552
1605
  };
1553
1606
  window.addEventListener('paste', debugPasteHandler, true); // capture phase
@@ -1661,6 +1714,14 @@ function FrontendVisualEditorInner({
1661
1714
  // Scaleツールなど特定のツールを使用中の場合、選択ツールに戻す
1662
1715
  if (activeTool !== 'select') {
1663
1716
  setActiveTool('select');
1717
+ // コメントツールを抜けるだけ(下書きは次の Esc で消す = 1 段ずつ戻る)
1718
+ if (activeTool === 'comment') return;
1719
+ }
1720
+
1721
+ // コメントの範囲の下書きがあれば、それを解除して終わり
1722
+ if (commentDraftRectRef.current) {
1723
+ setCommentDraftRect(null);
1724
+ return;
1664
1725
  }
1665
1726
 
1666
1727
  if (iframeDoc) {
@@ -1852,6 +1913,8 @@ function FrontendVisualEditorInner({
1852
1913
  eraser: () => setActiveTool('eraser'),
1853
1914
  text: () => setActiveTool('text'),
1854
1915
  frame: () => setActiveTool('frame'),
1916
+ // コメントの範囲指定(コメント機能があるホストだけ)
1917
+ comment: can('commentAction') ? () => setActiveTool('comment') : undefined,
1855
1918
  // 編集操作
1856
1919
  // undo/redo は常に生やす(履歴が無いときは内部で何もしない)。
1857
1920
  // 条件付きで undefined にするとブラウザ既定の取り消しが走ってしまう。
@@ -2011,6 +2074,7 @@ function FrontendVisualEditorInner({
2011
2074
  setPptCommentsOpen(true);
2012
2075
  setPptCommentFocus((n) => n + 1);
2013
2076
  },
2077
+ pickRegion: () => setActiveTool('comment'),
2014
2078
  }}
2015
2079
  />
2016
2080
  </>
@@ -2161,6 +2225,7 @@ function FrontendVisualEditorInner({
2161
2225
  onImageUpload={() => openFilePicker({ x: 100, y: 100 })}
2162
2226
  onOpenMediaLibrary={can('apiFetch') ? () => setIsMediaLibraryOpen(true) : undefined}
2163
2227
  isMediaReplaceMode={isImageSelected}
2228
+ canComment={can('commentAction')}
2164
2229
  onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
2165
2230
  onOpenVariables={() => setIsVariablesPanelOpen(true)}
2166
2231
  hasVariables={hasVariables}
@@ -2207,9 +2272,22 @@ function FrontendVisualEditorInner({
2207
2272
  <PptCommentMarkers
2208
2273
  page={Number(currentContentId ?? contentId) || 1}
2209
2274
  onOpenThread={(id) => {
2275
+ setPptFormatPaneOpen(false);
2210
2276
  setPptCommentsOpen(true);
2211
2277
  setPptActiveThread(id);
2212
2278
  }}
2279
+ activeThreadId={pptCommentsOpen ? pptActiveThread : null}
2280
+ draftRect={commentDraftRect}
2281
+ regionActive={activeTool === 'comment'}
2282
+ onRegion={(rect) => {
2283
+ // 範囲が決まったら選択ツールに戻し、パネルを開いて本文へフォーカス
2284
+ setCommentDraftRect(rect);
2285
+ setActiveTool('select');
2286
+ setPptFormatPaneOpen(false);
2287
+ setPptCommentsOpen(true);
2288
+ setPptActiveThread(null);
2289
+ setPptCommentFocus((n) => n + 1);
2290
+ }}
2213
2291
  />
2214
2292
  )}
2215
2293
  {isPpt && pptFormatPaneOpen && (
@@ -2224,6 +2302,9 @@ function FrontendVisualEditorInner({
2224
2302
  focusSignal={pptCommentFocus}
2225
2303
  activeThreadId={pptActiveThread}
2226
2304
  onActiveThread={setPptActiveThread}
2305
+ pendingRect={commentDraftRect}
2306
+ onClearPendingRect={() => setCommentDraftRect(null)}
2307
+ onPickRegion={() => setActiveTool('comment')}
2227
2308
  />
2228
2309
  </div>
2229
2310
  )}
@@ -2501,7 +2582,7 @@ function FrontendVisualEditorInner({
2501
2582
 
2502
2583
  // 絶対配置に変換(トップレベルでimport済み)
2503
2584
  const count = convertToAbsolutePositioning(iframeDoc);
2504
- console.log('[Toast] Converted', count, 'elements to absolute positioning');
2585
+ debugLog('[Toast] Converted', count, 'elements to absolute positioning');
2505
2586
 
2506
2587
  // DOMツリーを再構築
2507
2588
  const tree = buildDomTree(iframeDoc);
@@ -2577,6 +2658,7 @@ export function FrontendVisualEditor({
2577
2658
  headerExtra,
2578
2659
  onContentChange: onContentChangeProp,
2579
2660
  canvasStorageKey,
2661
+ documentAttributes,
2580
2662
  }: FrontendVisualEditorProps) {
2581
2663
  const { getIdToken } = useAuth();
2582
2664
  const [contentList, setContentList] = useState<ContentListItem[]>(externalContentList || []);
@@ -2616,7 +2698,7 @@ export function FrontendVisualEditor({
2616
2698
  ? await fromIo()
2617
2699
  : await getCSSVariablesList(effectiveParentId!, scope);
2618
2700
  setInitialVariables(variables);
2619
- console.log(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
2701
+ debugLog(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
2620
2702
  } catch (error) {
2621
2703
  console.error('[FrontendVisualEditor] Failed to load CSS variables:', error);
2622
2704
  } finally {
@@ -2632,7 +2714,7 @@ export function FrontendVisualEditor({
2632
2714
  const toIo = io().saveVariables;
2633
2715
  if (toIo) {
2634
2716
  await toIo(variables);
2635
- console.log('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
2717
+ debugLog('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
2636
2718
  return;
2637
2719
  }
2638
2720
  if (!effectiveParentId) {
@@ -2641,7 +2723,7 @@ export function FrontendVisualEditor({
2641
2723
  // editorModeに応じてスコープを決定
2642
2724
  const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
2643
2725
  await saveCSSVariables(effectiveParentId, variables, { scope });
2644
- console.log(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
2726
+ debugLog(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
2645
2727
  }, [effectiveParentId, editorMode]);
2646
2728
 
2647
2729
  // CSS変数をロードするコールバック
@@ -2856,6 +2938,7 @@ export function FrontendVisualEditor({
2856
2938
  contentList={contentList}
2857
2939
  currentContentId={currentContentId}
2858
2940
  onContentChange={handleContentChange}
2941
+ documentAttributes={documentAttributes}
2859
2942
  initialLayoutMode={currentLayoutMode}
2860
2943
  initialVariables={initialVariables}
2861
2944
  onSaveVariables={handleSaveVariables}