diff --git a/app/globals.css b/app/globals.css index 4c0bc3f..7b8a537 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,8 +3,8 @@ @tailwind utilities; :root { - --foreground-rgb: 255, 255, 255; - --background-rgb: 2, 6, 23; + --foreground-rgb: 30, 41, 59; + --background-rgb: 248, 250, 252; } body { @@ -12,6 +12,72 @@ body { background: rgb(var(--background-rgb)); } +/* React Flow Customization - n8n style */ +.react-flow__node { + cursor: grab; +} + +.react-flow__node:active { + cursor: grabbing; +} + +.react-flow__node.selected { + outline: none; +} + +.react-flow__edge-path { + stroke-linecap: round; +} + +.react-flow__controls { + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); +} + +.react-flow__controls-button { + background: white; + border-bottom: 1px solid #e2e8f0; + width: 28px; + height: 28px; +} + +.react-flow__controls-button:hover { + background: #f1f5f9; +} + +.react-flow__controls-button svg { + fill: #64748b; +} + +/* Range input styling */ +input[type="range"] { + -webkit-appearance: none; + appearance: none; + background: transparent; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: currentColor; + cursor: pointer; + margin-top: -6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + background: #e2e8f0; + border-radius: 2px; +} + +input[type="range"]:focus { + outline: none; +} + /* Scrollbar */ ::-webkit-scrollbar { width: 8px; @@ -19,14 +85,14 @@ body { } ::-webkit-scrollbar-track { - background: #1e293b; + background: #f1f5f9; } ::-webkit-scrollbar-thumb { - background: #475569; + background: #cbd5e1; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: #64748b; + background: #94a3b8; } diff --git a/app/page.tsx b/app/page.tsx index 1754314..d193704 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,155 +1,23 @@ 'use client' -import { useState } from 'react' import dynamic from 'next/dynamic' -const FundingFunnel = dynamic(() => import('@/components/FundingFunnel'), { +const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), { ssr: false, loading: () => ( -
-
Loading visualization...
+
+
+
+ Loading flow editor... +
), }) export default function Home() { - const [view, setView] = useState<'single' | 'multi'>('single') - return ( -
- {/* Header */} -
-
-
-

- Threshold-Based Flow Funding -

-

- Interactive visualization of funding flows with minimum and maximum thresholds -

-
-
- - -
-
-
- - {/* Legend */} -
-
-
- Critical Zone (below min) -
-
-
- Healthy Zone (min to max) -
-
-
- Overflow Zone (above max) -
-
- - {/* Funnel Visualizations */} - {view === 'single' ? ( -
- -
- ) : ( -
- - - -
- )} - - {/* Instructions */} -
-
-

How It Works

-
    -
  • - 1. - - Minimum Threshold — Below this level, - the funnel narrows, restricting outflow. Funds are conserved until the minimum is reached. - -
  • -
  • - 2. - - Healthy Range — Between min and max, - the funnel has straight walls. Normal operations with balanced in/out flows. - -
  • -
  • - 3. - - Maximum Threshold — Above this level, - excess funds overflow and can be redistributed to other pools or purposes. - -
  • -
  • - - - Drag the threshold lines to adjust - minimum and maximum levels interactively. - -
  • -
-
-
+
+
) } diff --git a/components/FlowCanvas.tsx b/components/FlowCanvas.tsx new file mode 100644 index 0000000..f9bcd9f --- /dev/null +++ b/components/FlowCanvas.tsx @@ -0,0 +1,271 @@ +'use client' + +import { useCallback, useState, useEffect } from 'react' +import { + ReactFlow, + Controls, + Background, + BackgroundVariant, + useNodesState, + useEdgesState, + addEdge, + Connection, + MarkerType, + Panel, +} from '@xyflow/react' +import '@xyflow/react/dist/style.css' + +import SourceNode from './nodes/SourceNode' +import ThresholdNode from './nodes/ThresholdNode' +import RecipientNode from './nodes/RecipientNode' +import type { FlowNode, FlowEdge, SourceNodeData, ThresholdNodeData, RecipientNodeData } from '@/lib/types' + +const nodeTypes = { + source: SourceNode, + threshold: ThresholdNode, + recipient: RecipientNode, +} + +const initialNodes: FlowNode[] = [ + { + id: 'source-1', + type: 'source', + position: { x: 50, y: 150 }, + data: { + label: 'Treasury', + balance: 75000, + flowRate: 500, + }, + }, + { + id: 'threshold-1', + type: 'threshold', + position: { x: 350, y: 50 }, + data: { + label: 'Public Goods Gate', + minThreshold: 10000, + maxThreshold: 50000, + currentValue: 32000, + }, + }, + { + id: 'threshold-2', + type: 'threshold', + position: { x: 350, y: 350 }, + data: { + label: 'Research Gate', + minThreshold: 5000, + maxThreshold: 30000, + currentValue: 8500, + }, + }, + { + id: 'recipient-1', + type: 'recipient', + position: { x: 700, y: 50 }, + data: { + label: 'Project Alpha', + received: 24500, + target: 30000, + }, + }, + { + id: 'recipient-2', + type: 'recipient', + position: { x: 700, y: 250 }, + data: { + label: 'Project Beta', + received: 8000, + target: 25000, + }, + }, + { + id: 'recipient-3', + type: 'recipient', + position: { x: 700, y: 450 }, + data: { + label: 'Research Fund', + received: 15000, + target: 15000, + }, + }, +] + +const initialEdges: FlowEdge[] = [ + { + id: 'e1', + source: 'source-1', + target: 'threshold-1', + animated: true, + style: { stroke: '#3b82f6', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' }, + }, + { + id: 'e2', + source: 'source-1', + target: 'threshold-2', + animated: true, + style: { stroke: '#3b82f6', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' }, + }, + { + id: 'e3', + source: 'threshold-1', + target: 'recipient-1', + animated: true, + style: { stroke: '#a855f7', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a855f7' }, + }, + { + id: 'e4', + source: 'threshold-1', + target: 'recipient-2', + animated: true, + style: { stroke: '#a855f7', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a855f7' }, + }, + { + id: 'e5', + source: 'threshold-2', + target: 'recipient-3', + animated: true, + style: { stroke: '#a855f7', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a855f7' }, + }, +] + +export default function FlowCanvas() { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) + const [isSimulating, setIsSimulating] = useState(true) + + const onConnect = useCallback( + (params: Connection) => + setEdges((eds) => + addEdge( + { + ...params, + animated: true, + style: { stroke: '#64748b', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#64748b' }, + }, + eds + ) + ), + [setEdges] + ) + + // Simulation effect + useEffect(() => { + if (!isSimulating) return + + const interval = setInterval(() => { + setNodes((nds) => + nds.map((node) => { + if (node.type === 'source') { + const data = node.data as SourceNodeData + return { + ...node, + data: { + ...data, + balance: Math.max(0, data.balance - data.flowRate / 3600), + }, + } + } + if (node.type === 'threshold') { + const data = node.data as ThresholdNodeData + const change = (Math.random() - 0.3) * 100 + return { + ...node, + data: { + ...data, + currentValue: Math.max(0, Math.min(100000, data.currentValue + change)), + }, + } + } + if (node.type === 'recipient') { + const data = node.data as RecipientNodeData + if (data.received < data.target) { + return { + ...node, + data: { + ...data, + received: Math.min(data.target, data.received + Math.random() * 50), + }, + } + } + } + return node + }) + ) + }, 1000) + + return () => clearInterval(interval) + }, [isSimulating, setNodes]) + + return ( +
+ + + + + {/* Top Panel - Title and Controls */} + +

Threshold-Based Flow Funding

+

Drag nodes to rearrange • Connect nodes to create flows

+
+ + {/* Simulation Toggle */} + + + + + {/* Legend */} + +
Node Types
+
+
+
+ Source (Funding Origin) +
+
+
+ Threshold Gate (Min/Max) +
+
+
+ Recipient (Funded) +
+
+
+ Recipient (Pending) +
+
+ + +
+ ) +} diff --git a/components/FundingFunnel.tsx b/components/FundingFunnel.tsx deleted file mode 100644 index a5e3133..0000000 --- a/components/FundingFunnel.tsx +++ /dev/null @@ -1,576 +0,0 @@ -'use client' - -import { useState, useEffect, useRef, useCallback } from 'react' - -interface FundingFunnelProps { - name: string - currentBalance: number - minThreshold: number - maxThreshold: number - inflowRate: number // per hour - outflowRate: number // per hour - maxCapacity?: number - onMinThresholdChange?: (value: number) => void - onMaxThresholdChange?: (value: number) => void -} - -export default function FundingFunnel({ - name, - currentBalance, - minThreshold: initialMin, - maxThreshold: initialMax, - inflowRate, - outflowRate, - maxCapacity = 100000, - onMinThresholdChange, - onMaxThresholdChange, -}: FundingFunnelProps) { - const [minThreshold, setMinThreshold] = useState(initialMin) - const [maxThreshold, setMaxThreshold] = useState(initialMax) - const [balance, setBalance] = useState(currentBalance) - const [isDraggingMin, setIsDraggingMin] = useState(false) - const [isDraggingMax, setIsDraggingMax] = useState(false) - const containerRef = useRef(null) - - // Funnel dimensions - const width = 300 - const height = 500 - const funnelTopWidth = 280 - const funnelNarrowWidth = 80 - const padding = 10 - - // Calculate Y positions for thresholds (inverted - 0 is at bottom) - const minY = height - (minThreshold / maxCapacity) * height - const maxY = height - (maxThreshold / maxCapacity) * height - const balanceY = height - (balance / maxCapacity) * height - - // Funnel shape points - const funnelPath = ` - M ${padding} ${maxY} - L ${padding} ${minY} - L ${(width - funnelNarrowWidth) / 2} ${height - padding} - L ${(width + funnelNarrowWidth) / 2} ${height - padding} - L ${width - padding} ${minY} - L ${width - padding} ${maxY} - L ${padding} ${maxY} - ` - - // Overflow zone (above max) - const overflowPath = ` - M ${padding} ${padding} - L ${padding} ${maxY} - L ${width - padding} ${maxY} - L ${width - padding} ${padding} - Z - ` - - // Calculate fill path based on current balance - const getFillPath = () => { - if (balance <= 0) return '' - - const fillY = Math.max(balanceY, padding) - - if (balance >= maxThreshold) { - // Overflow zone - straight walls above max - const overflowY = Math.max(fillY, padding) - return ` - M ${padding} ${minY} - L ${(width - funnelNarrowWidth) / 2} ${height - padding} - L ${(width + funnelNarrowWidth) / 2} ${height - padding} - L ${width - padding} ${minY} - L ${width - padding} ${overflowY} - L ${padding} ${overflowY} - Z - ` - } else if (balance >= minThreshold) { - // Between min and max - straight walls - return ` - M ${padding} ${minY} - L ${(width - funnelNarrowWidth) / 2} ${height - padding} - L ${(width + funnelNarrowWidth) / 2} ${height - padding} - L ${width - padding} ${minY} - L ${width - padding} ${fillY} - L ${padding} ${fillY} - Z - ` - } else { - // Below min - in the funnel narrowing section - const ratio = balance / minThreshold - const bottomWidth = funnelNarrowWidth - const topWidth = funnelTopWidth - 2 * padding - const currentWidth = bottomWidth + (topWidth - bottomWidth) * ratio - const leftX = (width - currentWidth) / 2 - const rightX = (width + currentWidth) / 2 - - return ` - M ${(width - funnelNarrowWidth) / 2} ${height - padding} - L ${(width + funnelNarrowWidth) / 2} ${height - padding} - L ${rightX} ${fillY} - L ${leftX} ${fillY} - Z - ` - } - } - - // Simulate balance changes - useEffect(() => { - const interval = setInterval(() => { - setBalance((prev) => { - const netFlow = (inflowRate - outflowRate) / 3600 // per second - const newBalance = prev + netFlow - return Math.max(0, Math.min(maxCapacity * 1.2, newBalance)) - }) - }, 100) - return () => clearInterval(interval) - }, [inflowRate, outflowRate, maxCapacity]) - - // Handle threshold dragging - const handleMouseMove = useCallback( - (e: MouseEvent) => { - if (!containerRef.current) return - const rect = containerRef.current.getBoundingClientRect() - const y = e.clientY - rect.top - const value = Math.max(0, Math.min(maxCapacity, ((height - y) / height) * maxCapacity)) - - if (isDraggingMin) { - const newMin = Math.min(value, maxThreshold - 1000) - setMinThreshold(Math.max(0, newMin)) - onMinThresholdChange?.(Math.max(0, newMin)) - } else if (isDraggingMax) { - const newMax = Math.max(value, minThreshold + 1000) - setMaxThreshold(Math.min(maxCapacity, newMax)) - onMaxThresholdChange?.(Math.min(maxCapacity, newMax)) - } - }, - [isDraggingMin, isDraggingMax, maxThreshold, minThreshold, maxCapacity, onMinThresholdChange, onMaxThresholdChange] - ) - - const handleMouseUp = useCallback(() => { - setIsDraggingMin(false) - setIsDraggingMax(false) - }, []) - - useEffect(() => { - if (isDraggingMin || isDraggingMax) { - window.addEventListener('mousemove', handleMouseMove) - window.addEventListener('mouseup', handleMouseUp) - return () => { - window.removeEventListener('mousemove', handleMouseMove) - window.removeEventListener('mouseup', handleMouseUp) - } - } - }, [isDraggingMin, isDraggingMax, handleMouseMove, handleMouseUp]) - - // Determine zone status - const getZoneStatus = () => { - if (balance < minThreshold) return { zone: 'critical', color: '#F43F5E', label: 'Below Minimum' } - if (balance > maxThreshold) return { zone: 'overflow', color: '#F59E0B', label: 'Overflow' } - return { zone: 'healthy', color: '#10B981', label: 'Healthy Range' } - } - - const status = getZoneStatus() - - return ( -
-

{name}

- -
- {/* Main Funnel Visualization */} -
- - - {/* Gradient for the fill */} - - - - - - - {/* Glow filter */} - - - - - - - - - {/* Wave pattern for liquid effect */} - - - - - - {/* Background zones */} - {/* Overflow zone (above max) */} - - - {/* Healthy zone (between min and max) - straight walls */} - - - {/* Funnel zone (below min) */} - - - {/* Funnel outline */} - - - {/* Fill (current balance) */} - - - - - {/* Animated inflow particles */} - {inflowRate > 0 && ( - <> - {[...Array(5)].map((_, i) => ( - - - - - - ))} - - )} - - {/* Animated outflow particles */} - {outflowRate > 0 && balance > 0 && ( - <> - {[...Array(3)].map((_, i) => ( - - - - - - ))} - - )} - - {/* Max threshold line (draggable) */} - setIsDraggingMax(true)} - > - - - - MAX ${maxThreshold.toLocaleString()} - - - - {/* Min threshold line (draggable) */} - setIsDraggingMin(true)} - > - - - - MIN ${minThreshold.toLocaleString()} - - - - {/* Current balance indicator */} - - - - - - - {/* Zone labels */} -
- OVERFLOW -
-
- HEALTHY -
-
- CRITICAL -
-
- - {/* Stats Panel */} -
- {/* Current Balance */} -
-
Current Balance
-
- ${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })} -
-
- {status.label} -
-
- - {/* Flow Rates */} -
-
Flow Rates
- -
-
-
-
- Inflow -
- +${inflowRate}/hr -
- -
-
-
- Outflow -
- -${outflowRate}/hr -
- -
-
- Net Flow - = 0 ? 'text-emerald-400' : 'text-rose-400' - }`} - > - {inflowRate - outflowRate >= 0 ? '+' : ''}${inflowRate - outflowRate}/hr - -
-
-
-
- - {/* Thresholds */} -
-
Thresholds
- -
-
-
- Minimum - ${minThreshold.toLocaleString()} -
-
- Drag the red line to adjust -
-
- -
-
- Maximum - ${maxThreshold.toLocaleString()} -
-
- Drag the yellow line to adjust -
-
-
-
- - {/* Progress to thresholds */} -
-
Progress
- -
-
-
- To Minimum - - {Math.min(100, (balance / minThreshold) * 100).toFixed(0)}% - -
-
-
-
-
- -
-
- To Maximum - - {Math.min(100, (balance / maxThreshold) * 100).toFixed(0)}% - -
-
-
-
-
-
-
-
-
-
- ) -} diff --git a/components/nodes/RecipientNode.tsx b/components/nodes/RecipientNode.tsx new file mode 100644 index 0000000..a258db0 --- /dev/null +++ b/components/nodes/RecipientNode.tsx @@ -0,0 +1,83 @@ +'use client' + +import { memo } from 'react' +import { Handle, Position } from '@xyflow/react' +import type { NodeProps } from '@xyflow/react' +import type { RecipientNodeData } from '@/lib/types' + +function RecipientNode({ data, selected }: NodeProps) { + const { label, received, target } = data as RecipientNodeData + const progress = Math.min(100, (received / target) * 100) + const isFunded = received >= target + + return ( +
+ {/* Input Handle */} + + + {/* Header */} +
+
+
+ + + +
+ {label} + {isFunded && ( + + + + )} +
+
+ + {/* Body */} +
+
+ Received + + ${received.toLocaleString()} + +
+ + {/* Progress bar */} +
+
+ Progress + {progress.toFixed(0)}% +
+
+
+
+
+ +
+ Target + + ${target.toLocaleString()} + +
+
+
+ ) +} + +export default memo(RecipientNode) diff --git a/components/nodes/SourceNode.tsx b/components/nodes/SourceNode.tsx new file mode 100644 index 0000000..b5191f6 --- /dev/null +++ b/components/nodes/SourceNode.tsx @@ -0,0 +1,57 @@ +'use client' + +import { memo } from 'react' +import { Handle, Position } from '@xyflow/react' +import type { NodeProps } from '@xyflow/react' +import type { SourceNodeData } from '@/lib/types' + +function SourceNode({ data, selected }: NodeProps) { + const { label, balance, flowRate } = data as SourceNodeData + + return ( +
+ {/* Header */} +
+
+
+ + + +
+ {label} +
+
+ + {/* Body */} +
+
+ Balance + + ${balance.toLocaleString()} + +
+
+ Flow Rate + + ${flowRate}/hr + +
+
+ + {/* Output Handle */} + +
+ ) +} + +export default memo(SourceNode) diff --git a/components/nodes/ThresholdNode.tsx b/components/nodes/ThresholdNode.tsx new file mode 100644 index 0000000..a64d0b7 --- /dev/null +++ b/components/nodes/ThresholdNode.tsx @@ -0,0 +1,145 @@ +'use client' + +import { memo, useState, useCallback } from 'react' +import { Handle, Position } from '@xyflow/react' +import type { NodeProps } from '@xyflow/react' +import type { ThresholdNodeData } from '@/lib/types' + +function ThresholdNode({ data, selected }: NodeProps) { + const nodeData = data as ThresholdNodeData + const [minThreshold, setMinThreshold] = useState(nodeData.minThreshold) + const [maxThreshold, setMaxThreshold] = useState(nodeData.maxThreshold) + const currentValue = nodeData.currentValue + + // Calculate status + const getStatus = () => { + if (currentValue < minThreshold) return { label: 'Below Min', color: 'red', bg: 'bg-red-500' } + if (currentValue > maxThreshold) return { label: 'Overflow', color: 'amber', bg: 'bg-amber-500' } + return { label: 'Active', color: 'green', bg: 'bg-emerald-500' } + } + + const status = getStatus() + const fillPercent = Math.min(100, Math.max(0, ((currentValue - minThreshold) / (maxThreshold - minThreshold)) * 100)) + + return ( +
+ {/* Input Handle */} + + + {/* Header */} +
+
+
+
+ + + +
+ {nodeData.label} +
+ + {status.label} + +
+
+ + {/* Body */} +
+ {/* Current Value Display */} +
+ + ${currentValue.toLocaleString()} + +

Current Value

+
+ + {/* Visual Bar */} +
+
+ {/* Fill */} +
maxThreshold ? 'bg-amber-400' : 'bg-emerald-400' + }`} + style={{ + width: currentValue < minThreshold + ? `${(currentValue / minThreshold) * 33}%` + : currentValue > maxThreshold + ? '100%' + : `${33 + fillPercent * 0.67}%` + }} + /> + {/* Min marker */} +
+ {/* Max marker */} +
+
+
+ $0 + Min + Max +
+
+ + {/* Threshold Controls */} +
+
+
+ + ${minThreshold.toLocaleString()} +
+ setMinThreshold(Number(e.target.value))} + className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-red-500" + /> +
+ +
+
+ + ${maxThreshold.toLocaleString()} +
+ setMaxThreshold(Number(e.target.value))} + className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-amber-500" + /> +
+
+
+ + {/* Output Handle */} + +
+ ) +} + +export default memo(ThresholdNode) diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..df87227 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,26 @@ +import type { Node, Edge } from '@xyflow/react' + +export interface SourceNodeData { + label: string + balance: number + flowRate: number + [key: string]: unknown +} + +export interface ThresholdNodeData { + label: string + minThreshold: number + maxThreshold: number + currentValue: number + [key: string]: unknown +} + +export interface RecipientNodeData { + label: string + received: number + target: number + [key: string]: unknown +} + +export type FlowNode = Node +export type FlowEdge = Edge<{ animated?: boolean }> diff --git a/package.json b/package.json index cb3e3c6..779f21c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "dependencies": { "next": "^14.2.0", "react": "^18.3.0", - "react-dom": "^18.3.0" + "react-dom": "^18.3.0", + "@xyflow/react": "^12.0.0", + "zustand": "^4.5.0" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65abb57..9e80619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@xyflow/react': + specifier: ^12.0.0 + version: 12.10.0(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: ^14.2.0 version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17,6 +20,9 @@ importers: react-dom: specifier: ^18.3.0 version: 18.3.1(react@18.3.1) + zustand: + specifier: ^4.5.0 + version: 4.5.7(@types/react@18.3.27)(react@18.3.1) devDependencies: '@types/node': specifier: ^20.0.0 @@ -134,6 +140,24 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + '@types/node@20.19.30': resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} @@ -148,6 +172,15 @@ packages: '@types/react@18.3.27': resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@xyflow/react@12.10.0': + resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.74': + resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -197,6 +230,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -212,6 +248,44 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -535,9 +609,29 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -604,6 +698,27 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/node@20.19.30': dependencies: undici-types: 6.21.0 @@ -619,6 +734,29 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@xyflow/react@12.10.0(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.74 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.74': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + any-promise@1.3.0: {} anymatch@3.1.3: @@ -673,6 +811,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + classcat@5.0.5: {} + client-only@0.0.1: {} commander@4.1.1: {} @@ -681,6 +821,42 @@ snapshots: csstype@3.2.3: {} + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -977,4 +1153,15 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} + + zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + react: 18.3.1