Skip to content

Dashboard Setup — Developer Guide

Feature: F0.2 — Next.js Dashboard Scaffold
Status: Completed
App: apps/dashboard (Next.js 15, App Router)


Overview

The dashboard is a Next.js 15 App Router application. It uses:

  • Tailwind CSS 4 with custom CSS variables for theming
  • shadcn/ui (New York style) for all UI primitives
  • next-themes for dark/light mode persistence
  • Geist Sans + Geist Mono for typography (matches Vercel/Linear aesthetic)

The layout uses a two-panel structure: a fixed sidebar on the left and a scrollable main content area on the right. On mobile, the sidebar collapses and is accessible via a Sheet drawer triggered by a hamburger button in the header.


Project Structure

apps/dashboard/
├── src/
│   ├── app/
│   │   ├── globals.css              # Tailwind + all CSS theme variables
│   │   ├── layout.tsx               # Root layout: fonts, ThemeProvider
│   │   ├── page.tsx                 # Redirects / → /dashboard
│   │   └── (panel)/
│   │       ├── layout.tsx           # Sidebar + Header shell
│   │       └── dashboard/
│   │           └── page.tsx         # Dashboard home page
│   ├── components/
│   │   ├── ui/                      # shadcn/ui primitives (DO NOT edit manually)
│   │   │   ├── button.tsx
│   │   │   ├── card.tsx
│   │   │   ├── separator.tsx
│   │   │   ├── sheet.tsx
│   │   │   └── skeleton.tsx
│   │   ├── layout/
│   │   │   ├── Sidebar.tsx          # Desktop sidebar navigation
│   │   │   ├── Header.tsx           # Top bar (mobile trigger + theme toggle)
│   │   │   ├── MobileSidebar.tsx    # Sheet-wrapped Sidebar for mobile
│   │   │   └── ThemeToggle.tsx      # Sun/Moon dark mode button
│   │   ├── dashboard/
│   │   │   └── StatCard.tsx         # Reusable metric card with loading state
│   │   └── providers/
│   │       └── ThemeProvider.tsx    # next-themes client wrapper
│   └── lib/
│       └── utils.ts                 # cn() helper (clsx + tailwind-merge)
├── components.json                  # shadcn/ui configuration
├── next.config.ts                   # transpilePackages: @vexlyx/shared
├── postcss.config.mjs               # @tailwindcss/postcss plugin
└── package.json

Theme System

All colors are defined as CSS variables in globals.css. Never hardcode color values — always use the semantic tokens.

Available Tokens

TokenUse Case
bg-backgroundPage background
bg-cardCard / panel surfaces
text-foregroundPrimary text
text-muted-foregroundSecondary / label text
bg-primary / text-primaryBrand actions, active states
bg-destructiveDelete / danger actions
border-borderBorders and dividers
bg-sidebar / text-sidebar-foregroundSidebar-specific surfaces
bg-sidebar-accentSidebar hover/active states

Dark mode is handled automatically via the .dark class on <html> (set by next-themes).


Adding a New Page

  1. Create the page file under the (panel) route group:

    src/app/(panel)/your-feature/page.tsx
  2. Export a default React component:

    tsx
    export default function YourFeaturePage() {
      return <div>...</div>;
    }
  3. Add a navigation link in Sidebar.tsx:

    tsx
    const navItems = [
      // ...existing items
      { label: "Your Feature", href: "/your-feature", icon: YourIcon },
    ];

The page will automatically inherit the sidebar + header layout from (panel)/layout.tsx.


Adding shadcn/ui Components

The project uses shadcn/ui components via the radix-ui unified package (shadcn v4+). To add a new component:

bash
cd apps/dashboard
npx shadcn@latest add [component-name]

Import from @/components/ui/[component-name]. Do not edit files in src/components/ui/ — they are regenerated by shadcn.

Currently installed components: button, card, separator, sheet, skeleton.


Dark Mode

Dark mode uses next-themes with the class strategy. The ThemeProvider wraps the entire app in layout.tsx. suppressHydrationWarning is set on <html> to prevent hydration mismatches.

The ThemeToggle button (Sun/Moon icons) is rendered in the Header and persists the user's choice across sessions via localStorage.


StatCard Component

StatCard is a reusable metric display for the dashboard overview. It supports a loading state that renders skeleton placeholders instead of values.

tsx
import { StatCard } from "@/components/dashboard/StatCard";
import { Server } from "lucide-react";

// Normal state
<StatCard title="Active Projects" value="12" description="+2 this week" icon={Server} />

// Loading state (show skeletons while data fetches)
<StatCard title="Active Projects" icon={Server} isLoading />

Mobile Behavior

On screens narrower than md (768px):

  • The desktop sidebar (<aside className="hidden md:flex">) is hidden
  • The Header shows a hamburger button (MobileSidebar)
  • Clicking it opens the Sidebar inside a Sheet (slide-in drawer from the left)
  • Clicking any navigation link closes the sheet automatically

Running Locally

bash
# From monorepo root
pnpm dev          # Starts dashboard on http://localhost:3000

# From apps/dashboard only
pnpm dev          # Same, but without other packages
pnpm typecheck    # TypeScript strict check
pnpm lint         # ESLint check
pnpm build        # Production build (must pass before any PR)

Released under the MIT license.