diff --git a/src/App.tsx b/src/App.tsx index f5a5739..74d6ab6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -68,6 +68,7 @@ export default function App() { const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) const [rfInstance, setRfInstance] = React.useState(null) const [renamingNodeId, setRenamingNodeId] = React.useState(null) + const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null) const wrapperRef = React.useRef(null) const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null) @@ -85,6 +86,37 @@ export default function App() { [setEdges] ) + const isValidConnection = React.useCallback( + (connection: Connection) => { + const sourceNode = nodes.find((n) => n.id === connection.source) + const targetNode = nodes.find((n) => n.id === connection.target) + const sourceType = sourceNode?.type + const targetType = targetNode?.type + if (!sourceType || !targetType) return false + if (connection.source === connection.target) return false + if (targetType === 'variable') return false + if (targetType === 'render' && (sourceType === 'variable' || sourceType === 'function')) return false + if (sourceType === 'render') return false + return true + }, + [nodes] + ) + + const onConnectStart = React.useCallback( + (_: React.MouseEvent | React.TouchEvent, params: { nodeId?: string | null; handleId?: string | null; handleType?: string | null }) => { + if (params.handleType !== 'source' || !params.nodeId) { + setConnectionFrom(null) + return + } + setConnectionFrom({ nodeId: params.nodeId, sourceHandle: params.handleId ?? undefined }) + }, + [] + ) + + const onConnectEnd = React.useCallback(() => { + setConnectionFrom(null) + }, []) + const onInit = React.useCallback((instance: any) => { setRfInstance(instance) }, []) @@ -204,7 +236,7 @@ export default function App() { > {theme === 'dark' ? : } - +
@@ -215,6 +247,9 @@ export default function App() { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} + onConnectStart={onConnectStart} + onConnectEnd={onConnectEnd} + isValidConnection={isValidConnection} nodeTypes={nodeTypes} edgeTypes={edgeTypes} defaultEdgeOptions={{ type: 'animated' }} diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index 9dee7f5..321473c 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -161,7 +161,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: : undefined return ( - }> + }> } title={} /> diff --git a/src/components/graph/FunctionNode.tsx b/src/components/graph/FunctionNode.tsx index ff0cc60..f3f57b7 100644 --- a/src/components/graph/FunctionNode.tsx +++ b/src/components/graph/FunctionNode.tsx @@ -48,7 +48,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height : undefined return ( - }> + }> } title={} /> diff --git a/src/components/graph/NodeHandles.tsx b/src/components/graph/NodeHandles.tsx index 24b5df2..ebf2a60 100644 --- a/src/components/graph/NodeHandles.tsx +++ b/src/components/graph/NodeHandles.tsx @@ -1,17 +1,35 @@ -import React from 'react' +import React, { useContext } from 'react' import { Handle, Position } from '@xyflow/react' import { ArrowDownLeft, ArrowUpRight } from 'lucide-react' +import FlowContext from '@/lib/flowContext' +import { cn } from '@/lib/utils' type NodeHandleProps = { id: string + /** Pass when this handle is a connection target so valid highlight can show as soon as connection starts */ + nodeId?: string } -export function InputHandle({ id }: NodeHandleProps) { +export function InputHandle({ id, nodeId }: NodeHandleProps) { + const ctx = useContext(FlowContext) + const connectionFrom = ctx?.connectionFrom ?? null + const isValidConnection = ctx?.isValidConnection + const isConnecting = Boolean(nodeId && connectionFrom && connectionFrom.nodeId !== nodeId) + const isValidTarget = + isConnecting && + isValidConnection?.({ + source: connectionFrom!.nodeId, + sourceHandle: connectionFrom!.sourceHandle ?? undefined, + target: nodeId!, + targetHandle: id, + }) + return ( - }> + }> } title={} /> diff --git a/src/lib/flowContext.tsx b/src/lib/flowContext.tsx index abd751f..9b5b0ee 100644 --- a/src/lib/flowContext.tsx +++ b/src/lib/flowContext.tsx @@ -1,4 +1,7 @@ import React from 'react' +import type { Connection } from '@xyflow/react' + +export type ConnectionFrom = { nodeId: string; sourceHandle?: string } | null export type FlowContextValue = { nodes: any[] @@ -7,6 +10,10 @@ export type FlowContextValue = { setEdges: (updater: any) => void renamingNodeId: string | null setRenamingNodeId: (id: string | null) => void + /** Set when user starts dragging from an output handle; cleared on connect end. Used to highlight valid targets. */ + connectionFrom: ConnectionFrom + setConnectionFrom: (v: ConnectionFrom) => void + isValidConnection: (connection: Connection) => boolean } const FlowContext = React.createContext(null) diff --git a/src/styles.css b/src/styles.css index bcdda4e..a9275b8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -33,6 +33,29 @@ body { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15); } +/* Connection validation: valid targets clearly highlighted (from drag start and when hovering), invalid dimmed while connecting */ +.react-flow__handle.react-flow__handle-connecting.react-flow__handle-valid, +.react-flow__handle.connection-valid-target { + background: hsl(var(--primary)) !important; + color: hsl(var(--primary-foreground)); + box-shadow: 0 0 0 3px hsl(var(--primary)), 0 0 12px 2px hsl(var(--primary) / 0.5) !important; + opacity: 1 !important; +} +.react-flow__handle.react-flow__handle-connecting.react-flow__handle-valid svg, +.react-flow__handle.connection-valid-target svg { + color: inherit; +} +.dark .react-flow__handle.react-flow__handle-connecting.react-flow__handle-valid, +.dark .react-flow__handle.connection-valid-target { + background: hsl(var(--primary)) !important; + box-shadow: 0 0 0 3px hsl(var(--primary)), 0 0 14px 4px hsl(var(--primary) / 0.6) !important; +} +.react-flow__handle.react-flow__handle-connecting:not(.react-flow__handle-valid):not(.connection-valid-target), +.react-flow__handle.connection-invalid-target { + opacity: 0.25 !important; + pointer-events: none; +} + /* Animated React Flow edges: thicker stroke + path animation */ .react-flow__edge path.animated-edge-path, .animated-edge-path {