diff --git a/CLAUDE.md b/CLAUDE.md index e41422b..c65eca3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,10 @@ npm run test:unit # Vitest npm run test:e2e # Playwright npm run test # All tests (Vitest + Playwright) +npm run analyse # Bundle analyser UI (why is X bundled? bloat? splits?) + +fuser -k 3000/tcp 2>/dev/null; rm -f .next/dev/lock # Kill dev server + vercel list # Recent deployments and status vercel env ls # Check env vars are configured vercel whoami # Verify CLI is authenticated diff --git a/app/(root)/(no-right-sidebar)/layout.tsx b/app/(root)/(no-right-sidebar)/layout.tsx deleted file mode 100644 index 34cae74..0000000 --- a/app/(root)/(no-right-sidebar)/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// Full-width layout without right sidebar -// Layout: [LeftSidebar (sticky)] [Main Content] -const NoRightSidebarLayout = ({ children }: { children: React.ReactNode }) => { - return
{children}
; -}; - -export default NoRightSidebarLayout; diff --git a/app/(root)/(with-right-sidebar)/layout.tsx b/app/(root)/(with-right-sidebar)/layout.tsx deleted file mode 100644 index ad0517f..0000000 --- a/app/(root)/(with-right-sidebar)/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -// TODO: Add for tag widgets, hot questions, etc. -// Layout: [LeftSidebar (sticky)] [Main Content] [RightSidebar] -const WithRightSidebarLayout = ({ - children, -}: { - children: React.ReactNode; -}) => { - return
{children}
; -}; - -export default WithRightSidebarLayout; diff --git a/app/(root)/(with-right-sidebar)/ask-question/page.tsx b/app/(root)/ask-question/page.tsx similarity index 100% rename from app/(root)/(with-right-sidebar)/ask-question/page.tsx rename to app/(root)/ask-question/page.tsx diff --git a/app/(root)/(with-right-sidebar)/collections/page.tsx b/app/(root)/collections/page.tsx similarity index 100% rename from app/(root)/(with-right-sidebar)/collections/page.tsx rename to app/(root)/collections/page.tsx diff --git a/app/(root)/(no-right-sidebar)/community/page.tsx b/app/(root)/community/page.tsx similarity index 100% rename from app/(root)/(no-right-sidebar)/community/page.tsx rename to app/(root)/community/page.tsx diff --git a/app/(root)/(no-right-sidebar)/jobs/page.tsx b/app/(root)/jobs/page.tsx similarity index 100% rename from app/(root)/(no-right-sidebar)/jobs/page.tsx rename to app/(root)/jobs/page.tsx diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx index b4916a3..f812917 100644 --- a/app/(root)/layout.tsx +++ b/app/(root)/layout.tsx @@ -1,16 +1,39 @@ -import { LeftSidebar } from "@/components/navigation/left-sidebar"; -import { Navbar } from "@/components/navigation/navbar"; -import { SidebarProvider } from "@/components/sidebar-provider"; +import { cookies } from "next/headers"; +import { AppSidebar } from "@/components/app-sidebar"; +import { ContentTopBar } from "@/components/navigation/content-top-bar"; +import { MobileHeader } from "@/components/navigation/mobile-header"; +import { SidebarProvider } from "@/components/ui/sidebar"; +import { + CONTENT_HORIZONTAL_PADDING, + cn, + MOBILE_HEADER_TOP_OFFSET, +} from "@/lib/utils"; + +const RootLayout = async ({ children }: { children: React.ReactNode }) => { + const cookieStore = await cookies(); + const defaultOpen = cookieStore.get("sidebar_state")?.value !== "false"; -const RootLayout = ({ children }: { children: React.ReactNode }) => { return ( - -
- -
- -
{children}
-
+ + {/* Mobile-only fixed header */} + + + {/* Full-height sidebar */} + + + {/* Content area */} +
+ {/* Desktop-only top bar */} + +
+ {children} +
); diff --git a/app/(root)/(with-right-sidebar)/page.tsx b/app/(root)/page.tsx similarity index 100% rename from app/(root)/(with-right-sidebar)/page.tsx rename to app/(root)/page.tsx diff --git a/app/(root)/(no-right-sidebar)/profile/page.tsx b/app/(root)/profile/page.tsx similarity index 100% rename from app/(root)/(no-right-sidebar)/profile/page.tsx rename to app/(root)/profile/page.tsx diff --git a/app/(root)/(no-right-sidebar)/tags/page.tsx b/app/(root)/tags/page.tsx similarity index 100% rename from app/(root)/(no-right-sidebar)/tags/page.tsx rename to app/(root)/tags/page.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 104addc..53b9a4c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -30,7 +30,7 @@ export default function RootLayout({ diff --git a/components.json b/components.json index ce09c89..b27c50f 100644 --- a/components.json +++ b/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "", - "css": "globals.css", + "css": "app/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..c952b51 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { SignedIn, UserButton } from "@clerk/nextjs"; +import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ThemeLogo } from "@/components/navigation/full-logo"; +import { NAV_LINKS } from "@/components/navigation/nav-links.constants"; +import { SidebarToggleButton } from "@/components/navigation/sidebar-toggle-button"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar"; +import { + cn, + getNavIconClasses, + isRouteActive, + NAV_LINK_ACTIVE_CLASSES, + NAV_LINK_INACTIVE_CLASSES, +} from "@/lib/utils"; + +export function AppSidebar() { + const pathname = usePathname(); + + return ( + + {/* Header: Logo - h-14 matches ContentTopBar height */} + + + {/* Icon-only when collapsed */} + {/* biome-ignore lint/a11y/useAltText: Decorative logo, aria-label on parent link */} + {/* biome-ignore lint/performance/noImgElement: SVG logo doesn't benefit from next/image optimisation */} + + {/* Full logo when expanded */} + + + + + {/* Main Navigation */} + + + + + {NAV_LINKS.map((link) => { + const isActive = isRouteActive(pathname, link.route); + + return ( + + + + + {link.label} + + + + ); + })} + + + + + + {/* Footer: UserButton + Toggle */} + +
+ + + + +
+
+ + {/* Edge-click rail to toggle */} + +
+ ); +} diff --git a/components/clerk-provider.tsx b/components/clerk-provider.tsx index 3765916..8efbe20 100644 --- a/components/clerk-provider.tsx +++ b/components/clerk-provider.tsx @@ -19,7 +19,7 @@ export function ClerkProvider({ }, elements: { formButtonPrimary: - "bg-[image:var(--gradient-primary)] text-white hover:opacity-90", + "bg-(image:--gradient-primary) text-white hover:opacity-90", footer: "bg-card", avatarBox: "size-8", }, diff --git a/components/navigation/content-top-bar.tsx b/components/navigation/content-top-bar.tsx new file mode 100644 index 0000000..e05e16c --- /dev/null +++ b/components/navigation/content-top-bar.tsx @@ -0,0 +1,34 @@ +import { SignedOut, SignInButton, SignUpButton } from "@clerk/nextjs"; +import { ThemeToggle } from "@/components/navigation/theme-toggle"; +import { Button } from "@/components/ui/button"; +import { CONTENT_HORIZONTAL_PADDING, cn, HEADER_HEIGHT } from "@/lib/utils"; + +export function ContentTopBar() { + return ( + + ); +} diff --git a/components/navigation/full-logo.tsx b/components/navigation/full-logo.tsx index 41c5735..0a91628 100644 --- a/components/navigation/full-logo.tsx +++ b/components/navigation/full-logo.tsx @@ -17,7 +17,7 @@ export function ThemeLogo({ className }: ThemeLogoProps) {