feat: inline config panel for rFlows elements with tabbed Config/Analytics/Alloc

Double-clicking a flow element now opens a rich tabbed panel below the node:
- Config tab: form inputs (label, thresholds with range sliders, source type, status)
- Analytics tab: live-updating fill bars, conic-gradient outflow/overflow donut, stats
- Allocations tab: read-only allocation breakdowns with colored dots
- Simulation overlay preserved during ticks, analytics accumulate per-node metrics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-04 18:03:09 -08:00
parent 3b3eecdddb
commit a90e339323
8 changed files with 1140 additions and 308 deletions

View File

@ -0,0 +1,49 @@
---
id: TASK-102
title: 'rFunds → rFlows: Full module rename, mobile touch support & CSS alignment'
status: Done
assignee: []
created_date: '2026-03-04 03:19'
labels:
- rflows
- refactor
- mobile
- css
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Complete rename of the rfunds module to rflows across the entire rspace-online codebase, plus mobile touch support and CSS formatting alignment.
**Rename scope:** Directory, files, classes, custom elements, CSS classes, schemas, module registration, cross-codebase references (32+ files), Vite build config, Docker/Traefik configs, domain references (rfunds.online → rflows.online).
**Mobile touch:** Two-finger pinch-to-zoom and pan on the flow diagram SVG canvas, following the canvas.html gesture pattern. Includes center-point zoom, drag threshold, and touch-action: none.
**CSS alignment:** Renamed .funds-* → .flows-* class prefixes, aligned with rSpace dark theme conventions (slate palette, 8px border-radius, system-ui font, thin scrollbar, 44px mobile tap targets).
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 All rfunds references renamed to rflows across 32+ files
- [ ] #2 Vite build produces dist/modules/rflows/ with folk-flows-app.js, folk-flow-river.js, flows.css
- [ ] #3 Module loads at /{space}/rflows with correct shell and module switcher
- [ ] #4 Canvas embed button shows rFlows and uses moduleId rflows
- [ ] #5 Two-finger pinch-to-zoom on flow diagram SVG works with center-point zoom
- [ ] #6 Two-finger pan on flow diagram works
- [ ] #7 Single-touch node drag works with 5px threshold
- [ ] #8 CSS uses .flows-* prefix, 8px border-radius, system-ui font, thin scrollbar
- [ ] #9 Mobile toolbar collapses at 768px with 44px tap targets
- [ ] #10 Docker/Traefik configs updated for rflows.online domain
- [ ] #11 EncryptID CORS updated for rflows.online
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Completed full rfunds → rflows rename across the entire rspace-online codebase (32+ files). Added mobile two-finger pinch-to-zoom and pan to the flow diagram SVG canvas following canvas.html's gesture pattern. Aligned CSS with rSpace dark theme conventions. Build verified — dist/modules/rflows/ outputs correctly. Deployed to production successfully.
Commit: a6008a4 refactor: complete rfunds → rflows rename across configs and references
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@ -0,0 +1,48 @@
---
id: TASK-103
title: 'rFlows: Green flows, funnel coloring, full-page canvas, analytics popout'
status: Done
assignee: []
created_date: '2026-03-04 05:06'
labels:
- rFlows
- UX
- frontend
dependencies: []
references:
- modules/rflows/components/folk-flows-app.ts
- modules/rflows/components/flows.css
- modules/rflows/lib/presets.ts
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Overhaul the rFlows detail view UX:
1. **All flow edges → green shades** — Source inflow (#10b981), spending (#34d399), overflow (#6ee7b7). Ignores alloc.color for consistent visual language.
2. **Funnel 3-state coloring** — Critical (red, below min), Sustained (amber, between thresholds), Overflow (green, above max). Replaces previous 4-way sufficient/abundant/seeking/critical logic. Glow matches state color.
3. **Full-page canvas** — Removed tab system (Diagram/River/Table/Transactions). Canvas fills 100vh. Nav overlay (back link + title) floats inside canvas container.
4. **Analytics popout** — Left-side slide-in panel with Overview + Transactions sub-tabs. Toggled via toolbar button or Escape key. Reuses existing table/transaction renderers.
5. **Removed river tab** — Deleted renderRiverTab() and mountRiver(). folk-flow-river.ts kept on disk.
6. **Updated legend** — Edge colors as squares, funnel states as circles (Critical/Sustained/Thriving).
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Canvas fills entire viewport (no tab bar, no max-width constraint)
- [ ] #2 All flow edges are shades of green
- [ ] #3 Funnels: red when critical, amber when sustained, green when overflow
- [ ] #4 Analytics button in toolbar opens left-side panel with overview + transactions
- [ ] #5 Back button in nav overlay returns to landing page
- [ ] #6 Simulation and wiring still work
- [ ] #7 Editor panel (right side) still opens for node editing
- [ ] #8 Escape closes analytics panel
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Committed 676aaa7 on dev, merged to main. Two files changed: folk-flows-app.ts (199 line diff — removed tab system, added analytics panel, updated edge/funnel color logic) and flows.css (68 line diff — fullpage layout, nav overlay, analytics panel styles). TypeScript compiles clean.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@ -1,5 +1,75 @@
/* ── Flows module theme ───────────────────────────────── */ /* ── Flows module theme ───────────────────────────────── */
/* ── rFlows color tokens (dark defaults) ─────────────── */
:root {
/* Source node */
--rflows-source-bg: #064e3b;
--rflows-source-border: #10b981;
--rflows-source-rate: #6ee7b7;
/* Edge colors */
--rflows-edge-inflow: #10b981;
--rflows-edge-spending: #34d399;
--rflows-edge-overflow: #6ee7b7;
/* Funnel zones */
--rflows-zone-drain: #ef4444;
--rflows-zone-drain-opacity: 0.08;
--rflows-zone-healthy: #0ea5e9;
--rflows-zone-healthy-opacity: 0.06;
--rflows-zone-overflow: #f59e0b;
--rflows-zone-overflow-opacity: 0.06;
--rflows-fill-opacity: 0.25;
/* Funnel labels */
--rflows-label-inflow: #10b981;
--rflows-label-spending: #34d399;
--rflows-label-overflow: #6ee7b7;
/* Status colors */
--rflows-status-critical: #ef4444;
--rflows-status-sustained: #f59e0b;
--rflows-status-overflow: #10b981;
--rflows-status-thriving: #10b981;
--rflows-sat-bar: #10b981;
--rflows-sat-border: #fbbf24;
/* Outcome / progress */
--rflows-status-completed: #10b981;
--rflows-status-blocked: #ef4444;
--rflows-status-inprogress: #3b82f6;
--rflows-status-notstarted: #64748b;
--rflows-phase-unlocked: #10b981;
/* Score badge */
--rflows-score-gold: #fbbf24;
--rflows-score-green: #10b981;
--rflows-score-amber: #f59e0b;
--rflows-score-red: #ef4444;
/* Card value */
--rflows-card-value: #0ea5e9;
/* Selection */
--rflows-selected: #6366f1;
/* Inline edit buttons */
--rflows-btn-done: #10b981;
--rflows-btn-delete: #ef4444;
--rflows-btn-fund: #6366f1;
--rflows-btn-save: #10b981;
/* Sufficiency tooltip highlight */
--rflows-sufficiency-highlight: #fbbf24;
/* Edge drag handle */
--rflows-drag-handle-fill: #475569;
--rflows-drag-handle-stroke: #94a3b8;
/* Modal border accent */
--rflows-modal-border: #334155;
}
/* ── Base ────────────────────────────────────────────── */ /* ── Base ────────────────────────────────────────────── */
.flows-landing, .flows-detail { .flows-landing, .flows-detail {
font-family: system-ui, -apple-system, sans-serif; font-family: system-ui, -apple-system, sans-serif;
@ -54,7 +124,7 @@
} }
.flows-flow-card:hover { border-color: var(--rs-primary-hover); transform: translateY(-1px); } .flows-flow-card:hover { border-color: var(--rs-primary-hover); transform: translateY(-1px); }
.flows-flow-card__name { font-size: 15px; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 4px; } .flows-flow-card__name { font-size: 15px; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 4px; }
.flows-flow-card__value { font-size: 20px; font-weight: 700; color: #0ea5e9; margin-bottom: 4px; } .flows-flow-card__value { font-size: 20px; font-weight: 700; color: var(--rflows-card-value, #0ea5e9); margin-bottom: 4px; }
.flows-flow-card__meta { font-size: 12px; color: var(--rs-text-muted); } .flows-flow-card__meta { font-size: 12px; color: var(--rs-text-muted); }
/* About / how-it-works section */ /* About / how-it-works section */
@ -396,8 +466,8 @@
/* Drag handle on edge midpoint */ /* Drag handle on edge midpoint */
.edge-drag-handle { .edge-drag-handle {
fill: #475569; fill: var(--rflows-drag-handle-fill, #475569);
stroke: #94a3b8; stroke: var(--rflows-drag-handle-stroke, #94a3b8);
stroke-width: 1.5; stroke-width: 1.5;
cursor: grab; cursor: grab;
transition: fill 0.15s; transition: fill 0.15s;
@ -534,14 +604,174 @@
/* Inline edit overlay container */ /* Inline edit overlay container */
.inline-edit-overlay { pointer-events: all; } .inline-edit-overlay { pointer-events: all; }
/* Funnel overflow lip glow when overflowing */ /* Funnel overflow pipe (vessel metaphor) */
.funnel-lip { transition: fill 0.3s, opacity 0.3s; } .funnel-pipe { transition: fill 0.3s, height 0.3s, y 0.3s; }
.funnel-lip--active { fill: #10b981; opacity: 0.8; } .funnel-pipe--active { fill: #10b981; }
/* Threshold lines inside tank */
.threshold-line { pointer-events: none; }
/* Basin fill animation */
.basin-fill-rect { transition: y 120ms ease-out, height 120ms ease-out; }
/* Status badge in outcome inline edit */ /* Status badge in outcome inline edit */
.inline-status-badge { cursor: pointer; transition: opacity 0.15s; } .inline-status-badge { cursor: pointer; transition: opacity 0.15s; }
.inline-status-badge:hover { opacity: 0.8; } .inline-status-badge:hover { opacity: 0.8; }
/* ── Inline config panel (ICP) ─────────────────────── */
.inline-config-panel {
display: flex; flex-direction: column;
background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
border-radius: 10px; box-shadow: 0 4px 24px rgba(0,0,0,0.35);
overflow: hidden; font-family: system-ui, -apple-system, sans-serif;
color: var(--rs-text-primary); font-size: 12px;
}
.icp-tabs {
display: flex; border-bottom: 1px solid var(--rs-border-strong);
background: var(--rs-bg-surface-sunken);
}
.icp-tab {
flex: 1; padding: 6px 8px; text-align: center;
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
color: var(--rs-text-muted); cursor: pointer; border: none; background: none;
border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s;
}
.icp-tab:hover { color: var(--rs-text-secondary); }
.icp-tab--active {
color: var(--rs-primary); border-bottom-color: var(--rs-primary);
}
.icp-body {
flex: 1; overflow-y: auto; padding: 8px 10px;
max-height: 180px; min-height: 60px;
}
.icp-body::-webkit-scrollbar { width: 4px; }
.icp-body::-webkit-scrollbar-thumb { background: var(--rs-border-strong); border-radius: 2px; }
.icp-toolbar {
display: flex; gap: 4px; justify-content: center;
padding: 6px 10px; border-top: 1px solid var(--rs-border-strong);
background: var(--rs-bg-surface-sunken);
}
.icp-toolbar button {
padding: 4px 10px; border-radius: 5px; border: none;
font-size: 10px; cursor: pointer; font-weight: 600;
font-family: system-ui, -apple-system, sans-serif;
transition: opacity 0.15s;
}
.icp-toolbar button:hover { opacity: 0.85; }
/* Form fields */
.icp-field { margin-bottom: 6px; }
.icp-label {
display: block; font-size: 10px; font-weight: 600;
color: var(--rs-text-muted); text-transform: uppercase; letter-spacing: 0.03em;
margin-bottom: 2px;
}
.icp-input, .icp-select {
width: 100%; padding: 4px 6px; border-radius: 4px;
border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken);
color: var(--rs-text-primary); font-size: 12px;
font-family: system-ui, -apple-system, sans-serif;
outline: none; box-sizing: border-box;
}
.icp-input:focus, .icp-select:focus { border-color: var(--rs-primary); }
/* Range sliders */
.icp-range-group {
display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
}
.icp-range-label {
font-size: 10px; font-weight: 600; color: var(--rs-text-muted);
min-width: 28px; text-transform: uppercase;
}
.icp-range {
flex: 1; height: 4px; cursor: pointer;
accent-color: var(--rs-primary);
-webkit-appearance: none; appearance: none;
background: var(--rs-border-strong); border-radius: 2px;
}
.icp-range::-webkit-slider-thumb {
-webkit-appearance: none; width: 12px; height: 12px;
border-radius: 50%; background: var(--rs-primary); cursor: pointer;
}
.icp-range-value {
font-size: 11px; font-weight: 600; color: var(--rs-text-primary);
min-width: 40px; text-align: right;
}
/* Analytics bars */
.icp-analytics-row {
margin-bottom: 8px;
}
.icp-analytics-label {
display: flex; justify-content: space-between; font-size: 10px;
color: var(--rs-text-muted); margin-bottom: 2px;
}
.icp-analytics-bar {
height: 6px; background: var(--rs-border-strong); border-radius: 3px;
overflow: hidden;
}
.icp-analytics-fill {
height: 100%; border-radius: 3px; transition: width 120ms ease-out;
}
/* Conic-gradient donut for proportions */
.icp-proportion {
display: flex; align-items: center; gap: 10px; margin-bottom: 8px;
}
.icp-proportion-ring {
width: 48px; height: 48px; border-radius: 50%; flex-shrink: 0;
}
.icp-proportion-legend {
display: flex; flex-direction: column; gap: 3px; font-size: 10px;
}
.icp-proportion-item {
display: flex; align-items: center; gap: 4px;
}
.icp-proportion-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
/* Stat pairs */
.icp-stat-row {
display: flex; justify-content: space-between; font-size: 11px;
padding: 2px 0; color: var(--rs-text-secondary);
}
.icp-stat-value { font-weight: 600; color: var(--rs-text-primary); }
/* Allocation rows */
.icp-alloc-row {
display: flex; align-items: center; gap: 6px; padding: 3px 0;
font-size: 11px; color: var(--rs-text-secondary);
}
.icp-alloc-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
/* Fund Now button */
.icp-fund-btn {
width: 100%; padding: 6px; margin-top: 6px;
background: var(--rflows-btn-fund); color: white; border: none;
border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 11px;
font-family: system-ui, -apple-system, sans-serif;
transition: opacity 0.15s;
}
.icp-fund-btn:hover { opacity: 0.85; }
/* Sufficiency state label */
.icp-suf-badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 10px; font-weight: 600; text-transform: uppercase;
}
.icp-suf-badge--seeking { background: rgba(59,130,246,0.15); color: #3b82f6; }
.icp-suf-badge--sufficient { background: rgba(16,185,129,0.15); color: #10b981; }
.icp-suf-badge--abundant { background: rgba(245,158,11,0.15); color: #f59e0b; }
/* Empty state */
.icp-empty {
text-align: center; color: var(--rs-text-muted);
padding: 12px 8px; font-size: 11px; font-style: italic;
}
/* Side port arrows */ /* Side port arrows */
.port-group[data-port-side="left"] .port-arrow { /* horizontal arrow left handled inline */ } .port-group[data-port-side="left"] .port-arrow { /* horizontal arrow left handled inline */ }
.port-group[data-port-side="right"] .port-arrow { /* horizontal arrow right handled inline */ } .port-group[data-port-side="right"] .port-arrow { /* horizontal arrow right handled inline */ }
@ -594,6 +824,49 @@
--rflows-outcome-hover-bg: #fce7f3; --rflows-outcome-hover-bg: #fce7f3;
--rflows-danger-text: #dc2626; --rflows-danger-text: #dc2626;
--rflows-danger-hover-bg: #fee2e2; --rflows-danger-hover-bg: #fee2e2;
/* Source node */
--rflows-source-bg: #d1fae5;
--rflows-source-border: #059669;
--rflows-source-rate: #047857;
/* Edge colors */
--rflows-edge-inflow: #059669;
--rflows-edge-spending: #047857;
--rflows-edge-overflow: #059669;
/* Funnel zones */
--rflows-zone-drain-opacity: 0.15;
--rflows-zone-healthy-opacity: 0.12;
--rflows-zone-overflow-opacity: 0.12;
--rflows-fill-opacity: 0.35;
/* Funnel labels */
--rflows-label-inflow: #047857;
--rflows-label-spending: #047857;
--rflows-label-overflow: #059669;
/* Status colors (darken for light bg) */
--rflows-status-overflow: #059669;
--rflows-status-thriving: #059669;
--rflows-sat-bar: #059669;
/* Outcome */
--rflows-status-completed: #059669;
--rflows-phase-unlocked: #059669;
/* Card value */
--rflows-card-value: #0369a1;
/* Score badge */
--rflows-score-green: #059669;
/* Edge drag handle */
--rflows-drag-handle-fill: #94a3b8;
--rflows-drag-handle-stroke: #64748b;
/* Modal border accent */
--rflows-modal-border: #e2e8f0;
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root:not([data-theme]) { :root:not([data-theme]) {
@ -605,6 +878,37 @@
--rflows-outcome-hover-bg: #fce7f3; --rflows-outcome-hover-bg: #fce7f3;
--rflows-danger-text: #dc2626; --rflows-danger-text: #dc2626;
--rflows-danger-hover-bg: #fee2e2; --rflows-danger-hover-bg: #fee2e2;
--rflows-source-bg: #d1fae5;
--rflows-source-border: #059669;
--rflows-source-rate: #047857;
--rflows-edge-inflow: #059669;
--rflows-edge-spending: #047857;
--rflows-edge-overflow: #059669;
--rflows-zone-drain-opacity: 0.15;
--rflows-zone-healthy-opacity: 0.12;
--rflows-zone-overflow-opacity: 0.12;
--rflows-fill-opacity: 0.35;
--rflows-label-inflow: #047857;
--rflows-label-spending: #047857;
--rflows-label-overflow: #059669;
--rflows-status-overflow: #059669;
--rflows-status-thriving: #059669;
--rflows-sat-bar: #059669;
--rflows-status-completed: #059669;
--rflows-phase-unlocked: #059669;
--rflows-card-value: #0369a1;
--rflows-score-green: #059669;
--rflows-drag-handle-fill: #94a3b8;
--rflows-drag-handle-stroke: #64748b;
--rflows-modal-border: #e2e8f0;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,9 @@ export const demoNodes: FlowNode[] = [
{ {
id: "treasury", type: "funnel", position: { x: 630, y: 0 }, id: "treasury", type: "funnel", position: { x: 630, y: 0 },
data: { data: {
label: "Treasury", currentValue: 85000, minThreshold: 20000, maxThreshold: 70000, label: "Treasury", currentValue: 85000, desiredOutflow: 10000,
maxCapacity: 100000, inflowRate: 1000, sufficientThreshold: 60000, dynamicOverflow: true, minThreshold: 10000, sufficientThreshold: 30000, maxThreshold: 60000,
maxCapacity: 90000, inflowRate: 1000, dynamicOverflow: true,
overflowAllocations: [ overflowAllocations: [
{ targetId: "public-goods", percentage: 40, color: OVERFLOW_COLORS[0] }, { targetId: "public-goods", percentage: 40, color: OVERFLOW_COLORS[0] },
{ targetId: "research", percentage: 35, color: OVERFLOW_COLORS[1] }, { targetId: "research", percentage: 35, color: OVERFLOW_COLORS[1] },
@ -33,8 +34,9 @@ export const demoNodes: FlowNode[] = [
{ {
id: "public-goods", type: "funnel", position: { x: 170, y: 450 }, id: "public-goods", type: "funnel", position: { x: 170, y: 450 },
data: { data: {
label: "Public Goods", currentValue: 45000, minThreshold: 15000, maxThreshold: 50000, label: "Public Goods", currentValue: 45000, desiredOutflow: 7000,
maxCapacity: 70000, inflowRate: 400, sufficientThreshold: 42000, minThreshold: 7000, sufficientThreshold: 21000, maxThreshold: 42000,
maxCapacity: 63000, inflowRate: 400,
overflowAllocations: [], overflowAllocations: [],
spendingAllocations: [ spendingAllocations: [
{ targetId: "pg-infra", percentage: 50, color: SPENDING_COLORS[0] }, { targetId: "pg-infra", percentage: 50, color: SPENDING_COLORS[0] },
@ -46,8 +48,9 @@ export const demoNodes: FlowNode[] = [
{ {
id: "research", type: "funnel", position: { x: 975, y: 450 }, id: "research", type: "funnel", position: { x: 975, y: 450 },
data: { data: {
label: "Research", currentValue: 28000, minThreshold: 20000, maxThreshold: 45000, label: "Research", currentValue: 28000, desiredOutflow: 5000,
maxCapacity: 60000, inflowRate: 350, sufficientThreshold: 38000, minThreshold: 5000, sufficientThreshold: 15000, maxThreshold: 30000,
maxCapacity: 45000, inflowRate: 350,
overflowAllocations: [], overflowAllocations: [],
spendingAllocations: [ spendingAllocations: [
{ targetId: "research-grants", percentage: 70, color: SPENDING_COLORS[0] }, { targetId: "research-grants", percentage: 70, color: SPENDING_COLORS[0] },
@ -58,8 +61,9 @@ export const demoNodes: FlowNode[] = [
{ {
id: "emergency", type: "funnel", position: { x: 1320, y: 450 }, id: "emergency", type: "funnel", position: { x: 1320, y: 450 },
data: { data: {
label: "Emergency", currentValue: 12000, minThreshold: 25000, maxThreshold: 60000, label: "Emergency", currentValue: 12000, desiredOutflow: 8000,
maxCapacity: 80000, inflowRate: 250, sufficientThreshold: 50000, minThreshold: 8000, sufficientThreshold: 24000, maxThreshold: 48000,
maxCapacity: 72000, inflowRate: 250,
overflowAllocations: [], overflowAllocations: [],
spendingAllocations: [ spendingAllocations: [
{ targetId: "emergency-response", percentage: 100, color: SPENDING_COLORS[0] }, { targetId: "emergency-response", percentage: 100, color: SPENDING_COLORS[0] },

View File

@ -4,6 +4,7 @@
*/ */
import type { FlowNode, FunnelNodeData, OutcomeNodeData, SufficiencyState } from "./types"; import type { FlowNode, FunnelNodeData, OutcomeNodeData, SufficiencyState } from "./types";
import { deriveThresholds } from "./types";
export interface SimulationConfig { export interface SimulationConfig {
tickDivisor: number; tickDivisor: number;
@ -102,6 +103,15 @@ export function simulateTick(
const src = node.data as FunnelNodeData; const src = node.data as FunnelNodeData;
const data: FunnelNodeData = { ...src }; const data: FunnelNodeData = { ...src };
// Auto-derive thresholds from desiredOutflow when present
if (data.desiredOutflow) {
const derived = deriveThresholds(data.desiredOutflow);
data.minThreshold = derived.minThreshold;
data.sufficientThreshold = derived.sufficientThreshold;
data.maxThreshold = derived.maxThreshold;
data.maxCapacity = derived.maxCapacity;
}
let value = data.currentValue + data.inflowRate / tickDivisor; let value = data.currentValue + data.inflowRate / tickDivisor;
value += overflowIncoming.get(node.id) ?? 0; value += overflowIncoming.get(node.id) ?? 0;
value = Math.min(value, data.maxCapacity); value = Math.min(value, data.maxCapacity);
@ -136,7 +146,8 @@ export function simulateTick(
rateMultiplier = spendingRateCritical; rateMultiplier = spendingRateCritical;
} }
let drain = (data.inflowRate / tickDivisor) * rateMultiplier; const baseRate = data.desiredOutflow || data.inflowRate;
let drain = (baseRate / tickDivisor) * rateMultiplier;
drain = Math.min(drain, value); drain = Math.min(drain, value);
value -= drain; value -= drain;

View File

@ -46,6 +46,7 @@ export interface FunnelNodeData {
maxThreshold: number; maxThreshold: number;
maxCapacity: number; maxCapacity: number;
inflowRate: number; inflowRate: number;
desiredOutflow?: number;
sufficientThreshold?: number; sufficientThreshold?: number;
dynamicOverflow?: boolean; dynamicOverflow?: boolean;
overflowAllocations: OverflowAllocation[]; overflowAllocations: OverflowAllocation[];
@ -54,6 +55,15 @@ export interface FunnelNodeData {
[key: string]: unknown; [key: string]: unknown;
} }
export function deriveThresholds(desiredOutflow: number) {
return {
minThreshold: desiredOutflow * 1, // 1 month runway
sufficientThreshold: desiredOutflow * 3, // 3 months runway
maxThreshold: desiredOutflow * 6, // overflow point
maxCapacity: desiredOutflow * 9, // visual max
};
}
export interface PhaseTask { export interface PhaseTask {
label: string; label: string;
completed: boolean; completed: boolean;
@ -123,8 +133,8 @@ export const PORT_DEFS: Record<FlowNode["type"], PortDefinition[]> = {
], ],
funnel: [ funnel: [
{ kind: "inflow", dir: "in", xFrac: 0.5, yFrac: 0, color: "#60a5fa" }, { kind: "inflow", dir: "in", xFrac: 0.5, yFrac: 0, color: "#60a5fa" },
{ kind: "overflow", dir: "out", xFrac: 0.0, yFrac: 0.15, color: "#f59e0b", connectsTo: ["inflow"], side: "left" }, { kind: "overflow", dir: "out", xFrac: 0.0, yFrac: 0.55, color: "#f59e0b", connectsTo: ["inflow"], side: "left" },
{ kind: "overflow", dir: "out", xFrac: 1.0, yFrac: 0.15, color: "#f59e0b", connectsTo: ["inflow"], side: "right" }, { kind: "overflow", dir: "out", xFrac: 1.0, yFrac: 0.55, color: "#f59e0b", connectsTo: ["inflow"], side: "right" },
{ kind: "spending", dir: "out", xFrac: 0.5, yFrac: 1, color: "#8b5cf6", connectsTo: ["inflow"] }, { kind: "spending", dir: "out", xFrac: 0.5, yFrac: 1, color: "#8b5cf6", connectsTo: ["inflow"] },
], ],
outcome: [ outcome: [

View File

@ -1,14 +1,14 @@
/* rSplat — Gaussian Splat Viewer */ /* rSplat — Gaussian Splat Viewer */
:root { :root {
--splat-bg: #0f172a; --splat-bg: var(--rs-bg-page, #0f172a);
--splat-surface: #1e293b; --splat-surface: var(--rs-bg-surface, #1e293b);
--splat-border: #334155; --splat-border: var(--rs-border-strong, #334155);
--splat-text: #e2e8f0; --splat-text: var(--rs-text-primary, #e2e8f0);
--splat-text-muted: #94a3b8; --splat-text-muted: var(--rs-text-muted, #94a3b8);
--splat-accent: #818cf8; --splat-accent: #818cf8;
--splat-accent-hover: #6366f1; --splat-accent-hover: #6366f1;
--splat-card-bg: rgba(30, 41, 59, 0.8); --splat-card-bg: var(--rs-bg-surface, rgba(30, 41, 59, 0.8));
} }
/* ── Gallery ── */ /* ── Gallery ── */
@ -319,7 +319,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: calc(100vh - 56px); height: calc(100vh - 56px);
background: #111827; background: var(--splat-bg);
overflow: hidden; overflow: hidden;
} }
@ -344,7 +344,7 @@
gap: 0.375rem; gap: 0.375rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 8px; border-radius: 8px;
background: rgba(30, 41, 59, 0.85); background: var(--rs-glass-bg, rgba(30, 41, 59, 0.85));
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
color: var(--splat-text); color: var(--splat-text);
text-decoration: none; text-decoration: none;
@ -354,7 +354,7 @@
} }
.splat-viewer__back:hover { .splat-viewer__back:hover {
background: rgba(51, 65, 85, 0.9); background: var(--rs-bg-surface, rgba(51, 65, 85, 0.9));
} }
.splat-viewer__info { .splat-viewer__info {
@ -364,7 +364,7 @@
z-index: 10; z-index: 10;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-radius: 8px; border-radius: 8px;
background: rgba(30, 41, 59, 0.85); background: var(--rs-glass-bg, rgba(30, 41, 59, 0.85));
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border: 1px solid var(--splat-border); border: 1px solid var(--splat-border);
color: var(--splat-text); color: var(--splat-text);
@ -391,7 +391,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #111827; background: var(--splat-bg);
z-index: 20; z-index: 20;
transition: opacity 0.4s; transition: opacity 0.4s;
} }
@ -444,6 +444,46 @@
margin: 0; margin: 0;
} }
/* ── Light theme overrides ── */
[data-theme="light"] .splat-card__preview {
background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 50%, #f1f5f9 100%);
}
[data-theme="light"] .splat-card--pending .splat-card__preview {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #f1f5f9 100%);
}
[data-theme="light"] .splat-card--processing .splat-card__preview {
background: linear-gradient(135deg, #e0e7ff 0%, #bfdbfe 50%, #f1f5f9 100%);
}
[data-theme="light"] .splat-card--failed .splat-card__preview {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 50%, #f1f5f9 100%);
}
[data-theme="light"] .splat-card:hover {
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme]) .splat-card__preview {
background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 50%, #f1f5f9 100%);
}
:root:not([data-theme]) .splat-card--pending .splat-card__preview {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #f1f5f9 100%);
}
:root:not([data-theme]) .splat-card--processing .splat-card__preview {
background: linear-gradient(135deg, #e0e7ff 0%, #bfdbfe 50%, #f1f5f9 100%);
}
:root:not([data-theme]) .splat-card--failed .splat-card__preview {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 50%, #f1f5f9 100%);
}
:root:not([data-theme]) .splat-card:hover {
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12);
}
}
/* ── Responsive ── */ /* ── Responsive ── */
@media (max-width: 640px) { @media (max-width: 640px) {