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 && (