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:
parent
eefe4dfcc2
commit
14ebf0853d
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
49
app/page.tsx
49
app/page.tsx
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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'
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue