@growgroup/visual-editor 0.3.1 → 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 (60) hide show
  1. package/README.md +6 -1
  2. package/dist/editor.css +1 -1
  3. package/package.json +1 -1
  4. package/src/editor/EditorContext.tsx +3 -2
  5. package/src/editor/EditorToolbar.tsx +8 -2
  6. package/src/editor/FrontendVisualEditor.tsx +114 -44
  7. package/src/editor/components/EditorCanvas.tsx +73 -15
  8. package/src/editor/components/EditorFooter.tsx +2 -0
  9. package/src/editor/components/EditorHeader.tsx +3 -2
  10. package/src/editor/components/FigmaColorPicker.tsx +2 -1
  11. package/src/editor/components/JsEditorDialog.tsx +1 -0
  12. package/src/editor/components/PageSettingsDialog.tsx +1 -0
  13. package/src/editor/components/comments/CommentBoard.tsx +9 -6
  14. package/src/editor/components/multi-page/CanvasFrameDecor.tsx +172 -0
  15. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +173 -77
  16. package/src/editor/components/multi-page/PageFramePreview.tsx +96 -20
  17. package/src/editor/components/multi-page/index.ts +1 -0
  18. package/src/editor/components/ppt/PptChrome.tsx +6 -2
  19. package/src/editor/components/ppt/PptComments.tsx +411 -71
  20. package/src/editor/constants.ts +16 -1
  21. package/src/editor/contexts/EditorArtboardContext.tsx +8 -0
  22. package/src/editor/contexts/EditorComponentsContext.tsx +31 -27
  23. package/src/editor/contexts/EditorUIStateContext.tsx +4 -3
  24. package/src/editor/contexts/EditorVariablesContext.tsx +5 -4
  25. package/src/editor/contexts/MultiPageCanvasContext.tsx +26 -2
  26. package/src/editor/hooks/useAiReplace.ts +4 -3
  27. package/src/editor/hooks/useBrowserZoomPrevention.ts +2 -1
  28. package/src/editor/hooks/useComponentEditMode.ts +27 -26
  29. package/src/editor/hooks/useEditorMessages.ts +3 -2
  30. package/src/editor/hooks/useElementActions.ts +27 -26
  31. package/src/editor/hooks/useElementSelection.ts +19 -16
  32. package/src/editor/hooks/useFocusManagement.ts +7 -6
  33. package/src/editor/hooks/useGoogleFonts.ts +3 -2
  34. package/src/editor/hooks/useIframeInitializer.ts +6 -3
  35. package/src/editor/hooks/useIframeSetup.ts +4 -3
  36. package/src/editor/hooks/useImageUpload.ts +4 -3
  37. package/src/editor/hooks/useMarqueeSelection.ts +2 -1
  38. package/src/editor/hooks/usePageSettingsManager.ts +24 -23
  39. package/src/editor/hooks/useRichPaste.ts +11 -10
  40. package/src/editor/parts.ts +7 -25
  41. package/src/editor/utils/component-renderer.ts +8 -7
  42. package/src/editor/utils/component-sync.ts +16 -15
  43. package/src/editor/utils/debug.ts +52 -0
  44. package/src/editor/utils/dom-utils.ts +11 -10
  45. package/src/editor/utils/drop-target.ts +135 -0
  46. package/src/editor/utils/eyedropper.ts +3 -1
  47. package/src/editor/utils/figma-export.ts +6 -5
  48. package/src/editor/utils/figma-kiwi-decoder.ts +28 -27
  49. package/src/editor/utils/figma-kiwi-encoder.ts +9 -8
  50. package/src/editor/utils/figma-paste.ts +57 -56
  51. package/src/editor/utils/flex-utils.ts +12 -10
  52. package/src/editor/utils/override-apply.ts +3 -2
  53. package/src/editor/utils/override-detection.ts +11 -10
  54. package/src/editor/utils/tailwind-utils.ts +4 -3
  55. package/src/io.ts +22 -1
  56. package/src/lib/firebase/css-variables.ts +4 -3
  57. package/src/lib/firebase/storage.ts +3 -2
  58. package/src/styles/skin.css +9 -4
  59. package/src/types/editor.ts +6 -2
  60. package/src/version.ts +1 -1
