diff --git a/modules/rflows/components/flows.css b/modules/rflows/components/flows.css index 47736aa..1572701 100644 --- a/modules/rflows/components/flows.css +++ b/modules/rflows/components/flows.css @@ -698,6 +698,19 @@ min-width: 40px; text-align: right; } +/* Derived threshold info */ +.icp-derived-info { + margin-top: 6px; padding: 6px 0 0; + border-top: 1px solid var(--rs-border-subtle); +} +.icp-derived-row { + display: flex; align-items: center; gap: 6px; + font-size: 10px; color: var(--rs-text-secondary); margin-bottom: 3px; +} +.icp-derived-dot { + width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; +} + /* Analytics bars */ .icp-analytics-row { margin-bottom: 8px; diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts index 4cff464..5be4c6d 100644 --- a/modules/rflows/components/folk-flows-app.ts +++ b/modules/rflows/components/folk-flows-app.ts @@ -2444,11 +2444,11 @@ class FolkFlowsApp extends HTMLElement { this.renderFunnelThresholdMarkers(overlay, node, s); } - // Panel positioned below the node - const panelW = Math.max(280, s.w); + // Panel positioned beside the node (right side) + const panelW = 280; const panelH = 260; - const panelX = (s.w - panelW) / 2; - const panelY = s.h + 8; + const panelX = s.w + 12; + const panelY = 0; overlay.innerHTML += ` @@ -2504,30 +2504,20 @@ class FolkFlowsApp extends HTMLElement { private renderFunnelConfigTab(node: FlowNode): string { const d = node.data as FunnelNodeData; - const cap = d.maxCapacity || 1; - const sufVal = d.sufficientThreshold ?? d.maxThreshold; + const outflow = d.desiredOutflow || 0; return `
- Min - - ${this.formatDollar(d.minThreshold)} + $/mo + + ${this.formatDollar(outflow)}
-
- Suf - - ${this.formatDollar(sufVal)} -
-
- Max - - ${this.formatDollar(d.maxThreshold)} -
-
-
-
-
`; +
+
Min (1mo): ${this.formatDollar(d.minThreshold)}
+
Sufficient (4mo): ${this.formatDollar(d.sufficientThreshold ?? d.maxThreshold)}
+
Overflow (6mo): ${this.formatDollar(d.maxThreshold)}
+
`; } private renderOutcomeConfigTab(node: FlowNode): string { @@ -2777,6 +2767,35 @@ class FolkFlowsApp extends HTMLElement { this.redrawThresholdMarkers(node); }); }); + + // Outflow slider — auto-derives all thresholds + overlay.querySelectorAll("[data-icp-outflow]").forEach((el) => { + const input = el as HTMLInputElement; + input.addEventListener("input", () => { + const val = parseFloat(input.value) || 0; + const fd = node.data as FunnelNodeData; + fd.desiredOutflow = val; + const derived = deriveThresholds(val); + fd.minThreshold = derived.minThreshold; + fd.sufficientThreshold = derived.sufficientThreshold; + fd.maxThreshold = derived.maxThreshold; + fd.maxCapacity = derived.maxCapacity; + const valueSpan = input.parentElement?.querySelector(".icp-range-value") as HTMLElement; + if (valueSpan) valueSpan.textContent = this.formatDollar(val); + // Update derived info display + const info = overlay.querySelector(".icp-derived-info"); + if (info) { + const rows = info.querySelectorAll(".icp-derived-row"); + if (rows[0]) rows[0].innerHTML = `Min (1mo): ${this.formatDollar(derived.minThreshold)}`; + if (rows[1]) rows[1].innerHTML = `Sufficient (4mo): ${this.formatDollar(derived.sufficientThreshold)}`; + if (rows[2]) rows[2].innerHTML = `Overflow (6mo): ${this.formatDollar(derived.maxThreshold)}`; + } + this.redrawNodeOnly(node); + this.redrawEdges(); + this.redrawThresholdMarkers(node); + this.scheduleSave(); + }); + }); } private attachThresholdDragListeners(overlay: Element, node: FlowNode) { @@ -2995,7 +3014,7 @@ class FolkFlowsApp extends HTMLElement {
Thresholds ${derived ? "(auto-derived from outflow)" : ""}
-
+
diff --git a/modules/rflows/lib/presets.ts b/modules/rflows/lib/presets.ts index 2346d58..0510682 100644 --- a/modules/rflows/lib/presets.ts +++ b/modules/rflows/lib/presets.ts @@ -19,7 +19,7 @@ export const demoNodes: FlowNode[] = [ id: "treasury", type: "funnel", position: { x: 630, y: 0 }, data: { label: "Treasury", currentValue: 85000, desiredOutflow: 10000, - minThreshold: 10000, sufficientThreshold: 30000, maxThreshold: 60000, + minThreshold: 10000, sufficientThreshold: 40000, maxThreshold: 60000, maxCapacity: 90000, inflowRate: 1000, dynamicOverflow: true, overflowAllocations: [ { targetId: "public-goods", percentage: 40, color: OVERFLOW_COLORS[0] }, @@ -35,7 +35,7 @@ export const demoNodes: FlowNode[] = [ id: "public-goods", type: "funnel", position: { x: 170, y: 450 }, data: { label: "Public Goods", currentValue: 45000, desiredOutflow: 7000, - minThreshold: 7000, sufficientThreshold: 21000, maxThreshold: 42000, + minThreshold: 7000, sufficientThreshold: 28000, maxThreshold: 42000, maxCapacity: 63000, inflowRate: 400, overflowAllocations: [], spendingAllocations: [ @@ -49,7 +49,7 @@ export const demoNodes: FlowNode[] = [ id: "research", type: "funnel", position: { x: 975, y: 450 }, data: { label: "Research", currentValue: 28000, desiredOutflow: 5000, - minThreshold: 5000, sufficientThreshold: 15000, maxThreshold: 30000, + minThreshold: 5000, sufficientThreshold: 20000, maxThreshold: 30000, maxCapacity: 45000, inflowRate: 350, overflowAllocations: [], spendingAllocations: [ @@ -62,7 +62,7 @@ export const demoNodes: FlowNode[] = [ id: "emergency", type: "funnel", position: { x: 1320, y: 450 }, data: { label: "Emergency", currentValue: 12000, desiredOutflow: 8000, - minThreshold: 8000, sufficientThreshold: 24000, maxThreshold: 48000, + minThreshold: 8000, sufficientThreshold: 32000, maxThreshold: 48000, maxCapacity: 72000, inflowRate: 250, overflowAllocations: [], spendingAllocations: [ diff --git a/modules/rflows/lib/types.ts b/modules/rflows/lib/types.ts index a7acd00..30216fd 100644 --- a/modules/rflows/lib/types.ts +++ b/modules/rflows/lib/types.ts @@ -58,7 +58,7 @@ export interface FunnelNodeData { export function deriveThresholds(desiredOutflow: number) { return { minThreshold: desiredOutflow * 1, // 1 month runway - sufficientThreshold: desiredOutflow * 3, // 3 months runway + sufficientThreshold: desiredOutflow * 4, // 4 months runway (1 min + 3 buffer) maxThreshold: desiredOutflow * 6, // overflow point maxCapacity: desiredOutflow * 9, // visual max };