feat(rflows): replace drain valve bar with rotary knob, move allocations to panel
- Add renderDrainKnob() with rotating handle matching source valve style - Remove rectangular ◁ $/mo ▷ drag bar and split control overlays from canvas - Add editable range sliders in inline config Allocations tab - Rewire drag handler for live knob rotation during drain rate adjustment - Clean up dead split-divider CSS and event listeners Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8635b54800
commit
c35f39380e
|
|
@ -283,11 +283,14 @@
|
||||||
.node-glow { filter: drop-shadow(0 0 6px rgba(251,191,36,0.5)); }
|
.node-glow { filter: drop-shadow(0 0 6px rgba(251,191,36,0.5)); }
|
||||||
|
|
||||||
/* Funnel drag handles — hidden by default, visible on hover */
|
/* Funnel drag handles — hidden by default, visible on hover */
|
||||||
.funnel-valve-handle, .funnel-height-handle { opacity: 0; transition: opacity 0.2s; }
|
.funnel-drain-knob, .funnel-height-handle { opacity: 0; transition: opacity 0.2s; }
|
||||||
.flow-node:hover .funnel-valve-handle,
|
.flow-node:hover .funnel-drain-knob,
|
||||||
.flow-node:hover .funnel-height-handle { opacity: 0.8; }
|
.flow-node:hover .funnel-height-handle { opacity: 0.85; }
|
||||||
.funnel-valve-handle:hover { opacity: 1 !important; }
|
.funnel-drain-knob:hover { opacity: 1 !important; }
|
||||||
.funnel-height-handle:hover { opacity: 1 !important; }
|
.funnel-height-handle:hover { opacity: 1 !important; }
|
||||||
|
.drain-knob { cursor: ew-resize; transition: filter 0.15s; }
|
||||||
|
.drain-knob:hover { filter: brightness(1.15); }
|
||||||
|
.drain-handle-group { transition: transform 0.2s ease; }
|
||||||
|
|
||||||
/* HTML card nodes (foreignObject) */
|
/* HTML card nodes (foreignObject) */
|
||||||
.node-card {
|
.node-card {
|
||||||
|
|
@ -526,12 +529,6 @@
|
||||||
.satisfaction-bar-bg { opacity: 0.3; }
|
.satisfaction-bar-bg { opacity: 0.3; }
|
||||||
.satisfaction-bar-fill { transition: width 0.3s ease; }
|
.satisfaction-bar-fill { transition: width 0.3s ease; }
|
||||||
|
|
||||||
/* Split controls — proportional allocation sliders */
|
|
||||||
.split-control { pointer-events: all; }
|
|
||||||
.split-seg { transition: width 50ms ease-out; }
|
|
||||||
.split-divider { cursor: ew-resize; }
|
|
||||||
.split-divider:hover rect { fill: #6366f1; stroke: #818cf8; }
|
|
||||||
.split-divider:active rect { fill: #4f46e5; }
|
|
||||||
|
|
||||||
/* ── Node detail modals ──────────────────────────────── */
|
/* ── Node detail modals ──────────────────────────────── */
|
||||||
.flows-modal-backdrop {
|
.flows-modal-backdrop {
|
||||||
|
|
|
||||||
|
|
@ -1254,7 +1254,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
// Delegated funnel valve + height drag handles
|
// Delegated funnel valve + height drag handles
|
||||||
svg.addEventListener("pointerdown", (e: PointerEvent) => {
|
svg.addEventListener("pointerdown", (e: PointerEvent) => {
|
||||||
const target = e.target as Element;
|
const target = e.target as Element;
|
||||||
const valveG = target.closest(".funnel-valve-handle") as SVGGElement | null;
|
const valveG = target.closest(".funnel-drain-knob") as SVGGElement | null;
|
||||||
const heightG = target.closest(".funnel-height-handle") as SVGGElement | null;
|
const heightG = target.closest(".funnel-height-handle") as SVGGElement | null;
|
||||||
const handleG = valveG || heightG;
|
const handleG = valveG || heightG;
|
||||||
if (!handleG) return;
|
if (!handleG) return;
|
||||||
|
|
@ -1271,13 +1271,20 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
if (valveG) {
|
if (valveG) {
|
||||||
const startDrain = fd.drainRate || 0;
|
const startDrain = fd.drainRate || 0;
|
||||||
handleG.setPointerCapture(e.pointerId);
|
handleG.setPointerCapture(e.pointerId);
|
||||||
const label = handleG.querySelector("text");
|
const knobLabel = handleG.querySelector(".drain-knob-label") as SVGTextElement | null;
|
||||||
|
const handleGroup = handleG.querySelector(".drain-handle-group") as SVGGElement | null;
|
||||||
|
const knobCircle = handleG.querySelector(".drain-knob") as SVGCircleElement | null;
|
||||||
|
const cx = knobCircle ? parseFloat(knobCircle.getAttribute("cx")!) : s.w / 2;
|
||||||
|
const cy = knobCircle ? parseFloat(knobCircle.getAttribute("cy")!) : s.h - 12;
|
||||||
const onMove = (ev: PointerEvent) => {
|
const onMove = (ev: PointerEvent) => {
|
||||||
const deltaX = (ev.clientX - startX) / this.canvasZoom;
|
const deltaX = (ev.clientX - startX) / this.canvasZoom;
|
||||||
let newDrain = Math.round((startDrain + (deltaX / s.w) * 10000) / 50) * 50;
|
let newDrain = Math.round((startDrain + (deltaX / s.w) * 10000) / 50) * 50;
|
||||||
newDrain = Math.max(0, Math.min(10000, newDrain));
|
newDrain = Math.max(0, Math.min(10000, newDrain));
|
||||||
fd.drainRate = newDrain;
|
fd.drainRate = newDrain;
|
||||||
if (label) label.textContent = `◁ ${this.formatDollar(newDrain)}/mo ▷`;
|
// Live rotation update
|
||||||
|
const angle = Math.min(90, (newDrain / 10000) * 90);
|
||||||
|
if (handleGroup) handleGroup.setAttribute("transform", `rotate(${-90 + angle},${cx},${cy})`);
|
||||||
|
if (knobLabel) knobLabel.textContent = `${this.formatDollar(newDrain)}/mo`;
|
||||||
};
|
};
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
handleG.removeEventListener("pointermove", onMove as EventListener);
|
handleG.removeEventListener("pointermove", onMove as EventListener);
|
||||||
|
|
@ -1611,32 +1618,6 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split control drag (delegated on node layer)
|
|
||||||
const nodeLayerForSplit = this.shadow.getElementById("node-layer");
|
|
||||||
if (nodeLayerForSplit) {
|
|
||||||
nodeLayerForSplit.addEventListener("pointerdown", (e: PointerEvent) => {
|
|
||||||
const divider = (e.target as Element).closest(".split-divider") as SVGGElement | null;
|
|
||||||
if (!divider) return;
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
const nodeId = divider.dataset.nodeId!;
|
|
||||||
const allocType = divider.dataset.allocType!;
|
|
||||||
const dividerIdx = parseInt(divider.dataset.dividerIdx!, 10);
|
|
||||||
|
|
||||||
// Capture starting percentages
|
|
||||||
const allocs = this.getSplitAllocs(nodeId, allocType);
|
|
||||||
if (!allocs || allocs.length < 2) return;
|
|
||||||
|
|
||||||
this._splitDragging = true;
|
|
||||||
this._splitDragNodeId = nodeId;
|
|
||||||
this._splitDragAllocType = allocType;
|
|
||||||
this._splitDragDividerIdx = dividerIdx;
|
|
||||||
this._splitDragStartX = e.clientX;
|
|
||||||
this._splitDragStartPcts = allocs.map(a => a.percentage);
|
|
||||||
(e.target as Element).setPointerCapture?.(e.pointerId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeline minimize
|
// Timeline minimize
|
||||||
const timelineMin = this.shadow.getElementById("timeline-minimize");
|
const timelineMin = this.shadow.getElementById("timeline-minimize");
|
||||||
if (timelineMin) {
|
if (timelineMin) {
|
||||||
|
|
@ -1912,10 +1893,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
const streamY = nozzleEndY + nozzleBotW;
|
const streamY = nozzleEndY + nozzleBotW;
|
||||||
const streamH = h - streamY;
|
const streamH = h - streamY;
|
||||||
|
|
||||||
// Split control replaces old allocation bar
|
const allocBar = "";
|
||||||
const allocBar = d.targetAllocations && d.targetAllocations.length >= 2
|
|
||||||
? this.renderSplitControl(n.id, "source", d.targetAllocations, w / 2, h - 8, w - 40)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `<g class="flow-node ${selected ? "selected" : ""}" data-node-id="${n.id}" data-collab-id="node:${n.id}" transform="translate(${x},${y})">
|
return `<g class="flow-node ${selected ? "selected" : ""}" data-node-id="${n.id}" data-collab-id="node:${n.id}" transform="translate(${x},${y})">
|
||||||
<rect class="node-bg" x="0" y="0" width="${w}" height="${h}" rx="8" fill="transparent" stroke="none"/>
|
<rect class="node-bg" x="0" y="0" width="${w}" height="${h}" rx="8" fill="transparent" stroke="none"/>
|
||||||
|
|
@ -1972,6 +1950,19 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
return `M ${pts.join(" L ")} Z`;
|
return `M ${pts.join(" L ")} Z`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Render a rotary drain knob SVG group matching the source valve style */
|
||||||
|
private renderDrainKnob(nodeId: string, cx: number, cy: number, drainRate: number): string {
|
||||||
|
const r = 18;
|
||||||
|
const angle = Math.min(90, (drainRate / 10000) * 90);
|
||||||
|
return `<g class="funnel-drain-knob" data-handle="valve" data-node-id="${nodeId}">
|
||||||
|
<circle class="drain-knob" cx="${cx}" cy="${cy}" r="${r}" fill="var(--rflows-label-spending)" stroke="var(--rs-bg-surface)" stroke-width="1.5"/>
|
||||||
|
<g class="drain-handle-group" transform="rotate(${-90 + angle},${cx},${cy})">
|
||||||
|
<rect x="${cx - 3}" y="${cy - r - 4}" width="6" height="${r + 4}" rx="3" fill="var(--rs-bg-surface)" opacity="0.8"/>
|
||||||
|
</g>
|
||||||
|
<text class="drain-knob-label" x="${cx}" y="${cy + 28}" text-anchor="middle" fill="var(--rs-text-secondary)" font-size="12" font-weight="600" pointer-events="none">${this.formatDollar(drainRate)}/mo</text>
|
||||||
|
</g>`;
|
||||||
|
}
|
||||||
|
|
||||||
private renderFunnelNodeSvg(n: FlowNode, selected: boolean, sat?: { actual: number; needed: number; ratio: number }): string {
|
private renderFunnelNodeSvg(n: FlowNode, selected: boolean, sat?: { actual: number; needed: number; ratio: number }): string {
|
||||||
const d = n.data as FunnelNodeData;
|
const d = n.data as FunnelNodeData;
|
||||||
const s = this.getNodeSize(n);
|
const s = this.getNodeSize(n);
|
||||||
|
|
@ -2157,24 +2148,8 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
${overflowSpill}
|
${overflowSpill}
|
||||||
<rect x="24" y="${satBarY}" width="${satBarW}" height="8" rx="4" style="fill:var(--rs-bg-surface-raised)" opacity="0.3" class="satisfaction-bar-bg"/>
|
<rect x="24" y="${satBarY}" width="${satBarW}" height="8" rx="4" style="fill:var(--rs-bg-surface-raised)" opacity="0.3" class="satisfaction-bar-bg"/>
|
||||||
<rect x="24" y="${satBarY}" width="${satFillW}" height="8" rx="4" style="fill:var(--rflows-sat-bar)" class="satisfaction-bar-fill" ${satBarBorder}/>
|
<rect x="24" y="${satBarY}" width="${satFillW}" height="8" rx="4" style="fill:var(--rflows-sat-bar)" class="satisfaction-bar-fill" ${satBarBorder}/>
|
||||||
<!-- Drain valve handle at spout -->
|
<!-- Rotary drain knob at spout -->
|
||||||
<g class="funnel-valve-handle" data-handle="valve" data-node-id="${n.id}">
|
${this.renderDrainKnob(n.id, w / 2, h - 12, drain)}
|
||||||
<rect x="${drainInset - 8}" y="${h - 16}" width="${drainW + 16}" height="18" rx="5"
|
|
||||||
style="fill:var(--rflows-label-spending);cursor:ew-resize;stroke:white;stroke-width:1.5"/>
|
|
||||||
<text x="${w / 2}" y="${h - 4}" text-anchor="middle" fill="white" font-size="11" font-weight="600" pointer-events="none">
|
|
||||||
◁ ${this.formatDollar(drain)}/mo ▷
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<!-- Spending drain pipe stub -->
|
|
||||||
${(() => { const spW = fwFunnel ? Math.max(4, Math.round(fwFunnel.spendingWidthPx)) : 4; return `<rect x="${(w / 2) - spW / 2}" y="${h}" width="${spW}" height="18" rx="${Math.min(4, spW / 2)}" fill="var(--rflows-edge-spending)" opacity="0.6"/>`; })()}
|
|
||||||
<!-- Spending split control at drain spout -->
|
|
||||||
${d.spendingAllocations.length >= 2
|
|
||||||
? this.renderSplitControl(n.id, "spending", d.spendingAllocations, w / 2, h + 26, Math.min(w - 20, drainW + 60))
|
|
||||||
: ""}
|
|
||||||
<!-- Overflow split control at pipe area -->
|
|
||||||
${d.overflowAllocations.length >= 2
|
|
||||||
? this.renderSplitControl(n.id, "overflow", d.overflowAllocations, w / 2, pipeY - 12, w - 40)
|
|
||||||
: ""}
|
|
||||||
<g class="funnel-height-handle" data-handle="height" data-node-id="${n.id}">
|
<g class="funnel-height-handle" data-handle="height" data-node-id="${n.id}">
|
||||||
<rect x="${w / 2 - 28}" y="${h + 4}" width="56" height="12" rx="5"
|
<rect x="${w / 2 - 28}" y="${h + 4}" width="56" height="12" rx="5"
|
||||||
style="fill:var(--rs-border-strong);cursor:ns-resize;stroke:var(--rs-text-muted);stroke-width:1"/>
|
style="fill:var(--rs-border-strong);cursor:ns-resize;stroke:var(--rs-text-muted);stroke-width:1"/>
|
||||||
|
|
@ -2196,8 +2171,6 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
${overflowH > 20 ? `<text x="${w / 2}" y="${zoneTop + overflowH / 2 + 4}" text-anchor="middle" fill="#f59e0b" font-size="10" font-weight="600" opacity="0.5" pointer-events="none">OVERFLOW</text>` : ""}
|
${overflowH > 20 ? `<text x="${w / 2}" y="${zoneTop + overflowH / 2 + 4}" text-anchor="middle" fill="#f59e0b" font-size="10" font-weight="600" opacity="0.5" pointer-events="none">OVERFLOW</text>` : ""}
|
||||||
<!-- Value text -->
|
<!-- Value text -->
|
||||||
<text class="funnel-value-text" data-node-id="${n.id}" x="${w / 2}" y="${h - drainInset - 44}" text-anchor="middle" fill="var(--rs-text-muted)" font-size="13" font-weight="500" pointer-events="none">$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.overflowThreshold).toLocaleString()}</text>
|
<text class="funnel-value-text" data-node-id="${n.id}" x="${w / 2}" y="${h - drainInset - 44}" text-anchor="middle" fill="var(--rs-text-muted)" font-size="13" font-weight="500" pointer-events="none">$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.overflowThreshold).toLocaleString()}</text>
|
||||||
<!-- Drain rate label -->
|
|
||||||
<text x="${w / 2}" y="${h + 20}" text-anchor="middle" fill="#34d399" font-size="12" font-weight="600" pointer-events="none">${this.formatDollar(drain)}/mo \u25BE</text>
|
|
||||||
<!-- Overflow labels at pipe positions -->
|
<!-- Overflow labels at pipe positions -->
|
||||||
${isOverflow ? `<text x="${-pipeW - 6}" y="${pipeY + pipeH / 2 + 4}" text-anchor="end" fill="#6ee7b7" font-size="11" font-weight="500" opacity="0.8" pointer-events="none">${overflowLabel}</text>
|
${isOverflow ? `<text x="${-pipeW - 6}" y="${pipeY + pipeH / 2 + 4}" text-anchor="end" fill="#6ee7b7" font-size="11" font-weight="500" opacity="0.8" pointer-events="none">${overflowLabel}</text>
|
||||||
<text x="${w + pipeW + 6}" y="${pipeY + pipeH / 2 + 4}" text-anchor="start" fill="#6ee7b7" font-size="11" font-weight="500" opacity="0.8" pointer-events="none">${overflowLabel}</text>` : ""}
|
<text x="${w + pipeW + 6}" y="${pipeY + pipeH / 2 + 4}" text-anchor="start" fill="#6ee7b7" font-size="11" font-weight="500" opacity="0.8" pointer-events="none">${overflowLabel}</text>` : ""}
|
||||||
|
|
@ -3494,14 +3467,18 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
// ── Allocations tab ──
|
// ── Allocations tab ──
|
||||||
|
|
||||||
private renderInlineAllocTab(node: FlowNode): string {
|
private renderInlineAllocTab(node: FlowNode): string {
|
||||||
const renderRows = (title: string, allocs: { targetId: string; percentage: number; color: string }[]) => {
|
const renderRows = (title: string, allocType: string, allocs: { targetId: string; percentage: number; color: string }[]) => {
|
||||||
if (!allocs || allocs.length === 0) return "";
|
if (!allocs || allocs.length === 0) return "";
|
||||||
let html = `<div style="font-size:10px;color:var(--rs-text-muted);text-transform:uppercase;font-weight:600;margin-bottom:4px">${title}</div>`;
|
let html = `<div style="font-size:10px;color:var(--rs-text-muted);text-transform:uppercase;font-weight:600;margin-bottom:4px">${title}</div>`;
|
||||||
for (const a of allocs) {
|
for (let i = 0; i < allocs.length; i++) {
|
||||||
html += `<div class="icp-alloc-row">
|
const a = allocs[i];
|
||||||
|
html += `<div class="icp-alloc-row" style="flex-wrap:wrap;gap:4px">
|
||||||
<span class="icp-alloc-dot" style="background:${a.color}"></span>
|
<span class="icp-alloc-dot" style="background:${a.color}"></span>
|
||||||
<span style="flex:1">${this.esc(this.getNodeLabel(a.targetId))}</span>
|
<span style="flex:1;min-width:60px">${this.esc(this.getNodeLabel(a.targetId))}</span>
|
||||||
<span style="font-weight:600">${a.percentage}%</span>
|
<span class="icp-alloc-pct" style="font-weight:600;min-width:36px;text-align:right">${a.percentage}%</span>
|
||||||
|
${allocs.length >= 2 ? `<input type="range" class="icp-alloc-range" min="1" max="99" value="${a.percentage}"
|
||||||
|
data-alloc-type="${allocType}" data-alloc-idx="${i}" data-node-id="${node.id}"
|
||||||
|
style="width:100%;accent-color:${a.color};height:16px;margin:2px 0 4px"/>` : ""}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
|
|
@ -3509,17 +3486,17 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
|
|
||||||
if (node.type === "source") {
|
if (node.type === "source") {
|
||||||
const d = node.data as SourceNodeData;
|
const d = node.data as SourceNodeData;
|
||||||
const html = renderRows("Target Allocations", d.targetAllocations);
|
const html = renderRows("Target Allocations", "source", d.targetAllocations);
|
||||||
return html || '<div class="icp-empty">No allocations configured</div>';
|
return html || '<div class="icp-empty">No allocations configured</div>';
|
||||||
}
|
}
|
||||||
if (node.type === "funnel") {
|
if (node.type === "funnel") {
|
||||||
const d = node.data as FunnelNodeData;
|
const d = node.data as FunnelNodeData;
|
||||||
let html = renderRows("Spending Allocations", d.spendingAllocations);
|
let html = renderRows("Spending Allocations", "spending", d.spendingAllocations);
|
||||||
html += renderRows("Overflow Allocations", d.overflowAllocations);
|
html += renderRows("Overflow Allocations", "overflow", d.overflowAllocations);
|
||||||
return html || '<div class="icp-empty">No allocations configured</div>';
|
return html || '<div class="icp-empty">No allocations configured</div>';
|
||||||
}
|
}
|
||||||
const od = node.data as OutcomeNodeData;
|
const od = node.data as OutcomeNodeData;
|
||||||
const html = renderRows("Overflow Allocations", od.overflowAllocations || []);
|
const html = renderRows("Overflow Allocations", "overflow", od.overflowAllocations || []);
|
||||||
return html || '<div class="icp-empty">No allocations configured</div>';
|
return html || '<div class="icp-empty">No allocations configured</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3785,6 +3762,61 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
this.scheduleSave();
|
this.scheduleSave();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allocation range sliders — proportional rebalancing
|
||||||
|
overlay.querySelectorAll(".icp-alloc-range").forEach((el) => {
|
||||||
|
const input = el as HTMLInputElement;
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
const allocType = input.dataset.allocType!;
|
||||||
|
const idx = parseInt(input.dataset.allocIdx!, 10);
|
||||||
|
const allocs = this.getSplitAllocs(node.id, allocType);
|
||||||
|
if (!allocs || allocs.length < 2) return;
|
||||||
|
const newPct = parseInt(input.value, 10);
|
||||||
|
const oldPct = allocs[idx].percentage;
|
||||||
|
const delta = newPct - oldPct;
|
||||||
|
if (delta === 0) return;
|
||||||
|
|
||||||
|
// Proportional rebalancing of siblings
|
||||||
|
const MIN_PCT = 1;
|
||||||
|
const othersTotal = allocs.reduce((s, a, i) => i === idx ? s : s + a.percentage, 0);
|
||||||
|
allocs[idx].percentage = newPct;
|
||||||
|
const remaining = 100 - newPct;
|
||||||
|
|
||||||
|
for (let i = 0; i < allocs.length; i++) {
|
||||||
|
if (i === idx) continue;
|
||||||
|
if (othersTotal > 0) {
|
||||||
|
allocs[i].percentage = Math.max(MIN_PCT, Math.round((allocs[i].percentage / othersTotal) * remaining));
|
||||||
|
} else {
|
||||||
|
allocs[i].percentage = Math.max(MIN_PCT, Math.round(remaining / (allocs.length - 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Normalize to exactly 100
|
||||||
|
const total = allocs.reduce((s, a) => s + a.percentage, 0);
|
||||||
|
if (total !== 100 && allocs.length > 1) {
|
||||||
|
const lastOther = allocs.findIndex((_, i) => i !== idx);
|
||||||
|
if (lastOther >= 0) allocs[lastOther].percentage += 100 - total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sibling labels + slider values in the panel
|
||||||
|
const row = input.closest(".icp-alloc-row");
|
||||||
|
const pctSpan = row?.querySelector(".icp-alloc-pct");
|
||||||
|
if (pctSpan) pctSpan.textContent = `${newPct}%`;
|
||||||
|
const allRows = overlay.querySelectorAll(`.icp-alloc-range[data-alloc-type="${allocType}"]`);
|
||||||
|
allRows.forEach((sibEl) => {
|
||||||
|
const sib = sibEl as HTMLInputElement;
|
||||||
|
const si = parseInt(sib.dataset.allocIdx!, 10);
|
||||||
|
if (si === idx) return;
|
||||||
|
sib.value = String(allocs[si].percentage);
|
||||||
|
const sibRow = sib.closest(".icp-alloc-row");
|
||||||
|
const sibPct = sibRow?.querySelector(".icp-alloc-pct");
|
||||||
|
if (sibPct) sibPct.textContent = `${allocs[si].percentage}%`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redrawNodeOnly(node);
|
||||||
|
this.redrawEdges();
|
||||||
|
this.scheduleSave();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachThresholdDragListeners(overlay: Element, node: FlowNode) {
|
private attachThresholdDragListeners(overlay: Element, node: FlowNode) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue