Files
vibn-frontend/components/layout/vibn-sidebar.tsx
Mark Henderson 651ddf1e11 Rip out Theia, ship P5.1 attach E2E + Justine UI work-in-progress
Theia rip-out:
- Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim)
- Delete app/api/projects/[projectId]/workspace/route.ts and
  app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning)
- Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts
- Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/
  theiaError from app/api/projects/create/route.ts response
- Remove Theia callbackUrl branch in app/auth/page.tsx
- Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx
- Drop theiaWorkspaceUrl from deployment/page.tsx Project type
- Strip Theia IDE line + theia-code-os from advisor + agent-chat
  context strings
- Scrub Theia mention from lib/auth/workspace-auth.ts comment

P5.1 (custom apex domains + DNS):
- lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS
  XML auth, Cloud DNS plumbing
- scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS +
  prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup

In-progress (Justine onboarding/build, MVP setup, agent telemetry):
- New (justine)/stories, project (home) layouts, mvp-setup, run, tasks
  routes + supporting components
- Project shell + sidebar + nav refactor for the Stackless palette
- Agent session API hardening (sessions, events, stream, approve,
  retry, stop) + atlas-chat, advisor, design-surfaces refresh
- New scripts/sync-db-url-from-coolify.mjs +
  scripts/prisma-db-push.mjs + docker-compose.local-db.yml for
  local Prisma workflows
- lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts
- Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel

Made-with: Cursor
2026-04-22 18:05:01 -07:00

