@growgroup/visual-editor 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/editor.css +1 -1
- package/package.json +1 -1
- package/src/editor/EditorContext.tsx +3 -2
- package/src/editor/EditorToolbar.tsx +8 -2
- package/src/editor/FrontendVisualEditor.tsx +123 -44
- package/src/editor/components/EditorCanvas.tsx +73 -15
- package/src/editor/components/EditorFooter.tsx +2 -0
- package/src/editor/components/EditorHeader.tsx +3 -2
- package/src/editor/components/FigmaColorPicker.tsx +2 -1
- package/src/editor/components/JsEditorDialog.tsx +1 -0
- package/src/editor/components/PageSettingsDialog.tsx +1 -0
- package/src/editor/components/comments/CommentBoard.tsx +9 -6
- package/src/editor/components/multi-page/CanvasFrameDecor.tsx +172 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +179 -77
- package/src/editor/components/multi-page/PageFramePreview.tsx +99 -21
- package/src/editor/components/multi-page/index.ts +1 -0
- package/src/editor/components/ppt/PptChrome.tsx +6 -2
- package/src/editor/components/ppt/PptComments.tsx +411 -71
- package/src/editor/constants.ts +16 -1
- package/src/editor/contexts/EditorArtboardContext.tsx +8 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +31 -27
- package/src/editor/contexts/EditorUIStateContext.tsx +4 -3
- package/src/editor/contexts/EditorVariablesContext.tsx +5 -4
- package/src/editor/contexts/MultiPageCanvasContext.tsx +194 -26
- package/src/editor/hooks/useAiReplace.ts +4 -3
- package/src/editor/hooks/useBrowserZoomPrevention.ts +2 -1
- package/src/editor/hooks/useComponentEditMode.ts +27 -26
- package/src/editor/hooks/useEditorMessages.ts +3 -2
- package/src/editor/hooks/useElementActions.ts +27 -26
- package/src/editor/hooks/useElementSelection.ts +19 -16
- package/src/editor/hooks/useFocusManagement.ts +7 -6
- package/src/editor/hooks/useGoogleFonts.ts +3 -2
- package/src/editor/hooks/useIframeInitializer.ts +6 -3
- package/src/editor/hooks/useIframeSetup.ts +4 -3
- package/src/editor/hooks/useImageUpload.ts +4 -3
- package/src/editor/hooks/useMarqueeSelection.ts +2 -1
- package/src/editor/hooks/usePageSettingsManager.ts +24 -23
- package/src/editor/hooks/useRichPaste.ts +11 -10
- package/src/editor/parts.ts +7 -25
- package/src/editor/utils/component-renderer.ts +8 -7
- package/src/editor/utils/component-sync.ts +16 -15
- package/src/editor/utils/debug.ts +52 -0
- package/src/editor/utils/dom-utils.ts +43 -11
- package/src/editor/utils/drop-target.ts +135 -0
- package/src/editor/utils/eyedropper.ts +3 -1
- package/src/editor/utils/figma-export.ts +6 -5
- package/src/editor/utils/figma-kiwi-decoder.ts +28 -27
- package/src/editor/utils/figma-kiwi-encoder.ts +9 -8
- package/src/editor/utils/figma-paste.ts +57 -56
- package/src/editor/utils/flex-utils.ts +12 -10
- package/src/editor/utils/override-apply.ts +3 -2
- package/src/editor/utils/override-detection.ts +11 -10
- package/src/editor/utils/tailwind-utils.ts +4 -3
- package/src/io.ts +22 -1
- package/src/lib/firebase/css-variables.ts +4 -3
- package/src/lib/firebase/storage.ts +3 -2
- package/src/styles/skin.css +9 -4
- package/src/types/editor.ts +6 -2
- package/src/version.ts +1 -1
|
@@ -32,6 +32,9 @@ import { useMultiPageCanvasOptional } from "../contexts/MultiPageCanvasContext";
|
|
|
32
32
|
import { applyDocumentAttributes } from "./multi-page/PageFramePreview";
|
|
33
33
|
import { setupEmbeddedCanvasBridge } from "../utils/embedded-canvas-bridge";
|
|
34
34
|
import { endContentSwitch } from "../autosave";
|
|
35
|
+
import { showPartDropIndicator, clearPartDropIndicator, findFlowInsertion } from "../utils/drop-target";
|
|
36
|
+
import { debugLog } from '../utils/debug';
|
|
37
|
+
import { useEditorComponents } from "../contexts/EditorComponentsContext";
|
|
35
38
|
import type {
|
|
36
39
|
DOMTreeNode,
|
|
37
40
|
DragState,
|
|
@@ -189,6 +192,9 @@ export function EditorCanvas() {
|
|
|
189
192
|
const setHtmlRef = useRef(setHtml);
|
|
190
193
|
const clearHistoryRef = useRef(clearHistory);
|
|
191
194
|
const editorModeRef = useRef(editorMode);
|
|
195
|
+
// 部品モードか(io に loadParts があるか)。挿入位置の印を出してよいかの判定に使う
|
|
196
|
+
const { partsMode } = useEditorComponents();
|
|
197
|
+
const partsModeRef = useRef(partsMode);
|
|
192
198
|
|
|
193
199
|
// Webページモード用のコンテンツ高さ追跡
|
|
194
200
|
const [contentHeight, setContentHeight] = useState(WEBPAGE_MIN_HEIGHT);
|
|
@@ -208,6 +214,7 @@ export function EditorCanvas() {
|
|
|
208
214
|
setHtmlRef.current = setHtml;
|
|
209
215
|
clearHistoryRef.current = clearHistory;
|
|
210
216
|
editorModeRef.current = editorMode;
|
|
217
|
+
partsModeRef.current = partsMode;
|
|
211
218
|
contentHeightRef.current = contentHeight;
|
|
212
219
|
}, [
|
|
213
220
|
activeTool,
|
|
@@ -218,6 +225,7 @@ export function EditorCanvas() {
|
|
|
218
225
|
setHtml,
|
|
219
226
|
clearHistory,
|
|
220
227
|
editorMode,
|
|
228
|
+
partsMode,
|
|
221
229
|
contentHeight,
|
|
222
230
|
]);
|
|
223
231
|
|
|
@@ -467,7 +475,7 @@ export function EditorCanvas() {
|
|
|
467
475
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
468
476
|
if (!iframeDoc) return;
|
|
469
477
|
|
|
470
|
-
|
|
478
|
+
debugLog("[Canvas] iframe loaded, initializing...");
|
|
471
479
|
|
|
472
480
|
// 最初のペイントより先に現在の倍率を当ててから見せる。
|
|
473
481
|
// initLayout はフォント待ち等で数百msかかるため、その後に適用すると
|
|
@@ -481,7 +489,7 @@ export function EditorCanvas() {
|
|
|
481
489
|
// これにより、iframeがリロードされたり状態が変わるたびに
|
|
482
490
|
// 古いリスナーを削除せずに新しいリスナーが追加される問題を防ぐ
|
|
483
491
|
if (cleanupFunctionsRef.current.length > 0) {
|
|
484
|
-
|
|
492
|
+
debugLog("[Canvas] Cleaning up", cleanupFunctionsRef.current.length, "previous listeners");
|
|
485
493
|
cleanupFunctionsRef.current.forEach(cleanup => {
|
|
486
494
|
try {
|
|
487
495
|
cleanup();
|
|
@@ -634,7 +642,7 @@ export function EditorCanvas() {
|
|
|
634
642
|
if (iframe) iframe.focus();
|
|
635
643
|
if (iframeDoc.body) {
|
|
636
644
|
iframeDoc.body.focus();
|
|
637
|
-
|
|
645
|
+
debugLog('[Canvas] Focus restored after background click');
|
|
638
646
|
}
|
|
639
647
|
});
|
|
640
648
|
}
|
|
@@ -667,10 +675,28 @@ export function EditorCanvas() {
|
|
|
667
675
|
// 7. ドラッグ&ドロップ(画像アップロード用)
|
|
668
676
|
let dragCounter = 0;
|
|
669
677
|
|
|
678
|
+
// 部品のドラッグか(ファイルのドラッグと見分ける)。
|
|
679
|
+
// dragover 中は getData が使えないので types だけで判定する
|
|
680
|
+
const isPartDrag = (e: DragEvent) =>
|
|
681
|
+
!!e.dataTransfer?.types.includes("application/x-editor-component");
|
|
682
|
+
|
|
683
|
+
// 挿入位置の横棒を出してよい場面か。
|
|
684
|
+
// FrontendVisualEditor の drop 処理が insertIntoFlow(フローに差し込む)を使うのは
|
|
685
|
+
// 「部品モード かつ Web ページモード」のときだけで、それ以外(スライド、および
|
|
686
|
+
// Firestore のコンポーネント)は落とした座標に絶対配置する。条件を揃えないと
|
|
687
|
+
// 「横棒の位置と実際に入る位置が違う」という一番たちの悪いズレ方をする
|
|
688
|
+
const canShowFlowIndicator = () =>
|
|
689
|
+
partsModeRef.current && editorModeRef.current === "webpage";
|
|
690
|
+
|
|
691
|
+
// 前回の挿入先。同じなら描き直さない(dragover は毎秒数十回来るので、
|
|
692
|
+
// 毎回 DOM を差し替えると強制レイアウトが走り、印もちらつく)
|
|
693
|
+
let lastTarget: { parent: Element; before: Element | null } | null = null;
|
|
694
|
+
|
|
670
695
|
const handleDragEnter = (e: DragEvent) => {
|
|
671
696
|
e.preventDefault();
|
|
672
697
|
e.stopPropagation();
|
|
673
698
|
dragCounter++;
|
|
699
|
+
// 画像のオーバーレイはファイルのときだけ。部品は挿入位置の横棒で示す
|
|
674
700
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
675
701
|
window.parent.postMessage({ type: "IFRAME_DRAG_ENTER" }, "*");
|
|
676
702
|
}
|
|
@@ -681,6 +707,17 @@ export function EditorCanvas() {
|
|
|
681
707
|
e.stopPropagation();
|
|
682
708
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
683
709
|
e.dataTransfer.dropEffect = "copy";
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (isPartDrag(e)) {
|
|
713
|
+
e.dataTransfer!.dropEffect = "copy";
|
|
714
|
+
if (!canShowFlowIndicator()) return;
|
|
715
|
+
// 落とす先を紙面の中に描く。実際の挿入と同じ関数で位置を出しているので、
|
|
716
|
+
// 印の場所とずれない(drop-target.ts)
|
|
717
|
+
const next = findFlowInsertion(iframeDoc, e.clientX, e.clientY);
|
|
718
|
+
if (lastTarget && lastTarget.parent === next.parent && lastTarget.before === next.before) return;
|
|
719
|
+
lastTarget = next;
|
|
720
|
+
showPartDropIndicator(iframeDoc, e.clientX, e.clientY);
|
|
684
721
|
}
|
|
685
722
|
};
|
|
686
723
|
|
|
@@ -689,19 +726,33 @@ export function EditorCanvas() {
|
|
|
689
726
|
e.stopPropagation();
|
|
690
727
|
dragCounter--;
|
|
691
728
|
if (dragCounter === 0) {
|
|
729
|
+
lastTarget = null;
|
|
730
|
+
clearPartDropIndicator(iframeDoc);
|
|
692
731
|
window.parent.postMessage({ type: "IFRAME_DRAG_LEAVE" }, "*");
|
|
693
732
|
}
|
|
694
733
|
};
|
|
695
734
|
|
|
735
|
+
// ドラッグが途中で捨てられた(Esc・パネルの上で離した)ときも印を残さない。
|
|
736
|
+
// dragend が起きるのはドラッグ「元」= 親ドキュメントのパネル項目なので、
|
|
737
|
+
// iframe ではなく親の document で受ける
|
|
738
|
+
const handleDragEnd = () => {
|
|
739
|
+
dragCounter = 0;
|
|
740
|
+
lastTarget = null;
|
|
741
|
+
clearPartDropIndicator(iframeDoc);
|
|
742
|
+
};
|
|
743
|
+
|
|
696
744
|
const handleDrop = (e: DragEvent) => {
|
|
697
745
|
e.preventDefault();
|
|
698
746
|
e.stopPropagation();
|
|
699
747
|
dragCounter = 0;
|
|
748
|
+
lastTarget = null;
|
|
749
|
+
// 印は挿入より先に消す。残っていると履歴と保存HTMLに混ざる
|
|
750
|
+
clearPartDropIndicator(iframeDoc);
|
|
700
751
|
|
|
701
752
|
// コンポーネントのドロップをチェックして親ウィンドウに転送
|
|
702
753
|
const componentData = e.dataTransfer?.getData('application/x-editor-component');
|
|
703
754
|
if (componentData) {
|
|
704
|
-
|
|
755
|
+
debugLog('[EditorCanvas] Component drop detected, forwarding to parent:', componentData);
|
|
705
756
|
window.parent.postMessage(
|
|
706
757
|
{
|
|
707
758
|
type: "IFRAME_COMPONENT_DROP",
|
|
@@ -756,12 +807,15 @@ export function EditorCanvas() {
|
|
|
756
807
|
iframeDoc.addEventListener("dragenter", handleDragEnter);
|
|
757
808
|
iframeDoc.addEventListener("dragover", handleDragOver);
|
|
758
809
|
iframeDoc.addEventListener("dragleave", handleDragLeave);
|
|
810
|
+
document.addEventListener("dragend", handleDragEnd);
|
|
759
811
|
iframeDoc.addEventListener("drop", handleDrop);
|
|
760
812
|
cleanupFunctions.push(() => {
|
|
761
813
|
iframeDoc.removeEventListener("dragenter", handleDragEnter);
|
|
762
814
|
iframeDoc.removeEventListener("dragover", handleDragOver);
|
|
763
815
|
iframeDoc.removeEventListener("dragleave", handleDragLeave);
|
|
816
|
+
document.removeEventListener("dragend", handleDragEnd);
|
|
764
817
|
iframeDoc.removeEventListener("drop", handleDrop);
|
|
818
|
+
clearPartDropIndicator(iframeDoc);
|
|
765
819
|
});
|
|
766
820
|
|
|
767
821
|
// [修正] ここにあった「iframe内のpasteを親へpostMessageして画像を挿入する」
|
|
@@ -769,7 +823,7 @@ export function EditorCanvas() {
|
|
|
769
823
|
// 直接 paste リスナーを張っており、2系統が同時に走るため1回の貼り付けで
|
|
770
824
|
// 画像が2枚入っていた。ペーストの処理は FrontendVisualEditor の1本に集約する。
|
|
771
825
|
|
|
772
|
-
|
|
826
|
+
debugLog("[Canvas] Drag/drop handlers registered");
|
|
773
827
|
|
|
774
828
|
// 9. レイアウトモードに応じた初期化とDOMツリー構築
|
|
775
829
|
const initLayout = async () => {
|
|
@@ -818,17 +872,17 @@ export function EditorCanvas() {
|
|
|
818
872
|
// 別の幅で崩れ、デザインツールへの取り込みで意味を失う。
|
|
819
873
|
if (editorModeRef.current !== "webpage") {
|
|
820
874
|
const converted = convertToAbsolutePositioning(iframeDoc);
|
|
821
|
-
|
|
875
|
+
debugLog('[Canvas] 絶対配置へ一括変換:', converted, '要素');
|
|
822
876
|
}
|
|
823
877
|
|
|
824
878
|
// ここまでが「開いただけ」の姿。以後の変化=ユーザーの編集、と
|
|
825
879
|
// 切り分けるための指紋を全要素に刻む(保存時の変換ノイズ巻き戻しに使う)
|
|
826
880
|
const stamped = stampBaselines(iframeDoc);
|
|
827
|
-
|
|
881
|
+
debugLog('[Canvas] 書き戻し用ベースライン:', stamped, '要素');
|
|
828
882
|
|
|
829
883
|
const artboardEl = iframeDoc.getElementById("artboard");
|
|
830
884
|
const tree = buildDomTree(iframeDoc, artboardEl || undefined);
|
|
831
|
-
|
|
885
|
+
debugLog("[Canvas] Built DOM tree with", tree.length, "root nodes");
|
|
832
886
|
|
|
833
887
|
setDomTreeRef.current(tree);
|
|
834
888
|
const firstLevelIds = new Set<string>(
|
|
@@ -844,7 +898,7 @@ export function EditorCanvas() {
|
|
|
844
898
|
// 変換後の姿を「変更なし」の基準にする。これをしないと開いただけで
|
|
845
899
|
// 未保存扱いになり、サムネイル移動のたびに確認ダイアログが出る
|
|
846
900
|
setOriginalHtml(initializedHtml);
|
|
847
|
-
|
|
901
|
+
debugLog(
|
|
848
902
|
"[Canvas] History reset with initialized HTML from artboard"
|
|
849
903
|
);
|
|
850
904
|
|
|
@@ -879,7 +933,7 @@ export function EditorCanvas() {
|
|
|
879
933
|
resizeObserver.observe(artboardEl);
|
|
880
934
|
cleanupFunctions.push(() => resizeObserver.disconnect());
|
|
881
935
|
|
|
882
|
-
|
|
936
|
+
debugLog(
|
|
883
937
|
"[Canvas] Webpage mode: content height tracking initialized on artboard"
|
|
884
938
|
);
|
|
885
939
|
}
|
|
@@ -888,11 +942,11 @@ export function EditorCanvas() {
|
|
|
888
942
|
// 重要: クリーンアップ関数をrefに保存して、
|
|
889
943
|
// 次のiframeロード時やアンマウント時にクリーンアップできるようにする
|
|
890
944
|
cleanupFunctionsRef.current = cleanupFunctions;
|
|
891
|
-
|
|
945
|
+
debugLog("[Canvas] Registered", cleanupFunctions.length, "cleanup functions");
|
|
892
946
|
|
|
893
947
|
// iframeの読み込み完了をマーク(CSS変数注入等のトリガー用)
|
|
894
948
|
setIframeReady(true);
|
|
895
|
-
|
|
949
|
+
debugLog("[Canvas] iframe ready, setIframeReady(true) called");
|
|
896
950
|
|
|
897
951
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
898
952
|
}, []);
|
|
@@ -924,7 +978,7 @@ export function EditorCanvas() {
|
|
|
924
978
|
// 重要: アンマウント時にすべてのイベントリスナーをクリーンアップ
|
|
925
979
|
// これにより、メモリリークとゴーストリスナーを防ぐ
|
|
926
980
|
if (cleanupFunctionsRef.current.length > 0) {
|
|
927
|
-
|
|
981
|
+
debugLog("[Canvas] Unmounting: cleaning up", cleanupFunctionsRef.current.length, "listeners");
|
|
928
982
|
cleanupFunctionsRef.current.forEach(cleanup => {
|
|
929
983
|
try {
|
|
930
984
|
cleanup();
|
|
@@ -946,6 +1000,7 @@ export function EditorCanvas() {
|
|
|
946
1000
|
"move-mode",
|
|
947
1001
|
"draw-mode",
|
|
948
1002
|
"text-mode",
|
|
1003
|
+
"comment-mode",
|
|
949
1004
|
"tool-rectangle",
|
|
950
1005
|
"tool-ellipse",
|
|
951
1006
|
"tool-line",
|
|
@@ -975,6 +1030,9 @@ export function EditorCanvas() {
|
|
|
975
1030
|
iframeDoc.body.classList.add("draw-mode", `tool-${activeTool}`);
|
|
976
1031
|
} else if (activeTool === "text") {
|
|
977
1032
|
iframeDoc.body.classList.add("text-mode", "tool-text");
|
|
1033
|
+
} else if (activeTool === "comment") {
|
|
1034
|
+
// コメントの範囲指定。要素の選択・ホバーを止め、紙面のドラッグを範囲にする
|
|
1035
|
+
iframeDoc.body.classList.add("comment-mode");
|
|
978
1036
|
}
|
|
979
1037
|
}
|
|
980
1038
|
}, [activeTool, getIframeDoc]);
|
|
@@ -1001,7 +1059,7 @@ export function EditorCanvas() {
|
|
|
1001
1059
|
refreshSelectionOverlay(iframeDoc);
|
|
1002
1060
|
});
|
|
1003
1061
|
|
|
1004
|
-
|
|
1062
|
+
debugLog(
|
|
1005
1063
|
`[Canvas] Applied zoom: ${zoom}%, wrapper transform: scale(${currentScale})`
|
|
1006
1064
|
);
|
|
1007
1065
|
}, [zoom, editorMode, contentHeight, viewportWidth, getIframeDoc]);
|
|
@@ -1067,7 +1125,7 @@ export function EditorCanvas() {
|
|
|
1067
1125
|
: SLIDE_HEIGHT;
|
|
1068
1126
|
recalculateViewportUnits(iframeDoc, canvasWidth, canvasHeight);
|
|
1069
1127
|
|
|
1070
|
-
|
|
1128
|
+
debugLog(`[Canvas] Viewport width changed to ${viewportWidth}px, recalculated viewport units with canvas dimensions: ${canvasWidth}x${canvasHeight}`);
|
|
1071
1129
|
}, [viewportWidth, editorMode, getIframeDoc, calculateFitZoom, setFitZoom]);
|
|
1072
1130
|
|
|
1073
1131
|
return (
|
|
@@ -10,11 +10,13 @@ export function EditorFooter() {
|
|
|
10
10
|
const cmd = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘' : 'Ctrl+';
|
|
11
11
|
const hint = activeTool === 'move' ? 'ドラッグで紙面を移動 · V で選択に戻る'
|
|
12
12
|
: activeTool === 'scale' ? 'ドラッグで拡大・縮小 · V で選択に戻る'
|
|
13
|
+
: activeTool === 'comment' ? '紙面をドラッグして範囲を指定 · クリックで点 · Esc で選択に戻る'
|
|
13
14
|
: activeTool !== 'select' ? 'ドラッグで追加 · Esc でキャンセル'
|
|
14
15
|
: selectedElement || selectedElementIds.length ? 'ダブルクリックで文字を編集 · Esc で選択を解除'
|
|
15
16
|
: 'クリックで選択 · Space+ドラッグで紙面を移動';
|
|
16
17
|
const shortcuts = [
|
|
17
18
|
['選択 / 移動 / 拡大・縮小', 'V / H / K'],
|
|
19
|
+
['コメント(範囲を指定)', 'C'],
|
|
18
20
|
['元に戻す / やり直し', `${cmd}Z / ${cmd}⇧Z`],
|
|
19
21
|
['保存', `${cmd}S`], ['複製', `${cmd}D`],
|
|
20
22
|
['全体表示 / 100% / 選択範囲', `${cmd}0 / ${cmd}1 / ${cmd}2`],
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
getArtboardContent,
|
|
39
39
|
} from "../utils/dom-utils";
|
|
40
40
|
import { can } from "../../io";
|
|
41
|
+
import { debugLog } from '../utils/debug';
|
|
41
42
|
|
|
42
43
|
interface EditorHeaderProps {
|
|
43
44
|
theme?: "light" | "dark";
|
|
@@ -164,7 +165,7 @@ export function EditorHeader({
|
|
|
164
165
|
|
|
165
166
|
// 全要素を絶対配置に変換
|
|
166
167
|
const count = convertToAbsolutePositioning(iframeDoc);
|
|
167
|
-
|
|
168
|
+
debugLog(
|
|
168
169
|
"[Header] Converted",
|
|
169
170
|
count,
|
|
170
171
|
"elements to absolute positioning",
|
|
@@ -226,7 +227,7 @@ export function EditorHeader({
|
|
|
226
227
|
element.style.removeProperty("flex-grow");
|
|
227
228
|
element.style.removeProperty("flex-shrink");
|
|
228
229
|
});
|
|
229
|
-
|
|
230
|
+
debugLog(
|
|
230
231
|
"[Header] Removed absolute positioning from",
|
|
231
232
|
editableElements.length,
|
|
232
233
|
"elements",
|
|
@@ -53,6 +53,7 @@ import { useEditorVariables } from "../EditorContext";
|
|
|
53
53
|
import { isEyeDropperSupported, pickScreenColor } from "../utils/eyedropper";
|
|
54
54
|
import type { CSSVariableDefinition } from "../../types/css-variables";
|
|
55
55
|
import { isVariableReference, extractVariableName, generateVarReference } from "../../types/css-variables";
|
|
56
|
+
import { debugLog } from '../utils/debug';
|
|
56
57
|
|
|
57
58
|
// 塗りのタイプ
|
|
58
59
|
export type FillType = "solid" | "linear" | "radial" | "image" | "variable" | "none";
|
|
@@ -794,7 +795,7 @@ export function FigmaColorPicker({
|
|
|
794
795
|
// 変数選択ハンドラ
|
|
795
796
|
const handleVariableSelect = useCallback((variable: CSSVariableDefinition) => {
|
|
796
797
|
const varRef = generateVarReference(variable);
|
|
797
|
-
|
|
798
|
+
debugLog('[DEBUG FigmaColorPicker] handleVariableSelect:', {
|
|
798
799
|
variable: { id: variable.id, name: variable.name, cssName: variable.cssName },
|
|
799
800
|
generatedVarReference: varRef,
|
|
800
801
|
hasOnVariableSelect: !!onVariableSelect,
|
|
@@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
|
|
|
10
10
|
import { Button } from '../../components/ui/button';
|
|
11
11
|
import { Textarea } from '../../components/ui/textarea';
|
|
12
12
|
import { Braces, Trash2, RefreshCw, AlertTriangle } from 'lucide-react';
|
|
13
|
+
import { debugLog } from '../utils/debug';
|
|
13
14
|
|
|
14
15
|
export interface JsEditorDialogProps {
|
|
15
16
|
isOpen: boolean;
|
|
@@ -14,6 +14,7 @@ import { Label } from '../../components/ui/label';
|
|
|
14
14
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
|
|
15
15
|
import { Switch } from '../../components/ui/switch';
|
|
16
16
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
|
|
17
|
+
import { debugLog } from '../utils/debug';
|
|
17
18
|
import {
|
|
18
19
|
Settings,
|
|
19
20
|
Search,
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { useState } from 'react';
|
|
24
|
-
import { Check, CornerUpLeft, ExternalLink, MapPin, MessageSquare, RotateCcw, Send, Trash2 } from 'lucide-react';
|
|
24
|
+
import { Check, CornerUpLeft, Crosshair, ExternalLink, MapPin, MessageSquare, RotateCcw, Send, Trash2 } from 'lucide-react';
|
|
25
25
|
import type { Deck, SlideComment } from '../../../lib/deck';
|
|
26
26
|
import { PPT_PALETTES, type PptTheme } from '../ppt/PptChrome';
|
|
27
|
-
import { fmtTime, unresolvedCount } from '../ppt/PptComments';
|
|
27
|
+
import { describeRect, fmtTime, unresolvedCount } from '../ppt/PptComments';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* アンカー・返信のリンク色。
|
|
@@ -368,15 +368,18 @@ function BoardCard({
|
|
|
368
368
|
</span>
|
|
369
369
|
</div>
|
|
370
370
|
|
|
371
|
-
{/*
|
|
371
|
+
{/* 何に対する指摘か。範囲 > 要素 > ページ全体 */}
|
|
372
372
|
<div
|
|
373
373
|
className="mt-1.5 flex items-center gap-1 text-[10px]"
|
|
374
|
-
style={{ color: comment.anchorSrc ? accent : pal.sub }}
|
|
374
|
+
style={{ color: comment.anchorRect || comment.anchorSrc ? accent : pal.sub }}
|
|
375
375
|
title={comment.anchorSrc}
|
|
376
376
|
>
|
|
377
|
-
<MapPin className="h-3 w-3 shrink-0" />
|
|
377
|
+
{comment.anchorRect ? <Crosshair className="h-3 w-3 shrink-0" /> : <MapPin className="h-3 w-3 shrink-0" />}
|
|
378
378
|
<span className="truncate">
|
|
379
|
-
{comment.
|
|
379
|
+
{comment.seq != null && <span className="mr-1 tabular-nums">#{comment.seq}</span>}
|
|
380
|
+
{comment.anchorRect
|
|
381
|
+
? describeRect(comment.anchorRect)
|
|
382
|
+
: comment.anchorSrc ? (comment.anchorLabel ? `「${comment.anchorLabel}」` : '要素に添付') : 'ページ全体'}
|
|
380
383
|
</span>
|
|
381
384
|
</div>
|
|
382
385
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* フレームの枠線と階層の線を <canvas> に描く(画面の座標)。
|
|
5
|
+
*
|
|
6
|
+
* [なぜ DOM ではなく canvas か]
|
|
7
|
+
* 枠線を転写層の中の要素に持たせる(box-shadow / outline / border のどれでも)と、
|
|
8
|
+
* 倍率が変わるたびに転写層まるごとが塗り直される。26 ページで 1 段あたり 10〜20ms、
|
|
9
|
+
* 30 段の縮小で主スレッドの Paint が 870ms になる(実測)。階層の線(SVG)も同じ。
|
|
10
|
+
* 逆に、転写層の中身が「白い矩形 + 合成済みの iframe」だけなら 1 段の Paint は 0.5ms 前後。
|
|
11
|
+
*
|
|
12
|
+
* 26 ページ・30 段の縮小での実測(DPR 2、CPU 4 倍遅く):
|
|
13
|
+
* 枠線 box-shadow + 線 SVG … Paint 287 回 / 1293ms、フレーム p95 42ms
|
|
14
|
+
* 枠線 outline + 線 SVG … Paint 218 回 / 793ms、フレーム p95 42ms
|
|
15
|
+
* どちらも消す(この実装と同じ状態) … Paint 82 回 / 64ms、フレーム p95 25ms
|
|
16
|
+
*
|
|
17
|
+
* そこで枠線と線は転写層から出し、画面の座標で 1 枚の canvas に描く。倍率・位置が
|
|
18
|
+
* 変わるたびに描き直すが、26 本の矩形と 25 本の折れ線を 1600×1000 に描くだけなので
|
|
19
|
+
* 1ms もかからない。定規(CanvasRulers)と同じ考え方
|
|
20
|
+
*
|
|
21
|
+
* [書き方の約束]
|
|
22
|
+
* - canvas の寸法(width / height / style)は載せたときと容器の大きさが変わったときだけ書く。
|
|
23
|
+
* 毎段書くと再確保が起き、パン・ズームで style が変わる要素の数も増える(検証が数えている)
|
|
24
|
+
* - 色は載せたときとテーマが変わったときだけ読む(毎回 getComputedStyle すると同期のレイアウトが走る)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { memo, useEffect, useRef } from 'react';
|
|
28
|
+
import { FRAME_GAP, useMultiPageCanvas, type CanvasLayoutKind, type PageFrameLayout } from '../../contexts/MultiPageCanvasContext';
|
|
29
|
+
|
|
30
|
+
/** 枠線の太さ(画面上の px)。以前の box-shadow: 0 0 0 1px と同じ見え方 */
|
|
31
|
+
const EDGE_WIDTH = 1;
|
|
32
|
+
/** 階層の線の太さ(画面上の px) */
|
|
33
|
+
const CONNECTOR_WIDTH = 1.5;
|
|
34
|
+
|
|
35
|
+
function readVar(el: HTMLElement, name: string, fallback: string): string {
|
|
36
|
+
const v = getComputedStyle(el).getPropertyValue(name).trim();
|
|
37
|
+
return v || fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const CanvasFrameDecor = memo(function CanvasFrameDecor({ theme }: { theme: 'light' | 'dark' }) {
|
|
41
|
+
const { pages, layout, editorMode, viewStore } = useMultiPageCanvas();
|
|
42
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
43
|
+
// 描き直しの入口。倍率・位置の購読と、ページ・テーマの変化の両方から呼ぶ
|
|
44
|
+
const drawRef = useRef<() => void>(() => undefined);
|
|
45
|
+
// 購読のコールバックは載せたときの props を閉じ込めてしまうので、ref 越しに読む
|
|
46
|
+
const pagesRef = useRef<PageFrameLayout[]>(pages);
|
|
47
|
+
pagesRef.current = pages;
|
|
48
|
+
const layoutRef = useRef<CanvasLayoutKind>(layout);
|
|
49
|
+
layoutRef.current = layout;
|
|
50
|
+
const gapYRef = useRef(FRAME_GAP[editorMode].y);
|
|
51
|
+
gapYRef.current = FRAME_GAP[editorMode].y;
|
|
52
|
+
const colorsRef = useRef({ edge: 'rgba(0,0,0,0.08)', line: '#e6e6e6' });
|
|
53
|
+
const sizeRef = useRef({ width: 0, height: 0, dpr: 1 });
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const canvas = canvasRef.current;
|
|
57
|
+
if (!canvas) return;
|
|
58
|
+
// 容器は自分の親をそのまま使う。context の containerRef は親(MultiPageCanvasView)の
|
|
59
|
+
// useEffect で入るので、子のこの useEffect が走る時点ではまだ null
|
|
60
|
+
const host = canvas.parentElement as HTMLElement | null;
|
|
61
|
+
if (!host) return;
|
|
62
|
+
|
|
63
|
+
const resize = () => {
|
|
64
|
+
const dpr = window.devicePixelRatio || 1;
|
|
65
|
+
const width = host.clientWidth;
|
|
66
|
+
const height = host.clientHeight;
|
|
67
|
+
const prev = sizeRef.current;
|
|
68
|
+
if (width === prev.width && height === prev.height && dpr === prev.dpr) return width > 0 && height > 0;
|
|
69
|
+
sizeRef.current = { width, height, dpr };
|
|
70
|
+
canvas.width = Math.round(width * dpr);
|
|
71
|
+
canvas.height = Math.round(height * dpr);
|
|
72
|
+
canvas.style.width = `${width}px`;
|
|
73
|
+
canvas.style.height = `${height}px`;
|
|
74
|
+
return width > 0 && height > 0;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const readColors = () => {
|
|
78
|
+
colorsRef.current = {
|
|
79
|
+
edge: readVar(host, '--ed-frame-edge', theme === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)'),
|
|
80
|
+
line: readVar(host, '--ed-line-strong', theme === 'dark' ? '#5a5a5a' : '#d0d0d0'),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const draw = () => {
|
|
85
|
+
const { width, height, dpr } = sizeRef.current;
|
|
86
|
+
if (width === 0 || height === 0) return;
|
|
87
|
+
const ctx = canvas.getContext('2d');
|
|
88
|
+
if (!ctx) return;
|
|
89
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
90
|
+
ctx.clearRect(0, 0, width, height);
|
|
91
|
+
const { canvasOffset: o, canvasZoom: z } = viewStore.get();
|
|
92
|
+
const list = pagesRef.current;
|
|
93
|
+
if (list.length === 0) return;
|
|
94
|
+
// 画面の座標へ。半 px ずらして 1px の線がぼやけないようにする
|
|
95
|
+
const rectOf = (p: PageFrameLayout) => ({
|
|
96
|
+
x: Math.round(o.x + p.position.x * z),
|
|
97
|
+
y: Math.round(o.y + p.position.y * z),
|
|
98
|
+
w: Math.round(p.size.width * z),
|
|
99
|
+
h: Math.round(p.size.height * z),
|
|
100
|
+
});
|
|
101
|
+
const visible = (r: { x: number; y: number; w: number; h: number }) =>
|
|
102
|
+
r.x + r.w >= -2 && r.x <= width + 2 && r.y + r.h >= -2 && r.y <= height + 2;
|
|
103
|
+
|
|
104
|
+
// 階層の線 → 枠線の順(線の端が枠に隠れる)
|
|
105
|
+
if (layoutRef.current === 'tree') {
|
|
106
|
+
const byId = new Map(list.map((p) => [p.id, p] as const));
|
|
107
|
+
ctx.strokeStyle = colorsRef.current.line;
|
|
108
|
+
ctx.lineWidth = CONNECTOR_WIDTH;
|
|
109
|
+
ctx.lineJoin = 'round';
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
for (const child of list) {
|
|
112
|
+
const parent = child.parentId ? byId.get(child.parentId) : undefined;
|
|
113
|
+
if (!parent) continue;
|
|
114
|
+
// 画素の境目に載せる(端数のままだと 1.5px の線が 3 画素に散ってぼやける)
|
|
115
|
+
const x1 = Math.round(o.x + (parent.position.x + parent.size.width / 2) * z);
|
|
116
|
+
const y1 = Math.round(o.y + (parent.position.y + parent.size.height) * z);
|
|
117
|
+
const x2 = Math.round(o.x + (child.position.x + child.size.width / 2) * z);
|
|
118
|
+
const y2 = Math.round(o.y + child.position.y * z);
|
|
119
|
+
// 画面から外れている組はたどらない(150 枚でも描く本数は見えている分だけ)
|
|
120
|
+
if (Math.max(x1, x2) < -2 || Math.min(x1, x2) > width + 2 || y2 < -2 || y1 > height + 2) continue;
|
|
121
|
+
// 親の下から、子の行との隙間の真ん中まで下り、横へ移って子の上へ
|
|
122
|
+
const ym = Math.round(y2 - (gapYRef.current * z) / 2);
|
|
123
|
+
ctx.moveTo(x1, y1);
|
|
124
|
+
ctx.lineTo(x1, ym);
|
|
125
|
+
ctx.lineTo(x2, ym);
|
|
126
|
+
ctx.lineTo(x2, y2);
|
|
127
|
+
}
|
|
128
|
+
ctx.stroke();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ctx.strokeStyle = colorsRef.current.edge;
|
|
132
|
+
ctx.lineWidth = EDGE_WIDTH;
|
|
133
|
+
ctx.lineJoin = 'miter';
|
|
134
|
+
ctx.beginPath();
|
|
135
|
+
for (const p of list) {
|
|
136
|
+
const r = rectOf(p);
|
|
137
|
+
if (!visible(r)) continue;
|
|
138
|
+
// 以前の box-shadow: 0 0 0 1px は枠の外側に出ていたので、境目に重ねて同じ見え方にする
|
|
139
|
+
ctx.rect(r.x - 0.5, r.y - 0.5, r.w + 1, r.h + 1);
|
|
140
|
+
}
|
|
141
|
+
ctx.stroke();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
drawRef.current = draw;
|
|
145
|
+
readColors();
|
|
146
|
+
resize();
|
|
147
|
+
draw();
|
|
148
|
+
const unsubscribe = viewStore.subscribe(draw);
|
|
149
|
+
let observer: ResizeObserver | null = null;
|
|
150
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
151
|
+
observer = new ResizeObserver(() => { resize(); draw(); });
|
|
152
|
+
observer.observe(host);
|
|
153
|
+
}
|
|
154
|
+
return () => {
|
|
155
|
+
unsubscribe();
|
|
156
|
+
observer?.disconnect();
|
|
157
|
+
};
|
|
158
|
+
}, [viewStore, theme]);
|
|
159
|
+
|
|
160
|
+
// ページの並び・高さが変わったら描き直す(倍率・位置は上の購読が受ける)。
|
|
161
|
+
// viewStore を空更新して呼ぶことはしない ── 他の購読者(定規・コメントのピン)まで巻き込むので
|
|
162
|
+
useEffect(() => { drawRef.current(); }, [pages, layout, editorMode]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<canvas
|
|
166
|
+
ref={canvasRef}
|
|
167
|
+
data-canvas-decor
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
className="pointer-events-none absolute left-0 top-0"
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
});
|