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;
|
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 */
|
/* Analytics bars */
|
||||||
.icp-analytics-row {
|
.icp-analytics-row {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
|
||||||
|
|
@ -2444,11 +2444,11 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
this.renderFunnelThresholdMarkers(overlay, node, s);
|
this.renderFunnelThresholdMarkers(overlay, node, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel positioned below the node
|
// Panel positioned beside the node (right side)
|
||||||
const panelW = Math.max(280, s.w);
|
const panelW = 280;
|
||||||
const panelH = 260;
|
const panelH = 260;
|
||||||
const panelX = (s.w - panelW) / 2;
|
const panelX = s.w + 12;
|
||||||
const panelY = s.h + 8;
|
const panelY = 0;
|
||||||
|
|
||||||
overlay.innerHTML += `
|
overlay.innerHTML += `
|
||||||
<foreignObject x="${panelX}" y="${panelY}" width="${panelW}" height="${panelH}">
|
<foreignObject x="${panelX}" y="${panelY}" width="${panelW}" height="${panelH}">
|
||||||
|
|
@ -2504,30 +2504,20 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
|
|
||||||
private renderFunnelConfigTab(node: FlowNode): string {
|
private renderFunnelConfigTab(node: FlowNode): string {
|
||||||
const d = node.data as FunnelNodeData;
|
const d = node.data as FunnelNodeData;
|
||||||
const cap = d.maxCapacity || 1;
|
const outflow = d.desiredOutflow || 0;
|
||||||
const sufVal = d.sufficientThreshold ?? d.maxThreshold;
|
|
||||||
return `
|
return `
|
||||||
<div class="icp-field"><label class="icp-label">Label</label>
|
<div class="icp-field"><label class="icp-label">Label</label>
|
||||||
<input class="icp-input" data-icp-field="label" value="${this.esc(d.label)}"/></div>
|
<input class="icp-input" data-icp-field="label" value="${this.esc(d.label)}"/></div>
|
||||||
<div class="icp-range-group">
|
<div class="icp-range-group">
|
||||||
<span class="icp-range-label">Min</span>
|
<span class="icp-range-label">$/mo</span>
|
||||||
<input class="icp-range" type="range" min="0" max="${cap}" value="${d.minThreshold}" data-icp-range="minThreshold"/>
|
<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(d.minThreshold)}</span>
|
<span class="icp-range-value">${this.formatDollar(outflow)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="icp-range-group">
|
<div class="icp-derived-info">
|
||||||
<span class="icp-range-label">Suf</span>
|
<div class="icp-derived-row"><span class="icp-derived-dot" style="background:var(--rflows-status-critical)"></span>Min (1mo): ${this.formatDollar(d.minThreshold)}</div>
|
||||||
<input class="icp-range" type="range" min="0" max="${cap}" value="${sufVal}" data-icp-range="sufficientThreshold"/>
|
<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>
|
||||||
<span class="icp-range-value">${this.formatDollar(sufVal)}</span>
|
<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>
|
</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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderOutcomeConfigTab(node: FlowNode): string {
|
private renderOutcomeConfigTab(node: FlowNode): string {
|
||||||
|
|
@ -2777,6 +2767,35 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
this.redrawThresholdMarkers(node);
|
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) {
|
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-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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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 },
|
id: "treasury", type: "funnel", position: { x: 630, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
label: "Treasury", currentValue: 85000, desiredOutflow: 10000,
|
label: "Treasury", currentValue: 85000, desiredOutflow: 10000,
|
||||||
minThreshold: 10000, sufficientThreshold: 30000, maxThreshold: 60000,
|
minThreshold: 10000, sufficientThreshold: 40000, maxThreshold: 60000,
|
||||||
maxCapacity: 90000, inflowRate: 1000, dynamicOverflow: true,
|
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] },
|
||||||
|
|
@ -35,7 +35,7 @@ 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, desiredOutflow: 7000,
|
label: "Public Goods", currentValue: 45000, desiredOutflow: 7000,
|
||||||
minThreshold: 7000, sufficientThreshold: 21000, maxThreshold: 42000,
|
minThreshold: 7000, sufficientThreshold: 28000, maxThreshold: 42000,
|
||||||
maxCapacity: 63000, inflowRate: 400,
|
maxCapacity: 63000, inflowRate: 400,
|
||||||
overflowAllocations: [],
|
overflowAllocations: [],
|
||||||
spendingAllocations: [
|
spendingAllocations: [
|
||||||
|
|
@ -49,7 +49,7 @@ 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, desiredOutflow: 5000,
|
label: "Research", currentValue: 28000, desiredOutflow: 5000,
|
||||||
minThreshold: 5000, sufficientThreshold: 15000, maxThreshold: 30000,
|
minThreshold: 5000, sufficientThreshold: 20000, maxThreshold: 30000,
|
||||||
maxCapacity: 45000, inflowRate: 350,
|
maxCapacity: 45000, inflowRate: 350,
|
||||||
overflowAllocations: [],
|
overflowAllocations: [],
|
||||||
spendingAllocations: [
|
spendingAllocations: [
|
||||||
|
|
@ -62,7 +62,7 @@ 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, desiredOutflow: 8000,
|
label: "Emergency", currentValue: 12000, desiredOutflow: 8000,
|
||||||
minThreshold: 8000, sufficientThreshold: 24000, maxThreshold: 48000,
|
minThreshold: 8000, sufficientThreshold: 32000, maxThreshold: 48000,
|
||||||
maxCapacity: 72000, inflowRate: 250,
|
maxCapacity: 72000, inflowRate: 250,
|
||||||
overflowAllocations: [],
|
overflowAllocations: [],
|
||||||
spendingAllocations: [
|
spendingAllocations: [
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export interface FunnelNodeData {
|
||||||
export function deriveThresholds(desiredOutflow: number) {
|
export function deriveThresholds(desiredOutflow: number) {
|
||||||
return {
|
return {
|
||||||
minThreshold: desiredOutflow * 1, // 1 month runway
|
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
|
maxThreshold: desiredOutflow * 6, // overflow point
|
||||||
maxCapacity: desiredOutflow * 9, // visual max
|
maxCapacity: desiredOutflow * 9, // visual max
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue