From aaa3f51592a872df200772f927681cbc24c6f7e8 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 2 Mar 2026 16:01:33 -0800 Subject: [PATCH] Adopt Stackless UI: warm palette, sidebar, project tab bar with Design tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../project/[projectId]/build/page.tsx | 176 ++++++ .../project/[projectId]/layout.tsx | 32 +- .../project/[projectId]/overview/page.tsx | 134 +---- .../project/[projectId]/prd/page.tsx | 191 +++++++ app/[workspace]/projects/layout.tsx | 21 +- app/[workspace]/projects/page.tsx | 507 ++++++++---------- app/globals.css | 59 +- components/layout/project-shell.tsx | 160 ++++++ components/layout/vibn-sidebar.tsx | 222 ++++++++ 9 files changed, 1051 insertions(+), 451 deletions(-) create mode 100644 app/[workspace]/project/[projectId]/build/page.tsx create mode 100644 app/[workspace]/project/[projectId]/prd/page.tsx create mode 100644 components/layout/project-shell.tsx create mode 100644 components/layout/vibn-sidebar.tsx diff --git a/app/[workspace]/project/[projectId]/build/page.tsx b/app/[workspace]/project/[projectId]/build/page.tsx new file mode 100644 index 0000000..6dcb870 --- /dev/null +++ b/app/[workspace]/project/[projectId]/build/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import Link from "next/link"; + +interface Project { + id: string; + status?: string; + prd?: string; + giteaRepoUrl?: string; +} + +const BUILD_FEATURES = [ + "Authentication system", + "Database schema", + "API endpoints", + "Core UI", + "Business logic", + "Tests", +]; + +export default function BuildPage() { + const params = useParams(); + const projectId = params.projectId as string; + const workspace = params.workspace as string; + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`/api/projects/${projectId}`) + .then((r) => r.json()) + .then((d) => { + setProject(d.project); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, [projectId]); + + if (loading) { + return ( +
+ Loading… +
+ ); + } + + const hasRepo = Boolean(project?.giteaRepoUrl); + const hasPRD = Boolean(project?.prd); + + if (!hasPRD) { + return ( +
+
+
+ 🔒 +
+

+ Complete your PRD first +

+

+ Finish your discovery with Atlas, then the builder unlocks automatically. +

+ + Continue with Atlas → + +
+
+ ); + } + + if (!hasRepo) { + return ( +
+
+
+ ⚡ +
+

+ PRD ready — build coming soon +

+

+ The Architect agent will generate your project structure and kick off the build pipeline. + This feature is in active development. +

+
+
+ ); + } + + return ( +
+
+

+ Build progress +

+ {BUILD_FEATURES.map((f, i) => ( +
+ + {f} +
+
+
+ + 0% + +
+ ))} +
+
+ ); +} diff --git a/app/[workspace]/project/[projectId]/layout.tsx b/app/[workspace]/project/[projectId]/layout.tsx index be54a38..77d4065 100644 --- a/app/[workspace]/project/[projectId]/layout.tsx +++ b/app/[workspace]/project/[projectId]/layout.tsx @@ -1,7 +1,13 @@ -import { AppShell } from "@/components/layout/app-shell"; +import { ProjectShell } from "@/components/layout/project-shell"; import { query } from "@/lib/db-postgres"; -async function getProjectName(projectId: string): Promise { +interface ProjectData { + name: string; + status?: string; + progress?: number; +} + +async function getProjectData(projectId: string): Promise { try { const rows = await query<{ data: any }>( `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, @@ -9,12 +15,16 @@ async function getProjectName(projectId: string): Promise { ); if (rows.length > 0) { const data = rows[0].data; - return data?.productName || data?.name || "Project"; + return { + name: data?.productName || data?.name || "Project", + status: data?.status, + progress: data?.progress ?? 0, + }; } } catch (error) { - console.error("Error fetching project name:", error); + console.error("Error fetching project:", error); } - return "Project"; + return { name: "Project" }; } export default async function ProjectLayout({ @@ -25,11 +35,17 @@ export default async function ProjectLayout({ params: Promise<{ workspace: string; projectId: string }>; }) { const { workspace, projectId } = await params; - const projectName = await getProjectName(projectId); + const project = await getProjectData(projectId); return ( - + {children} - + ); } diff --git a/app/[workspace]/project/[projectId]/overview/page.tsx b/app/[workspace]/project/[projectId]/overview/page.tsx index 6d6e2cf..27dfb59 100644 --- a/app/[workspace]/project/[projectId]/overview/page.tsx +++ b/app/[workspace]/project/[projectId]/overview/page.tsx @@ -3,149 +3,54 @@ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { useSession } from "next-auth/react"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { OrchestratorChat } from "@/components/OrchestratorChat"; import { AtlasChat } from "@/components/AtlasChat"; -import { - Terminal, - Loader2, - RefreshCw, -} from "lucide-react"; -import { toast } from "sonner"; +import { OrchestratorChat } from "@/components/OrchestratorChat"; +import { Loader2 } from "lucide-react"; interface Project { id: string; - name: string; productName: string; - productVision?: string; - status?: string; - currentPhase?: string; - theiaWorkspaceUrl?: string; - stage?: 'discovery' | 'architecture' | 'building' | 'active'; - prd?: string; + stage?: "discovery" | "architecture" | "building" | "active"; } - export default function ProjectOverviewPage() { const params = useParams(); const projectId = params.projectId as string; const { status: authStatus } = useSession(); - const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [refreshing, setRefreshing] = useState(false); - const [provisioning, setProvisioning] = useState(false); - - const fetchProject = async () => { - try { - const res = await fetch(`/api/projects/${projectId}`); - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || "Failed to load project"); - } - const data = await res.json(); - setProject(data.project); - setError(null); - } catch (err) { - setError(err instanceof Error ? err.message : "Unknown error"); - } finally { - setLoading(false); - setRefreshing(false); - } - }; useEffect(() => { - if (authStatus === "authenticated") fetchProject(); - else if (authStatus === "unauthenticated") setLoading(false); - }, [authStatus, projectId]); - - const handleRefresh = () => { - setRefreshing(true); - fetchProject(); - }; - - const handleProvisionWorkspace = async () => { - setProvisioning(true); - try { - const res = await fetch(`/api/projects/${projectId}/workspace`, { method: 'POST' }); - const data = await res.json(); - if (res.ok && data.workspaceUrl) { - toast.success('Workspace provisioned — starting up…'); - await fetchProject(); - } else { - toast.error(data.error || 'Failed to provision workspace'); - } - } catch { - toast.error('An error occurred'); - } finally { - setProvisioning(false); + if (authStatus !== "authenticated") { + if (authStatus === "unauthenticated") setLoading(false); + return; } - }; + fetch(`/api/projects/${projectId}`) + .then((r) => r.json()) + .then((d) => setProject(d.project)) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [authStatus, projectId]); if (loading) { return ( -
- +
+
); } - if (error || !project) { + if (!project) { return ( -
-
-

{error ?? "Project not found"}

-
+
+ Project not found.
); } return ( -
- - {/* ── Header ── */} -
-
-

{project.productName}

- {project.productVision && ( -

{project.productVision}

- )} -
- - {project.status ?? "active"} - - {project.currentPhase && ( - {project.currentPhase} - )} -
-
-
- - {project.theiaWorkspaceUrl ? ( - - ) : ( - - )} -
-
- - {/* ── Agent Panel — Atlas for discovery, Orchestrator once PRD is done ── */} - {(!project.stage || project.stage === 'discovery') ? ( +
+ {(!project.stage || project.stage === "discovery") ? ( )} -
); } diff --git a/app/[workspace]/project/[projectId]/prd/page.tsx b/app/[workspace]/project/[projectId]/prd/page.tsx new file mode 100644 index 0000000..0a5c840 --- /dev/null +++ b/app/[workspace]/project/[projectId]/prd/page.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; + +const PRD_SECTIONS = [ + { id: "executive_summary", label: "Executive Summary" }, + { id: "problem_statement", label: "Problem Statement" }, + { id: "users_personas", label: "Users & Personas" }, + { id: "user_flows", label: "User Flows" }, + { id: "feature_requirements", label: "Feature Requirements" }, + { id: "screen_specs", label: "Screen Specs" }, + { id: "business_model", label: "Business Model" }, + { id: "non_functional", label: "Non-Functional Reqs" }, + { id: "risks", label: "Risks" }, +]; + +interface PRDSection { + id: string; + label: string; + status: "done" | "active" | "pending"; + pct: number; + content?: string; +} + +interface Project { + id: string; + prd?: string; + prdSections?: Record; +} + +export default function PRDPage() { + const params = useParams(); + const projectId = params.projectId as string; + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`/api/projects/${projectId}`) + .then((r) => r.json()) + .then((d) => { + setProject(d.project); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, [projectId]); + + // Build sections with real status if available + const sections: PRDSection[] = PRD_SECTIONS.map((s) => { + const saved = project?.prdSections?.[s.id]; + return { + ...s, + status: (saved?.status as PRDSection["status"]) ?? "pending", + pct: saved?.pct ?? 0, + content: saved?.content, + }; + }); + + // If we have a raw PRD markdown, show that instead of the section list + const hasPRD = Boolean(project?.prd); + + const totalPct = Math.round(sections.reduce((a, s) => a + s.pct, 0) / sections.length); + const doneCount = sections.filter((s) => s.status === "done").length; + + if (loading) { + return ( +
+ Loading… +
+ ); + } + + return ( +
+ {hasPRD ? ( + /* ── Raw PRD view ── */ +
+
+

+ Product Requirements +

+ + PRD approved + +
+
+ {project?.prd} +
+
+ ) : ( + /* ── Section progress view ── */ +
+ {/* Progress bar */} +
+
+ {totalPct}% +
+
+
+
+
+
+ + {doneCount}/{sections.length} approved + +
+ + {/* Sections */} + {sections.map((s, i) => ( +
(e.currentTarget.style.borderColor = "#d0ccc4")} + onMouseLeave={(e) => (e.currentTarget.style.borderColor = "#e8e4dc")} + > + {/* Status icon */} +
+ {s.status === "done" ? "✓" : s.status === "active" ? "◐" : "○"} +
+ + + {s.label} + + + {/* Mini progress bar */} +
+
+
+ + + {s.pct}% + +
+ ))} + + {/* Hint */} +

+ Continue chatting with Atlas to complete your PRD +

+
+ )} +
+ ); +} diff --git a/app/[workspace]/projects/layout.tsx b/app/[workspace]/projects/layout.tsx index cda5391..1c933a9 100644 --- a/app/[workspace]/projects/layout.tsx +++ b/app/[workspace]/projects/layout.tsx @@ -1,9 +1,8 @@ "use client"; -import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail"; -import { RightPanel } from "@/components/layout/right-panel"; +import { VIBNSidebar } from "@/components/layout/vibn-sidebar"; import { ProjectAssociationPrompt } from "@/components/project-association-prompt"; -import { ReactNode, useState } from "react"; +import { ReactNode } from "react"; import { useParams } from "next/navigation"; import { Toaster } from "sonner"; @@ -14,26 +13,16 @@ export default function ProjectsLayout({ }) { const params = useParams(); const workspace = params.workspace as string; - const [activeSection, setActiveSection] = useState("projects"); return ( <> -
- {/* Left Rail - Workspace Navigation */} - - - {/* Main Content Area */} -
+
+ +
{children}
- - {/* Right Panel - AI Chat */} -
- - {/* Project Association Prompt - Detects new workspaces */} - ); diff --git a/app/[workspace]/projects/page.tsx b/app/[workspace]/projects/page.tsx index 1e8a55c..955d1a0 100644 --- a/app/[workspace]/projects/page.tsx +++ b/app/[workspace]/projects/page.tsx @@ -2,38 +2,9 @@ import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - CardDescription, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { - Plus, - Sparkles, - Loader2, - MoreVertical, - Trash2, - GitBranch, - GitCommit, - Rocket, - Terminal, - CheckCircle2, - XCircle, - Clock, -} from "lucide-react"; -import Link from "next/link"; import { useParams } from "next/navigation"; +import Link from "next/link"; import { ProjectCreationModal } from "@/components/project-creation-modal"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, @@ -44,34 +15,16 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { Loader2, Trash2 } from "lucide-react"; import { toast } from "sonner"; -interface ContextSnapshot { - lastCommit?: { sha: string; message: string; author?: string; timestamp?: string }; - currentBranch?: string; - openPRs?: { number: number; title: string }[]; - openIssues?: { number: number; title: string }[]; - lastDeployment?: { status: string; url?: string }; -} - interface ProjectWithStats { id: string; - name: string; - slug: string; productName: string; productVision?: string; - workspacePath?: string; status?: string; - createdAt: string | null; updatedAt: string | null; - giteaRepo?: string; - giteaRepoUrl?: string; - theiaWorkspaceUrl?: string; - contextSnapshot?: ContextSnapshot; - stats: { - sessions: number; - costs: number; - }; + stats: { sessions: number; costs: number }; } function timeAgo(dateStr?: string | null): string { @@ -89,19 +42,27 @@ function timeAgo(dateStr?: string | null): string { return `${Math.floor(days / 30)}mo ago`; } -function DeployDot({ status }: { status?: string }) { - if (!status) return null; - const map: Record = { - finished: "bg-green-500", - in_progress: "bg-blue-500 animate-pulse", - queued: "bg-yellow-400", - failed: "bg-red-500", - }; +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 ( - + + ); +} + +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 ( + + {label} + ); } @@ -112,8 +73,7 @@ export default function ProjectsPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [showCreationModal, setShowCreationModal] = useState(false); + const [showNew, setShowNew] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); @@ -121,29 +81,11 @@ export default function ProjectsPage() { try { setLoading(true); const res = await fetch("/api/projects"); - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || "Failed to fetch projects"); - } + if (!res.ok) throw new Error("Failed to fetch projects"); const data = await res.json(); - const loaded: ProjectWithStats[] = data.projects || []; - setProjects(loaded); - setError(null); - - // Fire-and-forget: prewarm all provisioned IDE workspaces so containers - // are already running by the time the user clicks "Open IDE" - const warmUrls = loaded - .map((p) => p.theiaWorkspaceUrl) - .filter((u): u is string => Boolean(u)); - if (warmUrls.length > 0) { - fetch("/api/projects/prewarm", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ urls: warmUrls }), - }).catch(() => {}); // ignore errors — this is best-effort - } - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Unknown error"); + setProjects(data.projects ?? []); + } catch { + /* silent */ } finally { setLoading(false); } @@ -154,7 +96,7 @@ export default function ProjectsPage() { else if (status === "unauthenticated") setLoading(false); }, [status]); - const handleDeleteProject = async () => { + const handleDelete = async () => { if (!projectToDelete) return; setIsDeleting(true); try { @@ -178,204 +120,201 @@ export default function ProjectsPage() { } }; - if (status === "loading") { - return ( -
- -
- ); - } + const statusSummary = () => { + const live = projects.filter((p) => p.status === "live").length; + const building = projects.filter((p) => p.status === "building").length; + const defining = projects.filter((p) => !p.status || p.status === "defining").length; + const parts = []; + if (defining) parts.push(`${defining} defining`); + if (building) parts.push(`${building} building`); + if (live) parts.push(`${live} live`); + return `${projects.length} total · ${parts.join(" · ")}`; + }; return ( - <> -
- {/* Header */} -
-
-

Projects

-

{session?.user?.email}

-
- +
+ {/* Header */} +
+
+

+ Projects +

+ {!loading && ( +

{statusSummary()}

+ )}
- - {/* States */} - {loading && ( -
- -
- )} - - {error && ( - - -

Error: {error}

-
-
- )} - - {/* Projects Grid */} - {!loading && !error && projects.length > 0 && ( -
- {projects.map((project) => { - const href = `/${workspace}/project/${project.id}/overview`; - const snap = project.contextSnapshot; - const deployStatus = snap?.lastDeployment?.status; - - return ( -
- - - -
-
- {project.productName} - - {timeAgo(project.updatedAt)} - -
-
- - {project.status ?? "active"} - - - e.preventDefault()}> - - - - { - e.preventDefault(); - setProjectToDelete(project); - }} - > - - Delete - - - -
-
-
- - - {/* Vision */} - {project.productVision && ( -

- {project.productVision} -

- )} - - {/* Gitea repo + last commit */} - {project.giteaRepo && ( -
-
- - {project.giteaRepo} - {snap?.currentBranch && ( - - {snap.currentBranch} - - )} -
- {snap?.lastCommit ? ( -
- - {snap.lastCommit.sha.slice(0, 7)} - {snap.lastCommit.message} - {timeAgo(snap.lastCommit.timestamp)} -
- ) : ( -

No commits yet

- )} -
- )} - - {/* Footer row: deploy + stats + IDE */} -
-
- {deployStatus && ( - - - {deployStatus === "finished" ? "Live" : deployStatus} - - )} - {project.stats.sessions} sessions - ${project.stats.costs.toFixed(2)} -
- {project.theiaWorkspaceUrl && ( - e.stopPropagation()} - className="flex items-center gap-1 text-[10px] text-primary hover:underline" - > - - IDE - - )} -
-
-
- -
- ); - })} - - {/* Create card */} - setShowCreationModal(true)} - > - -
- -
-

New Project

-

- Auto-provisions a Gitea repo and workspace -

-
-
-
- )} - - {/* Empty state */} - {!loading && !error && projects.length === 0 && ( - - -
- -
-

No projects yet

-

- Create your first project. Vibn will automatically provision a Gitea repo, - register webhooks, and prepare your IDE workspace. -

- -
-
- )} +
+ {/* Loading */} + {loading && ( +
+ +
+ )} + + {/* Project list */} + {!loading && ( +
+ {projects.map((p, i) => ( +
+ { + e.currentTarget.style.borderColor = "#d0ccc4"; + e.currentTarget.style.boxShadow = "0 2px 8px #1a1a1a0a"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = "#e8e4dc"; + e.currentTarget.style.boxShadow = "0 1px 2px #1a1a1a05"; + }} + > + {/* Project initial */} +
+ + {p.productName[0]?.toUpperCase() ?? "P"} + +
+ + {/* Name + vision */} +
+
+ + {p.productName} + + +
+ {p.productVision && ( + + {p.productVision} + + )} +
+ + {/* Meta */} +
+
+
+ Last active +
+
{timeAgo(p.updatedAt)}
+
+
+
+ Sessions +
+
{p.stats.sessions}
+
+
+ + {/* Delete (hover) */} + + +
+ ))} + + {/* New project card */} + +
+ )} + + {/* Empty state */} + {!loading && projects.length === 0 && ( +
+

+ No projects yet +

+

+ Tell Atlas what you want to build and it will figure out the rest. +

+ +
+ )} + { - setShowCreationModal(open); - if (!open) fetchProjects(); - }} + open={showNew} + onOpenChange={(open) => { setShowNew(open); if (!open) fetchProjects(); }} workspace={workspace} /> @@ -391,20 +330,16 @@ export default function ProjectsPage() { Cancel - {isDeleting ? ( - - ) : ( - - )} + {isDeleting ? : } Delete Project - +
); } diff --git a/app/globals.css b/app/globals.css index c1f503a..d8d990b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,8 +1,14 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=Outfit:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap'); @custom-variant dark (&:is(.dark *)); +@keyframes vibn-enter { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } } +@keyframes vibn-blink { 0%,100%{opacity:.2} 50%{opacity:.8} } +@keyframes vibn-breathe { 0%,100%{transform:scale(1)} 50%{transform:scale(1.15)} } +.vibn-enter { animation: vibn-enter 0.35s ease both; } + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); @@ -45,37 +51,38 @@ :root { --radius: 0.5rem; - --background: oklch(0.985 0 0); - --foreground: oklch(0.09 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.09 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.09 0 0); - --primary: oklch(0.09 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.94 0 0); - --secondary-foreground: oklch(0.09 0 0); - --muted: oklch(0.94 0 0); - --muted-foreground: oklch(0.50 0 0); - --accent: oklch(0.94 0 0); - --accent-foreground: oklch(0.09 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.88 0 0); - --input: oklch(0.88 0 0); - --ring: oklch(0.70 0 0); + /* Stackless warm beige palette */ + --background: #f6f4f0; + --foreground: #1a1a1a; + --card: #ffffff; + --card-foreground: #1a1a1a; + --popover: #ffffff; + --popover-foreground: #1a1a1a; + --primary: #1a1a1a; + --primary-foreground: #ffffff; + --secondary: #f0ece4; + --secondary-foreground: #1a1a1a; + --muted: #f0ece4; + --muted-foreground: #a09a90; + --accent: #eae6de; + --accent-foreground: #1a1a1a; + --destructive: #d32f2f; + --border: #e8e4dc; + --input: #e0dcd4; + --ring: #d0ccc4; --chart-1: oklch(0.70 0.15 60); --chart-2: oklch(0.70 0.12 210); --chart-3: oklch(0.55 0.10 220); --chart-4: oklch(0.40 0.08 230); --chart-5: oklch(0.75 0.15 70); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.09 0 0); - --sidebar-primary: oklch(0.09 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.94 0 0); - --sidebar-accent-foreground: oklch(0.09 0 0); - --sidebar-border: oklch(0.88 0 0); - --sidebar-ring: oklch(0.70 0 0); + --sidebar: #ffffff; + --sidebar-foreground: #1a1a1a; + --sidebar-primary: #1a1a1a; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #f6f4f0; + --sidebar-accent-foreground: #1a1a1a; + --sidebar-border: #e8e4dc; + --sidebar-ring: #d0ccc4; } .dark { diff --git a/components/layout/project-shell.tsx b/components/layout/project-shell.tsx new file mode 100644 index 0000000..7da00fe --- /dev/null +++ b/components/layout/project-shell.tsx @@ -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 ( + + {label} + + ); +} + +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 ( + <> +
+ {/* Sidebar */} + + + {/* Main content */} +
+ {/* Project header */} +
+
+
+ + {projectName[0]?.toUpperCase() ?? "P"} + +
+
+
+

+ {projectName} +

+ +
+
+
+
+ {progress}% +
+
+ + {/* Tab bar */} +
+ {TABS.map((t) => ( + + {t.label} + + ))} +
+ + {/* Page content */} +
+ {children} +
+
+
+ + + ); +} diff --git a/components/layout/vibn-sidebar.tsx b/components/layout/vibn-sidebar.tsx new file mode 100644 index 0000000..2b20687 --- /dev/null +++ b/components/layout/vibn-sidebar.tsx @@ -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 ( + + ); +} + +export function VIBNSidebar({ workspace }: VIBNSidebarProps) { + const pathname = usePathname(); + const { data: session } = useSession(); + const [projects, setProjects] = useState([]); + + 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 ( + + ); +}