diff --git a/frontend/src/components/network/NetworkCanvas.tsx b/frontend/src/components/network/NetworkCanvas.tsx index 2c6af6b1..7f19361a 100644 --- a/frontend/src/components/network/NetworkCanvas.tsx +++ b/frontend/src/components/network/NetworkCanvas.tsx @@ -24,6 +24,8 @@ interface NetworkCanvasProps { onEdgeSelect: (edgeId: string | null) => void onDrop: (event: React.DragEvent) => void onDragOver: (event: React.DragEvent) => void + onDragLeave?: (event: React.DragEvent) => void + isDragOver?: boolean } export function NetworkCanvas({ @@ -36,6 +38,8 @@ export function NetworkCanvas({ onEdgeSelect, onDrop, onDragOver, + onDragLeave, + isDragOver, }: NetworkCanvasProps) { const handleSelectionChange = useCallback(({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => { if (selectedNodes.length === 1) { @@ -56,32 +60,41 @@ export function NetworkCanvas({ }, [onNodeSelect, onEdgeSelect]) return ( - - - - - +
+ + + + + + {isDragOver && ( +
+ + Drop to add + +
+ )} +
) } diff --git a/frontend/src/components/network/panels/DeviceToolbar.tsx b/frontend/src/components/network/panels/DeviceToolbar.tsx index fa5ea810..2aa38970 100644 --- a/frontend/src/components/network/panels/DeviceToolbar.tsx +++ b/frontend/src/components/network/panels/DeviceToolbar.tsx @@ -1,5 +1,5 @@ import { useState, useMemo, useCallback } from 'react' -import { Search, Plus, ChevronDown, ChevronRight, X, LayoutGrid } from 'lucide-react' +import { Search, Plus, ChevronDown, ChevronRight, X, LayoutGrid, GripVertical } from 'lucide-react' import { getDeviceRenderConfig, CATEGORY_LABELS, CATEGORY_ORDER } from '../nodes/deviceRegistry' import type { DeviceTypeResponse, DeviceTypeCreate } from '@/types' import { deviceTypesApi } from '@/api' @@ -107,8 +107,9 @@ export function DeviceToolbar({ deviceTypes, onDeviceTypesChange }: DeviceToolba key={dt.id} draggable onDragStart={e => handleDragStart(e, dt)} - className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing" + className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing active:scale-[0.98] transition-transform" > + {dt.label} @@ -140,8 +141,9 @@ export function DeviceToolbar({ deviceTypes, onDeviceTypesChange }: DeviceToolba e.dataTransfer.setData('application/reactflow-group', JSON.stringify(item)) e.dataTransfer.effectAllowed = 'move' }} - className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing" + className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing active:scale-[0.98] transition-transform" > + {item.label} diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx index d8b82740..88d71b74 100644 --- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx +++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx @@ -50,6 +50,7 @@ function DiagramEditorInner() { const [deviceTypes, setDeviceTypes] = useState([]) const [loading, setLoading] = useState(!!id) + const [isDragOver, setIsDragOver] = useState(false) useEffect(() => { isDirtyRef.current = isDirty }, [isDirty]) useEffect(() => { diagramIdRef.current = diagramId }, [diagramId]) @@ -196,10 +197,18 @@ function DiagramEditorInner() { const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault() event.dataTransfer.dropEffect = 'move' + setIsDragOver(true) + }, []) + + const onDragLeave = useCallback((event: React.DragEvent) => { + const relatedTarget = event.relatedTarget as HTMLElement | null + if (relatedTarget && (event.currentTarget as HTMLElement).contains(relatedTarget)) return + setIsDragOver(false) }, []) const onDrop = useCallback((event: React.DragEvent) => { event.preventDefault() + setIsDragOver(false) const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }) @@ -415,6 +424,8 @@ function DiagramEditorInner() { onEdgeSelect={setSelectedEdgeId} onDrop={onDrop} onDragOver={onDragOver} + onDragLeave={onDragLeave} + isDragOver={isDragOver} />