Add flow visualization components and design spec

- DESIGN.md: Complete visual design specification for TBFF
- FlowCanvas: Main React Flow canvas with interactive demo
- SourceNode: Treasury/funding source visualization
- RecipientNode: Project recipient with threshold progress
- ThresholdGate: Visual gate showing open/locked state
- AnimatedFlowEdge: Animated particles showing fund flow
- Real-time flow simulation (balances update live)

Visual features:
- Particle animation along flow paths
- Threshold progress bars with color states
- Dark theme optimized for data visualization
- Mini-map and zoom controls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-01-29 18:01:38 +00:00
parent eefe4dfcc2
commit 14ebf0853d
11 changed files with 2591 additions and 21 deletions

269
DESIGN.md Normal file
View File

@ -0,0 +1,269 @@
# Flow Funding Visual Interface Design
## Core Concept: Threshold-Based Flow Funding (TBFF)
Unlike traditional discrete grants, TBFF enables **continuous resource flows** that respond dynamically to conditions. Funds stream like water through a network, with thresholds acting as gates that open, close, or modulate flow rates.
---
## Visual Metaphors
### 1. The River System
- **Sources** = Springs/tributaries (funding origins)
- **Flows** = Rivers/streams (animated fund movement)
- **Recipients** = Lakes/deltas (fund destinations)
- **Thresholds** = Dams/locks (control points)
### 2. The Circulatory System
- **Sources** = Heart (pumping resources)
- **Flows** = Arteries/veins (distribution network)
- **Recipients** = Organs (entities needing resources)
- **Thresholds** = Valves (flow regulation)
### 3. The Electrical Grid
- **Sources** = Power plants (generation)
- **Flows** = Transmission lines (distribution)
- **Recipients** = Consumers (utilization)
- **Thresholds** = Circuit breakers (protection/regulation)
---
## Key Visual Elements
### Flow Nodes
```
┌─────────────────────────────────────────────────────────┐
│ SOURCE NODE │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ╭──────────────╮ │ │
│ │ │ Treasury │ Balance: $50,000 │ │
│ │ │ 💰 │ Flow Rate: $100/hr │ │
│ │ ╰──────┬───────╯ Status: ● Active │ │
│ │ │ │ │
│ │ ═════╧═════▶ (animated particles) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ RECIPIENT NODE │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ═════╤═════▶ (incoming flow) │ │
│ │ │ │ │
│ │ ╭──────┴───────╮ │ │
│ │ │ Project │ Received: $12,340 │ │
│ │ │ 🎯 │ Rate: $85/hr │ │
│ │ ╰──────────────╯ Health: ████████░░ 80% │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### Threshold Visualization
```
THRESHOLD STATES:
Below Threshold At Threshold Above Threshold
(Flow Blocked) (Flow Starting) (Full Flow)
╱╲ ╱╲ ╱╲
⛔ ╲ ⚠️ ╲ ✅ ╲
╱──────╲ ╱──────╲ ╱──────╲
│░░░░░░│ │▓▓▓░░░│ │██████│
│░░░░░░│ │▓▓▓░░░│ │██████│
└──────┘ └──────┘ └──────┘
$2,000 $4,500 $8,000
─────── ─────── ───────
Threshold: $5,000 Threshold: $5,000 Threshold: $5,000
```
### Flow Animation Styles
```
1. PARTICLE FLOW (recommended)
○───○───○───○───○───▶
Dots flowing along the path, speed indicates rate
2. GRADIENT PULSE
████▓▓▒▒░░░░░░░░░░░▶
Color gradient pulses along the line
3. DASHED ANIMATION
─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶
Animated dashes moving toward recipient
4. THICKNESS ENCODING
═══════════════════▶ High flow
────────────────────▶ Medium flow
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ Low flow
```
---
## Interactive Features
### 1. Threshold Slider
Users can drag thresholds to see how changes affect flow dynamics:
```
┌────────────────────────────────────────────────────────┐
│ THRESHOLD CONTROL │
│ │
│ Min ├────────────●────────────────────────────┤ Max │
│ $0 $5,000 $50,000 │
│ ▲ │
│ │ │
│ Current: $5,000 │
│ │
│ Flow activates when source balance > threshold │
└────────────────────────────────────────────────────────┘
```
### 2. Flow Rate Dial
Adjust how fast funds flow when threshold is met:
```
╭─────────────╮
│ ╲
│ ────●──── │
╲ │
│ ╲
╰──────┴──────╯
$10 $100 $1000
/hr
Current Rate: $50/hr
```
### 3. Time Simulation
Scrub through time to see how flows evolve:
```
┌─────────────────────────────────────────────────────────┐
│ ◀◀ ◀ ▶ ▶▶ │▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░│ │
│ ▲ │
│ NOW │
│ │
│ Jan 2025 ─────────────────────────────────── Dec 2025 │
└─────────────────────────────────────────────────────────┘
```
---
## Screen Layouts
### Main Canvas View
```
┌──────────────────────────────────────────────────────────────────┐
│ FLOW FUNDING [+] [⚙] [?] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ╭─────────╮ ╭─────────╮ │
│ │ Treasury│ │Project A│ │
│ │ $50K │══════════════════════════│ $12K │ │
│ ╰────┬────╯ ╰─────────╯ │
│ │
│ │ ┌──────┐ │
│ ├═════│ GATE │═══════╗ ╭─────────╮ │
│ │ │$5K TH│ ║ │Project B│ │
│ │ └──────┘ ╚══════════│ $8K │ │
│ │ ╰─────────╯ │
│ │ │
│ │ ╭─────────╮ │
│ ╰═══════════════════════════════│Project C│ │
│ │ $5K │ │
│ ╰─────────╯ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ Total Flowing: $300/hr │ Active Flows: 3 │ Blocked: 1 │
└──────────────────────────────────────────────────────────────────┘
```
### Detail Panel (on node click)
```
┌─────────────────────────────────────┐
│ PROJECT A [×] │
├─────────────────────────────────────┤
│ │
│ Current Balance $12,340.50 │
│ Incoming Rate $85.00/hr │
│ ──────────────────────────────── │
│ │
│ THRESHOLD STATUS │
│ ████████████████░░░░ 80% │
│ $12,340 / $15,000 threshold │
│ │
│ When threshold met: │
│ → Unlock outgoing flow to Proj D │
│ → Increase rate to $150/hr │
│ │
│ ──────────────────────────────── │
│ FLOW HISTORY │
│ ┌─────────────────────────────┐ │
│ │ ╱╲ ╱╲ │ │
│ │ ╲ ╱╲ │ │
│ │ ╲╱ ╲ ╲ │ │
│ │ ╲╱ ╲ │ │
│ └─────────────────────────────┘ │
│ Jan Feb Mar Apr May Jun │
│ │
└─────────────────────────────────────┘
```
---
## Color System
| State | Color | Hex | Usage |
|-------|-------|-----|-------|
| Active Flow | Electric Blue | `#3B82F6` | Flowing connections |
| Threshold Met | Emerald | `#10B981` | Success states |
| Near Threshold | Amber | `#F59E0B` | Warning/attention |
| Below Threshold | Rose | `#F43F5E` | Blocked/inactive |
| Pending | Purple | `#8B5CF6` | Processing states |
| Background | Slate | `#0F172A` | Canvas background |
---
## Interaction Patterns
### Creating a New Flow
1. Click source node → drag to recipient
2. Release to open threshold configuration
3. Set threshold amount and flow rate
4. Flow appears with animation
### Adjusting Threshold
1. Click on threshold gate icon
2. Drag slider or enter exact value
3. See real-time preview of flow state change
4. Changes apply immediately or on confirm
### Time Travel
1. Pause current state
2. Drag timeline scrubber
3. See historical or projected flow states
4. Resume real-time or stay at selected time
---
## Technical Implementation
### React Flow Integration
- Custom node types: `SourceNode`, `RecipientNode`, `ThresholdGate`
- Custom edge type: `AnimatedFlowEdge` with particle animation
- Minimap for large networks
- Pan/zoom controls
### Animation System
- Canvas-based particle system for flow animation
- 60fps smooth animations
- Performance-optimized for 100+ nodes
### State Management
- Real-time flow calculations
- Threshold state machine
- Time-series data for history/projections

