From 123d61109e76734d3756bbb6cb942acc9dad789e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 15 Apr 2026 16:54:50 -0400 Subject: [PATCH] feat(rnetwork): add shell tab bar to graph view with Members as default Replace inline filter buttons with standard shell sub-tabs (Members, People, Companies, Trust, Layers). Default view now renders the 3D graph on the Members tab with proper shell navigation. Co-Authored-By: Claude Opus 4.6 --- .../rnetwork/components/folk-graph-viewer.ts | 108 ++++++++++-------- modules/rnetwork/mod.ts | 47 ++++++-- 2 files changed, 97 insertions(+), 58 deletions(-) diff --git a/modules/rnetwork/components/folk-graph-viewer.ts b/modules/rnetwork/components/folk-graph-viewer.ts index 3bbc2688..c4322f3b 100644 --- a/modules/rnetwork/components/folk-graph-viewer.ts +++ b/modules/rnetwork/components/folk-graph-viewer.ts @@ -195,12 +195,71 @@ class FolkGraphViewer extends HTMLElement { private _delegationsHandler: ((e: Event) => void) | null = null; + private _onTabChange = (e: Event) => { + const tab = (e as CustomEvent).detail?.tab; + if (!tab) return; + this.applyTab(tab); + }; + + private applyTab(tab: string) { + const wasTrust = this.trustMode; + const wasLayers = this.layersMode; + + switch (tab) { + case "members": + this.filter = "all"; + this.trustMode = false; + if (wasLayers) this.exitLayersMode(); + break; + case "people": + this.filter = "person"; + this.trustMode = false; + if (wasLayers) this.exitLayersMode(); + break; + case "companies": + this.filter = "company"; + this.trustMode = false; + if (wasLayers) this.exitLayersMode(); + break; + case "trust": + this.filter = "all"; + this.trustMode = true; + if (wasLayers) this.exitLayersMode(); + if (this.layoutMode !== "rings") { + this.layoutMode = "rings"; + const ringsBtn = this.shadow.getElementById("rings-toggle"); + if (ringsBtn) ringsBtn.classList.add("active"); + } + break; + case "layers": + this.filter = "all"; + this.trustMode = false; + if (!wasLayers) this.enterLayersMode(); + break; + } + this.updateAuthorityBar(); + // Trust mode change needs full data reload + if (this.trustMode !== wasTrust) { + this.loadData(); + } else { + this.updateGraphData(); + } + } + connectedCallback() { this.space = this.getAttribute("space") || "demo"; + + // Read initial tab from attribute or URL + const attrTab = this.getAttribute("active-tab"); + if (attrTab) this.applyTab(attrTab); + this.renderDOM(); this.loadData(); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rnetwork', context: 'Network Graph' })); + // Listen for shell tab bar changes + document.addEventListener("rapp-tab-change", this._onTabChange); + // Listen for cross-component delegation updates this._delegationsHandler = () => { this._textSpriteCache.clear(); @@ -219,6 +278,7 @@ class FolkGraphViewer extends HTMLElement { disconnectedCallback() { this._stopPresence?.(); + document.removeEventListener("rapp-tab-change", this._onTabChange); if (this._keyHandler) { document.removeEventListener("keydown", this._keyHandler); this._keyHandler = null; @@ -869,15 +929,8 @@ class FolkGraphViewer extends HTMLElement {
- - - - - - -
@@ -932,17 +985,6 @@ class FolkGraphViewer extends HTMLElement { } private attachListeners() { - // Filter buttons - this.shadow.querySelectorAll("[data-filter]").forEach(el => { - el.addEventListener("click", () => { - this.filter = (el as HTMLElement).dataset.filter as any; - // Update active state - this.shadow.querySelectorAll("[data-filter]").forEach(b => b.classList.remove("active")); - el.classList.add("active"); - this.updateGraphData(); - }); - }); - // Search let searchTimeout: any; this.shadow.getElementById("search-input")?.addEventListener("input", (e) => { @@ -951,21 +993,6 @@ class FolkGraphViewer extends HTMLElement { searchTimeout = setTimeout(() => this.updateGraphData(), 200); }); - // Trust toggle - this.shadow.getElementById("trust-toggle")?.addEventListener("click", () => { - this.trustMode = !this.trustMode; - const btn = this.shadow.getElementById("trust-toggle"); - if (btn) btn.classList.toggle("active", this.trustMode); - // Auto-enable rings when trust mode is turned on - if (this.trustMode && this.layoutMode !== "rings") { - this.layoutMode = "rings"; - const ringsBtn = this.shadow.getElementById("rings-toggle"); - if (ringsBtn) ringsBtn.classList.add("active"); - } - this.updateAuthorityBar(); - this.loadData(); - }); - // Rings toggle this.shadow.getElementById("rings-toggle")?.addEventListener("click", () => { this.layoutMode = this.layoutMode === "rings" ? "force" : "rings"; @@ -1028,17 +1055,6 @@ class FolkGraphViewer extends HTMLElement { }); }); - // Layers toggle - this.shadow.getElementById("layers-toggle")?.addEventListener("click", () => { - if (this.layersMode) { - this.exitLayersMode(); - } else { - this.enterLayersMode(); - } - const btn = this.shadow.getElementById("layers-toggle"); - if (btn) btn.classList.toggle("active", this.layersMode); - }); - // Keyboard shortcuts if (this._keyHandler) document.removeEventListener("keydown", this._keyHandler); this._keyHandler = (e: KeyboardEvent) => { @@ -1067,10 +1083,6 @@ class FolkGraphViewer extends HTMLElement { case "F": if (this.graph) this.graph.zoomToFit(300, 20); break; - case "t": - case "T": - this.shadow.getElementById("trust-toggle")?.click(); - break; case "l": case "L": this.shadow.getElementById("list-toggle")?.click(); diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts index bbe8e2bd..8731fedd 100644 --- a/modules/rnetwork/mod.ts +++ b/modules/rnetwork/mod.ts @@ -692,6 +692,17 @@ routes.get("/api/opportunities", async (c) => { return c.json({ opportunities }); }); +// ── Graph tabs (main view) ── +const GRAPH_TABS = [ + { id: "members", label: "Members" }, + { id: "people", label: "People" }, + { id: "companies", label: "Companies" }, + { id: "trust", label: "Trust" }, + { id: "layers", label: "Layers" }, +] as const; + +const GRAPH_TAB_IDS = new Set(GRAPH_TABS.map(t => t.id)); + // ── CRM sub-route — API-driven CRM view ── const CRM_TABS = [ { id: "pipeline", label: "Pipeline" }, @@ -735,6 +746,31 @@ routes.get("/crm/:tabId", (c) => { return c.html(renderCrm(space, tabId, c.get("isSubdomain"))); }); +// ── Graph sub-tab routes ── +function renderGraph(space: string, activeTab: string, isSubdomain: boolean) { + return renderShell({ + title: `${space} — Network | rSpace`, + moduleId: "rnetwork", + spaceSlug: space, + modules: getModuleInfoList(), + head: GRAPH3D_HEAD, + body: ``, + scripts: ``, + styles: ``, + tabs: [...GRAPH_TABS], + activeTab, + tabBasePath: isSubdomain ? `/rnetwork` : `/${space}/rnetwork`, + }); +} + +routes.get("/:tabId", (c, next) => { + const tabId = c.req.param("tabId"); + // Only handle graph tab IDs here; let other routes (crm, api, etc.) pass through + if (!GRAPH_TAB_IDS.has(tabId as any)) return next(); + const space = c.req.param("space") || "demo"; + return c.html(renderGraph(space, tabId, c.get("isSubdomain"))); +}); + // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; @@ -744,16 +780,7 @@ routes.get("/", (c) => { return c.redirect(c.get("isSubdomain") ? `/rnetwork/crm` : `/${space}/rnetwork/crm`, 301); } - return c.html(renderShell({ - title: `${space} — Network | rSpace`, - moduleId: "rnetwork", - spaceSlug: space, - modules: getModuleInfoList(), - head: GRAPH3D_HEAD, - body: ``, - scripts: ``, - styles: ``, - })); + return c.html(renderGraph(space, "members", c.get("isSubdomain"))); }); // ── MI Data Export ──