Technical Documentation
admin.ui
A production-grade admin UI prototype — built to demonstrate real-world architecture decisions, component design patterns, and engineering tradeoffs in a modern Next.js stack.
01
Technology stack
Every dependency is intentional — chosen for type safety, composability, or production credibility.
Framework
Next.js 16 (App Router)
Server Components by default. Route groups for layout isolation. No Pages Router.
Styling
Tailwind v4 + @theme inline
CSS-first config. Design tokens defined as raw CSS vars, mapped via @theme inline so utilities respond to runtime theme changes.
Component library
shadcn/ui (base-ui v4)
Headless primitives via @base-ui/react. No asChild on DropdownMenuTrigger — use onClick navigation or direct <Link> instead.
Charts
Recharts
Revenue area chart. Grid, tick, and activeDot colors use var(--color-*) refs so they respond to dark/light mode automatically.
Forms & validation
react-hook-form + Zod v4
zodResolver wires the schema to RHF. Zod v4 drops invalid_type_error — use per-rule messages instead.
Theme switching
next-themes
Writes class="dark" / class="light" to <html>. ThemeProvider wraps the entire tree in root layout.
Animations
tw-animate-css
Provides animate-in / fade-in / slide-in-from-* utilities used for page transitions and dropdown entrances.
Toast notifications
sonner
Mounted once in root layout via <Toaster />. Used for success/info/error feedback across all actions.
02
Architecture & route groups
Route groups isolate layout trees. The dashboard shell never touches auth pages.
File structure
src/app/ ├── layout.tsx ← Root: providers only (ThemeProvider, Toaster) ├── globals.css ← Design tokens + Tailwind v4 config │ ├── (dashboard)/ ← Route group — owns the shell │ ├── layout.tsx ← AppSidebar + AppHeader + PageTransition │ ├── page.tsx ← / (Dashboard) │ ├── loading.tsx ← Skeleton fallback for all dashboard routes │ ├── error.tsx ← Error boundary for all dashboard routes │ ├── invoices/ │ │ ├── page.tsx ← /invoices │ │ ├── new/page.tsx ← /invoices/new │ │ └── [id]/ │ │ ├── page.tsx ← /invoices/:id │ │ └── edit/page.tsx ← /invoices/:id/edit │ ├── customers/page.tsx ← /customers │ ├── billing/page.tsx ← /billing │ ├── settings/page.tsx ← /settings │ └── profile/page.tsx ← /profile │ ├── (auth)/ ← Route group — no shell │ ├── layout.tsx ← Passthrough (required to prevent layout leak) │ ├── signin/page.tsx ← /signin │ └── documentation/ │ ├── page.tsx ← /documentation (this page) │ ├── collapsible-code.tsx │ └── docs-nav.tsx │ ├── not-found.tsx ← 404 (custom) └── global-error.tsx ← Root-level error (has own html/body)
Dashboard layout
ServerOwns the sidebar + header shell. Every dashboard route inherits this layout without re-mounting the shell on navigation.
Auth layout passthrough
ServerWithout an explicit layout.tsx in (auth), Next.js walks up and applies (dashboard)/layout.tsx — rendering the sidebar on the login page. This file prevents that.
03
Design system
Two-layer token system: raw CSS vars per theme, then @theme inline maps them to Tailwind utilities.
Token architecture
Theme switching
Token reference
Token Tailwind utility Dark value Light value ───────────────────────────────────────────────────────────────────── --color-bg bg-bg #060912 #f8fafc --color-bg-soft bg-bg-soft #0a0f1e #f1f5f9 --color-surface bg-surface #0d1322 #ffffff --color-surface-hi bg-surface-hi #111827 #f1f5f9 --color-border border-border rgba(255,255,255,.08) rgba(0,0,0,.08) --color-border-hi border-border-hi rgba(96,165,250,.25) rgba(59,130,246,.35) --color-fg text-fg #e8edf5 #0f172a --color-fg-soft text-fg-soft #94a3b8 #475569 --color-fg-muted text-fg-muted #475569 #94a3b8 --color-accent text-accent / bg-accent #3b82f6 #3b82f6 (same) --color-accent-hi text-accent-hi #60a5fa #2563eb --color-success text-success #34d399 #10b981 --color-danger text-danger #f87171 #ef4444 --color-warning text-warning #fbbf24 #f59e0b
Status badge variants
All variants are pure CSS — no hardcoded hex. They respond to dark/light mode via the token system.
04
Pages & routes
All dashboard routes are protected by the shell layout. Auth routes are public and layoutless.
Dashboard routes
Client/KPI cards, revenue area chart (12-month), recent invoices table/invoicesInvoice table — sort, filter by status, bulk select, pagination/invoices/newNew invoice form with line-items editor and live summary/invoices/[id]Invoice detail — read-only view with status timeline/invoices/[id]/editEdit invoice — same form pre-filled from mock data/customersCustomer table with sort, filter, avatar initials/billingBilling overview — plan card, usage meters, payment history/settingsTabbed settings: General, Team, Notifications, Danger Zone/profileProfile tabs: Personal Info, Security (2FA), Activity logAuth & public routes
Server/signinSign-in page — email/password form + mock Google OAuth button/documentationThis page — public, no auth guard, Server Component/*not-found.tsx — custom 404 with Go Back + Dashboard actions05
Custom UI components
Three custom form primitives that fill gaps in the shadcn/base-ui component set.
ComboboxField
Searchable select built from Popover (@base-ui/react) + Command (cmdk). Width matches trigger via var(--anchor-width). Integrates with RHF via Controller. Used for: Customer select in invoice form, Timezone/Currency/Role in settings.
value: stringonChange: (v: string) => voidoptions: {value, label}[]placeholder?: stringCurrencyInput
A dual-mode input: shows formatted $1,234.56 when blurred, switches to raw numeric string on focus for easy editing. Strips non-numeric characters on change. Uses inputMode="decimal" for mobile keyboards.
value: numberonChange: (n: number) => voidDatePickerField
Calendar picker built on Popover + react-day-picker. Stores and emits values as YYYY-MM-DD strings using the local date constructor — avoids all UTC/timezone offset bugs that plague new Date(isoString).
value: stringonChange: (v: string) => voidplaceholder?: string06
Invoice form & validation
RHF manages form state. Zod v4 validates fields. superRefine handles cross-field rules.
Schema definition (Zod v4)
Each field has per-rule messages. Zod v4 removed invalid_type_error — messages live on individual refinements (min, max). superRefine runs after all fields pass and handles cross-field logic.
Form setup
zodResolver bridges the schema to react-hook-form. defaultValues pre-fill edit mode from an existing invoice, or generate fresh defaults (invoice number, today's date, +14 days due) for new mode.
Custom fields via Controller
ComboboxField, CurrencyInput, and DatePickerField don't use a native <input> ref, so they integrate via react-hook-form's Controller component instead of register().
Error display pattern
Each field shows errors?.field?.message below the input with an AlertCircle icon. Warning state (non-blocking, e.g. tax > 30%) is shown in yellow text-warning without blocking submission.
Line items editor
Dynamic array managed with RHF useFieldArray. Each row has description, quantity, and unit price. The summary panel below reacts in real time — computing subtotal, applying tax rate and discount, then showing the final total. All math runs on the client with watched field values.
07
Layout system
Three client components build the persistent shell. Each has a single focused responsibility.
AppSidebar
Collapsible sidebar (w-56 / w-16). Hidden on mobile — the header provides a slide-in drawer instead. Nav groups (Main / Finance / Settings) with labeled sections that collapse to icons-only mode. Uses style={{ zIndex: 10 }} — not Tailwind z-10 — because globals.css gives z-index: 1 to all aside elements outside any CSS layer.
AppHeader
Sticky top bar with three zones: mobile menu button (hamburger, sm:hidden) → breadcrumb (auto-generated from pathname) → right actions (notifications bell + user menu). Notification dropdown tracks unread count with per-item mark-as-read and mark-all-read. User menu logout button calls router.push('/signin').
PageTransition
Wraps every page inside <main>. Uses key={pathname} to force React to remount the div on each navigation, re-triggering the CSS entry animation. Intentionally has no h-full — that would collapse main's bottom padding on scrollable pages.
08
Mobile & responsiveness
Mobile-first breakpoints. Tables become card lists. Sidebar becomes a slide-in drawer.
Table → card pattern
Both the invoice table and customer table use the same pattern: a standard <table> with hidden sm:table and a card list with sm:hidden. Zero JS — pure CSS breakpoints.
Responsive rules
Overflow prevention checklist
z-index caveat
globals.css sets z-index: 1 on main, header, footer, section, asideoutside any CSS layer. This silently overrides Tailwind's z-N utilities on those elements. Fix: always use style={{ zIndex: N }} for sticky navs, dropdowns, and sidebars — never Tailwind z-classes on these element types.
09
Error handling
Four layers of error handling — from route-level boundaries to the root fallback.
Route segment error boundary — error.tsx
Catches runtime errors thrown inside the (dashboard) route segment. Provides Try again (calls reset() to re-render), Go Back (router.back()), and Dashboard (href='/') actions. Logs error to console for monitoring. Shows digest ID when available.
Loading skeleton — loading.tsx
Next.js shows this immediately while a route segment suspends during navigation. Renders a grid of skeleton cards that match the dashboard layout shape, reducing perceived load time.
Custom 404 — not-found.tsx
Rendered when notFound() is called or a route has no match. Has a Go Back button and a link back to the dashboard so users are never stranded.
Root-level error — global-error.tsx
Catches errors that escape all route boundaries — including errors inside root layout.tsx. Must include its own <html> and <body> tags since the normal layout is unavailable. Provides a simple Reload button.
Error layer summary
| File | Scope | Type |
|---|---|---|
| (dashboard)/error.tsx | All dashboard routes | Client error boundary |
| (dashboard)/loading.tsx | All dashboard routes | Suspense skeleton |
| not-found.tsx | All routes | Server 404 |
| global-error.tsx | Root layout + above | Client root fallback |