347 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { signOut, useSession } from "next-auth/react";
interface TabItem {
id: string;
label: string;
path: string;
}
interface VIBNSidebarProps {
workspace: string;
tabs?: TabItem[];
activeTab?: string;
}
interface ProjectData {
id: string;
productName?: string;
name?: string;
status?: string;
}
// ── Main sidebar ─────────────────────────────────────────────────────────────
const COLLAPSED_KEY = "vibn_sidebar_collapsed";
const COLLAPSED_W = 52;
const EXPANDED_W = 216;
export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
const pathname = usePathname();
const { data: session } = useSession();
const [collapsed, setCollapsed] = useState(false);
const [mounted, setMounted] = useState(false);
// Project-specific data
const [project, setProject] = useState<ProjectData | null>(null);
// Global projects list (used when NOT inside a project)
const [projects, setProjects] = useState<Array<{ id: string; productName: string; status?: string }>>([]);
const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null;
// Restore collapse state
useEffect(() => {
const stored = localStorage.getItem(COLLAPSED_KEY);
if (stored === "1") setCollapsed(true);
setMounted(true);
}, []);
const toggle = () => {
setCollapsed(prev => {
localStorage.setItem(COLLAPSED_KEY, prev ? "0" : "1");
return !prev;
});
};
// Fetch global projects list (for non-project pages)
useEffect(() => {
if (activeProjectId) return;
fetch("/api/projects")
.then(r => r.json())
.then(d => setProjects(d.projects ?? []))
.catch(() => {});
}, [activeProjectId]);
// Fetch project-specific data when inside a project
useEffect(() => {
if (!activeProjectId) { setProject(null); return; }
fetch(`/api/projects/${activeProjectId}`)
.then(r => r.json())
.then(d => setProject(d.project ?? null))
.catch(() => {});
}, [activeProjectId]);
const isProjects = !activeProjectId && (pathname?.includes("/projects") || pathname?.includes("/project"));
const isActivity = !activeProjectId && pathname?.includes("/activity");
const isSettings = !activeProjectId && pathname?.includes("/settings");
const topNavItems = [
{ id: "projects", label: "Projects", icon: "⌗", href: `/${workspace}/projects` },
{ id: "activity", label: "Activity", icon: "↗", href: `/${workspace}/activity` },
{ id: "settings", label: "Settings", icon: "⚙", href: `/${workspace}/settings` },
];
const userInitial = session?.user?.name?.[0]?.toUpperCase()
?? session?.user?.email?.[0]?.toUpperCase()
?? "?";
const w = collapsed ? COLLAPSED_W : EXPANDED_W;
const transition = mounted ? "width 0.2s cubic-bezier(0.4,0,0.2,1)" : "none";
const base = `/${workspace}/project/${activeProjectId}`;
return (
<nav style={{
width: w, height: "100vh",
background: "#fff", borderRight: "1px solid #e8e4dc",
display: "flex", flexDirection: "column",
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
flexShrink: 0, overflow: "hidden",
transition, position: "relative",
}}>
{/* ── Logo + toggle ── */}
{collapsed ? (
<div style={{ flexShrink: 0 }}>
<div style={{ display: "flex", justifyContent: "center", padding: "14px 0 6px" }}>
<Link href={`/${workspace}/projects`} title="VIBN" style={{ textDecoration: "none" }}>
<div style={{ width: 26, height: 26, borderRadius: 7, overflow: "hidden" }}>
<img src="/vibn-black-circle-logo.png" alt="VIBN" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
</div>
</Link>
</div>
<div style={{ display: "flex", justifyContent: "center", paddingBottom: 8 }}>
<button onClick={toggle} title="Expand sidebar" style={{
background: "#f0ece4", border: "none", cursor: "pointer",
color: "#6b6560", width: 26, height: 20, borderRadius: 5,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "0.8rem", fontWeight: 700,
}}
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); }}
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); }}
></button>
</div>
</div>
) : (
<div style={{ padding: "14px 10px 14px 16px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 9, flexShrink: 0 }}>
<Link href={`/${workspace}/projects`} style={{ display: "flex", alignItems: "center", gap: 9, textDecoration: "none", minWidth: 0 }}>
<div style={{ width: 26, height: 26, borderRadius: 7, overflow: "hidden", flexShrink: 0 }}>
<img src="/vibn-black-circle-logo.png" alt="VIBN" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
</div>
<span style={{ fontSize: "0.92rem", fontWeight: 600, color: "#1a1a1a", letterSpacing: "-0.03em", fontFamily: "var(--font-lora), ui-serif, serif", whiteSpace: "nowrap" }}>
vibn
</span>
</Link>
<button onClick={toggle} title="Collapse sidebar" style={{
background: "#f0ece4", border: "none", cursor: "pointer",
color: "#6b6560", width: 24, height: 22, borderRadius: 5,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "0.8rem", fontWeight: 700, flexShrink: 0,
}}
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); }}
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); }}
></button>
</div>
)}
{/* ── Top nav ── */}
<div style={{ padding: collapsed ? "2px 6px" : "2px 8px", flexShrink: 0 }}>
{topNavItems.map(n => {
const isActive = n.id === "projects" ? isProjects
: n.id === "activity" ? isActivity
: isSettings;
return (
<Link key={n.id} href={n.href} title={collapsed ? n.label : undefined} style={{
width: "100%", display: "flex", alignItems: "center",
justifyContent: collapsed ? "center" : "flex-start",
gap: 8, padding: collapsed ? "8px 0" : "7px 10px",
borderRadius: 6,
background: isActive ? "#f6f4f0" : "transparent",
color: isActive ? "#1a1a1a" : "#6b6560",
fontSize: "0.8rem", fontWeight: isActive ? 600 : 500,
transition: "background 0.12s", textDecoration: "none",
}}
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
>
<span style={{ fontSize: collapsed ? "0.95rem" : "0.78rem", opacity: collapsed ? (isActive ? 0.9 : 0.45) : 0.45, width: collapsed ? "auto" : 16, textAlign: "center" }}>
{n.icon}
</span>
{!collapsed && n.label}
</Link>
);
})}
</div>
<div style={{ height: 1, background: "#eae6de", margin: "8px 14px", flexShrink: 0 }} />
{/* ── Lower section ── */}
<div style={{ flex: 1, overflow: "auto", paddingBottom: 8 }}>
{activeProjectId && project ? (
/* ── PROJECT VIEW: name + status + section tabs ── */
<>
{!collapsed && (
<>
<div style={{ padding: "6px 12px 8px" }}>
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: "#1a1a1a", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{project.productName || project.name || "Project"}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 5, marginTop: 3 }}>
<span style={{
width: 6, height: 6, borderRadius: "50%", flexShrink: 0, display: "inline-block",
background: project.status === "live" ? "#2e7d32"
: project.status === "building" ? "#3d5afe"
: "#d4a04a",
}} />
<span style={{ fontSize: "0.68rem", color: "#8a8478" }}>
{project.status === "live" ? "Live" : project.status === "building" ? "Building" : "Defining"}
</span>
</div>
</div>
{tabs && tabs.length > 0 && (
<div style={{ padding: "2px 8px" }}>
{tabs.map(t => {
const isActive = activeTab === t.id;
return (
<Link
key={t.id}
href={`/${workspace}/project/${activeProjectId}/${t.path}`}
style={{
width: "100%", display: "flex", alignItems: "center",
padding: "7px 10px", borderRadius: 6,
background: isActive ? "#f6f4f0" : "transparent",
color: isActive ? "#1a1a1a" : "#6b6560",
fontSize: "0.8rem", fontWeight: isActive ? 600 : 500,
transition: "background 0.12s", textDecoration: "none",
}}
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
>
{t.label}
</Link>
);
})}
</div>
)}
</>
)}
{collapsed && (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", paddingTop: 8, gap: 6 }}>
<span style={{
width: 7, height: 7, borderRadius: "50%", display: "inline-block",
background: project.status === "live" ? "#2e7d32"
: project.status === "building" ? "#3d5afe"
: "#d4a04a",
}} title={project.productName || project.name} />
{tabs && tabs.map(t => {
const isActive = activeTab === t.id;
return (
<Link
key={t.id}
href={`/${workspace}/project/${activeProjectId}/${t.path}`}
title={t.label}
style={{
width: 28, height: 28, borderRadius: 6, display: "flex",
alignItems: "center", justifyContent: "center",
background: isActive ? "#f6f4f0" : "transparent",
color: isActive ? "#1a1a1a" : "#a09a90",
fontSize: "0.6rem", fontWeight: 700, textDecoration: "none",
textTransform: "uppercase", letterSpacing: "0.02em",
transition: "background 0.12s",
}}
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
>
{t.label.slice(0, 2)}
</Link>
);
})}
</div>
)}
</>
) : (
/* ── GLOBAL VIEW: projects list ── */
<div style={{ padding: collapsed ? "2px 6px" : "2px 8px" }}>
{!collapsed && (
<div style={{ fontSize: "0.58rem", fontWeight: 600, color: "#a09a90", letterSpacing: "0.1em", textTransform: "uppercase", padding: "6px 10px 8px" }}>
Projects
</div>
)}
{projects.map(p => {
const isActive = activeProjectId === p.id;
const color = p.status === "live" ? "#2e7d32" : p.status === "building" ? "#3d5afe" : "#d4a04a";
return (
<Link key={p.id} href={`/${workspace}/project/${p.id}`}
title={collapsed ? p.productName : undefined}
style={{
width: "100%", display: "flex", alignItems: "center",
justifyContent: collapsed ? "center" : "flex-start",
gap: 9, padding: collapsed ? "9px 0" : "7px 10px",
borderRadius: 6,
background: isActive ? "#f6f4f0" : "transparent",
color: "#1a1a1a", fontSize: "0.8rem",
fontWeight: isActive ? 600 : 450,
transition: "background 0.12s", textDecoration: "none", overflow: "hidden",
}}
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
>
<span style={{ width: 7, height: 7, borderRadius: "50%", background: color, display: "inline-block", flexShrink: 0 }} />
{!collapsed && (
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{p.productName}
</span>
)}
</Link>
);
})}
</div>
)}
</div>
{/* ── User footer ── */}
<div style={{
padding: collapsed ? "10px 0" : "12px 14px",
borderTop: "1px solid #eae6de",
display: "flex", alignItems: "center",
justifyContent: collapsed ? "center" : "flex-start",
gap: 9, flexShrink: 0,
}}>
<div title={collapsed ? (session?.user?.name ?? session?.user?.email ?? "Account") : undefined}
style={{
width: 26, height: 26, borderRadius: "50%",
background: "#f0ece4", display: "flex", alignItems: "center",
justifyContent: "center", fontSize: "0.7rem", fontWeight: 600,
color: "#8a8478", flexShrink: 0, cursor: "default",
}}>
{userInitial}
</div>
{!collapsed && (
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: "0.76rem", fontWeight: 500, color: "#1a1a1a", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{session?.user?.name ?? session?.user?.email?.split("@")[0] ?? "Account"}
</div>
<button onClick={() => signOut({ callbackUrl: "/auth" })} style={{
background: "none", border: "none", padding: 0,
fontSize: "0.62rem", color: "#a09a90", cursor: "pointer",
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
}}>
Sign out
</button>
</div>
)}
</div>
</nav>
);
}