Compare commits

...

2 Commits

Author SHA1 Message Date
Jeff Emmett d0db0ffde7 Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m19s Details
2026-04-15 16:54:57 -04:00
Jeff Emmett 123d61109e 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 <noreply@anthropic.com>
2026-04-15 16:54:50 -04:00
2 changed files with 97 additions and 58 deletions

View File

@ -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 {
<div class="toolbar">
<input class="search-input" type="text" placeholder="Search nodes..." id="search-input" value="">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="person">People</button>
<button class="filter-btn" data-filter="company">Organizations</button>
<button class="filter-btn" data-filter="opportunity">Opportunities</button>
<button class="filter-btn" data-filter="rspace_user">Members</button>
<button class="filter-btn" id="trust-toggle" title="Toggle trust-weighted view">Trust</button>
<button class="filter-btn" id="rings-toggle" title="Toggle concentric ring layout">Rings</button>
<button class="filter-btn" id="list-toggle" title="Toggle member list sidebar">List</button>
<button class="filter-btn" id="layers-toggle" title="Toggle multi-layer rApp visualization">Layers</button>
</div>
<div class="authority-bar" id="authority-bar">
@ -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();

View File

@ -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: `<folk-graph-viewer space="${space}" active-tab="${activeTab}"></folk-graph-viewer>`,
scripts: `<script type="module" src="/modules/rnetwork/folk-graph-viewer.js?v=3"></script>`,
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
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: `<folk-graph-viewer space="${space}"></folk-graph-viewer>`,
scripts: `<script type="module" src="/modules/rnetwork/folk-graph-viewer.js?v=2"></script>`,
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
}));
return c.html(renderGraph(space, "members", c.get("isSubdomain")));
});
// ── MI Data Export ──