@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.
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 +123 -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 +179 -77
  16. package/src/editor/components/multi-page/PageFramePreview.tsx +99 -21
  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 +194 -26
  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 +43 -11
  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)。
@@ -372,8 +374,22 @@ function FrontendVisualEditorInner({
372
374
  useEffect(() => { if (pptCommentsOpen) setCommentsMounted(true); }, [pptCommentsOpen]);
373
375
  // PowerPoint風UIの右ペイン「図の書式設定」(影・反射・光彩・ぼかし)
374
376
  const [pptFormatPaneOpen, setPptFormatPaneOpen] = useState(false);
377
+ // コメントツール(C)にした時点で右のコメントパネルも開く。範囲を描き終えるまで開かないと、
378
+ // 書き込む先が見えないまま範囲を引くことになる。入口(ツールバー・校閲タブ・
379
+ // パネルの「範囲を指定」・キー C)はどれも activeTool を変えるだけなので、ここ 1 か所で拾う。
380
+ // ツールを抜けても閉じない(閉じるのは利用者の操作)。開いているときは state が同じ値で止まる
381
+ useEffect(() => {
382
+ if (activeTool !== 'comment' || !can('commentAction')) return;
383
+ setPptFormatPaneOpen(false);
384
+ setPptCommentsOpen(true);
385
+ }, [activeTool]);
375
386
  const [pptCommentFocus, setPptCommentFocus] = useState(0);
376
387
  const [pptActiveThread, setPptActiveThread] = useState<string | null>(null);
388
+ // コメントツール(C)で指定した範囲の下書き。投稿・解除・Esc・ページ切替で消える
389
+ const [commentDraftRect, setCommentDraftRect] = useState<CommentRect | null>(null);
390
+ const commentDraftRectRef = useRef<CommentRect | null>(null);
391
+ commentDraftRectRef.current = commentDraftRect;
392
+ useEffect(() => { setCommentDraftRect(null); }, [currentContentId]);
377
393
 
378
394
  // CSS変数パネル状態
379
395
  const [isVariablesPanelOpen, setIsVariablesPanelOpen] = useState(false);
@@ -411,7 +427,7 @@ function FrontendVisualEditorInner({
411
427
  useEffect(() => {
412
428
  // iframeが準備できていない場合は何もしない
413
429
  if (!iframeReady) {
414
- console.log('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
430
+ debugLog('[applyCssVariablesToIframe] Waiting for iframe to be ready...');
415
431
  return;
416
432
  }
417
433
  if (!cssString) return;
@@ -433,9 +449,9 @@ function FrontendVisualEditorInner({
433
449
 
434
450
  if (styleElement.textContent !== cssString) {
435
451
  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 })));
452
+ debugLog('[applyCssVariablesToIframe] Applied CSS variables:', variables.length, 'variables');
453
+ debugLog('[applyCssVariablesToIframe] CSS String:', cssString);
454
+ debugLog('[applyCssVariablesToIframe] Variables:', variables.map(v => ({ name: v.name, cssName: v.cssName, value: v.value })));
439
455
  }
440
456
  }, [cssString, variables.length, getIframeDoc, iframeReady]);
441
457
 
@@ -462,7 +478,7 @@ function FrontendVisualEditorInner({
462
478
  ? `/api/websites/${parentId}/pages/${contentId}`
463
479
  : `/api/presentations/${parentId}/slides/${contentId}`;
464
480
  await api.patch(apiPath, { layoutMode });
465
- console.log('[FrontendVisualEditor] Saved layout mode:', layoutMode);
481
+ debugLog('[FrontendVisualEditor] Saved layout mode:', layoutMode);
466
482
  } catch (error) {
467
483
  console.error('[FrontendVisualEditor] Failed to save layout mode:', error);
468
484
  }
@@ -1118,7 +1134,7 @@ function FrontendVisualEditorInner({
1118
1134
  }
1119
1135
  } else if (event.data?.type === 'IFRAME_PASTE_IMAGE') {
1120
1136
  // iframe内でペーストされた画像を処理
1121
- console.log('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
1137
+ debugLog('[FrontendVisualEditor] Received IFRAME_PASTE_IMAGE message', event.data);
1122
1138
  const { dataUrl, fileName, fileType } = event.data;
1123
1139
  if (dataUrl && fileType?.startsWith('image/')) {
1124
1140
  try {
@@ -1133,12 +1149,14 @@ function FrontendVisualEditorInner({
1133
1149
  } else if (event.data?.type === 'IFRAME_COMPONENT_DROP') {
1134
1150
  // iframe内にドロップされたコンポーネントを処理
1135
1151
  setIsDraggingOver(false);
1152
+ // 印は挿入より先に消す。残したまま挿入すると履歴と保存HTMLに混ざる
1153
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1136
1154
  const { componentData, x, y } = event.data;
1137
- console.log('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
1155
+ debugLog('[FrontendVisualEditor] Received IFRAME_COMPONENT_DROP:', componentData, 'at', x, y);
1138
1156
  if (componentData && contentId) {
1139
1157
  try {
1140
1158
  const { componentId } = JSON.parse(componentData);
1141
- console.log('[FrontendVisualEditor] Parsed componentId:', componentId);
1159
+ debugLog('[FrontendVisualEditor] Parsed componentId:', componentId);
1142
1160
  if (componentId && partsMode) {
1143
1161
  // 部品モード: 実体化してフローへ(iframe 内の座標がそのまま来る)
1144
1162
  const iframeDoc = iframeRef.current?.contentDocument;
@@ -1170,11 +1188,11 @@ function FrontendVisualEditorInner({
1170
1188
  } else if (componentId) {
1171
1189
  // インスタンスを作成
1172
1190
  const instance = createInstance(componentId, undefined, contentId);
1173
- console.log('[FrontendVisualEditor] Created instance:', instance);
1191
+ debugLog('[FrontendVisualEditor] Created instance:', instance);
1174
1192
  if (instance) {
1175
1193
  // マスターコンポーネントを取得
1176
1194
  const master = getMasterComponent(componentId);
1177
- console.log('[FrontendVisualEditor] Master component:', master);
1195
+ debugLog('[FrontendVisualEditor] Master component:', master);
1178
1196
  if (master && iframeRef.current) {
1179
1197
  const iframeDoc = iframeRef.current.contentDocument;
1180
1198
  if (iframeDoc) {
@@ -1202,7 +1220,7 @@ function FrontendVisualEditorInner({
1202
1220
  element.classList.add('selected');
1203
1221
  }
1204
1222
 
1205
- console.log('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
1223
+ debugLog('[FrontendVisualEditor] Component instance created via iframe message:', instance.id);
1206
1224
  }
1207
1225
  } else {
1208
1226
  console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
@@ -1223,17 +1241,24 @@ function FrontendVisualEditorInner({
1223
1241
  }, [uploadFromFile, contentId, createInstance, getMasterComponent, iframeRef, notifyIframeChange, setSelectedElement, setSelectedElementIds, partsMode, materializePartInstance, editorMode]);
1224
1242
 
1225
1243
  // ドラッグ&ドロップハンドラ
1244
+ //
1245
+ // [不具合の修正] 以前は「部品のドラッグ」と「ファイルのドラッグ」を同じ条件で扱い、
1246
+ // 部品をパネルからキャンバスへ運ぶ間じゅう「ここに画像をドロップ」の青いオーバーレイが
1247
+ // 紙面を覆っていた。落とせる場所が分からなくなるうえ、画像を入れる操作だと誤解させる。
1248
+ // 画像用のオーバーレイは Files のときだけ出し、部品は紙面の中に出す挿入位置の横棒
1249
+ // (drop-target.ts の印)で示す。
1226
1250
  const handleDragEnter = useCallback((e: React.DragEvent) => {
1227
1251
  e.preventDefault();
1228
1252
  e.stopPropagation();
1229
1253
 
1230
1254
  dragCounterRef.current++;
1231
1255
 
1232
- // コンポーネントまたはファイルがドラッグされているか確認
1233
- if (e.dataTransfer.types.includes('application/x-editor-component') ||
1234
- e.dataTransfer.types.includes('Files')) {
1256
+ if (e.dataTransfer.types.includes('Files')) {
1235
1257
  setIsDraggingOver(true);
1236
1258
  e.dataTransfer.dropEffect = 'copy';
1259
+ } else if (e.dataTransfer.types.includes('application/x-editor-component')) {
1260
+ // 落とせることは dropEffect で示すだけ。オーバーレイは出さない
1261
+ e.dataTransfer.dropEffect = 'copy';
1237
1262
  }
1238
1263
  }, []);
1239
1264
 
@@ -1241,7 +1266,6 @@ function FrontendVisualEditorInner({
1241
1266
  e.preventDefault();
1242
1267
  e.stopPropagation();
1243
1268
 
1244
- // コンポーネントまたはファイルがドラッグされているか確認
1245
1269
  if (e.dataTransfer.types.includes('application/x-editor-component') ||
1246
1270
  e.dataTransfer.types.includes('Files')) {
1247
1271
  e.dataTransfer.dropEffect = 'copy';
@@ -1251,14 +1275,17 @@ function FrontendVisualEditorInner({
1251
1275
  const handleDragLeave = useCallback((e: React.DragEvent) => {
1252
1276
  e.preventDefault();
1253
1277
  e.stopPropagation();
1254
-
1278
+
1255
1279
  dragCounterRef.current--;
1256
-
1280
+
1257
1281
  // カウンターが0になったら本当に外に出た
1258
1282
  if (dragCounterRef.current === 0) {
1259
1283
  setIsDraggingOver(false);
1284
+ // 紙面の外に出たら挿入位置の印も消す(iframe 側の dragleave が
1285
+ // 来ないまま外へ抜ける経路があるため、ここでも必ず落とす)
1286
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1260
1287
  }
1261
- }, []);
1288
+ }, [iframeRef]);
1262
1289
 
1263
1290
  const handleDrop = useCallback(async (e: React.DragEvent) => {
1264
1291
  e.preventDefault();
@@ -1267,14 +1294,16 @@ function FrontendVisualEditorInner({
1267
1294
  // ドラッグ状態をリセット
1268
1295
  dragCounterRef.current = 0;
1269
1296
  setIsDraggingOver(false);
1297
+ // 印は履歴に載る前に消す(notifyIframeChange より必ず先)
1298
+ clearPartDropIndicator(iframeRef.current?.contentDocument);
1270
1299
 
1271
1300
  // コンポーネントのドロップをチェック
1272
1301
  const componentData = e.dataTransfer.getData('application/x-editor-component');
1273
- console.log('[FrontendVisualEditor] Drop event - componentData:', componentData);
1302
+ debugLog('[FrontendVisualEditor] Drop event - componentData:', componentData);
1274
1303
  if (componentData) {
1275
1304
  try {
1276
1305
  const { componentId, variantId } = JSON.parse(componentData);
1277
- console.log('[FrontendVisualEditor] Parsed componentId:', componentId, 'variantId:', variantId, 'contentId:', contentId, 'iframeRef:', !!iframeRef.current);
1306
+ debugLog('[FrontendVisualEditor] Parsed componentId:', componentId, 'variantId:', variantId, 'contentId:', contentId, 'iframeRef:', !!iframeRef.current);
1278
1307
  if (componentId && iframeRef.current && contentId) {
1279
1308
  const iframeDoc = iframeRef.current.contentDocument;
1280
1309
  if (iframeDoc) {
@@ -1283,6 +1312,28 @@ function FrontendVisualEditorInner({
1283
1312
  // 縮小表示されている。画面座標をそのまま使うとスライド左上を原点にできないので、
1284
1313
  // スライドのルート要素の実測矩形を基準に、スライド内座標へ換算する。
1285
1314
  const iframeRect = iframeRef.current.getBoundingClientRect();
1315
+
1316
+ // [不具合の修正] いま編集していないページの紙面に落としたときに、
1317
+ // 黙って現在ページの末尾へ入れてしまうのを止める。
1318
+ //
1319
+ // キャンバス表示では、紙面の枠(`[data-page-preview]`)の中の iframe に
1320
+ // `pointer-events: none` が掛かっている。そのため別ページの紙面に落としても
1321
+ // drop はその iframe ではなく親ドキュメント側に来て、この関数が受ける。
1322
+ // ここは「現在ページの iframe」を基準に座標を出すので、別ページの上で
1323
+ // 落とすと換算結果が紙面の外になり、挿入先が見つからず末尾送りになっていた。
1324
+ // 落とした場所と違うページに黙って入るのは気づけないので、入れずに捨てる。
1325
+ // (どのページに落としたかを見て、そのページを開いてから入れるのが本筋だが、
1326
+ // それはマルチフレーム側の担当なので、ここでは誤挿入を防ぐに留める)
1327
+ const insideLiveFrame =
1328
+ e.clientX >= iframeRect.left &&
1329
+ e.clientX <= iframeRect.right &&
1330
+ e.clientY >= iframeRect.top &&
1331
+ e.clientY <= iframeRect.bottom;
1332
+ if (!insideLiveFrame) {
1333
+ console.warn('[FrontendVisualEditor] いま編集しているページの外に落とされたため、部品を入れませんでした');
1334
+ return;
1335
+ }
1336
+
1286
1337
  // 親の座標 → iframe の座標。iframe が外側の transform で縮んでいる
1287
1338
  // (マルチフレーム)ときは実測の倍率で割る。単独表示では 1
1288
1339
  const outerScale = iframeRect.width / (iframeRef.current.clientWidth || iframeRect.width) || 1;
@@ -1336,7 +1387,7 @@ function FrontendVisualEditorInner({
1336
1387
 
1337
1388
  // インスタンスを作成(バリアントIDが指定されていれば使用)
1338
1389
  // Pass position directly to createInstance to avoid React state batching race condition
1339
- console.log('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
1390
+ debugLog('[FrontendVisualEditor] Creating instance for component:', componentId, 'variant:', variantId);
1340
1391
  const instance = createInstance(
1341
1392
  componentId,
1342
1393
  variantId,
@@ -1347,11 +1398,11 @@ function FrontendVisualEditorInner({
1347
1398
  undefined, // initialPropertyValues
1348
1399
  { x, y } // initialPosition
1349
1400
  );
1350
- console.log('[FrontendVisualEditor] Created instance:', instance);
1401
+ debugLog('[FrontendVisualEditor] Created instance:', instance);
1351
1402
  if (instance) {
1352
1403
  // マスターコンポーネントを取得
1353
1404
  const master = getMasterComponent(componentId);
1354
- console.log('[FrontendVisualEditor] Master component:', master);
1405
+ debugLog('[FrontendVisualEditor] Master component:', master);
1355
1406
  if (master) {
1356
1407
  // インスタンスをHTMLにレンダリング (position will be applied from instance.position)
1357
1408
  const { renderInstance } = await import('./utils/component-renderer');
@@ -1374,7 +1425,7 @@ function FrontendVisualEditorInner({
1374
1425
  element.classList.add('selected');
1375
1426
  }
1376
1427
 
1377
- console.log('[FrontendVisualEditor] Component instance created:', instance.id);
1428
+ debugLog('[FrontendVisualEditor] Component instance created:', instance.id);
1378
1429
  } else {
1379
1430
  console.error('[FrontendVisualEditor] Master component not found for id:', componentId);
1380
1431
  }
@@ -1422,7 +1473,7 @@ function FrontendVisualEditorInner({
1422
1473
  const handlePaste = useCallback(async (e: Event) => {
1423
1474
  if (handledPasteRef.current.has(e)) return;
1424
1475
  handledPasteRef.current.add(e);
1425
- console.log('[FrontendVisualEditor] handlePaste triggered');
1476
+ debugLog('[FrontendVisualEditor] handlePaste triggered');
1426
1477
  const clipboardEvent = e as ClipboardEvent;
1427
1478
 
1428
1479
  // テキスト編集中は通常のペーストを許可
@@ -1473,14 +1524,14 @@ function FrontendVisualEditorInner({
1473
1524
  }
1474
1525
 
1475
1526
  const items = Array.from(clipboardEvent.clipboardData.items);
1476
- console.log('[FrontendVisualEditor] Clipboard items:', items.map(i => ({ type: i.type, kind: i.kind })));
1527
+ debugLog('[FrontendVisualEditor] Clipboard items:', items.map(i => ({ type: i.type, kind: i.kind })));
1477
1528
 
1478
1529
  // デバッグ: クリップボードの内容を詳細ログ
1479
1530
  debugClipboard(clipboardEvent.clipboardData);
1480
1531
 
1481
1532
  // コンテンツタイプを最初に判定(Excel/Wordは画像より優先)
1482
1533
  const contentType = detectContentType(clipboardEvent.clipboardData);
1483
- console.log('[FrontendVisualEditor] Detected content type:', contentType);
1534
+ debugLog('[FrontendVisualEditor] Detected content type:', contentType);
1484
1535
 
1485
1536
  // 画像の場合(純粋な画像ペーストのみ、Excel/Wordは除外済み)
1486
1537
  if (contentType === 'image') {
@@ -1512,7 +1563,7 @@ function FrontendVisualEditorInner({
1512
1563
  }
1513
1564
  }
1514
1565
 
1515
- console.log('[FrontendVisualEditor] Calling uploadFromClipboard for image');
1566
+ debugLog('[FrontendVisualEditor] Calling uploadFromClipboard for image');
1516
1567
  await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
1517
1568
  return;
1518
1569
  }
@@ -1521,44 +1572,44 @@ function FrontendVisualEditorInner({
1521
1572
  if (canHandleRichPaste(clipboardEvent.clipboardData)) {
1522
1573
  e.preventDefault();
1523
1574
  e.stopPropagation();
1524
- console.log('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
1575
+ debugLog('[FrontendVisualEditor] Calling pasteRichContent for:', contentType);
1525
1576
  const result = await pasteRichContent(clipboardEvent.clipboardData);
1526
- console.log('[FrontendVisualEditor] Rich paste result:', result);
1577
+ debugLog('[FrontendVisualEditor] Rich paste result:', result);
1527
1578
 
1528
1579
  // Figmaなどでデコード失敗した場合は画像としてフォールバック
1529
1580
  if (result.shouldFallbackToImage) {
1530
- console.log('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
1581
+ debugLog('[FrontendVisualEditor] Rich paste failed, falling back to image upload');
1531
1582
  const imageResult = await uploadFromClipboard(clipboardEvent.clipboardData, { x: 100, y: 100 });
1532
1583
 
1533
1584
  // 画像もない場合はプレーンテキストとしてペースト
1534
1585
  if (!imageResult) {
1535
- console.log('[FrontendVisualEditor] No image found, falling back to plain text');
1586
+ debugLog('[FrontendVisualEditor] No image found, falling back to plain text');
1536
1587
  const plainText = clipboardEvent.clipboardData.getData('text/plain');
1537
1588
  if (plainText) {
1538
1589
  // プレーンテキストとして強制的に処理
1539
1590
  const textResult = await pasteRichContent(clipboardEvent.clipboardData, 'plain-text');
1540
- console.log('[FrontendVisualEditor] Plain text paste result:', textResult);
1591
+ debugLog('[FrontendVisualEditor] Plain text paste result:', textResult);
1541
1592
  }
1542
1593
  }
1543
1594
  }
1544
1595
  return;
1545
1596
  }
1546
1597
  } else {
1547
- console.log('[FrontendVisualEditor] No clipboardData available');
1598
+ debugLog('[FrontendVisualEditor] No clipboardData available');
1548
1599
  }
1549
1600
  }, [getIframeDoc, uploadFromClipboard, canHandleRichPaste, pasteRichContent, debugClipboard, detectContentType]);
1550
1601
 
1551
1602
  // メインドキュメントとiframe両方にペーストイベントを登録
1552
1603
  useEffect(() => {
1553
- console.log('[FrontendVisualEditor] Registering paste event listeners');
1604
+ debugLog('[FrontendVisualEditor] Registering paste event listeners');
1554
1605
 
1555
1606
  // デバッグ用:ウィンドウレベルでキャプチャフェーズでリスナー追加
1556
1607
  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);
1608
+ debugLog('[FrontendVisualEditor] Window paste event captured (capture phase)', e);
1609
+ debugLog('[FrontendVisualEditor] Target:', e.target);
1610
+ debugLog('[FrontendVisualEditor] ClipboardData:', e.clipboardData);
1560
1611
  if (e.clipboardData) {
1561
- console.log('[FrontendVisualEditor] Items:', Array.from(e.clipboardData.items).map(i => ({ type: i.type, kind: i.kind })));
1612
+ debugLog('[FrontendVisualEditor] Items:', Array.from(e.clipboardData.items).map(i => ({ type: i.type, kind: i.kind })));
1562
1613
  }
1563
1614
  };
1564
1615
  window.addEventListener('paste', debugPasteHandler, true); // capture phase
@@ -1672,6 +1723,14 @@ function FrontendVisualEditorInner({
1672
1723
  // Scaleツールなど特定のツールを使用中の場合、選択ツールに戻す
1673
1724
  if (activeTool !== 'select') {
1674
1725
  setActiveTool('select');
1726
+ // コメントツールを抜けるだけ(下書きは次の Esc で消す = 1 段ずつ戻る)
1727
+ if (activeTool === 'comment') return;
1728
+ }
1729
+
1730
+ // コメントの範囲の下書きがあれば、それを解除して終わり
1731
+ if (commentDraftRectRef.current) {
1732
+ setCommentDraftRect(null);
1733
+ return;
1675
1734
  }
1676
1735
 
1677
1736
  if (iframeDoc) {
@@ -1863,6 +1922,8 @@ function FrontendVisualEditorInner({
1863
1922
  eraser: () => setActiveTool('eraser'),
1864
1923
  text: () => setActiveTool('text'),
1865
1924
  frame: () => setActiveTool('frame'),
1925
+ // コメントの範囲指定(コメント機能があるホストだけ)
1926
+ comment: can('commentAction') ? () => setActiveTool('comment') : undefined,
1866
1927
  // 編集操作
1867
1928
  // undo/redo は常に生やす(履歴が無いときは内部で何もしない)。
1868
1929
  // 条件付きで undefined にするとブラウザ既定の取り消しが走ってしまう。
@@ -2022,6 +2083,7 @@ function FrontendVisualEditorInner({
2022
2083
  setPptCommentsOpen(true);
2023
2084
  setPptCommentFocus((n) => n + 1);
2024
2085
  },
2086
+ pickRegion: () => setActiveTool('comment'),
2025
2087
  }}
2026
2088
  />
2027
2089
  </>
@@ -2172,6 +2234,7 @@ function FrontendVisualEditorInner({
2172
2234
  onImageUpload={() => openFilePicker({ x: 100, y: 100 })}
2173
2235
  onOpenMediaLibrary={can('apiFetch') ? () => setIsMediaLibraryOpen(true) : undefined}
2174
2236
  isMediaReplaceMode={isImageSelected}
2237
+ canComment={can('commentAction')}
2175
2238
  onAiRegenerate={can('apiFetch') && selectedElement && selectedElementIds.length <= 1 ? openAiPrompt : undefined}
2176
2239
  onOpenVariables={() => setIsVariablesPanelOpen(true)}
2177
2240
  hasVariables={hasVariables}
@@ -2218,9 +2281,22 @@ function FrontendVisualEditorInner({
2218
2281
  <PptCommentMarkers
2219
2282
  page={Number(currentContentId ?? contentId) || 1}
2220
2283
  onOpenThread={(id) => {
2284
+ setPptFormatPaneOpen(false);
2221
2285
  setPptCommentsOpen(true);
2222
2286
  setPptActiveThread(id);
2223
2287
  }}
2288
+ activeThreadId={pptCommentsOpen ? pptActiveThread : null}
2289
+ draftRect={commentDraftRect}
2290
+ regionActive={activeTool === 'comment'}
2291
+ onRegion={(rect) => {
2292
+ // 範囲が決まったら選択ツールに戻し、パネルを開いて本文へフォーカス
2293
+ setCommentDraftRect(rect);
2294
+ setActiveTool('select');
2295
+ setPptFormatPaneOpen(false);
2296
+ setPptCommentsOpen(true);
2297
+ setPptActiveThread(null);
2298
+ setPptCommentFocus((n) => n + 1);
2299
+ }}
2224
2300
  />
2225
2301
  )}
2226
2302
  {isPpt && pptFormatPaneOpen && (
@@ -2235,6 +2311,9 @@ function FrontendVisualEditorInner({
2235
2311
  focusSignal={pptCommentFocus}
2236
2312
  activeThreadId={pptActiveThread}
2237
2313
  onActiveThread={setPptActiveThread}
2314
+ pendingRect={commentDraftRect}
2315
+ onClearPendingRect={() => setCommentDraftRect(null)}
2316
+ onPickRegion={() => setActiveTool('comment')}
2238
2317
  />
2239
2318
  </div>
2240
2319
  )}
@@ -2512,7 +2591,7 @@ function FrontendVisualEditorInner({
2512
2591
 
2513
2592
  // 絶対配置に変換(トップレベルでimport済み)
2514
2593
  const count = convertToAbsolutePositioning(iframeDoc);
2515
- console.log('[Toast] Converted', count, 'elements to absolute positioning');
2594
+ debugLog('[Toast] Converted', count, 'elements to absolute positioning');
2516
2595
 
2517
2596
  // DOMツリーを再構築
2518
2597
  const tree = buildDomTree(iframeDoc);
@@ -2628,7 +2707,7 @@ export function FrontendVisualEditor({
2628
2707
  ? await fromIo()
2629
2708
  : await getCSSVariablesList(effectiveParentId!, scope);
2630
2709
  setInitialVariables(variables);
2631
- console.log(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
2710
+ debugLog(`[FrontendVisualEditor] Loaded CSS variables (${fromIo ? 'io' : scope}):`, variables.length);
2632
2711
  } catch (error) {
2633
2712
  console.error('[FrontendVisualEditor] Failed to load CSS variables:', error);
2634
2713
  } finally {
@@ -2644,7 +2723,7 @@ export function FrontendVisualEditor({
2644
2723
  const toIo = io().saveVariables;
2645
2724
  if (toIo) {
2646
2725
  await toIo(variables);
2647
- console.log('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
2726
+ debugLog('[FrontendVisualEditor] Saved CSS variables (io):', variables.length);
2648
2727
  return;
2649
2728
  }
2650
2729
  if (!effectiveParentId) {
@@ -2653,7 +2732,7 @@ export function FrontendVisualEditor({
2653
2732
  // editorModeに応じてスコープを決定
2654
2733
  const scope: CSSVariableScope = editorMode === 'webpage' ? 'website' : 'presentation';
2655
2734
  await saveCSSVariables(effectiveParentId, variables, { scope });
2656
- console.log(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
2735
+ debugLog(`[FrontendVisualEditor] Saved CSS variables (${scope}):`, variables.length);
2657
2736
  }, [effectiveParentId, editorMode]);
2658
2737
 
2659
2738
  // CSS変数をロードするコールバック