input highlights
This commit is contained in:
37
src/App.tsx
37
src/App.tsx
@@ -68,6 +68,7 @@ export default function App() {
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||
const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
|
||||
const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null)
|
||||
|
||||
const wrapperRef = React.useRef<HTMLDivElement | null>(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' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges, renamingNodeId, setRenamingNodeId }}>
|
||||
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges, renamingNodeId, setRenamingNodeId, connectionFrom, setConnectionFrom, isValidConnection }}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
@@ -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' }}
|
||||
|
||||
@@ -161,7 +161,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" /><OutputHandle id="out" /></>}>
|
||||
<BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" nodeId={id} /><OutputHandle id="out" /></>}>
|
||||
<BaseNodeHeaderRow icon={<ScrollText className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={`${id}`} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="in" /><OutputHandle id="out" /></>}>
|
||||
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="in" nodeId={id} /><OutputHandle id="out" /></>}>
|
||||
<BaseNodeHeaderRow icon={<Code2 className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
|
||||
@@ -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 (
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={id}
|
||||
className={cn(isValidTarget && 'connection-valid-target', isConnecting && !isValidTarget && 'connection-invalid-target')}
|
||||
style={{
|
||||
top: 20,
|
||||
width: 20,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { NodeMenubar } from './NodeMenubar'
|
||||
import { NodeStatusIndicator } from './NodeStatusIndicator'
|
||||
import { MenubarItem, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '../ui/menubar'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||
import { InputHandle } from './NodeHandles'
|
||||
|
||||
// Use relative URL so Vite dev proxy (and optional prod proxy) avoids CORS
|
||||
const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg'
|
||||
@@ -414,7 +414,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
||||
|
||||
return (
|
||||
<NodeStatusIndicator status={status} variant="border" width={dimensions?.width} height={dimensions?.height}>
|
||||
<BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" /><OutputHandle id="out" /></>}>
|
||||
<BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} handles={<InputHandle id="ain" nodeId={id} />}>
|
||||
<BaseNodeHeaderRow icon={<Sparkles className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
|
||||
@@ -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<FlowContextValue | null>(null)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user