From a7213aa0d478b9c955926d244b9766e6828a70b8 Mon Sep 17 00:00:00 2001
From: Shawn Anderson
Date: Mon, 24 Nov 2025 16:05:47 -0800
Subject: [PATCH] Demo updates.
---
.claude/DEMOS_DASHBOARD.md | 276 ++++++++++
app/demos/page.tsx | 365 +++++++++++++
app/layout.tsx | 8 +
app/tbff-flow/page.tsx | 671 ++++++++++++++++++++++++
components/hero-section.tsx | 14 +-
lib/tbff-flow/README.md | 404 +++++++++++++++
lib/tbff-flow/algorithms.ts | 267 ++++++++++
lib/tbff-flow/rendering.ts | 319 ++++++++++++
lib/tbff-flow/sample-networks.ts | 329 ++++++++++++
lib/tbff-flow/types.ts | 90 ++++
lib/tbff-flow/utils.ts | 176 +++++++
package.json | 4 +-
pnpm-lock.yaml | 804 ++++++++++++++++++++++++++++-
public/screenshots/flow-v2.png | Bin 0 -> 86581 bytes
public/screenshots/flowfunding.png | Bin 0 -> 262128 bytes
public/screenshots/italism.png | Bin 0 -> 61922 bytes
public/screenshots/tbff-flow.png | Bin 0 -> 70720 bytes
public/screenshots/tbff.png | Bin 0 -> 80836 bytes
scripts/README.md | 138 +++++
scripts/capture-screenshots.mjs | 78 +++
20 files changed, 3935 insertions(+), 8 deletions(-)
create mode 100644 .claude/DEMOS_DASHBOARD.md
create mode 100644 app/demos/page.tsx
create mode 100644 app/tbff-flow/page.tsx
create mode 100644 lib/tbff-flow/README.md
create mode 100644 lib/tbff-flow/algorithms.ts
create mode 100644 lib/tbff-flow/rendering.ts
create mode 100644 lib/tbff-flow/sample-networks.ts
create mode 100644 lib/tbff-flow/types.ts
create mode 100644 lib/tbff-flow/utils.ts
create mode 100644 public/screenshots/flow-v2.png
create mode 100644 public/screenshots/flowfunding.png
create mode 100644 public/screenshots/italism.png
create mode 100644 public/screenshots/tbff-flow.png
create mode 100644 public/screenshots/tbff.png
create mode 100644 scripts/README.md
create mode 100755 scripts/capture-screenshots.mjs
diff --git a/.claude/DEMOS_DASHBOARD.md b/.claude/DEMOS_DASHBOARD.md
new file mode 100644
index 0000000..cc4e87b
--- /dev/null
+++ b/.claude/DEMOS_DASHBOARD.md
@@ -0,0 +1,276 @@
+# Flow Funding Demos Dashboard
+
+**Created**: 2025-11-23
+**Location**: `/app/demos/page.tsx`
+**Live URL**: `http://localhost:3000/demos`
+
+---
+
+## Overview
+
+A beautiful, comprehensive dashboard showcasing all Flow Funding interactive demonstrations. Inspired by the infinite-agents demo gallery design pattern.
+
+## Features
+
+### Visual Design
+- **Gradient Background**: Purple-to-blue gradient matching the project's aesthetic
+- **Card Layout**: Modern card-based design with hover effects
+- **Status Badges**: Color-coded status indicators (Complete, Beta, Prototype)
+- **Stats Dashboard**: Quick overview of total demos, completion status
+- **Responsive**: Mobile-first design with grid layouts
+
+### Functionality
+- **Search**: Real-time search across demo names, descriptions, types, and features
+- **Category Filters**: Quick filtering by demo category
+- **Feature Tags**: Quick-glance feature highlights for each demo
+- **Direct Links**: One-click access to each demo
+
+### Categories
+
+#### 1. **Threshold-Based Flow Funding (TBFF)** π―
+- **TBFF Interactive** (Milestones 1-3 Complete)
+ - Static visualization with color-coding
+ - Interactive allocation creation
+ - Initial distribution algorithm
+ - Multiple sample networks
+
+- **TBFF Flow Simulation** (Beta)
+ - Continuous flow mechanics
+ - Progressive outflow
+
+#### 2. **Flow Dynamics V2** π
+- **Continuous Flow Dynamics** (Complete)
+ - Per-second simulation engine
+ - Progressive outflow formula (fixed)
+ - Network overflow node
+ - 60 FPS rendering
+ - Animated flow particles
+
+#### 3. **Interactive Canvas** π¨
+- **Italism** (Complete)
+ - Live arrow propagators
+ - Shape drawing and editing
+ - Expression-based connections
+ - Undo/redo functionality
+ - FolkJS-inspired architecture
+
+#### 4. **Prototypes** π¬
+- **Flow Funding (Original)** (Prototype)
+ - Basic flow mechanics
+ - Early concept exploration
+
+## Integration Points
+
+### Main Landing Page
+- Updated hero section with "View Interactive Demos" button
+- Primary CTA now links to `/demos`
+
+### Demo Pages
+- Each demo page can link back to dashboard
+- Consistent navigation experience
+
+## Technical Details
+
+### Component Structure
+```tsx
+DemosPage (Client Component)
+βββ Header with title and description
+βββ Statistics cards (Total, Complete, Beta, Categories)
+βββ Search and filter controls
+βββ Category sections
+ βββ Demo cards with:
+ βββ Status badge
+ βββ Title and description
+ βββ Feature tags
+ βββ Type label
+ βββ Launch link
+```
+
+### Data Model
+```typescript
+interface Demo {
+ number: number
+ title: string
+ description: string
+ path: string
+ type: string
+ status: 'complete' | 'beta' | 'prototype'
+ features: string[]
+ milestone?: string
+}
+```
+
+### State Management
+- Local React state for search and filters
+- No external dependencies
+- Client-side filtering for performance
+
+## Design Patterns
+
+### Inspired by infinite-agents
+- Category-based organization
+- Stats bar at the top
+- Search and filter controls
+- Card-based demo display
+- Hover effects and transitions
+- Status badges
+
+### Improvements Over Original
+- React/Next.js instead of vanilla JS
+- Type-safe with TypeScript
+- Responsive Tailwind CSS
+- Status badges (Complete/Beta/Prototype)
+- Feature tags for each demo
+- Milestone tracking
+
+## Future Enhancements
+
+### Short-term
+- [ ] Add screenshots for each demo
+- [ ] Implement screenshot preview on hover
+- [ ] Add "New" badge for recently added demos
+- [ ] Add demo tags (e.g., "Interactive", "Simulation", "Educational")
+
+### Medium-term
+- [ ] Add demo ratings/feedback
+- [ ] Implement demo bookmarking
+- [ ] Add video previews/tours
+- [ ] Create guided learning paths
+- [ ] Add "What's New" section
+
+### Long-term
+- [ ] User accounts and personalization
+- [ ] Demo creation wizard
+- [ ] Community contributions
+- [ ] Analytics and usage tracking
+- [ ] A/B testing for different presentations
+
+## Usage
+
+### Accessing the Dashboard
+1. Navigate to `http://localhost:3000/demos`
+2. Or click "View Interactive Demos" from the home page
+
+### Searching Demos
+- Type in the search box to filter by:
+ - Demo name
+ - Description text
+ - Type
+ - Feature names
+
+### Filtering by Category
+- Click any category button to show only that category
+- Click "All Demos" to reset
+
+### Launching a Demo
+- Click anywhere on a demo card
+- Or click the "Launch Demo β" link
+
+## Maintenance
+
+### Adding New Demos
+Edit `/app/demos/page.tsx`:
+
+```typescript
+const demos: Record = {
+ categoryName: [
+ {
+ number: X,
+ title: 'Demo Title',
+ description: 'Demo description...',
+ path: '/demo-route',
+ type: 'Demo Type',
+ status: 'complete' | 'beta' | 'prototype',
+ features: ['Feature 1', 'Feature 2', ...],
+ milestone: 'Optional milestone note'
+ }
+ ]
+}
+```
+
+### Updating Categories
+Add to the `categories` array:
+
+```typescript
+{
+ id: 'categoryKey',
+ label: 'Display Name',
+ count: demos.categoryKey.length,
+ icon: 'π¬'
+}
+```
+
+## Performance Considerations
+
+### Current Performance
+- Minimal bundle size (no heavy dependencies)
+- Client-side rendering with static demo data
+- Fast search (no API calls)
+- Instant category filtering
+
+### Optimization Opportunities
+- Lazy load demo screenshots
+- Virtual scrolling for many demos
+- Server-side rendering for initial load
+- Static generation at build time
+
+## Accessibility
+
+### Current Features
+- Semantic HTML structure
+- Keyboard navigation support
+- Focus states on interactive elements
+- High contrast color schemes
+
+### Future Improvements
+- ARIA labels for all interactive elements
+- Screen reader optimizations
+- Keyboard shortcuts
+- Focus management
+
+## Design Philosophy
+
+### Post-Appitalism Alignment
+- **Transparent**: All demos visible and accessible
+- **Exploratory**: Encourages browsing and discovery
+- **Educational**: Descriptions explain what each demo teaches
+- **Beautiful**: Aesthetically compelling design
+- **Functional**: No unnecessary complexity
+
+### User Experience Goals
+1. **Immediate Value**: See all demos at a glance
+2. **Easy Discovery**: Search and filter make finding demos trivial
+3. **Clear Status**: Know which demos are production-ready
+4. **Feature Visibility**: Understand what each demo offers
+5. **Quick Access**: One click to launch any demo
+
+---
+
+## Metrics for Success
+
+### User Engagement
+- Time spent on dashboard
+- Demos launched from dashboard
+- Search usage patterns
+- Filter usage patterns
+
+### Content Quality
+- Complete demos ratio
+- Feature completeness
+- Description clarity
+- User feedback scores
+
+### Technical Performance
+- Page load time < 2s
+- Search response < 100ms
+- Filter transition < 300ms
+- Mobile responsiveness scores
+
+---
+
+**Status**: β
Complete and deployed to development
+**Next Steps**: Add screenshots, test with users, gather feedback
+
+---
+
+*"Make the abstract concrete. Make the complex simple. Make it beautiful."*
diff --git a/app/demos/page.tsx b/app/demos/page.tsx
new file mode 100644
index 0000000..4d4fcc6
--- /dev/null
+++ b/app/demos/page.tsx
@@ -0,0 +1,365 @@
+'use client'
+
+import { useState } from 'react'
+
+interface Demo {
+ number: number
+ title: string
+ description: string
+ path: string
+ type: string
+ status: 'complete' | 'beta' | 'prototype'
+ features: string[]
+ milestone?: string
+ screenshot?: string
+}
+
+const demos: Record = {
+ tbff: [
+ {
+ number: 1,
+ title: 'Threshold-Based Flow Funding (Interactive)',
+ description: 'Complete interactive demo with Milestones 1-3: Static visualization, interactive allocations, and initial distribution algorithm. Create accounts, draw allocation arrows, add funding, and watch resources flow.',
+ path: '/tbff',
+ type: 'Interactive Simulation',
+ status: 'complete',
+ milestone: 'Milestones 1-3 Complete',
+ screenshot: '/screenshots/tbff.png',
+ features: [
+ 'Visual threshold-based coloring',
+ 'Interactive allocation creation',
+ 'Automatic normalization',
+ 'Initial distribution algorithm',
+ 'Multiple sample networks',
+ 'Real-time balance updates'
+ ]
+ },
+ {
+ number: 2,
+ title: 'TBFF Flow Simulation',
+ description: 'Alternative implementation exploring continuous flow dynamics with progressive outflow ratios.',
+ path: '/tbff-flow',
+ type: 'Flow Simulation',
+ status: 'beta',
+ screenshot: '/screenshots/tbff-flow.png',
+ features: [
+ 'Continuous flow mechanics',
+ 'Progressive outflow',
+ 'Network equilibrium',
+ 'Visual flow indicators'
+ ]
+ }
+ ],
+ flowV2: [
+ {
+ number: 3,
+ title: 'Flow Funding V2: Continuous Flow Dynamics',
+ description: 'Redesigned as continuous per-second flow simulation with per-month UI. Features progressive outflow formula ensuring monotonic increase in sharing as accounts approach "enough".',
+ path: '/flow-v2',
+ type: 'Continuous Flow',
+ status: 'complete',
+ screenshot: '/screenshots/flow-v2.png',
+ features: [
+ 'Per-second simulation engine',
+ 'Progressive outflow formula (fixed)',
+ 'Network overflow node',
+ 'Smooth 60 FPS rendering',
+ 'Animated flow particles',
+ 'Time-scale architecture'
+ ]
+ }
+ ],
+ canvas: [
+ {
+ number: 4,
+ title: 'Italism: Interactive Canvas with Propagators',
+ description: 'Original canvas demo with live propagators. Draw shapes, connect them with arrows, and watch data flow through the network. Foundation for malleable software vision.',
+ path: '/italism',
+ type: 'Live Programming Canvas',
+ status: 'complete',
+ screenshot: '/screenshots/italism.png',
+ features: [
+ 'Live arrow propagators',
+ 'Shape drawing and editing',
+ 'Expression-based connections',
+ 'Undo/redo functionality',
+ 'Real-time data flow',
+ 'FolkJS-inspired architecture'
+ ]
+ }
+ ],
+ prototypes: [
+ {
+ number: 5,
+ title: 'Flow Funding (Original)',
+ description: 'Earlier prototype exploring initial flow funding concepts.',
+ path: '/flowfunding',
+ type: 'Prototype',
+ status: 'prototype',
+ screenshot: '/screenshots/flowfunding.png',
+ features: [
+ 'Basic flow mechanics',
+ 'Threshold visualization',
+ 'Network simulation'
+ ]
+ }
+ ]
+}
+
+export default function DemosPage() {
+ const [searchTerm, setSearchTerm] = useState('')
+ const [activeFilter, setActiveFilter] = useState('all')
+
+ const allDemos = Object.values(demos).flat()
+ const totalDemos = allDemos.length
+ const completeDemos = allDemos.filter(d => d.status === 'complete').length
+ const betaDemos = allDemos.filter(d => d.status === 'beta').length
+
+ const categories = [
+ { id: 'all', label: 'All Demos', count: totalDemos },
+ { id: 'tbff', label: 'TBFF Interactive', count: demos.tbff.length, icon: 'π―' },
+ { id: 'flowV2', label: 'Flow Dynamics V2', count: demos.flowV2.length, icon: 'π' },
+ { id: 'canvas', label: 'Interactive Canvas', count: demos.canvas.length, icon: 'π¨' },
+ { id: 'prototypes', label: 'Prototypes', count: demos.prototypes.length, icon: 'π¬' }
+ ]
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'complete': return 'bg-green-500'
+ case 'beta': return 'bg-yellow-500'
+ case 'prototype': return 'bg-gray-500'
+ default: return 'bg-gray-400'
+ }
+ }
+
+ const getStatusLabel = (status: string) => {
+ switch (status) {
+ case 'complete': return 'Complete'
+ case 'beta': return 'Beta'
+ case 'prototype': return 'Prototype'
+ default: return status
+ }
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ {/* Stats */}
+
+
+
+ {totalDemos}
+
+
Total Demos
+
+
+
+ {completeDemos}
+
+
Complete
+
+
+
+ {betaDemos}
+
+
Beta
+
+
+
+
+ {/* Search & Filter */}
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {categories.map(cat => (
+
+ ))}
+
+
+
+ {/* Demo Categories */}
+ {Object.entries(demos).map(([categoryKey, categoryDemos]) => {
+ if (activeFilter !== 'all' && activeFilter !== categoryKey) return null
+
+ const categoryInfo = {
+ tbff: { title: 'Threshold-Based Flow Funding', icon: 'π―', desc: 'Interactive demos with allocation creation and distribution algorithms' },
+ flowV2: { title: 'Flow Dynamics V2', icon: 'π', desc: 'Continuous per-second flow simulation with progressive outflow' },
+ canvas: { title: 'Interactive Canvas', icon: 'π¨', desc: 'Live programming environment with propagator networks' },
+ prototypes: { title: 'Early Prototypes', icon: 'π¬', desc: 'Initial explorations and concept validation' }
+ }[categoryKey] || { title: categoryKey, icon: 'π', desc: '' }
+
+ return (
+
+
+
+
+ {categoryInfo.icon}
+
+
+
{categoryInfo.title}
+
{categoryInfo.desc}
+
+
+ {categoryDemos.length} {categoryDemos.length === 1 ? 'demo' : 'demos'}
+
+
+
+
+
+
+ )
+ })}
+
+ {/* Footer */}
+
+
+
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index d2d3792..e6063f5 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -11,6 +11,14 @@ export const metadata: Metadata = {
title: "Project Interlay | Post-Appitalism",
description: "Weaving a post-appitalist future. Decomposing the data silos of capitalist business models.",
generator: "v0.app",
+ icons: {
+ icon: [
+ {
+ url: "data:image/svg+xml,",
+ type: "image/svg+xml",
+ },
+ ],
+ },
}
export default function RootLayout({
diff --git a/app/tbff-flow/page.tsx b/app/tbff-flow/page.tsx
new file mode 100644
index 0000000..665af20
--- /dev/null
+++ b/app/tbff-flow/page.tsx
@@ -0,0 +1,671 @@
+"use client"
+
+import { useEffect, useRef, useState } from "react"
+import Link from "next/link"
+import type { FlowNetwork, FlowAllocation, FlowParticle } from "@/lib/tbff-flow/types"
+import { renderFlowNetwork } from "@/lib/tbff-flow/rendering"
+import {
+ flowSampleNetworks,
+ flowNetworkOptions,
+ getFlowSampleNetwork,
+} from "@/lib/tbff-flow/sample-networks"
+import {
+ formatFlow,
+ getFlowStatusColorClass,
+ normalizeFlowAllocations,
+ calculateFlowNetworkTotals,
+ updateFlowNodeProperties,
+} from "@/lib/tbff-flow/utils"
+import { propagateFlow, updateFlowParticles } from "@/lib/tbff-flow/algorithms"
+
+type Tool = 'select' | 'create-allocation'
+
+export default function TBFFFlowPage() {
+ const canvasRef = useRef(null)
+ const animationFrameRef = useRef(null)
+
+ const [network, setNetwork] = useState(flowSampleNetworks.linear)
+ const [particles, setParticles] = useState([])
+ const [selectedNodeId, setSelectedNodeId] = useState(null)
+ const [selectedAllocationId, setSelectedAllocationId] = useState(null)
+ const [selectedNetworkKey, setSelectedNetworkKey] = useState('linear')
+ const [tool, setTool] = useState('select')
+ const [allocationSourceId, setAllocationSourceId] = useState(null)
+ const [isAnimating, setIsAnimating] = useState(true)
+
+ // Animation loop
+ useEffect(() => {
+ if (!isAnimating) return
+
+ const animate = () => {
+ // Update particles
+ setParticles(prev => updateFlowParticles(prev))
+
+ animationFrameRef.current = requestAnimationFrame(animate)
+ }
+
+ animationFrameRef.current = requestAnimationFrame(animate)
+
+ return () => {
+ if (animationFrameRef.current) {
+ cancelAnimationFrame(animationFrameRef.current)
+ }
+ }
+ }, [isAnimating])
+
+ // Render canvas
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext("2d")
+ if (!ctx) return
+
+ // Set canvas size
+ canvas.width = canvas.offsetWidth
+ canvas.height = canvas.offsetHeight
+
+ // Render the network
+ renderFlowNetwork(
+ ctx,
+ network,
+ canvas.width,
+ canvas.height,
+ particles,
+ selectedNodeId,
+ selectedAllocationId
+ )
+ }, [network, particles, selectedNodeId, selectedAllocationId])
+
+ // Propagate flow when network changes
+ const handlePropagateFlow = () => {
+ const result = propagateFlow(network)
+ setNetwork(result.network)
+ setParticles(result.particles)
+ }
+
+ // Set external flow for selected node
+ const handleSetNodeFlow = (nodeId: string, flow: number) => {
+ const updatedNodes = network.nodes.map(node =>
+ node.id === nodeId
+ ? { ...node, externalFlow: Math.max(0, flow) }
+ : node
+ )
+
+ const updatedNetwork = {
+ ...network,
+ nodes: updatedNodes,
+ }
+
+ setNetwork(updatedNetwork)
+
+ // Auto-propagate
+ setTimeout(() => {
+ const result = propagateFlow(updatedNetwork)
+ setNetwork(result.network)
+ setParticles(result.particles)
+ }, 100)
+ }
+
+ // Handle canvas click
+ const handleCanvasClick = (e: React.MouseEvent) => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const rect = canvas.getBoundingClientRect()
+ const x = e.clientX - rect.left
+ const y = e.clientY - rect.top
+
+ // Find clicked node
+ const clickedNode = network.nodes.find(
+ node =>
+ x >= node.x &&
+ x <= node.x + node.width &&
+ y >= node.y &&
+ y <= node.y + node.height
+ )
+
+ if (tool === 'select') {
+ if (clickedNode) {
+ setSelectedNodeId(clickedNode.id)
+ setSelectedAllocationId(null)
+ } else {
+ // Check if clicked on allocation
+ const clickedAllocation = findAllocationAtPoint(x, y)
+ if (clickedAllocation) {
+ setSelectedAllocationId(clickedAllocation.id)
+ setSelectedNodeId(null)
+ } else {
+ // Deselect all
+ setSelectedNodeId(null)
+ setSelectedAllocationId(null)
+ }
+ }
+ } else if (tool === 'create-allocation') {
+ if (clickedNode) {
+ if (!allocationSourceId) {
+ // First click: set source
+ setAllocationSourceId(clickedNode.id)
+ } else {
+ // Second click: create allocation
+ if (clickedNode.id !== allocationSourceId) {
+ createAllocation(allocationSourceId, clickedNode.id)
+ }
+ setAllocationSourceId(null)
+ }
+ }
+ }
+ }
+
+ // Find allocation at point
+ const findAllocationAtPoint = (x: number, y: number): FlowAllocation | null => {
+ const tolerance = 15
+
+ for (const allocation of network.allocations) {
+ const sourceNode = network.nodes.find(n => n.id === allocation.sourceNodeId)
+ const targetNode = network.nodes.find(n => n.id === allocation.targetNodeId)
+ if (!sourceNode || !targetNode) continue
+
+ const sourceCenter = {
+ x: sourceNode.x + sourceNode.width / 2,
+ y: sourceNode.y + sourceNode.height / 2,
+ }
+ const targetCenter = {
+ x: targetNode.x + targetNode.width / 2,
+ y: targetNode.y + targetNode.height / 2,
+ }
+
+ const distance = pointToLineDistance(
+ x, y,
+ sourceCenter.x, sourceCenter.y,
+ targetCenter.x, targetCenter.y
+ )
+
+ if (distance < tolerance) {
+ return allocation
+ }
+ }
+
+ return null
+ }
+
+ // Point to line distance
+ const pointToLineDistance = (
+ px: number, py: number,
+ x1: number, y1: number,
+ x2: number, y2: number
+ ): number => {
+ const dot = (px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)
+ const lenSq = (x2 - x1) ** 2 + (y2 - y1) ** 2
+ const param = lenSq !== 0 ? dot / lenSq : -1
+
+ let xx, yy
+ if (param < 0) {
+ [xx, yy] = [x1, y1]
+ } else if (param > 1) {
+ [xx, yy] = [x2, y2]
+ } else {
+ xx = x1 + param * (x2 - x1)
+ yy = y1 + param * (y2 - y1)
+ }
+
+ return Math.sqrt((px - xx) ** 2 + (py - yy) ** 2)
+ }
+
+ // Create allocation
+ const createAllocation = (sourceId: string, targetId: string) => {
+ const newAllocation: FlowAllocation = {
+ id: `alloc_${Date.now()}`,
+ sourceNodeId: sourceId,
+ targetNodeId: targetId,
+ percentage: 0.5,
+ }
+
+ const updatedAllocations = [...network.allocations, newAllocation]
+ const sourceAllocations = updatedAllocations.filter(a => a.sourceNodeId === sourceId)
+ const normalized = normalizeFlowAllocations(sourceAllocations)
+
+ const finalAllocations = updatedAllocations.map(a => {
+ const normalizedVersion = normalized.find(n => n.id === a.id)
+ return normalizedVersion || a
+ })
+
+ const updatedNetwork = {
+ ...network,
+ allocations: finalAllocations,
+ }
+
+ setNetwork(updatedNetwork)
+ setSelectedAllocationId(newAllocation.id)
+ setTool('select')
+
+ // Re-propagate
+ setTimeout(() => handlePropagateFlow(), 100)
+ }
+
+ // Update allocation percentage
+ const updateAllocationPercentage = (allocationId: string, newPercentage: number) => {
+ const allocation = network.allocations.find(a => a.id === allocationId)
+ if (!allocation) return
+
+ const updatedAllocations = network.allocations.map(a =>
+ a.id === allocationId
+ ? { ...a, percentage: Math.max(0, Math.min(1, newPercentage)) }
+ : a
+ )
+
+ const sourceAllocations = updatedAllocations.filter(
+ a => a.sourceNodeId === allocation.sourceNodeId
+ )
+ const normalized = normalizeFlowAllocations(sourceAllocations)
+
+ const finalAllocations = updatedAllocations.map(a => {
+ const normalizedVersion = normalized.find(n => n.id === a.id)
+ return normalizedVersion || a
+ })
+
+ const updatedNetwork = {
+ ...network,
+ allocations: finalAllocations,
+ }
+
+ setNetwork(updatedNetwork)
+
+ // Re-propagate
+ setTimeout(() => {
+ const result = propagateFlow(updatedNetwork)
+ setNetwork(result.network)
+ setParticles(result.particles)
+ }, 100)
+ }
+
+ // Delete allocation
+ const deleteAllocation = (allocationId: string) => {
+ const allocation = network.allocations.find(a => a.id === allocationId)
+ if (!allocation) return
+
+ const updatedAllocations = network.allocations.filter(a => a.id !== allocationId)
+
+ const sourceAllocations = updatedAllocations.filter(
+ a => a.sourceNodeId === allocation.sourceNodeId
+ )
+ const normalized = normalizeFlowAllocations(sourceAllocations)
+
+ const finalAllocations = updatedAllocations.map(a => {
+ const normalizedVersion = normalized.find(n => n.id === a.id)
+ return normalizedVersion || a
+ })
+
+ const updatedNetwork = {
+ ...network,
+ allocations: finalAllocations,
+ }
+
+ setNetwork(updatedNetwork)
+ setSelectedAllocationId(null)
+
+ // Re-propagate
+ setTimeout(() => handlePropagateFlow(), 100)
+ }
+
+ // Load network
+ const handleLoadNetwork = (key: string) => {
+ setSelectedNetworkKey(key)
+ const newNetwork = getFlowSampleNetwork(key as keyof typeof flowSampleNetworks)
+ setNetwork(newNetwork)
+ setSelectedNodeId(null)
+ setSelectedAllocationId(null)
+ setAllocationSourceId(null)
+ setTool('select')
+
+ // Propagate initial flows
+ setTimeout(() => handlePropagateFlow(), 100)
+ }
+
+ // Get selected details
+ const selectedNode = selectedNodeId
+ ? network.nodes.find(n => n.id === selectedNodeId)
+ : null
+
+ const selectedAllocation = selectedAllocationId
+ ? network.allocations.find(a => a.id === selectedAllocationId)
+ : null
+
+ const outgoingAllocations = selectedNode
+ ? network.allocations.filter(a => a.sourceNodeId === selectedNode.id)
+ : []
+
+ const selectedAllocationSiblings = selectedAllocation
+ ? network.allocations.filter(a => a.sourceNodeId === selectedAllocation.sourceNodeId)
+ : []
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setTool('select')
+ setAllocationSourceId(null)
+ setSelectedNodeId(null)
+ setSelectedAllocationId(null)
+ } else if (e.key === 'Delete' && selectedAllocationId) {
+ deleteAllocation(selectedAllocationId)
+ } else if (e.key === ' ') {
+ e.preventDefault()
+ setIsAnimating(prev => !prev)
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [selectedAllocationId])
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Main Content */}
+
+ {/* Canvas */}
+
+
+
+ {/* Tool indicator */}
+ {allocationSourceId && (
+
+ Click target node to create allocation
+
+ )}
+
+ {/* Animation status */}
+
+ {isAnimating ? 'βΆοΈ Animating' : 'βΈοΈ Paused'} (Space to toggle)
+
+
+
+ {/* Sidebar */}
+
+ {/* Tools */}
+
+
Tools
+
+
+
+
+
+
+ {/* Network Selector */}
+
+
Select Network
+
+
+
+ {/* Network Info */}
+
+
{network.name}
+
+
+ Nodes:
+ {network.nodes.filter(n => !n.isOverflowSink).length}
+
+
+ Allocations:
+ {network.allocations.length}
+
+
+ Total Inflow:
+ {formatFlow(network.totalInflow)}
+
+
+ Total Absorbed:
+ {formatFlow(network.totalAbsorbed)}
+
+
+ Total Outflow:
+ {formatFlow(network.totalOutflow)}
+
+
+
+
+ {/* Set Flow Input */}
+ {selectedNode && !selectedNode.isOverflowSink && (
+
+
π§ Set Flow Input
+
+
+
+ handleSetNodeFlow(selectedNode.id, parseFloat(e.target.value) || 0)}
+ className="w-full px-3 py-2 bg-slate-700 rounded text-sm"
+ min="0"
+ step="10"
+ />
+
+
+ Current inflow: {formatFlow(selectedNode.inflow)}
+
+ Absorbed: {formatFlow(selectedNode.absorbed)}
+
+ Outflow: {formatFlow(selectedNode.outflow)}
+
+
+
+ )}
+
+ {/* Selected Allocation Editor */}
+ {selectedAllocation && (
+
+
Edit Allocation
+
+
+ From:
+
+ {network.nodes.find(n => n.id === selectedAllocation.sourceNodeId)?.name}
+
+
+
+ To:
+
+ {network.nodes.find(n => n.id === selectedAllocation.targetNodeId)?.name}
+
+
+
+
+ {selectedAllocationSiblings.length === 1 ? (
+
+ Single allocation must be 100%.
+
+ ) : (
+
+ updateAllocationPercentage(
+ selectedAllocation.id,
+ parseFloat(e.target.value) / 100
+ )
+ }
+ className="w-full"
+ />
+ )}
+
+
+
+
+ )}
+
+ {/* Selected Node Details */}
+ {selectedNode && (
+
+
Node Details
+
+
+ Name:
+ {selectedNode.name}
+
+
+ Status:
+
+ {selectedNode.status.toUpperCase()}
+
+
+
+
+ Inflow:
+ {formatFlow(selectedNode.inflow)}
+
+
+ Absorbed:
+ {formatFlow(selectedNode.absorbed)}
+
+
+ Outflow:
+ {formatFlow(selectedNode.outflow)}
+
+
+ Min Absorption:
+ {formatFlow(selectedNode.minAbsorption)}
+
+
+ Max Absorption:
+ {formatFlow(selectedNode.maxAbsorption)}
+
+
+
+ {/* Outgoing Allocations */}
+ {outgoingAllocations.length > 0 && (
+
+
Outgoing Allocations:
+ {outgoingAllocations.map((alloc) => {
+ const target = network.nodes.find(n => n.id === alloc.targetNodeId)
+ return (
+
setSelectedAllocationId(alloc.id)}
+ >
+ β {target?.name}
+
+ {Math.round(alloc.percentage * 100)}%
+
+
+ )
+ })}
+
+ )}
+
+
+ )}
+
+ {/* Legend */}
+
+
Legend
+
+
+
+
Starved - Below minimum absorption
+
+
+
+
Minimum - At minimum absorption
+
+
+
+
Healthy - Between min and max
+
+
+
+
Saturated - At maximum capacity
+
+
+
+
Particle - Flow animation
+
+
+
+
+ {/* Instructions */}
+
+
+ Flow-Based Model
+
+
+ - Click node to select and set flow
+ - Use Create Arrow to draw allocations
+ - Watch flows propagate in real-time
+ - Press Space to pause/play animation
+ - Overflow sink appears automatically if needed
+
+
+
+
+
+ )
+}
diff --git a/components/hero-section.tsx b/components/hero-section.tsx
index b2c6c91..81e9845 100644
--- a/components/hero-section.tsx
+++ b/components/hero-section.tsx
@@ -36,12 +36,16 @@ export function HeroSection() {
diff --git a/lib/tbff-flow/README.md b/lib/tbff-flow/README.md
new file mode 100644
index 0000000..e57ce7f
--- /dev/null
+++ b/lib/tbff-flow/README.md
@@ -0,0 +1,404 @@
+# Flow-Based Flow Funding Module
+
+**Status**: Initial Implementation Complete β
+**Route**: `/tbff-flow`
+**Last Updated**: 2025-11-09
+
+---
+
+## Overview
+
+This module implements a **flow-based** visualization of Flow Funding, focusing on resource circulation rather than accumulation. Unlike the stock-based model (`/tbff`), this visualizes how resources move through networks in real-time.
+
+## Core Concepts
+
+### Flow vs Stock
+
+**Stock Model** (`/tbff`):
+- Accounts have balances (accumulated resources)
+- Distribution adds to balances
+- Thresholds define states (deficit, healthy, overflow)
+- Overflow triggers redistribution
+
+**Flow Model** (`/tbff-flow`):
+- Nodes have flow rates (resources in motion)
+- Flow continuously circulates
+- Thresholds define absorption capacity
+- Real-time animation shows circulation
+
+### 1. Flow Node
+
+Each node receives flow, absorbs what it needs, and passes excess onward.
+
+**Properties**:
+- `minAbsorption`: Minimum flow needed to function (survival level)
+- `maxAbsorption`: Maximum flow that can be absorbed (capacity)
+- `externalFlow`: Flow injected by user (source)
+- `inflow`: Total flow entering (external + from allocations)
+- `absorbed`: Amount kept (between min and max)
+- `outflow`: Excess flow leaving (to allocations)
+
+**Status**:
+- π΄ **Starved**: absorbed < minAbsorption
+- π‘ **Minimum**: absorbed β minAbsorption
+- π΅ **Healthy**: minAbsorption < absorbed < maxAbsorption
+- π’ **Saturated**: absorbed β₯ maxAbsorption
+
+### 2. Flow Propagation
+
+**Algorithm**:
+1. Start with external flows (user-set inputs)
+2. For each iteration:
+ - Calculate absorption: `min(inflow, maxAbsorption)`
+ - Calculate outflow: `max(0, inflow - absorbed)`
+ - Distribute outflow via allocations
+ - Update inflows for next iteration
+3. Repeat until convergence (flows stabilize)
+4. Create overflow sink if needed
+
+**Convergence**: Flows change by less than 0.01 between iterations
+
+### 3. Overflow Sink
+
+**Auto-created** when network has unallocated outflow.
+
+**Purpose**: Capture excess flow that has nowhere to go
+
+**Behavior**:
+- Appears automatically when needed
+- Disappears when no longer needed
+- Infinite absorption capacity
+- Visualized with "SINK" label
+
+### 4. Flow Particles
+
+**Animation**: Particles move along allocation arrows
+
+**Properties**:
+- Position along arrow (0.0 to 1.0)
+- Amount (affects size and color)
+- Speed (faster for higher flow)
+- Continuous loop (resets at end)
+
+**Purpose**: Visual feedback of resource circulation
+
+---
+
+## Module Structure
+
+```
+lib/tbff-flow/
+βββ types.ts # Flow-based types (FlowNode, FlowNetwork, etc.)
+βββ utils.ts # Utility functions (absorption, status, etc.)
+βββ algorithms.ts # Flow propagation algorithm
+βββ rendering.ts # Canvas rendering with particles
+βββ sample-networks.ts # Demo networks (linear, split, circular)
+βββ README.md # This file
+
+app/tbff-flow/
+βββ page.tsx # Main page with real-time animation
+```
+
+---
+
+## Features
+
+### β
Implemented
+
+1. **Flow Propagation Algorithm**
+ - Iterative flow distribution
+ - Convergence detection
+ - Comprehensive console logging
+ - Automatic overflow node creation
+
+2. **Real-Time Animation**
+ - Continuous particle movement
+ - Pause/play with spacebar
+ - Smooth 60fps rendering
+ - Visual flow indicators
+
+3. **Interactive Controls**
+ - Click node to set external flow
+ - Create allocations with arrow tool
+ - Edit allocation percentages
+ - Delete allocations
+
+4. **Sample Networks**
+ - Linear Flow (A β B β C)
+ - Split Flow (Projects + Commons)
+ - Circular Flow (A β B β C)
+ - Empty Network (build your own)
+
+5. **Visual Design**
+ - Flow bars show inflow/absorption/outflow
+ - Status colors (red/yellow/blue/green)
+ - Arrow thickness = flow amount
+ - Particle density = flow volume
+ - External flow indicators (green dots)
+
+---
+
+## Usage
+
+### Setting Flow
+
+1. Select a node (click on it)
+2. Enter external flow value in sidebar
+3. Watch flow propagate automatically
+4. See particles animate along arrows
+
+### Creating Allocations
+
+1. Click "Create Arrow" tool
+2. Click source node
+3. Click target node
+4. Allocation created with 50% default
+5. Flow re-propagates automatically
+
+### Observing Flow
+
+- **Inflow bars** (blue): Total flow entering
+- **Absorption bars** (status color): Amount kept
+- **Outflow bars** (green): Excess leaving
+- **Particles**: Moving resources
+- **Arrow thickness**: Flow amount
+
+### Keyboard Shortcuts
+
+- **Space**: Pause/play animation
+- **Escape**: Cancel creation, deselect
+- **Delete**: Remove selected allocation
+
+---
+
+## Sample Networks
+
+### 1. Linear Flow
+
+**Structure**: A β B β C
+
+**Setup**: Alice receives 100 flow, passes excess to Bob, who passes to Carol
+
+**Demonstrates**: Sequential absorption and propagation
+
+### 2. Split Flow
+
+**Structure**: Source β Projects (A, B) β Commons
+
+**Setup**: Source splits 60/40 between projects, which merge at Commons
+
+**Demonstrates**: Branching and merging flows
+
+### 3. Circular Flow
+
+**Structure**: A β B β C β A
+
+**Setup**: Alice injects 50 flow, which circulates continuously
+
+**Demonstrates**: Circular resource circulation
+
+### 4. Empty Network
+
+**Structure**: 3 unconnected nodes
+
+**Purpose**: Build custom flow patterns from scratch
+
+---
+
+## Algorithm Details
+
+### Flow Propagation Example
+
+```
+Initial State:
+ Alice: externalFlow = 100, maxAbsorption = 50
+ Bob: externalFlow = 0, maxAbsorption = 30
+ Alice β Bob (100%)
+
+Iteration 1:
+ Alice: inflow = 100, absorbed = 50, outflow = 50
+ Bob: inflow = 0 (not yet received)
+
+Iteration 2:
+ Alice: inflow = 100, absorbed = 50, outflow = 50
+ Bob: inflow = 50, absorbed = 30, outflow = 20
+
+Iteration 3 onwards:
+ (Converged - no change)
+
+Result:
+ Alice absorbs 50, passes 50 to Bob
+ Bob absorbs 30, has 20 outflow (needs overflow node)
+```
+
+### Convergence
+
+**Condition**: `max(|inflow[i] - inflow[i-1]|) < 0.01` for all nodes
+
+**Max Iterations**: 100
+
+**Typical**: Converges in 10-20 iterations for most networks
+
+---
+
+## Technical Implementation
+
+### State Management
+
+```typescript
+const [network, setNetwork] = useState(...)
+const [particles, setParticles] = useState([])
+const [isAnimating, setIsAnimating] = useState(true)
+```
+
+### Animation Loop
+
+```typescript
+useEffect(() => {
+ const animate = () => {
+ setParticles(prev => updateFlowParticles(prev))
+ requestAnimationFrame(animate)
+ }
+ requestAnimationFrame(animate)
+}, [isAnimating])
+```
+
+### Canvas Rendering
+
+```typescript
+useEffect(() => {
+ renderFlowNetwork(ctx, network, width, height, particles, ...)
+}, [network, particles, selectedNodeId, selectedAllocationId])
+```
+
+### Flow Propagation Trigger
+
+- On network load
+- On external flow change
+- On allocation create/update/delete
+- Automatic 100ms after change
+
+---
+
+## Design Decisions
+
+### 1. Why Separate from Stock Model?
+
+**Decision**: Create `/tbff-flow` as separate route
+
+**Reasoning**:
+- Different mental models (flow vs stock)
+- Different visualizations (particles vs bars)
+- Different algorithms (propagation vs distribution)
+- Users can compare both approaches
+
+### 2. Why Real-Time Continuous Animation?
+
+**Decision**: Particles move continuously at 60fps
+
+**Reasoning**:
+- Emphasizes circulation over states
+- More engaging and dynamic
+- Matches "flow" concept intuitively
+- Educational - see resources in motion
+
+**Trade-off**: More CPU usage vs better UX
+
+### 3. Why Auto-Create Overflow Node?
+
+**Decision**: Automatically create/remove overflow sink
+
+**Reasoning**:
+- Unallocated outflow needs destination
+- Prevents "leaking" flow
+- Conserves resources (total inflow = absorbed + overflow)
+- User shouldn't have to manually manage
+
+### 4. Why Absorption Thresholds?
+
+**Decision**: Min/max thresholds define absorption capacity
+
+**Reasoning**:
+- Maps to real resource needs (minimum to function, maximum to benefit)
+- Similar to stock model (easy to understand)
+- Allows partial absorption (not all-or-nothing)
+- Generates meaningful outflow for circulation
+
+---
+
+## Comparison with Stock Model
+
+| Aspect | Stock Model (`/tbff`) | Flow Model (`/tbff-flow`) |
+|--------|----------------------|---------------------------|
+| **Core Metric** | Balance (accumulated) | Flow rate (per time) |
+| **Visualization** | Fill height | Flow bars + particles |
+| **Input** | Add funding (one-time) | Set flow (continuous) |
+| **Algorithm** | Initial distribution | Flow propagation |
+| **Animation** | Static (for now) | Real-time particles |
+| **Overflow** | Triggers redistribution | Continuous outflow |
+| **Use Case** | Budget allocation | Resource circulation |
+
+---
+
+## Future Enhancements
+
+### Phase 2
+
+- [ ] Variable particle colors (by source)
+- [ ] Flow rate history graphs
+- [ ] Equilibrium detection indicator
+- [ ] Save/load custom networks
+- [ ] Export flow data (CSV, JSON)
+
+### Phase 3
+
+- [ ] Multiple simultaneous external flows
+- [ ] Time-varying flows (pulses, waves)
+- [ ] Flow constraints (min/max on arrows)
+- [ ] Network analysis (bottlenecks, unutilized capacity)
+
+### Phase 4
+
+- [ ] Combine stock and flow models
+- [ ] Hybrid visualization
+- [ ] Round-based simulation mode
+- [ ] Multi-network comparison
+
+---
+
+## Resources
+
+- **Stock Model**: `/lib/tbff/README.md`
+- **Design Session**: `/.claude/journal/FLOW_FUNDING_DESIGN_SESSION.md`
+- **Academic Paper**: `../../../threshold-based-flow-funding.md`
+
+---
+
+## Testing Checklist
+
+- [x] Load default network (Linear Flow)
+- [x] Switch between sample networks
+- [x] Set external flow on node
+- [x] Watch flow propagate
+- [x] See particles animate
+- [x] Create allocation with arrow tool
+- [x] Edit allocation percentage
+- [x] Delete allocation
+- [x] Pause/play animation with Space
+- [x] See overflow node appear when needed
+- [x] See overflow node disappear when not needed
+- [x] Check console for propagation logs
+- [x] Verify convergence
+- [x] Test circular flow network
+
+---
+
+**Built with**: TypeScript, React, Next.js, Canvas API, requestAnimationFrame
+
+**Module Owner**: TBFF Flow Team
+
+**Philosophy**: "Resources are meant to circulate, not accumulate."
+
+---
+
+*Flow where needed, absorb what's needed, pass on the rest.*
diff --git a/lib/tbff-flow/algorithms.ts b/lib/tbff-flow/algorithms.ts
new file mode 100644
index 0000000..4a19c6e
--- /dev/null
+++ b/lib/tbff-flow/algorithms.ts
@@ -0,0 +1,267 @@
+/**
+ * Flow propagation algorithms
+ *
+ * Models how flow circulates through the network:
+ * 1. Flow enters nodes (external + from allocations)
+ * 2. Nodes absorb what they can (up to max threshold)
+ * 3. Excess flows out via allocations
+ * 4. Repeat until steady state
+ */
+
+import type { FlowNetwork, FlowNode, FlowParticle, FlowPropagationResult } from './types'
+import { updateFlowNodeProperties, calculateFlowNetworkTotals, createOverflowNode, needsOverflowNode } from './utils'
+
+const MAX_ITERATIONS = 100
+const CONVERGENCE_THRESHOLD = 0.01
+
+/**
+ * Propagate flow through the network
+ *
+ * Algorithm:
+ * 1. Reset all inflows to external flow only
+ * 2. For each iteration:
+ * a. Calculate absorption and outflow for each node
+ * b. Distribute outflow via allocations
+ * c. Update inflows for next iteration
+ * 3. Repeat until flows stabilize or max iterations
+ * 4. Create overflow node if needed
+ * 5. Generate flow particles for animation
+ *
+ * @param network - Current network state
+ * @returns Updated network with flow propagation results
+ */
+export function propagateFlow(network: FlowNetwork): FlowPropagationResult {
+ console.log('\nπ Flow Propagation Started')
+ console.log('β'.repeat(50))
+
+ let currentNetwork = { ...network }
+ let iterations = 0
+ let converged = false
+
+ // Initialize: set inflow to external flow
+ let nodes = currentNetwork.nodes.map(node => ({
+ ...node,
+ inflow: node.externalFlow,
+ }))
+
+ console.log('Initial external flows:')
+ nodes.forEach(node => {
+ if (node.externalFlow > 0) {
+ console.log(` ${node.name}: ${node.externalFlow.toFixed(1)}`)
+ }
+ })
+
+ // Iterate until convergence
+ while (iterations < MAX_ITERATIONS && !converged) {
+ iterations++
+
+ // Step 1: Calculate absorption and outflow for each node
+ nodes = nodes.map(updateFlowNodeProperties)
+
+ // Step 2: Calculate new inflows from allocations
+ const newInflows = new Map()
+
+ // Initialize with external flows
+ nodes.forEach(node => {
+ newInflows.set(node.id, node.externalFlow)
+ })
+
+ // Add flow from allocations
+ nodes.forEach(sourceNode => {
+ if (sourceNode.outflow <= 0) return
+
+ // Get allocations from this node
+ const allocations = currentNetwork.allocations.filter(
+ a => a.sourceNodeId === sourceNode.id
+ )
+
+ if (allocations.length === 0) {
+ // No allocations - this flow will need overflow node
+ return
+ }
+
+ // Distribute outflow via allocations
+ allocations.forEach(allocation => {
+ const flowToTarget = sourceNode.outflow * allocation.percentage
+ const currentInflow = newInflows.get(allocation.targetNodeId) || 0
+ newInflows.set(allocation.targetNodeId, currentInflow + flowToTarget)
+ })
+ })
+
+ // Step 3: Check for convergence
+ let maxChange = 0
+ nodes.forEach(node => {
+ const newInflow = newInflows.get(node.id) || node.externalFlow
+ const change = Math.abs(newInflow - node.inflow)
+ maxChange = Math.max(maxChange, change)
+ })
+
+ converged = maxChange < CONVERGENCE_THRESHOLD
+
+ // Step 4: Update inflows for next iteration
+ nodes = nodes.map(node => ({
+ ...node,
+ inflow: newInflows.get(node.id) || node.externalFlow,
+ }))
+
+ if (iterations % 10 === 0 || converged) {
+ console.log(`Iteration ${iterations}: max change = ${maxChange.toFixed(3)}`)
+ }
+ }
+
+ console.log(`\n${converged ? 'β' : 'β οΈ'} ${converged ? 'Converged' : 'Max iterations reached'} after ${iterations} iterations`)
+
+ // Final property update
+ nodes = nodes.map(updateFlowNodeProperties)
+
+ // Check if we need an overflow node
+ let finalNodes = nodes
+ let overflowNodeId: string | null = currentNetwork.overflowNodeId
+
+ const needsOverflow = needsOverflowNode({ ...currentNetwork, nodes })
+
+ if (needsOverflow && !overflowNodeId) {
+ // Create overflow node
+ const overflowNode = createOverflowNode(600, 300)
+ finalNodes = [...nodes, overflowNode]
+ overflowNodeId = overflowNode.id
+
+ console.log('\nπ§ Created overflow sink node')
+
+ // Calculate total unallocated outflow
+ let totalUnallocated = 0
+ nodes.forEach(node => {
+ const hasAllocations = currentNetwork.allocations.some(a => a.sourceNodeId === node.id)
+ if (!hasAllocations && node.outflow > 0) {
+ totalUnallocated += node.outflow
+ }
+ })
+
+ // Update overflow node
+ const overflowNode2 = finalNodes.find(n => n.id === overflowNodeId)!
+ const updatedOverflowNode = updateFlowNodeProperties({
+ ...overflowNode2,
+ inflow: totalUnallocated,
+ })
+
+ finalNodes = finalNodes.map(n => n.id === overflowNodeId ? updatedOverflowNode : n)
+
+ console.log(` Receiving ${totalUnallocated.toFixed(1)} unallocated flow`)
+ } else if (!needsOverflow && overflowNodeId) {
+ // Remove overflow node
+ finalNodes = nodes.filter(n => !n.isOverflowSink)
+ overflowNodeId = null
+ console.log('\nποΈ Removed overflow sink node (no longer needed)')
+ }
+
+ // Generate flow particles for animation
+ const particles = generateFlowParticles(currentNetwork, finalNodes)
+
+ // Build final network
+ const finalNetwork = calculateFlowNetworkTotals({
+ ...currentNetwork,
+ nodes: finalNodes,
+ overflowNodeId,
+ })
+
+ console.log('\nπ Final State:')
+ console.log(` Total inflow: ${finalNetwork.totalInflow.toFixed(1)}`)
+ console.log(` Total absorbed: ${finalNetwork.totalAbsorbed.toFixed(1)}`)
+ console.log(` Total outflow: ${finalNetwork.totalOutflow.toFixed(1)}`)
+
+ console.log('\nπ― Node States:')
+ finalNodes.forEach(node => {
+ if (node.inflow > 0 || node.absorbed > 0 || node.outflow > 0) {
+ console.log(
+ ` ${node.name.padEnd(15)} ` +
+ `in: ${node.inflow.toFixed(1).padStart(6)} ` +
+ `abs: ${node.absorbed.toFixed(1).padStart(6)} ` +
+ `out: ${node.outflow.toFixed(1).padStart(6)} ` +
+ `[${node.status}]`
+ )
+ }
+ })
+
+ return {
+ network: finalNetwork,
+ iterations,
+ converged,
+ particles,
+ }
+}
+
+/**
+ * Generate flow particles for animation
+ * Creates particles traveling along allocations based on flow amount
+ */
+function generateFlowParticles(network: FlowNetwork, nodes: FlowNode[]): FlowParticle[] {
+ const particles: FlowParticle[] = []
+ let particleId = 0
+
+ // Create particles for each allocation based on flow amount
+ network.allocations.forEach(allocation => {
+ const sourceNode = nodes.find(n => n.id === allocation.sourceNodeId)
+ if (!sourceNode || sourceNode.outflow <= 0) return
+
+ const flowAmount = sourceNode.outflow * allocation.percentage
+
+ // Create particles proportional to flow amount
+ // More flow = more particles
+ const particleCount = Math.min(10, Math.max(1, Math.floor(flowAmount / 10)))
+
+ for (let i = 0; i < particleCount; i++) {
+ particles.push({
+ id: `particle_${particleId++}`,
+ allocationId: allocation.id,
+ progress: i / particleCount, // Spread along arrow
+ amount: flowAmount / particleCount,
+ speed: 0.01 + (flowAmount / 1000), // Faster for more flow
+ })
+ }
+ })
+
+ // Create particles for overflow node flows
+ nodes.forEach(node => {
+ if (node.outflow > 0) {
+ const hasAllocations = network.allocations.some(a => a.sourceNodeId === node.id)
+ if (!hasAllocations && network.overflowNodeId) {
+ // Create virtual allocation to overflow node
+ const particleCount = Math.min(5, Math.max(1, Math.floor(node.outflow / 20)))
+ for (let i = 0; i < particleCount; i++) {
+ particles.push({
+ id: `particle_overflow_${particleId++}`,
+ allocationId: `virtual_${node.id}_overflow`,
+ progress: i / particleCount,
+ amount: node.outflow / particleCount,
+ speed: 0.01,
+ })
+ }
+ }
+ }
+ })
+
+ return particles
+}
+
+/**
+ * Update particles animation
+ * Moves particles along their paths
+ */
+export function updateFlowParticles(particles: FlowParticle[]): FlowParticle[] {
+ return particles.map(particle => {
+ const newProgress = particle.progress + particle.speed
+
+ // If particle reached end, reset to beginning
+ if (newProgress >= 1.0) {
+ return {
+ ...particle,
+ progress: 0,
+ }
+ }
+
+ return {
+ ...particle,
+ progress: newProgress,
+ }
+ })
+}
diff --git a/lib/tbff-flow/rendering.ts b/lib/tbff-flow/rendering.ts
new file mode 100644
index 0000000..dbd32df
--- /dev/null
+++ b/lib/tbff-flow/rendering.ts
@@ -0,0 +1,319 @@
+/**
+ * Canvas rendering for flow-based visualization
+ */
+
+import type { FlowNetwork, FlowNode, FlowAllocation, FlowParticle } from './types'
+import { getFlowNodeCenter, getFlowStatusColor } from './utils'
+
+/**
+ * Render a flow node
+ * Shows inflow, absorption, and outflow rates
+ */
+export function renderFlowNode(
+ ctx: CanvasRenderingContext2D,
+ node: FlowNode,
+ isSelected: boolean = false
+): void {
+ const { x, y, width, height } = node
+
+ // Background
+ ctx.fillStyle = isSelected ? '#1e293b' : '#0f172a'
+ ctx.fillRect(x, y, width, height)
+
+ // Border (thicker if selected)
+ ctx.strokeStyle = isSelected ? '#06b6d4' : '#334155'
+ ctx.lineWidth = isSelected ? 3 : 1
+ ctx.strokeRect(x, y, width, height)
+
+ // Flow visualization bars
+ const barWidth = width - 20
+ const barHeight = 8
+ const barX = x + 10
+ let barY = y + 25
+
+ // Inflow bar (blue)
+ if (node.inflow > 0) {
+ const inflowPercent = Math.min(1, node.inflow / node.maxAbsorption)
+ ctx.fillStyle = 'rgba(59, 130, 246, 0.7)'
+ ctx.fillRect(barX, barY, barWidth * inflowPercent, barHeight)
+ ctx.strokeStyle = 'rgba(59, 130, 246, 0.5)'
+ ctx.strokeRect(barX, barY, barWidth, barHeight)
+ }
+
+ barY += barHeight + 4
+
+ // Absorption bar (status color)
+ if (node.absorbed > 0) {
+ const absorbedPercent = Math.min(1, node.absorbed / node.maxAbsorption)
+ ctx.fillStyle = getFlowStatusColor(node.status, 0.7)
+ ctx.fillRect(barX, barY, barWidth * absorbedPercent, barHeight)
+ ctx.strokeStyle = getFlowStatusColor(node.status, 0.5)
+ ctx.strokeRect(barX, barY, barWidth, barHeight)
+ }
+
+ barY += barHeight + 4
+
+ // Outflow bar (green)
+ if (node.outflow > 0) {
+ const outflowPercent = Math.min(1, node.outflow / node.maxAbsorption)
+ ctx.fillStyle = 'rgba(16, 185, 129, 0.7)'
+ ctx.fillRect(barX, barY, barWidth * outflowPercent, barHeight)
+ ctx.strokeStyle = 'rgba(16, 185, 129, 0.5)'
+ ctx.strokeRect(barX, barY, barWidth, barHeight)
+ }
+
+ // Node name
+ ctx.fillStyle = '#f1f5f9'
+ ctx.font = 'bold 14px sans-serif'
+ ctx.textAlign = 'center'
+ ctx.fillText(node.name, x + width / 2, y + 16)
+
+ // Flow rates
+ ctx.font = '10px monospace'
+ ctx.fillStyle = '#94a3b8'
+ const textX = x + width / 2
+ let textY = y + height - 30
+
+ if (node.inflow > 0) {
+ ctx.fillText(`β ${node.inflow.toFixed(1)}`, textX, textY)
+ textY += 12
+ }
+ if (node.absorbed > 0) {
+ ctx.fillText(`β ${node.absorbed.toFixed(1)}`, textX, textY)
+ textY += 12
+ }
+ if (node.outflow > 0) {
+ ctx.fillText(`β ${node.outflow.toFixed(1)}`, textX, textY)
+ }
+
+ // External flow indicator
+ if (node.externalFlow > 0 && !node.isOverflowSink) {
+ ctx.fillStyle = '#10b981'
+ ctx.beginPath()
+ ctx.arc(x + width - 10, y + 10, 5, 0, 2 * Math.PI)
+ ctx.fill()
+ }
+
+ // Overflow sink indicator
+ if (node.isOverflowSink) {
+ ctx.fillStyle = '#64748b'
+ ctx.font = 'bold 12px sans-serif'
+ ctx.textAlign = 'center'
+ ctx.fillText('SINK', x + width / 2, y + height / 2)
+ }
+
+ // Center dot for connections
+ const centerX = x + width / 2
+ const centerY = y + height / 2
+ ctx.fillStyle = isSelected ? '#06b6d4' : '#475569'
+ ctx.beginPath()
+ ctx.arc(centerX, centerY, 4, 0, 2 * Math.PI)
+ ctx.fill()
+}
+
+/**
+ * Render a flow allocation arrow
+ * Thickness represents flow amount
+ */
+export function renderFlowAllocation(
+ ctx: CanvasRenderingContext2D,
+ allocation: FlowAllocation,
+ sourceNode: FlowNode,
+ targetNode: FlowNode,
+ isSelected: boolean = false
+): void {
+ const source = getFlowNodeCenter(sourceNode)
+ const target = getFlowNodeCenter(targetNode)
+
+ // Calculate arrow properties
+ const dx = target.x - source.x
+ const dy = target.y - source.y
+ const angle = Math.atan2(dy, dx)
+ const length = Math.sqrt(dx * dx + dy * dy)
+
+ // Shorten arrow to not overlap nodes
+ const shortenStart = 60
+ const shortenEnd = 60
+ const startX = source.x + (shortenStart / length) * dx
+ const startY = source.y + (shortenStart / length) * dy
+ const endX = target.x - (shortenEnd / length) * dx
+ const endY = target.y - (shortenEnd / length) * dy
+
+ // Arrow thickness based on flow amount
+ const flowAmount = sourceNode.outflow * allocation.percentage
+ const thickness = Math.max(2, Math.min(12, 2 + flowAmount / 10))
+
+ // Color based on selection and flow amount
+ const hasFlow = flowAmount > 0.1
+ const baseColor = isSelected ? '#06b6d4' : hasFlow ? '#10b981' : '#475569'
+ const alpha = hasFlow ? 0.8 : 0.3
+
+ // Draw arrow line
+ ctx.strokeStyle = baseColor
+ ctx.globalAlpha = alpha
+ ctx.lineWidth = thickness
+ ctx.lineCap = 'round'
+
+ ctx.beginPath()
+ ctx.moveTo(startX, startY)
+ ctx.lineTo(endX, endY)
+ ctx.stroke()
+
+ // Draw arrowhead
+ const headSize = 10 + thickness
+ ctx.fillStyle = baseColor
+ ctx.beginPath()
+ ctx.moveTo(endX, endY)
+ ctx.lineTo(
+ endX - headSize * Math.cos(angle - Math.PI / 6),
+ endY - headSize * Math.sin(angle - Math.PI / 6)
+ )
+ ctx.lineTo(
+ endX - headSize * Math.cos(angle + Math.PI / 6),
+ endY - headSize * Math.sin(angle + Math.PI / 6)
+ )
+ ctx.closePath()
+ ctx.fill()
+
+ ctx.globalAlpha = 1.0
+
+ // Label with flow amount
+ if (hasFlow || isSelected) {
+ const midX = (startX + endX) / 2
+ const midY = (startY + endY) / 2
+
+ // Background for text
+ ctx.fillStyle = '#0f172a'
+ ctx.fillRect(midX - 20, midY - 8, 40, 16)
+
+ // Text
+ ctx.fillStyle = baseColor
+ ctx.font = '11px monospace'
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ ctx.fillText(flowAmount.toFixed(1), midX, midY)
+ }
+}
+
+/**
+ * Render flow particles moving along allocations
+ */
+export function renderFlowParticles(
+ ctx: CanvasRenderingContext2D,
+ particles: FlowParticle[],
+ network: FlowNetwork
+): void {
+ particles.forEach(particle => {
+ // Find the allocation
+ const allocation = network.allocations.find(a => a.id === particle.allocationId)
+ if (!allocation) {
+ // Handle virtual overflow allocations
+ if (particle.allocationId.startsWith('virtual_')) {
+ const sourceNodeId = particle.allocationId.split('_')[1]
+ const sourceNode = network.nodes.find(n => n.id === sourceNodeId)
+ const overflowNode = network.nodes.find(n => n.isOverflowSink)
+ if (sourceNode && overflowNode) {
+ renderParticle(ctx, particle, sourceNode, overflowNode)
+ }
+ }
+ return
+ }
+
+ const sourceNode = network.nodes.find(n => n.id === allocation.sourceNodeId)
+ const targetNode = network.nodes.find(n => n.id === allocation.targetNodeId)
+
+ if (!sourceNode || !targetNode) return
+
+ renderParticle(ctx, particle, sourceNode, targetNode)
+ })
+}
+
+/**
+ * Render a single particle
+ */
+function renderParticle(
+ ctx: CanvasRenderingContext2D,
+ particle: FlowParticle,
+ sourceNode: FlowNode,
+ targetNode: FlowNode
+): void {
+ const source = getFlowNodeCenter(sourceNode)
+ const target = getFlowNodeCenter(targetNode)
+
+ // Interpolate position
+ const x = source.x + (target.x - source.x) * particle.progress
+ const y = source.y + (target.y - source.y) * particle.progress
+
+ // Particle size based on amount
+ const size = Math.max(3, Math.min(8, particle.amount / 10))
+
+ // Draw particle
+ ctx.fillStyle = '#10b981'
+ ctx.globalAlpha = 0.8
+ ctx.beginPath()
+ ctx.arc(x, y, size, 0, 2 * Math.PI)
+ ctx.fill()
+ ctx.globalAlpha = 1.0
+
+ // Glow effect
+ const gradient = ctx.createRadialGradient(x, y, 0, x, y, size * 2)
+ gradient.addColorStop(0, 'rgba(16, 185, 129, 0.3)')
+ gradient.addColorStop(1, 'rgba(16, 185, 129, 0)')
+ ctx.fillStyle = gradient
+ ctx.beginPath()
+ ctx.arc(x, y, size * 2, 0, 2 * Math.PI)
+ ctx.fill()
+}
+
+/**
+ * Render entire flow network
+ */
+export function renderFlowNetwork(
+ ctx: CanvasRenderingContext2D,
+ network: FlowNetwork,
+ canvasWidth: number,
+ canvasHeight: number,
+ particles: FlowParticle[],
+ selectedNodeId: string | null = null,
+ selectedAllocationId: string | null = null
+): void {
+ // Clear canvas
+ ctx.fillStyle = '#0f172a'
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight)
+
+ // Draw allocations (arrows) first
+ network.allocations.forEach(allocation => {
+ const sourceNode = network.nodes.find(n => n.id === allocation.sourceNodeId)
+ const targetNode = network.nodes.find(n => n.id === allocation.targetNodeId)
+ if (sourceNode && targetNode) {
+ renderFlowAllocation(
+ ctx,
+ allocation,
+ sourceNode,
+ targetNode,
+ allocation.id === selectedAllocationId
+ )
+ }
+ })
+
+ // Draw particles
+ renderFlowParticles(ctx, particles, network)
+
+ // Draw nodes on top
+ network.nodes.forEach(node => {
+ renderFlowNode(ctx, node, node.id === selectedNodeId)
+ })
+
+ // Draw network stats in corner
+ ctx.fillStyle = '#f1f5f9'
+ ctx.font = '12px monospace'
+ ctx.textAlign = 'left'
+ const statsX = 10
+ let statsY = 20
+
+ ctx.fillText(`Inflow: ${network.totalInflow.toFixed(1)}`, statsX, statsY)
+ statsY += 16
+ ctx.fillText(`Absorbed: ${network.totalAbsorbed.toFixed(1)}`, statsX, statsY)
+ statsY += 16
+ ctx.fillText(`Outflow: ${network.totalOutflow.toFixed(1)}`, statsX, statsY)
+}
diff --git a/lib/tbff-flow/sample-networks.ts b/lib/tbff-flow/sample-networks.ts
new file mode 100644
index 0000000..f462563
--- /dev/null
+++ b/lib/tbff-flow/sample-networks.ts
@@ -0,0 +1,329 @@
+/**
+ * Sample flow networks for demonstration
+ */
+
+import type { FlowNetwork, FlowNode } from './types'
+import { updateFlowNodeProperties, calculateFlowNetworkTotals } from './utils'
+
+/**
+ * Create a simple linear flow: A β B β C
+ * Flow enters A, passes through to C
+ */
+export function createLinearFlowNetwork(): FlowNetwork {
+ const nodes: FlowNode[] = [
+ {
+ id: 'alice',
+ name: 'Alice',
+ x: 100,
+ y: 200,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 50,
+ inflow: 100, // Start with 100 flow
+ absorbed: 0,
+ outflow: 0,
+ status: 'healthy',
+ externalFlow: 100,
+ isOverflowSink: false,
+ },
+ {
+ id: 'bob',
+ name: 'Bob',
+ x: 300,
+ y: 200,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 30,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ {
+ id: 'carol',
+ name: 'Carol',
+ x: 500,
+ y: 200,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 40,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ ]
+
+ const allocations = [
+ { id: 'alloc_1', sourceNodeId: 'alice', targetNodeId: 'bob', percentage: 1.0 },
+ { id: 'alloc_2', sourceNodeId: 'bob', targetNodeId: 'carol', percentage: 1.0 },
+ ]
+
+ return calculateFlowNetworkTotals({
+ name: 'Linear Flow (A β B β C)',
+ nodes: nodes.map(updateFlowNodeProperties),
+ allocations,
+ totalInflow: 0,
+ totalAbsorbed: 0,
+ totalOutflow: 0,
+ overflowNodeId: null,
+ })
+}
+
+/**
+ * Create a split flow: A β B and C
+ * Flow enters A, splits between B and C
+ */
+export function createSplitFlowNetwork(): FlowNetwork {
+ const nodes: FlowNode[] = [
+ {
+ id: 'source',
+ name: 'Source',
+ x: 100,
+ y: 200,
+ width: 120,
+ height: 100,
+ minAbsorption: 5,
+ maxAbsorption: 20,
+ inflow: 100,
+ absorbed: 0,
+ outflow: 0,
+ status: 'healthy',
+ externalFlow: 100,
+ isOverflowSink: false,
+ },
+ {
+ id: 'project_a',
+ name: 'Project A',
+ x: 300,
+ y: 100,
+ width: 120,
+ height: 100,
+ minAbsorption: 15,
+ maxAbsorption: 40,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ {
+ id: 'project_b',
+ name: 'Project B',
+ x: 300,
+ y: 300,
+ width: 120,
+ height: 100,
+ minAbsorption: 15,
+ maxAbsorption: 40,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ {
+ id: 'commons',
+ name: 'Commons',
+ x: 500,
+ y: 200,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 30,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ ]
+
+ const allocations = [
+ { id: 'alloc_1', sourceNodeId: 'source', targetNodeId: 'project_a', percentage: 0.6 },
+ { id: 'alloc_2', sourceNodeId: 'source', targetNodeId: 'project_b', percentage: 0.4 },
+ { id: 'alloc_3', sourceNodeId: 'project_a', targetNodeId: 'commons', percentage: 1.0 },
+ { id: 'alloc_4', sourceNodeId: 'project_b', targetNodeId: 'commons', percentage: 1.0 },
+ ]
+
+ return calculateFlowNetworkTotals({
+ name: 'Split Flow (Source β Projects β Commons)',
+ nodes: nodes.map(updateFlowNodeProperties),
+ allocations,
+ totalInflow: 0,
+ totalAbsorbed: 0,
+ totalOutflow: 0,
+ overflowNodeId: null,
+ })
+}
+
+/**
+ * Create a circular flow: A β B β C β A
+ * Flow circulates through the network
+ */
+export function createCircularFlowNetwork(): FlowNetwork {
+ const centerX = 350
+ const centerY = 250
+ const radius = 150
+
+ const nodes: FlowNode[] = [
+ {
+ id: 'alice',
+ name: 'Alice',
+ x: centerX + radius * Math.cos(0) - 60,
+ y: centerY + radius * Math.sin(0) - 50,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 30,
+ inflow: 50,
+ absorbed: 0,
+ outflow: 0,
+ status: 'healthy',
+ externalFlow: 50,
+ isOverflowSink: false,
+ },
+ {
+ id: 'bob',
+ name: 'Bob',
+ x: centerX + radius * Math.cos((2 * Math.PI) / 3) - 60,
+ y: centerY + radius * Math.sin((2 * Math.PI) / 3) - 50,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 30,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ {
+ id: 'carol',
+ name: 'Carol',
+ x: centerX + radius * Math.cos((4 * Math.PI) / 3) - 60,
+ y: centerY + radius * Math.sin((4 * Math.PI) / 3) - 50,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 30,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ ]
+
+ const allocations = [
+ { id: 'alloc_1', sourceNodeId: 'alice', targetNodeId: 'bob', percentage: 1.0 },
+ { id: 'alloc_2', sourceNodeId: 'bob', targetNodeId: 'carol', percentage: 1.0 },
+ { id: 'alloc_3', sourceNodeId: 'carol', targetNodeId: 'alice', percentage: 1.0 },
+ ]
+
+ return calculateFlowNetworkTotals({
+ name: 'Circular Flow (A β B β C β A)',
+ nodes: nodes.map(updateFlowNodeProperties),
+ allocations,
+ totalInflow: 0,
+ totalAbsorbed: 0,
+ totalOutflow: 0,
+ overflowNodeId: null,
+ })
+}
+
+/**
+ * Create an empty network for user to build
+ */
+export function createEmptyFlowNetwork(): FlowNetwork {
+ const nodes: FlowNode[] = [
+ {
+ id: 'node1',
+ name: 'Node 1',
+ x: 150,
+ y: 150,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 50,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ {
+ id: 'node2',
+ name: 'Node 2',
+ x: 350,
+ y: 150,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 50,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ {
+ id: 'node3',
+ name: 'Node 3',
+ x: 250,
+ y: 300,
+ width: 120,
+ height: 100,
+ minAbsorption: 10,
+ maxAbsorption: 50,
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'starved',
+ externalFlow: 0,
+ isOverflowSink: false,
+ },
+ ]
+
+ return calculateFlowNetworkTotals({
+ name: 'Empty Network (Set flows to begin)',
+ nodes: nodes.map(updateFlowNodeProperties),
+ allocations: [],
+ totalInflow: 0,
+ totalAbsorbed: 0,
+ totalOutflow: 0,
+ overflowNodeId: null,
+ })
+}
+
+export const flowSampleNetworks = {
+ linear: createLinearFlowNetwork(),
+ split: createSplitFlowNetwork(),
+ circular: createCircularFlowNetwork(),
+ empty: createEmptyFlowNetwork(),
+}
+
+export const flowNetworkOptions = [
+ { value: 'linear', label: 'Linear Flow (A β B β C)' },
+ { value: 'split', label: 'Split Flow (Projects + Commons)' },
+ { value: 'circular', label: 'Circular Flow (A β B β C)' },
+ { value: 'empty', label: 'Empty Network' },
+]
+
+export function getFlowSampleNetwork(key: keyof typeof flowSampleNetworks): FlowNetwork {
+ return flowSampleNetworks[key]
+}
diff --git a/lib/tbff-flow/types.ts b/lib/tbff-flow/types.ts
new file mode 100644
index 0000000..529430a
--- /dev/null
+++ b/lib/tbff-flow/types.ts
@@ -0,0 +1,90 @@
+/**
+ * Flow-based Flow Funding Types
+ *
+ * This model focuses on resource circulation rather than accumulation.
+ * Nodes receive flow, absorb what they need, and pass excess to others.
+ */
+
+export type FlowNodeStatus = 'starved' | 'minimum' | 'healthy' | 'saturated'
+
+/**
+ * A node in the flow network
+ * Flow enters, gets partially absorbed, and excess flows out
+ */
+export interface FlowNode {
+ id: string
+ name: string
+
+ // Position for rendering
+ x: number
+ y: number
+ width: number
+ height: number
+
+ // Flow thresholds (per time unit)
+ minAbsorption: number // Minimum flow needed to function
+ maxAbsorption: number // Maximum flow that can be absorbed
+
+ // Current flow state (computed)
+ inflow: number // Total flow entering this node
+ absorbed: number // Amount absorbed (between min and max)
+ outflow: number // Excess flow leaving this node
+ status: FlowNodeStatus // Derived from absorbed vs thresholds
+
+ // External flow input (set by user)
+ externalFlow: number // Flow injected into this node
+
+ // Special node type
+ isOverflowSink: boolean // True for the auto-created overflow node
+}
+
+/**
+ * Allocation defines how outflow is distributed
+ * Same as stock model but applied to outflow instead of overflow
+ */
+export interface FlowAllocation {
+ id: string
+ sourceNodeId: string
+ targetNodeId: string
+ percentage: number // 0.0 to 1.0
+}
+
+/**
+ * The complete flow network
+ */
+export interface FlowNetwork {
+ name: string
+ nodes: FlowNode[]
+ allocations: FlowAllocation[]
+
+ // Network-level computed properties
+ totalInflow: number // Sum of all external flows
+ totalAbsorbed: number // Sum of all absorbed flow
+ totalOutflow: number // Sum of all outflow
+
+ // Overflow sink
+ overflowNodeId: string | null // ID of auto-created overflow node
+}
+
+/**
+ * Flow particle for animation
+ * Moves along allocation arrows
+ */
+export interface FlowParticle {
+ id: string
+ allocationId: string // Which arrow it's traveling along
+ progress: number // 0.0 to 1.0 (position along arrow)
+ amount: number // Size/color intensity
+ speed: number // How fast it moves
+}
+
+/**
+ * Flow propagation result
+ * Shows how flow moved through the network
+ */
+export interface FlowPropagationResult {
+ network: FlowNetwork
+ iterations: number // How many steps to converge
+ converged: boolean // Did it reach steady state?
+ particles: FlowParticle[] // Active particles for animation
+}
diff --git a/lib/tbff-flow/utils.ts b/lib/tbff-flow/utils.ts
new file mode 100644
index 0000000..a9b93b9
--- /dev/null
+++ b/lib/tbff-flow/utils.ts
@@ -0,0 +1,176 @@
+/**
+ * Utility functions for flow-based calculations
+ */
+
+import type { FlowNode, FlowNodeStatus, FlowNetwork, FlowAllocation } from './types'
+
+/**
+ * Calculate node status based on absorbed flow vs thresholds
+ */
+export function getFlowNodeStatus(node: FlowNode): FlowNodeStatus {
+ if (node.absorbed < node.minAbsorption) return 'starved'
+ if (node.absorbed >= node.maxAbsorption) return 'saturated'
+ if (Math.abs(node.absorbed - node.minAbsorption) < 0.01) return 'minimum'
+ return 'healthy'
+}
+
+/**
+ * Calculate how much flow a node absorbs given inflow
+ * Absorbs between min and max thresholds
+ */
+export function calculateAbsorption(inflow: number, minAbsorption: number, maxAbsorption: number): number {
+ // Absorb as much as possible, up to max
+ return Math.min(inflow, maxAbsorption)
+}
+
+/**
+ * Calculate outflow (excess that couldn't be absorbed)
+ */
+export function calculateOutflow(inflow: number, absorbed: number): number {
+ return Math.max(0, inflow - absorbed)
+}
+
+/**
+ * Update all computed properties on a node
+ */
+export function updateFlowNodeProperties(node: FlowNode): FlowNode {
+ const absorbed = calculateAbsorption(node.inflow, node.minAbsorption, node.maxAbsorption)
+ const outflow = calculateOutflow(node.inflow, absorbed)
+ const status = getFlowNodeStatus({ ...node, absorbed })
+
+ return {
+ ...node,
+ absorbed,
+ outflow,
+ status,
+ }
+}
+
+/**
+ * Calculate network-level totals
+ */
+export function calculateFlowNetworkTotals(network: FlowNetwork): FlowNetwork {
+ const totalInflow = network.nodes.reduce((sum, node) => sum + node.externalFlow, 0)
+ const totalAbsorbed = network.nodes.reduce((sum, node) => sum + node.absorbed, 0)
+ const totalOutflow = network.nodes.reduce((sum, node) => sum + node.outflow, 0)
+
+ return {
+ ...network,
+ totalInflow,
+ totalAbsorbed,
+ totalOutflow,
+ }
+}
+
+/**
+ * Normalize allocations so they sum to 1.0
+ * Same as stock model
+ */
+export function normalizeFlowAllocations(allocations: FlowAllocation[]): FlowAllocation[] {
+ if (allocations.length === 1) {
+ return allocations.map(a => ({ ...a, percentage: 1.0 }))
+ }
+
+ const total = allocations.reduce((sum, a) => sum + a.percentage, 0)
+
+ if (total === 0) {
+ const equalShare = 1.0 / allocations.length
+ return allocations.map((a) => ({ ...a, percentage: equalShare }))
+ }
+
+ if (Math.abs(total - 1.0) < 0.0001) {
+ return allocations
+ }
+
+ return allocations.map((a) => ({
+ ...a,
+ percentage: a.percentage / total,
+ }))
+}
+
+/**
+ * Get center point of a node (for arrow endpoints)
+ */
+export function getFlowNodeCenter(node: FlowNode): { x: number; y: number } {
+ return {
+ x: node.x + node.width / 2,
+ y: node.y + node.height / 2,
+ }
+}
+
+/**
+ * Get status color for rendering
+ */
+export function getFlowStatusColor(status: FlowNodeStatus, alpha: number = 1): string {
+ const colors = {
+ starved: `rgba(239, 68, 68, ${alpha})`, // Red
+ minimum: `rgba(251, 191, 36, ${alpha})`, // Yellow
+ healthy: `rgba(99, 102, 241, ${alpha})`, // Blue
+ saturated: `rgba(16, 185, 129, ${alpha})`, // Green
+ }
+ return colors[status]
+}
+
+/**
+ * Get status color as Tailwind class
+ */
+export function getFlowStatusColorClass(status: FlowNodeStatus): string {
+ const classes = {
+ starved: 'text-red-400',
+ minimum: 'text-yellow-400',
+ healthy: 'text-blue-400',
+ saturated: 'text-green-400',
+ }
+ return classes[status]
+}
+
+/**
+ * Format flow rate for display
+ */
+export function formatFlow(rate: number): string {
+ return rate.toFixed(1)
+}
+
+/**
+ * Format percentage for display
+ */
+export function formatPercentage(decimal: number): string {
+ return `${Math.round(decimal * 100)}%`
+}
+
+/**
+ * Create the overflow sink node
+ */
+export function createOverflowNode(x: number, y: number): FlowNode {
+ return {
+ id: 'overflow-sink',
+ name: 'Overflow',
+ x,
+ y,
+ width: 120,
+ height: 80,
+ minAbsorption: 0, // Can absorb any amount
+ maxAbsorption: Infinity, // No limit
+ inflow: 0,
+ absorbed: 0,
+ outflow: 0,
+ status: 'healthy',
+ externalFlow: 0,
+ isOverflowSink: true,
+ }
+}
+
+/**
+ * Check if network needs an overflow node
+ * Returns true if any node has outflow with no allocations
+ */
+export function needsOverflowNode(network: FlowNetwork): boolean {
+ return network.nodes.some(node => {
+ if (node.isOverflowSink) return false
+
+ const hasOutflow = node.outflow > 0.01
+ const hasAllocations = network.allocations.some(a => a.sourceNodeId === node.id)
+
+ return hasOutflow && !hasAllocations
+ })
+}
diff --git a/package.json b/package.json
index 142e10e..c187d90 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
- "start": "next start"
+ "start": "next start",
+ "screenshots": "node scripts/capture-screenshots.mjs"
},
"dependencies": {
"@folkjs/propagators": "link:../folkjs/packages/propagators",
@@ -67,6 +68,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8.5",
+ "puppeteer": "^24.31.0",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
"typescript": "^5"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3dbf16c..934a8e7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -177,6 +177,9 @@ importers:
postcss:
specifier: ^8.5
version: 8.5.0
+ puppeteer:
+ specifier: ^24.31.0
+ version: 24.31.0(typescript@5.0.2)
tailwindcss:
specifier: ^4.1.9
version: 4.1.9
@@ -197,6 +200,14 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
@@ -421,6 +432,11 @@ packages:
cpu: [x64]
os: [win32]
+ '@puppeteer/browsers@2.10.13':
+ resolution: {integrity: sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@@ -1168,6 +1184,9 @@ packages:
'@tailwindcss/postcss@4.1.9':
resolution: {integrity: sha512-v3DKzHibZO8ioVDmuVHCW1PR0XSM7nS40EjZFJEA1xPuvTuQPaR5flE1LyikU3hu2u1KNWBtEaSe8qsQjX3tyg==}
+ '@tootallnate/quickjs-emscripten@0.23.0':
+ resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
+
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@@ -1204,6 +1223,9 @@ packages:
'@types/react@19.0.0':
resolution: {integrity: sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==}
+ '@types/yauzl@2.10.3':
+ resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
+
'@vercel/analytics@1.5.0':
resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
peerDependencies:
@@ -1230,10 +1252,29 @@ packages:
vue-router:
optional: true
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
+ ast-types@0.13.4:
+ resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
+ engines: {node: '>=4'}
+
autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
@@ -1241,15 +1282,72 @@ packages:
peerDependencies:
postcss: ^8.1.0
+ b4a@1.7.3:
+ resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
+ peerDependencies:
+ react-native-b4a: '*'
+ peerDependenciesMeta:
+ react-native-b4a:
+ optional: true
+
+ bare-events@2.8.2:
+ resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
+ peerDependencies:
+ bare-abort-controller: '*'
+ peerDependenciesMeta:
+ bare-abort-controller:
+ optional: true
+
+ bare-fs@4.5.1:
+ resolution: {integrity: sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==}
+ engines: {bare: '>=1.16.0'}
+ peerDependencies:
+ bare-buffer: '*'
+ peerDependenciesMeta:
+ bare-buffer:
+ optional: true
+
+ bare-os@3.6.2:
+ resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==}
+ engines: {bare: '>=1.14.0'}
+
+ bare-path@3.0.0:
+ resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==}
+
+ bare-stream@2.7.0:
+ resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==}
+ peerDependencies:
+ bare-buffer: '*'
+ bare-events: '*'
+ peerDependenciesMeta:
+ bare-buffer:
+ optional: true
+ bare-events:
+ optional: true
+
+ bare-url@2.3.2:
+ resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==}
+
baseline-browser-mapping@2.8.23:
resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==}
hasBin: true
+ basic-ftp@5.0.5:
+ resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
+ engines: {node: '>=10.0.0'}
+
browserslist@4.27.0:
resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ buffer-crc32@0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
caniuse-lite@1.0.30001752:
resolution: {integrity: sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==}
@@ -1257,12 +1355,21 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
+ chromium-bidi@11.0.0:
+ resolution: {integrity: sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==}
+ peerDependencies:
+ devtools-protocol: '*'
+
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1273,6 +1380,22 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ cosmiconfig@9.0.0:
+ resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -1320,15 +1443,32 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
+ data-uri-to-buffer@6.0.2:
+ resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
+ engines: {node: '>= 14'}
+
date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+ degenerator@5.0.1:
+ resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
+ engines: {node: '>= 14'}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1336,6 +1476,9 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ devtools-protocol@0.0.1521046:
+ resolution: {integrity: sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==}
+
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@@ -1355,31 +1498,100 @@ packages:
embla-carousel@8.5.1:
resolution: {integrity: sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==}
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ escodegen@2.1.0:
+ resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
+ engines: {node: '>=6.0'}
+ hasBin: true
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ events-universal@1.0.1:
+ resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
+
+ extract-zip@2.0.1:
+ resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
+ engines: {node: '>= 10.17.0'}
+ hasBin: true
+
fast-equals@5.3.2:
resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==}
engines: {node: '>=6.0.0'}
+ fast-fifo@1.3.2:
+ resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
+
+ fd-slicer@1.1.0:
+ resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
+
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
+ get-stream@5.2.0:
+ resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
+ engines: {node: '>=8'}
+
+ get-uri@6.0.5:
+ resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==}
+ engines: {node: '>= 14'}
+
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
input-otp@1.4.1:
resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==}
peerDependencies:
@@ -1390,6 +1602,17 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
+ ip-address@10.1.0:
+ resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
+ engines: {node: '>= 12'}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@@ -1397,6 +1620,13 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
lightningcss-darwin-arm64@1.30.1:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
@@ -1461,6 +1691,9 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -1468,6 +1701,10 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lru-cache@7.18.3:
+ resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
+ engines: {node: '>=12'}
+
lucide-react@0.454.0:
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
peerDependencies:
@@ -1484,11 +1721,21 @@ packages:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ netmask@2.0.2:
+ resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
+ engines: {node: '>= 0.4.0'}
+
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
@@ -1527,6 +1774,28 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ pac-proxy-agent@7.2.0:
+ resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
+ engines: {node: '>= 14'}
+
+ pac-resolver@7.0.1:
+ resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
+ engines: {node: '>= 14'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ pend@1.2.0:
+ resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1541,9 +1810,32 @@ packages:
resolution: {integrity: sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==}
engines: {node: ^10 || ^12 || >=14}
+ progress@2.0.3:
+ resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
+ engines: {node: '>=0.4.0'}
+
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ proxy-agent@6.5.0:
+ resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==}
+ engines: {node: '>= 14'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ pump@3.0.3:
+ resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
+
+ puppeteer-core@24.31.0:
+ resolution: {integrity: sha512-pnAohhSZipWQoFpXuGV7xCZfaGhqcBR9C4pVrU0QSrcMi7tQMH9J9lDBqBvyMAHQqe8HCARuREqFuVKRQOgTvg==}
+ engines: {node: '>=18'}
+
+ puppeteer@24.31.0:
+ resolution: {integrity: sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
react-day-picker@9.8.0:
resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==}
engines: {node: '>=18'}
@@ -1629,6 +1921,14 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -1641,6 +1941,18 @@ packages:
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+
+ socks-proxy-agent@8.0.5:
+ resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
+ engines: {node: '>= 14'}
+
+ socks@2.8.7:
+ resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
+ engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+
sonner@1.7.4:
resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
peerDependencies:
@@ -1651,6 +1963,21 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ streamx@2.23.0:
+ resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -1679,10 +2006,19 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ tar-fs@3.1.1:
+ resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==}
+
+ tar-stream@3.1.7:
+ resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
+
tar@7.5.2:
resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==}
engines: {node: '>=18'}
+ text-decoder@1.2.3:
+ resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
+
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -1692,6 +2028,9 @@ packages:
tw-animate-css@1.3.3:
resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==}
+ typed-query-selector@2.12.0:
+ resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==}
+
typescript@5.0.2:
resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==}
engines: {node: '>=12.20'}
@@ -1740,10 +2079,47 @@ packages:
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ webdriver-bidi-protocol@0.3.9:
+ resolution: {integrity: sha512-uIYvlRQ0PwtZR1EzHlTMol1G0lAlmOe6wPykF9a77AK3bkpvZHzIVxRE2ThOx5vjy2zISe0zhwf5rzuUfbo1PQ==}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ ws@8.18.3:
+ resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+ yauzl@2.10.0:
+ resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
+
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -1756,6 +2132,14 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
'@babel/runtime@7.28.4': {}
'@date-fns/tz@1.2.0': {}
@@ -1919,6 +2303,21 @@ snapshots:
'@next/swc-win32-x64-msvc@16.0.0':
optional: true
+ '@puppeteer/browsers@2.10.13':
+ dependencies:
+ debug: 4.4.3
+ extract-zip: 2.0.1
+ progress: 2.0.3
+ proxy-agent: 6.5.0
+ semver: 7.7.3
+ tar-fs: 3.1.1
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - react-native-b4a
+ - supports-color
+
'@radix-ui/number@1.1.0': {}
'@radix-ui/primitive@1.1.1': {}
@@ -2687,6 +3086,8 @@ snapshots:
postcss: 8.5.0
tailwindcss: 4.1.9
+ '@tootallnate/quickjs-emscripten@0.23.0': {}
+
'@types/d3-array@3.2.2': {}
'@types/d3-color@3.1.3': {}
@@ -2723,15 +3124,34 @@ snapshots:
dependencies:
csstype: 3.1.3
+ '@types/yauzl@2.10.3':
+ dependencies:
+ '@types/node': 22.0.0
+ optional: true
+
'@vercel/analytics@1.5.0(next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
optionalDependencies:
next: 16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
+ agent-base@7.1.4: {}
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
+ ast-types@0.13.4:
+ dependencies:
+ tslib: 2.8.1
+
autoprefixer@10.4.20(postcss@8.5.0):
dependencies:
browserslist: 4.27.0
@@ -2742,8 +3162,49 @@ snapshots:
postcss: 8.5.0
postcss-value-parser: 4.2.0
+ b4a@1.7.3: {}
+
+ bare-events@2.8.2: {}
+
+ bare-fs@4.5.1:
+ dependencies:
+ bare-events: 2.8.2
+ bare-path: 3.0.0
+ bare-stream: 2.7.0(bare-events@2.8.2)
+ bare-url: 2.3.2
+ fast-fifo: 1.3.2
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+ optional: true
+
+ bare-os@3.6.2:
+ optional: true
+
+ bare-path@3.0.0:
+ dependencies:
+ bare-os: 3.6.2
+ optional: true
+
+ bare-stream@2.7.0(bare-events@2.8.2):
+ dependencies:
+ streamx: 2.23.0
+ optionalDependencies:
+ bare-events: 2.8.2
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+ optional: true
+
+ bare-url@2.3.2:
+ dependencies:
+ bare-path: 3.0.0
+ optional: true
+
baseline-browser-mapping@2.8.23: {}
+ basic-ftp@5.0.5: {}
+
browserslist@4.27.0:
dependencies:
baseline-browser-mapping: 2.8.23
@@ -2752,16 +3213,32 @@ snapshots:
node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.27.0)
+ buffer-crc32@0.2.13: {}
+
+ callsites@3.1.0: {}
+
caniuse-lite@1.0.30001752: {}
chownr@3.0.0: {}
+ chromium-bidi@11.0.0(devtools-protocol@0.0.1521046):
+ dependencies:
+ devtools-protocol: 0.0.1521046
+ mitt: 3.0.1
+ zod: 3.25.76
+
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
client-only@0.0.1: {}
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
clsx@2.1.1: {}
cmdk@1.0.4(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
@@ -2776,6 +3253,21 @@ snapshots:
- '@types/react'
- '@types/react-dom'
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ cosmiconfig@9.0.0(typescript@5.0.2):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 5.0.2
+
csstype@3.1.3: {}
d3-array@3.2.4:
@@ -2816,16 +3308,30 @@ snapshots:
d3-timer@3.0.1: {}
+ data-uri-to-buffer@6.0.2: {}
+
date-fns-jalali@4.1.0-0: {}
date-fns@4.1.0: {}
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
decimal.js-light@2.5.1: {}
+ degenerator@5.0.1:
+ dependencies:
+ ast-types: 0.13.4
+ escodegen: 2.1.0
+ esprima: 4.0.1
+
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
+ devtools-protocol@0.0.1521046: {}
+
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.28.4
@@ -2845,23 +3351,104 @@ snapshots:
embla-carousel@8.5.1: {}
+ emoji-regex@8.0.0: {}
+
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
+ env-paths@2.2.1: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
escalade@3.2.0: {}
+ escodegen@2.1.0:
+ dependencies:
+ esprima: 4.0.1
+ estraverse: 5.3.0
+ esutils: 2.0.3
+ optionalDependencies:
+ source-map: 0.6.1
+
+ esprima@4.0.1: {}
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
eventemitter3@4.0.7: {}
+ events-universal@1.0.1:
+ dependencies:
+ bare-events: 2.8.2
+ transitivePeerDependencies:
+ - bare-abort-controller
+
+ extract-zip@2.0.1:
+ dependencies:
+ debug: 4.4.3
+ get-stream: 5.2.0
+ yauzl: 2.10.0
+ optionalDependencies:
+ '@types/yauzl': 2.10.3
+ transitivePeerDependencies:
+ - supports-color
+
fast-equals@5.3.2: {}
+ fast-fifo@1.3.2: {}
+
+ fd-slicer@1.1.0:
+ dependencies:
+ pend: 1.2.0
+
fraction.js@4.3.7: {}
+ get-caller-file@2.0.5: {}
+
get-nonce@1.0.1: {}
+ get-stream@5.2.0:
+ dependencies:
+ pump: 3.0.3
+
+ get-uri@6.0.5:
+ dependencies:
+ basic-ftp: 5.0.5
+ data-uri-to-buffer: 6.0.2
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
graceful-fs@4.2.11: {}
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
input-otp@1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
@@ -2869,10 +3456,22 @@ snapshots:
internmap@2.0.3: {}
+ ip-address@10.1.0: {}
+
+ is-arrayish@0.2.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
jiti@2.6.1: {}
js-tokens@4.0.0: {}
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ json-parse-even-better-errors@2.3.1: {}
+
lightningcss-darwin-arm64@1.30.1:
optional: true
@@ -2918,12 +3517,16 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1
+ lines-and-columns@1.2.4: {}
+
lodash@4.17.21: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
+ lru-cache@7.18.3: {}
+
lucide-react@0.454.0(react@19.2.0):
dependencies:
react: 19.2.0
@@ -2938,8 +3541,14 @@ snapshots:
dependencies:
minipass: 7.1.2
+ mitt@3.0.1: {}
+
+ ms@2.1.3: {}
+
nanoid@3.3.11: {}
+ netmask@2.0.2: {}
+
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
@@ -2974,6 +3583,41 @@ snapshots:
object-assign@4.1.1: {}
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ pac-proxy-agent@7.2.0:
+ dependencies:
+ '@tootallnate/quickjs-emscripten': 0.23.0
+ agent-base: 7.1.4
+ debug: 4.4.3
+ get-uri: 6.0.5
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ pac-resolver: 7.0.1
+ socks-proxy-agent: 8.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ pac-resolver@7.0.1:
+ dependencies:
+ degenerator: 5.0.1
+ netmask: 2.0.2
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ pend@1.2.0: {}
+
picocolors@1.1.1: {}
postcss-value-parser@4.2.0: {}
@@ -2990,12 +3634,68 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ progress@2.0.3: {}
+
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
+ proxy-agent@6.5.0:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ lru-cache: 7.18.3
+ pac-proxy-agent: 7.2.0
+ proxy-from-env: 1.1.0
+ socks-proxy-agent: 8.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ proxy-from-env@1.1.0: {}
+
+ pump@3.0.3:
+ dependencies:
+ end-of-stream: 1.4.5
+ once: 1.4.0
+
+ puppeteer-core@24.31.0:
+ dependencies:
+ '@puppeteer/browsers': 2.10.13
+ chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046)
+ debug: 4.4.3
+ devtools-protocol: 0.0.1521046
+ typed-query-selector: 2.12.0
+ webdriver-bidi-protocol: 0.3.9
+ ws: 8.18.3
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - bufferutil
+ - react-native-b4a
+ - supports-color
+ - utf-8-validate
+
+ puppeteer@24.31.0(typescript@5.0.2):
+ dependencies:
+ '@puppeteer/browsers': 2.10.13
+ chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046)
+ cosmiconfig: 9.0.0(typescript@5.0.2)
+ devtools-protocol: 0.0.1521046
+ puppeteer-core: 24.31.0
+ typed-query-selector: 2.12.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - bufferutil
+ - react-native-b4a
+ - supports-color
+ - typescript
+ - utf-8-validate
+
react-day-picker@9.8.0(react@19.2.0):
dependencies:
'@date-fns/tz': 1.2.0
@@ -3084,10 +3784,13 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
+ require-directory@2.1.1: {}
+
+ resolve-from@4.0.0: {}
+
scheduler@0.27.0: {}
- semver@7.7.3:
- optional: true
+ semver@7.7.3: {}
sharp@0.34.4:
dependencies:
@@ -3119,6 +3822,21 @@ snapshots:
'@img/sharp-win32-x64': 0.34.4
optional: true
+ smart-buffer@4.2.0: {}
+
+ socks-proxy-agent@8.0.5:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ socks: 2.8.7
+ transitivePeerDependencies:
+ - supports-color
+
+ socks@2.8.7:
+ dependencies:
+ ip-address: 10.1.0
+ smart-buffer: 4.2.0
+
sonner@1.7.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
@@ -3126,6 +3844,28 @@ snapshots:
source-map-js@1.2.1: {}
+ source-map@0.6.1:
+ optional: true
+
+ streamx@2.23.0:
+ dependencies:
+ events-universal: 1.0.1
+ fast-fifo: 1.3.2
+ text-decoder: 1.2.3
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
styled-jsx@5.1.6(react@19.2.0):
dependencies:
client-only: 0.0.1
@@ -3141,6 +3881,27 @@ snapshots:
tapable@2.3.0: {}
+ tar-fs@3.1.1:
+ dependencies:
+ pump: 3.0.3
+ tar-stream: 3.1.7
+ optionalDependencies:
+ bare-fs: 4.5.1
+ bare-path: 3.0.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - bare-buffer
+ - react-native-b4a
+
+ tar-stream@3.1.7:
+ dependencies:
+ b4a: 1.7.3
+ fast-fifo: 1.3.2
+ streamx: 2.23.0
+ transitivePeerDependencies:
+ - bare-abort-controller
+ - react-native-b4a
+
tar@7.5.2:
dependencies:
'@isaacs/fs-minipass': 4.0.1
@@ -3149,12 +3910,20 @@ snapshots:
minizlib: 3.1.0
yallist: 5.0.0
+ text-decoder@1.2.3:
+ dependencies:
+ b4a: 1.7.3
+ transitivePeerDependencies:
+ - react-native-b4a
+
tiny-invariant@1.3.3: {}
tslib@2.8.1: {}
tw-animate-css@1.3.3: {}
+ typed-query-selector@2.12.0: {}
+
typescript@5.0.2: {}
undici-types@6.11.1: {}
@@ -3210,6 +3979,37 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
+ webdriver-bidi-protocol@0.3.9: {}
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrappy@1.0.2: {}
+
+ ws@8.18.3: {}
+
+ y18n@5.0.8: {}
+
yallist@5.0.0: {}
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
+ yauzl@2.10.0:
+ dependencies:
+ buffer-crc32: 0.2.13
+ fd-slicer: 1.1.0
+
zod@3.25.76: {}
diff --git a/public/screenshots/flow-v2.png b/public/screenshots/flow-v2.png
new file mode 100644
index 0000000000000000000000000000000000000000..0741642109147fde9c4a276d9d3f62becc760f4a
GIT binary patch
literal 86581
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 |