diff --git a/modules/rnetwork/components/folk-graph-viewer.ts b/modules/rnetwork/components/folk-graph-viewer.ts index 6a705ac..fc01bbb 100644 --- a/modules/rnetwork/components/folk-graph-viewer.ts +++ b/modules/rnetwork/components/folk-graph-viewer.ts @@ -97,6 +97,7 @@ class FolkGraphViewer extends HTMLElement { private layoutMode: "force" | "rings" = "force"; private ringGuides: any[] = []; private demoDelegations: GraphEdge[] = []; + private showMemberList = false; // Multi-select delegation state private selectedDelegates: Map }> = new Map(); @@ -309,9 +310,9 @@ class FolkGraphViewer extends HTMLElement { const vals = Object.values(acct.effectiveWeight); ew = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0; } - // Normalize against max in current filtered view + // Normalize against max in current filtered view — dramatic sizing const maxEW = this._currentMaxEffectiveWeight || 1; - return 4 + (ew / maxEW) * 26; + return 6 + (ew / maxEW) * 50; } if (node.type === "rspace_user") return 10; return 12; @@ -364,13 +365,43 @@ class FolkGraphViewer extends HTMLElement { .filter-btn:hover { border-color: var(--rs-border-strong); } .filter-btn.active { border-color: var(--rs-primary-hover); color: var(--rs-primary-hover); } + .graph-row { + display: flex; flex: 1; gap: 0; min-height: 400px; + } .graph-canvas { - width: 100%; flex: 1; min-height: 400px; border-radius: 12px; + flex: 1; min-height: 400px; border-radius: 12px; background: var(--rs-canvas-bg); border: 1px solid var(--rs-border); position: relative; overflow: hidden; } .graph-canvas canvas { border-radius: 12px; } + .member-list-panel { + display: none; width: 260px; flex-shrink: 0; + background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); + border-radius: 0 12px 12px 0; overflow-y: auto; + font-size: 12px; margin-left: -1px; + } + .member-list-panel.visible { display: block; } + .member-group { padding: 8px 12px; } + .member-group-header { + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.05em; padding: 6px 0 4px; border-bottom: 1px solid var(--rs-border); + display: flex; justify-content: space-between; align-items: center; + } + .member-group-count { + font-size: 10px; font-weight: 400; color: var(--rs-text-muted); + background: var(--rs-bg-surface-raised, rgba(255,255,255,0.06)); + padding: 1px 6px; border-radius: 8px; + } + .member-item { + padding: 4px 0; display: flex; align-items: center; gap: 6px; + cursor: pointer; border-radius: 4px; + } + .member-item:hover { background: var(--rs-bg-hover, rgba(255,255,255,0.04)); } + .member-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + .member-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .member-weight { font-size: 10px; color: var(--rs-text-muted); font-weight: 600; min-width: 28px; text-align: right; } + .zoom-controls { position: absolute; bottom: 12px; right: 12px; display: flex; align-items: center; gap: 4px; @@ -515,7 +546,9 @@ class FolkGraphViewer extends HTMLElement { .deleg-auth-label { font-size: 9px; font-weight: 600; min-width: 30px; text-align: center; } @media (max-width: 768px) { + .graph-row { flex-direction: column; } .graph-canvas { min-height: 300px; } + .member-list-panel { width: 100%; max-height: 200px; border-radius: 0 0 12px 12px; margin-left: 0; margin-top: -1px; } .workspace-list { grid-template-columns: 1fr; } .stats { flex-wrap: wrap; gap: 12px; } .toolbar { flex-direction: column; align-items: stretch; } @@ -532,6 +565,7 @@ class FolkGraphViewer extends HTMLElement { +
@@ -539,14 +573,17 @@ class FolkGraphViewer extends HTMLElement { ${DELEGATION_AUTHORITIES.map(a => ``).join("")}
-
-
-
- - 100% - - +
+
+
+
+ + 100% + + +
+
@@ -637,21 +674,36 @@ class FolkGraphViewer extends HTMLElement { } }); - // Zoom controls + // Zoom controls — aggressive steps for fast navigation this.shadow.getElementById("zoom-in")?.addEventListener("click", () => { if (!this.graph) return; const cam = this.graph.camera(); const dist = cam.position.length(); - this.animateCameraDistance(dist * 0.75); + this.animateCameraDistance(dist * 0.5); }); this.shadow.getElementById("zoom-out")?.addEventListener("click", () => { if (!this.graph) return; const cam = this.graph.camera(); const dist = cam.position.length(); - this.animateCameraDistance(dist * 1.33); + this.animateCameraDistance(dist * 2); }); this.shadow.getElementById("zoom-fit")?.addEventListener("click", () => { - if (this.graph) this.graph.zoomToFit(400, 40); + if (this.graph) this.graph.zoomToFit(300, 20); + }); + + // Member list toggle + this.shadow.getElementById("list-toggle")?.addEventListener("click", () => { + this.showMemberList = !this.showMemberList; + const btn = this.shadow.getElementById("list-toggle"); + if (btn) btn.classList.toggle("active", this.showMemberList); + this.updateMemberList(); + // Resize graph when panel toggles + requestAnimationFrame(() => { + if (this.graph && this.graphContainer) { + const rect = this.graphContainer.getBoundingClientRect(); + if (rect.width > 0) this.graph.width(rect.width); + } + }); }); } @@ -663,7 +715,7 @@ class FolkGraphViewer extends HTMLElement { this.graph.cameraPosition( { x: target.x, y: target.y, z: target.z }, undefined, - 600 + 200 ); } @@ -816,6 +868,7 @@ class FolkGraphViewer extends HTMLElement { controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 }; controls.enableDamping = true; controls.dampingFactor = 0.12; + controls.zoomSpeed = 2.5; // faster scroll wheel zoom } // ResizeObserver for responsive canvas @@ -936,17 +989,17 @@ class FolkGraphViewer extends HTMLElement { if (!ctx) return null; const text = node.name; - const fontSize = node.type === "company" ? 28 : 24; - canvas.width = 256; - canvas.height = 64; + const fontSize = node.type === "company" ? 42 : 36; + canvas.width = 512; + canvas.height = 96; - ctx.font = `${node.type === "company" ? "600" : "400"} ${fontSize}px system-ui, sans-serif`; + ctx.font = `${node.type === "company" ? "600" : "500"} ${fontSize}px system-ui, sans-serif`; ctx.fillStyle = "#e2e8f0"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; - ctx.shadowColor = "rgba(0,0,0,0.8)"; - ctx.shadowBlur = 4; - ctx.fillText(text.length > 20 ? text.slice(0, 18) + "\u2026" : text, 128, 32); + ctx.shadowColor = "rgba(0,0,0,0.9)"; + ctx.shadowBlur = 6; + ctx.fillText(text.length > 24 ? text.slice(0, 22) + "\u2026" : text, 256, 48); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; @@ -956,7 +1009,7 @@ class FolkGraphViewer extends HTMLElement { depthTest: false, }); const sprite = new THREE.Sprite(spriteMaterial); - sprite.scale.set(8, 2, 1); + sprite.scale.set(14, 3.5, 1); return sprite; } @@ -1055,6 +1108,9 @@ class FolkGraphViewer extends HTMLElement { setTimeout(() => { if (this.graph) this.graph.zoomToFit(400, 40); }, 500); + + // Refresh member list if visible + if (this.showMemberList) this.updateMemberList(); } // ── Ring layout ── @@ -1493,6 +1549,77 @@ class FolkGraphViewer extends HTMLElement { } } + private updateMemberList() { + const panel = this.shadow.getElementById("member-list-panel"); + if (!panel) return; + + if (!this.showMemberList) { + panel.classList.remove("visible"); + return; + } + panel.classList.add("visible"); + + const groups: { label: string; color: string; role: string; nodes: GraphNode[] }[] = [ + { label: "Admins", color: "#a78bfa", role: "admin", nodes: [] }, + { label: "Members", color: "#10b981", role: "member", nodes: [] }, + { label: "Viewers", color: "#3b82f6", role: "viewer", nodes: [] }, + ]; + + const filtered = this.getFilteredNodes().filter(n => n.type === "rspace_user" || n.type === "person"); + for (const n of filtered) { + const g = groups.find(g => g.role === n.role) || groups[2]; + g.nodes.push(n); + } + + // Sort each group by effective weight (descending) + for (const g of groups) { + g.nodes.sort((a, b) => { + const aw = a.weightAccounting ? Object.values(a.weightAccounting.effectiveWeight).reduce((s, v) => s + v, 0) : 0; + const bw = b.weightAccounting ? Object.values(b.weightAccounting.effectiveWeight).reduce((s, v) => s + v, 0) : 0; + return bw - aw; + }); + } + + panel.innerHTML = groups.filter(g => g.nodes.length > 0).map(g => ` +
+
+ ${g.label} + ${g.nodes.length} +
+ ${g.nodes.map(n => { + const ew = n.weightAccounting ? (Object.values(n.weightAccounting.effectiveWeight).reduce((s, v) => s + v, 0) / 3).toFixed(1) : ""; + return `
+ + ${this.esc(n.name)} + ${ew ? `${ew}` : ""} +
`; + }).join("")} +
+ `).join(""); + + // Click to select/focus node in graph + panel.querySelectorAll("[data-member-id]").forEach(el => { + el.addEventListener("click", () => { + const id = (el as HTMLElement).dataset.memberId!; + const node = this.nodes.find(n => n.id === id); + if (node && this.graph) { + this.selectedNode = node; + this.updateDetailPanel(); + this.updateGraphData(); + // Fly camera to node + if (node.x != null && node.y != null && node.z != null) { + const dist = 60; + this.graph.cameraPosition( + { x: node.x + dist, y: node.y + dist * 0.3, z: node.z + dist }, + { x: node.x, y: node.y, z: node.z }, + 400 + ); + } + } + }); + }); + } + private updateWorkspaceList() { const section = this.shadow.getElementById("workspace-section"); if (!section) return;