Layout
The root layout, app/layout.tsx, is the single place for app-wide providers and global infrastructure.
Do not remove any existing entries without a reason. Current infrastructure in the layout:
| Entry |
Purpose |
ThemeProvider |
Light/dark mode via next-themes |
Toaster |
Global toast notifications via Sonner |
ChunkLoadErrorHandler |
Required — prevents known ChunkLoadError race condition bug |
Typography
| Role |
Font |
Tailwind Class |
Default Usage |
| Body |
DM Sans |
font-sans |
All body text, labels, descriptions |
| Display |
Plus Jakarta Sans |
font-display |
Page titles, hero headings, section headers |
| Mono |
JetBrains Mono |
font-mono |
Code snippets, numeric data, IDs, timestamps |
Size hierarchy: Use Tailwind's scale. Headings: text-4xl→text-3xl→text-2xl→text-xl. Body: text-base→text-sm. Captions: text-xs.
Always use tracking-tight on large headings (text-2xl and above).
Color System (Design Tokens)
All colors use CSS variables — never hardcode color values.
| Token |
Purpose |
background / foreground |
Page-level bg and text |
card / card-foreground |
Card surfaces |
primary / primary-foreground |
Brand buttons, links, accents |
secondary / secondary-foreground |
Secondary buttons, subtle highlights |
muted / muted-foreground |
Disabled states, helper text, subtle backgrounds |
accent / accent-foreground |
Hover states, active nav items |
destructive / destructive-foreground |
Errors, delete actions |
border |
Borders and dividers |
input |
Form input borders |
ring |
Focus rings |
Usage: bg-primary, text-muted-foreground, border-border, etc.
Spacing Scale
Based on an 8px grid. Use these CSS variables or Tailwind equivalents:
| Token |
Value |
Tailwind |
--spacing-xs |
4px |
p-1, gap-1 |
--spacing-sm |
8px |
p-2, gap-2 |
--spacing-md |
16px |
p-4, gap-4 |
--spacing-lg |
24px |
p-6, gap-6 |
--spacing-xl |
32px |
p-8, gap-8 |
--spacing-2xl |
48px |
p-12, gap-12 |
--spacing-3xl |
64px |
p-16, gap-16 |
Vary spacing rhythm — don't use the same gap everywhere. Hero → large gap → content → medium gap → footer.
Shadow Scale
| Token |
Default Usage |
--shadow-sm |
Subtle card lift, input focus |
--shadow-md |
Cards, dropdowns, popovers |
--shadow-lg |
Modals, elevated panels |
These are CSS variables only — use them directly in inline styles or custom CSS as var(--shadow-sm) etc. They are not mapped to Tailwind's shadow-* utilities.
Border Radius
| Token |
Value |
Default Usage |
--radius |
0.625rem |
Default (buttons, inputs, cards) |
--radius-sm |
calc(var(--radius) - 4px) |
Small elements (badges, chips) |
--radius-lg |
calc(var(--radius) + 4px) |
Large containers, hero cards |
--radius-full |
9999px |
Avatars, pills, circular buttons |
Animation Timing
| Token |
Value |
Tailwind Class |
Default Usage |
--duration-fast |
150ms |
duration-fast |
Hover states, toggles |
--duration-normal |
250ms |
duration-normal |
Page transitions, reveals |
--duration-slow |
350ms |
duration-slow |
Complex animations, modals |
Layout Components
The live app shell is @/components/app-shell (sidebar + top bar + main
content, wired through app/(app)/layout.tsx); screens render their own
headers inline. Login/signup use a centered card built directly in their
route files. The generic components/layouts/* wrappers documented here
previously were never imported and have been removed — reintroduce shared
layout primitives only when a second consumer exists.
Animation Components — @/components/ui/animate
| Component |
Default Usage |
Key Props |
FadeIn |
Reveal content on scroll |
delay, duration |
ScaleIn |
Pop-in effect |
delay |
SlideIn |
Directional entrance |
from (top|bottom|left|right), delay |
Stagger + StaggerItem |
Sequential reveal for lists/grids |
staggerDelay |
HoverLift |
Lift effect on hover (cards, links) |
— |
PressScale |
Press-down feedback for buttons |
— |
SkeletonPulse |
Loading placeholder |
className (set width/height) |
Pattern: Wrap page sections in FadeIn, list items in Stagger/StaggerItem, interactive cards in HoverLift.
UI Components — @/components/ui/
Core
| Component |
Import |
Key Props |
Button |
@/components/ui/button |
variant (default|secondary|outline|ghost|destructive|link|glass-dark|glass-light), size (default|xs|sm|lg|icon|icon-sm), loading (boolean). glass-dark: for dark/vivid backgrounds. glass-light: for light/pale backgrounds. Link: focus uses underline, not ring. |
Badge |
@/components/ui/badge |
variant (default|secondary|outline|destructive) |
Card |
@/components/ui/card |
variant (default|interactive|glass-dark|glass-dark-interactive|glass-light|glass-light-interactive|ghost). Composed: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter. Interactive: always wrap in <a> or <button> for keyboard access. |
Separator |
@/components/ui/separator |
orientation (horizontal|vertical) |
Forms
| Component |
Import |
Input |
@/components/ui/input |
Textarea |
@/components/ui/textarea |
Label |
@/components/ui/label |
Select |
@/components/ui/select |
Checkbox |
@/components/ui/checkbox |
RadioGroup |
@/components/ui/radio-group |
Switch |
@/components/ui/switch |
Slider |
@/components/ui/slider |
Calendar |
@/components/ui/calendar |
DateRangePicker |
@/components/ui/date-range-picker |
InputOTP |
@/components/ui/input-otp |
Form |
@/components/ui/form (react-hook-form integration) |
Navigation & Layout
| Component |
Import |
Tabs |
@/components/ui/tabs — Tabs, TabsList, TabsTrigger, TabsContent |
Accordion |
@/components/ui/accordion — Accordion, AccordionItem, AccordionTrigger, AccordionContent |
NavigationMenu |
@/components/ui/navigation-menu |
Breadcrumb |
@/components/ui/breadcrumb |
Pagination |
@/components/ui/pagination |
Menubar |
@/components/ui/menubar |
ScrollArea |
@/components/ui/scroll-area |
Resizable |
@/components/ui/resizable |
Overlays & Feedback
| Component |
Import |
Dialog |
@/components/ui/dialog |
AlertDialog |
@/components/ui/alert-dialog |
Sheet |
@/components/ui/sheet — side-panel overlay |
Drawer |
@/components/ui/drawer — bottom sheet (great for mobile) |
Popover |
@/components/ui/popover |
Tooltip |
@/components/ui/tooltip |
HoverCard |
@/components/ui/hover-card |
ContextMenu |
@/components/ui/context-menu |
DropdownMenu |
@/components/ui/dropdown-menu |
Command |
@/components/ui/command — command palette / searchable list |
Alert |
@/components/ui/alert |
toast |
import { toast } from 'sonner' — toast.success(), toast.error() |
Data Display
| Component |
Import |
Table |
@/components/ui/table — Table, TableHeader, TableBody, TableRow, TableHead, TableCell |
Avatar |
@/components/ui/avatar |
Progress |
@/components/ui/progress |
Skeleton |
@/components/ui/skeleton |
AspectRatio |
@/components/ui/aspect-ratio |
Carousel |
@/components/ui/carousel |
Misc
| Component |
Import |
Toggle |
@/components/ui/toggle |
ToggleGroup |
@/components/ui/toggle-group |
Collapsible |
@/components/ui/collapsible |
ThemeToggle |
@/components/theme-toggle — light/dark mode switch |
SSR / Hydration Safety — @/components/client-only, @/components/safe-format
Server-rendered HTML must match the client's first render. An automated SSR lint
(eslint.ssr.config.mjs — do not delete) runs after every build and fails it on unsafe
patterns. Use these primitives instead of hand-rolling fixes:
| Primitive |
Import |
Use for |
ClientOnly |
@/components/client-only |
Anything browser-only or non-deterministic: window/localStorage reads, live clocks, random values, third-party widgets. Pass a fallback sized like the content. |
useMounted() |
@/components/client-only |
Hook variant when you need the boolean directly. |
SafeDate / SafeTime |
@/components/safe-format |
Dates/times — formats with explicit locale + UTC so SSR matches client. localize re-renders in the visitor's timezone after mount. Props: date (Date|string|number), options? (Intl.DateTimeFormatOptions), locale?, localize?, className? — there is no format/value prop; e.g. <SafeDate date={post.createdAt} options={{ dateStyle: 'medium' }} />. |
SafeNumber |
@/components/safe-format |
Numbers/currency, same guarantees. Props: value (number), options? (Intl.NumberFormatOptions), currency? (currency="USD"), locale?, localize?, className?. |
Rules of thumb: never touch window/document at module scope; never seed useState with |
|
|
Date.now()/Math.random()/new Date(); never call toLocaleString-style methods without |
|
|
an explicit locale (and timeZone for dates). |
|
|