feat: inline config beside node, simplified funnel outflow slider
- Inline config panel now appears beside (right of) the node instead of beneath it - Funnel config simplified to single $/mo outflow slider (0-3000) that auto-derives all thresholds: min=1mo, sufficient=4mo, overflow=6mo - Updated deriveThresholds to 4mo sufficient (was 3mo) - Demo presets updated to match new 4mo formula Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ecebf84bcc
commit
0318f0a7e1
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 += `
|
||||
<foreignObject x="${panelX}" y="${panelY}" width="${panelW}" height="${panelH}">
|
||||
|
|
@ -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 `
|
||||
<div class="icp-field"><label class="icp-label">Label</label>
|
||||
<input class="icp-input" data-icp-field="label" value="${this.esc(d.label)}"/></div>
|
||||
<div class="icp-range-group">
|
||||
<span class="icp-range-label">Min</span>
|
||||
<input class="icp-range" type="range" min="0" max="${cap}" value="${d.minThreshold}" data-icp-range="minThreshold"/>
|
||||
<span class="icp-range-value">${this.formatDollar(d.minThreshold)}</span>
|
||||
<span class="icp-range-label">$/mo</span>
|
||||
<input class="icp-range" type="range" min="0" max="3000" step="50" value="${outflow}" data-icp-outflow="desiredOutflow"/>
|
||||
<span class="icp-range-value">${this.formatDollar(outflow)}</span>
|
||||
</div>
|
||||
<div class="icp-range-group">
|
||||
<span class="icp-range-label">Suf</span>
|
||||
<input class="icp-range" type="range" min="0" max="${cap}" value="${sufVal}" data-icp-range="sufficientThreshold"/>
|
||||
<span class="icp-range-value">${this.formatDollar(sufVal)}</span>
|
||||
</div>
|
||||
<div class="icp-range-group">
|
||||
<span class="icp-range-label">Max</span>
|
||||
<input class="icp-range" type="range" min="0" max="${cap}" value="${d.maxThreshold}" data-icp-range="maxThreshold"/>
|
||||
<span class="icp-range-value">${this.formatDollar(d.maxThreshold)}</span>
|
||||
</div>
|
||||
<div class="icp-field"><label class="icp-label">Max Capacity</label>
|
||||
<input class="icp-input" data-icp-field="maxCapacity" type="number" value="${d.maxCapacity}"/></div>
|
||||
<div class="icp-field"><label class="icp-label">Inflow Rate ($/tick)</label>
|
||||
<input class="icp-input" data-icp-field="inflowRate" type="number" value="${d.inflowRate}"/></div>`;
|
||||
<div class="icp-derived-info">
|
||||
<div class="icp-derived-row"><span class="icp-derived-dot" style="background:var(--rflows-status-critical)"></span>Min (1mo): ${this.formatDollar(d.minThreshold)}</div>
|
||||
<div class="icp-derived-row"><span class="icp-derived-dot" style="background:var(--rflows-status-sustained)"></span>Sufficient (4mo): ${this.formatDollar(d.sufficientThreshold ?? d.maxThreshold)}</div>
|
||||
<div class="icp-derived-row"><span class="icp-derived-dot" style="background:var(--rflows-status-thriving)"></span>Overflow (6mo): ${this.formatDollar(d.maxThreshold)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 = `<span class="icp-derived-dot" style="background:var(--rflows-status-critical)"></span>Min (1mo): ${this.formatDollar(derived.minThreshold)}`;
|
||||
if (rows[1]) rows[1].innerHTML = `<span class="icp-derived-dot" style="background:var(--rflows-status-sustained)"></span>Sufficient (4mo): ${this.formatDollar(derived.sufficientThreshold)}`;
|
||||
if (rows[2]) rows[2].innerHTML = `<span class="icp-derived-dot" style="background:var(--rflows-status-thriving)"></span>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 {
|
|||
<div class="editor-section-title">Thresholds ${derived ? "(auto-derived from outflow)" : ""}</div>
|
||||
<div class="editor-field"><label class="editor-label">Min (1mo)${derived ? ` — ${this.formatDollar(derived.minThreshold)}` : ""}</label>
|
||||
<input class="editor-input" data-field="minThreshold" type="number" value="${d.minThreshold}"/></div>
|
||||
<div class="editor-field"><label class="editor-label">Sufficient (3mo)${derived ? ` — ${this.formatDollar(derived.sufficientThreshold)}` : ""}</label>
|
||||
<div class="editor-field"><label class="editor-label">Sufficient (4mo)${derived ? ` — ${this.formatDollar(derived.sufficientThreshold)}` : ""}</label>
|
||||
<input class="editor-input" data-field="sufficientThreshold" type="number" value="${d.sufficientThreshold ?? d.maxThreshold}"/></div>
|
||||
<div class="editor-field"><label class="editor-label">Overflow (6mo)${derived ? ` — ${this.formatDollar(derived.maxThreshold)}` : ""}</label>
|
||||
<input class="editor-input" data-field="maxThreshold" type="number" value="${d.maxThreshold}"/></div>
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue