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 */}
-
-
- {/* 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 (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 */}
-
-
-
- {/* Zone labels */}
-
- OVERFLOW
-
-
- HEALTHY
-
-
- CRITICAL
-
-
-
- {/* Stats Panel */}
-
- {/* Current Balance */}
-
-
Current Balance
-
- ${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })}
-
-
- {status.label}
-
-
-
- {/* Flow Rates */}
-
-
Flow Rates
-
-
-
-
-
-
-
-
- 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 */}
+
+
+ {/* 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 */}
+
+
+ {/* 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