View File

@ -3,18 +3,71 @@
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: 10, 10, 10;
}
--foreground-rgb: 255, 255, 255;
--background-rgb: 2, 6, 23;
}
body {
color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb));
}
/* Flow Animation */
@keyframes flowAnimation {
from {
stroke-dashoffset: 16;
}
to {
stroke-dashoffset: 0;
}
}
/* React Flow Customization */
.react-flow__node {
cursor: grab;
}
.react-flow__node:active {
cursor: grabbing;
}
.react-flow__controls {
background: #1e293b !important;
border: 1px solid #334155 !important;
border-radius: 8px !important;
}
.react-flow__controls-button {
background: #1e293b !important;
border-bottom: 1px solid #334155 !important;
fill: #94a3b8 !important;
}
.react-flow__controls-button:hover {
background: #334155 !important;
}
.react-flow__minimap {
background: #0f172a !important;
border: 1px solid #334155 !important;
border-radius: 8px !important;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}

View File

@ -1,20 +1,45 @@
'use client'
import dynamic from 'next/dynamic'
// Dynamic import to avoid SSR issues with React Flow
const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center bg-slate-950">
<div className="text-slate-400 animate-pulse">Loading flow visualization...</div>
</div>
),
})
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-8">
<div className="text-center space-y-6">
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-emerald-400 bg-clip-text text-transparent">
Flow Funding
</h1>
<p className="text-xl text-slate-300 max-w-2xl">
Visual interactive interface for threshold-based flow funding mechanisms
</p>
<div className="mt-8 p-8 rounded-2xl border border-slate-700 bg-slate-800/50 backdrop-blur">
<p className="text-slate-400">
Interactive visualization coming soon...
</p>
<main className="h-screen w-screen overflow-hidden">
{/* Header */}
<header className="absolute top-0 left-0 right-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-700">
<div className="px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-emerald-400 bg-clip-text text-transparent">
Flow Funding
</h1>
<span className="text-xs text-slate-500 border border-slate-700 px-2 py-0.5 rounded">
TBFF Demo
</span>
</div>
<div className="flex items-center gap-4">
<button className="px-3 py-1.5 text-sm text-slate-300 hover:text-white transition">
Reset
</button>
<button className="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition">
+ Add Node
</button>
</div>
</div>
</header>
{/* Flow Canvas */}
<div className="pt-16 h-full">
<FlowCanvas />
</div>
</main>
)

