From 684fb07e4711c373c66869a86a736bb103ed861a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 14 Apr 2026 00:38:51 +0000 Subject: [PATCH] feat(network): add pointer/hand mode toggle to diagram toolbar - Header shows MousePointer2 (select) and Hand (pan) toggle buttons - Select mode: drag on canvas draws a selection box (selectionOnDrag) - Pan mode: drag on canvas pans the viewport (panOnDrag) - Space held in either mode temporarily switches to pan (panActivationKeyCode) - Keyboard shortcuts: V = select mode, H = pan mode - Cursor changes to grab/grabbing in pan mode Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/network/DiagramHeader.tsx | 39 ++++++++++++++++++- .../src/components/network/NetworkCanvas.tsx | 8 +++- .../network/hooks/useCanvasShortcuts.ts | 14 ++++++- .../pages/NetworkDiagrams/DiagramEditor.tsx | 8 +++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/network/DiagramHeader.tsx b/frontend/src/components/network/DiagramHeader.tsx index 1b6916ca..f4a790a4 100644 --- a/frontend/src/components/network/DiagramHeader.tsx +++ b/frontend/src/components/network/DiagramHeader.tsx @@ -1,6 +1,9 @@ import { useState, useCallback, useRef, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { ChevronLeft, Save, Download, FileJson, Image, FileText, Undo2, Redo2 } from 'lucide-react' +import { ChevronLeft, Save, Download, FileJson, Image, FileText, Undo2, Redo2, MousePointer2, Hand } from 'lucide-react' +import { cn } from '@/lib/utils' + +export type InteractionMode = 'select' | 'pan' interface DiagramHeaderProps { name: string @@ -18,6 +21,8 @@ interface DiagramHeaderProps { onRedo: () => void canUndo: boolean canRedo: boolean + interactionMode: InteractionMode + onModeChange: (mode: InteractionMode) => void } export function DiagramHeader({ @@ -36,6 +41,8 @@ export function DiagramHeader({ onRedo, canUndo, canRedo, + interactionMode, + onModeChange, }: DiagramHeaderProps) { const navigate = useNavigate() const [editing, setEditing] = useState(false) @@ -117,6 +124,36 @@ export function DiagramHeader({
+ {/* Interaction mode toggle */} +
+ + +
+ +
+ {editing ? ( void onPaneContextMenu?: (event: MouseEvent | React.MouseEvent) => void onPaneClick?: () => void + interactionMode?: InteractionMode } export function NetworkCanvas({ @@ -48,6 +50,7 @@ export function NetworkCanvas({ onNodeContextMenu, onPaneContextMenu, onPaneClick: onPaneClickProp, + interactionMode = 'select', }: NetworkCanvasProps) { const handleSelectionChange = useCallback(({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => { if (selectedNodes.length === 1) { @@ -93,10 +96,13 @@ export function NetworkCanvas({ defaultEdgeOptions={{ type: 'connection' }} deleteKeyCode={['Backspace', 'Delete']} multiSelectionKeyCode="Shift" + panOnDrag={interactionMode === 'pan'} + selectionOnDrag={interactionMode === 'select'} + panActivationKeyCode="Space" snapToGrid={true} snapGrid={[20, 20]} fitView - className="bg-page" + className={interactionMode === 'pan' ? 'bg-page cursor-grab active:cursor-grabbing' : 'bg-page'} > diff --git a/frontend/src/components/network/hooks/useCanvasShortcuts.ts b/frontend/src/components/network/hooks/useCanvasShortcuts.ts index 2aeb1645..955ce0be 100644 --- a/frontend/src/components/network/hooks/useCanvasShortcuts.ts +++ b/frontend/src/components/network/hooks/useCanvasShortcuts.ts @@ -36,6 +36,7 @@ export function useCanvasShortcuts({ onUndo, onRedo, onNudge, + onSetMode, }: { nodes: Node[] edges: Edge[] @@ -46,6 +47,7 @@ export function useCanvasShortcuts({ onUndo: () => void onRedo: () => void onNudge: (dx: number, dy: number) => void + onSetMode: (mode: 'select' | 'pan') => void }) { const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow() const clipboardRef = useRef(null) @@ -242,6 +244,16 @@ export function useCanvasShortcuts({ return } + // Mode shortcuts: V = select, H = pan + if (!ctrl && e.key === 'v') { + onSetMode('select') + return + } + if (!ctrl && e.key === 'h') { + onSetMode('pan') + return + } + if (ctrl && e.key === 'c') { e.preventDefault() copyNodes() @@ -268,7 +280,7 @@ export function useCanvasShortcuts({ document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) - }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge]) + }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge, onSetMode]) return { copyNodes, diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx index 749883d5..5259f39a 100644 --- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx +++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx @@ -18,7 +18,7 @@ import { NetworkCanvas } from '@/components/network/NetworkCanvas' import { ContextMenu, getNodeMenuActions, getCanvasMenuActions } from '@/components/network/ContextMenu' import { useCanvasShortcuts } from '@/components/network/hooks/useCanvasShortcuts' import { useDiagramCommands } from '@/components/network/hooks/useDiagramCommands' -import { DiagramHeader } from '@/components/network/DiagramHeader' +import { DiagramHeader, type InteractionMode } from '@/components/network/DiagramHeader' import { DeviceToolbar } from '@/components/network/panels/DeviceToolbar' import { PropertiesPanel } from '@/components/network/panels/PropertiesPanel' import { AIAssistPanel } from '@/components/network/panels/AIAssistPanel' @@ -69,6 +69,8 @@ function DiagramEditorInner() { const [loading, setLoading] = useState(!!id) const [isDragOver, setIsDragOver] = useState(false) + const [interactionMode, setInteractionMode] = useState('select') + const canvasRef = useRef(null) const [contextMenu, setContextMenu] = useState(null) const [pendingDeleteNodeId, setPendingDeleteNodeId] = useState(null) @@ -154,6 +156,7 @@ function DiagramEditorInner() { onUndo: undo, onRedo: redo, onNudge, + onSetMode: setInteractionMode, }) const handleNodesChange: typeof onNodesChange = useCallback((changes) => { @@ -675,6 +678,8 @@ function DiagramEditorInner() { onRedo={redo} canUndo={canUndo} canRedo={canRedo} + interactionMode={interactionMode} + onModeChange={setInteractionMode} />
@@ -695,6 +700,7 @@ function DiagramEditorInner() { onNodeContextMenu={handleNodeContextMenu} onPaneContextMenu={handlePaneContextMenu} onPaneClick={closeContextMenu} + interactionMode={interactionMode} /> {nodes.length === 0 && !loading && (