Add Stackless right panel to project shell
Shows Discovery phase tracker (Big Picture → Risks), Captured data from Atlas, and Project Info (created, last active, features). Data flows from DB via layout server component. Made-with: Cursor
This commit is contained in:
@@ -13,27 +13,60 @@ interface ProjectShellProps {
|
||||
projectName: string;
|
||||
projectStatus?: string;
|
||||
projectProgress?: number;
|
||||
discoveryPhase?: number;
|
||||
capturedData?: Record<string, string>;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
featureCount?: 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" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
const DISCOVERY_PHASES = [
|
||||
"Big Picture",
|
||||
"Users & Personas",
|
||||
"Features",
|
||||
"Business Model",
|
||||
"Screens",
|
||||
"Risks",
|
||||
];
|
||||
|
||||
function timeAgo(dateStr?: string): string {
|
||||
if (!dateStr) return "—";
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return "—";
|
||||
const diff = (Date.now() - date.getTime()) / 1000;
|
||||
if (diff < 60) return "just now";
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||||
const days = Math.floor(diff / 86400);
|
||||
if (days === 1) return "Yesterday";
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return new Date(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontSize: "0.6rem", fontWeight: 600, color: "#a09a90",
|
||||
letterSpacing: "0.1em", textTransform: "uppercase", marginBottom: 12,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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";
|
||||
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,
|
||||
@@ -53,22 +86,27 @@ export function ProjectShell({
|
||||
projectName,
|
||||
projectStatus,
|
||||
projectProgress,
|
||||
discoveryPhase = 0,
|
||||
capturedData = {},
|
||||
createdAt,
|
||||
updatedAt,
|
||||
featureCount = 0,
|
||||
}: ProjectShellProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Determine which tab is active
|
||||
const activeTab = TABS.find((t) => pathname?.includes(`/${t.path}`))?.id ?? "overview";
|
||||
|
||||
const progress = projectProgress ?? 0;
|
||||
|
||||
const capturedEntries = Object.entries(capturedData);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "flex", height: "100vh", background: "#f6f4f0", overflow: "hidden" }}>
|
||||
{/* Sidebar */}
|
||||
{/* Left sidebar */}
|
||||
<VIBNSidebar workspace={workspace} />
|
||||
|
||||
{/* Main content */}
|
||||
{/* Main column */}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
|
||||
{/* Project header */}
|
||||
<div style={{
|
||||
padding: "18px 32px",
|
||||
@@ -85,10 +123,7 @@ export function ProjectShell({
|
||||
background: "#1a1a1a12",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: "Newsreader, serif",
|
||||
fontSize: "1rem", fontWeight: 500, color: "#1a1a1a",
|
||||
}}>
|
||||
<span style={{ fontFamily: "Newsreader, serif", fontSize: "1rem", fontWeight: 500, color: "#1a1a1a" }}>
|
||||
{projectName[0]?.toUpperCase() ?? "P"}
|
||||
</span>
|
||||
</div>
|
||||
@@ -96,8 +131,7 @@ export function ProjectShell({
|
||||
<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,
|
||||
letterSpacing: "-0.02em", fontFamily: "Outfit, sans-serif", margin: 0,
|
||||
}}>
|
||||
{projectName}
|
||||
</h2>
|
||||
@@ -120,7 +154,6 @@ export function ProjectShell({
|
||||
padding: "0 32px",
|
||||
borderBottom: "1px solid #e8e4dc",
|
||||
display: "flex",
|
||||
gap: 0,
|
||||
background: "#fff",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
@@ -130,8 +163,6 @@ export function ProjectShell({
|
||||
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",
|
||||
@@ -153,6 +184,97 @@ export function ProjectShell({
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right panel */}
|
||||
<div style={{
|
||||
width: 230,
|
||||
borderLeft: "1px solid #e8e4dc",
|
||||
background: "#fff",
|
||||
padding: "22px 18px",
|
||||
overflow: "auto",
|
||||
flexShrink: 0,
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
}}>
|
||||
{/* Discovery phases */}
|
||||
<SectionLabel>Discovery</SectionLabel>
|
||||
{DISCOVERY_PHASES.map((phase, i) => {
|
||||
const isDone = i < discoveryPhase;
|
||||
const isActive = i === discoveryPhase;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 10,
|
||||
padding: "9px 0",
|
||||
borderBottom: i < DISCOVERY_PHASES.length - 1 ? "1px solid #f0ece4" : "none",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 20, height: 20, borderRadius: 5, flexShrink: 0,
|
||||
background: isDone ? "#2e7d3210" : isActive ? "#d4a04a12" : "#f6f4f0",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.58rem", fontWeight: 700,
|
||||
color: isDone ? "#2e7d32" : isActive ? "#9a7b3a" : "#c5c0b8",
|
||||
}}>
|
||||
{isDone ? "✓" : isActive ? "→" : i + 1}
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: "0.78rem",
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
color: isDone ? "#6b6560" : isActive ? "#1a1a1a" : "#b5b0a6",
|
||||
}}>
|
||||
{phase}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={{ height: 1, background: "#f0ece4", margin: "16px 0" }} />
|
||||
|
||||
{/* Captured data */}
|
||||
<SectionLabel>Captured</SectionLabel>
|
||||
{capturedEntries.length > 0 ? (
|
||||
capturedEntries.map(([k, v], i) => (
|
||||
<div key={i} style={{ marginBottom: 14 }}>
|
||||
<div style={{
|
||||
fontSize: "0.62rem", color: "#b5b0a6",
|
||||
textTransform: "uppercase", letterSpacing: "0.05em",
|
||||
marginBottom: 3, fontWeight: 600,
|
||||
}}>
|
||||
{k}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.8rem", color: "#4a4640", lineHeight: 1.45 }}>
|
||||
{v}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p style={{ fontSize: "0.78rem", color: "#c5c0b8", lineHeight: 1.5, margin: 0 }}>
|
||||
Atlas will capture key details here as you chat.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ height: 1, background: "#f0ece4", margin: "16px 0" }} />
|
||||
|
||||
{/* Project info */}
|
||||
<SectionLabel>Project Info</SectionLabel>
|
||||
{[
|
||||
{ k: "Created", v: timeAgo(createdAt) },
|
||||
{ k: "Last active", v: timeAgo(updatedAt) },
|
||||
{ k: "Features", v: featureCount > 0 ? `${featureCount} defined` : "None yet" },
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ marginBottom: 12 }}>
|
||||
<div style={{
|
||||
fontSize: "0.62rem", color: "#b5b0a6",
|
||||
textTransform: "uppercase", letterSpacing: "0.05em",
|
||||
marginBottom: 3, fontWeight: 600,
|
||||
}}>
|
||||
{item.k}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.8rem", color: "#4a4640" }}>{item.v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user