15
backlog/config.yml Normal file
View File

@ -0,0 +1,15 @@
project_name: "Flow Funding"
default_status: "To Do"
statuses: ["To Do", "In Progress", "Done"]
labels: []
milestones: []
date_format: yyyy-mm-dd
max_column_width: 20
default_editor: "nvim"
auto_open_browser: true
default_port: 6420
remote_operations: true
auto_commit: false
bypass_git_hooks: false
check_active_branches: true
active_branch_days: 30

246
components/FlowCanvas.tsx Normal file
View File

@ -0,0 +1,246 @@
'use client'
import { useCallback, useState, useEffect } from 'react'
import {
ReactFlow,
MiniMap,
Controls,
Background,
BackgroundVariant,
useNodesState,
useEdgesState,
addEdge,
type Connection,
type Node,
type Edge,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import SourceNode from './nodes/SourceNode'
import RecipientNode from './nodes/RecipientNode'
import ThresholdGate from './nodes/ThresholdGate'
import AnimatedFlowEdge from './edges/AnimatedFlowEdge'
const nodeTypes = {
source: SourceNode,
recipient: RecipientNode,
threshold: ThresholdGate,
}
const edgeTypes = {
animated: AnimatedFlowEdge,
}
// Demo initial nodes
const initialNodes: Node[] = [
{
id: 'treasury',
type: 'source',
position: { x: 50, y: 200 },
data: {
label: 'Community Treasury',
balance: 50000,
flowRate: 100,
isActive: true,
},
},
{
id: 'gate1',
type: 'threshold',
position: { x: 300, y: 100 },
data: {
threshold: 5000,
currentValue: 8000,
label: 'Project Gate',
},
},
{
id: 'gate2',
type: 'threshold',
position: { x: 300, y: 300 },
data: {
threshold: 10000,
currentValue: 7500,
label: 'Research Gate',
},
},
{
id: 'project-a',
type: 'recipient',
position: { x: 550, y: 50 },
data: {
label: 'Project Alpha',
received: 12340,
incomingRate: 85,
threshold: 15000,
isReceiving: true,
},
},
{
id: 'project-b',
type: 'recipient',
position: { x: 550, y: 200 },
data: {
label: 'Project Beta',
received: 8200,
incomingRate: 50,
threshold: 20000,
isReceiving: true,
},
},
{
id: 'project-c',
type: 'recipient',
position: { x: 550, y: 350 },
data: {
label: 'Research Fund',
received: 3100,
incomingRate: 0,
threshold: 10000,
isReceiving: false,
},
},
]
const initialEdges: Edge[] = [
{
id: 'e-treasury-gate1',
source: 'treasury',
target: 'gate1',
type: 'animated',
data: { flowRate: 100, isActive: true, color: '#3B82F6' },
},
{
id: 'e-treasury-gate2',
source: 'treasury',
target: 'gate2',
type: 'animated',
data: { flowRate: 75, isActive: true, color: '#3B82F6' },
},
{
id: 'e-gate1-projecta',
source: 'gate1',
target: 'project-a',
type: 'animated',
data: { flowRate: 85, isActive: true, color: '#10B981' },
},
{
id: 'e-gate1-projectb',
source: 'gate1',
target: 'project-b',
type: 'animated',
data: { flowRate: 50, isActive: true, color: '#10B981' },
},
{
id: 'e-gate2-projectc',
source: 'gate2',
target: 'project-c',
type: 'animated',
data: { flowRate: 0, isActive: false, color: '#F43F5E' },
},
]
export default function FlowCanvas() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge({
...params,
type: 'animated',
data: { flowRate: 50, isActive: true, color: '#8B5CF6' },
}, eds)),
[setEdges]
)
// Simulate flow updates
useEffect(() => {
const interval = setInterval(() => {
setNodes((nds) =>
nds.map((node) => {
if (node.type === 'recipient' && node.data.isReceiving) {
const hourlyRate = node.data.incomingRate / 3600 // per second
return {
...node,
data: {
...node.data,
received: node.data.received + hourlyRate,
},
}
}
if (node.type === 'source' && node.data.isActive) {
const hourlyRate = node.data.flowRate / 3600
return {
...node,
data: {
...node.data,
balance: Math.max(0, node.data.balance - hourlyRate),
},
}
}
return node
})
)
}, 1000)
return () => clearInterval(interval)
}, [setNodes])
return (
<div className="w-full h-full">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
className="bg-slate-950"
>
<Controls className="bg-slate-800 border-slate-700" />
<MiniMap
className="bg-slate-800 border-slate-700"
nodeColor={(node) => {
switch (node.type) {
case 'source':
return '#3B82F6'
case 'recipient':
return '#8B5CF6'
case 'threshold':
return '#F59E0B'
default:
return '#64748B'
}
}}
/>
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1E293B" />
</ReactFlow>
{/* Stats Bar */}
<div className="absolute bottom-0 left-0 right-0 bg-slate-900/90 backdrop-blur border-t border-slate-700 px-6 py-3">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
<span className="text-slate-400">Total Flowing:</span>
<span className="text-white font-mono">$175/hr</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">Active Flows:</span>
<span className="text-emerald-400 font-mono">4</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">Blocked:</span>
<span className="text-rose-400 font-mono">1</span>
</div>
</div>
<div className="text-slate-500 text-xs">
Drag nodes to rearrange Click to inspect Connect nodes to create flows
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,110 @@
'use client'
import { memo } from 'react'
import { BaseEdge, EdgeProps, getBezierPath } from '@xyflow/react'
export interface AnimatedFlowEdgeData {
flowRate: number
isActive: boolean
color?: string
}
function AnimatedFlowEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
style = {},
}: EdgeProps<AnimatedFlowEdgeData>) {
const { flowRate = 0, isActive = false, color = '#3B82F6' } = (data || {}) as AnimatedFlowEdgeData
const [edgePath] = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
})
// Calculate animation speed based on flow rate
const animationDuration = Math.max(2 - (flowRate / 100), 0.5)
return (
<>
{/* Background path */}
<BaseEdge
id={id}
path={edgePath}
style={{
...style,
stroke: isActive ? color : '#475569',
strokeWidth: isActive ? 3 : 2,
strokeOpacity: isActive ? 0.3 : 0.5,
}}
/>
{/* Animated dashed overlay when active */}
{isActive && (
<path
d={edgePath}
fill="none"
stroke={color}
strokeWidth={3}
strokeDasharray="8 8"
style={{
animation: `flowAnimation ${animationDuration}s linear infinite`,
}}
/>
)}
{/* Flow particles */}
{isActive && (
<>
<circle r="4" fill={color}>
<animateMotion
dur={`${animationDuration}s`}
repeatCount="indefinite"
path={edgePath}
/>
</circle>
<circle r="4" fill={color}>
<animateMotion
dur={`${animationDuration}s`}
repeatCount="indefinite"
path={edgePath}
begin={`${animationDuration / 3}s`}
/>
</circle>
<circle r="4" fill={color}>
<animateMotion
dur={`${animationDuration}s`}
repeatCount="indefinite"
path={edgePath}
begin={`${(animationDuration / 3) * 2}s`}
/>
</circle>
</>
)}
{/* Flow rate label */}
{isActive && flowRate > 0 && (
<text
x={(sourceX + targetX) / 2}
y={(sourceY + targetY) / 2 - 10}
textAnchor="middle"
className="text-xs fill-slate-400 font-mono"
style={{ fontSize: '10px' }}
>
${flowRate}/hr
</text>
)}
</>
)
}
export default memo(AnimatedFlowEdge)

