Adopt Stackless UI: warm palette, sidebar, project tab bar with Design tab

- Add Google Fonts (Newsreader/Outfit/IBM Plex Mono) + warm beige CSS palette
- New VIBNSidebar: Stackless-style 220px sidebar with project list + user footer
- New ProjectShell: project header with name/status/progress% + tab bar
- Tabs: Atlas → PRD → Design → Build → Deploy → Settings
- New /prd page: section-by-section progress view
- New /build page: locked until PRD complete
- Projects list page: Stackless-style row layout
- Simplify overview page to just render AtlasChat

Made-with: Cursor
This commit is contained in:
2026-03-02 16:01:33 -08:00
parent 7ba3b9563e
commit aaa3f51592
9 changed files with 1051 additions and 451 deletions

View File

@@ -0,0 +1,160 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode } from "react";
import { VIBNSidebar } from "./vibn-sidebar";
import { Toaster } from "sonner";
interface ProjectShellProps {
children: ReactNode;
workspace: string;
projectId: string;
projectName: string;
projectStatus?: string;
projectProgress?: number;
}
const TABS = [
{ id: "overview", label: "Atlas", path: "overview" },
{ id: "prd", label: "PRD", path: "prd" },
{ id: "design", label: "Design", path: "design" },
{ id: "build", label: "Build", path: "build" },
{ id: "deployment", label: "Deploy", path: "deployment" },
{ id: "settings", label: "Settings", path: "settings" },
];
function StatusTag({ status }: { status?: string }) {
const label = status === "live" ? "Live"
: status === "building" ? "Building"
: "Defining";
const color = status === "live" ? "#2e7d32"
: status === "building" ? "#3d5afe"
: "#9a7b3a";
const bg = status === "live" ? "#2e7d3210"
: status === "building" ? "#3d5afe10"
: "#d4a04a12";
return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
padding: "3px 9px", borderRadius: 4,
fontSize: "0.68rem", fontWeight: 600, letterSpacing: "0.02em",
color, background: bg, fontFamily: "Outfit, sans-serif",
}}>
{label}
</span>
);
}
export function ProjectShell({
children,
workspace,
projectId,
projectName,
projectStatus,
projectProgress,
}: ProjectShellProps) {
const pathname = usePathname();
// Determine which tab is active
const activeTab = TABS.find((t) => pathname?.includes(`/${t.path}`))?.id ?? "overview";
const progress = projectProgress ?? 0;
return (
<>
<div style={{ display: "flex", height: "100vh", background: "#f6f4f0", overflow: "hidden" }}>
{/* Sidebar */}
<VIBNSidebar workspace={workspace} />
{/* Main content */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
{/* Project header */}
<div style={{
padding: "18px 32px",
borderBottom: "1px solid #e8e4dc",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#fff",
flexShrink: 0,
}}>
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
<div style={{
width: 34, height: 34, borderRadius: 9,
background: "#1a1a1a12",
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<span style={{
fontFamily: "Newsreader, serif",
fontSize: "1rem", fontWeight: 500, color: "#1a1a1a",
}}>
{projectName[0]?.toUpperCase() ?? "P"}
</span>
</div>
<div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<h2 style={{
fontSize: "1.05rem", fontWeight: 600, color: "#1a1a1a",
letterSpacing: "-0.02em", fontFamily: "Outfit, sans-serif",
margin: 0,
}}>
{projectName}
</h2>
<StatusTag status={projectStatus} />
</div>
</div>
</div>
<div style={{
fontFamily: "IBM Plex Mono, monospace",
fontSize: "0.78rem", fontWeight: 500,
color: "#1a1a1a", background: "#f6f4f0",
padding: "6px 12px", borderRadius: 6,
}}>
{progress}%
</div>
</div>
{/* Tab bar */}
<div style={{
padding: "0 32px",
borderBottom: "1px solid #e8e4dc",
display: "flex",
gap: 0,
background: "#fff",
flexShrink: 0,
}}>
{TABS.map((t) => (
<Link
key={t.id}
href={`/${workspace}/project/${projectId}/${t.path}`}
style={{
padding: "12px 18px",
border: "none",
background: "none",
fontSize: "0.8rem",
fontWeight: 500,
color: activeTab === t.id ? "#1a1a1a" : "#a09a90",
borderBottom: activeTab === t.id ? "2px solid #1a1a1a" : "2px solid transparent",
transition: "all 0.12s",
fontFamily: "Outfit, sans-serif",
textDecoration: "none",
display: "block",
whiteSpace: "nowrap",
}}
>
{t.label}
</Link>
))}
</div>
{/* Page content */}
<div style={{ flex: 1, overflow: "auto" }}>
{children}
</div>
</div>
</div>
<Toaster position="top-center" />
</>
);
}

View File

@@ -0,0 +1,222 @@
"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 Project {
id: string;
productName: string;
status?: string;
}
interface VIBNSidebarProps {
workspace: string;
}
function StatusDot({ status }: { status?: string }) {
const color =
status === "live" ? "#2e7d32"
: status === "building" ? "#3d5afe"
: "#d4a04a";
const anim = status === "building" ? "vibn-breathe 2.5s ease infinite" : "none";
return (
<span
style={{
width: 7, height: 7, borderRadius: "50%",
background: color, display: "inline-block",
flexShrink: 0, animation: anim,
}}
/>
);
}
export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
const pathname = usePathname();
const { data: session } = useSession();
const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => {
fetch("/api/projects")
.then((r) => r.json())
.then((d) => setProjects(d.projects ?? []))
.catch(() => {});
}, []);
// Derive active project from URL
const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null;
// Derive active top-level section
const isProjects = !activeProjectId && (pathname?.includes("/projects") || pathname?.includes("/project"));
const isActivity = pathname?.includes("/activity");
const isSettings = pathname?.includes("/settings");
const topNavItems = [
{ id: "projects", label: "Projects", icon: "⌗", href: `/${workspace}/projects` },
{ id: "settings", label: "Settings", icon: "⚙", href: `/${workspace}/settings` },
];
const userInitial = session?.user?.name?.[0]?.toUpperCase()
?? session?.user?.email?.[0]?.toUpperCase()
?? "?";
return (
<nav
style={{
width: 220,
height: "100vh",
background: "#fff",
borderRight: "1px solid #e8e4dc",
display: "flex",
flexDirection: "column",
fontFamily: "Outfit, sans-serif",
flexShrink: 0,
}}
>
{/* Logo */}
<Link
href={`/${workspace}/projects`}
style={{
padding: "22px 18px 18px",
display: "flex",
alignItems: "center",
gap: 9,
textDecoration: "none",
}}
>
<div
style={{
width: 28, height: 28, 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.95rem", fontWeight: 600, color: "#1a1a1a",
letterSpacing: "-0.03em", fontFamily: "Newsreader, serif",
}}
>
vibn
</span>
</Link>
{/* Top nav */}
<div style={{ padding: "4px 10px" }}>
{topNavItems.map((n) => {
const isActive = n.id === "projects" ? isProjects && !activeProjectId
: n.id === "settings" ? isSettings
: false;
return (
<Link
key={n.id}
href={n.href}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 9,
padding: "8px 10px",
borderRadius: 6,
background: isActive ? "#f6f4f0" : "transparent",
color: isActive ? "#1a1a1a" : "#6b6560",
fontSize: "0.82rem",
fontWeight: isActive ? 600 : 500,
transition: "all 0.12s",
textDecoration: "none",
}}
>
<span style={{ fontSize: "0.8rem", opacity: 0.45, width: 18, textAlign: "center" }}>
{n.icon}
</span>
{n.label}
</Link>
);
})}
</div>
<div style={{ height: 1, background: "#eae6de", margin: "10px 18px" }} />
{/* Projects list */}
<div style={{ padding: "2px 10px", flex: 1, overflow: "auto" }}>
<div
style={{
fontSize: "0.6rem", fontWeight: 600, color: "#a09a90",
letterSpacing: "0.1em", textTransform: "uppercase",
padding: "6px 10px 8px",
}}
>
Projects
</div>
{projects.map((p) => {
const isActive = activeProjectId === p.id;
return (
<Link
key={p.id}
href={`/${workspace}/project/${p.id}/overview`}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 9,
padding: "7px 10px",
borderRadius: 6,
background: isActive ? "#f6f4f0" : "transparent",
color: "#1a1a1a",
fontSize: "0.82rem",
fontWeight: isActive ? 600 : 450,
transition: "background 0.12s",
textDecoration: "none",
overflow: "hidden",
}}
>
<StatusDot status={p.status} />
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{p.productName}
</span>
</Link>
);
})}
</div>
{/* User footer */}
<div
style={{
padding: "14px 18px",
borderTop: "1px solid #eae6de",
display: "flex",
alignItems: "center",
gap: 9,
}}
>
<div
style={{
width: 28, height: 28, borderRadius: "50%",
background: "#f0ece4", display: "flex", alignItems: "center",
justifyContent: "center", fontSize: "0.72rem", fontWeight: 600,
color: "#8a8478", flexShrink: 0,
}}
>
{userInitial}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: "0.78rem", 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: "Outfit, sans-serif",
}}
>
Sign out
</button>
</div>
</div>
</nav>
);
}