From afa98282ff9c355f5fb86c99c260697cd00e98c9 Mon Sep 17 00:00:00 2001 From: "Leivur R. Djurhuus" Date: Sat, 28 Feb 2026 21:09:07 -0600 Subject: [PATCH] Add app shell layout with sidebar, topbar, breadcrumbs, theme toggle - Collapsible sidebar (256px / 64px) with nav items and tooltips - Topbar with breadcrumbs and notification bell placeholder - Theme toggle (light/dark/system) via dropdown - Zustand store for sidebar collapsed state - Placeholder pages for all main routes (dashboard, projects, my-work, notifications, settings) - Authenticated (app) layout group wrapping all protected routes Co-Authored-By: Claude Opus 4.6 --- src/app/(app)/dashboard/page.tsx | 10 ++ src/app/(app)/layout.tsx | 14 +++ src/app/(app)/my-work/page.tsx | 10 ++ src/app/(app)/notifications/page.tsx | 10 ++ src/app/(app)/projects/page.tsx | 10 ++ src/app/(app)/settings/page.tsx | 10 ++ src/components/layout/breadcrumbs.tsx | 61 +++++++++++ src/components/layout/sidebar.tsx | 137 +++++++++++++++++++++++++ src/components/layout/theme-toggle.tsx | 41 ++++++++ src/components/layout/topbar.tsx | 29 ++++++ src/stores/sidebar-store.ts | 13 +++ 11 files changed, 345 insertions(+) create mode 100644 src/app/(app)/dashboard/page.tsx create mode 100644 src/app/(app)/layout.tsx create mode 100644 src/app/(app)/my-work/page.tsx create mode 100644 src/app/(app)/notifications/page.tsx create mode 100644 src/app/(app)/projects/page.tsx create mode 100644 src/app/(app)/settings/page.tsx create mode 100644 src/components/layout/breadcrumbs.tsx create mode 100644 src/components/layout/sidebar.tsx create mode 100644 src/components/layout/theme-toggle.tsx create mode 100644 src/components/layout/topbar.tsx create mode 100644 src/stores/sidebar-store.ts diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx new file mode 100644 index 0000000..e008e63 --- /dev/null +++ b/src/app/(app)/dashboard/page.tsx @@ -0,0 +1,10 @@ +export default function DashboardPage() { + return ( +
+

Dashboard

+

+ Production pipeline overview — KPIs and charts coming in Phase 3. +

+
+ ); +} diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..46eb3a9 --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,14 @@ +import { Sidebar } from "@/components/layout/sidebar"; +import { Topbar } from "@/components/layout/topbar"; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ +
{children}
+
+
+ ); +} diff --git a/src/app/(app)/my-work/page.tsx b/src/app/(app)/my-work/page.tsx new file mode 100644 index 0000000..8ed9c48 --- /dev/null +++ b/src/app/(app)/my-work/page.tsx @@ -0,0 +1,10 @@ +export default function MyWorkPage() { + return ( +
+

My Work

+

+ Your assigned stages and tasks — coming in Phase 2. +

+
+ ); +} diff --git a/src/app/(app)/notifications/page.tsx b/src/app/(app)/notifications/page.tsx new file mode 100644 index 0000000..2dab215 --- /dev/null +++ b/src/app/(app)/notifications/page.tsx @@ -0,0 +1,10 @@ +export default function NotificationsPage() { + return ( +
+

Notifications

+

+ Notification history — coming in Phase 3. +

+
+ ); +} diff --git a/src/app/(app)/projects/page.tsx b/src/app/(app)/projects/page.tsx new file mode 100644 index 0000000..0cf445d --- /dev/null +++ b/src/app/(app)/projects/page.tsx @@ -0,0 +1,10 @@ +export default function ProjectsPage() { + return ( +
+

Projects

+

+ Project list and management — CRUD coming next. +

+
+ ); +} diff --git a/src/app/(app)/settings/page.tsx b/src/app/(app)/settings/page.tsx new file mode 100644 index 0000000..e5464f5 --- /dev/null +++ b/src/app/(app)/settings/page.tsx @@ -0,0 +1,10 @@ +export default function SettingsPage() { + return ( +
+

Settings

+

+ User and organization settings — coming in Phase 4. +

+
+ ); +} diff --git a/src/components/layout/breadcrumbs.tsx b/src/components/layout/breadcrumbs.tsx new file mode 100644 index 0000000..7004c8b --- /dev/null +++ b/src/components/layout/breadcrumbs.tsx @@ -0,0 +1,61 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ChevronRight, Home } from "lucide-react"; +import { Fragment } from "react"; + +const LABEL_MAP: Record = { + dashboard: "Dashboard", + projects: "Projects", + "my-work": "My Work", + notifications: "Notifications", + settings: "Settings", + table: "Table View", + board: "Board View", + timeline: "Timeline View", + deliverables: "Deliverables", +}; + +export function Breadcrumbs() { + const pathname = usePathname(); + const segments = pathname.split("/").filter(Boolean); + + if (segments.length === 0) return null; + + const crumbs = segments.map((segment, index) => { + const href = "/" + segments.slice(0, index + 1).join("/"); + const label = LABEL_MAP[segment] || decodeURIComponent(segment); + const isLast = index === segments.length - 1; + + return { href, label, isLast }; + }); + + return ( + + ); +} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx new file mode 100644 index 0000000..340beab --- /dev/null +++ b/src/components/layout/sidebar.tsx @@ -0,0 +1,137 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + LayoutDashboard, + FolderKanban, + ClipboardList, + Bell, + Settings, + PanelLeftClose, + PanelLeft, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { useSidebarStore } from "@/stores/sidebar-store"; + +const navItems = [ + { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, + { href: "/projects", label: "Projects", icon: FolderKanban }, + { href: "/my-work", label: "My Work", icon: ClipboardList }, + { href: "/notifications", label: "Notifications", icon: Bell }, +]; + +const bottomItems = [ + { href: "/settings", label: "Settings", icon: Settings }, +]; + +export function Sidebar() { + const pathname = usePathname(); + const { isCollapsed, toggle } = useSidebarStore(); + + return ( + + ); +} diff --git a/src/components/layout/theme-toggle.tsx b/src/components/layout/theme-toggle.tsx new file mode 100644 index 0000000..98ee87b --- /dev/null +++ b/src/components/layout/theme-toggle.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Moon, Sun, Monitor } from "lucide-react"; +import { useTheme } from "next-themes"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ThemeToggle() { + const { setTheme, theme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + + Light + + setTheme("dark")}> + + Dark + + setTheme("system")}> + + System + + + + ); +} diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx new file mode 100644 index 0000000..a77a119 --- /dev/null +++ b/src/components/layout/topbar.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Bell } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/components/layout/theme-toggle"; +import { Breadcrumbs } from "@/components/layout/breadcrumbs"; +import { Badge } from "@/components/ui/badge"; + +export function Topbar() { + return ( +
+ + +
+ + +
+
+ ); +} diff --git a/src/stores/sidebar-store.ts b/src/stores/sidebar-store.ts new file mode 100644 index 0000000..9fa0bbc --- /dev/null +++ b/src/stores/sidebar-store.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface SidebarState { + isCollapsed: boolean; + toggle: () => void; + setCollapsed: (collapsed: boolean) => void; +} + +export const useSidebarStore = create((set) => ({ + isCollapsed: false, + toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })), + setCollapsed: (collapsed) => set({ isCollapsed: collapsed }), +}));