diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 7af504a..b180b18 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -188,6 +188,8 @@ export function CanvasPage({ projectId }: CanvasPageProps) { const flowActionsRef = useRef<{ pasteAtViewportCenter: () => void; fitView: () => void } | null>(null) const [contextTarget, setContextTarget] = React.useState(null) const [lastCreatedNodeId, setLastCreatedNodeId] = React.useState(null) + const [isPanning, setIsPanning] = React.useState(false) + const [isSelecting, setIsSelecting] = React.useState(false) const [ariaAnnouncement, setAriaAnnouncement] = React.useState(null) const nodesRef = useRef(nodes) nodesRef.current = nodes @@ -286,6 +288,11 @@ export function CanvasPage({ projectId }: CanvasPageProps) { const onInit = useCallback((instance: unknown) => setRfInstance(instance), []) + const onMoveStart = useCallback(() => setIsPanning(true), []) + const onMoveEnd = useCallback(() => setIsPanning(false), []) + const onSelectionDragStart = useCallback(() => setIsSelecting(true), []) + const onSelectionDragStop = useCallback(() => setIsSelecting(false), []) + const onNodeDragStart = useCallback(() => saveForDragEnd(), [saveForDragEnd]) const onNodeDragStop = useCallback(() => commitDragEnd(), [commitDragEnd]) @@ -612,6 +619,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) { ({ ...n, className: [n.className, 'nowheel'].filter(Boolean).join(' '), @@ -624,6 +632,10 @@ export function CanvasPage({ projectId }: CanvasPageProps) { onConnectEnd={onConnectEnd} onNodeDragStart={onNodeDragStart} onNodeDragStop={onNodeDragStop} + onMoveStart={onMoveStart} + onMoveEnd={onMoveEnd} + onSelectionDragStart={onSelectionDragStart} + onSelectionDragStop={onSelectionDragStop} isValidConnection={isValidConnection} nodeTypes={nodeTypes} edgeTypes={edgeTypes} @@ -639,6 +651,11 @@ export function CanvasPage({ projectId }: CanvasPageProps) { nodesDraggable nodesConnectable elementsSelectable + panOnScroll + zoomOnScroll + zoomActivationKeyCode="Meta" + panOnDrag={[1]} + selectionOnDrag > {/* BackgroundVariant from @xyflow/system expects enum; 'dots' is valid at runtime */} ['variant']} gap={20} /> diff --git a/frontend/src/app/canvas/ContextualZoomNode.tsx b/frontend/src/app/canvas/ContextualZoomNode.tsx index b276fa1..0b9a6f8 100644 --- a/frontend/src/app/canvas/ContextualZoomNode.tsx +++ b/frontend/src/app/canvas/ContextualZoomNode.tsx @@ -13,7 +13,7 @@ import { cn } from '@/lib/utils' const MIN_ZOOM = 0.1 const MAX_ZOOM = 2 /** When zoom <= this value, show compact icon view (bottom ~30% of zoom range). */ -export const CONTEXTUAL_ZOOM_THRESHOLD = MIN_ZOOM + 0.3 * (MAX_ZOOM - MIN_ZOOM) +export const CONTEXTUAL_ZOOM_THRESHOLD = MIN_ZOOM + 0.1 * (MAX_ZOOM - MIN_ZOOM) /** Handle id per type for compact view (input, output). */ const COMPACT_HANDLES: Record = { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 3efd409..5dc2a2a 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -20,6 +20,17 @@ body { min-height: 0; } +/* Canvas pane cursors: crosshair for selection box, grab/grabbing for middle-click pan */ +.react-flow__pane { + cursor: crosshair; +} +.react-flow.react-flow--panning .react-flow__pane { + cursor: grabbing; +} +.react-flow.react-flow--selecting .react-flow__pane { + cursor: crosshair; +} + /* Node specific overrides */ .react-flow__node { display: block;