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:
parent
3b3eecdddb
commit
a90e339323
|
|
@ -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 -->
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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
|
|
@ -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] },
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue