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 <noreply@anthropic.com>
This commit is contained in:
@@ -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({
|
||||
|
||||
<div className="mx-2 h-5 w-px bg-border-default" />
|
||||
|
||||
{/* Interaction mode toggle */}
|
||||
<div className="flex items-center rounded border border-default overflow-hidden">
|
||||
<button
|
||||
onClick={() => onModeChange('select')}
|
||||
title="Select (V)"
|
||||
className={cn(
|
||||
'p-1.5 transition-colors',
|
||||
interactionMode === 'select'
|
||||
? 'bg-elevated text-primary'
|
||||
: 'text-muted-foreground hover:text-primary hover:bg-elevated/50',
|
||||
)}
|
||||
>
|
||||
<MousePointer2 size={15} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onModeChange('pan')}
|
||||
title="Pan (H)"
|
||||
className={cn(
|
||||
'p-1.5 transition-colors',
|
||||
interactionMode === 'pan'
|
||||
? 'bg-elevated text-primary'
|
||||
: 'text-muted-foreground hover:text-primary hover:bg-elevated/50',
|
||||
)}
|
||||
>
|
||||
<Hand size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mx-2 h-5 w-px bg-border-default" />
|
||||
|
||||
{editing ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { nodeTypes } from './nodes/nodeTypes'
|
||||
import { edgeTypes } from './edges/edgeTypes'
|
||||
import { getDeviceRenderConfig } from './nodes/deviceRegistry'
|
||||
import type { DeviceNodeData } from './nodes/DeviceNode'
|
||||
import type { InteractionMode } from './DiagramHeader'
|
||||
|
||||
interface NetworkCanvasProps {
|
||||
nodes: Node[]
|
||||
@@ -31,6 +32,7 @@ interface NetworkCanvasProps {
|
||||
onNodeContextMenu?: (event: React.MouseEvent, node: Node) => 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'}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} color="var(--color-border-default)" gap={20} size={1} />
|
||||
<Controls className="!border-default !bg-card [&>button]:!border-default [&>button]:!bg-card [&>button]:!fill-text-primary" />
|
||||
|
||||
@@ -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<ClipboardData | null>(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,
|
||||
|
||||
Reference in New Issue
Block a user