View File

@ -0,0 +1,101 @@
'use client'
import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
export interface RecipientNodeData {
label: string
received: number
incomingRate: number
threshold: number
isReceiving: boolean
}
function RecipientNode({ data }: NodeProps<RecipientNodeData>) {
const { label, received, incomingRate, threshold, isReceiving } = data as RecipientNodeData
const progress = Math.min((received / threshold) * 100, 100)
const thresholdMet = received >= threshold
return (
<div className={`
px-6 py-4 rounded-xl border-2 min-w-[200px]
bg-gradient-to-br from-slate-800 to-slate-900
${thresholdMet ? 'border-emerald-500 shadow-lg shadow-emerald-500/20' :
isReceiving ? 'border-purple-500 shadow-lg shadow-purple-500/20' : 'border-slate-600'}
transition-all duration-300
`}>
<Handle
type="target"
position={Position.Left}
className="w-3 h-3 bg-purple-500 border-2 border-slate-900"
/>
<div className="flex items-center gap-3 mb-3">
<div className={`
w-10 h-10 rounded-full flex items-center justify-center text-xl
${thresholdMet ? 'bg-emerald-500/20' : isReceiving ? 'bg-purple-500/20' : 'bg-slate-700'}
`}>
🎯
</div>
<div>
<div className="font-semibold text-white">{label}</div>
<div className="text-xs text-slate-400">Recipient</div>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-400">Received</span>
<span className="text-white font-mono">
${received.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">Rate</span>
<span className={`font-mono ${isReceiving ? 'text-purple-400' : 'text-slate-500'}`}>
+${incomingRate}/hr
</span>
</div>
</div>
{/* Threshold Progress Bar */}
<div className="mt-4">
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">Threshold</span>
<span className={thresholdMet ? 'text-emerald-400' : 'text-slate-400'}>
{progress.toFixed(0)}%
</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-500 ${
thresholdMet ? 'bg-emerald-500' :
progress > 75 ? 'bg-amber-500' : 'bg-purple-500'
}`}
style={{ width: `${progress}%` }}
/>
</div>
<div className="flex justify-between text-xs mt-1 text-slate-500">
<span>${received.toLocaleString()}</span>
<span>${threshold.toLocaleString()}</span>
</div>
</div>
{thresholdMet && (
<div className="mt-3 flex items-center gap-2 text-emerald-400 text-sm">
<span></span>
<span>Threshold Met!</span>
</div>
)}
{/* Optional: outgoing handle for cascading flows */}
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 bg-emerald-500 border-2 border-slate-900"
/>
</div>
)
}
export default memo(RecipientNode)

View File

@ -0,0 +1,66 @@
'use client'
import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
export interface SourceNodeData {
label: string
balance: number
flowRate: number
isActive: boolean
}
function SourceNode({ data }: NodeProps<SourceNodeData>) {
const { label, balance, flowRate, isActive } = data as SourceNodeData
return (
<div className={`
px-6 py-4 rounded-xl border-2 min-w-[180px]
bg-gradient-to-br from-slate-800 to-slate-900
${isActive ? 'border-blue-500 shadow-lg shadow-blue-500/20' : 'border-slate-600'}
transition-all duration-300
`}>
<div className="flex items-center gap-3 mb-3">
<div className={`
w-10 h-10 rounded-full flex items-center justify-center text-xl
${isActive ? 'bg-blue-500/20' : 'bg-slate-700'}
`}>
💰
</div>
<div>
<div className="font-semibold text-white">{label}</div>
<div className="text-xs text-slate-400">Source</div>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-400">Balance</span>
<span className="text-white font-mono">
${balance.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">Flow Rate</span>
<span className={`font-mono ${isActive ? 'text-blue-400' : 'text-slate-500'}`}>
${flowRate}/hr
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${isActive ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'}`} />
<span className={isActive ? 'text-emerald-400' : 'text-slate-500'}>
{isActive ? 'Flowing' : 'Inactive'}
</span>
</div>
</div>
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 bg-blue-500 border-2 border-slate-900"
/>
</div>
)
}
export default memo(SourceNode)

