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 ───────────────────────────────── */
|
||||
|
||||
/* ── 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 ────────────────────────────────────────────── */
|
||||
.flows-landing, .flows-detail {
|
||||
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__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); }
|
||||
|
||||
/* About / how-it-works section */
|
||||
|
|
@ -396,8 +466,8 @@
|
|||
|
||||
/* Drag handle on edge midpoint */
|
||||
.edge-drag-handle {
|
||||
fill: #475569;
|
||||
stroke: #94a3b8;
|
||||
fill: var(--rflows-drag-handle-fill, #475569);
|
||||
stroke: var(--rflows-drag-handle-stroke, #94a3b8);
|
||||
stroke-width: 1.5;
|
||||
cursor: grab;
|
||||
transition: fill 0.15s;
|
||||
|
|
@ -534,14 +604,174 @@
|
|||
/* Inline edit overlay container */
|
||||
.inline-edit-overlay { pointer-events: all; }
|
||||
|
||||
/* Funnel overflow lip glow when overflowing */
|
||||
.funnel-lip { transition: fill 0.3s, opacity 0.3s; }
|
||||
.funnel-lip--active { fill: #10b981; opacity: 0.8; }
|
||||
/* Funnel overflow pipe (vessel metaphor) */
|
||||
.funnel-pipe { transition: fill 0.3s, height 0.3s, y 0.3s; }
|
||||
.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 */
|
||||
.inline-status-badge { cursor: pointer; transition: opacity 0.15s; }
|
||||
.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 */
|
||||
.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 */ }
|
||||
|
|
@ -594,6 +824,49 @@
|
|||
--rflows-outcome-hover-bg: #fce7f3;
|
||||
--rflows-danger-text: #dc2626;
|
||||
--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) {
|
||||
:root:not([data-theme]) {
|
||||
|
|
@ -605,6 +878,37 @@
|
|||
--rflows-outcome-hover-bg: #fce7f3;
|
||||
--rflows-danger-text: #dc2626;
|
||||
--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 },
|
||||
data: {
|
||||
label: "Treasury", currentValue: 85000, minThreshold: 20000, maxThreshold: 70000,
|
||||
maxCapacity: 100000, inflowRate: 1000, sufficientThreshold: 60000, dynamicOverflow: true,
|
||||
label: "Treasury", currentValue: 85000, desiredOutflow: 10000,
|
||||
minThreshold: 10000, sufficientThreshold: 30000, maxThreshold: 60000,
|
||||
maxCapacity: 90000, inflowRate: 1000, dynamicOverflow: true,
|
||||
overflowAllocations: [
|
||||
{ targetId: "public-goods", percentage: 40, color: OVERFLOW_COLORS[0] },
|
||||
{ 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 },
|
||||
data: {
|
||||
label: "Public Goods", currentValue: 45000, minThreshold: 15000, maxThreshold: 50000,
|
||||
maxCapacity: 70000, inflowRate: 400, sufficientThreshold: 42000,
|
||||
label: "Public Goods", currentValue: 45000, desiredOutflow: 7000,
|
||||
minThreshold: 7000, sufficientThreshold: 21000, maxThreshold: 42000,
|
||||
maxCapacity: 63000, inflowRate: 400,
|
||||
overflowAllocations: [],
|
||||
spendingAllocations: [
|
||||
{ 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 },
|
||||
data: {
|
||||
label: "Research", currentValue: 28000, minThreshold: 20000, maxThreshold: 45000,
|
||||
maxCapacity: 60000, inflowRate: 350, sufficientThreshold: 38000,
|
||||
label: "Research", currentValue: 28000, desiredOutflow: 5000,
|
||||
minThreshold: 5000, sufficientThreshold: 15000, maxThreshold: 30000,
|
||||
maxCapacity: 45000, inflowRate: 350,
|
||||
overflowAllocations: [],
|
||||
spendingAllocations: [
|
||||
{ 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 },
|
||||
data: {
|
||||
label: "Emergency", currentValue: 12000, minThreshold: 25000, maxThreshold: 60000,
|
||||
maxCapacity: 80000, inflowRate: 250, sufficientThreshold: 50000,
|
||||
label: "Emergency", currentValue: 12000, desiredOutflow: 8000,
|
||||
minThreshold: 8000, sufficientThreshold: 24000, maxThreshold: 48000,
|
||||
maxCapacity: 72000, inflowRate: 250,
|
||||
overflowAllocations: [],
|
||||
spendingAllocations: [
|
||||
{ targetId: "emergency-response", percentage: 100, color: SPENDING_COLORS[0] },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import type { FlowNode, FunnelNodeData, OutcomeNodeData, SufficiencyState } from "./types";
|
||||
import { deriveThresholds } from "./types";
|
||||
|
||||
export interface SimulationConfig {
|
||||
tickDivisor: number;
|
||||
|
|
@ -102,6 +103,15 @@ export function simulateTick(
|
|||
const src = node.data as FunnelNodeData;
|
||||
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;
|
||||
value += overflowIncoming.get(node.id) ?? 0;
|
||||
value = Math.min(value, data.maxCapacity);
|
||||
|
|
@ -136,7 +146,8 @@ export function simulateTick(
|
|||
rateMultiplier = spendingRateCritical;
|
||||
}
|
||||
|
||||
let drain = (data.inflowRate / tickDivisor) * rateMultiplier;
|
||||
const baseRate = data.desiredOutflow || data.inflowRate;
|
||||
let drain = (baseRate / tickDivisor) * rateMultiplier;
|
||||
drain = Math.min(drain, value);
|
||||
value -= drain;
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export interface FunnelNodeData {
|
|||
maxThreshold: number;
|
||||
maxCapacity: number;
|
||||
inflowRate: number;
|
||||
desiredOutflow?: number;
|
||||
sufficientThreshold?: number;
|
||||
dynamicOverflow?: boolean;
|
||||
overflowAllocations: OverflowAllocation[];
|
||||
|
|
@ -54,6 +55,15 @@ export interface FunnelNodeData {
|
|||
[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 {
|
||||
label: string;
|
||||
completed: boolean;
|
||||
|
|
@ -123,8 +133,8 @@ export const PORT_DEFS: Record<FlowNode["type"], PortDefinition[]> = {
|
|||
],
|
||||
funnel: [
|
||||
{ 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: 1.0, yFrac: 0.15, color: "#f59e0b", connectsTo: ["inflow"], side: "right" },
|
||||
{ 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.55, color: "#f59e0b", connectsTo: ["inflow"], side: "right" },
|
||||
{ kind: "spending", dir: "out", xFrac: 0.5, yFrac: 1, color: "#8b5cf6", connectsTo: ["inflow"] },
|
||||
],
|
||||
outcome: [
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
/* rSplat — Gaussian Splat Viewer */
|
||||
|
||||
:root {
|
||||
--splat-bg: #0f172a;
|
||||
--splat-surface: #1e293b;
|
||||
--splat-border: #334155;
|
||||
--splat-text: #e2e8f0;
|
||||
--splat-text-muted: #94a3b8;
|
||||
--splat-bg: var(--rs-bg-page, #0f172a);
|
||||
--splat-surface: var(--rs-bg-surface, #1e293b);
|
||||
--splat-border: var(--rs-border-strong, #334155);
|
||||
--splat-text: var(--rs-text-primary, #e2e8f0);
|
||||
--splat-text-muted: var(--rs-text-muted, #94a3b8);
|
||||
--splat-accent: #818cf8;
|
||||
--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 ── */
|
||||
|
|
@ -319,7 +319,7 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 56px);
|
||||
background: #111827;
|
||||
background: var(--splat-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
@ -344,7 +344,7 @@
|
|||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
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);
|
||||
color: var(--splat-text);
|
||||
text-decoration: none;
|
||||
|
|
@ -354,7 +354,7 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
|
|
@ -364,7 +364,7 @@
|
|||
z-index: 10;
|
||||
padding: 0.75rem 1rem;
|
||||
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);
|
||||
border: 1px solid var(--splat-border);
|
||||
color: var(--splat-text);
|
||||
|
|
@ -391,7 +391,7 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #111827;
|
||||
background: var(--splat-bg);
|
||||
z-index: 20;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
|
@ -444,6 +444,46 @@
|
|||
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 ── */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue