feat(rnetwork): multi-select delegation with per-node sliders and fuzzy search
- Click any person/member node to add them to delegation selection - Each selected node gets 3 inline sliders (Gov/Econ/Tech) for weight assignment - Fuzzy search input in delegation panel to find and add members by name - Remaining weight display per authority - "Confirm All Delegations" commits all at once, recomputes weights live - Replaces old two-step popup with single-panel multi-select UX Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
48cbf22492
commit
e5b5c551b1
|
|
@ -96,11 +96,12 @@ class FolkGraphViewer extends HTMLElement {
|
|||
private authority: AuthoritySelection = "gov-ops";
|
||||
private layoutMode: "force" | "rings" = "force";
|
||||
private ringGuides: any[] = [];
|
||||
private delegationTarget: GraphNode | null = null;
|
||||
private delegationTotal = 50;
|
||||
private delegationSplit = { "gov-ops": 34, "fin-ops": 33, "dev-ops": 33 };
|
||||
private demoDelegations: GraphEdge[] = [];
|
||||
|
||||
// Multi-select delegation state
|
||||
private selectedDelegates: Map<string, { node: GraphNode; weights: Record<string, number> }> = new Map();
|
||||
private delegateSearchQuery = "";
|
||||
|
||||
// 3D graph instance
|
||||
private graph: any = null;
|
||||
private graphContainer: HTMLDivElement | null = null;
|
||||
|
|
@ -458,36 +459,60 @@ class FolkGraphViewer extends HTMLElement {
|
|||
}
|
||||
.node-label-org { font-size: 11px; font-weight: 600; }
|
||||
|
||||
.btn-delegate {
|
||||
padding: 6px 14px; border: 1px solid #a78bfa; border-radius: 8px;
|
||||
background: rgba(167,139,250,0.1); color: #a78bfa; cursor: pointer;
|
||||
font-size: 12px; font-weight: 600; margin-top: 8px;
|
||||
}
|
||||
.btn-delegate:hover { background: rgba(167,139,250,0.2); }
|
||||
|
||||
.deleg-popup {
|
||||
/* ── Delegation panel (bottom drawer) ── */
|
||||
.deleg-panel {
|
||||
display: none; background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
|
||||
border-radius: 12px; padding: 16px; margin-top: 12px; position: relative;
|
||||
border-radius: 12px; padding: 12px 16px; margin-top: 8px;
|
||||
}
|
||||
.deleg-popup.visible { display: block; }
|
||||
.deleg-popup-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
|
||||
.deleg-popup-close {
|
||||
position: absolute; top: 10px; right: 12px;
|
||||
background: none; border: none; color: var(--rs-text-muted); cursor: pointer; font-size: 16px;
|
||||
.deleg-panel.visible { display: block; }
|
||||
.deleg-panel-header {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
|
||||
}
|
||||
.deleg-slider-row {
|
||||
display: flex; align-items: center; gap: 10px; margin: 8px 0;
|
||||
.deleg-panel-title { font-size: 13px; font-weight: 600; flex: 1; }
|
||||
.deleg-panel-close {
|
||||
background: none; border: none; color: var(--rs-text-muted); cursor: pointer; font-size: 14px;
|
||||
}
|
||||
.deleg-slider-label { font-size: 12px; font-weight: 500; min-width: 50px; }
|
||||
.deleg-slider { flex: 1; accent-color: #a78bfa; }
|
||||
.deleg-slider-val { font-size: 12px; font-weight: 700; min-width: 36px; text-align: right; }
|
||||
.deleg-confirm {
|
||||
.deleg-search-wrap { position: relative; margin-bottom: 8px; }
|
||||
.deleg-search {
|
||||
width: 100%; padding: 6px 10px; border: 1px solid var(--rs-input-border);
|
||||
border-radius: 8px; background: var(--rs-input-bg); color: var(--rs-input-text);
|
||||
font-size: 12px; outline: none;
|
||||
}
|
||||
.deleg-search:focus { border-color: #a78bfa; }
|
||||
.deleg-results {
|
||||
position: absolute; top: 100%; left: 0; right: 0; z-index: 20;
|
||||
background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
|
||||
border-radius: 8px; max-height: 160px; overflow-y: auto; margin-top: 2px;
|
||||
}
|
||||
.deleg-result-item {
|
||||
padding: 6px 10px; cursor: pointer; font-size: 12px;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.deleg-result-item:hover { background: var(--rs-bg-hover, rgba(255,255,255,0.05)); }
|
||||
.deleg-result-role { font-size: 10px; color: var(--rs-text-muted); margin-left: auto; }
|
||||
|
||||
.deleg-row {
|
||||
display: flex; align-items: center; gap: 6px; padding: 6px 0;
|
||||
border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.06));
|
||||
}
|
||||
.deleg-row:last-child { border-bottom: none; }
|
||||
.deleg-row-name { font-size: 12px; font-weight: 500; min-width: 90px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.deleg-row-sliders { display: flex; gap: 4px; flex: 1; align-items: center; }
|
||||
.deleg-mini-slider { width: 60px; height: 4px; accent-color: #a78bfa; }
|
||||
.deleg-mini-val { font-size: 10px; font-weight: 700; min-width: 24px; text-align: right; }
|
||||
.deleg-row-remove {
|
||||
background: none; border: none; color: var(--rs-text-muted); cursor: pointer;
|
||||
font-size: 12px; padding: 2px 4px; border-radius: 4px;
|
||||
}
|
||||
.deleg-row-remove:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
|
||||
.deleg-confirm-all {
|
||||
padding: 6px 16px; border: none; border-radius: 8px;
|
||||
background: #a78bfa; color: #fff; cursor: pointer;
|
||||
font-size: 13px; font-weight: 600; margin-top: 10px;
|
||||
font-size: 12px; font-weight: 600; margin-top: 8px; width: 100%;
|
||||
}
|
||||
.deleg-confirm:hover { background: #8b5cf6; }
|
||||
.deleg-step-label { font-size: 11px; color: var(--rs-text-muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.deleg-confirm-all:hover { background: #8b5cf6; }
|
||||
.deleg-remaining { font-size: 11px; color: var(--rs-text-muted); margin-top: 6px; }
|
||||
.deleg-auth-label { font-size: 9px; font-weight: 600; min-width: 30px; text-align: center; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.graph-canvas { min-height: 300px; }
|
||||
|
|
@ -525,7 +550,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
</div>
|
||||
|
||||
<div class="detail-panel" id="detail-panel"></div>
|
||||
<div class="deleg-popup" id="deleg-popup"></div>
|
||||
<div class="deleg-panel" id="deleg-panel"></div>
|
||||
|
||||
<div class="legend" id="legend">
|
||||
<div class="legend-item"><span class="legend-dot dot-person"></span> People</div>
|
||||
|
|
@ -730,13 +755,26 @@ class FolkGraphViewer extends HTMLElement {
|
|||
return "#c4b5fd";
|
||||
})
|
||||
.onNodeClick((node: GraphNode) => {
|
||||
const canDelegate = node.type === "rspace_user" || node.type === "person";
|
||||
|
||||
// Toggle detail panel for inspection
|
||||
if (this.selectedNode?.id === node.id) {
|
||||
this.selectedNode = null;
|
||||
} else {
|
||||
this.selectedNode = node;
|
||||
}
|
||||
this.updateDetailPanel();
|
||||
this.updateGraphData(); // refresh highlight
|
||||
|
||||
// Add to delegation selection if delegatable
|
||||
if (canDelegate && !this.selectedDelegates.has(node.id)) {
|
||||
this.selectedDelegates.set(node.id, {
|
||||
node,
|
||||
weights: { "gov-ops": 10, "fin-ops": 10, "dev-ops": 10 },
|
||||
});
|
||||
this.renderDelegationPanel();
|
||||
}
|
||||
|
||||
this.updateGraphData();
|
||||
})
|
||||
.d3AlphaDecay(0.02)
|
||||
.d3VelocityDecay(0.3)
|
||||
|
|
@ -1161,8 +1199,6 @@ class FolkGraphViewer extends HTMLElement {
|
|||
}).join("");
|
||||
}
|
||||
|
||||
const canDelegate = n.type === "rspace_user" || n.type === "person";
|
||||
|
||||
panel.classList.add("visible");
|
||||
panel.innerHTML = `
|
||||
<div class="detail-header">
|
||||
|
|
@ -1176,126 +1212,207 @@ class FolkGraphViewer extends HTMLElement {
|
|||
${n.description ? `<p class="detail-desc">${this.esc(n.description)}</p>` : ""}
|
||||
${trust >= 0 ? `<div class="detail-trust"><span class="trust-label">Trust Score</span><span class="trust-bar"><span class="trust-fill" style="width:${trust}%"></span></span><span class="trust-val">${trust}</span></div>` : ""}
|
||||
${weightHtml}
|
||||
${canDelegate ? `<button class="btn-delegate" id="btn-delegate">Delegate to ${this.esc(n.name.split(" ")[0])}</button>` : ""}
|
||||
${connected.length > 0 ? `
|
||||
<div class="detail-section">Connected (${connected.length})</div>
|
||||
${connected.map(c => `<div class="detail-conn"><span class="conn-dot" style="background:${c.type === "company" ? "#22c55e" : "#3b82f6"}"></span>${this.esc(c.name)}<span class="conn-role">${this.esc(c.role || c.type)}</span></div>`).join("")}
|
||||
` : ""}
|
||||
`;
|
||||
|
||||
// Delegate button listener
|
||||
this.shadow.getElementById("btn-delegate")?.addEventListener("click", () => {
|
||||
this.delegationTarget = n;
|
||||
this.delegationTotal = 50;
|
||||
this.delegationSplit = { "gov-ops": 34, "fin-ops": 33, "dev-ops": 33 };
|
||||
this.showDelegationPopup();
|
||||
});
|
||||
}
|
||||
|
||||
private showDelegationPopup() {
|
||||
const popup = this.shadow.getElementById("deleg-popup");
|
||||
if (!popup || !this.delegationTarget) return;
|
||||
// ── Delegation panel (multi-select + fuzzy search) ──
|
||||
|
||||
const target = this.delegationTarget;
|
||||
const total = this.delegationTotal;
|
||||
const split = this.delegationSplit;
|
||||
private renderDelegationPanel() {
|
||||
const panel = this.shadow.getElementById("deleg-panel");
|
||||
if (!panel) return;
|
||||
|
||||
popup.classList.add("visible");
|
||||
popup.innerHTML = `
|
||||
<button class="deleg-popup-close" id="deleg-close">\u2715</button>
|
||||
<div class="deleg-popup-title">Delegate to ${this.esc(target.name)}</div>
|
||||
if (this.selectedDelegates.size === 0) {
|
||||
panel.classList.remove("visible");
|
||||
panel.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="deleg-step-label">Step 1: Total weight</div>
|
||||
<div class="deleg-slider-row">
|
||||
<span class="deleg-slider-label">Total</span>
|
||||
<input type="range" class="deleg-slider" id="deleg-total" min="0" max="100" value="${total}">
|
||||
<span class="deleg-slider-val" id="deleg-total-val">${total}%</span>
|
||||
// Compute remaining weight per authority
|
||||
const spent: Record<string, number> = { "gov-ops": 0, "fin-ops": 0, "dev-ops": 0 };
|
||||
this.selectedDelegates.forEach(({ weights }) => {
|
||||
for (const a of DELEGATION_AUTHORITIES) spent[a] += weights[a] || 0;
|
||||
});
|
||||
|
||||
panel.classList.add("visible");
|
||||
const rows: string[] = [];
|
||||
this.selectedDelegates.forEach(({ node, weights }, id) => {
|
||||
rows.push(`
|
||||
<div class="deleg-row" data-deleg-id="${id}">
|
||||
<span class="deleg-row-name" title="${this.esc(node.name)}">${this.esc(node.name)}</span>
|
||||
<div class="deleg-row-sliders">
|
||||
${DELEGATION_AUTHORITIES.map(a => {
|
||||
const disp = AUTHORITY_DISPLAY[a];
|
||||
const w = weights[a] || 0;
|
||||
return `<span class="deleg-auth-label" style="color:${disp?.color}">${disp?.label}</span>
|
||||
<input type="range" class="deleg-mini-slider" data-did="${id}" data-auth="${a}" min="0" max="100" value="${w}" style="accent-color:${disp?.color}">
|
||||
<span class="deleg-mini-val" data-did-val="${id}-${a}" style="color:${disp?.color}">${w}%</span>`;
|
||||
}).join("")}
|
||||
</div>
|
||||
<button class="deleg-row-remove" data-remove="${id}" title="Remove">\u2715</button>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="deleg-panel-header">
|
||||
<span class="deleg-panel-title">Delegate Weight (${this.selectedDelegates.size} selected)</span>
|
||||
<button class="deleg-panel-close" id="deleg-panel-close">\u2715</button>
|
||||
</div>
|
||||
|
||||
<div class="deleg-step-label">Step 2: Domain split</div>
|
||||
${DELEGATION_AUTHORITIES.map(a => {
|
||||
const disp = AUTHORITY_DISPLAY[a];
|
||||
const pct = split[a] || 0;
|
||||
return `<div class="deleg-slider-row">
|
||||
<span class="deleg-slider-label" style="color:${disp?.color || '#a78bfa'}">${disp?.label || a}</span>
|
||||
<input type="range" class="deleg-slider" data-deleg-auth="${a}" min="0" max="100" value="${pct}" style="accent-color:${disp?.color || '#a78bfa'}">
|
||||
<span class="deleg-slider-val" data-deleg-val="${a}">${pct}%</span>
|
||||
</div>`;
|
||||
}).join("")}
|
||||
<div class="deleg-search-wrap">
|
||||
<input class="deleg-search" type="text" placeholder="Search members to add..." id="deleg-search" value="${this.esc(this.delegateSearchQuery)}">
|
||||
<div class="deleg-results" id="deleg-results" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<button class="deleg-confirm" id="deleg-confirm">Confirm Delegation</button>
|
||||
${rows.join("")}
|
||||
|
||||
<div class="deleg-remaining">${DELEGATION_AUTHORITIES.map(a => {
|
||||
const disp = AUTHORITY_DISPLAY[a];
|
||||
return `<span style="color:${disp?.color}">${disp?.label}: ${Math.max(0, 100 - spent[a])}% left</span>`;
|
||||
}).join(" · ")}</div>
|
||||
|
||||
<button class="deleg-confirm-all" id="deleg-confirm-all">Confirm All Delegations</button>
|
||||
`;
|
||||
|
||||
this.attachDelegationListeners(panel);
|
||||
}
|
||||
|
||||
private attachDelegationListeners(panel: HTMLElement) {
|
||||
// Close
|
||||
this.shadow.getElementById("deleg-close")?.addEventListener("click", () => {
|
||||
this.delegationTarget = null;
|
||||
popup.classList.remove("visible");
|
||||
this.shadow.getElementById("deleg-panel-close")?.addEventListener("click", () => {
|
||||
this.selectedDelegates.clear();
|
||||
this.delegateSearchQuery = "";
|
||||
this.renderDelegationPanel();
|
||||
});
|
||||
|
||||
// Total slider
|
||||
this.shadow.getElementById("deleg-total")?.addEventListener("input", (e) => {
|
||||
this.delegationTotal = parseInt((e.target as HTMLInputElement).value);
|
||||
const valEl = this.shadow.getElementById("deleg-total-val");
|
||||
if (valEl) valEl.textContent = this.delegationTotal + "%";
|
||||
// Remove buttons
|
||||
panel.querySelectorAll("[data-remove]").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
const id = (el as HTMLElement).dataset.remove!;
|
||||
this.selectedDelegates.delete(id);
|
||||
this.renderDelegationPanel();
|
||||
});
|
||||
});
|
||||
|
||||
// Domain sliders — adjust others proportionally
|
||||
popup.querySelectorAll("[data-deleg-auth]").forEach(el => {
|
||||
// Per-delegate per-authority sliders
|
||||
panel.querySelectorAll("[data-did][data-auth]").forEach(el => {
|
||||
el.addEventListener("input", (e) => {
|
||||
const auth = (el as HTMLElement).dataset.delegAuth!;
|
||||
const newVal = parseInt((e.target as HTMLInputElement).value);
|
||||
const others = DELEGATION_AUTHORITIES.filter(a => a !== auth);
|
||||
const oldOtherSum = others.reduce((s, a) => s + (this.delegationSplit[a] || 0), 0);
|
||||
this.delegationSplit[auth] = newVal;
|
||||
|
||||
// Redistribute remaining to others proportionally
|
||||
const remaining = 100 - newVal;
|
||||
if (oldOtherSum > 0) {
|
||||
for (const o of others) {
|
||||
this.delegationSplit[o] = Math.round((this.delegationSplit[o] / oldOtherSum) * remaining);
|
||||
}
|
||||
} else {
|
||||
const each = Math.round(remaining / others.length);
|
||||
for (const o of others) this.delegationSplit[o] = each;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
for (const a of DELEGATION_AUTHORITIES) {
|
||||
const slider = popup.querySelector(`[data-deleg-auth="${a}"]`) as HTMLInputElement;
|
||||
const val = popup.querySelector(`[data-deleg-val="${a}"]`);
|
||||
if (slider) slider.value = String(this.delegationSplit[a]);
|
||||
if (val) val.textContent = this.delegationSplit[a] + "%";
|
||||
const id = (el as HTMLElement).dataset.did!;
|
||||
const auth = (el as HTMLElement).dataset.auth!;
|
||||
const val = parseInt((e.target as HTMLInputElement).value);
|
||||
const entry = this.selectedDelegates.get(id);
|
||||
if (entry) {
|
||||
entry.weights[auth] = val;
|
||||
const valEl = panel.querySelector(`[data-did-val="${id}-${auth}"]`);
|
||||
if (valEl) valEl.textContent = val + "%";
|
||||
// Update remaining display
|
||||
this.updateRemainingDisplay(panel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm
|
||||
this.shadow.getElementById("deleg-confirm")?.addEventListener("click", () => {
|
||||
this.confirmDelegation();
|
||||
// Fuzzy search
|
||||
const searchInput = this.shadow.getElementById("deleg-search") as HTMLInputElement | null;
|
||||
const resultsDiv = this.shadow.getElementById("deleg-results");
|
||||
let searchTimeout: any;
|
||||
|
||||
searchInput?.addEventListener("input", () => {
|
||||
this.delegateSearchQuery = searchInput.value;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
this.updateSearchResults(searchInput.value, resultsDiv);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
searchInput?.addEventListener("focus", () => {
|
||||
if (searchInput.value.trim()) {
|
||||
this.updateSearchResults(searchInput.value, resultsDiv);
|
||||
}
|
||||
});
|
||||
|
||||
// Confirm all
|
||||
this.shadow.getElementById("deleg-confirm-all")?.addEventListener("click", () => {
|
||||
this.confirmAllDelegations();
|
||||
});
|
||||
}
|
||||
|
||||
private confirmDelegation() {
|
||||
if (!this.delegationTarget) return;
|
||||
const target = this.delegationTarget;
|
||||
const totalWeight = this.delegationTotal / 100;
|
||||
|
||||
// Create delegation edges for each authority
|
||||
for (const a of DELEGATION_AUTHORITIES) {
|
||||
const pct = this.delegationSplit[a] || 0;
|
||||
const weight = Math.round(totalWeight * (pct / 100) * 100) / 100;
|
||||
if (weight <= 0) continue;
|
||||
|
||||
const edge: GraphEdge = {
|
||||
source: "me-demo",
|
||||
target: target.id,
|
||||
type: "delegates_to",
|
||||
weight,
|
||||
authority: a,
|
||||
};
|
||||
this.edges.push(edge);
|
||||
this.demoDelegations.push(edge);
|
||||
private updateRemainingDisplay(panel: HTMLElement) {
|
||||
const spent: Record<string, number> = { "gov-ops": 0, "fin-ops": 0, "dev-ops": 0 };
|
||||
this.selectedDelegates.forEach(({ weights }) => {
|
||||
for (const a of DELEGATION_AUTHORITIES) spent[a] += weights[a] || 0;
|
||||
});
|
||||
const remainEl = panel.querySelector(".deleg-remaining");
|
||||
if (remainEl) {
|
||||
remainEl.innerHTML = DELEGATION_AUTHORITIES.map(a => {
|
||||
const disp = AUTHORITY_DISPLAY[a];
|
||||
const left = Math.max(0, 100 - spent[a]);
|
||||
return `<span style="color:${disp?.color}${left === 0 ? ';opacity:0.5' : ''}">${disp?.label}: ${left}% left</span>`;
|
||||
}).join(" · ");
|
||||
}
|
||||
}
|
||||
|
||||
private fuzzyMatch(query: string, name: string): boolean {
|
||||
const q = query.toLowerCase();
|
||||
const n = name.toLowerCase();
|
||||
// Substring match
|
||||
if (n.includes(q)) return true;
|
||||
// Fuzzy: all query chars appear in order
|
||||
let qi = 0;
|
||||
for (let i = 0; i < n.length && qi < q.length; i++) {
|
||||
if (n[i] === q[qi]) qi++;
|
||||
}
|
||||
return qi === q.length;
|
||||
}
|
||||
|
||||
private updateSearchResults(query: string, resultsDiv: HTMLElement | null) {
|
||||
if (!resultsDiv) return;
|
||||
const q = query.trim();
|
||||
if (!q) {
|
||||
resultsDiv.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = this.nodes.filter(n =>
|
||||
(n.type === "rspace_user" || n.type === "person") &&
|
||||
!this.selectedDelegates.has(n.id) &&
|
||||
this.fuzzyMatch(q, n.name)
|
||||
).slice(0, 8);
|
||||
|
||||
if (matches.length === 0) {
|
||||
resultsDiv.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
resultsDiv.style.display = "block";
|
||||
resultsDiv.innerHTML = matches.map(n =>
|
||||
`<div class="deleg-result-item" data-add-id="${n.id}">
|
||||
<span>${this.esc(n.name)}</span>
|
||||
<span class="deleg-result-role">${n.role || n.type}</span>
|
||||
</div>`
|
||||
).join("");
|
||||
|
||||
resultsDiv.querySelectorAll("[data-add-id]").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
const id = (el as HTMLElement).dataset.addId!;
|
||||
const node = this.nodes.find(n => n.id === id);
|
||||
if (node) {
|
||||
this.selectedDelegates.set(id, {
|
||||
node,
|
||||
weights: { "gov-ops": 10, "fin-ops": 10, "dev-ops": 10 },
|
||||
});
|
||||
this.delegateSearchQuery = "";
|
||||
this.renderDelegationPanel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private confirmAllDelegations() {
|
||||
if (this.selectedDelegates.size === 0) return;
|
||||
|
||||
// Ensure "me-demo" node exists
|
||||
if (!this.nodes.find(n => n.id === "me-demo")) {
|
||||
|
|
@ -1308,13 +1425,28 @@ class FolkGraphViewer extends HTMLElement {
|
|||
});
|
||||
}
|
||||
|
||||
// Recompute weight accounting
|
||||
this.recomputeWeightAccounting();
|
||||
// Create delegation edges for each selected delegate
|
||||
this.selectedDelegates.forEach(({ node, weights }) => {
|
||||
for (const a of DELEGATION_AUTHORITIES) {
|
||||
const weight = Math.round((weights[a] || 0) / 100 * 100) / 100;
|
||||
if (weight <= 0) continue;
|
||||
const edge: GraphEdge = {
|
||||
source: "me-demo",
|
||||
target: node.id,
|
||||
type: "delegates_to",
|
||||
weight,
|
||||
authority: a,
|
||||
};
|
||||
this.edges.push(edge);
|
||||
this.demoDelegations.push(edge);
|
||||
}
|
||||
});
|
||||
|
||||
// Close popup and refresh
|
||||
this.delegationTarget = null;
|
||||
const popup = this.shadow.getElementById("deleg-popup");
|
||||
if (popup) popup.classList.remove("visible");
|
||||
// Recompute weight accounting + refresh
|
||||
this.recomputeWeightAccounting();
|
||||
this.selectedDelegates.clear();
|
||||
this.delegateSearchQuery = "";
|
||||
this.renderDelegationPanel();
|
||||
this.updateGraphData();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue