Dashboard: faithful 1:1 clone of the design doc (example workspace + claw view)
Replaces the approximation with an exact port of Clawmates Dashboard.html: - breadcrumb Acme Org / Acme Corp / Growth Team / <claw> (active-tier highlight) - ORG/CO/TEAM/CLAW tier rail; clicking a tier shows the example structure (org→3 companies, company→4 teams, team→6 agents) - morphing topology canvas with all 6 patterns (hub-spoke/pipeline/ring/mesh/ swarm/debate) + exact POS/LNK tables + animated links - company grid canvas (team cards) + org radial canvas (company nodes) - claw view: left compartment panel + ANATOMY canvas (Skills/Personality/Memory/ Tools·Doors/Capabilities/Safety) + the agent "Computer" slide-out (apps / now-running / dock), matching the doc - exact status colours, gradients, animations, status bar Example workspace data baked in (as in the doc) so every tier is populated; real-workspace wiring is a follow-up layer. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bd6f48c2b7
commit
9f266d5806
@@ -1,401 +1,542 @@
|
||||
"use client";
|
||||
|
||||
// The integrated post-login dashboard (design comp: Clawmates Dashboard.html).
|
||||
// One screen: top bar (breadcrumb) · ORG/CO/TEAM/CLAW tier rail · context list ·
|
||||
// topology canvas · agent "computer" slide-out · status bar. Self-contained
|
||||
// in-memory state machine, wired to the existing API.
|
||||
// The integrated dashboard — a faithful clone of the design doc
|
||||
// (~/downloads/clawmates/Clawmates Dashboard.html): top bar + breadcrumb,
|
||||
// ORG/CO/TEAM/CLAW tier rail, tier-aware context list, the morphing topology
|
||||
// canvas (6 patterns), the claw anatomy view, and the agent "computer"
|
||||
// slide-out. Ships with the doc's example workspace so every tier is populated;
|
||||
// real-workspace wiring is a follow-up layer.
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import Link from "next/link";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { MeshMark } from "@/components/brand/MeshMark";
|
||||
import { ComputerPanel } from "@/components/computer/ComputerPanel";
|
||||
import type { Agent } from "@/lib/api/schemas";
|
||||
import type { GroupSummary, StructureNode, StructureStats } from "@/lib/api/structure";
|
||||
import {
|
||||
TopologyCanvas,
|
||||
modeForKind,
|
||||
type CanvasNode,
|
||||
type TopologyMode,
|
||||
} from "./TopologyCanvas";
|
||||
|
||||
type Tier = "org" | "company" | "team" | "claw";
|
||||
type Topo = "hub-spoke" | "pipeline" | "ring" | "mesh" | "swarm" | "debate";
|
||||
|
||||
async function getJSON<T>(url: string, fallback: T): Promise<T> {
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
return r.ok ? ((await r.json()) as T) : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||
|
||||
interface Claw {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
initial: string;
|
||||
grad: string;
|
||||
ink: string;
|
||||
status: "online" | "running" | "idle";
|
||||
}
|
||||
const CLAWS: Claw[] = [
|
||||
{ id: "atlas", name: "Atlas", role: "team lead", initial: "A", grad: "linear-gradient(135deg,#ff9a6a,#ff6f4a)", ink: "#2a0d05", status: "online" },
|
||||
{ id: "iris", name: "Iris", role: "researcher", initial: "I", grad: "linear-gradient(135deg,#6fd0c0,#4aa3b8)", ink: "#06201f", status: "running" },
|
||||
{ id: "echo", name: "Echo", role: "researcher", initial: "E", grad: "linear-gradient(135deg,#8a9af0,#5a6ad8)", ink: "#0a0e2a", status: "running" },
|
||||
{ id: "nova", name: "Nova", role: "writer", initial: "N", grad: "linear-gradient(135deg,#e8c46a,#d89a3a)", ink: "#2a1d05", status: "online" },
|
||||
{ id: "sable", name: "Sable", role: "critic", initial: "S", grad: "linear-gradient(135deg,#c98af0,#9a5ad8)", ink: "#1a0a2a", status: "idle" },
|
||||
{ id: "morpheus", name: "Morpheus", role: "analyst", initial: "M", grad: "linear-gradient(135deg,#ff8a7a,#ff5f57)", ink: "#2a0d0a", status: "online" },
|
||||
];
|
||||
const POS: Record<Topo, [number, number][]> = {
|
||||
"hub-spoke": [[50, 48], [22, 26], [24, 72], [50, 15], [78, 28], [78, 70]],
|
||||
pipeline: [[11, 50], [27, 50], [42, 50], [58, 50], [73, 50], [89, 50]],
|
||||
ring: [[50, 15], [80, 32], [80, 68], [50, 85], [20, 68], [20, 32]],
|
||||
mesh: [[31, 26], [69, 24], [85, 52], [67, 80], [31, 80], [15, 52]],
|
||||
swarm: [[40, 38], [60, 32], [66, 55], [48, 66], [33, 55], [53, 47]],
|
||||
debate: [[26, 22], [26, 50], [26, 78], [74, 22], [74, 50], [74, 78]],
|
||||
};
|
||||
const LNK: Record<Topo, [number, number][]> = {
|
||||
"hub-spoke": [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5]],
|
||||
pipeline: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]],
|
||||
ring: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0]],
|
||||
mesh: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [0, 3], [1, 4], [2, 5]],
|
||||
swarm: [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [1, 3], [2, 4]],
|
||||
debate: [[0, 3], [1, 4], [2, 5], [0, 4], [1, 3], [1, 5], [2, 4]],
|
||||
};
|
||||
const TOPO_META: Record<Topo, { label: string; kind: string }> = {
|
||||
"hub-spoke": { label: "Hub-Spoke", kind: "one coordinator routes every claw" },
|
||||
pipeline: { label: "Pipeline", kind: "output of each claw feeds the next" },
|
||||
ring: { label: "Ring", kind: "cyclic hand-off around the loop" },
|
||||
mesh: { label: "Mesh", kind: "every claw talks to every claw" },
|
||||
swarm: { label: "Swarm", kind: "parallel claws, loose coordination" },
|
||||
debate: { label: "Debate", kind: "adversarial cross-examination" },
|
||||
};
|
||||
const TOPOS = Object.keys(TOPO_META) as Topo[];
|
||||
const statusColor = (s: string) => (s === "running" ? "#5ec8d8" : s === "online" ? "#5fd08a" : "#3a3a40");
|
||||
|
||||
// example org/company structure
|
||||
const COMPANIES = [
|
||||
{ name: "Acme Corp", meta: "4 teams · 18 claws", active: true },
|
||||
{ name: "Helix Labs", meta: "2 teams · 9 claws", active: false },
|
||||
{ name: "Vega Studio", meta: "1 team · 4 claws", active: false },
|
||||
];
|
||||
const TEAMS = [
|
||||
{ name: "Intake Team", meta: "star-MoE · 3", dot: "#5ec8d8", active: false },
|
||||
{ name: "Growth Team", meta: "hub-spoke · 6 · running", dot: "#ff6f61", active: true },
|
||||
{ name: "Research Team", meta: "blackboard · 4", dot: "#5ec8d8", active: false },
|
||||
{ name: "Ops Team", meta: "holacratic · 5", dot: "#3a3a40", active: false },
|
||||
];
|
||||
const COMPARTMENTS = [
|
||||
{ name: "Anatomy", dot: "#ff6f61", active: true },
|
||||
{ name: "Skills", dot: "#ff6f61", count: "7" },
|
||||
{ name: "Tools", dot: "#5ec8d8", count: "4" },
|
||||
{ name: "Personality", dot: "#c98af0" },
|
||||
{ name: "Capabilities", dot: "#e8b465" },
|
||||
{ name: "Memory", dot: "#5fd08a" },
|
||||
{ name: "Safety · §15", dot: "#6fd0c0" },
|
||||
];
|
||||
|
||||
const railIcon: Record<Tier, React.ReactNode> = {
|
||||
org: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.4" fill="none" />
|
||||
<circle cx="10" cy="10" r="2.6" fill="currentColor" />
|
||||
</svg>
|
||||
),
|
||||
company: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" />
|
||||
<rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" />
|
||||
</svg>
|
||||
),
|
||||
team: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="5" r="2.2" fill="currentColor" />
|
||||
<circle cx="5" cy="13.5" r="2.2" fill="currentColor" />
|
||||
<circle cx="15" cy="13.5" r="2.2" fill="currentColor" />
|
||||
<path d="M10 5 L5 13.5 M10 5 L15 13.5 M5 13.5 L15 13.5" stroke="currentColor" strokeWidth="1.1" opacity=".5" />
|
||||
</svg>
|
||||
),
|
||||
claw: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" strokeWidth="1.4" fill="none" />
|
||||
<circle cx="10" cy="10" r="2.4" fill="currentColor" />
|
||||
</svg>
|
||||
),
|
||||
org: (<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="10" cy="10" r="2.6" fill="currentColor" /></svg>),
|
||||
company: (<svg width="20" height="20" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>),
|
||||
team: (<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="5" r="2.2" fill="currentColor" /><circle cx="5" cy="13.5" r="2.2" fill="currentColor" /><circle cx="15" cy="13.5" r="2.2" fill="currentColor" /><path d="M10 5 L5 13.5 M10 5 L15 13.5 M5 13.5 L15 13.5" stroke="currentColor" strokeWidth="1.1" opacity=".5" /></svg>),
|
||||
claw: (<svg width="20" height="20" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="10" cy="10" r="2.4" fill="currentColor" /></svg>),
|
||||
};
|
||||
const TIERS: { key: Tier; label: string }[] = [
|
||||
{ key: "org", label: "ORG" },
|
||||
{ key: "company", label: "CO" },
|
||||
{ key: "team", label: "TEAM" },
|
||||
{ key: "claw", label: "CLAW" },
|
||||
const TIER_TABS: { key: Tier; label: string }[] = [
|
||||
{ key: "org", label: "ORG" }, { key: "company", label: "CO" }, { key: "team", label: "TEAM" }, { key: "claw", label: "CLAW" },
|
||||
];
|
||||
|
||||
const companyIcon = (size: number) => (
|
||||
<svg width={size} height={size} viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>
|
||||
);
|
||||
|
||||
export function Dashboard({ user }: { user: { display_name?: string; email?: string } }) {
|
||||
const [tier, setTier] = useState<Tier>("team");
|
||||
const [orgId, setOrgId] = useState<string | null>(null);
|
||||
const [companyId, setCompanyId] = useState<string | null>(null);
|
||||
const [teamId, setTeamId] = useState<string | null>(null);
|
||||
const [clawId, setClawId] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<TopologyMode>("hub-spoke");
|
||||
const [topology, setTopology] = useState<Topo>("hub-spoke");
|
||||
const [selected, setSelected] = useState<string>("morpheus");
|
||||
|
||||
const [orgs, setOrgs] = useState<GroupSummary[]>([]);
|
||||
const [companies, setCompanies] = useState<GroupSummary[]>([]);
|
||||
const [teams, setTeams] = useState<GroupSummary[]>([]);
|
||||
const [roster, setRoster] = useState<Agent[]>([]);
|
||||
const [stats, setStats] = useState<StructureStats | null>(null);
|
||||
const [node, setNode] = useState<StructureNode | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const modeKey = useRef<string>("");
|
||||
const isOrg = tier === "org", isCompany = tier === "company", isTeam = tier === "team", isClaw = tier === "claw";
|
||||
const sel = CLAWS.find((c) => c.id === selected) ?? CLAWS[0];
|
||||
const selIdx = CLAWS.findIndex((c) => c.id === selected);
|
||||
const userInitial = (user.display_name || user.email || "O").trim().charAt(0).toUpperCase();
|
||||
|
||||
// Load the workspace + default to the most-recent team.
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
void (async () => {
|
||||
const [t, c, o, r, s] = await Promise.all([
|
||||
getJSON<GroupSummary[]>("/api/teams", []),
|
||||
getJSON<GroupSummary[]>("/api/companies", []),
|
||||
getJSON<GroupSummary[]>("/api/orgs", []),
|
||||
getJSON<Agent[]>("/api/team/claws", []),
|
||||
getJSON<StructureStats | null>("/api/structure/stats", null),
|
||||
]);
|
||||
if (!live) return;
|
||||
setTeams(t);
|
||||
setCompanies(c);
|
||||
setOrgs(o);
|
||||
setRoster(r);
|
||||
setStats(s);
|
||||
if (t[0]) {
|
||||
setTier("team");
|
||||
setTeamId(t[0].id);
|
||||
} else if (c[0]) {
|
||||
setTier("company");
|
||||
setCompanyId(c[0].id);
|
||||
} else if (o[0]) {
|
||||
setTier("org");
|
||||
setOrgId(o[0].id);
|
||||
}
|
||||
setLoaded(true);
|
||||
})();
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// The canvas always renders a group's children; a claw selection keeps the
|
||||
// team canvas and opens the slide-out.
|
||||
const canvasLevel: "org" | "company" | "team" | null =
|
||||
tier === "org" ? "org" : tier === "company" ? "company" : "team";
|
||||
const canvasId = tier === "org" ? orgId : tier === "company" ? companyId : teamId;
|
||||
|
||||
// Clear the canvas when its target changes (render-phase reset — avoids a
|
||||
// synchronous setState inside the effect).
|
||||
const ckey = `${canvasLevel ?? ""}:${canvasId ?? ""}`;
|
||||
const [seenCanvas, setSeenCanvas] = useState(ckey);
|
||||
if (seenCanvas !== ckey) {
|
||||
setSeenCanvas(ckey);
|
||||
setNode(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasLevel || !canvasId) return;
|
||||
let live = true;
|
||||
void (async () => {
|
||||
const n = await getJSON<StructureNode | null>(
|
||||
`/api/structure/${canvasLevel}/${canvasId}`,
|
||||
null,
|
||||
);
|
||||
if (!live) return;
|
||||
setNode(n);
|
||||
// Reset the view-mode to the group's real kind only when the group changes.
|
||||
const key = `${canvasLevel}:${canvasId}`;
|
||||
if (n && modeKey.current !== key) {
|
||||
modeKey.current = key;
|
||||
setMode(modeForKind(n.kind));
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [canvasLevel, canvasId]);
|
||||
|
||||
const rosterMap = new Map(roster.map((a) => [a.id, a]));
|
||||
const nodes: CanvasNode[] = (node?.children ?? []).map((c) => ({
|
||||
id: c.child_id,
|
||||
label: c.child_name || rosterMap.get(c.child_id)?.name || c.role,
|
||||
role: c.role,
|
||||
level: c.child_level,
|
||||
status: rosterMap.get(c.child_id)?.status,
|
||||
}));
|
||||
|
||||
function onNodeClick(n: CanvasNode) {
|
||||
if (n.level === "claw") {
|
||||
setClawId(n.id);
|
||||
setTier("claw");
|
||||
} else if (n.level === "team") {
|
||||
setTeamId(n.id);
|
||||
setClawId(null);
|
||||
setTier("team");
|
||||
} else {
|
||||
setCompanyId(n.id);
|
||||
setTeamId(null);
|
||||
setClawId(null);
|
||||
setTier("company");
|
||||
}
|
||||
}
|
||||
|
||||
function gotoTier(t: Tier) {
|
||||
if (t === "org" && !orgId && orgs[0]) setOrgId(orgs[0].id);
|
||||
if (t === "company" && !companyId && companies[0]) setCompanyId(companies[0].id);
|
||||
if (t === "team" && !teamId && teams[0]) setTeamId(teams[0].id);
|
||||
if (t === "claw" && !clawId) return; // nothing selected yet
|
||||
setTier(t);
|
||||
}
|
||||
|
||||
const teamName = teams.find((x) => x.id === teamId)?.name;
|
||||
const companyName = companies.find((x) => x.id === companyId)?.name;
|
||||
const orgName = orgs.find((x) => x.id === orgId)?.name;
|
||||
const clawName = clawId ? (rosterMap.get(clawId)?.name ?? "Claw") : null;
|
||||
const crumbs = [orgName, companyName, teamName, tier === "claw" ? clawName : null].filter(
|
||||
Boolean,
|
||||
) as string[];
|
||||
const initial = (user.display_name || user.email || "?").trim().charAt(0).toUpperCase();
|
||||
const running = stats?.running_now ?? 0;
|
||||
const clawCount = stats?.claw_count ?? roster.length;
|
||||
|
||||
const empty = loaded && teams.length === 0 && companies.length === 0 && orgs.length === 0;
|
||||
const crumbStyle = (on: boolean): CSSProperties =>
|
||||
on
|
||||
? { color: "#fff", background: "rgba(255,111,97,.14)", border: "1px solid rgba(255,111,97,.3)", padding: "3px 8px", borderRadius: 6, cursor: "pointer" }
|
||||
: { color: "#6a6a72", padding: "3px 5px", cursor: "pointer" };
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col bg-background text-foreground">
|
||||
<div style={{ width: "100%", height: "100dvh", minHeight: 640, background: "#08080a", display: "flex", flexDirection: "column", color: "#f3f3f5", overflow: "hidden" }}>
|
||||
{/* TOP BAR */}
|
||||
<header className="flex h-[54px] flex-none items-center gap-3.5 border-b border-white/[0.06] bg-gradient-to-b from-[#0d0d10] to-[#0a0a0c] px-4">
|
||||
<Link href="/" className="flex items-center gap-2.5 text-coral">
|
||||
<MeshMark size={22} title="" />
|
||||
<span className="text-[15px] font-bold tracking-[-0.01em] text-foreground">Clawmates</span>
|
||||
<div style={{ height: 54, flex: "none", display: "flex", alignItems: "center", gap: 14, padding: "0 18px", borderBottom: "1px solid rgba(255,255,255,.06)", background: "linear-gradient(180deg,#0d0d10,#0a0a0c)" }}>
|
||||
<Link href="/" style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M11 3 L18.5 17 L3.5 17 Z" stroke="#ff6f61" strokeWidth="1.3" strokeLinejoin="round" opacity="0.55" /><circle cx="11" cy="3.5" r="2.4" fill="#ff6f61" /><circle cx="18" cy="17" r="2.4" fill="#ff6f61" /><circle cx="4" cy="17" r="2.4" fill="#ff6f61" /></svg>
|
||||
<span style={{ fontSize: 15, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.01em" }}>Clawmates</span>
|
||||
</Link>
|
||||
<span className="h-[22px] w-px bg-white/[0.08]" />
|
||||
<nav className="flex items-center gap-1.5 font-mono text-xs">
|
||||
{crumbs.length === 0 ? (
|
||||
<span className="text-[#6a6a72]">Workspace</span>
|
||||
) : (
|
||||
crumbs.map((c, i) => (
|
||||
<span key={i} className="flex items-center gap-1.5">
|
||||
{i > 0 ? <span className="text-[#3a3a40]">/</span> : null}
|
||||
<span className={i === crumbs.length - 1 ? "text-coral-light" : "text-[#9a9aa2]"}>
|
||||
{c}
|
||||
</span>
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</nav>
|
||||
<div className="flex-1" />
|
||||
<div
|
||||
className="hidden items-center gap-1.5 rounded-[7px] border px-2.5 py-[5px] font-mono text-[11px] text-online sm:flex"
|
||||
style={{ borderColor: "rgba(95,208,138,.25)", background: "rgba(95,208,138,.06)" }}
|
||||
>
|
||||
<span className={`size-[7px] rounded-full bg-online ${running > 0 ? "cm-blink" : ""}`} />
|
||||
{clawCount} claws{running > 0 ? " · running" : ""}
|
||||
<div style={{ width: 1, height: 22, background: "rgba(255,255,255,.08)" }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 12 }}>
|
||||
<span style={crumbStyle(isOrg)} onClick={() => setTier("org")}>Acme Org</span>
|
||||
<span style={{ color: "#3a3a40" }}>/</span>
|
||||
<span style={crumbStyle(isCompany)} onClick={() => setTier("company")}>Acme Corp</span>
|
||||
<span style={{ color: "#3a3a40" }}>/</span>
|
||||
<span style={crumbStyle(isTeam)} onClick={() => setTier("team")}>Growth Team</span>
|
||||
{isClaw ? (<><span style={{ color: "#3a3a40" }}>/</span><span style={crumbStyle(true)}>{sel.name}</span></>) : null}
|
||||
</div>
|
||||
<Link
|
||||
href="/claws/new"
|
||||
className="flex items-center gap-1.5 rounded-lg bg-gradient-to-br from-coral-light to-coral-dark px-3 py-1.5 text-xs font-bold text-[#2a0d0a] transition-[filter] hover:brightness-110"
|
||||
>
|
||||
<span className="text-[15px] leading-none">+</span> Deploy
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, fontFamily: mono, fontSize: 11, color: "#5fd08a", padding: "5px 10px", border: "1px solid rgba(95,208,138,.25)", borderRadius: 7, background: "rgba(95,208,138,.06)" }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#5fd08a", animation: "cm-blink 1.6s infinite" }} />
|
||||
6 claws · running
|
||||
</div>
|
||||
<Link href="/claws/new" style={{ display: "flex", alignItems: "center", gap: 7, padding: "6px 12px", borderRadius: 8, background: "linear-gradient(135deg,#ff8a7a,#ff5f57)", color: "#2a0d0a", fontSize: 12, fontWeight: 700, textDecoration: "none" }}>
|
||||
<span style={{ fontSize: 15, lineHeight: 1 }}>+</span> Deploy from template
|
||||
</Link>
|
||||
<span
|
||||
className="flex size-8 items-center justify-center rounded-lg text-[13px] font-bold text-[#2a0d0a]"
|
||||
style={{ background: "linear-gradient(135deg,#ff8a7a,#ff5f57)" }}
|
||||
title={user.display_name || user.email}
|
||||
>
|
||||
{initial}
|
||||
</span>
|
||||
</header>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: "linear-gradient(135deg,#ff8a7a,#ff5f57)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 13, fontWeight: 700, color: "#2a0d0a" }}>{userInitial}</div>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{/* TIER RAIL */}
|
||||
<div className="flex w-[60px] flex-none flex-col items-center border-r border-white/[0.06] bg-[#0a0a0c] py-3.5">
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
{TIERS.map((t) => {
|
||||
const active = tier === t.key;
|
||||
<div style={{ flex: 1, display: "flex", minHeight: 0 }}>
|
||||
{/* STRUCTURE RAIL */}
|
||||
<div style={{ width: 60, flex: "none", borderRight: "1px solid rgba(255,255,255,.06)", background: "#0a0a0c", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "center" }}>
|
||||
{TIER_TABS.map((t) => {
|
||||
const on = tier === t.key;
|
||||
return (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
onClick={() => gotoTier(t.key)}
|
||||
className={`relative flex h-12 w-[42px] flex-col items-center justify-center gap-1 rounded-[10px] transition-colors ${
|
||||
active ? "bg-coral/[0.12] text-coral" : "text-[#6a6a72] hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{active ? (
|
||||
<span className="absolute left-0 top-2 bottom-2 w-[3px] rounded-r bg-coral" />
|
||||
) : null}
|
||||
<div key={t.key} onClick={() => setTier(t.key)} style={{ position: "relative", width: 42, height: 48, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 4, borderRadius: 10, cursor: "pointer", color: on ? "#ff6f61" : "#5a5a62", background: on ? "rgba(255,111,97,.1)" : "transparent" }}>
|
||||
{on ? <span style={{ position: "absolute", left: 0, top: 8, bottom: 8, width: 3, borderRadius: "0 3px 3px 0", background: "#ff6f61" }} /> : null}
|
||||
{railIcon[t.key]}
|
||||
<span className="font-mono text-[8px]">{t.label}</span>
|
||||
</button>
|
||||
<span style={{ fontFamily: mono, fontSize: 8 }}>{t.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<Link
|
||||
href="/claws/new"
|
||||
title="Deploy"
|
||||
className="flex size-[30px] items-center justify-center rounded-lg border border-dashed border-white/[0.16] text-coral"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</Link>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Link href="/claws/new" style={{ width: 30, height: 30, borderRadius: 8, border: "1px dashed rgba(255,255,255,.16)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff6f61", fontSize: 18, fontWeight: 300, textDecoration: "none" }}>+</Link>
|
||||
</div>
|
||||
|
||||
{/* CONTEXT LIST */}
|
||||
<div className="flex w-[252px] flex-none flex-col border-r border-white/[0.06] bg-[#0b0b0e]">
|
||||
<div className="px-4 pb-2.5 pt-4">
|
||||
<div className="font-mono text-[10px] tracking-[0.12em] text-[#5a5a62]">
|
||||
{tier === "org" || tier === "company" || tier === "team"
|
||||
? `${tier.toUpperCase()} · ${nodes.length}`
|
||||
: "CLAW"}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold tracking-[-0.01em]">{node?.name ?? "…"}</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-2 pb-2 [&::-webkit-scrollbar]:hidden">
|
||||
{nodes.length === 0 ? (
|
||||
<p className="px-2 text-xs text-muted-foreground">
|
||||
{loaded ? "Empty" : "Loading…"}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-0.5">
|
||||
{nodes.map((n) => {
|
||||
const sel = n.level === "claw" && n.id === clawId;
|
||||
const grad = rosterMap.get(n.id)?.accent;
|
||||
<div style={{ width: 252, flex: "none", borderRight: "1px solid rgba(255,255,255,.06)", background: "#0b0b0e", display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||
{isTeam ? <ContextHeader label="TEAM · 6 CLAWS" title="Growth Team" tabs={["Members", "Templates"]} /> : null}
|
||||
{isTeam ? (
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: 8 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{CLAWS.map((c) => {
|
||||
const on = c.id === selected;
|
||||
return (
|
||||
<li key={n.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNodeClick(n)}
|
||||
className={`relative flex w-full items-center gap-2.5 rounded-[9px] px-2.5 py-2 text-left transition-colors ${
|
||||
sel ? "bg-coral/[0.12]" : "hover:bg-hover-bg"
|
||||
}`}
|
||||
>
|
||||
{sel ? (
|
||||
<span className="absolute left-0 top-2 bottom-2 w-[3px] rounded-r bg-coral" />
|
||||
) : null}
|
||||
<span
|
||||
className="flex size-[30px] shrink-0 items-center justify-center rounded-lg text-xs font-bold text-[#08080a]"
|
||||
style={{ background: grad || "var(--color-coral)" }}
|
||||
>
|
||||
{(n.label || n.role).charAt(0).toUpperCase()}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[13px] font-semibold">{n.label}</span>
|
||||
<span className="block truncate font-mono text-[10px] text-[#6a6a72]">
|
||||
{n.role}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<div key={c.id} onClick={() => { setSelected(c.id); setTier("claw"); }} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", borderRadius: 9, cursor: "pointer", position: "relative", background: on ? "rgba(255,111,97,.12)" : "transparent" }}>
|
||||
{on ? <span style={{ position: "absolute", left: 0, top: 8, bottom: 8, width: 3, borderRadius: "0 3px 3px 0", background: "#ff6f61" }} /> : null}
|
||||
<div style={{ width: 30, height: 30, flex: "none", borderRadius: 8, background: c.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 700, color: c.ink }}>{c.initial}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: on ? "#fff" : "#dcdce2" }}>{c.name}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, color: "#6a6a72" }}>{c.role}</div>
|
||||
</div>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: statusColor(c.status) }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isCompany ? <ContextHeader label="COMPANY · 4 TEAMS" title="Acme Corp" tabs={["Teams", "Templates"]} /> : null}
|
||||
{isCompany ? (
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: 8 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{TEAMS.map((t) => (
|
||||
<div key={t.name} onClick={() => setTier("team")} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 10px", borderRadius: 9, cursor: "pointer", position: "relative", background: t.active ? "rgba(255,111,97,.12)" : "transparent", border: t.active ? "1px solid rgba(255,111,97,.28)" : undefined }}>
|
||||
{t.active ? <span style={{ position: "absolute", left: 0, top: 8, bottom: 8, width: 3, borderRadius: "0 3px 3px 0", background: "#ff6f61" }} /> : null}
|
||||
<div style={{ width: 30, height: 30, flex: "none", borderRadius: 8, background: t.active ? "#1a1216" : "#141417", border: t.active ? "1px solid rgba(255,111,97,.3)" : "1px solid rgba(255,255,255,.08)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: t.dot === "#ff6f61" ? "50%" : 2, background: t.dot }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: t.active ? "#fff" : "#eaeaee" }}>{t.name}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, color: t.active ? "#ff8a7a" : "#6a6a72" }}>{t.meta}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isOrg ? <ContextHeader label="ORG · 3 COMPANIES" title="Acme Org" tabs={["Companies", "Templates"]} /> : null}
|
||||
{isOrg ? (
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: 8 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{COMPANIES.map((co) => (
|
||||
<div key={co.name} onClick={() => setTier("company")} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 10px", borderRadius: 9, cursor: "pointer", position: "relative", background: co.active ? "rgba(255,111,97,.12)" : "transparent", border: co.active ? "1px solid rgba(255,111,97,.28)" : undefined }}>
|
||||
{co.active ? <span style={{ position: "absolute", left: 0, top: 8, bottom: 8, width: 3, borderRadius: "0 3px 3px 0", background: "#ff6f61" }} /> : null}
|
||||
<div style={{ width: 30, height: 30, flex: "none", borderRadius: 8, background: co.active ? "#1a1216" : "#141417", border: co.active ? "1px solid rgba(255,111,97,.3)" : "1px solid rgba(255,255,255,.08)", display: "flex", alignItems: "center", justifyContent: "center", color: co.active ? "#ff6f61" : "#6a6a72" }}>{companyIcon(15)}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: co.active ? "#fff" : "#eaeaee" }}>{co.name}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, color: co.active ? "#ff8a7a" : "#6a6a72" }}>{co.meta}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isClaw ? (
|
||||
<>
|
||||
<div style={{ padding: "14px 16px 12px" }}>
|
||||
<div onClick={() => setTier("team")} style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: mono, fontSize: 10, color: "#6a6a72", cursor: "pointer", marginBottom: 12 }}>← Growth Team</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 11 }}>
|
||||
<div style={{ width: 42, height: 42, flex: "none", borderRadius: 11, background: sel.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 17, fontWeight: 700, color: sel.ink }}>{sel.initial}</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 17, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.01em" }}>{sel.name}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, color: "#6a6a72" }}>{sel.role} · Growth Team</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 11, display: "flex", alignItems: "center", gap: 7, padding: "7px 10px", borderRadius: 8, background: "#101014", border: "1px solid rgba(255,255,255,.07)" }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#ff6f61" }} />
|
||||
<span style={{ fontFamily: mono, fontSize: 10, color: "#9a9aa2" }}>Claude Sonnet 4.5</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
<span style={{ fontFamily: mono, fontSize: 9, color: "#5fd08a" }}>online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: "0 12px 6px" }}>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", padding: "6px 6px 8px" }}>COMPARTMENTS</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{COMPARTMENTS.map((c) => (
|
||||
<div key={c.name} style={{ display: "flex", alignItems: "center", gap: 9, padding: "8px 10px", borderRadius: 8, background: c.active ? "rgba(255,111,97,.1)" : "transparent", color: c.active ? "#f3f3f5" : "#9a9aa2", fontSize: 12, fontWeight: c.active ? 600 : 400, cursor: "pointer" }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: c.dot }} />{c.name}
|
||||
{c.count ? (<><span style={{ flex: 1 }} /><span style={{ fontFamily: mono, fontSize: 9, color: "#5a5a62" }}>{c.count}</span></>) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* CANVAS */}
|
||||
<div className="min-w-0 flex-1">
|
||||
{empty ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 text-center">
|
||||
<MeshMark size={40} title="" />
|
||||
<h1 className="text-xl font-semibold">No teams yet</h1>
|
||||
<p className="text-sm text-muted-foreground">Deploy a team to populate your dashboard.</p>
|
||||
<Link
|
||||
href="/claws/new"
|
||||
className="rounded-full bg-coral px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
Deploy a team
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<TopologyCanvas
|
||||
nodes={nodes}
|
||||
mode={mode}
|
||||
onModeChange={setMode}
|
||||
onNodeClick={onNodeClick}
|
||||
selectedId={clawId}
|
||||
/>
|
||||
)}
|
||||
<div style={{ flex: 1, position: "relative", minWidth: 0, overflow: "hidden" }}>
|
||||
{isTeam ? <TeamCanvas topology={topology} setTopology={setTopology} selIdx={selIdx} selected={selected} onSelect={(id) => { setSelected(id); setTier("claw"); }} /> : null}
|
||||
{isCompany ? <CompanyCanvas onEnter={() => setTier("team")} /> : null}
|
||||
{isOrg ? <OrgCanvas onCompany={() => setTier("company")} /> : null}
|
||||
{isClaw ? <ClawAnatomyCanvas sel={sel} /> : null}
|
||||
</div>
|
||||
|
||||
{/* COMPUTER SLIDE-OUT */}
|
||||
{clawId ? (
|
||||
<div className="cm-fade flex w-[330px] flex-none flex-col border-l border-white/[0.06] bg-[#0a0a0c] p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-mono text-[10px] tracking-[0.12em] text-[#5a5a62]">COMPUTER</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClawId(null);
|
||||
if (tier === "claw") setTier("team");
|
||||
}}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="Close computer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto [&::-webkit-scrollbar]:hidden">
|
||||
<ComputerPanel clawId={clawId} clawName={clawName ?? "Claw"} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{/* COMPUTER PANEL */}
|
||||
{isClaw ? <ComputerPanelStatic sel={sel} onClose={() => setTier("team")} /> : null}
|
||||
</div>
|
||||
|
||||
{/* STATUS BAR */}
|
||||
<footer className="flex h-7 flex-none items-center gap-4 border-t border-white/[0.06] bg-[#0a0a0c] px-4 font-mono text-[10px] text-[#6a6a72]">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className={`size-[6px] rounded-full ${running > 0 ? "bg-running cm-blink" : "bg-[#3a3a40]"}`} />
|
||||
durable runner {running > 0 ? `· ${running} active` : "· idle"}
|
||||
</span>
|
||||
<span className="text-[#3a3a40]">·</span>
|
||||
<span className="text-online">§15 sandbox: isolated</span>
|
||||
<div className="flex-1" />
|
||||
<Link href="/approvals" className="hover:text-foreground">
|
||||
doors awaiting approval
|
||||
</Link>
|
||||
</footer>
|
||||
<div style={{ height: 28, flex: "none", display: "flex", alignItems: "center", gap: 18, padding: "0 16px", borderTop: "1px solid rgba(255,255,255,.06)", background: "#0a0a0c", fontFamily: mono, fontSize: 10, color: "#5a5a62" }}>
|
||||
<span style={{ color: "#5fd08a" }}>● durable runner ok</span>
|
||||
<span>checkpoint 3s ago</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
<span>§15 sandbox: isolated</span>
|
||||
<Link href="/approvals" style={{ color: "#5ec8d8", textDecoration: "none" }}>2 doors awaiting approval</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextHeader({ label, title, tabs }: { label: string; title: string; tabs: [string, string] }) {
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: "16px 16px 12px" }}>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 6 }}>{label}</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.01em" }}>{title}</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 18, padding: "0 16px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: "#f3f3f5", paddingBottom: 9, borderBottom: "2px solid #ff6f61" }}>{tabs[0]}</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 500, color: "#6a6a72", paddingBottom: 9, borderBottom: "2px solid transparent", cursor: "pointer" }}>{tabs[1]}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamCanvas({ topology, setTopology, selIdx, selected, onSelect }: { topology: Topo; setTopology: (t: Topo) => void; selIdx: number; selected: string; onSelect: (id: string) => void }) {
|
||||
const pos = POS[topology];
|
||||
const links = LNK[topology].map(([a, b]) => {
|
||||
const [ax, ay] = pos[a], [bx, by] = pos[b];
|
||||
let stroke = "rgba(255,255,255,.12)", width = 1.5, dash = "0", anim = "";
|
||||
if (a === selIdx || b === selIdx) { stroke = "rgba(255,111,97,.6)"; width = 1.8; dash = "3 4"; anim = "cm-flow .9s linear infinite"; }
|
||||
else if (a === 1 || a === 2 || b === 1 || b === 2) { stroke = "rgba(94,200,216,.5)"; width = 1.6; dash = "3 4"; anim = "cm-flow 1.2s linear infinite"; }
|
||||
return { d: `M ${ax},${ay} L ${bx},${by}`, stroke, width, dash, anim };
|
||||
});
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "radial-gradient(120% 90% at 55% 40%, #0e0e13 0%, #08080a 70%)" }}>
|
||||
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "14px 18px 10px" }}>
|
||||
<span style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", flex: "none" }}>TOPOLOGY</span>
|
||||
<div style={{ flex: 1, minWidth: 0, display: "flex", gap: 6, overflowX: "auto", whiteSpace: "nowrap", paddingBottom: 2 }}>
|
||||
{TOPOS.map((id) => {
|
||||
const on = id === topology;
|
||||
return (
|
||||
<div key={id} onClick={() => setTopology(id)} style={{ flex: "none", fontFamily: mono, fontSize: 11, fontWeight: 600, padding: "5px 11px", borderRadius: 7, cursor: "pointer", transition: "all .2s", background: on ? "rgba(255,111,97,.14)" : "#101014", border: on ? "1px solid rgba(255,111,97,.45)" : "1px solid rgba(255,255,255,.08)", color: on ? "#ff8a7a" : "#9a9aa2" }}>{TOPO_META[id].label}</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span style={{ fontFamily: mono, fontSize: 10, color: "#5ec8d8", flex: "none", cursor: "pointer", padding: "5px 10px", border: "1px solid rgba(94,200,216,.22)", borderRadius: 7 }}>⇄ compare all</span>
|
||||
</div>
|
||||
<div style={{ flex: "none", padding: "0 18px 4px" }}>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.01em" }}>{TOPO_META[topology].label}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 11, color: "#6a6a72", marginTop: 2 }}>{TOPO_META[topology].kind}</div>
|
||||
</div>
|
||||
<span style={{ position: "absolute", bottom: 14, left: 18, right: 18, zIndex: 5, fontFamily: mono, fontSize: 10, color: "#3a3a40" }}>click a claw to open its computer · pick a topology to re-wire the team</span>
|
||||
<div style={{ position: "relative", flex: 1, minHeight: 0 }}>
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
|
||||
{links.map((lk, i) => (<path key={i} d={lk.d} fill="none" stroke={lk.stroke} strokeWidth={lk.width} strokeDasharray={lk.dash} vectorEffect="non-scaling-stroke" style={{ animation: lk.anim || undefined, transition: "stroke .3s" }} />))}
|
||||
</svg>
|
||||
{CLAWS.map((c, i) => {
|
||||
const [x, y] = pos[i];
|
||||
const on = c.id === selected;
|
||||
return (
|
||||
<div key={c.id} onClick={() => onSelect(c.id)} style={{ position: "absolute", left: `${x}%`, top: `${y}%`, transform: "translate(-50%,-50%)", transition: "left .55s cubic-bezier(.4,0,.2,1), top .55s cubic-bezier(.4,0,.2,1)", display: "flex", flexDirection: "column", alignItems: "center", gap: 7, zIndex: 3, cursor: "pointer" }}>
|
||||
<div style={{ position: "relative", width: 50, height: 50 }}>
|
||||
{c.status === "running" ? <div style={{ position: "absolute", inset: 0, borderRadius: "50%", background: "rgba(94,200,216,.4)", animation: "cm-halo 1.8s ease-out infinite" }} /> : null}
|
||||
{on ? <div style={{ position: "absolute", inset: -6, borderRadius: "50%", border: "2px solid #ff6f61", boxShadow: "0 0 0 4px rgba(255,111,97,.12)" }} /> : null}
|
||||
<div style={{ position: "relative", width: 50, height: 50, borderRadius: "50%", background: c.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18, fontWeight: 700, color: c.ink, boxShadow: "0 0 28px rgba(0,0,0,.45)" }}>{c.initial}</div>
|
||||
<span style={{ position: "absolute", right: -1, bottom: -1, width: 12, height: 12, borderRadius: "50%", background: statusColor(c.status), border: "2px solid #0a0a0c" }} />
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: on ? "#fff" : "#dcdce2" }}>{c.name}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 9, color: "#6a6a72" }}>{c.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompanyCanvas({ onEnter }: { onEnter: () => void }) {
|
||||
const card = (left: string, top: string, width: number, z: number, active: boolean, dot: string, blink: string | undefined, name: string, meta: string, avatars: string[], extra?: string) => (
|
||||
<div style={{ position: "absolute", left, top, transform: "translate(-50%,-50%)", width, zIndex: z, cursor: "pointer" }} onClick={onEnter}>
|
||||
<div style={{ borderRadius: 12, background: active ? "#141014" : "#0f0f13", border: active ? "1.5px solid #ff6f61" : "1px solid rgba(255,255,255,.09)", padding: "11px 12px", boxShadow: active ? "0 0 0 4px rgba(255,111,97,.1), 0 10px 28px rgba(0,0,0,.5)" : "0 8px 24px rgba(0,0,0,.4)" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: dot === "#ff6f61" || dot === "#5ec8d8" ? "50%" : 2, background: dot, animation: blink }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: active ? "#fff" : "#eaeaee" }}>{name}</span>
|
||||
{active ? (<><span style={{ flex: 1 }} /><span style={{ fontFamily: mono, fontSize: 9, color: "#ff8a7a" }}>▾ enter</span></>) : null}
|
||||
</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 9, color: active ? "#ff8a7a" : "#6a6a72", marginBottom: 9 }}>{meta}</div>
|
||||
<div style={{ display: "flex", gap: 4 }}>
|
||||
{avatars.map((g, i) => (<span key={i} style={{ width: 15, height: 15, borderRadius: 5, background: g }} />))}
|
||||
{extra ? <span style={{ width: 15, height: 15, borderRadius: 5, background: "#1a1a1e", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: mono, fontSize: 8, color: "#8a8a92" }}>{extra}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0, background: "#08080a", backgroundImage: "linear-gradient(rgba(255,255,255,.022) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.022) 1px, transparent 1px)", backgroundSize: "28px 28px" }}>
|
||||
<div style={{ position: "absolute", top: 16, left: 18, zIndex: 5, display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ fontFamily: mono, fontSize: 11, letterSpacing: ".1em", color: "#e8b465", padding: "4px 9px", borderRadius: 6, border: "1px solid rgba(232,196,106,.25)", background: "rgba(232,196,106,.07)" }}>PIPELINE</span>
|
||||
<span style={{ fontFamily: mono, fontSize: 11, color: "#5a5a62" }}>company topology · click a team to enter its claws</span>
|
||||
</div>
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
|
||||
<path d="M16,34 H31 V22 H46" fill="none" stroke="rgba(94,200,216,.5)" strokeWidth="1.6" strokeDasharray="3 4" vectorEffect="non-scaling-stroke" style={{ animation: "cm-flow 1.1s linear infinite" }} />
|
||||
<path d="M16,34 H30 V66 H44" fill="none" stroke="rgba(232,196,106,.45)" strokeWidth="1.6" strokeDasharray="3 4" vectorEffect="non-scaling-stroke" style={{ animation: "cm-flow 1.4s linear infinite" }} />
|
||||
<path d="M46,22 H63 V46 H80" fill="none" stroke="rgba(255,111,97,.55)" strokeWidth="1.8" strokeDasharray="3 4" vectorEffect="non-scaling-stroke" style={{ animation: "cm-flow .9s linear infinite" }} />
|
||||
<path d="M44,66 H62 V46 H80" fill="none" stroke="rgba(255,255,255,.12)" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
|
||||
</svg>
|
||||
{card("16%", "34%", 158, 3, false, "#5ec8d8", undefined, "Intake Team", "star-MoE · 3 claws", ["linear-gradient(135deg,#6fd0c0,#4aa3b8)", "linear-gradient(135deg,#8a9af0,#5a6ad8)", "linear-gradient(135deg,#e8c46a,#d89a3a)"])}
|
||||
{card("46%", "22%", 170, 4, true, "#ff6f61", "cm-blink 1.5s infinite", "Growth Team", "hub-spoke · 6 claws · running", ["linear-gradient(135deg,#ff9a6a,#ff6f4a)", "linear-gradient(135deg,#6fd0c0,#4aa3b8)", "linear-gradient(135deg,#8a9af0,#5a6ad8)", "linear-gradient(135deg,#e8c46a,#d89a3a)"], "+2")}
|
||||
{card("44%", "66%", 158, 3, false, "#5ec8d8", "cm-blink 1.7s infinite", "Research Team", "blackboard · 4 claws", ["linear-gradient(135deg,#8a9af0,#5a6ad8)", "linear-gradient(135deg,#6fd0c0,#4aa3b8)", "linear-gradient(135deg,#c98af0,#9a5ad8)"], "+1")}
|
||||
{card("80%", "46%", 152, 3, false, "#3a3a40", undefined, "Ops Team", "holacratic · 5 claws", ["linear-gradient(135deg,#e8c46a,#d89a3a)", "linear-gradient(135deg,#ff9a6a,#ff6f4a)"], "+3")}
|
||||
<span style={{ position: "absolute", bottom: 16, left: 18, zIndex: 5, fontFamily: mono, fontSize: 10, color: "#3a3a40" }}>↑ zoom out to Org · ↓ click a team to enter</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OrgCanvas({ onCompany }: { onCompany: () => void }) {
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0, background: "radial-gradient(120% 90% at 50% 40%, #0e0e13 0%, #08080a 70%)" }}>
|
||||
<div style={{ position: "absolute", top: 16, left: 18, zIndex: 5, display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ fontFamily: mono, fontSize: 11, letterSpacing: ".1em", color: "#c98af0", padding: "4px 9px", borderRadius: 6, border: "1px solid rgba(201,138,240,.25)", background: "rgba(201,138,240,.07)" }}>PORTFOLIO</span>
|
||||
<span style={{ fontFamily: mono, fontSize: 11, color: "#5a5a62" }}>org topology · click a company to descend</span>
|
||||
</div>
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
|
||||
<path d="M50,46 L26,28" fill="none" stroke="rgba(255,111,97,.5)" strokeWidth="1.8" strokeDasharray="3 4" vectorEffect="non-scaling-stroke" style={{ animation: "cm-flow 1s linear infinite" }} />
|
||||
<path d="M50,46 L76,30" fill="none" stroke="rgba(255,255,255,.12)" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
|
||||
<path d="M50,46 L62,74" fill="none" stroke="rgba(255,255,255,.12)" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
|
||||
</svg>
|
||||
<div style={{ position: "absolute", left: "50%", top: "46%", transform: "translate(-50%,-50%)", display: "flex", flexDirection: "column", alignItems: "center", gap: 9, zIndex: 3 }}>
|
||||
<div style={{ width: 60, height: 60, borderRadius: 16, background: "#141417", border: "1px solid rgba(255,255,255,.1)", display: "flex", alignItems: "center", justifyContent: "center", color: "#9a9aa2", boxShadow: "0 0 30px rgba(0,0,0,.5)" }}>
|
||||
<svg width="26" height="26" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.4" fill="none" /><circle cx="10" cy="10" r="2.6" fill="currentColor" /></svg>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}><div style={{ fontSize: 13, fontWeight: 700, color: "#fff" }}>Acme Org</div><div style={{ fontFamily: mono, fontSize: 9, color: "#6a6a72" }}>3 companies</div></div>
|
||||
</div>
|
||||
<OrgCompanyNode left="26%" top="28%" size={54} active onClick={onCompany} name="Acme Corp" meta="4 teams · enter ▸" />
|
||||
<OrgCompanyNode left="76%" top="30%" size={48} onClick={onCompany} name="Helix Labs" meta="2 teams" />
|
||||
<OrgCompanyNode left="62%" top="74%" size={48} onClick={onCompany} name="Vega Studio" meta="1 team" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OrgCompanyNode({ left, top, size, active, onClick, name, meta }: { left: string; top: string; size: number; active?: boolean; onClick: () => void; name: string; meta: string }) {
|
||||
return (
|
||||
<div onClick={onClick} style={{ position: "absolute", left, top, transform: "translate(-50%,-50%)", display: "flex", flexDirection: "column", alignItems: "center", gap: 8, zIndex: 3, cursor: "pointer" }}>
|
||||
<div style={{ width: size, height: size, borderRadius: active ? 14 : 13, background: active ? "linear-gradient(135deg,#ff8a7a,#ff5f57)" : "#141417", border: active ? undefined : "1px solid rgba(255,255,255,.1)", display: "flex", alignItems: "center", justifyContent: "center", color: active ? "#2a0d0a" : "#8a8a92", boxShadow: active ? "0 0 26px rgba(255,111,97,.4)" : undefined }}>
|
||||
<svg width={active ? 22 : 20} height={active ? 22 : 20} viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" strokeWidth={active ? 1.6 : 1.4} fill="none" /><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" strokeWidth={active ? 1.6 : 1.4} fill="none" /></svg>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}><div style={{ fontSize: 12, fontWeight: active ? 700 : 600, color: active ? "#fff" : "#dcdce2" }}>{name}</div><div style={{ fontFamily: mono, fontSize: 9, color: active ? "#ff8a7a" : "#6a6a72" }}>{meta}</div></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AnatomyCard({ tint, label, count, children }: { tint: string; label: string; count?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<div style={{ borderRadius: 12, background: "#0f0f13", border: `1px solid ${tint}38`, padding: 11, boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 9 }}>
|
||||
<span style={{ width: 18, height: 18, borderRadius: 5, background: `${tint}29`, display: "inline-block" }} />
|
||||
<span style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".08em", color: tint }}>{label}</span>
|
||||
{count ? (<><span style={{ flex: 1 }} /><span style={{ fontFamily: mono, fontSize: 9, color: "#5a5a62" }}>{count}</span></>) : null}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const tag = (t: string, dim?: boolean): CSSProperties => ({ fontFamily: mono, fontSize: 10, color: dim ? "#8a8a92" : "#cfcfd5", padding: "3px 7px", borderRadius: 5, background: dim ? "rgba(255,255,255,.03)" : "rgba(255,255,255,.05)" });
|
||||
|
||||
function ClawAnatomyCanvas({ sel }: { sel: Claw }) {
|
||||
const row = (a: string, b: string, bc = "#cfcfd5") => (<div style={{ display: "flex" }}><span style={{ flex: 1 }}>{a}</span><span style={{ color: bc }}>{b}</span></div>);
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%)" }}>
|
||||
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "14px 18px 6px" }}>
|
||||
<span style={{ fontFamily: mono, fontSize: 11, letterSpacing: ".1em", color: "#ff8a7a", padding: "4px 9px", borderRadius: 6, border: "1px solid rgba(255,111,97,.25)", background: "rgba(255,111,97,.07)" }}>ANATOMY</span>
|
||||
<span style={{ fontFamily: mono, fontSize: 11, color: "#6a6a72" }}>what {sel.name} is made of · compartments wired to the core</span>
|
||||
</div>
|
||||
<div style={{ position: "relative", flex: 1, minHeight: 0, overflow: "auto", padding: "22px 20px 28px" }}>
|
||||
<div style={{ maxWidth: 820, margin: "0 auto", display: "flex", flexDirection: "column", alignItems: "center", gap: 20 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 9 }}>
|
||||
<div style={{ position: "relative", width: 78, height: 78 }}>
|
||||
<div style={{ position: "absolute", inset: -7, borderRadius: "50%", border: "1.5px solid rgba(255,111,97,.35)" }} />
|
||||
<div style={{ position: "absolute", inset: 0, borderRadius: "50%", background: "rgba(255,111,97,.32)", animation: "cm-halo 2s ease-out infinite" }} />
|
||||
<div style={{ position: "relative", width: 78, height: 78, borderRadius: "50%", background: sel.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 28, fontWeight: 700, color: sel.ink, boxShadow: "0 0 44px rgba(255,111,97,.45)" }}>{sel.initial}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}><div style={{ fontSize: 14, fontWeight: 700, color: "#fff" }}>{sel.name}</div><div style={{ fontFamily: mono, fontSize: 9, color: "#8a8a92", letterSpacing: ".04em" }}>CLAUDE SONNET · CORE</div></div>
|
||||
</div>
|
||||
<div style={{ width: "100%", display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(196px, 1fr))", gap: 14, alignItems: "start" }}>
|
||||
<AnatomyCard tint="#ff6f61" label="SKILLS" count="7"><div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}><span style={tag("")}>Web research</span><span style={tag("")}>Summarize</span><span style={tag("")}>Data viz</span><span style={tag("", true)}>+4</span></div></AnatomyCard>
|
||||
<AnatomyCard tint="#c98af0" label="PERSONALITY"><div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}><span style={tag("")}>Analytical</span><span style={tag("")}>Concise</span><span style={tag("")}>Skeptical</span></div></AnatomyCard>
|
||||
<AnatomyCard tint="#5fd08a" label="MEMORY"><div style={{ display: "flex", flexDirection: "column", gap: 5, fontFamily: mono, fontSize: 10, color: "#9a9aa2" }}>{row("Long-term", "2,418 notes")}{row("Recent ctx", "18 msgs")}</div></AnatomyCard>
|
||||
<AnatomyCard tint="#5ec8d8" label="TOOLS · DOORS"><div style={{ display: "flex", flexDirection: "column", gap: 4, fontFamily: mono, fontSize: 10 }}>{row("Email", "gated ✓", "#5fd08a")}{row("Slack", "gated ✓", "#5fd08a")}{row("Browser", "gated ✓", "#5fd08a")}{row("Shell", "blocked ⨯", "#e8b465")}</div></AnatomyCard>
|
||||
<AnatomyCard tint="#e8b465" label="CAPABILITIES"><div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}><span style={tag("")}>File mgmt</span><span style={tag("")}>Scheduling</span><span style={tag("")}>Code exec</span></div></AnatomyCard>
|
||||
<AnatomyCard tint="#6fd0c0" label="SAFETY · §15"><div style={{ display: "flex", flexDirection: "column", gap: 5, fontFamily: mono, fontSize: 10, color: "#9a9aa2" }}>{row("Sandbox", "isolated", "#6fd0c0")}{row("Network", "none", "#6fd0c0")}</div></AnatomyCard>
|
||||
</div>
|
||||
<span style={{ textAlign: "center", fontFamily: mono, fontSize: 10, color: "#3a3a40" }}>click a compartment to inspect · the computer panel runs this claw's apps & routines</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComputerPanelStatic({ sel, onClose }: { sel: Claw; onClose: () => void }) {
|
||||
const dockTile = (label: string, coral: boolean, icon: React.ReactNode) => (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: 11, background: "linear-gradient(135deg,#ff8a7a,#ff5f57)", display: "flex", alignItems: "center", justifyContent: "center" }}>{icon}</div>
|
||||
<span style={{ fontSize: 10, color: coral ? "#ff8a7a" : "#b5b5bd" }}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ width: 330, flex: "none", borderLeft: "1px solid rgba(255,255,255,.06)", background: "#0b0b0e", display: "flex", flexDirection: "column", minHeight: 0, animation: "cm-fade .25s ease" }}>
|
||||
<div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div style={{ position: "relative", width: 38, height: 38 }}>
|
||||
<div style={{ width: 38, height: 38, borderRadius: 10, background: sel.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16, fontWeight: 700, color: sel.ink }}>{sel.initial}</div>
|
||||
<span style={{ position: "absolute", right: -2, bottom: -2, width: 11, height: 11, borderRadius: "50%", background: "#5fd08a", border: "2px solid #0b0b0e" }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: "#f3f3f5" }}>{sel.name}'s Computer</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, color: "#5ec8d8" }}>● sandbox live · no network</div>
|
||||
</div>
|
||||
<div onClick={onClose} style={{ width: 26, height: 26, borderRadius: 7, border: "1px solid rgba(255,255,255,.1)", display: "flex", alignItems: "center", justifyContent: "center", color: "#6a6a72", fontSize: 13, cursor: "pointer" }}>⤢</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: 16 }}>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 12 }}>APPS</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: "10px 6px", marginBottom: 22 }}>
|
||||
<AppTile label="Browser"><svg width="26" height="26" viewBox="0 0 26 26"><circle cx="13" cy="13" r="11" fill="#fff" stroke="#e0e0e0" /><circle cx="13" cy="13" r="4.4" fill="#4a90e2" /><path d="M13 8.6 H24" stroke="#ea4335" strokeWidth="3.6" /><path d="M9.2 11 L4 3.4" stroke="#34a853" strokeWidth="3.6" /><path d="M13 17.4 L7.5 22.5" stroke="#fbbc05" strokeWidth="3.6" /></svg></AppTile>
|
||||
<AppTile label="Slack"><svg width="22" height="22" viewBox="0 0 22 22"><rect x="9" y="2" width="4" height="11" rx="2" fill="#36c5f0" /><rect x="9" y="13" width="4" height="7" rx="2" fill="#2eb67d" /><rect x="2" y="9" width="11" height="4" rx="2" fill="#ecb22e" /><rect x="9" y="9" width="11" height="4" rx="2" fill="#e01e5a" /></svg></AppTile>
|
||||
<AppTile label="Claw Chat" coral><svg width="22" height="22" viewBox="0 0 22 22"><path d="M3 5a2 2 0 012-2h12a2 2 0 012 2v8a2 2 0 01-2 2H8l-4 4v-4H5a2 2 0 01-2-2z" fill="#fff" /></svg></AppTile>
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 13, border: "1.5px dashed rgba(255,255,255,.18)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff6f61", fontSize: 22, fontWeight: 300, cursor: "pointer" }}>+</div>
|
||||
<span style={{ fontSize: 10, color: "#8a8a92" }}>Add Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 10 }}>NOW RUNNING</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ padding: "10px 11px", borderRadius: 10, background: "#101014", border: "1px solid rgba(255,255,255,.06)" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 6 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#5ec8d8", animation: "cm-blink 1.4s infinite" }} />
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: "#e6e6ea" }}>Q3 competitor scan</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
<span style={{ fontFamily: mono, fontSize: 9, color: "#5ec8d8" }}>loop · step 14</span>
|
||||
</div>
|
||||
<div style={{ height: 4, borderRadius: 2, background: "rgba(255,255,255,.08)", overflow: "hidden" }}><div style={{ width: "62%", height: "100%", background: "linear-gradient(90deg,#5ec8d8,#4aa3b8)" }} /></div>
|
||||
</div>
|
||||
<div style={{ padding: "10px 11px", borderRadius: 10, background: "#101014", border: "1px solid rgba(255,255,255,.06)", display: "flex", alignItems: "center", gap: 7 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#e8b465" }} />
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: "#e6e6ea" }}>Daily digest</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
<span style={{ fontFamily: mono, fontSize: 9, color: "#e8b465" }}>cron · 08:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "none", margin: "0 12px 14px", padding: 12, borderRadius: 14, background: "#121216", border: "1px solid rgba(255,255,255,.07)", display: "flex", justifyContent: "space-around" }}>
|
||||
{dockTile("Skills", false, <svg width="18" height="18" viewBox="0 0 18 18"><path d="M10 1 L3 10 H8 L7 17 L15 7 H9 Z" fill="#fff" /></svg>)}
|
||||
{dockTile("Files", false, <svg width="18" height="18" viewBox="0 0 18 18"><path d="M2 5a1.5 1.5 0 011.5-1.5H7l1.5 1.5h6A1.5 1.5 0 0116 6.5v7A1.5 1.5 0 0114.5 15h-11A1.5 1.5 0 012 13.5z" fill="#fff" /></svg>)}
|
||||
{dockTile("Routines", true, <svg width="18" height="18" viewBox="0 0 18 18"><path d="M9 2a5 5 0 00-5 5c0 4-1.5 5-1.5 5h13S14 11 14 7a5 5 0 00-5-5z" fill="#fff" /><path d="M7.5 15a1.5 1.5 0 003 0" fill="#fff" /></svg>)}
|
||||
{dockTile("Settings", false, <svg width="18" height="18" viewBox="0 0 18 18"><circle cx="9" cy="9" r="2.6" fill="#fff" /><path d="M9 1.5v2M9 14.5v2M1.5 9h2M14.5 9h2M3.7 3.7l1.4 1.4M12.9 12.9l1.4 1.4M14.3 3.7l-1.4 1.4M5.1 12.9l-1.4 1.4" stroke="#fff" strokeWidth="1.5" /></svg>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppTile({ label, coral, children }: { label: string; coral?: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 13, background: coral ? "linear-gradient(135deg,#ff8a7a,#ff5f57)" : "#fff", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 3px 10px rgba(0,0,0,.4)" }}>{children}</div>
|
||||
<span style={{ fontSize: 10, color: "#b5b5bd" }}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user