Add funnel-style TBFF visualization with draggable thresholds
New FundingFunnel component with three distinct zones: - Overflow zone (above MAX): Straight walls, excess redistribution - Healthy zone (MIN to MAX): Straight walls, normal operations - Critical zone (below MIN): Narrowing funnel, restricted outflow Features: - Draggable MIN/MAX threshold lines with real-time updates - Animated inflow particles (blue) dropping from top - Animated outflow particles (pink) from bottom spout - Gradient liquid fill with glow effect - Live balance simulation based on flow rates - Status panel showing current balance, rates, progress - Single/Multi-funnel view toggle - Zone color coding (rose/emerald/amber) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
14ebf0853d
commit
4b0491e999
398
DESIGN.md
398
DESIGN.md
|
|
@ -2,268 +2,232 @@
|
||||||
|
|
||||||
## Core Concept: Threshold-Based Flow Funding (TBFF)
|
## 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.
|
Unlike traditional discrete grants, TBFF enables **continuous resource flows** that respond dynamically to threshold conditions. The funnel visualization makes these dynamics intuitive and interactive.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Visual Metaphors
|
## The Funnel Metaphor
|
||||||
|
|
||||||
### 1. The River System
|
The funding funnel has three distinct zones defined by minimum and maximum thresholds:
|
||||||
- **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)
|
│ │ OVERFLOW ZONE
|
||||||
- **Recipients** = Organs (entities needing resources)
|
│ Excess funds │ (above MAX)
|
||||||
- **Thresholds** = Valves (flow regulation)
|
│ redistribute │
|
||||||
|
MAX ─ ─ ─ ─ ─ ─ ┼──────────────────────┼ ─ ─ ─ ─ ─ ─
|
||||||
|
│ │
|
||||||
|
│ HEALTHY ZONE │ STRAIGHT WALLS
|
||||||
|
│ Normal operations │ (MIN to MAX)
|
||||||
|
│ │
|
||||||
|
MIN ─ ─ ─ ─ ─ ─ ┼──────────────────────┼ ─ ─ ─ ─ ─ ─
|
||||||
|
╲ ╱
|
||||||
|
╲ CRITICAL ZONE ╱ NARROWING FUNNEL
|
||||||
|
╲ Restricted ╱ (below MIN)
|
||||||
|
╲ outflow ╱
|
||||||
|
╲ ╱
|
||||||
|
╲________╱
|
||||||
|
║║
|
||||||
|
outflow
|
||||||
|
```
|
||||||
|
|
||||||
### 3. The Electrical Grid
|
### Zone Behaviors
|
||||||
- **Sources** = Power plants (generation)
|
|
||||||
- **Flows** = Transmission lines (distribution)
|
| Zone | Shape | Behavior |
|
||||||
- **Recipients** = Consumers (utilization)
|
|------|-------|----------|
|
||||||
- **Thresholds** = Circuit breakers (protection/regulation)
|
| **Overflow** (above MAX) | Straight walls | Excess funds spill over, can redirect to other pools |
|
||||||
|
| **Healthy** (MIN to MAX) | Straight walls | Normal flow rate, balanced operations |
|
||||||
|
| **Critical** (below MIN) | Narrowing funnel | Outflow restricted, conservation mode |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Visual Elements
|
## Visual Elements
|
||||||
|
|
||||||
### Flow Nodes
|
### Animated Flows
|
||||||
|
|
||||||
|
**Inflow (blue particles)**
|
||||||
|
- Drops falling from above into the funnel
|
||||||
|
- Speed/density indicates inflow rate
|
||||||
|
- Particles merge into the liquid surface
|
||||||
|
|
||||||
|
**Outflow (pink particles)**
|
||||||
|
- Drops falling from the bottom spout
|
||||||
|
- Throttled in critical zone (fewer/slower)
|
||||||
|
- Full rate in healthy zone
|
||||||
|
|
||||||
|
**Liquid Fill**
|
||||||
|
- Gradient fill showing current balance level
|
||||||
|
- Subtle glow/pulse animation
|
||||||
|
- Wave effect at surface
|
||||||
|
|
||||||
|
### Threshold Indicators
|
||||||
|
|
||||||
|
**Minimum Threshold (Rose/Red)**
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────┐
|
════════════╳════════════ ← Draggable handle
|
||||||
│ SOURCE NODE │
|
MIN $20,000
|
||||||
│ ┌─────────────────────────────────────────────────┐ │
|
|
||||||
│ │ ╭──────────────╮ │ │
|
|
||||||
│ │ │ 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
|
**Maximum Threshold (Amber/Yellow)**
|
||||||
|
|
||||||
```
|
```
|
||||||
THRESHOLD STATES:
|
════════════╳════════════ ← Draggable handle
|
||||||
|
MAX $80,000
|
||||||
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
|
### Status Panel
|
||||||
|
|
||||||
```
|
```
|
||||||
1. PARTICLE FLOW (recommended)
|
┌─────────────────────────────┐
|
||||||
○───○───○───○───○───▶
|
│ Current Balance │
|
||||||
Dots flowing along the path, speed indicates rate
|
│ ████████████████░░░░░░░░ │
|
||||||
|
│ $45,000 │
|
||||||
2. GRADIENT PULSE
|
│ ● Healthy Range │
|
||||||
████▓▓▒▒░░░░░░░░░░░▶
|
├─────────────────────────────┤
|
||||||
Color gradient pulses along the line
|
│ Flow Rates │
|
||||||
|
│ ↓ Inflow: +$500/hr │
|
||||||
3. DASHED ANIMATION
|
│ ↑ Outflow: -$300/hr │
|
||||||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶
|
│ ═ Net: +$200/hr │
|
||||||
Animated dashes moving toward recipient
|
├─────────────────────────────┤
|
||||||
|
│ Progress │
|
||||||
4. THICKNESS ENCODING
|
│ To MIN: ████████░░ 80% │
|
||||||
═══════════════════▶ High flow
|
│ To MAX: ██████░░░░ 60% │
|
||||||
────────────────────▶ Medium flow
|
└─────────────────────────────┘
|
||||||
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ Low flow
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Interactive Features
|
## Interaction Design
|
||||||
|
|
||||||
### 1. Threshold Slider
|
### Dragging Thresholds
|
||||||
Users can drag thresholds to see how changes affect flow dynamics:
|
|
||||||
|
1. **Hover** over threshold line → cursor changes to resize
|
||||||
|
2. **Click & drag** up/down to adjust value
|
||||||
|
3. **Real-time preview** of zone changes
|
||||||
|
4. **Constraints**: MIN cannot exceed MAX, minimum gap enforced
|
||||||
|
|
||||||
|
### Visual Feedback During Drag
|
||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────────────────────────────────┐
|
Before drag: During drag: After release:
|
||||||
│ THRESHOLD CONTROL │
|
════════════ ════════════════ ════════════
|
||||||
│ │
|
│ ║ (thicker) │
|
||||||
│ Min ├────────────●────────────────────────────┤ Max │
|
│ ║ │
|
||||||
│ $0 $5,000 $50,000 │
|
│ New value shown │
|
||||||
│ ▲ │
|
|
||||||
│ │ │
|
|
||||||
│ Current: $5,000 │
|
|
||||||
│ │
|
|
||||||
│ Flow activates when source balance > threshold │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Flow Rate Dial
|
### Responsive Behaviors
|
||||||
Adjust how fast funds flow when threshold is met:
|
|
||||||
|
|
||||||
```
|
| Balance State | Visual Response |
|
||||||
╭─────────────╮
|
|---------------|-----------------|
|
||||||
╱ │ ╲
|
| Approaching MIN | Rose tint intensifies, warning pulse |
|
||||||
│ ────●──── │
|
| Crossing MIN ↓ | Funnel narrows animation, outflow slows |
|
||||||
│ ╱ ╲ │
|
| Crossing MIN ↑ | Funnel widens animation, outflow normalizes |
|
||||||
╲ ╱ │ ╲ ╱
|
| Approaching MAX | Amber tint at top, overflow warning |
|
||||||
╰──────┴──────╯
|
| Crossing MAX ↑ | Overflow particles, excess redistribution |
|
||||||
$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
|
## Color System
|
||||||
|
|
||||||
| State | Color | Hex | Usage |
|
| Element | Color | Hex | Usage |
|
||||||
|-------|-------|-----|-------|
|
|---------|-------|-----|-------|
|
||||||
| Active Flow | Electric Blue | `#3B82F6` | Flowing connections |
|
| Inflow | Blue | `#3B82F6` | Incoming fund particles |
|
||||||
| Threshold Met | Emerald | `#10B981` | Success states |
|
| Outflow | Pink | `#EC4899` | Outgoing fund particles |
|
||||||
| Near Threshold | Amber | `#F59E0B` | Warning/attention |
|
| Fill Gradient | Blue→Purple→Pink | gradient | Liquid level |
|
||||||
| Below Threshold | Rose | `#F43F5E` | Blocked/inactive |
|
| Critical Zone | Rose | `#F43F5E` | Below minimum |
|
||||||
| Pending | Purple | `#8B5CF6` | Processing states |
|
| Healthy Zone | Emerald | `#10B981` | Normal range |
|
||||||
| Background | Slate | `#0F172A` | Canvas background |
|
| Overflow Zone | Amber | `#F59E0B` | Above maximum |
|
||||||
|
| Background | Slate | `#0F172A` | Canvas |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Interaction Patterns
|
## Multi-Funnel View
|
||||||
|
|
||||||
### Creating a New Flow
|
When displaying multiple funding pools:
|
||||||
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
|
│ Threshold-Based Flow Funding │
|
||||||
3. See real-time preview of flow state change
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
4. Changes apply immediately or on confirm
|
│ │
|
||||||
|
│ ╔═══════╗ ╔═══════╗ ╔═══════╗ │
|
||||||
|
│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │
|
||||||
|
│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │
|
||||||
|
│ ║░░░░░░░║ ║▓▓▓▓▓▓▓║ ║███████║ │
|
||||||
|
│ ║░░░░░░░║ ╚═══╤═══╝ ╚═══╤═══╝ │
|
||||||
|
│ ╚═══╤═══╝ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ Public Goods Research Emergency │
|
||||||
|
│ $12K / $30K $45K / $60K $85K / $70K │
|
||||||
|
│ ● Critical ● Healthy ● Overflow │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
### Time Travel
|
### Interconnected Flows
|
||||||
1. Pause current state
|
|
||||||
2. Drag timeline scrubber
|
Future enhancement: Connect funnels so overflow from one feeds into another:
|
||||||
3. See historical or projected flow states
|
|
||||||
4. Resume real-time or stay at selected time
|
```
|
||||||
|
Fund A Fund B
|
||||||
|
╔═════╗ ╔═════╗
|
||||||
|
║█████║ ═══overflow═══▶ ║░░░░░║
|
||||||
|
║█████║ ║░░░░░║
|
||||||
|
╚══╤══╝ ╚══╤══╝
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technical Implementation
|
## Technical Implementation
|
||||||
|
|
||||||
### React Flow Integration
|
### Component Architecture
|
||||||
- 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
|
<FundingFunnel>
|
||||||
- 60fps smooth animations
|
├── <SVG Canvas>
|
||||||
- Performance-optimized for 100+ nodes
|
│ ├── Zone backgrounds (overflow, healthy, critical)
|
||||||
|
│ ├── Funnel outline path
|
||||||
|
│ ├── Liquid fill path (animated)
|
||||||
|
│ ├── Inflow particles (animated)
|
||||||
|
│ ├── Outflow particles (animated)
|
||||||
|
│ ├── Threshold lines (draggable)
|
||||||
|
│ └── Balance indicator
|
||||||
|
└── <Stats Panel>
|
||||||
|
├── Current balance display
|
||||||
|
├── Flow rates
|
||||||
|
└── Progress bars
|
||||||
|
```
|
||||||
|
|
||||||
### State Management
|
### State Management
|
||||||
- Real-time flow calculations
|
|
||||||
- Threshold state machine
|
```typescript
|
||||||
- Time-series data for history/projections
|
interface FunnelState {
|
||||||
|
balance: number // Current fund level
|
||||||
|
minThreshold: number // Minimum threshold (draggable)
|
||||||
|
maxThreshold: number // Maximum threshold (draggable)
|
||||||
|
inflowRate: number // Funds per hour coming in
|
||||||
|
outflowRate: number // Funds per hour going out
|
||||||
|
maxCapacity: number // Visual ceiling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animation Loop
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simulation runs at 10Hz
|
||||||
|
setInterval(() => {
|
||||||
|
const netFlow = (inflowRate - outflowRate) / 3600 // per second
|
||||||
|
balance = clamp(balance + netFlow, 0, maxCapacity * 1.2)
|
||||||
|
}, 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Cascading Thresholds** — When one fund overflows, automatically route to connected funds
|
||||||
|
2. **Historical Playback** — Scrub through time to see past flow dynamics
|
||||||
|
3. **Prediction Mode** — Project future states based on current rates
|
||||||
|
4. **Smart Routing** — AI-suggested threshold adjustments for optimal distribution
|
||||||
|
5. **Real Data Integration** — Connect to actual treasury/DAO data sources
|
||||||
|
6. **Export/Share** — Save configurations and share visualizations
|
||||||
|
|
|
||||||
151
app/page.tsx
151
app/page.tsx
|
|
@ -1,45 +1,154 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
// Dynamic import to avoid SSR issues with React Flow
|
const FundingFunnel = dynamic(() => import('@/components/FundingFunnel'), {
|
||||||
const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), {
|
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div className="w-full h-full flex items-center justify-center bg-slate-950">
|
<div className="w-[500px] h-[600px] flex items-center justify-center bg-slate-900 rounded-2xl">
|
||||||
<div className="text-slate-400 animate-pulse">Loading flow visualization...</div>
|
<div className="text-slate-400 animate-pulse">Loading visualization...</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [view, setView] = useState<'single' | 'multi'>('single')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="h-screen w-screen overflow-hidden">
|
<main className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="absolute top-0 left-0 right-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-700">
|
<header className="mb-8">
|
||||||
<div className="px-6 py-4 flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div>
|
||||||
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-emerald-400 bg-clip-text text-transparent">
|
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||||
Flow Funding
|
Threshold-Based Flow Funding
|
||||||
</h1>
|
</h1>
|
||||||
<span className="text-xs text-slate-500 border border-slate-700 px-2 py-0.5 rounded">
|
<p className="text-slate-400 mt-2">
|
||||||
TBFF Demo
|
Interactive visualization of funding flows with minimum and maximum thresholds
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex gap-2">
|
||||||
<button className="px-3 py-1.5 text-sm text-slate-300 hover:text-white transition">
|
<button
|
||||||
Reset
|
onClick={() => setView('single')}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm transition ${
|
||||||
|
view === 'single'
|
||||||
|
? 'bg-purple-600 text-white'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Single Funnel
|
||||||
</button>
|
</button>
|
||||||
<button className="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition">
|
<button
|
||||||
+ Add Node
|
onClick={() => setView('multi')}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm transition ${
|
||||||
|
view === 'multi'
|
||||||
|
? 'bg-purple-600 text-white'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Multi-Funnel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Flow Canvas */}
|
{/* Legend */}
|
||||||
<div className="pt-16 h-full">
|
<div className="mb-8 flex items-center gap-8 text-sm">
|
||||||
<FlowCanvas />
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-rose-500/30 border border-rose-500" />
|
||||||
|
<span className="text-slate-400">Critical Zone (below min)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-emerald-500/30 border border-emerald-500" />
|
||||||
|
<span className="text-slate-400">Healthy Zone (min to max)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded bg-amber-500/30 border border-amber-500" />
|
||||||
|
<span className="text-slate-400">Overflow Zone (above max)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Funnel Visualizations */}
|
||||||
|
{view === 'single' ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<FundingFunnel
|
||||||
|
name="Community Treasury"
|
||||||
|
currentBalance={35000}
|
||||||
|
minThreshold={20000}
|
||||||
|
maxThreshold={80000}
|
||||||
|
inflowRate={500}
|
||||||
|
outflowRate={300}
|
||||||
|
maxCapacity={100000}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8 justify-items-center">
|
||||||
|
<FundingFunnel
|
||||||
|
name="Public Goods Fund"
|
||||||
|
currentBalance={45000}
|
||||||
|
minThreshold={25000}
|
||||||
|
maxThreshold={75000}
|
||||||
|
inflowRate={400}
|
||||||
|
outflowRate={350}
|
||||||
|
maxCapacity={100000}
|
||||||
|
/>
|
||||||
|
<FundingFunnel
|
||||||
|
name="Research Grant Pool"
|
||||||
|
currentBalance={12000}
|
||||||
|
minThreshold={30000}
|
||||||
|
maxThreshold={60000}
|
||||||
|
inflowRate={200}
|
||||||
|
outflowRate={150}
|
||||||
|
maxCapacity={80000}
|
||||||
|
/>
|
||||||
|
<FundingFunnel
|
||||||
|
name="Emergency Reserve"
|
||||||
|
currentBalance={85000}
|
||||||
|
minThreshold={50000}
|
||||||
|
maxThreshold={70000}
|
||||||
|
inflowRate={300}
|
||||||
|
outflowRate={100}
|
||||||
|
maxCapacity={100000}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div className="mt-12 max-w-2xl mx-auto">
|
||||||
|
<div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-4">How It Works</h3>
|
||||||
|
<ul className="space-y-3 text-slate-300">
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="text-rose-400 font-bold">1.</span>
|
||||||
|
<span>
|
||||||
|
<strong className="text-rose-400">Minimum Threshold</strong> — Below this level,
|
||||||
|
the funnel narrows, restricting outflow. Funds are conserved until the minimum is reached.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="text-emerald-400 font-bold">2.</span>
|
||||||
|
<span>
|
||||||
|
<strong className="text-emerald-400">Healthy Range</strong> — Between min and max,
|
||||||
|
the funnel has straight walls. Normal operations with balanced in/out flows.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="text-amber-400 font-bold">3.</span>
|
||||||
|
<span>
|
||||||
|
<strong className="text-amber-400">Maximum Threshold</strong> — Above this level,
|
||||||
|
excess funds overflow and can be redistributed to other pools or purposes.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="text-purple-400 font-bold">↕</span>
|
||||||
|
<span>
|
||||||
|
<strong className="text-purple-400">Drag the threshold lines</strong> to adjust
|
||||||
|
minimum and maximum levels interactively.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,576 @@
|
||||||
|
'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<HTMLDivElement>(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 (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">{name}</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-8">
|
||||||
|
{/* Main Funnel Visualization */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative bg-slate-900 rounded-2xl p-4 border border-slate-700"
|
||||||
|
style={{ width: width + 80, height: height + 40 }}
|
||||||
|
>
|
||||||
|
<svg width={width} height={height} className="overflow-visible">
|
||||||
|
<defs>
|
||||||
|
{/* Gradient for the fill */}
|
||||||
|
<linearGradient id={`fill-gradient-${name}`} x1="0%" y1="100%" x2="0%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#3B82F6" stopOpacity="0.9" />
|
||||||
|
<stop offset="50%" stopColor="#8B5CF6" stopOpacity="0.8" />
|
||||||
|
<stop offset="100%" stopColor="#EC4899" stopOpacity="0.7" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
{/* Glow filter */}
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
{/* Wave pattern for liquid effect */}
|
||||||
|
<pattern id="wave" x="0" y="0" width="20" height="10" patternUnits="userSpaceOnUse">
|
||||||
|
<path
|
||||||
|
d="M0 5 Q5 0 10 5 T20 5"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255,255,255,0.1)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Background zones */}
|
||||||
|
{/* Overflow zone (above max) */}
|
||||||
|
<rect
|
||||||
|
x={padding}
|
||||||
|
y={padding}
|
||||||
|
width={width - 2 * padding}
|
||||||
|
height={maxY - padding}
|
||||||
|
fill="#F59E0B"
|
||||||
|
fillOpacity="0.1"
|
||||||
|
stroke="#F59E0B"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Healthy zone (between min and max) - straight walls */}
|
||||||
|
<rect
|
||||||
|
x={padding}
|
||||||
|
y={maxY}
|
||||||
|
width={width - 2 * padding}
|
||||||
|
height={minY - maxY}
|
||||||
|
fill="#10B981"
|
||||||
|
fillOpacity="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Funnel zone (below min) */}
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M ${padding} ${minY}
|
||||||
|
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||||
|
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||||
|
L ${width - padding} ${minY}
|
||||||
|
Z
|
||||||
|
`}
|
||||||
|
fill="#F43F5E"
|
||||||
|
fillOpacity="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Funnel outline */}
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M ${padding} ${padding}
|
||||||
|
L ${padding} ${minY}
|
||||||
|
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||||
|
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||||
|
L ${width - padding} ${minY}
|
||||||
|
L ${width - padding} ${padding}
|
||||||
|
`}
|
||||||
|
fill="none"
|
||||||
|
stroke="#475569"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Fill (current balance) */}
|
||||||
|
<path
|
||||||
|
d={getFillPath()}
|
||||||
|
fill={`url(#fill-gradient-${name})`}
|
||||||
|
filter="url(#glow)"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.8;1;0.8"
|
||||||
|
dur="2s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
{/* Animated inflow particles */}
|
||||||
|
{inflowRate > 0 && (
|
||||||
|
<>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<circle
|
||||||
|
key={`inflow-${i}`}
|
||||||
|
r="4"
|
||||||
|
fill="#3B82F6"
|
||||||
|
opacity="0.8"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="cy"
|
||||||
|
values={`-10;${balanceY}`}
|
||||||
|
dur={`${1 + i * 0.2}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${i * 0.2}s`}
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="cx"
|
||||||
|
values={`${width / 2 - 20 + i * 10};${width / 2 - 10 + i * 5}`}
|
||||||
|
dur={`${1 + i * 0.2}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${i * 0.2}s`}
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.8;0.8;0"
|
||||||
|
dur={`${1 + i * 0.2}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${i * 0.2}s`}
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Animated outflow particles */}
|
||||||
|
{outflowRate > 0 && balance > 0 && (
|
||||||
|
<>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<circle
|
||||||
|
key={`outflow-${i}`}
|
||||||
|
r="3"
|
||||||
|
fill="#EC4899"
|
||||||
|
opacity="0.8"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="cy"
|
||||||
|
values={`${height - padding};${height + 30}`}
|
||||||
|
dur={`${0.8 + i * 0.15}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${i * 0.25}s`}
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="cx"
|
||||||
|
values={`${width / 2 - 10 + i * 10};${width / 2 - 15 + i * 15}`}
|
||||||
|
dur={`${0.8 + i * 0.15}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${i * 0.25}s`}
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.8;0.6;0"
|
||||||
|
dur={`${0.8 + i * 0.15}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${i * 0.25}s`}
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Max threshold line (draggable) */}
|
||||||
|
<g
|
||||||
|
className="cursor-ns-resize"
|
||||||
|
onMouseDown={() => setIsDraggingMax(true)}
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1={0}
|
||||||
|
y1={maxY}
|
||||||
|
x2={width}
|
||||||
|
y2={maxY}
|
||||||
|
stroke="#F59E0B"
|
||||||
|
strokeWidth={isDraggingMax ? 4 : 2}
|
||||||
|
strokeDasharray="8 4"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={width - 8}
|
||||||
|
y={maxY - 12}
|
||||||
|
width={16}
|
||||||
|
height={24}
|
||||||
|
rx={4}
|
||||||
|
fill="#F59E0B"
|
||||||
|
className="cursor-ns-resize"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={width + 12}
|
||||||
|
y={maxY + 5}
|
||||||
|
fill="#F59E0B"
|
||||||
|
fontSize="12"
|
||||||
|
fontFamily="monospace"
|
||||||
|
>
|
||||||
|
MAX ${maxThreshold.toLocaleString()}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Min threshold line (draggable) */}
|
||||||
|
<g
|
||||||
|
className="cursor-ns-resize"
|
||||||
|
onMouseDown={() => setIsDraggingMin(true)}
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1={padding}
|
||||||
|
y1={minY}
|
||||||
|
x2={width - padding}
|
||||||
|
y2={minY}
|
||||||
|
stroke="#F43F5E"
|
||||||
|
strokeWidth={isDraggingMin ? 4 : 2}
|
||||||
|
strokeDasharray="8 4"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={width - 8}
|
||||||
|
y={minY - 12}
|
||||||
|
width={16}
|
||||||
|
height={24}
|
||||||
|
rx={4}
|
||||||
|
fill="#F43F5E"
|
||||||
|
className="cursor-ns-resize"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={width + 12}
|
||||||
|
y={minY + 5}
|
||||||
|
fill="#F43F5E"
|
||||||
|
fontSize="12"
|
||||||
|
fontFamily="monospace"
|
||||||
|
>
|
||||||
|
MIN ${minThreshold.toLocaleString()}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Current balance indicator */}
|
||||||
|
<g>
|
||||||
|
<line
|
||||||
|
x1={0}
|
||||||
|
y1={balanceY}
|
||||||
|
x2={padding - 2}
|
||||||
|
y2={balanceY}
|
||||||
|
stroke={status.color}
|
||||||
|
strokeWidth={3}
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points={`${padding - 2},${balanceY - 6} ${padding - 2},${balanceY + 6} ${padding + 6},${balanceY}`}
|
||||||
|
fill={status.color}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Zone labels */}
|
||||||
|
<div
|
||||||
|
className="absolute text-xs text-amber-400/60 font-medium"
|
||||||
|
style={{ right: 8, top: maxY / 2 + 20 }}
|
||||||
|
>
|
||||||
|
OVERFLOW
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute text-xs text-emerald-400/60 font-medium"
|
||||||
|
style={{ right: 8, top: (maxY + minY) / 2 + 20 }}
|
||||||
|
>
|
||||||
|
HEALTHY
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute text-xs text-rose-400/60 font-medium"
|
||||||
|
style={{ right: 8, top: (minY + height) / 2 + 10 }}
|
||||||
|
>
|
||||||
|
CRITICAL
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Panel */}
|
||||||
|
<div className="flex flex-col gap-4 min-w-[200px]">
|
||||||
|
{/* Current Balance */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||||
|
<div className="text-sm text-slate-400 mb-1">Current Balance</div>
|
||||||
|
<div className="text-3xl font-bold font-mono" style={{ color: status.color }}>
|
||||||
|
${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-sm mt-2 px-2 py-1 rounded-full inline-block"
|
||||||
|
style={{ backgroundColor: `${status.color}20`, color: status.color }}
|
||||||
|
>
|
||||||
|
{status.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flow Rates */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||||
|
<div className="text-sm text-slate-400 mb-3">Flow Rates</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||||
|
<span className="text-slate-300">Inflow</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-blue-400">+${inflowRate}/hr</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-pink-500" />
|
||||||
|
<span className="text-slate-300">Outflow</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-pink-400">-${outflowRate}/hr</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-600 pt-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-slate-300">Net Flow</span>
|
||||||
|
<span
|
||||||
|
className={`font-mono font-bold ${
|
||||||
|
inflowRate - outflowRate >= 0 ? 'text-emerald-400' : 'text-rose-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{inflowRate - outflowRate >= 0 ? '+' : ''}${inflowRate - outflowRate}/hr
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thresholds */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||||
|
<div className="text-sm text-slate-400 mb-3">Thresholds</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between text-sm mb-1">
|
||||||
|
<span className="text-rose-400">Minimum</span>
|
||||||
|
<span className="font-mono text-rose-400">${minThreshold.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Drag the red line to adjust
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between text-sm mb-1">
|
||||||
|
<span className="text-amber-400">Maximum</span>
|
||||||
|
<span className="font-mono text-amber-400">${maxThreshold.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Drag the yellow line to adjust
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress to thresholds */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||||
|
<div className="text-sm text-slate-400 mb-3">Progress</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-slate-400">To Minimum</span>
|
||||||
|
<span className="text-slate-400">
|
||||||
|
{Math.min(100, (balance / minThreshold) * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-rose-500 transition-all duration-300"
|
||||||
|
style={{ width: `${Math.min(100, (balance / minThreshold) * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-slate-400">To Maximum</span>
|
||||||
|
<span className="text-slate-400">
|
||||||
|
{Math.min(100, (balance / maxThreshold) * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-amber-500 transition-all duration-300"
|
||||||
|
style={{ width: `${Math.min(100, (balance / maxThreshold) * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue