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 ──