@@ -35,7 +35,7 @@ import { registerAutoSaveFlush, flushAutoSave, beginContentSwitch, endContentSwi
35
35
  import { getCleanHtml } from './utils/html-utils';
36
36
  import { EditorAppearanceContext, useEditorTheme } from './contexts/EditorAppearanceContext';
37
37
  import { CanvasAppearance } from './components/shell/CanvasAppearance';
38
- import { can, io } from '../io';
38
+ import { can, io, type CommentRect } from '../io';
39
39
  import { insertIntoFlow } from './parts';
40
40
  import type { Slide } from '../types/slide';
41
41
  import {
@@ -93,6 +93,8 @@ import {
93
93
  import { Input } from '../components/ui/input';
94
94
  import { Button } from '../components/ui/button';
95
95
  import { cn } from '../lib/utils';
96
+ import { clearPartDropIndicator } from './utils/drop-target';
97
+ import { debugLog } from './utils/debug';
96
98
 
97
99
  /**
98
100
  * 自動保存のデバウンス(ms)。
@@ -374,6 +376,11 @@ function FrontendVisualEditorInner({
374
376
  const [pptFormatPaneOpen, setPptFormatPaneOpen] = useState(false);
375
377
  const [pptCommentFocus, setPptCommentFocus] = useState(0);
376
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]);
377
384
 
378
385
  // CSS変数パネル状態
379
386
  const [isVariablesPanelOpen, setIsVariablesPanelOpen] = useState(false);
@@ -411,7 +418,7 @@ function FrontendVisualEditorInner({
411
418
  useEffect(() => {
412
419
  // iframeが準備できていない場合は何もしない
413
420
  if (!iframeReady) {
414
- console.log('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
421
+ debugLog('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
415
422
  return;
416
423
  }
417
424
  if (!cssString) return;
@@ -433,9 +440,9 @@ function FrontendVisualEditorInner({
433
440
 
434
441
  if (styleElement.textContent !== cssString) {
435
442
  styleElement.textContent = cssString;
436
- console.log('[applyCssVariablesToIframe] Applied CSS variables:', variables.length, 'variables');
437
- console.log('[applyCssVariablesToIframe] CSS String:', cssString);
438
- 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 })));
439
446
  }
440
447
  }, [cssString, variables.length, getIframeDoc, iframeReady]);
441
448
 
@@ -462,7 +469,7 @@ function FrontendVisualEditorInner({
462
469
  ? `/api/websites/${parentId}/pages/${contentId}`
463
470
  : `/api/presentations/${parentId}/slides/${contentId}`;
464
471
  await api.patch(apiPath, { layoutMode });
465
- console.log('[FrontendVisualEditor] Saved layout mode:', layoutMode);
472
+ debugLog('[FrontendVisualEditor] Saved layout mode:', layoutMode);
466
473
  } catch (error) {
467
474
  console.error('[FrontendVisualEditor] Failed to save layout mode:', error);
468
475
  }
@@ -1118,7 +1125,7 @@ function FrontendVisualEditorInner({
1118
1125
  }
1119
1126
  } else if (event.data?.type === 'IFRAME_PASTE_IMAGE') {
1120
1127
  // iframe内でペーストされた画像を処理
1121
- console.log('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
1128
+ debugLog('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
1122
1129
  const { dataUrl, fileName, fileType } = event.data;
1123
1130
  if (dataUrl && fileType?.startsWith('image/')) {
1124
1131
  try {
@@ -1133,12 +1140,14 @@ function FrontendVisualEditorInner({
1133
1140
  } else if (event.data?.type === 'IFRAME_COMPONENT_DROP') {
1134
1141
  // iframe内にドロップされたコンポーネントを処理
1135
1142
  setIsDraggingOver(false);
1143
+ // 印は挿入より先に消す。残したまま挿入すると履歴と保存HTMLに混ざる
1144
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1136
1145
  const { componentData, x, y } = event.data;
1137
- console.log('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
1146
+ debugLog('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
1138
1147
  if (componentData && contentId) {
1139
1148
  try {
1140
1149
  const { componentId } = JSON.parse(componentData);
1141
- console.log('[FrontendVisualEditor] Parsed componentId:', componentId);
1150
+ debugLog('[FrontendVisualEditor] Parsed componentId:', componentId);
1142
1151
  if (componentId && partsMode) {
1143
1152
  // 部品モード: 実体化してフローへ(iframe 内の座標がそのまま来る)
1144
1153
  const iframeDoc = iframeRef.current?.contentDocument;
@@ -1170,11 +1179,11 @@ function FrontendVisualEditorInner({
1170
1179
  } else if (componentId) {
1171
1180
  // インスタンスを作成
1172
1181
  const instance = createInstance(componentId, undefined, contentId);
1173
- console.log('[FrontendVisualEditor] Created instance:', instance);
1182
+ debugLog('[FrontendVisualEditor] Created instance:', instance);
1174
1183
  if (instance) {
1175
1184
  // マスターコンポーネントを取得
1176
1185
  const master = getMasterComponent(componentId);
1177
- console.log('[FrontendVisualEditor] Master component:', master);
1186
+ debugLog('[FrontendVisualEditor] Master component:', master);
1178
1187
  if (master && iframeRef.current) {
1179
1188
  const iframeDoc = iframeRef.current.contentDocument;
1180
1189
  if (iframeDoc) {
@@ -1202,7 +1211,7 @@ function FrontendVisualEditorInner({
1202
1211
  element.classList.add('selected');
1203
1212
  }
1204
1213
 
1205
- console.log('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
1214
+ debugLog('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
1206
1215
  }
1207
1216
  } else {
1208
1217
  console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
@@ -1223,17 +1232,24 @@ function FrontendVisualEditorInner({
1223
1232
  }, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds, partsMode, materializePartInstance, editorMode]);
1224
1233
 
1225
1234
  // ドラッグ&ドロップハンドラ
1235
+ //
1236
+ // [不具合の修正] 以前は「部品のドラッグ」と「ファイルのドラッグ」を同じ条件で扱い、
1237
+ // 部品をパネルからキャンバスへ運ぶ間じゅう「ここに画像をドロップ」の青いオーバーレイが
1238
+ // 紙面を覆っていた。落とせる場所が分からなくなるうえ、画像を入れる操作だと誤解させる。
1239
+ // 画像用のオーバーレイは Files のときだけ出し、部品は紙面の中に出す挿入位置の横棒
1240
+ // (drop-target.ts の印)で示す。
1226
1241
  const handleDragEnter = useCallback((e: React.DragEvent) => {
1227
1242
  e.preventDefault();
1228
1243
  e.stopPropagation();
1229
1244
 
1230
1245
  dragCounterRef.current++;
1231
1246
 
1232
- // コンポーネントまたはファイルがドラッグされているか確認
1233
- if (e.dataTransfer.types.includes('application/x-editor-component') ||
1234
- e.dataTransfer.types.includes('Files')) {
1247
+ if (e.dataTransfer.types.includes('Files')) {
1235
1248
  setIsDraggingOver(true);
1236
1249
  e.dataTransfer.dropEffect = 'copy';
1250
+ } else if (e.dataTransfer.types.includes('application/x-editor-component')) {
1251
+ // 落とせることは dropEffect で示すだけ。オーバーレイは出さない
1252
+ e.dataTransfer.dropEffect = 'copy';
1237
1253
  }
1238
1254
  }, []);
1239
1255
 
@@ -1241,7 +1257,6 @@ function FrontendVisualEditorInner({
1241
1257
  e.preventDefault();
1242
1258
  e.stopPropagation();
1243
1259
 
1244
- // コンポーネントまたはファイルがドラッグされているか確認
1245
1260
  if (e.dataTransfer.types.includes('application/x-editor-component') ||
1246
1261
  e.dataTransfer.types.includes('Files')) {
1247
1262
  e.dataTransfer.dropEffect = 'copy';
@@ -1251,14 +1266,17 @@ function FrontendVisualEditorInner({
1251
1266
  const handleDragLeave = useCallback((e: React.DragEvent) => {
1252
1267
  e.preventDefault();
1253
1268
  e.stopPropagation();
1254
-
1269
+
1255
1270
  dragCounterRef.current--;
1256
-
1271
+
1257
1272
  // カウンターが0になったら本当に外に出た
1258
1273
  if (dragCounterRef.current === 0) {
1259
1274
  setIsDraggingOver(false);
1275
+ // 紙面の外に出たら挿入位置の印も消す(iframe 側の dragleave が
1276
+ // 来ないまま外へ抜ける経路があるため、ここでも必ず落とす)
1277
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1260
1278
  }
1261
- }, []);
1279
+ }, [iframeRef]);
1262
1280
 
1263
1281
  const handleDrop = useCallback(async (e: React.DragEvent) => {
1264
1282
  e.preventDefault();
@@ -1267,14 +1285,16 @@ function FrontendVisualEditorInner({
1267
1285
  // ドラッグ状態をリセット
1268
1286
  dragCounterRef.current = 0;
1269
1287
  setIsDraggingOver(false);
1288
+ // 印は履歴に載る前に消す(notifyIframeChange より必ず先)
1289
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1270
1290
 
1271
1291
  // コンポーネントのドロップをチェック
1272
1292
  const componentData = e.dataTransfer.getData('application/x-editor-component');
1273
- console.log('[FrontendVisualEditor] Drop event - componentData:', componentData);
1293
+ debugLog('[FrontendVisualEditor] Drop event - componentData:', componentData);
1274
1294
  if (componentData) {
1275
1295
  try {
1276
1296
  const { componentId, variantId } = JSON.parse(componentData);
1277
- 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);
1278
1298
  if (componentId && iframeRef.current && contentId) {
1279
1299
  const iframeDoc = iframeRef.current.contentDocument;
1280
1300
  if (iframeDoc) {
@@ -1283,6 +1303,28 @@ function FrontendVisualEditorInner({
1283
1303
  // 縮小表示されている。画面座標をそのまま使うとスライド左上を原点にできないので、
1284
1304
  // スライドのルート要素の実測矩形を基準に、スライド内座標へ換算する。
1285
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
+
1286
1328
  // 親の座標 → iframe の座標。iframe が外側の transform で縮んでいる
1287
1329
  // (マルチフレーム)ときは実測の倍率で割る。単独表示では 1
1288
1330
  const outerScale = iframeRect.width / (iframeRef.current.clientWidth || iframeRect.width) || 1;
@@ -1336,7 +1378,7 @@ function FrontendVisualEditorInner({
1336
1378
 
1337
1379
  // インスタンスを作成(バリアントIDが指定されていれば使用)
1338
1380
  // Pass position directly to createInstance to avoid React state batching race condition
1339
- console.log('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
1381
+ debugLog('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
1340
1382
  const instance = createInstance(
1341
1383
  componentId,
1342
1384
  variantId,
@@ -1347,11 +1389,11 @@ function FrontendVisualEditorInner({
1347
1389
  undefined, // initialPropertyValues
1348
1390
  { x, y } // initialPosition
1349
1391
  );
1350
- console.log('[FrontendVisualEditor] Created instance:', instance);
1392
+ debugLog('[FrontendVisualEditor] Created instance:', instance);
1351
1393
  if (instance) {
1352
1394
  // マスターコンポーネントを取得
1353
1395
  const master = getMasterComponent(componentId);
1354
- console.log('[FrontendVisualEditor] Master component:', master);
1396
+ debugLog('[FrontendVisualEditor] Master component:', master);
1355
1397
  if (master) {
1356
1398
  // インスタンスをHTMLにレンダリング (position will be applied from instance.position)
1357
1399
  const { renderInstance } = await import('./utils/component-renderer');
@@ -1374,7 +1416,7 @@ function FrontendVisualEditorInner({
1374
1416
  element.classList.add('selected');
1375
1417
  }
1376
1418
 
1377
- console.log('[FrontendVisualEditor] Component instance created:', instance.id);
1419
+ debugLog('[FrontendVisualEditor] Component instance created:', instance.id);
1378
1420
  } else {
1379
1421
  console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
1380
1422
  }
@@ -1422,7 +1464,7 @@ function FrontendVisualEditorInner({
1422
1464
  const handlePaste = useCallback(async (e: Event) => {
1423
1465
  if (handledPasteRef.current.has(e)) return;
1424
1466
  handledPasteRef.current.add(e);
1425
- console.log('[FrontendVisualEditor] handlePaste triggered');
1467
+ debugLog('[FrontendVisualEditor] handlePaste triggered');
1426
1468
  const clipboardEvent = e as ClipboardEvent;
1427
1469
 
1428
1470
  // テキスト編集中は通常のペーストを許可
@@ -1473,14 +1515,14 @@ function FrontendVisualEditorInner({
1473
1515
  }
1474
1516
 
1475
1517
  const items = Array.from(clipboardEvent.clipboardData.items);
1476
- 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 })));
1477
1519
 
1478
1520
  // デバッグ: クリップボードの内容を詳細ログ
1479
1521
  debugClipboard(clipboardEvent.clipboardData);
1480
1522
 
1481
1523
  // コンテンツタイプを最初に判定(Excel/Wordは画像より優先)
1482
1524
  const contentType = detectContentType(clipboardEvent.clipboardData);
1483
- console.log('[FrontendVisualEditor] Detected content type:', contentType);
1525
+ debugLog('[FrontendVisualEditor] Detected content type:', contentType);
1484
1526
 
1485
1527
  // 画像の場合(純粋な画像ペーストのみ、Excel/Wordは除外済み)
1486
1528
  if (contentType === 'image') {
@@ -1512,7 +1554,7 @@ function FrontendVisualEditorInner({
1512
1554
  }
1513
1555
  }
1514
1556
 
1515
- console.log('[FrontendVisualEditor] Calling uploadFromClipboard for image');
1557
+ debugLog('[FrontendVisualEditor] Calling uploadFromClipboard for image');
1516
1558
  await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
1517
1559
  return;
1518
1560
  }
@@ -1521,44 +1563,44 @@ function FrontendVisualEditorInner({
1521
1563
  if (canHandleRichPaste(clipboardEvent.clipboardData)) {
1522
1564
  e.preventDefault();
1523
1565
  e.stopPropagation();
1524
- console.log('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
1566
+ debugLog('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
1525
1567
  const result = await pasteRichContent(clipboardEvent.clipboardData);
1526
- console.log('[FrontendVisualEditor] Rich paste result:', result);
1568
+ debugLog('[FrontendVisualEditor] Rich paste result:', result);
1527
1569
 
1528
1570
  // Figmaなどでデコード失敗した場合は画像としてフォールバック
1529
1571
  if (result.shouldFallbackToImage) {
1530
- console.log('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
1572
+ debugLog('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
1531
1573
  const imageResult = await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
1532
1574
 
1533
1575
  // 画像もない場合はプレーンテキストとしてペースト
1534
1576
  if (!imageResult) {
1535
- console.log('[FrontendVisualEditor] No image found, falling back to plain text');
1577
+ debugLog('[FrontendVisualEditor] No image found, falling back to plain text');
1536
1578
  const plainText = clipboardEvent.clipboardData.getData('text/plain');
1537
1579
  if (plainText) {
1538
1580
  // プレーンテキストとして強制的に処理
1539
1581
  const textResult = await pasteRichContent(clipboardEvent.clipboardData, 'plain-text');
1540
- console.log('[FrontendVisualEditor] Plain text paste result:', textResult);
1582
+ debugLog('[FrontendVisualEditor] Plain text paste result:', textResult);
1541
1583
  }
1542
1584
  }
1543
1585
  }
1544
1586
  return;
1545
1587
  }
1546
1588
  } else {
1547
- console.log('[FrontendVisualEditor] No clipboardData available');
1589
+ debugLog('[FrontendVisualEditor] No clipboardData available');
1548
1590
  }
1549
1591
  }, [getIframeDoc, uploadFromClipboard, canHandleRichPaste, pasteRichContent, debugClipboard, detectContentType]);
1550
1592
 
1551
1593
  // メインドキュメントとiframe両方にペーストイベントを登録
1552
1594
  useEffect(() => {
1553
- console.log('[FrontendVisualEditor] Registering paste event listeners');
1595
+ debugLog('[FrontendVisualEditor] Registering paste event listeners');
1554
1596
 
1555
1597
  // デバッグ用:ウィンドウレベルでキャプチャフェーズでリスナー追加
1556
1598
  const debugPasteHandler = (e: ClipboardEvent) => {
1557
- console.log('[FrontendVisualEditor] Window paste event captured (capture phase)', e);
1558
- console.log('[FrontendVisualEditor] Target:', e.target);
1559
- 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);
1560
1602
  if (e.clipboardData) {
1561
- 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 })));
1562
1604
  }
1563
1605
  };
1564
1606
  window.addEventListener('paste', debugPasteHandler, true); // capture phase
@@ -1672,6 +1714,14 @@ function FrontendVisualEditorInner({
1672
1714
  // Scaleツールなど特定のツールを使用中の場合、選択ツールに戻す
1673
1715
  if (activeTool !== 'select') {
1674
1716
  setActiveTool('select');
1717
+ // コメントツールを抜けるだけ(下書きは次の Esc で消す = 1 段ずつ戻る)
1718
+ if (activeTool === 'comment') return;
1719
+ }
1720
+
1721
+ // コメントの範囲の下書きがあれば、それを解除して終わり
1722
+ if (commentDraftRectRef.current) {
1723
+ setCommentDraftRect(null);
1724
+ return;
1675
1725
  }
1676
1726
 
1677
1727
  if (iframeDoc) {
@@ -1863,6 +1913,8 @@ function FrontendVisualEditorInner({
1863
1913
  eraser: () => setActiveTool('eraser'),
1864
1914
  text: () => setActiveTool('text'),
1865
1915
  frame: () => setActiveTool('frame'),
1916
+ // コメントの範囲指定(コメント機能があるホストだけ)
1917
+ comment: can('commentAction') ? () => setActiveTool('comment') : undefined,
1866
1918
  // 編集操作
1867
1919
  // undo/redo は常に生やす(履歴が無いときは内部で何もしない)。
1868
1920
  // 条件付きで undefined にするとブラウザ既定の取り消しが走ってしまう。
@@ -2022,6 +2074,7 @@ function FrontendVisualEditorInner({
2022
2074
  setPptCommentsOpen(true);
2023
2075
  setPptCommentFocus((n) => n + 1);
2024
2076
  },
2077
+ pickRegion: () => setActiveTool('comment'),
2025
2078
  }}
2026
2079
  />
2027
2080
  </>
@@ -2172,6 +2225,7 @@ function FrontendVisualEditorInner({
2172
2225
  onImageUpload={() => openFilePicker({ x: 100, y: 100 })}
2173
2226
  onOpenMediaLibrary={can('apiFetch') ? () => setIsMediaLibraryOpen(true) : undefined}
2174
2227
  isMediaReplaceMode={isImageSelected}
2228
+ canComment={can('commentAction')}
2175
2229
  onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
2176
2230
  onOpenVariables={() => setIsVariablesPanelOpen(true)}
2177
2231
  hasVariables={hasVariables}
@@ -2218,9 +2272,22 @@ function FrontendVisualEditorInner({
2218
2272
  <PptCommentMarkers
2219
2273
  page={Number(currentContentId ?? contentId) || 1}
2220
2274
  onOpenThread={(id) => {
2275
+ setPptFormatPaneOpen(false);
2221
2276
  setPptCommentsOpen(true);
2222
2277
  setPptActiveThread(id);
2223
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
+ }}
2224
2291
  />
2225
2292
  )}
2226
2293
  {isPpt && pptFormatPaneOpen && (
@@ -2235,6 +2302,9 @@ function FrontendVisualEditorInner({
2235
2302
  focusSignal={pptCommentFocus}
2236
2303
  activeThreadId={pptActiveThread}
2237
2304
  onActiveThread={setPptActiveThread}
2305
+ pendingRect={commentDraftRect}
2306
+ onClearPendingRect={() => setCommentDraftRect(null)}
2307
+ onPickRegion={() => setActiveTool('comment')}
2238
2308
  />
2239
2309
  </div>
2240
2310
  )}
@@ -2512,7 +2582,7 @@ function FrontendVisualEditorInner({
2512
2582
 
2513
2583
  // 絶対配置に変換(トップレベルでimport済み)
2514
2584
  const count = convertToAbsolutePositioning(iframeDoc);
2515
- console.log('[Toast] Converted', count, 'elements to absolute positioning');
2585
+ debugLog('[Toast] Converted', count, 'elements to absolute positioning');
2516
2586
 
2517
2587
  // DOMツリーを再構築
2518
2588
  const tree = buildDomTree(iframeDoc);
@@ -2628,7 +2698,7 @@ export function FrontendVisualEditor({
2628
2698
  ? await fromIo()
2629
2699
  : await getCSSVariablesList(effectiveParentId!, scope);
2630
2700
  setInitialVariables(variables);
2631
- console.log(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
2701
+ debugLog(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
2632
2702
  } catch (error) {
2633
2703
  console.error('[FrontendVisualEditor] Failed to load CSS variables:', error);
2634
2704
  } finally {
@@ -2644,7 +2714,7 @@ export function FrontendVisualEditor({
2644
2714
  const toIo = io().saveVariables;
2645
2715
  if (toIo) {
2646
2716
  await toIo(variables);
2647
- console.log('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
2717
+ debugLog('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
2648
2718
  return;
2649
2719
  }
2650
2720
  if (!effectiveParentId) {
@@ -2653,7 +2723,7 @@ export function FrontendVisualEditor({
2653
2723
  // editorModeに応じてスコープを決定
2654
2724
  const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
2655
2725
  await saveCSSVariables(effectiveParentId, variables, { scope });
2656
- console.log(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
2726
+ debugLog(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
2657
2727
  }, [effectiveParentId, editorMode]);
2658
2728
 
2659
2729
  // CSS変数をロードするコールバック