From 4b0491e99996878fa1c69ee7f5183b45728d3343 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 29 Jan 2026 18:05:58 +0000 Subject: [PATCH] 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 --- DESIGN.md | 398 +++++++++++------------- app/page.tsx | 151 +++++++-- components/FundingFunnel.tsx | 576 +++++++++++++++++++++++++++++++++++ 3 files changed, 887 insertions(+), 238 deletions(-) create mode 100644 components/FundingFunnel.tsx diff --git a/DESIGN.md b/DESIGN.md index e027643..0559f8a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,268 +2,232 @@ ## 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 -- **Sources** = Springs/tributaries (funding origins) -- **Flows** = Rivers/streams (animated fund movement) -- **Recipients** = Lakes/deltas (fund destinations) -- **Thresholds** = Dams/locks (control points) +The funding funnel has three distinct zones defined by minimum and maximum thresholds: -### 2. The Circulatory System -- **Sources** = Heart (pumping resources) -- **Flows** = Arteries/veins (distribution network) -- **Recipients** = Organs (entities needing resources) -- **Thresholds** = Valves (flow regulation) +``` + ════════════════════════ + │ │ OVERFLOW ZONE + │ Excess funds │ (above MAX) + │ 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 -- **Sources** = Power plants (generation) -- **Flows** = Transmission lines (distribution) -- **Recipients** = Consumers (utilization) -- **Thresholds** = Circuit breakers (protection/regulation) +### Zone Behaviors + +| Zone | Shape | Behavior | +|------|-------|----------| +| **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)** ``` -┌─────────────────────────────────────────────────────────┐ -│ 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% │ │ -│ └─────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ +════════════╳════════════ ← Draggable handle + MIN $20,000 ``` -### Threshold Visualization - +**Maximum Threshold (Amber/Yellow)** ``` -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 +════════════╳════════════ ← Draggable handle + MAX $80,000 ``` -### Flow Animation Styles +### Status Panel ``` -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 +┌─────────────────────────────┐ +│ Current Balance │ +│ ████████████████░░░░░░░░ │ +│ $45,000 │ +│ ● Healthy Range │ +├─────────────────────────────┤ +│ Flow Rates │ +│ ↓ Inflow: +$500/hr │ +│ ↑ Outflow: -$300/hr │ +│ ═ Net: +$200/hr │ +├─────────────────────────────┤ +│ Progress │ +│ To MIN: ████████░░ 80% │ +│ To MAX: ██████░░░░ 60% │ +└─────────────────────────────┘ ``` --- -## Interactive Features +## Interaction Design -### 1. Threshold Slider -Users can drag thresholds to see how changes affect flow dynamics: +### Dragging Thresholds + +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 ``` -┌────────────────────────────────────────────────────────┐ -│ THRESHOLD CONTROL │ -│ │ -│ Min ├────────────●────────────────────────────┤ Max │ -│ $0 $5,000 $50,000 │ -│ ▲ │ -│ │ │ -│ Current: $5,000 │ -│ │ -│ Flow activates when source balance > threshold │ -└────────────────────────────────────────────────────────┘ +Before drag: During drag: After release: +════════════ ════════════════ ════════════ + │ ║ (thicker) │ + │ ║ │ + │ New value shown │ ``` -### 2. Flow Rate Dial -Adjust how fast funds flow when threshold is met: +### Responsive Behaviors -``` - ╭─────────────╮ - ╱ │ ╲ - │ ────●──── │ - │ ╱ ╲ │ - ╲ ╱ │ ╲ ╱ - ╰──────┴──────╯ - $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 │ -│ │ -└─────────────────────────────────────┘ -``` +| 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 | --- ## 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 | +| Element | Color | Hex | Usage | +|---------|-------|-----|-------| +| Inflow | Blue | `#3B82F6` | Incoming fund particles | +| Outflow | Pink | `#EC4899` | Outgoing fund particles | +| Fill Gradient | Blue→Purple→Pink | gradient | Liquid level | +| Critical Zone | Rose | `#F43F5E` | Below minimum | +| Healthy Zone | Emerald | `#10B981` | Normal range | +| Overflow Zone | Amber | `#F59E0B` | Above maximum | +| Background | Slate | `#0F172A` | Canvas | --- -## Interaction Patterns +## Multi-Funnel View -### 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 +When displaying multiple funding pools: -### 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 +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Threshold-Based Flow Funding │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ╔═══════╗ ╔═══════╗ ╔═══════╗ │ +│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │ +│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │ +│ ║░░░░░░░║ ║▓▓▓▓▓▓▓║ ║███████║ │ +│ ║░░░░░░░║ ╚═══╤═══╝ ╚═══╤═══╝ │ +│ ╚═══╤═══╝ │ │ │ +│ │ │ │ │ +│ Public Goods Research Emergency │ +│ $12K / $30K $45K / $60K $85K / $70K │ +│ ● Critical ● Healthy ● Overflow │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` -### 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 +### Interconnected Flows + +Future enhancement: Connect funnels so overflow from one feeds into another: + +``` + Fund A Fund B + ╔═════╗ ╔═════╗ + ║█████║ ═══overflow═══▶ ║░░░░░║ + ║█████║ ║░░░░░║ + ╚══╤══╝ ╚══╤══╝ +``` --- ## 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 +### Component Architecture -### Animation System -- Canvas-based particle system for flow animation -- 60fps smooth animations -- 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 +└── + ├── Current balance display + ├── Flow rates + └── Progress bars +``` ### State Management -- Real-time flow calculations -- Threshold state machine -- Time-series data for history/projections + +```typescript +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 diff --git a/app/page.tsx b/app/page.tsx index 9182b15..1754314 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,45 +1,154 @@ 'use client' +import { useState } from 'react' import dynamic from 'next/dynamic' -// Dynamic import to avoid SSR issues with React Flow -const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), { +const FundingFunnel = dynamic(() => import('@/components/FundingFunnel'), { ssr: false, loading: () => ( -
-
Loading flow visualization...
+
+
Loading visualization...
), }) export default function Home() { + const [view, setView] = useState<'single' | 'multi'>('single') + return ( -
+
{/* Header */} -
-
-
-

- Flow Funding +
+
+
+

+ Threshold-Based Flow Funding

- - TBFF Demo - +

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

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

How It Works

+
    +
  • + 1. + + Minimum Threshold — Below this level, + the funnel narrows, restricting outflow. Funds are conserved until the minimum is reached. + +
  • +
  • + 2. + + Healthy Range — Between min and max, + the funnel has straight walls. Normal operations with balanced in/out flows. + +
  • +
  • + 3. + + Maximum Threshold — Above this level, + excess funds overflow and can be redistributed to other pools or purposes. + +
  • +
  • + + + Drag the threshold lines to adjust + minimum and maximum levels interactively. + +
  • +
+

) diff --git a/components/FundingFunnel.tsx b/components/FundingFunnel.tsx new file mode 100644 index 0000000..a5e3133 --- /dev/null +++ b/components/FundingFunnel.tsx @@ -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(null) + + // Funnel dimensions + const width = 300 + const height = 500 + const funnelTopWidth = 280 + const funnelNarrowWidth = 80 + const padding = 10 + + // Calculate Y positions for thresholds (inverted - 0 is at bottom) + const minY = height - (minThreshold / maxCapacity) * height + const maxY = height - (maxThreshold / maxCapacity) * height + const balanceY = height - (balance / maxCapacity) * height + + // Funnel shape points + const funnelPath = ` + M ${padding} ${maxY} + L ${padding} ${minY} + L ${(width - funnelNarrowWidth) / 2} ${height - padding} + L ${(width + funnelNarrowWidth) / 2} ${height - padding} + L ${width - padding} ${minY} + L ${width - padding} ${maxY} + L ${padding} ${maxY} + ` + + // Overflow zone (above max) + const overflowPath = ` + M ${padding} ${padding} + L ${padding} ${maxY} + L ${width - padding} ${maxY} + L ${width - padding} ${padding} + Z + ` + + // Calculate fill path based on current balance + const getFillPath = () => { + if (balance <= 0) return '' + + const fillY = Math.max(balanceY, padding) + + if (balance >= maxThreshold) { + // Overflow zone - straight walls above max + const overflowY = Math.max(fillY, padding) + return ` + M ${padding} ${minY} + L ${(width - funnelNarrowWidth) / 2} ${height - padding} + L ${(width + funnelNarrowWidth) / 2} ${height - padding} + L ${width - padding} ${minY} + L ${width - padding} ${overflowY} + L ${padding} ${overflowY} + Z + ` + } else if (balance >= minThreshold) { + // Between min and max - straight walls + return ` + M ${padding} ${minY} + L ${(width - funnelNarrowWidth) / 2} ${height - padding} + L ${(width + funnelNarrowWidth) / 2} ${height - padding} + L ${width - padding} ${minY} + L ${width - padding} ${fillY} + L ${padding} ${fillY} + Z + ` + } else { + // Below min - in the funnel narrowing section + const ratio = balance / minThreshold + const bottomWidth = funnelNarrowWidth + const topWidth = funnelTopWidth - 2 * padding + const currentWidth = bottomWidth + (topWidth - bottomWidth) * ratio + const leftX = (width - currentWidth) / 2 + const rightX = (width + currentWidth) / 2 + + return ` + M ${(width - funnelNarrowWidth) / 2} ${height - padding} + L ${(width + funnelNarrowWidth) / 2} ${height - padding} + L ${rightX} ${fillY} + L ${leftX} ${fillY} + Z + ` + } + } + + // Simulate balance changes + useEffect(() => { + const interval = setInterval(() => { + setBalance((prev) => { + const netFlow = (inflowRate - outflowRate) / 3600 // per second + const newBalance = prev + netFlow + return Math.max(0, Math.min(maxCapacity * 1.2, newBalance)) + }) + }, 100) + return () => clearInterval(interval) + }, [inflowRate, outflowRate, maxCapacity]) + + // Handle threshold dragging + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (!containerRef.current) return + const rect = containerRef.current.getBoundingClientRect() + const y = e.clientY - rect.top + const value = Math.max(0, Math.min(maxCapacity, ((height - y) / height) * maxCapacity)) + + if (isDraggingMin) { + const newMin = Math.min(value, maxThreshold - 1000) + setMinThreshold(Math.max(0, newMin)) + onMinThresholdChange?.(Math.max(0, newMin)) + } else if (isDraggingMax) { + const newMax = Math.max(value, minThreshold + 1000) + setMaxThreshold(Math.min(maxCapacity, newMax)) + onMaxThresholdChange?.(Math.min(maxCapacity, newMax)) + } + }, + [isDraggingMin, isDraggingMax, maxThreshold, minThreshold, maxCapacity, onMinThresholdChange, onMaxThresholdChange] + ) + + const handleMouseUp = useCallback(() => { + setIsDraggingMin(false) + setIsDraggingMax(false) + }, []) + + useEffect(() => { + if (isDraggingMin || isDraggingMax) { + window.addEventListener('mousemove', handleMouseMove) + window.addEventListener('mouseup', handleMouseUp) + return () => { + window.removeEventListener('mousemove', handleMouseMove) + window.removeEventListener('mouseup', handleMouseUp) + } + } + }, [isDraggingMin, isDraggingMax, handleMouseMove, handleMouseUp]) + + // Determine zone status + const getZoneStatus = () => { + if (balance < minThreshold) return { zone: 'critical', color: '#F43F5E', label: 'Below Minimum' } + if (balance > maxThreshold) return { zone: 'overflow', color: '#F59E0B', label: 'Overflow' } + return { zone: 'healthy', color: '#10B981', label: 'Healthy Range' } + } + + const status = getZoneStatus() + + return ( +
+

{name}

+ +
+ {/* Main Funnel Visualization */} +
+ + + {/* Gradient for the fill */} + + + + + + + {/* Glow filter */} + + + + + + + + + {/* Wave pattern for liquid effect */} + + + + + + {/* Background zones */} + {/* Overflow zone (above max) */} + + + {/* Healthy zone (between min and max) - straight walls */} + + + {/* Funnel zone (below min) */} + + + {/* Funnel outline */} + + + {/* Fill (current balance) */} + + + + + {/* Animated inflow particles */} + {inflowRate > 0 && ( + <> + {[...Array(5)].map((_, i) => ( + + + + + + ))} + + )} + + {/* Animated outflow particles */} + {outflowRate > 0 && balance > 0 && ( + <> + {[...Array(3)].map((_, i) => ( + + + + + + ))} + + )} + + {/* Max threshold line (draggable) */} + setIsDraggingMax(true)} + > + + + + MAX ${maxThreshold.toLocaleString()} + + + + {/* Min threshold line (draggable) */} + setIsDraggingMin(true)} + > + + + + MIN ${minThreshold.toLocaleString()} + + + + {/* Current balance indicator */} + + + + + + + {/* Zone labels */} +
+ OVERFLOW +
+
+ HEALTHY +
+
+ CRITICAL +
+
+ + {/* Stats Panel */} +
+ {/* Current Balance */} +
+
Current Balance
+
+ ${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })} +
+
+ {status.label} +
+
+ + {/* Flow Rates */} +
+
Flow Rates
+ +
+
+
+
+ Inflow +
+ +${inflowRate}/hr +
+ +
+
+
+ Outflow +
+ -${outflowRate}/hr +
+ +
+
+ Net Flow + = 0 ? 'text-emerald-400' : 'text-rose-400' + }`} + > + {inflowRate - outflowRate >= 0 ? '+' : ''}${inflowRate - outflowRate}/hr + +
+
+
+
+ + {/* Thresholds */} +
+
Thresholds
+ +
+
+
+ Minimum + ${minThreshold.toLocaleString()} +
+
+ Drag the red line to adjust +
+
+ +
+
+ Maximum + ${maxThreshold.toLocaleString()} +
+
+ Drag the yellow line to adjust +
+
+
+
+ + {/* Progress to thresholds */} +
+
Progress
+ +
+
+
+ To Minimum + + {Math.min(100, (balance / minThreshold) * 100).toFixed(0)}% + +
+
+
+
+
+ +
+
+ To Maximum + + {Math.min(100, (balance / maxThreshold) * 100).toFixed(0)}% + +
+
+
+
+
+
+
+
+
+
+ ) +}