"use client"; // The dashboard's topology canvas (design comp): the current group's children // laid out as a node graph, with a 6-pattern "view-as" selector // (hub-spoke / pipeline / ring / mesh / swarm / debate). Clicking a node // selects it (a claw opens the computer slide-out; a company/team drills down). import type { ReactNode } from "react"; export interface CanvasNode { id: string; label: string; role: string; level: "company" | "team" | "claw"; /** online | offline | provisioning | running */ status?: string; } const MODES = ["hub-spoke", "pipeline", "ring", "mesh", "swarm", "debate"] as const; export type TopologyMode = (typeof MODES)[number]; // Map a TopologyKind (from the team graph) onto one of the 6 view modes. export function modeForKind(kind: string | null | undefined): TopologyMode { switch ((kind ?? "").toLowerCase()) { case "pipeline": case "ring": return "pipeline"; case "mesh": case "blackboard": return "mesh"; case "swarm": case "flat": case "holacratic": return "swarm"; case "debate": return "debate"; default: return "hub-spoke"; // hierarchical / hub_spoke / star_moe / market } } const GRADS: [string, string, string][] = [ ["#ff9a6a", "#ff6f4a", "#2a0d05"], ["#6fd0c0", "#4aa3b8", "#06201f"], ["#e8c46a", "#d89a3a", "#2a1d05"], ["#8a9af0", "#5a6ad8", "#0a0e2a"], ["#c98af0", "#9a5ad8", "#1a0a2a"], ["#5ec8d8", "#3a8aa0", "#06222a"], ]; const STATUS: Record = { running: "#5ec8d8", online: "#5fd08a", provisioning: "#e8b465", offline: "#3a3a40", }; const ini = (s: string) => (s || "?").trim().charAt(0).toUpperCase(); /** Node positions (percent) for a mode + count. Generalizes the comp's POS. */ function layout(mode: TopologyMode, n: number): { x: number; y: number }[] { if (n <= 0) return []; const ring = (count: number, r: number, offset = 0, cx = 50, cy = 50) => Array.from({ length: count }, (_, i) => { const a = (i / count) * Math.PI * 2 - Math.PI / 2 + offset; return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) }; }); switch (mode) { case "pipeline": return Array.from({ length: n }, (_, i) => ({ x: n === 1 ? 50 : 12 + (i * 76) / (n - 1), y: 50, })); case "ring": case "mesh": return ring(n, 36); case "swarm": return Array.from({ length: n }, (_, i) => { const a = (i / n) * Math.PI * 2; const r = 14 + (i % 3) * 7; return { x: 50 + r * Math.cos(a), y: 50 + r * Math.sin(a) }; }); case "debate": { const half = Math.ceil(n / 2); return Array.from({ length: n }, (_, i) => { const left = i < half; const col = left ? i : i - half; const count = left ? half : n - half; return { x: left ? 28 : 72, y: count === 1 ? 50 : 18 + (col * 64) / (count - 1) }; }); } case "hub-spoke": default: if (n === 1) return [{ x: 50, y: 50 }]; return [{ x: 50, y: 50 }, ...ring(n - 1, 34)]; } } /** Edge index pairs for a mode + count. */ function links(mode: TopologyMode, n: number): [number, number][] { const e: [number, number][] = []; switch (mode) { case "pipeline": for (let i = 0; i < n - 1; i++) e.push([i, i + 1]); break; case "ring": for (let i = 0; i < n; i++) e.push([i, (i + 1) % n]); break; case "mesh": for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) e.push([i, j]); break; case "swarm": for (let i = 0; i < n; i++) e.push([i, (i + 1) % n]); break; case "debate": { const half = Math.ceil(n / 2); for (let i = 0; i < half; i++) for (let j = half; j < n; j++) if ((i + j) % 2 === 0) e.push([i, j]); break; } case "hub-spoke": default: for (let i = 1; i < n; i++) e.push([0, i]); } return e; } export function TopologyCanvas({ nodes, mode, onModeChange, onNodeClick, selectedId, header, }: { nodes: CanvasNode[]; mode: TopologyMode; onModeChange: (m: TopologyMode) => void; onNodeClick: (n: CanvasNode) => void; selectedId?: string | null; header?: ReactNode; }) { const pos = layout(mode, nodes.length); const edges = links(mode, nodes.length); return (
{/* mode selector */}
{header}
{MODES.map((m) => ( ))}
{/* graph */}
{edges.map(([a, b], i) => { const p = pos[a]; const q = pos[b]; if (!p || !q) return null; const live = selectedId && (nodes[a]?.id === selectedId || nodes[b]?.id === selectedId); return ( ); })} {nodes.map((node, i) => { const p = pos[i]; if (!p) return null; const [from, to, ink] = GRADS[i % GRADS.length]; const selected = node.id === selectedId; const dot = STATUS[node.status ?? "online"] ?? STATUS.online; return ( ); })} {nodes.length === 0 ? (
No members yet.
) : null}
); }