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:
160
components/layout/project-shell.tsx
Normal file
160
components/layout/project-shell.tsx
Normal 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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user