Files
vibn-frontend/components/layout/project-shell.tsx
Mark Henderson bb021be088 refactor: rework project page layout - sidebar as product OS, full-width content
- VIBNSidebar: when inside a project, lower section now shows 7 product
  layer sections (Apps, Layouts, Infrastructure, Growth, Monetize, Support,
  Analytics) instead of the projects list. Sections self-fetch data from
  /api/projects/[id] and /api/projects/[id]/apps. On non-project pages,
  reverts to the projects list as before.
- ProjectShell: removed the project header strip (name/status/progress bar)
  and the persistent 230px right panel entirely. Tab bar now sits at the
  top of the content area with no header above it. Content is full-width.
  Each page manages its own internal layout.

Made-with: Cursor
2026-03-06 13:26:08 -08:00

128 lines
4.3 KiB
TypeScript

"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;
projectDescription?: string;
projectStatus?: string;
projectProgress?: number;
discoveryPhase?: number;
capturedData?: Record<string, string>;
createdAt?: string;
updatedAt?: string;
featureCount?: number;
creationMode?: "fresh" | "chat-import" | "code-import" | "migration";
}
const ALL_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: "Launch", path: "deployment" },
{ id: "grow", label: "Grow", path: "grow" },
{ id: "insights", label: "Insights", path: "insights" },
{ id: "settings", label: "Settings", path: "settings" },
];
function getTabsForMode(
mode: "fresh" | "chat-import" | "code-import" | "migration" = "fresh"
) {
switch (mode) {
case "code-import":
return ALL_TABS.filter(t => t.id !== "prd");
case "migration":
return ALL_TABS
.filter(t => !["prd", "grow", "insights"].includes(t.id))
.map(t => t.id === "overview" ? { ...t, label: "Migration Plan" } : t);
default:
return ALL_TABS;
}
}
export function ProjectShell({
children,
workspace,
projectId,
creationMode,
}: ProjectShellProps) {
const pathname = usePathname();
const TABS = getTabsForMode(creationMode);
const activeTab = TABS.find(t => pathname?.includes(`/${t.path}`))?.id ?? "overview";
return (
<>
<style>{`
@media (max-width: 768px) {
.vibn-left-sidebar { display: none !important; }
.vibn-tab-bar { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.vibn-tab-bar a { padding: 10px 14px !important; font-size: 0.75rem !important; }
.vibn-page-content { padding-bottom: env(safe-area-inset-bottom); }
}
@media (max-width: 480px) {
.vibn-tab-bar a { padding: 10px 10px !important; }
}
`}</style>
<div style={{ display: "flex", height: "100dvh", background: "#f6f4f0", overflow: "hidden" }}>
{/* Left sidebar */}
<div className="vibn-left-sidebar" style={{ display: "flex" }}>
<VIBNSidebar workspace={workspace} />
</div>
{/* Main column — full width */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
{/* Tab bar — sits at the top, no header above it */}
<div className="vibn-tab-bar" style={{
padding: "0 24px",
borderBottom: "1px solid #e8e4dc",
display: "flex",
background: "#fff",
flexShrink: 0,
}}>
{TABS.map(t => (
<Link
key={t.id}
href={`/${workspace}/project/${projectId}/${t.path}`}
style={{
padding: "13px 16px",
fontSize: "0.8rem",
fontWeight: 500,
color: activeTab === t.id ? "#1a1a1a" : "#a09a90",
borderBottom: activeTab === t.id ? "2px solid #1a1a1a" : "2px solid transparent",
transition: "color 0.12s",
fontFamily: "Outfit, sans-serif",
textDecoration: "none",
display: "block",
whiteSpace: "nowrap",
}}
onMouseEnter={e => { if (activeTab !== t.id) (e.currentTarget as HTMLElement).style.color = "#6b6560"; }}
onMouseLeave={e => { if (activeTab !== t.id) (e.currentTarget as HTMLElement).style.color = "#a09a90"; }}
>
{t.label}
</Link>
))}
</div>
{/* Page content — full width, each page manages its own layout */}
<div className="vibn-page-content" style={{ flex: 1, overflow: "auto" }}>
{children}
</div>
</div>
</div>
<Toaster position="top-center" />
</>
);
}