From 2b329068f0e442e4933d72254f597b3c08fc751a Mon Sep 17 00:00:00 2001 From: tianma-if Date: Tue, 8 Sep 2026 00:54:13 +0800 Subject: [PATCH] feat(web): add diagram canvas navigation hint and shift-select feedback --- .../src/components/DiagramEditorPane.test.mjs | 6 ++++ apps/web/src/components/DiagramEditorPane.tsx | 29 +++++++++++++++++-- apps/web/src/styles/globals.css | 4 +++ packages/shared/src/i18n/en-US.ts | 3 ++ packages/shared/src/i18n/zh-CN.ts | 3 ++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/DiagramEditorPane.test.mjs b/apps/web/src/components/DiagramEditorPane.test.mjs index 3b52d71d..b95d7b63 100644 --- a/apps/web/src/components/DiagramEditorPane.test.mjs +++ b/apps/web/src/components/DiagramEditorPane.test.mjs @@ -56,6 +56,12 @@ describe("diagram editor canvas surface", () => { expect(source).toContain('multipleSelectionModifiers: ["ctrl", "meta", "shift"]'); expect(source).toContain("interacting: () => !readOnly && !spacePanActiveRef.current"); expect(source).toContain('data-space-pan={spacePanActive ? "active" : undefined}'); + expect(source).toContain('data-shift-select={shiftSelectActive ? "active" : undefined}'); + expect(source).toContain('t("diagram.navHintPan")'); + expect(source).toContain('t("diagram.navHintHoldShift")'); + expect(source).toContain('t("diagram.navHintBoxSelect")'); + expect(globalStyles).toContain('data-shift-select="active"'); + expect(globalStyles).toContain("cursor: crosshair"); expect(source).not.toContain("activeCanvasMode"); expect(source).not.toContain("data-canvas-mode"); }); diff --git a/apps/web/src/components/DiagramEditorPane.tsx b/apps/web/src/components/DiagramEditorPane.tsx index c2c0ad3d..e9f58861 100644 --- a/apps/web/src/components/DiagramEditorPane.tsx +++ b/apps/web/src/components/DiagramEditorPane.tsx @@ -1069,6 +1069,7 @@ export const DiagramEditorPane = ({ const searchInputRef = useRef(null); const [zoomPercent, setZoomPercent] = useState(100); const [spacePanActive, setSpacePanActive] = useState(false); + const [shiftSelectActive, setShiftSelectActive] = useState(false); const spacePanActiveRef = useRef(false); const [historyState, setHistoryState] = useState({ undo: false, redo: false }); const [nodeEditor, setNodeEditor] = useState(null); @@ -1090,15 +1091,21 @@ export const DiagramEditorPane = ({ const isTextInput = (target: EventTarget | null) => target instanceof HTMLElement && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)); const handleCanvasKeyDown = (event: KeyboardEvent) => { - if (event.metaKey || event.ctrlKey || event.altKey || isTextInput(event.target)) return; + if (isTextInput(event.target)) return; + if (event.key === "Shift") setShiftSelectActive(true); + if (event.metaKey || event.ctrlKey || event.altKey) return; if (event.code !== "Space" || !(event.target instanceof globalThis.Node) || !containerRef.current?.contains(event.target)) return; event.preventDefault(); setSpacePanActive(true); }; const handleCanvasKeyUp = (event: KeyboardEvent) => { + if (event.key === "Shift") setShiftSelectActive(false); if (event.code === "Space") setSpacePanActive(false); }; - const releaseTemporaryPan = () => setSpacePanActive(false); + const releaseTemporaryPan = () => { + setShiftSelectActive(false); + setSpacePanActive(false); + }; window.addEventListener("keydown", handleCanvasKeyDown); window.addEventListener("keyup", handleCanvasKeyUp); window.addEventListener("blur", releaseTemporaryPan); @@ -2513,6 +2520,7 @@ export const DiagramEditorPane = ({ data-diagram-appearance={resolvedTheme} data-diagram-kind={document.kind} data-diagram-theme={theme} + data-shift-select={shiftSelectActive ? "active" : undefined} data-space-pan={spacePanActive ? "active" : undefined} tabIndex={0} aria-label={t("diagram.canvas", { type: kindLabel })} @@ -2520,6 +2528,23 @@ export const DiagramEditorPane = ({ onDrop={handleArchitectureDrop} onPointerDownCapture={handlePendingArchitecturePlacement} /> + {!readOnly && ( +
+ {t("diagram.navHintPan")} + · + + {t("diagram.navHintHoldShift")} + + Shift + + {t("diagram.navHintBoxSelect")} + +
+ )} {pendingArchitectureItem ? (
{t("diagram.placeShapeHint", { shape: t(pendingArchitectureItem.labelKey) })} diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 3a9aba23..7f51bce5 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -501,6 +501,10 @@ html[data-edgeever-extension-theme] .edgeever-memo-divider { cursor: grab; } +.edgeever-diagram-canvas[data-shift-select="active"]:not([data-architecture-placement="active"]) { + cursor: crosshair; +} + .edgeever-diagram-scroller[data-panning="true"] .edgeever-diagram-canvas, .edgeever-diagram-canvas[data-space-pan="active"][data-panning="true"] { cursor: grabbing !important; diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index efd37f86..6a32cdce 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -347,6 +347,9 @@ export const enUS = { edgeText: "Connection label", editNode: "Edit node text", deleteSelection: "Delete selection", + navHintPan: "Drag to move canvas", + navHintHoldShift: "Hold", + navHintBoxSelect: "to box-select", undo: "Undo", redo: "Redo", zoomIn: "Zoom in", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index a1452c1d..d77ff933 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -347,6 +347,9 @@ export const zhCN = { edgeText: "连线说明", editNode: "编辑节点文字", deleteSelection: "删除所选内容", + navHintPan: "拖动画布", + navHintHoldShift: "按住", + navHintBoxSelect: "框选多选", undo: "撤销", redo: "重做", zoomIn: "放大",