View File

@ -0,0 +1,85 @@
'use client'
import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
export interface ThresholdGateData {
threshold: number
currentValue: number
label?: string
}
function ThresholdGate({ data }: NodeProps<ThresholdGateData>) {
const { threshold, currentValue, label } = data as ThresholdGateData
const isOpen = currentValue >= threshold
const progress = Math.min((currentValue / threshold) * 100, 100)
return (
<div className={`
relative px-4 py-3 rounded-lg border-2 min-w-[120px]
bg-slate-900/90 backdrop-blur
${isOpen ? 'border-emerald-500' : progress > 75 ? 'border-amber-500' : 'border-rose-500'}
transition-all duration-300
`}>
<Handle
type="target"
position={Position.Left}
className="w-3 h-3 bg-blue-500 border-2 border-slate-900"
/>
{/* Gate Icon */}
<div className="flex flex-col items-center">
<div className={`
text-2xl mb-1 transition-transform duration-300
${isOpen ? 'scale-110' : 'scale-100'}
`}>
{isOpen ? '🔓' : progress > 75 ? '⚠️' : '🔒'}
</div>
{label && (
<div className="text-xs text-slate-400 mb-2">{label}</div>
)}
{/* Mini threshold meter */}
<div className="w-full">
<div className="h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-300 ${
isOpen ? 'bg-emerald-500' :
progress > 75 ? 'bg-amber-500' : 'bg-rose-500'
}`}
style={{ width: `${progress}%` }}
/>
</div>
</div>
<div className="mt-2 text-center">
<div className={`text-xs font-mono ${
isOpen ? 'text-emerald-400' : 'text-slate-400'
}`}>
${currentValue.toLocaleString()}
</div>
<div className="text-xs text-slate-500">
/ ${threshold.toLocaleString()}
</div>
</div>
<div className={`
mt-2 text-xs px-2 py-0.5 rounded-full
${isOpen ? 'bg-emerald-500/20 text-emerald-400' :
progress > 75 ? 'bg-amber-500/20 text-amber-400' : 'bg-rose-500/20 text-rose-400'}
`}>
{isOpen ? 'OPEN' : progress > 75 ? 'NEAR' : 'LOCKED'}
</div>
</div>
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 bg-emerald-500 border-2 border-slate-900"
/>
</div>
)
}
export default memo(ThresholdGate)

View File

@ -0,0 +1,7 @@
export { default as SourceNode } from './SourceNode'
export { default as RecipientNode } from './RecipientNode'
export { default as ThresholdGate } from './ThresholdGate'
export type { SourceNodeData } from './SourceNode'
export type { RecipientNodeData } from './RecipientNode'
export type { ThresholdGateData } from './ThresholdGate'

1593
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff