Add documentation for component creation and design system structure
- **Component Creation Guide:** Document best practices for creating reusable, accessible components using CVA patterns. Includes guidance on when to compose vs create, decision trees, templates, prop design, testing checklists, and real-world examples. - **Design System README:** Introduce an organized structure for the design system documentation with quick navigation, learning paths, and reference links to key topics. Includes paths for quick starts, layouts, components, forms, and AI setup.
This commit is contained in:
@@ -1,802 +0,0 @@
|
||||
# Component Guide
|
||||
|
||||
**Project**: Next.js + FastAPI Template
|
||||
**Version**: 1.0
|
||||
**Last Updated**: 2025-10-31
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [shadcn/ui Components](#1-shadcn-ui-components)
|
||||
2. [Custom Components](#2-custom-components)
|
||||
3. [Component Composition](#3-component-composition)
|
||||
4. [Customization](#4-customization)
|
||||
5. [Accessibility](#5-accessibility)
|
||||
|
||||
---
|
||||
|
||||
## 1. shadcn/ui Components
|
||||
|
||||
### 1.1 Overview
|
||||
|
||||
This project uses [shadcn/ui](https://ui.shadcn.com), a collection of accessible, customizable components built on Radix UI primitives. Components are copied into the project (not installed as npm dependencies), giving you full control.
|
||||
|
||||
**Installation Method:**
|
||||
```bash
|
||||
npx shadcn@latest add button card input table dialog
|
||||
```
|
||||
|
||||
### 1.2 Core Components
|
||||
|
||||
#### Button
|
||||
|
||||
```typescript
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// Variants
|
||||
<Button variant="default">Default</Button>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
|
||||
// Sizes
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="icon"><IconName /></Button>
|
||||
|
||||
// States
|
||||
<Button disabled>Disabled</Button>
|
||||
<Button loading>Loading...</Button>
|
||||
|
||||
// As Link
|
||||
<Button asChild>
|
||||
<Link href="/users">View Users</Link>
|
||||
</Button>
|
||||
```
|
||||
|
||||
#### Card
|
||||
|
||||
```typescript
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card';
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>Manage system users</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content goes here</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Action</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
#### Dialog / Modal
|
||||
|
||||
```typescript
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger } from '@/components/ui/dialog';
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete User</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this user? This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>Cancel</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>Delete</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
#### Form
|
||||
|
||||
```typescript
|
||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
const form = useForm();
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="email@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your email address</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
```
|
||||
|
||||
#### Table
|
||||
|
||||
```typescript
|
||||
import { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell } from '@/components/ui/table';
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.role}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
```
|
||||
|
||||
#### Toast / Notifications
|
||||
|
||||
```typescript
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Success
|
||||
toast.success('User created successfully');
|
||||
|
||||
// Error
|
||||
toast.error('Failed to delete user');
|
||||
|
||||
// Info
|
||||
toast.info('Processing your request...');
|
||||
|
||||
// Loading
|
||||
toast.loading('Saving changes...');
|
||||
|
||||
// Custom
|
||||
toast('Event has been created', {
|
||||
description: 'Monday, January 3rd at 6:00pm',
|
||||
action: {
|
||||
label: 'Undo',
|
||||
onClick: () => console.log('Undo'),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Tabs
|
||||
|
||||
```typescript
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
|
||||
<Tabs defaultValue="profile">
|
||||
<TabsList>
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
<TabsTrigger value="sessions">Sessions</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="profile">
|
||||
<ProfileSettings />
|
||||
</TabsContent>
|
||||
<TabsContent value="password">
|
||||
<PasswordSettings />
|
||||
</TabsContent>
|
||||
<TabsContent value="sessions">
|
||||
<SessionManagement />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Custom Components
|
||||
|
||||
### 2.1 Layout Components
|
||||
|
||||
#### Header
|
||||
|
||||
```typescript
|
||||
import { Header } from '@/components/layout/Header';
|
||||
|
||||
// Usage (in layout.tsx)
|
||||
<Header />
|
||||
|
||||
// Features:
|
||||
// - Logo/brand
|
||||
// - Navigation links
|
||||
// - User menu (avatar, name, dropdown)
|
||||
// - Theme toggle
|
||||
// - Mobile menu button
|
||||
```
|
||||
|
||||
#### PageContainer
|
||||
|
||||
```typescript
|
||||
import { PageContainer } from '@/components/layout/PageContainer';
|
||||
|
||||
<PageContainer>
|
||||
<h1>Page Title</h1>
|
||||
<p>Page content...</p>
|
||||
</PageContainer>
|
||||
|
||||
// Provides:
|
||||
// - Consistent padding
|
||||
// - Max-width container
|
||||
// - Responsive layout
|
||||
```
|
||||
|
||||
#### PageHeader
|
||||
|
||||
```typescript
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
|
||||
<PageHeader
|
||||
title="Users"
|
||||
description="Manage system users"
|
||||
action={<Button>Create User</Button>}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2.2 Data Display Components
|
||||
|
||||
#### DataTable
|
||||
|
||||
Generic, reusable data table with sorting, filtering, and pagination.
|
||||
|
||||
```typescript
|
||||
import { DataTable } from '@/components/common/DataTable';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
// Define columns
|
||||
const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => (
|
||||
<Button onClick={() => handleEdit(row.original)}>Edit</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Use DataTable
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={users}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Search users..."
|
||||
/>
|
||||
```
|
||||
|
||||
#### LoadingSpinner
|
||||
|
||||
```typescript
|
||||
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
|
||||
|
||||
// Sizes
|
||||
<LoadingSpinner size="sm" />
|
||||
<LoadingSpinner size="md" />
|
||||
<LoadingSpinner size="lg" />
|
||||
|
||||
// With text
|
||||
<LoadingSpinner size="md" className="my-8">
|
||||
<p className="mt-2 text-sm text-muted-foreground">Loading users...</p>
|
||||
</LoadingSpinner>
|
||||
```
|
||||
|
||||
#### EmptyState
|
||||
|
||||
```typescript
|
||||
import { EmptyState } from '@/components/common/EmptyState';
|
||||
|
||||
<EmptyState
|
||||
icon={<Users className="h-12 w-12" />}
|
||||
title="No users found"
|
||||
description="Get started by creating a new user"
|
||||
action={
|
||||
<Button onClick={() => router.push('/admin/users/new')}>
|
||||
Create User
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2.3 Admin Components
|
||||
|
||||
#### UserTable
|
||||
|
||||
```typescript
|
||||
import { UserTable } from '@/components/admin/UserTable';
|
||||
|
||||
<UserTable
|
||||
filters={{ search: 'john', is_active: true }}
|
||||
onUserSelect={(user) => console.log(user)}
|
||||
/>
|
||||
|
||||
// Features:
|
||||
// - Search
|
||||
// - Filters (role, status)
|
||||
// - Sorting
|
||||
// - Pagination
|
||||
// - Bulk selection
|
||||
// - Bulk actions (activate, deactivate, delete)
|
||||
```
|
||||
|
||||
#### UserForm
|
||||
|
||||
```typescript
|
||||
import { UserForm } from '@/components/admin/UserForm';
|
||||
|
||||
// Create mode
|
||||
<UserForm
|
||||
mode="create"
|
||||
onSuccess={() => router.push('/admin/users')}
|
||||
/>
|
||||
|
||||
// Edit mode
|
||||
<UserForm
|
||||
mode="edit"
|
||||
user={user}
|
||||
onSuccess={() => toast.success('User updated')}
|
||||
/>
|
||||
|
||||
// Features:
|
||||
// - Validation with Zod
|
||||
// - Field errors
|
||||
// - Loading states
|
||||
// - Cancel/Submit actions
|
||||
```
|
||||
|
||||
#### OrganizationTable
|
||||
|
||||
```typescript
|
||||
import { OrganizationTable } from '@/components/admin/OrganizationTable';
|
||||
|
||||
<OrganizationTable />
|
||||
|
||||
// Features:
|
||||
// - Search
|
||||
// - Member count display
|
||||
// - Actions (edit, delete, view members)
|
||||
```
|
||||
|
||||
#### BulkActionBar
|
||||
|
||||
```typescript
|
||||
import { BulkActionBar } from '@/components/admin/BulkActionBar';
|
||||
|
||||
<BulkActionBar
|
||||
selectedIds={selectedUserIds}
|
||||
onAction={(action) => handleBulkAction(action, selectedUserIds)}
|
||||
onClearSelection={() => setSelectedUserIds([])}
|
||||
actions={[
|
||||
{ value: 'activate', label: 'Activate' },
|
||||
{ value: 'deactivate', label: 'Deactivate' },
|
||||
{ value: 'delete', label: 'Delete', variant: 'destructive' },
|
||||
]}
|
||||
/>
|
||||
|
||||
// Displays:
|
||||
// - Selection count
|
||||
// - Action dropdown
|
||||
// - Confirmation dialogs
|
||||
// - Progress indicators
|
||||
```
|
||||
|
||||
### 2.4 Settings Components
|
||||
|
||||
#### ProfileSettings
|
||||
|
||||
```typescript
|
||||
import { ProfileSettings } from '@/components/settings/ProfileSettings';
|
||||
|
||||
<ProfileSettings
|
||||
user={currentUser}
|
||||
onUpdate={(updatedUser) => console.log('Updated:', updatedUser)}
|
||||
/>
|
||||
|
||||
// Fields:
|
||||
// - First name, last name
|
||||
// - Email (readonly)
|
||||
// - Phone number
|
||||
// - Avatar upload (optional)
|
||||
// - Preferences
|
||||
```
|
||||
|
||||
#### PasswordSettings
|
||||
|
||||
```typescript
|
||||
import { PasswordSettings } from '@/components/settings/PasswordSettings';
|
||||
|
||||
<PasswordSettings />
|
||||
|
||||
// Fields:
|
||||
// - Current password
|
||||
// - New password
|
||||
// - Confirm password
|
||||
// - Option to logout all other devices
|
||||
```
|
||||
|
||||
#### SessionManagement
|
||||
|
||||
```typescript
|
||||
import { SessionManagement } from '@/components/settings/SessionManagement';
|
||||
|
||||
<SessionManagement />
|
||||
|
||||
// Features:
|
||||
// - List all active sessions
|
||||
// - Current session badge
|
||||
// - Device icons
|
||||
// - Location display
|
||||
// - Last used timestamp
|
||||
// - Revoke session button
|
||||
// - Logout all other devices button
|
||||
```
|
||||
|
||||
#### SessionCard
|
||||
|
||||
```typescript
|
||||
import { SessionCard } from '@/components/settings/SessionCard';
|
||||
|
||||
<SessionCard
|
||||
session={session}
|
||||
isCurrent={session.is_current}
|
||||
onRevoke={() => revokeSession(session.id)}
|
||||
/>
|
||||
|
||||
// Displays:
|
||||
// - Device icon (desktop/mobile/tablet)
|
||||
// - Device name
|
||||
// - Location (city, country)
|
||||
// - IP address
|
||||
// - Last used (relative time)
|
||||
// - "This device" badge if current
|
||||
// - Revoke button (disabled for current)
|
||||
```
|
||||
|
||||
### 2.5 Chart Components
|
||||
|
||||
#### BarChartCard
|
||||
|
||||
```typescript
|
||||
import { BarChartCard } from '@/components/charts/BarChartCard';
|
||||
|
||||
<BarChartCard
|
||||
title="User Registrations"
|
||||
description="Monthly user registrations"
|
||||
data={[
|
||||
{ month: 'Jan', count: 45 },
|
||||
{ month: 'Feb', count: 52 },
|
||||
{ month: 'Mar', count: 61 },
|
||||
]}
|
||||
dataKey="count"
|
||||
xAxisKey="month"
|
||||
/>
|
||||
```
|
||||
|
||||
#### LineChartCard
|
||||
|
||||
```typescript
|
||||
import { LineChartCard } from '@/components/charts/LineChartCard';
|
||||
|
||||
<LineChartCard
|
||||
title="Active Users"
|
||||
description="Daily active users over time"
|
||||
data={dailyActiveUsers}
|
||||
dataKey="count"
|
||||
xAxisKey="date"
|
||||
color="hsl(var(--primary))"
|
||||
/>
|
||||
```
|
||||
|
||||
#### PieChartCard
|
||||
|
||||
```typescript
|
||||
import { PieChartCard } from '@/components/charts/PieChartCard';
|
||||
|
||||
<PieChartCard
|
||||
title="Users by Role"
|
||||
description="Distribution of user roles"
|
||||
data={[
|
||||
{ name: 'Admin', value: 10 },
|
||||
{ name: 'User', value: 245 },
|
||||
{ name: 'Guest', value: 56 },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Composition
|
||||
|
||||
### 3.1 Form + Dialog Pattern
|
||||
|
||||
```typescript
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create User</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new user to the system
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<UserForm
|
||||
mode="create"
|
||||
onSuccess={() => {
|
||||
setIsOpen(false);
|
||||
queryClient.invalidateQueries(['users']);
|
||||
}}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
### 3.2 Card + Table Pattern
|
||||
|
||||
```typescript
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>Manage system users</CardDescription>
|
||||
</div>
|
||||
<Button onClick={() => router.push('/admin/users/new')}>
|
||||
Create User
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UserTable />
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### 3.3 Tabs + Settings Pattern
|
||||
|
||||
```typescript
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="profile">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
<TabsTrigger value="sessions">Sessions</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="profile">
|
||||
<ProfileSettings />
|
||||
</TabsContent>
|
||||
<TabsContent value="password">
|
||||
<PasswordSettings />
|
||||
</TabsContent>
|
||||
<TabsContent value="sessions">
|
||||
<SessionManagement />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### 3.4 Bulk Actions Pattern
|
||||
|
||||
```typescript
|
||||
function UserList() {
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const { data: users } = useUsers();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{selectedIds.length > 0 && (
|
||||
<BulkActionBar
|
||||
selectedIds={selectedIds}
|
||||
onAction={handleBulkAction}
|
||||
onClearSelection={() => setSelectedIds([])}
|
||||
/>
|
||||
)}
|
||||
<DataTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
enableRowSelection
|
||||
onRowSelectionChange={setSelectedIds}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Customization
|
||||
|
||||
### 4.1 Theming
|
||||
|
||||
Colors are defined in `tailwind.config.ts` using CSS variables:
|
||||
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
// ...
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Customize colors in `globals.css`:**
|
||||
```css
|
||||
@layer base {
|
||||
:root {
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
/* ... */
|
||||
}
|
||||
|
||||
.dark {
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Component Variants
|
||||
|
||||
Add new variants to existing components:
|
||||
|
||||
```typescript
|
||||
// components/ui/button.tsx
|
||||
const buttonVariants = cva(
|
||||
'base-classes',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: '...',
|
||||
destructive: '...',
|
||||
outline: '...',
|
||||
// Add custom variant
|
||||
success: 'bg-green-600 text-white hover:bg-green-700',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Usage
|
||||
<Button variant="success">Activate</Button>
|
||||
```
|
||||
|
||||
### 4.3 Extending Components
|
||||
|
||||
Create wrapper components:
|
||||
|
||||
```typescript
|
||||
// components/common/ConfirmDialog.tsx
|
||||
interface ConfirmDialogProps {
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel?: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Confirm',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
return (
|
||||
<Dialog open onOpenChange={onCancel}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Accessibility
|
||||
|
||||
### 5.1 Keyboard Navigation
|
||||
|
||||
All shadcn/ui components support keyboard navigation:
|
||||
- `Tab`: Move focus
|
||||
- `Enter`/`Space`: Activate
|
||||
- `Escape`: Close dialogs/dropdowns
|
||||
- Arrow keys: Navigate lists/menus
|
||||
|
||||
### 5.2 Screen Reader Support
|
||||
|
||||
Components include proper ARIA labels:
|
||||
|
||||
```typescript
|
||||
<button aria-label="Close dialog">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div role="status" aria-live="polite">
|
||||
Loading users...
|
||||
</div>
|
||||
|
||||
<input
|
||||
aria-invalid={!!errors.email}
|
||||
aria-describedby="email-error"
|
||||
/>
|
||||
```
|
||||
|
||||
### 5.3 Focus Management
|
||||
|
||||
Dialog components automatically manage focus:
|
||||
- Focus trap inside dialog
|
||||
- Return focus on close
|
||||
- Focus first focusable element
|
||||
|
||||
### 5.4 Color Contrast
|
||||
|
||||
All theme colors meet WCAG 2.1 Level AA standards:
|
||||
- Normal text: 4.5:1 contrast ratio
|
||||
- Large text: 3:1 contrast ratio
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This guide covers the essential components in the project. For more details:
|
||||
- **shadcn/ui docs**: https://ui.shadcn.com
|
||||
- **Radix UI docs**: https://www.radix-ui.com
|
||||
- **TanStack Table docs**: https://tanstack.com/table
|
||||
- **Recharts docs**: https://recharts.org
|
||||
|
||||
For implementation examples, see `FEATURE_EXAMPLES.md`.
|
||||
@@ -1,645 +0,0 @@
|
||||
# FastNext Template Design System
|
||||
|
||||
**Version**: 1.0
|
||||
**Last Updated**: November 2, 2025
|
||||
**Theme**: Modern Minimal (via [tweakcn.com](https://tweakcn.com))
|
||||
**Status**: Production Ready
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Color System](#color-system)
|
||||
3. [Typography](#typography)
|
||||
4. [Spacing & Layout](#spacing--layout)
|
||||
5. [Shadows](#shadows)
|
||||
6. [Border Radius](#border-radius)
|
||||
7. [Components](#components)
|
||||
8. [Dark Mode](#dark-mode)
|
||||
9. [Accessibility](#accessibility)
|
||||
10. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This design system is built on **shadcn/ui** component library with **Tailwind CSS 4**, using the **OKLCH color space** for superior perceptual uniformity and accessibility.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Framework**: Next.js 15 + React 19
|
||||
- **Styling**: Tailwind CSS 4 (CSS-first configuration)
|
||||
- **Components**: shadcn/ui (New York style)
|
||||
- **Color Space**: OKLCH
|
||||
- **Icons**: lucide-react
|
||||
- **Fonts**: Geist Sans + Geist Mono
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Minimal & Clean** - Simple, uncluttered interfaces
|
||||
2. **Accessible First** - WCAG AA compliance minimum
|
||||
3. **Consistent** - Predictable patterns across the application
|
||||
4. **Performant** - Optimized for speed and efficiency
|
||||
5. **Responsive** - Mobile-first approach
|
||||
|
||||
---
|
||||
|
||||
## Color System
|
||||
|
||||
Our color system uses **OKLCH** (Oklab LCH) color space for:
|
||||
- Perceptual uniformity across light and dark modes
|
||||
- Better accessibility with predictable contrast
|
||||
- More vibrant colors without sacrificing legibility
|
||||
|
||||
### Semantic Color Tokens
|
||||
|
||||
All colors follow the **background/foreground** convention:
|
||||
- `background` - The background color
|
||||
- `foreground` - The text color on that background
|
||||
|
||||
#### Primary Colors
|
||||
|
||||
**Purpose**: Main brand color, CTAs, primary actions
|
||||
|
||||
```css
|
||||
Light: --primary: oklch(0.6231 0.1880 259.8145) /* Blue */
|
||||
Dark: --primary: oklch(0.6231 0.1880 259.8145) /* Same blue */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
<Button>Primary Action</Button>
|
||||
<a href="#" className="text-primary">Link</a>
|
||||
```
|
||||
|
||||
#### Secondary Colors
|
||||
|
||||
**Purpose**: Secondary actions, less prominent UI elements
|
||||
|
||||
```css
|
||||
Light: --secondary: oklch(0.9670 0.0029 264.5419) /* Light gray-blue */
|
||||
Dark: --secondary: oklch(0.2686 0 0) /* Dark gray */
|
||||
```
|
||||
|
||||
#### Muted Colors
|
||||
|
||||
**Purpose**: Backgrounds for disabled states, subtle UI elements
|
||||
|
||||
```css
|
||||
Light: --muted: oklch(0.9846 0.0017 247.8389)
|
||||
Dark: --muted: oklch(0.2393 0 0)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
- Disabled button backgrounds
|
||||
- Skeleton loaders
|
||||
- TabsList backgrounds
|
||||
- Switch backgrounds (unchecked)
|
||||
|
||||
#### Accent Colors
|
||||
|
||||
**Purpose**: Hover states, focus indicators, highlights
|
||||
|
||||
```css
|
||||
Light: --accent: oklch(0.9514 0.0250 236.8242)
|
||||
Dark: --accent: oklch(0.3791 0.1378 265.5222)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
<DropdownMenuItem className="focus:bg-accent">Item</DropdownMenuItem>
|
||||
```
|
||||
|
||||
#### Destructive Colors
|
||||
|
||||
**Purpose**: Error states, delete actions, warnings
|
||||
|
||||
```css
|
||||
Light: --destructive: oklch(0.6368 0.2078 25.3313) /* Red */
|
||||
Dark: --destructive: oklch(0.6368 0.2078 25.3313) /* Same red */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Alert variant="destructive">Error message</Alert>
|
||||
```
|
||||
|
||||
#### Card & Popover
|
||||
|
||||
**Purpose**: Elevated surfaces (cards, popovers, dropdowns)
|
||||
|
||||
```css
|
||||
Light: --card: oklch(1.0000 0 0) /* White */
|
||||
Dark: --card: oklch(0.2686 0 0) /* Dark gray */
|
||||
|
||||
Light: --popover: oklch(1.0000 0 0) /* White */
|
||||
Dark: --popover: oklch(0.2686 0 0) /* Dark gray */
|
||||
```
|
||||
|
||||
#### Border & Input
|
||||
|
||||
**Purpose**: Borders, input field borders
|
||||
|
||||
```css
|
||||
Light: --border: oklch(0.9276 0.0058 264.5313)
|
||||
Dark: --border: oklch(0.3715 0 0)
|
||||
|
||||
Light: --input: oklch(0.9276 0.0058 264.5313)
|
||||
Dark: --input: oklch(0.3715 0 0)
|
||||
```
|
||||
|
||||
#### Focus Ring
|
||||
|
||||
**Purpose**: Focus indicators for keyboard navigation
|
||||
|
||||
```css
|
||||
Light: --ring: oklch(0.6231 0.1880 259.8145) /* Primary blue */
|
||||
Dark: --ring: oklch(0.6231 0.1880 259.8145)
|
||||
```
|
||||
|
||||
### Chart Colors
|
||||
|
||||
For data visualization, we provide 5 harmonious chart colors:
|
||||
|
||||
```css
|
||||
--chart-1: oklch(0.6231 0.1880 259.8145) /* Blue */
|
||||
--chart-2: oklch(0.5461 0.2152 262.8809) /* Purple-blue */
|
||||
--chart-3: oklch(0.4882 0.2172 264.3763) /* Deep purple */
|
||||
--chart-4: oklch(0.4244 0.1809 265.6377) /* Violet */
|
||||
--chart-5: oklch(0.3791 0.1378 265.5222) /* Deep violet */
|
||||
```
|
||||
|
||||
### Color Usage Guidelines
|
||||
|
||||
**DO**:
|
||||
- ✅ Use semantic tokens (`bg-primary`, `text-destructive`)
|
||||
- ✅ Test color combinations with contrast checkers
|
||||
- ✅ Use `muted` for subtle backgrounds
|
||||
- ✅ Use `accent` for hover states
|
||||
|
||||
**DON'T**:
|
||||
- ❌ Use arbitrary color values (`bg-blue-500`)
|
||||
- ❌ Mix OKLCH with RGB/HSL in the same context
|
||||
- ❌ Use `primary` for large background areas
|
||||
- ❌ Override foreground colors without checking contrast
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Families
|
||||
|
||||
```css
|
||||
--font-sans: Geist Sans, system-ui, -apple-system, sans-serif
|
||||
--font-mono: Geist Mono, ui-monospace, monospace
|
||||
--font-serif: ui-serif, Georgia, serif
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
<div className="font-sans">Body text</div>
|
||||
<code className="font-mono">const example = true;</code>
|
||||
```
|
||||
|
||||
### Type Scale
|
||||
|
||||
Tailwind CSS provides a comprehensive type scale. We use:
|
||||
|
||||
| Size | Class | Use Case |
|
||||
|------|-------|----------|
|
||||
| 3xl | `text-3xl` | Page titles |
|
||||
| 2xl | `text-2xl` | Section headings |
|
||||
| xl | `text-xl` | Card titles |
|
||||
| lg | `text-lg` | Subheadings |
|
||||
| base | `text-base` | Body text (default) |
|
||||
| sm | `text-sm` | Secondary text, captions |
|
||||
| xs | `text-xs` | Labels, helper text |
|
||||
|
||||
### Font Weights
|
||||
|
||||
| Weight | Class | Use Case |
|
||||
|--------|-------|----------|
|
||||
| 700 | `font-bold` | Headings, emphasis |
|
||||
| 600 | `font-semibold` | Subheadings, buttons |
|
||||
| 500 | `font-medium` | Labels, menu items |
|
||||
| 400 | `font-normal` | Body text (default) |
|
||||
| 300 | `font-light` | De-emphasized text |
|
||||
|
||||
### Typography Guidelines
|
||||
|
||||
**DO**:
|
||||
- ✅ Use `text-foreground` for body text
|
||||
- ✅ Use `text-muted-foreground` for secondary text
|
||||
- ✅ Maintain consistent heading hierarchy (h1 → h2 → h3)
|
||||
- ✅ Limit line length to 60-80 characters for readability
|
||||
|
||||
**DON'T**:
|
||||
- ❌ Use more than 3 font sizes on a single page
|
||||
- ❌ Use custom colors without checking contrast
|
||||
- ❌ Skip heading levels (h1 → h3)
|
||||
- ❌ Use `font-bold` excessively
|
||||
|
||||
**Line Height**:
|
||||
- Headings: `leading-tight` (1.25)
|
||||
- Body: `leading-normal` (1.5) - default
|
||||
- Dense UI: `leading-relaxed` (1.625)
|
||||
|
||||
---
|
||||
|
||||
## Spacing & Layout
|
||||
|
||||
### Spacing Scale
|
||||
|
||||
Tailwind uses a **0.25rem (4px) base unit**:
|
||||
|
||||
```css
|
||||
--spacing: 0.25rem;
|
||||
```
|
||||
|
||||
| Token | Value | Pixels | Use Case |
|
||||
|-------|-------|--------|----------|
|
||||
| `0` | 0 | 0px | No spacing |
|
||||
| `px` | 1px | 1px | Borders, dividers |
|
||||
| `0.5` | 0.125rem | 2px | Tight spacing |
|
||||
| `1` | 0.25rem | 4px | Icon gaps |
|
||||
| `2` | 0.5rem | 8px | Small gaps |
|
||||
| `3` | 0.75rem | 12px | Component padding |
|
||||
| `4` | 1rem | 16px | Standard spacing |
|
||||
| `6` | 1.5rem | 24px | Section spacing |
|
||||
| `8` | 2rem | 32px | Large gaps |
|
||||
| `12` | 3rem | 48px | Section dividers |
|
||||
| `16` | 4rem | 64px | Page sections |
|
||||
|
||||
### Container & Max Width
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Responsive container with horizontal padding */}
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Constrained width for readability */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Max Width Scale**:
|
||||
- `max-w-sm` - 384px - Small cards
|
||||
- `max-w-md` - 448px - Forms
|
||||
- `max-w-lg` - 512px - Modals
|
||||
- `max-w-2xl` - 672px - Article content
|
||||
- `max-w-4xl` - 896px - Wide layouts
|
||||
- `max-w-7xl` - 1280px - Full page width
|
||||
|
||||
### Layout Guidelines
|
||||
|
||||
**DO**:
|
||||
- ✅ Use multiples of 4 for spacing (4, 8, 12, 16, 24, 32...)
|
||||
- ✅ Use `gap-` utilities for flex/grid spacing
|
||||
- ✅ Use `space-y-` for vertical stacks
|
||||
- ✅ Use `container` for page-level constraints
|
||||
|
||||
**DON'T**:
|
||||
- ❌ Use arbitrary values like `p-[13px]`
|
||||
- ❌ Mix padding and margin inconsistently
|
||||
- ❌ Forget responsive spacing (`sm:p-6 lg:p-8`)
|
||||
|
||||
---
|
||||
|
||||
## Shadows
|
||||
|
||||
Professional shadow system for depth and elevation:
|
||||
|
||||
```css
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05)
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25)
|
||||
```
|
||||
|
||||
### Shadow Usage
|
||||
|
||||
| Elevation | Class | Use Case |
|
||||
|-----------|-------|----------|
|
||||
| Base | No shadow | Buttons, inline elements |
|
||||
| Low | `shadow-sm` | Cards, panels |
|
||||
| Medium | `shadow-md` | Dropdowns, tooltips |
|
||||
| High | `shadow-lg` | Modals, popovers |
|
||||
| Highest | `shadow-xl` | Notifications, floating |
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
<Card className="shadow-sm">Card content</Card>
|
||||
<DropdownMenu className="shadow-md">Menu</DropdownMenu>
|
||||
<Dialog className="shadow-xl">Modal</Dialog>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Border Radius
|
||||
|
||||
Consistent rounded corners across the application:
|
||||
|
||||
```css
|
||||
--radius: 0.375rem; /* 6px - base */
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px) /* 2px */
|
||||
--radius-md: calc(var(--radius) - 2px) /* 4px */
|
||||
--radius-lg: var(--radius) /* 6px */
|
||||
--radius-xl: calc(var(--radius) + 4px) /* 10px */
|
||||
```
|
||||
|
||||
### Border Radius Usage
|
||||
|
||||
| Token | Value | Use Case |
|
||||
|-------|-------|----------|
|
||||
| `rounded-sm` | 2px | Small elements, tags |
|
||||
| `rounded-md` | 4px | Inputs, small buttons |
|
||||
| `rounded-lg` | 6px | Cards, buttons (default) |
|
||||
| `rounded-xl` | 10px | Large cards, modals |
|
||||
| `rounded-full` | 9999px | Pills, avatars, icon buttons |
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
<Button className="rounded-lg">Default Button</Button>
|
||||
<Avatar className="rounded-full" />
|
||||
<Badge className="rounded-sm">Tag</Badge>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### shadcn/ui Component Library
|
||||
|
||||
We use **shadcn/ui** components (New York style) with CSS variables for theming.
|
||||
|
||||
**Installed Components**:
|
||||
- `alert` - Alerts and notifications
|
||||
- `avatar` - User avatars
|
||||
- `badge` - Tags and labels
|
||||
- `button` - Buttons (primary, secondary, outline, ghost, destructive)
|
||||
- `card` - Content cards
|
||||
- `checkbox` - Checkboxes
|
||||
- `dialog` - Modals and dialogs
|
||||
- `dropdown-menu` - Context menus, dropdowns
|
||||
- `input` - Text inputs
|
||||
- `label` - Form labels
|
||||
- `popover` - Tooltips, popovers
|
||||
- `select` - Select dropdowns
|
||||
- `separator` - Horizontal/vertical dividers
|
||||
- `sheet` - Side panels
|
||||
- `skeleton` - Loading skeletons
|
||||
- `table` - Data tables
|
||||
- `tabs` - Tabbed interfaces
|
||||
- `textarea` - Multi-line inputs
|
||||
|
||||
### Component Variants
|
||||
|
||||
#### Button Variants
|
||||
|
||||
```tsx
|
||||
<Button variant="default">Primary Button</Button>
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
<Button variant="outline">Outline Button</Button>
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Button variant="link">Link Button</Button>
|
||||
```
|
||||
|
||||
#### Button Sizes
|
||||
|
||||
```tsx
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="icon"><Icon /></Button>
|
||||
```
|
||||
|
||||
### Component Guidelines
|
||||
|
||||
**DO**:
|
||||
- ✅ Use semantic variant names (`destructive`, not `red`)
|
||||
- ✅ Compose components from shadcn/ui primitives
|
||||
- ✅ Follow the background/foreground convention
|
||||
- ✅ Add `aria-label` for icon-only buttons
|
||||
|
||||
**DON'T**:
|
||||
- ❌ Create custom variants without documenting them
|
||||
- ❌ Override component styles with arbitrary classes
|
||||
- ❌ Mix component libraries (stick to shadcn/ui)
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Dark mode is implemented using CSS classes (`.dark`) with automatic OS preference detection.
|
||||
|
||||
### Dark Mode Strategy
|
||||
|
||||
1. **CSS Variables**: All colors have light and dark variants
|
||||
2. **Class Toggle**: `.dark` class on `<html>` element
|
||||
3. **Persistent**: User preference stored in localStorage
|
||||
4. **Smooth Transition**: Optional transitions between modes
|
||||
|
||||
### Implementation
|
||||
|
||||
```tsx
|
||||
// Toggle dark mode
|
||||
<Button onClick={() => document.documentElement.classList.toggle('dark')}>
|
||||
Toggle Theme
|
||||
</Button>
|
||||
|
||||
// Check current mode
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
```
|
||||
|
||||
### Dark Mode Guidelines
|
||||
|
||||
**DO**:
|
||||
- ✅ Test all components in both light and dark modes
|
||||
- ✅ Use semantic color tokens (not hardcoded colors)
|
||||
- ✅ Maintain WCAG AA contrast in both modes
|
||||
- ✅ Provide a theme toggle in settings
|
||||
|
||||
**DON'T**:
|
||||
- ❌ Use absolute colors like `bg-white` or `bg-black`
|
||||
- ❌ Assume users only use one mode
|
||||
- ❌ Forget to test shadow visibility in dark mode
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
We follow **WCAG 2.1 Level AA** as the minimum standard.
|
||||
|
||||
### Color Contrast
|
||||
|
||||
All text must meet minimum contrast ratios:
|
||||
|
||||
- **Normal text (< 18px)**: 4.5:1 minimum
|
||||
- **Large text (≥ 18px or ≥ 14px bold)**: 3:1 minimum
|
||||
- **UI components**: 3:1 minimum
|
||||
|
||||
**Tools**:
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- Chrome DevTools Accessibility panel
|
||||
- Browser extensions (axe, WAVE)
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
**Requirements**:
|
||||
- ✅ All interactive elements focusable via Tab
|
||||
- ✅ Visible focus indicators (`:focus-visible`)
|
||||
- ✅ Logical tab order
|
||||
- ✅ Escape key closes modals/dropdowns
|
||||
- ✅ Arrow keys for navigation within components
|
||||
|
||||
### Screen Readers
|
||||
|
||||
**Requirements**:
|
||||
- ✅ Semantic HTML (headings, lists, nav, main, etc.)
|
||||
- ✅ `aria-label` for icon-only buttons
|
||||
- ✅ `aria-describedby` for form errors
|
||||
- ✅ `role` attributes where needed
|
||||
- ✅ Live regions for dynamic updates
|
||||
|
||||
### Focus Management
|
||||
|
||||
```tsx
|
||||
// Proper focus indicator
|
||||
<Button className="focus-visible:ring-2 focus-visible:ring-ring">
|
||||
Action
|
||||
</Button>
|
||||
|
||||
// Skip to main content link
|
||||
<a href="#main" className="sr-only focus:not-sr-only">
|
||||
Skip to main content
|
||||
</a>
|
||||
```
|
||||
|
||||
### Accessibility Checklist
|
||||
|
||||
- [ ] Color contrast meets WCAG AA (4.5:1 for text)
|
||||
- [ ] All interactive elements keyboard accessible
|
||||
- [ ] Focus indicators visible
|
||||
- [ ] Images have `alt` text
|
||||
- [ ] Forms have labels
|
||||
- [ ] Error messages associated with inputs
|
||||
- [ ] ARIA attributes used correctly
|
||||
- [ ] Tested with screen reader (NVDA, JAWS, VoiceOver)
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**DO**:
|
||||
```tsx
|
||||
// ✅ Descriptive, semantic names
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<div className="bg-card text-card-foreground">Content</div>
|
||||
```
|
||||
|
||||
**DON'T**:
|
||||
```tsx
|
||||
// ❌ Generic, non-semantic names
|
||||
<Button className="bg-red-500">Delete</Button>
|
||||
<div className="bg-white text-black">Content</div>
|
||||
```
|
||||
|
||||
### Component Structure
|
||||
|
||||
**DO**:
|
||||
```tsx
|
||||
// ✅ Compose with shadcn/ui primitives
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>Content</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
**DON'T**:
|
||||
```tsx
|
||||
// ❌ Create custom components for everything
|
||||
<CustomCard title="Title">Content</CustomCard>
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
**DO**:
|
||||
```tsx
|
||||
// ✅ Mobile-first responsive utilities
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl">Title</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
**DON'T**:
|
||||
```tsx
|
||||
// ❌ Desktop-first or no responsive design
|
||||
<div className="p-8">
|
||||
<h1 className="text-4xl">Title</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
**DO**:
|
||||
- ✅ Use CSS variables for theming (no runtime JS)
|
||||
- ✅ Minimize custom CSS (use Tailwind utilities)
|
||||
- ✅ Lazy load components when appropriate
|
||||
- ✅ Optimize images with Next.js Image component
|
||||
|
||||
**DON'T**:
|
||||
- ❌ Inline styles everywhere
|
||||
- ❌ Large custom CSS files
|
||||
- ❌ Unoptimized images
|
||||
- ❌ Excessive animation/transitions
|
||||
|
||||
### Code Organization
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── ui/ # shadcn/ui components (don't edit)
|
||||
├── auth/ # Authentication components
|
||||
├── layout/ # Layout components (Header, Footer)
|
||||
└── settings/ # Feature-specific components
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
**DO**:
|
||||
- ✅ Document custom variants in this file
|
||||
- ✅ Add JSDoc comments to complex components
|
||||
- ✅ Include usage examples
|
||||
- ✅ Update this document when making changes
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | Nov 2, 2025 | Initial design system with Modern Minimal theme |
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [shadcn/ui Documentation](https://ui.shadcn.com)
|
||||
- [Tailwind CSS 4 Documentation](https://tailwindcss.com/docs)
|
||||
- [OKLCH Color Space](https://oklch.com)
|
||||
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [Theme Generator](https://tweakcn.com)
|
||||
|
||||
---
|
||||
|
||||
**For questions or suggestions, refer to this document and the official documentation.**
|
||||
456
frontend/docs/design-system/00-quick-start.md
Normal file
456
frontend/docs/design-system/00-quick-start.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# Quick Start Guide
|
||||
|
||||
Get up and running with the FastNext design system immediately. This guide covers the essential patterns you need to build 80% of interfaces.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
```tsx
|
||||
// 1. Import from @/components/ui/*
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
|
||||
// 2. Use semantic color tokens
|
||||
className="bg-primary text-primary-foreground"
|
||||
className="text-destructive"
|
||||
|
||||
// 3. Use spacing scale (4, 8, 12, 16, 24, 32...)
|
||||
className="p-4 space-y-6"
|
||||
|
||||
// 4. Build layouts with these patterns
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Your content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Essential Components
|
||||
|
||||
### Buttons
|
||||
|
||||
```tsx
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// Primary action
|
||||
<Button>Save Changes</Button>
|
||||
|
||||
// Danger action
|
||||
<Button variant="destructive">Delete</Button>
|
||||
|
||||
// Secondary action
|
||||
<Button variant="outline">Cancel</Button>
|
||||
|
||||
// Subtle action
|
||||
<Button variant="ghost">Skip</Button>
|
||||
|
||||
// Sizes
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
```
|
||||
|
||||
**[See all button variants](/dev/components#button)**
|
||||
|
||||
---
|
||||
|
||||
### Cards
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter
|
||||
} from '@/components/ui/card';
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User Profile</CardTitle>
|
||||
<CardDescription>Manage your account settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content goes here</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Save</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
**[See card examples](/dev/components#card)**
|
||||
|
||||
---
|
||||
|
||||
### Forms
|
||||
|
||||
```tsx
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
**[See form patterns](./06-forms.md)** | **[Form examples](/dev/forms)**
|
||||
|
||||
---
|
||||
|
||||
### Dialogs/Modals
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTrigger
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Action</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to proceed?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Confirm</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
**[See dialog examples](/dev/components#dialog)**
|
||||
|
||||
---
|
||||
|
||||
### Alerts
|
||||
|
||||
```tsx
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
// Default alert
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is an informational message.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
// Error alert
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
Something went wrong. Please try again.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
**[See all component variants](/dev/components)**
|
||||
|
||||
---
|
||||
|
||||
## 2. Essential Layouts (1 minute)
|
||||
|
||||
### Page Container
|
||||
|
||||
```tsx
|
||||
// Standard page layout
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
<Card>{/* Content */}</Card>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dashboard Grid
|
||||
|
||||
```tsx
|
||||
// Responsive card grid
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map(item => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{item.content}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form Layout
|
||||
|
||||
```tsx
|
||||
// Centered form with max width
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Login</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Form fields */}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
**[See all layout patterns](./03-layouts.md)** | **[Layout examples](/dev/layouts)**
|
||||
|
||||
---
|
||||
|
||||
## 3. Color System
|
||||
|
||||
**Always use semantic tokens**, never arbitrary colors:
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Semantic tokens
|
||||
<div className="bg-primary text-primary-foreground">Primary</div>
|
||||
<div className="bg-destructive text-destructive-foreground">Error</div>
|
||||
<div className="bg-muted text-muted-foreground">Disabled</div>
|
||||
<p className="text-foreground">Body text</p>
|
||||
<p className="text-muted-foreground">Secondary text</p>
|
||||
|
||||
// ❌ BAD - Arbitrary colors
|
||||
<div className="bg-blue-500 text-white">Don't do this</div>
|
||||
```
|
||||
|
||||
**Available tokens:**
|
||||
- `primary` - Main brand color, CTAs
|
||||
- `destructive` - Errors, delete actions
|
||||
- `muted` - Disabled states, subtle backgrounds
|
||||
- `accent` - Hover states, highlights
|
||||
- `foreground` - Body text
|
||||
- `muted-foreground` - Secondary text
|
||||
- `border` - Borders, dividers
|
||||
|
||||
**[See complete color system](./01-foundations.md#color-system-oklch)**
|
||||
|
||||
---
|
||||
|
||||
## 4. Spacing System
|
||||
|
||||
**Use multiples of 4** (Tailwind's base unit is 0.25rem = 4px):
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Consistent spacing
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="mb-8">Content</div>
|
||||
</div>
|
||||
|
||||
// ❌ BAD - Arbitrary spacing
|
||||
<div className="p-[13px] space-y-[17px]">
|
||||
<div className="mb-[23px]">Content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Common spacing values:**
|
||||
- `2` (8px) - Tight spacing
|
||||
- `4` (16px) - Standard spacing
|
||||
- `6` (24px) - Section spacing
|
||||
- `8` (32px) - Large gaps
|
||||
- `12` (48px) - Section dividers
|
||||
|
||||
**Pro tip:** Use `gap-` for grids/flex, `space-y-` for vertical stacks:
|
||||
|
||||
```tsx
|
||||
// Grid spacing
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
|
||||
// Stack spacing
|
||||
<div className="space-y-4">
|
||||
```
|
||||
|
||||
**[Read spacing philosophy](./04-spacing-philosophy.md)**
|
||||
|
||||
---
|
||||
|
||||
## 5. Responsive Design
|
||||
|
||||
**Mobile-first approach** with Tailwind breakpoints:
|
||||
|
||||
```tsx
|
||||
<div className="
|
||||
p-4 // Mobile: 16px padding
|
||||
sm:p-6 // Tablet: 24px padding
|
||||
lg:p-8 // Desktop: 32px padding
|
||||
">
|
||||
<h1 className="
|
||||
text-2xl // Mobile: 24px
|
||||
sm:text-3xl // Tablet: 30px
|
||||
lg:text-4xl // Desktop: 36px
|
||||
font-bold
|
||||
">
|
||||
Responsive Title
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
// Grid columns
|
||||
<div className="grid
|
||||
grid-cols-1 // Mobile: 1 column
|
||||
md:grid-cols-2 // Tablet: 2 columns
|
||||
lg:grid-cols-3 // Desktop: 3 columns
|
||||
gap-6
|
||||
">
|
||||
```
|
||||
|
||||
**Breakpoints:**
|
||||
- `sm:` 640px+
|
||||
- `md:` 768px+
|
||||
- `lg:` 1024px+
|
||||
- `xl:` 1280px+
|
||||
|
||||
---
|
||||
|
||||
## 6. Accessibility
|
||||
|
||||
**Always include:**
|
||||
|
||||
```tsx
|
||||
// Labels for inputs
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" />
|
||||
|
||||
// ARIA for errors
|
||||
<Input
|
||||
aria-invalid={!!errors.email}
|
||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p id="email-error" className="text-sm text-destructive">
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
// ARIA labels for icon-only buttons
|
||||
<Button size="icon" aria-label="Close dialog">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
**[Complete accessibility guide](./07-accessibility.md)**
|
||||
|
||||
---
|
||||
|
||||
## 7. Common Patterns Cheat Sheet
|
||||
|
||||
### Loading State
|
||||
|
||||
```tsx
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-12 w-full" />
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### Dropdown Menu
|
||||
|
||||
```tsx
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Options</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
```
|
||||
|
||||
### Badge/Tag
|
||||
|
||||
```tsx
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
<Badge>New</Badge>
|
||||
<Badge variant="destructive">Urgent</Badge>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Next Steps
|
||||
|
||||
You now know enough to build most interfaces! For deeper knowledge:
|
||||
|
||||
### Learn More
|
||||
- **Components**: [Complete component guide](./02-components.md)
|
||||
- **Layouts**: [Layout patterns](./03-layouts.md)
|
||||
- **Forms**: [Form patterns & validation](./06-forms.md)
|
||||
- **Custom Components**: [Component creation guide](./05-component-creation.md)
|
||||
|
||||
### Interactive Examples
|
||||
- **[Component Showcase](/dev/components)** - All components with code
|
||||
- **[Layout Examples](/dev/layouts)** - Before/after comparisons
|
||||
- **[Form Examples](/dev/forms)** - Complete form implementations
|
||||
|
||||
### Reference
|
||||
- **[Quick Reference Tables](./99-reference.md)** - Bookmark this for lookups
|
||||
- **[Foundations](./01-foundations.md)** - Complete color/spacing/typography guide
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Golden Rules
|
||||
|
||||
Remember these and you'll be 95% compliant:
|
||||
|
||||
1. ✅ **Import from `@/components/ui/*`**
|
||||
2. ✅ **Use semantic colors**: `bg-primary`, not `bg-blue-500`
|
||||
3. ✅ **Use spacing scale**: 4, 8, 12, 16, 24, 32 (multiples of 4)
|
||||
4. ✅ **Use `cn()` for className merging**: `cn("base", conditional && "extra", className)`
|
||||
5. ✅ **Add accessibility**: Labels, ARIA, keyboard support
|
||||
6. ✅ **Test in dark mode**: Toggle with theme switcher
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Start Building!
|
||||
|
||||
You're ready to build. When you hit edge cases or need advanced patterns, refer back to the [full documentation](./README.md).
|
||||
|
||||
**Bookmark these:**
|
||||
- [Quick Reference](./99-reference.md) - For quick lookups
|
||||
- [AI Guidelines](./08-ai-guidelines.md) - If using AI assistants
|
||||
- [Component Showcase](/dev/components) - For copy-paste examples
|
||||
|
||||
Happy coding! 🎨
|
||||
909
frontend/docs/design-system/01-foundations.md
Normal file
909
frontend/docs/design-system/01-foundations.md
Normal file
@@ -0,0 +1,909 @@
|
||||
# Foundations
|
||||
|
||||
**The building blocks of our design system**: OKLCH colors, typography scale, spacing tokens, shadows, and border radius. Master these fundamentals to build consistent, accessible interfaces.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Technology Stack](#technology-stack)
|
||||
2. [Color System (OKLCH)](#color-system-oklch)
|
||||
3. [Typography](#typography)
|
||||
4. [Spacing Scale](#spacing-scale)
|
||||
5. [Shadows](#shadows)
|
||||
6. [Border Radius](#border-radius)
|
||||
7. [Quick Reference](#quick-reference)
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Technologies
|
||||
|
||||
- **Framework**: Next.js 15 + React 19
|
||||
- **Styling**: Tailwind CSS 4 (CSS-first configuration)
|
||||
- **Components**: shadcn/ui (New York style)
|
||||
- **Color Space**: OKLCH (perceptually uniform)
|
||||
- **Icons**: lucide-react
|
||||
- **Fonts**: Geist Sans + Geist Mono
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **🎨 Semantic First** - Use `bg-primary`, not `bg-blue-500`
|
||||
2. **♿ Accessible by Default** - WCAG AA compliance minimum (4.5:1 contrast)
|
||||
3. **📐 Consistent Spacing** - Multiples of 4px (0.25rem base unit)
|
||||
4. **🧩 Compose, Don't Create** - Use shadcn/ui primitives
|
||||
5. **🌗 Dark Mode Ready** - All components work in light/dark
|
||||
6. **⚡ Pareto Efficient** - 80% of needs with 20% of patterns
|
||||
|
||||
---
|
||||
|
||||
## Color System (OKLCH)
|
||||
|
||||
### Why OKLCH?
|
||||
|
||||
We use **OKLCH** (Oklab LCH) color space for:
|
||||
- ✅ **Perceptual uniformity** - Colors look consistent across light/dark modes
|
||||
- ✅ **Better accessibility** - Predictable contrast ratios
|
||||
- ✅ **Vibrant colors** - More saturated without sacrificing legibility
|
||||
- ✅ **Future-proof** - CSS native support (vs HSL/RGB)
|
||||
|
||||
**Learn more**: [oklch.com](https://oklch.com)
|
||||
|
||||
---
|
||||
|
||||
### Semantic Color Tokens
|
||||
|
||||
All colors follow the **background/foreground** convention:
|
||||
- `background` - The background color
|
||||
- `foreground` - The text color that goes on that background
|
||||
|
||||
**This ensures accessible contrast automatically.**
|
||||
|
||||
---
|
||||
|
||||
### Primary Colors
|
||||
|
||||
**Purpose**: Main brand color, CTAs, primary actions
|
||||
|
||||
```css
|
||||
/* Light & Dark Mode */
|
||||
--primary: oklch(0.6231 0.1880 259.8145) /* Blue */
|
||||
--primary-foreground: oklch(1 0 0) /* White text */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Primary button (most common)
|
||||
<Button>Save Changes</Button>
|
||||
|
||||
// Primary link
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Learn more
|
||||
</a>
|
||||
|
||||
// Primary badge
|
||||
<Badge className="bg-primary text-primary-foreground">New</Badge>
|
||||
```
|
||||
|
||||
**When to use**:
|
||||
- ✅ Call-to-action buttons
|
||||
- ✅ Primary links
|
||||
- ✅ Active states in navigation
|
||||
- ✅ Important badges/tags
|
||||
|
||||
**When NOT to use**:
|
||||
- ❌ Large background areas (too intense)
|
||||
- ❌ Body text (use `text-foreground`)
|
||||
- ❌ Disabled states (use `muted`)
|
||||
|
||||
---
|
||||
|
||||
### Secondary Colors
|
||||
|
||||
**Purpose**: Secondary actions, less prominent UI elements
|
||||
|
||||
```css
|
||||
/* Light Mode */
|
||||
--secondary: oklch(0.9670 0.0029 264.5419) /* Light gray-blue */
|
||||
--secondary-foreground: oklch(0.1529 0 0) /* Dark text */
|
||||
|
||||
/* Dark Mode */
|
||||
--secondary: oklch(0.2686 0 0) /* Dark gray */
|
||||
--secondary-foreground: oklch(0.9823 0 0) /* Light text */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Secondary button
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
|
||||
// Secondary badge
|
||||
<Badge variant="secondary">Draft</Badge>
|
||||
|
||||
// Muted background area
|
||||
<div className="bg-secondary text-secondary-foreground p-4 rounded-lg">
|
||||
Less important information
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Muted Colors
|
||||
|
||||
**Purpose**: Backgrounds for disabled states, subtle UI elements
|
||||
|
||||
```css
|
||||
/* Light Mode */
|
||||
--muted: oklch(0.9846 0.0017 247.8389)
|
||||
--muted-foreground: oklch(0.4667 0.0043 264.4327)
|
||||
|
||||
/* Dark Mode */
|
||||
--muted: oklch(0.2393 0 0)
|
||||
--muted-foreground: oklch(0.6588 0.0043 264.4327)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Disabled button
|
||||
<Button disabled>Submit</Button>
|
||||
|
||||
// Secondary/helper text
|
||||
<p className="text-muted-foreground text-sm">
|
||||
This action cannot be undone
|
||||
</p>
|
||||
|
||||
// Skeleton loader
|
||||
<Skeleton className="h-12 w-full" />
|
||||
|
||||
// TabsList background
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList className="bg-muted">
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
**Common use cases**:
|
||||
- Disabled button backgrounds
|
||||
- Placeholder/skeleton loaders
|
||||
- TabsList backgrounds
|
||||
- Switch backgrounds (unchecked state)
|
||||
- Helper text, captions, timestamps
|
||||
|
||||
---
|
||||
|
||||
### Accent Colors
|
||||
|
||||
**Purpose**: Hover states, focus indicators, highlights
|
||||
|
||||
```css
|
||||
/* Light Mode */
|
||||
--accent: oklch(0.9514 0.0250 236.8242)
|
||||
--accent-foreground: oklch(0.1529 0 0)
|
||||
|
||||
/* Dark Mode */
|
||||
--accent: oklch(0.3791 0.1378 265.5222)
|
||||
--accent-foreground: oklch(0.9823 0 0)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Dropdown menu item hover
|
||||
<DropdownMenu>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="focus:bg-accent focus:text-accent-foreground">
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
// Highlighted section
|
||||
<div className="bg-accent text-accent-foreground p-4 rounded-lg">
|
||||
Featured content
|
||||
</div>
|
||||
```
|
||||
|
||||
**Common use cases**:
|
||||
- Dropdown menu item hover states
|
||||
- Command palette hover states
|
||||
- Highlighted sections
|
||||
- Subtle emphasis backgrounds
|
||||
|
||||
---
|
||||
|
||||
### Destructive Colors
|
||||
|
||||
**Purpose**: Error states, delete actions, warnings
|
||||
|
||||
```css
|
||||
/* Light & Dark Mode */
|
||||
--destructive: oklch(0.6368 0.2078 25.3313) /* Red */
|
||||
--destructive-foreground: oklch(1 0 0) /* White text */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Delete button
|
||||
<Button variant="destructive">Delete Account</Button>
|
||||
|
||||
// Error alert
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
Something went wrong. Please try again.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
// Form error text
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.email?.message}
|
||||
</p>
|
||||
|
||||
// Destructive badge
|
||||
<Badge variant="destructive">Critical</Badge>
|
||||
```
|
||||
|
||||
**When to use**:
|
||||
- ✅ Delete/remove actions
|
||||
- ✅ Error messages
|
||||
- ✅ Validation errors
|
||||
- ✅ Critical warnings
|
||||
|
||||
---
|
||||
|
||||
### Card & Popover Colors
|
||||
|
||||
**Purpose**: Elevated surfaces (cards, popovers, dropdowns)
|
||||
|
||||
```css
|
||||
/* Light Mode */
|
||||
--card: oklch(1.0000 0 0) /* White */
|
||||
--card-foreground: oklch(0.1529 0 0) /* Dark text */
|
||||
--popover: oklch(1.0000 0 0) /* White */
|
||||
--popover-foreground: oklch(0.1529 0 0) /* Dark text */
|
||||
|
||||
/* Dark Mode */
|
||||
--card: oklch(0.2686 0 0) /* Dark gray */
|
||||
--card-foreground: oklch(0.9823 0 0) /* Light text */
|
||||
--popover: oklch(0.2686 0 0) /* Dark gray */
|
||||
--popover-foreground: oklch(0.9823 0 0) /* Light text */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Card (uses card colors by default)
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>Card content</CardContent>
|
||||
</Card>
|
||||
|
||||
// Popover
|
||||
<Popover>
|
||||
<PopoverTrigger>Open</PopoverTrigger>
|
||||
<PopoverContent>Popover content</PopoverContent>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Border & Input Colors
|
||||
|
||||
**Purpose**: Borders, input field borders, dividers
|
||||
|
||||
```css
|
||||
/* Light Mode */
|
||||
--border: oklch(0.9276 0.0058 264.5313)
|
||||
--input: oklch(0.9276 0.0058 264.5313)
|
||||
|
||||
/* Dark Mode */
|
||||
--border: oklch(0.3715 0 0)
|
||||
--input: oklch(0.3715 0 0)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Input border
|
||||
<Input type="email" placeholder="you@example.com" />
|
||||
|
||||
// Card with border
|
||||
<Card className="border">Content</Card>
|
||||
|
||||
// Separator
|
||||
<Separator />
|
||||
|
||||
// Custom border
|
||||
<div className="border-border border-2 rounded-lg p-4">
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Focus Ring
|
||||
|
||||
**Purpose**: Focus indicators for keyboard navigation
|
||||
|
||||
```css
|
||||
/* Light & Dark Mode */
|
||||
--ring: oklch(0.6231 0.1880 259.8145) /* Primary blue */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Button with focus ring (automatic)
|
||||
<Button>Click me</Button>
|
||||
|
||||
// Custom focusable element
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Focusable content
|
||||
</div>
|
||||
```
|
||||
|
||||
**Accessibility note**: Focus rings are critical for keyboard navigation. Never remove them with `outline: none` without providing an alternative.
|
||||
|
||||
---
|
||||
|
||||
### Chart Colors
|
||||
|
||||
**Purpose**: Data visualization with harmonious color palette
|
||||
|
||||
```css
|
||||
--chart-1: oklch(0.6231 0.1880 259.8145) /* Blue */
|
||||
--chart-2: oklch(0.5461 0.2152 262.8809) /* Purple-blue */
|
||||
--chart-3: oklch(0.4882 0.2172 264.3763) /* Deep purple */
|
||||
--chart-4: oklch(0.4244 0.1809 265.6377) /* Violet */
|
||||
--chart-5: oklch(0.3791 0.1378 265.5222) /* Deep violet */
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// In chart components
|
||||
const COLORS = [
|
||||
'hsl(var(--chart-1))',
|
||||
'hsl(var(--chart-2))',
|
||||
'hsl(var(--chart-3))',
|
||||
'hsl(var(--chart-4))',
|
||||
'hsl(var(--chart-5))',
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Color Decision Tree
|
||||
|
||||
```
|
||||
What's the purpose?
|
||||
│
|
||||
├─ Main action/CTA? → PRIMARY
|
||||
├─ Secondary action? → SECONDARY
|
||||
├─ Error/delete? → DESTRUCTIVE
|
||||
├─ Hover state? → ACCENT
|
||||
├─ Disabled/subtle? → MUTED
|
||||
├─ Card/elevated surface? → CARD
|
||||
├─ Border/divider? → BORDER
|
||||
└─ Focus indicator? → RING
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Color Usage Guidelines
|
||||
|
||||
#### ✅ DO
|
||||
|
||||
```tsx
|
||||
// Use semantic tokens
|
||||
<div className="bg-primary text-primary-foreground">CTA</div>
|
||||
<p className="text-destructive">Error message</p>
|
||||
<div className="bg-muted text-muted-foreground">Subtle background</div>
|
||||
|
||||
// Use accent for hover
|
||||
<div className="hover:bg-accent hover:text-accent-foreground">
|
||||
Hover me
|
||||
</div>
|
||||
|
||||
// Test contrast
|
||||
// Primary on white: 4.5:1 ✅
|
||||
// Destructive on white: 4.5:1 ✅
|
||||
```
|
||||
|
||||
#### ❌ DON'T
|
||||
|
||||
```tsx
|
||||
// Don't use arbitrary colors
|
||||
<div className="bg-blue-500 text-white">Bad</div>
|
||||
|
||||
// Don't mix color spaces
|
||||
<div className="bg-primary text-[#ff0000]">Bad</div>
|
||||
|
||||
// Don't use primary for large areas
|
||||
<div className="min-h-screen bg-primary">Too intense</div>
|
||||
|
||||
// Don't override foreground without checking contrast
|
||||
<div className="bg-primary text-gray-300">Low contrast!</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Families
|
||||
|
||||
```css
|
||||
--font-sans: Geist Sans, system-ui, -apple-system, sans-serif
|
||||
--font-mono: Geist Mono, ui-monospace, monospace
|
||||
--font-serif: ui-serif, Georgia, serif
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Sans serif (default)
|
||||
<div className="font-sans">Body text</div>
|
||||
|
||||
// Monospace (code)
|
||||
<code className="font-mono">const example = true;</code>
|
||||
|
||||
// Serif (rarely used)
|
||||
<blockquote className="font-serif italic">Quote</blockquote>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Size | Class | rem | px | Use Case |
|
||||
|------|-------|-----|----|----|
|
||||
| 9xl | `text-9xl` | 8rem | 128px | Hero text (rare) |
|
||||
| 8xl | `text-8xl` | 6rem | 96px | Hero text (rare) |
|
||||
| 7xl | `text-7xl` | 4.5rem | 72px | Hero text (rare) |
|
||||
| 6xl | `text-6xl` | 3.75rem | 60px | Hero text (rare) |
|
||||
| 5xl | `text-5xl` | 3rem | 48px | Landing page H1 |
|
||||
| 4xl | `text-4xl` | 2.25rem | 36px | Page H1 |
|
||||
| 3xl | `text-3xl` | 1.875rem | 30px | **Page titles** |
|
||||
| 2xl | `text-2xl` | 1.5rem | 24px | **Section headings** |
|
||||
| xl | `text-xl` | 1.25rem | 20px | **Card titles** |
|
||||
| lg | `text-lg` | 1.125rem | 18px | **Subheadings** |
|
||||
| base | `text-base` | 1rem | 16px | **Body text (default)** |
|
||||
| sm | `text-sm` | 0.875rem | 14px | **Secondary text, captions** |
|
||||
| xs | `text-xs` | 0.75rem | 12px | **Labels, helper text** |
|
||||
|
||||
**Bold = most commonly used**
|
||||
|
||||
---
|
||||
|
||||
### Font Weights
|
||||
|
||||
| Weight | Class | Numeric | Use Case |
|
||||
|--------|-------|---------|----------|
|
||||
| Bold | `font-bold` | 700 | **Headings, emphasis** |
|
||||
| Semibold | `font-semibold` | 600 | **Subheadings, buttons** |
|
||||
| Medium | `font-medium` | 500 | **Labels, menu items** |
|
||||
| Normal | `font-normal` | 400 | **Body text (default)** |
|
||||
| Light | `font-light` | 300 | De-emphasized text |
|
||||
|
||||
**Bold = most commonly used**
|
||||
|
||||
---
|
||||
|
||||
### Typography Patterns
|
||||
|
||||
#### Page Title
|
||||
```tsx
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
```
|
||||
|
||||
#### Section Heading
|
||||
```tsx
|
||||
<h2 className="text-2xl font-semibold mb-4">Section Heading</h2>
|
||||
```
|
||||
|
||||
#### Card Title
|
||||
```tsx
|
||||
<CardTitle className="text-xl font-semibold">Card Title</CardTitle>
|
||||
```
|
||||
|
||||
#### Body Text
|
||||
```tsx
|
||||
<p className="text-base text-foreground">
|
||||
Regular paragraph text uses the default text-base size.
|
||||
</p>
|
||||
```
|
||||
|
||||
#### Secondary Text
|
||||
```tsx
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Helper text, timestamps, captions
|
||||
</p>
|
||||
```
|
||||
|
||||
#### Label
|
||||
```tsx
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email Address
|
||||
</Label>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Line Height
|
||||
|
||||
| Class | Value | Use Case |
|
||||
|-------|-------|----------|
|
||||
| `leading-none` | 1 | Headings (rare) |
|
||||
| `leading-tight` | 1.25 | **Headings** |
|
||||
| `leading-snug` | 1.375 | Dense text |
|
||||
| `leading-normal` | 1.5 | **Body text (default)** |
|
||||
| `leading-relaxed` | 1.625 | Comfortable reading |
|
||||
| `leading-loose` | 2 | Very relaxed (rare) |
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Heading
|
||||
<h1 className="text-3xl font-bold leading-tight">
|
||||
Tight line height for headings
|
||||
</h1>
|
||||
|
||||
// Body (default)
|
||||
<p className="leading-normal">
|
||||
Normal line height for readability
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Typography Guidelines
|
||||
|
||||
#### ✅ DO
|
||||
|
||||
```tsx
|
||||
// Use semantic foreground colors
|
||||
<p className="text-foreground">Body text</p>
|
||||
<p className="text-muted-foreground">Secondary text</p>
|
||||
|
||||
// Maintain heading hierarchy
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
<h2 className="text-2xl font-semibold">Section</h2>
|
||||
<h3 className="text-xl font-semibold">Subsection</h3>
|
||||
|
||||
// Limit line length for readability
|
||||
<article className="max-w-2xl mx-auto">
|
||||
<p>60-80 characters per line is optimal</p>
|
||||
</article>
|
||||
|
||||
// Use responsive type sizes
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
|
||||
Responsive Title
|
||||
</h1>
|
||||
```
|
||||
|
||||
#### ❌ DON'T
|
||||
|
||||
```tsx
|
||||
// Don't use too many sizes on one page
|
||||
<p className="text-xs">Too small</p>
|
||||
<p className="text-sm">Still small</p>
|
||||
<p className="text-base">Base</p>
|
||||
<p className="text-lg">Large</p>
|
||||
<p className="text-xl">Larger</p>
|
||||
// ^ Pick 2-3 sizes max
|
||||
|
||||
// Don't skip heading levels
|
||||
<h1>Page</h1>
|
||||
<h3>Section</h3> // ❌ Skipped h2
|
||||
|
||||
// Don't use custom colors without contrast check
|
||||
<p className="text-blue-300">Low contrast</p>
|
||||
|
||||
// Don't overuse bold
|
||||
<p className="font-bold">
|
||||
<span className="font-bold">Every</span>
|
||||
<span className="font-bold">word</span>
|
||||
<span className="font-bold">bold</span>
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Tailwind uses a **0.25rem (4px) base unit**:
|
||||
|
||||
```css
|
||||
--spacing: 0.25rem;
|
||||
```
|
||||
|
||||
**All spacing should be multiples of 4px** for consistency.
|
||||
|
||||
### Spacing Tokens
|
||||
|
||||
| Token | rem | Pixels | Use Case |
|
||||
|-------|-----|--------|----------|
|
||||
| `0` | 0 | 0px | No spacing |
|
||||
| `px` | - | 1px | Borders, dividers |
|
||||
| `0.5` | 0.125rem | 2px | Very tight |
|
||||
| `1` | 0.25rem | 4px | Icon gaps |
|
||||
| `2` | 0.5rem | 8px | **Tight spacing** (label → input) |
|
||||
| `3` | 0.75rem | 12px | Component padding |
|
||||
| `4` | 1rem | 16px | **Standard spacing** (form fields) |
|
||||
| `5` | 1.25rem | 20px | Medium spacing |
|
||||
| `6` | 1.5rem | 24px | **Section spacing** (cards) |
|
||||
| `8` | 2rem | 32px | **Large gaps** |
|
||||
| `10` | 2.5rem | 40px | Very large gaps |
|
||||
| `12` | 3rem | 48px | **Section dividers** |
|
||||
| `16` | 4rem | 64px | **Page sections** |
|
||||
| `20` | 5rem | 80px | Extra large |
|
||||
| `24` | 6rem | 96px | Huge spacing |
|
||||
|
||||
**Bold = most commonly used**
|
||||
|
||||
---
|
||||
|
||||
### Container & Max Width
|
||||
|
||||
```tsx
|
||||
// Responsive container with horizontal padding
|
||||
<div className="container mx-auto px-4">
|
||||
Content
|
||||
</div>
|
||||
|
||||
// Constrained width for readability
|
||||
<div className="max-w-2xl mx-auto">
|
||||
Article content
|
||||
</div>
|
||||
```
|
||||
|
||||
### Max Width Scale
|
||||
|
||||
| Class | Pixels | Use Case |
|
||||
|-------|--------|----------|
|
||||
| `max-w-xs` | 320px | Tiny cards |
|
||||
| `max-w-sm` | 384px | Small cards |
|
||||
| `max-w-md` | 448px | **Forms** |
|
||||
| `max-w-lg` | 512px | **Modals** |
|
||||
| `max-w-xl` | 576px | Medium content |
|
||||
| `max-w-2xl` | 672px | **Article content** |
|
||||
| `max-w-3xl` | 768px | Documentation |
|
||||
| `max-w-4xl` | 896px | **Wide layouts** |
|
||||
| `max-w-5xl` | 1024px | Extra wide |
|
||||
| `max-w-6xl` | 1152px | Very wide |
|
||||
| `max-w-7xl` | 1280px | **Full page width** |
|
||||
|
||||
**Bold = most commonly used**
|
||||
|
||||
---
|
||||
|
||||
### Spacing Guidelines
|
||||
|
||||
#### ✅ DO
|
||||
|
||||
```tsx
|
||||
// Use multiples of 4
|
||||
<div className="p-4 space-y-6 mb-8">Content</div>
|
||||
|
||||
// Use gap for flex/grid
|
||||
<div className="flex gap-4">
|
||||
<Button>Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
// Use space-y for stacks
|
||||
<form className="space-y-4">
|
||||
<Input />
|
||||
<Input />
|
||||
</form>
|
||||
|
||||
// Use responsive spacing
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
Responsive padding
|
||||
</div>
|
||||
```
|
||||
|
||||
#### ❌ DON'T
|
||||
|
||||
```tsx
|
||||
// Don't use arbitrary values
|
||||
<div className="p-[13px] mb-[17px]">Bad</div>
|
||||
|
||||
// Don't mix methods inconsistently
|
||||
<div className="space-y-4">
|
||||
<div className="mb-2">Inconsistent</div>
|
||||
<div className="mb-6">Inconsistent</div>
|
||||
</div>
|
||||
|
||||
// Don't forget responsive spacing
|
||||
<div className="p-8">Too much padding on mobile</div>
|
||||
```
|
||||
|
||||
**See [Spacing Philosophy](./04-spacing-philosophy.md) for detailed spacing strategy.**
|
||||
|
||||
---
|
||||
|
||||
## Shadows
|
||||
|
||||
Professional shadow system for depth and elevation:
|
||||
|
||||
```css
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05)
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25)
|
||||
```
|
||||
|
||||
### Shadow Usage
|
||||
|
||||
| Elevation | Class | Use Case |
|
||||
|-----------|-------|----------|
|
||||
| Base | No shadow | Buttons, inline elements |
|
||||
| Low | `shadow-sm` | **Cards, panels** |
|
||||
| Medium | `shadow-md` | **Dropdowns, tooltips** |
|
||||
| High | `shadow-lg` | **Modals, popovers** |
|
||||
| Highest | `shadow-xl` | Notifications, floating elements |
|
||||
| Maximum | `shadow-2xl` | Dialogs (rare) |
|
||||
|
||||
**Usage**:
|
||||
```tsx
|
||||
// Card with subtle shadow
|
||||
<Card className="shadow-sm">Card content</Card>
|
||||
|
||||
// Dropdown with medium shadow
|
||||
<DropdownMenuContent className="shadow-md">
|
||||
Menu items
|
||||
</DropdownMenuContent>
|
||||
|
||||
// Modal with high shadow
|
||||
<DialogContent className="shadow-lg">
|
||||
Modal content
|
||||
</DialogContent>
|
||||
|
||||
// Floating notification
|
||||
<div className="fixed top-4 right-4 shadow-xl rounded-lg p-4">
|
||||
Notification
|
||||
</div>
|
||||
```
|
||||
|
||||
**Dark mode note**: Shadows are less visible in dark mode. Test both modes.
|
||||
|
||||
---
|
||||
|
||||
## Border Radius
|
||||
|
||||
Consistent rounded corners across the application:
|
||||
|
||||
```css
|
||||
--radius: 0.375rem; /* 6px - base */
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px) /* 2px */
|
||||
--radius-md: calc(var(--radius) - 2px) /* 4px */
|
||||
--radius-lg: var(--radius) /* 6px */
|
||||
--radius-xl: calc(var(--radius) + 4px) /* 10px */
|
||||
```
|
||||
|
||||
### Border Radius Scale
|
||||
|
||||
| Token | Class | Pixels | Use Case |
|
||||
|-------|-------|--------|----------|
|
||||
| None | `rounded-none` | 0px | Square elements |
|
||||
| Small | `rounded-sm` | 2px | **Tags, small badges** |
|
||||
| Medium | `rounded-md` | 4px | **Inputs, small buttons** |
|
||||
| Large | `rounded-lg` | 6px | **Cards, buttons (default)** |
|
||||
| XL | `rounded-xl` | 10px | **Large cards, modals** |
|
||||
| 2XL | `rounded-2xl` | 16px | Hero sections |
|
||||
| 3XL | `rounded-3xl` | 24px | Very rounded |
|
||||
| Full | `rounded-full` | 9999px | **Pills, avatars, icon buttons** |
|
||||
|
||||
**Bold = most commonly used**
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```tsx
|
||||
// Button (default)
|
||||
<Button className="rounded-lg">Default Button</Button>
|
||||
|
||||
// Input field
|
||||
<Input className="rounded-md" />
|
||||
|
||||
// Card
|
||||
<Card className="rounded-xl">Large card</Card>
|
||||
|
||||
// Avatar
|
||||
<Avatar className="rounded-full">
|
||||
<AvatarImage src="/avatar.jpg" />
|
||||
</Avatar>
|
||||
|
||||
// Badge/Tag
|
||||
<Badge className="rounded-sm">Small tag</Badge>
|
||||
|
||||
// Pill button
|
||||
<Button className="rounded-full">Pill Button</Button>
|
||||
```
|
||||
|
||||
### Directional Radius
|
||||
|
||||
```tsx
|
||||
// Top corners only
|
||||
<div className="rounded-t-lg">Top rounded</div>
|
||||
|
||||
// Bottom corners only
|
||||
<div className="rounded-b-lg">Bottom rounded</div>
|
||||
|
||||
// Left corners only
|
||||
<div className="rounded-l-lg">Left rounded</div>
|
||||
|
||||
// Right corners only
|
||||
<div className="rounded-r-lg">Right rounded</div>
|
||||
|
||||
// Individual corners
|
||||
<div className="rounded-tl-lg rounded-br-lg">
|
||||
Top-left and bottom-right
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Most Used Tokens
|
||||
|
||||
**Colors**:
|
||||
- `bg-primary text-primary-foreground` - CTAs
|
||||
- `bg-destructive text-destructive-foreground` - Delete/errors
|
||||
- `bg-muted text-muted-foreground` - Disabled/subtle
|
||||
- `text-foreground` - Body text
|
||||
- `text-muted-foreground` - Secondary text
|
||||
- `border-border` - Borders
|
||||
|
||||
**Typography**:
|
||||
- `text-3xl font-bold` - Page titles
|
||||
- `text-2xl font-semibold` - Section headings
|
||||
- `text-xl font-semibold` - Card titles
|
||||
- `text-base` - Body text
|
||||
- `text-sm text-muted-foreground` - Secondary text
|
||||
|
||||
**Spacing**:
|
||||
- `p-4` - Standard padding (16px)
|
||||
- `p-6` - Card padding (24px)
|
||||
- `gap-4` - Standard gap (16px)
|
||||
- `gap-6` - Section gap (24px)
|
||||
- `space-y-4` - Form field spacing (16px)
|
||||
- `space-y-6` - Section spacing (24px)
|
||||
|
||||
**Shadows & Radius**:
|
||||
- `shadow-sm` - Cards
|
||||
- `shadow-md` - Dropdowns
|
||||
- `shadow-lg` - Modals
|
||||
- `rounded-lg` - Buttons, cards (6px)
|
||||
- `rounded-full` - Avatars, pills
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Quick Start**: [5-minute crash course](./00-quick-start.md)
|
||||
- **Components**: [shadcn/ui component guide](./02-components.md)
|
||||
- **Layouts**: [Layout patterns](./03-layouts.md)
|
||||
- **Spacing**: [Spacing philosophy](./04-spacing-philosophy.md)
|
||||
- **Reference**: [Quick lookup tables](./99-reference.md)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
- [Components](./02-components.md) - shadcn/ui library
|
||||
- [Spacing Philosophy](./04-spacing-philosophy.md) - Margin vs padding strategy
|
||||
- [Accessibility](./07-accessibility.md) - WCAG compliance
|
||||
|
||||
**External Resources:**
|
||||
- [OKLCH Color Picker](https://oklch.com)
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
1228
frontend/docs/design-system/02-components.md
Normal file
1228
frontend/docs/design-system/02-components.md
Normal file
File diff suppressed because it is too large
Load Diff
586
frontend/docs/design-system/03-layouts.md
Normal file
586
frontend/docs/design-system/03-layouts.md
Normal file
@@ -0,0 +1,586 @@
|
||||
# Layout Patterns
|
||||
|
||||
**Master the 5 essential layouts** that cover 80% of all interface needs. Learn when to use Grid vs Flex, and build responsive, consistent layouts every time.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Grid vs Flex Decision Tree](#grid-vs-flex-decision-tree)
|
||||
2. [The 5 Essential Patterns](#the-5-essential-patterns)
|
||||
3. [Responsive Strategies](#responsive-strategies)
|
||||
4. [Common Mistakes](#common-mistakes)
|
||||
5. [Advanced Patterns](#advanced-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Grid vs Flex Decision Tree
|
||||
|
||||
Use this flowchart to choose between Grid and Flex:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Need equal-width columns? │
|
||||
│ (e.g., 3 cards of same width) │
|
||||
└──────────┬─YES──────────┬─NO────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
USE GRID Need 2D layout?
|
||||
(rows + columns)
|
||||
│
|
||||
┌────┴────┐
|
||||
│YES │NO
|
||||
▼ ▼
|
||||
USE GRID USE FLEX
|
||||
```
|
||||
|
||||
### Quick Rules
|
||||
|
||||
| Scenario | Solution |
|
||||
|----------|----------|
|
||||
| **Equal-width columns** | Grid (`grid grid-cols-3`) |
|
||||
| **Flexible item sizes** | Flex (`flex gap-4`) |
|
||||
| **2D layout (rows + cols)** | Grid (`grid grid-cols-2 grid-rows-3`) |
|
||||
| **1D layout (row OR col)** | Flex (`flex` or `flex flex-col`) |
|
||||
| **Card grid** | Grid (`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3`) |
|
||||
| **Navbar items** | Flex (`flex items-center gap-4`) |
|
||||
| **Sidebar + Content** | Flex (`flex gap-6`) |
|
||||
| **Form fields** | Flex column (`flex flex-col gap-4` or `space-y-4`) |
|
||||
|
||||
---
|
||||
|
||||
## The 5 Essential Patterns
|
||||
|
||||
These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
|
||||
---
|
||||
|
||||
### 1. Page Container Pattern
|
||||
|
||||
**Use case**: Standard page layout with readable content width
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Section Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
Page content goes here
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- `container` - Responsive container with max-width
|
||||
- `mx-auto` - Center horizontally
|
||||
- `px-4` - Horizontal padding (mobile-friendly)
|
||||
- `py-8` - Vertical padding
|
||||
- `max-w-4xl` - Constrain content width for readability
|
||||
- `space-y-6` - Vertical spacing between children
|
||||
|
||||
**When to use:**
|
||||
- Blog posts
|
||||
- Documentation pages
|
||||
- Settings pages
|
||||
- Any page with readable content
|
||||
|
||||
**[See live example](/dev/layouts#page-container)**
|
||||
|
||||
---
|
||||
|
||||
### 2. Dashboard Grid Pattern
|
||||
|
||||
**Use case**: Responsive card grid that adapts to screen size
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map(item => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardDescription>{item.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{item.value}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Responsive behavior:**
|
||||
- **Mobile** (`< 768px`): 1 column
|
||||
- **Tablet** (`≥ 768px`): 2 columns
|
||||
- **Desktop** (`≥ 1024px`): 3 columns
|
||||
|
||||
**Key Features:**
|
||||
- `grid` - Use CSS Grid
|
||||
- `grid-cols-1` - Default: 1 column (mobile-first)
|
||||
- `md:grid-cols-2` - 2 columns on tablet
|
||||
- `lg:grid-cols-3` - 3 columns on desktop
|
||||
- `gap-6` - Consistent spacing between items
|
||||
|
||||
**When to use:**
|
||||
- Dashboards
|
||||
- Product grids
|
||||
- Image galleries
|
||||
- Card collections
|
||||
|
||||
**[See live example](/dev/layouts#dashboard-grid)**
|
||||
|
||||
---
|
||||
|
||||
### 3. Form Layout Pattern
|
||||
|
||||
**Use case**: Centered form with constrained width
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to continue</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="you@example.com" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" />
|
||||
</div>
|
||||
|
||||
<Button className="w-full">Sign In</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- `max-w-md` - Constrain form width (448px max)
|
||||
- `mx-auto` - Center the form
|
||||
- `space-y-4` - Vertical spacing between fields
|
||||
- `w-full` - Full-width button
|
||||
|
||||
**Form width guidelines:**
|
||||
- **Short forms** (login, signup): `max-w-md` (448px)
|
||||
- **Medium forms** (profile, settings): `max-w-lg` (512px)
|
||||
- **Long forms** (checkout): `max-w-2xl` (672px)
|
||||
|
||||
**When to use:**
|
||||
- Login/signup forms
|
||||
- Contact forms
|
||||
- Settings forms
|
||||
- Any single-column form
|
||||
|
||||
**[See live example](/dev/layouts#form-layout)**
|
||||
|
||||
---
|
||||
|
||||
### 4. Sidebar Layout Pattern
|
||||
|
||||
**Use case**: Sidebar navigation with main content area
|
||||
|
||||
```tsx
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 border-r bg-muted/40 p-6">
|
||||
<nav className="space-y-2">
|
||||
<a href="#" className="block rounded-lg px-3 py-2 text-sm hover:bg-accent">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="#" className="block rounded-lg px-3 py-2 text-sm hover:bg-accent">
|
||||
Settings
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-6">Page Title</h1>
|
||||
{/* Content */}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- `flex` - Horizontal layout
|
||||
- `w-64` - Fixed sidebar width (256px)
|
||||
- `flex-1` - Main content takes remaining space
|
||||
- `min-h-screen` - Full viewport height
|
||||
- `border-r` - Visual separator
|
||||
|
||||
**Responsive strategy:**
|
||||
|
||||
```tsx
|
||||
// Mobile: Collapsible sidebar
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar - hidden on mobile */}
|
||||
<aside className="hidden lg:block w-64 border-r p-6">
|
||||
{/* Sidebar content */}
|
||||
</aside>
|
||||
|
||||
{/* Main content - full width on mobile */}
|
||||
<main className="flex-1 p-4 lg:p-6">
|
||||
{/* Content */}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
// Add mobile menu button
|
||||
<Button size="icon" className="lg:hidden">
|
||||
<Menu className="h-6 w-6" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Admin dashboards
|
||||
- Settings pages
|
||||
- Documentation sites
|
||||
- Apps with persistent navigation
|
||||
|
||||
**[See live example](/dev/layouts#sidebar-layout)**
|
||||
|
||||
---
|
||||
|
||||
### 5. Centered Content Pattern
|
||||
|
||||
**Use case**: Single-column content with optimal reading width
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<article className="max-w-2xl mx-auto">
|
||||
<h1 className="text-4xl font-bold mb-4">Article Title</h1>
|
||||
<p className="text-muted-foreground mb-8">Published on Nov 2, 2025</p>
|
||||
|
||||
<div className="prose prose-lg">
|
||||
<p>Article content with optimal line length for reading...</p>
|
||||
<p>More content...</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- `max-w-2xl` - Optimal reading width (672px)
|
||||
- `mx-auto` - Center content
|
||||
- `prose` - Typography styles (if using @tailwindcss/typography)
|
||||
|
||||
**Width recommendations:**
|
||||
- **Articles/Blogs**: `max-w-2xl` (672px)
|
||||
- **Documentation**: `max-w-3xl` (768px)
|
||||
- **Landing pages**: `max-w-4xl` (896px) or wider
|
||||
- **Forms**: `max-w-md` (448px)
|
||||
|
||||
**When to use:**
|
||||
- Blog posts
|
||||
- Articles
|
||||
- Documentation
|
||||
- Long-form content
|
||||
|
||||
**[See live example](/dev/layouts#centered-content)**
|
||||
|
||||
---
|
||||
|
||||
## Responsive Strategies
|
||||
|
||||
### Mobile-First Approach
|
||||
|
||||
Always start with mobile layout, then enhance for larger screens:
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Mobile first
|
||||
<div className="
|
||||
p-4 // Mobile: 16px padding
|
||||
sm:p-6 // Tablet: 24px padding
|
||||
lg:p-8 // Desktop: 32px padding
|
||||
">
|
||||
<div className="
|
||||
grid
|
||||
grid-cols-1 // Mobile: 1 column
|
||||
sm:grid-cols-2 // Tablet: 2 columns
|
||||
lg:grid-cols-3 // Desktop: 3 columns
|
||||
gap-4
|
||||
">
|
||||
{/* Items */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// ❌ WRONG - Desktop first
|
||||
<div className="p-8 md:p-6 sm:p-4"> // Don't do this
|
||||
```
|
||||
|
||||
### Breakpoints
|
||||
|
||||
| Breakpoint | Min Width | Typical Use |
|
||||
|------------|-----------|-------------|
|
||||
| `sm:` | 640px | Large phones, small tablets |
|
||||
| `md:` | 768px | Tablets |
|
||||
| `lg:` | 1024px | Laptops, desktops |
|
||||
| `xl:` | 1280px | Large desktops |
|
||||
| `2xl:` | 1536px | Extra large screens |
|
||||
|
||||
### Responsive Grid Columns
|
||||
|
||||
```tsx
|
||||
// 1→2→3→4 progression (common)
|
||||
grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4
|
||||
|
||||
// 1→2→3 progression (most common)
|
||||
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
|
||||
|
||||
// 1→2 progression (simple)
|
||||
grid-cols-1 md:grid-cols-2
|
||||
|
||||
// 1→3 progression (skip 2)
|
||||
grid-cols-1 lg:grid-cols-3
|
||||
```
|
||||
|
||||
### Responsive Text
|
||||
|
||||
```tsx
|
||||
// Heading sizes
|
||||
<h1 className="
|
||||
text-2xl sm:text-3xl lg:text-4xl
|
||||
font-bold
|
||||
">
|
||||
Responsive Title
|
||||
</h1>
|
||||
|
||||
// Body text (usually doesn't need responsive sizes)
|
||||
<p className="text-base">
|
||||
Body text stays consistent
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Mistake 1: Using Margins Instead of Gap
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Children have margins
|
||||
<div className="flex">
|
||||
<div className="mr-4">Item 1</div>
|
||||
<div className="mr-4">Item 2</div>
|
||||
<div>Item 3</div> {/* Last one has no margin */}
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Parent controls spacing
|
||||
<div className="flex gap-4">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
<div>Item 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### ❌ Mistake 2: Fixed Widths Instead of Responsive
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Fixed width, not responsive
|
||||
<div className="w-[800px]">
|
||||
Content
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Responsive width
|
||||
<div className="w-full max-w-4xl mx-auto px-4">
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
### ❌ Mistake 3: Not Using Container
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Content touches edges on large screens
|
||||
<div className="px-4">
|
||||
Content spans full width on 4K screens
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Container constrains width
|
||||
<div className="container mx-auto px-4">
|
||||
Content has maximum width
|
||||
</div>
|
||||
```
|
||||
|
||||
### ❌ Mistake 4: Desktop-First Responsive
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Desktop first
|
||||
<div className="p-8 lg:p-6 md:p-4">
|
||||
|
||||
// ✅ CORRECT - Mobile first
|
||||
<div className="p-4 md:p-6 lg:p-8">
|
||||
```
|
||||
|
||||
### ❌ Mistake 5: Using Flex for Equal Columns
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Flex doesn't guarantee equal widths
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">Col 1</div>
|
||||
<div className="flex-1">Col 2</div>
|
||||
<div className="flex-1">Col 3</div>
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Grid ensures equal widths
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>Col 1</div>
|
||||
<div>Col 2</div>
|
||||
<div>Col 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**[See before/after examples](/dev/layouts)**
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Asymmetric Grid
|
||||
|
||||
```tsx
|
||||
// 2/3 - 1/3 split
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="col-span-2">
|
||||
Main content (2/3 width)
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
Sidebar (1/3 width)
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Auto-fit Grid (Flexible columns)
|
||||
|
||||
```tsx
|
||||
// Columns adjust based on available space
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-6">
|
||||
<Card>Item 1</Card>
|
||||
<Card>Item 2</Card>
|
||||
<Card>Item 3</Card>
|
||||
{/* Adds as many columns as fit */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Sticky Sidebar
|
||||
|
||||
```tsx
|
||||
<div className="flex gap-6">
|
||||
<aside className="sticky top-6 h-fit w-64">
|
||||
{/* Stays in view while scrolling */}
|
||||
</aside>
|
||||
<main className="flex-1">
|
||||
{/* Scrollable content */}
|
||||
</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Full-height Layout
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<header className="h-16 border-b">Header</header>
|
||||
<main className="flex-1">Flexible content</main>
|
||||
<footer className="h-16 border-t">Footer</footer>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout Checklist
|
||||
|
||||
Before implementing a layout, ask:
|
||||
|
||||
- [ ] **Responsive?** Does it work on mobile, tablet, desktop?
|
||||
- [ ] **Container?** Is content constrained on large screens?
|
||||
- [ ] **Spacing?** Using `gap` or `space-y`, not margins on children?
|
||||
- [ ] **Mobile-first?** Starting with mobile layout?
|
||||
- [ ] **Semantic?** Using appropriate HTML tags (main, aside, nav)?
|
||||
- [ ] **Accessible?** Proper heading hierarchy, skip links?
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Grid Cheat Sheet
|
||||
|
||||
```tsx
|
||||
// Basic grid
|
||||
grid grid-cols-3 gap-6
|
||||
|
||||
// Responsive grid
|
||||
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6
|
||||
|
||||
// Asymmetric grid
|
||||
grid grid-cols-3 gap-6
|
||||
<div className="col-span-2">...</div>
|
||||
|
||||
// Auto-fit grid
|
||||
grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-6
|
||||
```
|
||||
|
||||
### Flex Cheat Sheet
|
||||
|
||||
```tsx
|
||||
// Horizontal flex
|
||||
flex gap-4
|
||||
|
||||
// Vertical flex
|
||||
flex flex-col gap-4
|
||||
|
||||
// Center items
|
||||
flex items-center justify-center
|
||||
|
||||
// Space between
|
||||
flex items-center justify-between
|
||||
|
||||
// Wrap items
|
||||
flex flex-wrap gap-4
|
||||
```
|
||||
|
||||
### Container Cheat Sheet
|
||||
|
||||
```tsx
|
||||
// Standard container
|
||||
container mx-auto px-4 py-8
|
||||
|
||||
// Constrained width
|
||||
max-w-4xl mx-auto px-4
|
||||
|
||||
// Full width
|
||||
w-full px-4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Practice**: Build pages using the 5 essential patterns
|
||||
- **Explore**: [Interactive layout examples](/dev/layouts)
|
||||
- **Deep Dive**: [Spacing Philosophy](./04-spacing-philosophy.md)
|
||||
- **Reference**: [Quick Reference Tables](./99-reference.md)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Spacing Philosophy](./04-spacing-philosophy.md) - When to use margin vs padding vs gap
|
||||
- [Foundations](./01-foundations.md) - Spacing tokens and scale
|
||||
- [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
708
frontend/docs/design-system/04-spacing-philosophy.md
Normal file
708
frontend/docs/design-system/04-spacing-philosophy.md
Normal file
@@ -0,0 +1,708 @@
|
||||
# Spacing Philosophy
|
||||
|
||||
**Master the "parent controls children" spacing strategy** that eliminates 90% of layout inconsistencies. Learn when to use margin, padding, or gap—and why children should never add their own margins.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [The Golden Rules](#the-golden-rules)
|
||||
2. [Parent Controls Children Strategy](#parent-controls-children-strategy)
|
||||
3. [Decision Tree: Margin vs Padding vs Gap](#decision-tree-margin-vs-padding-vs-gap)
|
||||
4. [Common Patterns](#common-patterns)
|
||||
5. [Before/After Examples](#beforeafter-examples)
|
||||
6. [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
|
||||
7. [Quick Reference](#quick-reference)
|
||||
|
||||
---
|
||||
|
||||
## The Golden Rules
|
||||
|
||||
These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
|
||||
### Rule 1: Parent Controls Children
|
||||
**Children don't add their own margins. The parent controls spacing between siblings.**
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Parent controls spacing
|
||||
<div className="space-y-4">
|
||||
<Card>Item 1</Card>
|
||||
<Card>Item 2</Card>
|
||||
<Card>Item 3</Card>
|
||||
</div>
|
||||
|
||||
// ❌ WRONG - Children add margins
|
||||
<div>
|
||||
<Card className="mb-4">Item 1</Card>
|
||||
<Card className="mb-4">Item 2</Card>
|
||||
<Card>Item 3</Card> {/* Inconsistent: last one has no margin */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Eliminates "last child" edge cases
|
||||
- Makes components reusable (they work in any context)
|
||||
- Changes propagate from one place (parent)
|
||||
- Prevents margin collapsing bugs
|
||||
|
||||
---
|
||||
|
||||
### Rule 2: Use Gap for Siblings
|
||||
**For flex and grid layouts, use `gap-*` to space siblings.**
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Gap for flex/grid
|
||||
<div className="flex gap-4">
|
||||
<Button>Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<Card>1</Card>
|
||||
<Card>2</Card>
|
||||
<Card>3</Card>
|
||||
</div>
|
||||
|
||||
// ❌ WRONG - Children with margins
|
||||
<div className="flex">
|
||||
<Button className="mr-4">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rule 3: Use Padding for Internal Spacing
|
||||
**Padding is for spacing _inside_ a component, between the border and content.**
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Padding for internal spacing
|
||||
<Card className="p-6">
|
||||
<CardTitle>Title</CardTitle>
|
||||
<CardContent>Content</CardContent>
|
||||
</Card>
|
||||
|
||||
// ❌ WRONG - Using margin for internal spacing
|
||||
<Card>
|
||||
<CardTitle className="m-6">Title</CardTitle>
|
||||
</Card>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rule 4: Use space-y for Vertical Stacks
|
||||
**For vertical stacks (not flex/grid), use `space-y-*` utility.**
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - space-y for stacks
|
||||
<form className="space-y-4">
|
||||
<Input />
|
||||
<Input />
|
||||
<Button />
|
||||
</form>
|
||||
|
||||
// ❌ WRONG - Children with margins
|
||||
<form>
|
||||
<Input className="mb-4" />
|
||||
<Input className="mb-4" />
|
||||
<Button />
|
||||
</form>
|
||||
```
|
||||
|
||||
**How space-y works:**
|
||||
```css
|
||||
/* space-y-4 applies margin-top to all children except first */
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem; /* 16px */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rule 5: Margins Only for Exceptions
|
||||
**Use margin only when a specific child needs different spacing from its siblings.**
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Margin for exception
|
||||
<div className="space-y-4">
|
||||
<Card>Normal spacing</Card>
|
||||
<Card>Normal spacing</Card>
|
||||
<Card className="mt-8">Extra spacing above this one</Card>
|
||||
<Card>Normal spacing</Card>
|
||||
</div>
|
||||
|
||||
// Use case: Visually group related items
|
||||
<div className="space-y-4">
|
||||
<h2>Section 1</h2>
|
||||
<p>Content</p>
|
||||
<h2 className="mt-12">Section 2</h2> {/* Extra margin to separate sections */}
|
||||
<p>Content</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parent Controls Children Strategy
|
||||
|
||||
### The Problem with Child-Controlled Spacing
|
||||
|
||||
When children control their own margins:
|
||||
|
||||
```tsx
|
||||
// ❌ ANTI-PATTERN
|
||||
function TodoItem({ className }: { className?: string }) {
|
||||
return <div className={cn("mb-4", className)}>Todo</div>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<div>
|
||||
<TodoItem /> {/* Has mb-4 */}
|
||||
<TodoItem /> {/* Has mb-4 */}
|
||||
<TodoItem /> {/* Has mb-4 - unwanted margin at bottom! */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
1. ❌ Last item has unwanted margin
|
||||
2. ❌ Can't change spacing without modifying component
|
||||
3. ❌ Margin collapsing creates unpredictable spacing
|
||||
4. ❌ Component not reusable in different contexts
|
||||
|
||||
---
|
||||
|
||||
### The Solution: Parent-Controlled Spacing
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT PATTERN
|
||||
function TodoItem({ className }: { className?: string }) {
|
||||
return <div className={className}>Todo</div>; // No margin!
|
||||
}
|
||||
|
||||
// Parent controls spacing
|
||||
<div className="space-y-4">
|
||||
<TodoItem />
|
||||
<TodoItem />
|
||||
<TodoItem /> {/* No unwanted margin */}
|
||||
</div>
|
||||
|
||||
// Different context, different spacing
|
||||
<div className="space-y-2">
|
||||
<TodoItem />
|
||||
<TodoItem />
|
||||
</div>
|
||||
|
||||
// Another context, flex layout
|
||||
<div className="flex gap-6">
|
||||
<TodoItem />
|
||||
<TodoItem />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
1. ✅ No edge cases (last child, first child, only child)
|
||||
2. ✅ Spacing controlled in one place
|
||||
3. ✅ Component works in any layout context
|
||||
4. ✅ No margin collapsing surprises
|
||||
5. ✅ Easier to maintain and modify
|
||||
|
||||
---
|
||||
|
||||
## Decision Tree: Margin vs Padding vs Gap
|
||||
|
||||
Use this flowchart to choose the right spacing method:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ What are you spacing? │
|
||||
└─────────────┬───────────────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
Siblings? Inside a component?
|
||||
│ │
|
||||
│ └──> USE PADDING
|
||||
│ className="p-4"
|
||||
│
|
||||
├──> Is parent using flex or grid?
|
||||
│ │
|
||||
│ ├─YES──> USE GAP
|
||||
│ │ className="flex gap-4"
|
||||
│ │ className="grid gap-6"
|
||||
│ │
|
||||
│ └─NO───> USE SPACE-Y or SPACE-X
|
||||
│ className="space-y-4"
|
||||
│ className="space-x-2"
|
||||
│
|
||||
└──> Exception case?
|
||||
(One child needs different spacing)
|
||||
│
|
||||
└──> USE MARGIN
|
||||
className="mt-8"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Form Fields (Vertical Stack)
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" />
|
||||
</div>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
**Spacing breakdown:**
|
||||
- `space-y-4` on form: 16px between field groups
|
||||
- `space-y-2` on field group: 8px between label and input
|
||||
- No margins on children
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Button Group (Horizontal Flex)
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
// Responsive: stack on mobile, row on desktop
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Why gap over space-x:**
|
||||
- Works with `flex-wrap`
|
||||
- Works with `flex-col` (changes direction)
|
||||
- Consistent spacing in all directions
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Card Grid
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card>Item 1</Card>
|
||||
<Card>Item 2</Card>
|
||||
<Card>Item 3</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Why gap:**
|
||||
- Consistent spacing between rows and columns
|
||||
- Works with responsive grid changes
|
||||
- No edge cases (first row, last column, etc.)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 4: Card Internal Spacing
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT
|
||||
<Card className="p-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Title</CardTitle>
|
||||
<CardDescription>Description</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p>Paragraph 1</p>
|
||||
<p>Paragraph 2</p>
|
||||
</CardContent>
|
||||
<CardFooter className="pt-4">
|
||||
<Button>Action</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
**Spacing breakdown:**
|
||||
- `p-6` on Card: 24px internal padding
|
||||
- `space-y-4` on CardContent: 16px between paragraphs
|
||||
- `pt-4` on CardFooter: Additional top padding for visual separation
|
||||
|
||||
---
|
||||
|
||||
### Pattern 5: Page Layout
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Section 1</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>Content</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Section 2</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>Content</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Spacing breakdown:**
|
||||
- `px-4`: Horizontal padding (prevents edge touching)
|
||||
- `py-8`: Vertical padding (top and bottom spacing)
|
||||
- `space-y-6`: 24px between sections
|
||||
- No margins on children
|
||||
|
||||
---
|
||||
|
||||
## Before/After Examples
|
||||
|
||||
### Example 1: Button Group
|
||||
|
||||
#### ❌ Before (Child-Controlled)
|
||||
```tsx
|
||||
function ActionButton({ children, className }: Props) {
|
||||
return <Button className={cn("mr-4", className)}>{children}</Button>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<div className="flex">
|
||||
<ActionButton>Cancel</ActionButton>
|
||||
<ActionButton>Save</ActionButton>
|
||||
<ActionButton>Delete</ActionButton> {/* Unwanted mr-4 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Last button has unwanted margin
|
||||
- Can't change spacing without modifying component
|
||||
- Hard to use in vertical layout
|
||||
|
||||
#### ✅ After (Parent-Controlled)
|
||||
```tsx
|
||||
function ActionButton({ children, className }: Props) {
|
||||
return <Button className={className}>{children}</Button>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<div className="flex gap-4">
|
||||
<ActionButton>Cancel</ActionButton>
|
||||
<ActionButton>Save</ActionButton>
|
||||
<ActionButton>Delete</ActionButton>
|
||||
</div>
|
||||
|
||||
// Different context: vertical
|
||||
<div className="flex flex-col gap-2">
|
||||
<ActionButton>Cancel</ActionButton>
|
||||
<ActionButton>Save</ActionButton>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No edge cases
|
||||
- Reusable in any layout
|
||||
- Easy to change spacing
|
||||
|
||||
---
|
||||
|
||||
### Example 2: List Items
|
||||
|
||||
#### ❌ Before (Child-Controlled)
|
||||
```tsx
|
||||
function ListItem({ title, description }: Props) {
|
||||
return (
|
||||
<div className="mb-6 p-4 border rounded">
|
||||
<h3 className="mb-2">{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div>
|
||||
<ListItem title="Item 1" description="..." />
|
||||
<ListItem title="Item 2" description="..." />
|
||||
<ListItem title="Item 3" description="..." /> {/* Unwanted mb-6 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Last item has unwanted bottom margin
|
||||
- Can't change list spacing without modifying component
|
||||
- Internal `mb-2` hard to override
|
||||
|
||||
#### ✅ After (Parent-Controlled)
|
||||
```tsx
|
||||
function ListItem({ title, description }: Props) {
|
||||
return (
|
||||
<div className="p-4 border rounded">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold">{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="space-y-6">
|
||||
<ListItem title="Item 1" description="..." />
|
||||
<ListItem title="Item 2" description="..." />
|
||||
<ListItem title="Item 3" description="..." />
|
||||
</div>
|
||||
|
||||
// Different context: compact spacing
|
||||
<div className="space-y-2">
|
||||
<ListItem title="Item 1" description="..." />
|
||||
<ListItem title="Item 2" description="..." />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No unwanted margins
|
||||
- Internal spacing controlled by `space-y-2`
|
||||
- Reusable with different spacings
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Form Fields
|
||||
|
||||
#### ❌ Before (Mixed Strategy)
|
||||
```tsx
|
||||
<form>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" className="mt-2" />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-6">Submit</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Spacing scattered across children
|
||||
- Hard to change consistently
|
||||
- Have to remember `mt-6` for button
|
||||
|
||||
#### ✅ After (Parent-Controlled)
|
||||
```tsx
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-2">Submit</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Spacing controlled in 2 places: form (`space-y-4`) and field groups (`space-y-2`)
|
||||
- Easy to change all field spacing at once
|
||||
- Consistent and predictable
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Anti-Pattern 1: Last Child Special Case
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
{items.map((item, index) => (
|
||||
<Card key={item.id} className={index < items.length - 1 ? "mb-4" : ""}>
|
||||
{item.name}
|
||||
</Card>
|
||||
))}
|
||||
|
||||
// ✅ CORRECT
|
||||
<div className="space-y-4">
|
||||
{items.map(item => (
|
||||
<Card key={item.id}>{item.name}</Card>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 2: Negative Margins to Fix Spacing
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Using negative margin to fix unwanted spacing
|
||||
<div className="-mt-4"> {/* Canceling out previous margin */}
|
||||
<Card>Content</Card>
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Parent controls spacing
|
||||
<div className="space-y-4">
|
||||
<Card>Content</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Why negative margins are bad:**
|
||||
- Indicates broken spacing strategy
|
||||
- Hard to maintain
|
||||
- Creates coupling between components
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 3: Mixing Gap and Child Margins
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Gap + child margins = unpredictable spacing
|
||||
<div className="flex gap-4">
|
||||
<Button className="mr-2">Cancel</Button> {/* gap + mr-2 = 24px */}
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Only gap
|
||||
<div className="flex gap-4">
|
||||
<Button>Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Exception case
|
||||
<div className="flex gap-4">
|
||||
<Button>Cancel</Button>
|
||||
<Button className="mr-8">Save</Button> {/* Intentional extra space */}
|
||||
<Button>Delete</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 4: Using Margins for Layout
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Using margins to create layout
|
||||
<div>
|
||||
<div className="ml-64"> {/* Pushing content for sidebar */}
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// ✅ CORRECT - Use proper layout (flex/grid)
|
||||
<div className="flex gap-6">
|
||||
<aside className="w-64">Sidebar</aside>
|
||||
<main className="flex-1">Content</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Spacing Method Cheat Sheet
|
||||
|
||||
| Use Case | Method | Example |
|
||||
|----------|--------|---------|
|
||||
| **Flex siblings** | `gap-*` | `flex gap-4` |
|
||||
| **Grid siblings** | `gap-*` | `grid gap-6` |
|
||||
| **Vertical stack** | `space-y-*` | `space-y-4` |
|
||||
| **Horizontal stack** | `space-x-*` | `space-x-2` |
|
||||
| **Inside component** | `p-*` | `p-6` |
|
||||
| **One child exception** | `m-*` | `mt-8` |
|
||||
|
||||
### Common Spacing Values
|
||||
|
||||
| Class | Pixels | Usage |
|
||||
|-------|--------|-------|
|
||||
| `gap-2` or `space-y-2` | 8px | Tight (label + input) |
|
||||
| `gap-4` or `space-y-4` | 16px | Standard (form fields) |
|
||||
| `gap-6` or `space-y-6` | 24px | Sections (cards) |
|
||||
| `gap-8` or `space-y-8` | 32px | Large gaps |
|
||||
| `p-4` | 16px | Standard padding |
|
||||
| `p-6` | 24px | Card padding |
|
||||
| `px-4 py-8` | 16px / 32px | Page padding |
|
||||
|
||||
### Decision Flowchart (Simplified)
|
||||
|
||||
```
|
||||
Need spacing?
|
||||
│
|
||||
├─ Between siblings?
|
||||
│ ├─ Flex/Grid parent? → gap-*
|
||||
│ └─ Regular parent? → space-y-* or space-x-*
|
||||
│
|
||||
├─ Inside component? → p-*
|
||||
│
|
||||
└─ Exception case? → m-* (sparingly)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
### Do ✅
|
||||
|
||||
1. **Use parent-controlled spacing** (`gap`, `space-y`, `space-x`)
|
||||
2. **Use `gap-*` for flex and grid** layouts
|
||||
3. **Use `space-y-*` for vertical stacks** (forms, content)
|
||||
4. **Use `p-*` for internal spacing** (padding inside components)
|
||||
5. **Use margin only for exceptions** (mt-8 to separate sections)
|
||||
6. **Let components be context-agnostic** (no built-in margins)
|
||||
|
||||
### Don't ❌
|
||||
|
||||
1. ❌ Add margins to reusable components
|
||||
2. ❌ Use last-child selectors or conditional margins
|
||||
3. ❌ Mix gap with child margins
|
||||
4. ❌ Use negative margins to fix spacing
|
||||
5. ❌ Use margins for layout (use flex/grid)
|
||||
6. ❌ Hard-code spacing in child components
|
||||
|
||||
---
|
||||
|
||||
## Spacing Checklist
|
||||
|
||||
Before implementing spacing, verify:
|
||||
|
||||
- [ ] **Parent controls children?** Using gap or space-y/x?
|
||||
- [ ] **No child margins?** Components don't have mb-* or mr-*?
|
||||
- [ ] **Consistent method?** Not mixing gap + child margins?
|
||||
- [ ] **Reusable components?** Work in different contexts?
|
||||
- [ ] **No edge cases?** No last-child or first-child special handling?
|
||||
- [ ] **Semantic spacing?** Using design system scale (4, 8, 12, 16...)?
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Practice**: Refactor existing components to use parent-controlled spacing
|
||||
- **Explore**: [Interactive spacing examples](/dev/spacing)
|
||||
- **Reference**: [Quick Reference Tables](./99-reference.md)
|
||||
- **Layout Patterns**: [Layouts Guide](./03-layouts.md)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Layouts](./03-layouts.md) - When to use Grid vs Flex
|
||||
- [Foundations](./01-foundations.md) - Spacing scale tokens
|
||||
- [Component Creation](./05-component-creation.md) - Building reusable components
|
||||
- [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
874
frontend/docs/design-system/05-component-creation.md
Normal file
874
frontend/docs/design-system/05-component-creation.md
Normal file
@@ -0,0 +1,874 @@
|
||||
# Component Creation Guide
|
||||
|
||||
**Learn when to create custom components vs composing existing ones**, and master the patterns for building reusable, accessible components with variants using CVA (class-variance-authority).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [When to Create vs Compose](#when-to-create-vs-compose)
|
||||
2. [Component Templates](#component-templates)
|
||||
3. [Variant Patterns (CVA)](#variant-patterns-cva)
|
||||
4. [Prop Design](#prop-design)
|
||||
5. [Testing Checklist](#testing-checklist)
|
||||
6. [Real-World Examples](#real-world-examples)
|
||||
|
||||
---
|
||||
|
||||
## When to Create vs Compose
|
||||
|
||||
### The Golden Rule
|
||||
|
||||
**80% of the time, you should COMPOSE existing shadcn/ui components.**
|
||||
|
||||
Only create custom components when:
|
||||
1. ✅ You're reusing the same composition 3+ times
|
||||
2. ✅ The pattern has complex business logic
|
||||
3. ✅ You need variants beyond what shadcn/ui provides
|
||||
|
||||
---
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
Do you need a UI element?
|
||||
│
|
||||
├─ Does shadcn/ui have this component?
|
||||
│ │
|
||||
│ ├─YES─> Use it directly
|
||||
│ │ <Button>Action</Button>
|
||||
│ │
|
||||
│ └─NO──> Can you compose multiple shadcn/ui components?
|
||||
│ │
|
||||
│ ├─YES─> Compose them inline first
|
||||
│ │ <Card>
|
||||
│ │ <CardHeader>...</CardHeader>
|
||||
│ │ </Card>
|
||||
│ │
|
||||
│ └─NO──> Are you using this composition 3+ times?
|
||||
│ │
|
||||
│ ├─NO──> Keep composing inline
|
||||
│ │
|
||||
│ └─YES─> Create a custom component
|
||||
│ function MyComponent() { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ GOOD: Compose First
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Compose inline
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{content}</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button onClick={onAction}>{actionLabel}</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
**Why this is good:**
|
||||
- Simple and direct
|
||||
- Easy to customize per use case
|
||||
- No abstraction overhead
|
||||
- Clear what's happening
|
||||
|
||||
---
|
||||
|
||||
### ❌ BAD: Over-Abstracting Too Soon
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Premature abstraction
|
||||
function ContentCard({ title, description, content, actionLabel, onAction }: Props) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{content}</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button onClick={onAction}>{actionLabel}</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Used once... why did we create this?
|
||||
<ContentCard title="..." description="..." content="..." />
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- ❌ Created before knowing if pattern is reused
|
||||
- ❌ Inflexible (what if we need 2 buttons?)
|
||||
- ❌ Unclear what it renders (abstraction hides structure)
|
||||
- ❌ Harder to customize
|
||||
|
||||
---
|
||||
|
||||
### ✅ GOOD: Extract After 3+ Uses
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - After seeing pattern used 3 times, extract
|
||||
function DashboardMetricCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
icon: Icon,
|
||||
}: DashboardMetricCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{change && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{change > 0 ? '+' : ''}{change}% from last month
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Now used in 5+ places
|
||||
<DashboardMetricCard title="Total Revenue" value="$45,231.89" change={20.1} />
|
||||
<DashboardMetricCard title="Subscriptions" value="+2350" change={12.5} />
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- ✅ Pattern validated (used 3+ times)
|
||||
- ✅ Specific purpose (dashboard metrics)
|
||||
- ✅ Consistent structure across uses
|
||||
- ✅ Easy to update all instances
|
||||
|
||||
---
|
||||
|
||||
## Component Templates
|
||||
|
||||
### Template 1: Basic Custom Component
|
||||
|
||||
**Use case**: Simple component with optional className override
|
||||
|
||||
```tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MyComponentProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MyComponent({ className, children }: MyComponentProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"base-classes-here", // Base styles
|
||||
className // Allow overrides
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<MyComponent className="custom-overrides">
|
||||
Content
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Always accept `className` prop
|
||||
- Use `cn()` utility for merging
|
||||
- Base classes first, overrides last
|
||||
|
||||
---
|
||||
|
||||
### Template 2: Component with Variants (CVA)
|
||||
|
||||
**Use case**: Component needs multiple visual variants
|
||||
|
||||
```tsx
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const componentVariants = cva(
|
||||
// Base classes (always applied)
|
||||
"inline-flex items-center justify-center rounded-lg font-medium transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "h-8 px-3 text-xs",
|
||||
default: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-6 text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface MyComponentProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof componentVariants> {
|
||||
// Additional props here
|
||||
}
|
||||
|
||||
export function MyComponent({
|
||||
variant,
|
||||
size,
|
||||
className,
|
||||
...props
|
||||
}: MyComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(componentVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<MyComponent variant="outline" size="lg">Content</MyComponent>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Use CVA for complex variant logic
|
||||
- Always provide `defaultVariants`
|
||||
- Extend `React.HTMLAttributes` for standard HTML props
|
||||
- Spread `...props` to pass through additional attributes
|
||||
|
||||
---
|
||||
|
||||
### Template 3: Composition Component
|
||||
|
||||
**Use case**: Wrap multiple shadcn/ui components with consistent structure
|
||||
|
||||
```tsx
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatCard({
|
||||
title,
|
||||
value,
|
||||
description,
|
||||
icon,
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<StatCard
|
||||
title="Total Users"
|
||||
value="1,234"
|
||||
description="+12% from last month"
|
||||
icon={<Users className="h-4 w-4 text-muted-foreground" />}
|
||||
/>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Compose from shadcn/ui primitives
|
||||
- Keep structure consistent
|
||||
- Optional props with `?`
|
||||
- Descriptive prop names
|
||||
|
||||
---
|
||||
|
||||
### Template 4: Controlled Component
|
||||
|
||||
**Use case**: Component manages state internally but can be controlled
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ToggleProps {
|
||||
value?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
defaultValue?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Toggle({
|
||||
value: controlledValue,
|
||||
onChange,
|
||||
defaultValue = false,
|
||||
children,
|
||||
}: ToggleProps) {
|
||||
// Uncontrolled state
|
||||
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
||||
|
||||
// Use controlled value if provided, otherwise use internal state
|
||||
const value = controlledValue ?? uncontrolledValue;
|
||||
const handleChange = (newValue: boolean) => {
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setUncontrolledValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={() => handleChange(!value)}>
|
||||
{value ? '✓' : '○'} {children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Uncontrolled usage
|
||||
<Toggle defaultValue={false}>Auto-save</Toggle>
|
||||
|
||||
// Controlled usage
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
<Toggle value={enabled} onChange={setEnabled}>Auto-save</Toggle>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Support both controlled and uncontrolled modes
|
||||
- Use `defaultValue` for initial uncontrolled value
|
||||
- Use `value` + `onChange` for controlled mode
|
||||
- Fallback to internal state if not controlled
|
||||
|
||||
---
|
||||
|
||||
## Variant Patterns (CVA)
|
||||
|
||||
### What is CVA?
|
||||
|
||||
**class-variance-authority** (CVA) is a utility for creating component variants with Tailwind CSS.
|
||||
|
||||
**Why use CVA?**
|
||||
- ✅ Type-safe variant props
|
||||
- ✅ Compound variants (combinations)
|
||||
- ✅ Default variants
|
||||
- ✅ Clean, readable syntax
|
||||
|
||||
---
|
||||
|
||||
### Basic Variant Pattern
|
||||
|
||||
```tsx
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
const alertVariants = cva(
|
||||
// Base classes (always applied)
|
||||
"relative w-full rounded-lg border p-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Usage
|
||||
<div className={alertVariants({ variant: "destructive" })}>
|
||||
Alert content
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Multiple Variants
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "h-8 px-3 text-xs",
|
||||
default: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-6 text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Usage
|
||||
<button className={buttonVariants({ variant: "outline", size: "lg" })}>
|
||||
Large Outline Button
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Compound Variants
|
||||
|
||||
**Use case**: Different classes when specific variant combinations are used
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva("base-classes", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary",
|
||||
destructive: "bg-destructive",
|
||||
},
|
||||
size: {
|
||||
sm: "h-8",
|
||||
lg: "h-12",
|
||||
},
|
||||
},
|
||||
// Compound variants: specific combinations
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: "destructive",
|
||||
size: "lg",
|
||||
class: "text-lg font-bold", // Applied when BOTH are true
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "sm",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prop Design
|
||||
|
||||
### Prop Naming Conventions
|
||||
|
||||
**DO**:
|
||||
```tsx
|
||||
// ✅ Descriptive, semantic names
|
||||
interface UserCardProps {
|
||||
user: User;
|
||||
onEdit: () => void;
|
||||
isLoading: boolean;
|
||||
showAvatar?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**DON'T**:
|
||||
```tsx
|
||||
// ❌ Generic, unclear names
|
||||
interface CardProps {
|
||||
data: any;
|
||||
onClick: () => void;
|
||||
loading: boolean;
|
||||
flag?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Required vs Optional Props
|
||||
|
||||
**Guidelines:**
|
||||
- Required: Core functionality depends on it
|
||||
- Optional: Nice-to-have, has sensible default
|
||||
|
||||
```tsx
|
||||
interface AlertProps {
|
||||
// Required: Core to component
|
||||
children: React.ReactNode;
|
||||
|
||||
// Optional: Has default variant
|
||||
variant?: 'default' | 'destructive';
|
||||
|
||||
// Optional: Component works without it
|
||||
onClose?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
|
||||
// Optional: Standard override
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Alert({
|
||||
children,
|
||||
variant = 'default', // Default for optional prop
|
||||
onClose,
|
||||
icon,
|
||||
className,
|
||||
}: AlertProps) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Prop Type Patterns
|
||||
|
||||
**Enum props** (limited options):
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
variant: 'default' | 'destructive' | 'outline';
|
||||
size: 'sm' | 'default' | 'lg';
|
||||
}
|
||||
```
|
||||
|
||||
**Boolean flags**:
|
||||
```tsx
|
||||
interface CardProps {
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
showBorder?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Callback props**:
|
||||
```tsx
|
||||
interface FormProps {
|
||||
onSubmit: (data: FormData) => void;
|
||||
onCancel?: () => void;
|
||||
onChange?: (field: string, value: any) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Render props** (advanced customization):
|
||||
```tsx
|
||||
interface ListProps<T> {
|
||||
items: T[];
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
renderEmpty?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<List
|
||||
items={users}
|
||||
renderItem={(user, i) => <UserCard key={i} user={user} />}
|
||||
renderEmpty={() => <EmptyState />}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Before shipping a custom component, verify:
|
||||
|
||||
### Visual Testing
|
||||
- [ ] **Light mode** - Component looks correct
|
||||
- [ ] **Dark mode** - Component looks correct (toggle theme)
|
||||
- [ ] **All variants** - Test each variant works
|
||||
- [ ] **Responsive** - Mobile, tablet, desktop sizes
|
||||
- [ ] **Loading state** - Shows loading correctly (if applicable)
|
||||
- [ ] **Error state** - Shows errors correctly (if applicable)
|
||||
- [ ] **Empty state** - Handles no data gracefully
|
||||
|
||||
### Accessibility Testing
|
||||
- [ ] **Keyboard navigation** - Can be focused and activated with Tab/Enter
|
||||
- [ ] **Focus indicators** - Visible focus ring (`:focus-visible`)
|
||||
- [ ] **Screen reader** - ARIA labels and roles present
|
||||
- [ ] **Color contrast** - 4.5:1 for text, 3:1 for UI (use contrast checker)
|
||||
- [ ] **Semantic HTML** - Using correct HTML elements (button, nav, etc.)
|
||||
|
||||
### Functional Testing
|
||||
- [ ] **Props work** - All props apply correctly
|
||||
- [ ] **className override** - Can override styles with className prop
|
||||
- [ ] **Controlled/uncontrolled** - Both modes work (if applicable)
|
||||
- [ ] **Event handlers** - onClick, onChange, etc. fire correctly
|
||||
- [ ] **TypeScript** - No type errors, props autocomplete
|
||||
|
||||
### Code Quality
|
||||
- [ ] **No console errors** - Check browser console
|
||||
- [ ] **No warnings** - React warnings, a11y warnings
|
||||
- [ ] **Performance** - No unnecessary re-renders
|
||||
- [ ] **Documentation** - JSDoc comments for complex props
|
||||
|
||||
---
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Stat Card
|
||||
|
||||
**Problem**: Dashboard shows 8 metric cards with same structure.
|
||||
|
||||
**Solution**: Extract composition after 3rd use.
|
||||
|
||||
```tsx
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
change?: number;
|
||||
icon?: LucideIcon;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
icon: Icon,
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{change !== undefined && (
|
||||
<p className={cn(
|
||||
"text-xs",
|
||||
change >= 0 ? "text-green-600" : "text-destructive"
|
||||
)}>
|
||||
{change >= 0 ? '+' : ''}{change}% from last month
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard title="Total Revenue" value="$45,231.89" change={20.1} icon={DollarSign} />
|
||||
<StatCard title="Subscriptions" value="+2350" change={12.5} icon={Users} />
|
||||
<StatCard title="Sales" value="+12,234" change={19} icon={CreditCard} />
|
||||
<StatCard title="Active Now" value="+573" change={-2.1} icon={Activity} />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Specific purpose (dashboard metrics)
|
||||
- Reused 8+ times
|
||||
- Consistent structure
|
||||
- Easy to update all instances
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Confirmation Dialog
|
||||
|
||||
**Problem**: Need to confirm delete actions throughout app.
|
||||
|
||||
**Solution**: Create reusable confirmation dialog wrapper.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: 'default' | 'destructive';
|
||||
onConfirm: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
variant = 'destructive',
|
||||
onConfirm,
|
||||
}: ConfirmDialogProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onConfirm();
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
variant={variant}
|
||||
onClick={handleConfirm}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Processing...' : confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
<ConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user? This action cannot be undone."
|
||||
confirmLabel="Delete"
|
||||
variant="destructive"
|
||||
onConfirm={async () => {
|
||||
await deleteUser(user.id);
|
||||
toast.success('User deleted');
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Common pattern (confirmations)
|
||||
- Handles loading states automatically
|
||||
- Consistent UX across app
|
||||
- Easy to use
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Page Header
|
||||
|
||||
**Problem**: Every page has header with title, description, and optional action.
|
||||
|
||||
**Solution**: Extract page header component.
|
||||
|
||||
```tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center justify-between", className)}>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action && <div>{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<PageHeader
|
||||
title="Users"
|
||||
description="Manage system users and permissions"
|
||||
action={
|
||||
<Button onClick={() => router.push('/users/new')}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create User
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Component Creation Checklist
|
||||
|
||||
Before creating a custom component, ask:
|
||||
|
||||
- [ ] **Is it reused 3+ times?** If no, compose inline
|
||||
- [ ] **Does shadcn/ui have this?** If yes, use it
|
||||
- [ ] **Can I compose existing components?** If yes, do that first
|
||||
- [ ] **Does it need variants?** Use CVA
|
||||
- [ ] **Is className supported?** Always allow overrides
|
||||
- [ ] **Is it accessible?** Test keyboard, screen reader, contrast
|
||||
- [ ] **Is it documented?** Add JSDoc comments
|
||||
- [ ] **Does it follow conventions?** Match shadcn/ui patterns
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Practice**: Refactor inline compositions into components after 3+ uses
|
||||
- **Explore**: [Component showcase](/dev/components)
|
||||
- **Reference**: [shadcn/ui source code](https://github.com/shadcn-ui/ui/tree/main/apps/www/registry)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Components](./02-components.md) - shadcn/ui component library
|
||||
- [AI Guidelines](./08-ai-guidelines.md) - Component templates for AI
|
||||
- [Forms](./06-forms.md) - Form component patterns
|
||||
- [Accessibility](./07-accessibility.md) - Accessibility requirements
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
838
frontend/docs/design-system/06-forms.md
Normal file
838
frontend/docs/design-system/06-forms.md
Normal file
@@ -0,0 +1,838 @@
|
||||
# Forms Guide
|
||||
|
||||
**Master form patterns with react-hook-form + Zod validation**: Learn field layouts, error handling, loading states, and accessibility best practices for bulletproof forms.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Form Architecture](#form-architecture)
|
||||
2. [Basic Form Pattern](#basic-form-pattern)
|
||||
3. [Field Patterns](#field-patterns)
|
||||
4. [Validation with Zod](#validation-with-zod)
|
||||
5. [Error Handling](#error-handling)
|
||||
6. [Loading & Submit States](#loading--submit-states)
|
||||
7. [Form Layouts](#form-layouts)
|
||||
8. [Advanced Patterns](#advanced-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Form Architecture
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **react-hook-form** - Form state management, validation
|
||||
- **Zod** - Schema validation
|
||||
- **@hookform/resolvers** - Zod resolver for react-hook-form
|
||||
- **shadcn/ui components** - Input, Label, Button, etc.
|
||||
|
||||
**Why this stack?**
|
||||
- ✅ Type-safe validation (TypeScript + Zod)
|
||||
- ✅ Minimal re-renders (react-hook-form)
|
||||
- ✅ Accessible by default (shadcn/ui)
|
||||
- ✅ Easy error handling
|
||||
- ✅ Built-in loading states
|
||||
|
||||
---
|
||||
|
||||
### Form Decision Tree
|
||||
|
||||
```
|
||||
Need a form?
|
||||
│
|
||||
├─ Single field (search, filter)?
|
||||
│ └─> Use uncontrolled input with onChange
|
||||
│ <Input onChange={(e) => setQuery(e.target.value)} />
|
||||
│
|
||||
├─ Simple form (1-3 fields, no complex validation)?
|
||||
│ └─> Use react-hook-form without Zod
|
||||
│ const form = useForm();
|
||||
│
|
||||
└─ Complex form (4+ fields, validation, async submit)?
|
||||
└─> Use react-hook-form + Zod
|
||||
const form = useForm({ resolver: zodResolver(schema) });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Form Pattern
|
||||
|
||||
### Minimal Form (No Validation)
|
||||
|
||||
```tsx
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface FormData {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function SimpleForm() {
|
||||
const form = useForm<FormData>();
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...form.register('email')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Complete Form Pattern (with Zod)
|
||||
|
||||
```tsx
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
// 1. Define validation schema
|
||||
const formSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
});
|
||||
|
||||
// 2. Infer TypeScript type from schema
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export function LoginForm() {
|
||||
// 3. Initialize form with Zod resolver
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Submit handler (type-safe!)
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
await loginUser(data);
|
||||
toast.success('Logged in successfully');
|
||||
} catch (error) {
|
||||
toast.error('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Email field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...form.register('email')}
|
||||
aria-invalid={!!form.formState.errors.email}
|
||||
aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{form.formState.errors.email && (
|
||||
<p id="email-error" className="text-sm text-destructive">
|
||||
{form.formState.errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
{...form.register('password')}
|
||||
aria-invalid={!!form.formState.errors.password}
|
||||
aria-describedby={form.formState.errors.password ? 'password-error' : undefined}
|
||||
/>
|
||||
{form.formState.errors.password && (
|
||||
<p id="password-error" className="text-sm text-destructive">
|
||||
{form.formState.errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit button */}
|
||||
<Button type="submit" disabled={form.formState.isSubmitting} className="w-full">
|
||||
{form.formState.isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
1. Define Zod schema first
|
||||
2. Infer TypeScript type with `z.infer`
|
||||
3. Use `zodResolver` in `useForm`
|
||||
4. Register fields with `{...form.register('fieldName')}`
|
||||
5. Show errors from `form.formState.errors`
|
||||
6. Disable submit during submission
|
||||
|
||||
---
|
||||
|
||||
## Field Patterns
|
||||
|
||||
### Text Input
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
{...form.register('name')}
|
||||
aria-invalid={!!form.formState.errors.name}
|
||||
aria-describedby={form.formState.errors.name ? 'name-error' : undefined}
|
||||
/>
|
||||
{form.formState.errors.name && (
|
||||
<p id="name-error" className="text-sm text-destructive">
|
||||
{form.formState.errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Textarea
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
rows={4}
|
||||
{...form.register('description')}
|
||||
/>
|
||||
{form.formState.errors.description && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.description.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Select
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select
|
||||
value={form.watch('role')}
|
||||
onValueChange={(value) => form.setValue('role', value)}
|
||||
>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="guest">Guest</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{form.formState.errors.role && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.role.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Checkbox
|
||||
|
||||
```tsx
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
checked={form.watch('acceptTerms')}
|
||||
onCheckedChange={(checked) => form.setValue('acceptTerms', checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="terms" className="text-sm font-normal">
|
||||
I accept the terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
{form.formState.errors.acceptTerms && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.acceptTerms.message}
|
||||
</p>
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Radio Group (Custom Pattern)
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label>Notification Method</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="email"
|
||||
value="email"
|
||||
{...form.register('notificationMethod')}
|
||||
/>
|
||||
<Label htmlFor="email" className="font-normal">Email</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="sms"
|
||||
value="sms"
|
||||
{...form.register('notificationMethod')}
|
||||
/>
|
||||
<Label htmlFor="sms" className="font-normal">SMS</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation with Zod
|
||||
|
||||
### Common Validation Patterns
|
||||
|
||||
```tsx
|
||||
import { z } from 'zod';
|
||||
|
||||
// Email
|
||||
z.string().email('Invalid email address')
|
||||
|
||||
// Min/max length
|
||||
z.string().min(8, 'Minimum 8 characters').max(100, 'Maximum 100 characters')
|
||||
|
||||
// Required field
|
||||
z.string().min(1, 'This field is required')
|
||||
|
||||
// Optional field
|
||||
z.string().optional()
|
||||
|
||||
// Number with range
|
||||
z.number().min(0).max(100)
|
||||
|
||||
// Number from string input
|
||||
z.coerce.number().min(0)
|
||||
|
||||
// Enum
|
||||
z.enum(['admin', 'user', 'guest'], {
|
||||
errorMap: () => ({ message: 'Invalid role' })
|
||||
})
|
||||
|
||||
// URL
|
||||
z.string().url('Invalid URL')
|
||||
|
||||
// Password with requirements
|
||||
z.string()
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
||||
|
||||
// Confirm password
|
||||
z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string()
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
|
||||
// Custom validation
|
||||
z.string().refine((val) => !val.includes('badword'), {
|
||||
message: 'Invalid input',
|
||||
})
|
||||
|
||||
// Conditional fields
|
||||
z.object({
|
||||
role: z.enum(['admin', 'user']),
|
||||
adminKey: z.string().optional(),
|
||||
}).refine((data) => {
|
||||
if (data.role === 'admin') {
|
||||
return !!data.adminKey;
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: 'Admin key required for admin role',
|
||||
path: ['adminKey'],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Full Form Schema Example
|
||||
|
||||
```tsx
|
||||
const userFormSchema = z.object({
|
||||
// Required text
|
||||
firstName: z.string().min(1, 'First name is required'),
|
||||
lastName: z.string().min(1, 'Last name is required'),
|
||||
|
||||
// Email
|
||||
email: z.string().email('Invalid email address'),
|
||||
|
||||
// Optional phone
|
||||
phone: z.string().optional(),
|
||||
|
||||
// Number
|
||||
age: z.coerce.number().min(18, 'Must be 18 or older').max(120),
|
||||
|
||||
// Enum
|
||||
role: z.enum(['admin', 'user', 'guest']),
|
||||
|
||||
// Boolean
|
||||
acceptTerms: z.boolean().refine((val) => val === true, {
|
||||
message: 'You must accept the terms',
|
||||
}),
|
||||
|
||||
// Nested object
|
||||
address: z.object({
|
||||
street: z.string().min(1),
|
||||
city: z.string().min(1),
|
||||
zip: z.string().regex(/^\d{5}$/, 'Invalid ZIP code'),
|
||||
}),
|
||||
|
||||
// Array
|
||||
tags: z.array(z.string()).min(1, 'At least one tag required'),
|
||||
});
|
||||
|
||||
type UserFormData = z.infer<typeof userFormSchema>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Field-Level Errors
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...form.register('email')}
|
||||
className={form.formState.errors.email ? 'border-destructive' : ''}
|
||||
aria-invalid={!!form.formState.errors.email}
|
||||
aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{form.formState.errors.email && (
|
||||
<p id="email-error" className="text-sm text-destructive">
|
||||
{form.formState.errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Accessibility notes:**
|
||||
- Use `aria-invalid` to indicate error state
|
||||
- Use `aria-describedby` to link error message
|
||||
- Error ID format: `{fieldName}-error`
|
||||
|
||||
---
|
||||
|
||||
### Form-Level Errors
|
||||
|
||||
```tsx
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
await submitForm(data);
|
||||
} catch (error) {
|
||||
// Set form-level error
|
||||
form.setError('root', {
|
||||
type: 'server',
|
||||
message: error.message || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Display form-level error
|
||||
{form.formState.errors.root && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{form.formState.errors.root.message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Server Validation Errors
|
||||
|
||||
```tsx
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
await createUser(data);
|
||||
} catch (error) {
|
||||
if (error.response?.data?.errors) {
|
||||
// Map server errors to form fields
|
||||
const serverErrors = error.response.data.errors;
|
||||
|
||||
Object.keys(serverErrors).forEach((field) => {
|
||||
form.setError(field as keyof FormData, {
|
||||
type: 'server',
|
||||
message: serverErrors[field],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Generic error
|
||||
form.setError('root', {
|
||||
type: 'server',
|
||||
message: 'Failed to create user',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading & Submit States
|
||||
|
||||
### Basic Loading State
|
||||
|
||||
```tsx
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Disable All Fields During Submit
|
||||
|
||||
```tsx
|
||||
const isDisabled = form.formState.isSubmitting;
|
||||
|
||||
<Input
|
||||
{...form.register('name')}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isDisabled}>
|
||||
{isDisabled ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Loading with Toast
|
||||
|
||||
```tsx
|
||||
const onSubmit = async (data: FormData) => {
|
||||
const loadingToast = toast.loading('Creating user...');
|
||||
|
||||
try {
|
||||
await createUser(data);
|
||||
toast.success('User created successfully', { id: loadingToast });
|
||||
router.push('/users');
|
||||
} catch (error) {
|
||||
toast.error('Failed to create user', { id: loadingToast });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form Layouts
|
||||
|
||||
### Centered Form (Login, Signup)
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign In</CardTitle>
|
||||
<CardDescription>Enter your credentials to continue</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Form fields */}
|
||||
<Button type="submit" className="w-full">
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Two-Column Form
|
||||
|
||||
```tsx
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Row 1: Two columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<Input id="firstName" {...form.register('firstName')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<Input id="lastName" {...form.register('lastName')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Full width */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" {...form.register('email')} />
|
||||
</div>
|
||||
|
||||
<Button type="submit">Save</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Form with Sections
|
||||
|
||||
```tsx
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* Section 1 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Personal Information</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Basic details about you
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
{/* Fields */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Account Settings</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure your account preferences
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
{/* Fields */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button type="button" variant="outline">Cancel</Button>
|
||||
<Button type="submit">Save Changes</Button>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Dynamic Fields (Array)
|
||||
|
||||
```tsx
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
|
||||
const schema = z.object({
|
||||
items: z.array(z.object({
|
||||
name: z.string().min(1),
|
||||
quantity: z.coerce.number().min(1),
|
||||
})).min(1, 'At least one item required'),
|
||||
});
|
||||
|
||||
function DynamicForm() {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
items: [{ name: '', quantity: 1 }],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'items',
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex gap-4">
|
||||
<Input
|
||||
{...form.register(`items.${index}.name`)}
|
||||
placeholder="Item name"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
{...form.register(`items.${index}.quantity`)}
|
||||
placeholder="Quantity"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => append({ name: '', quantity: 1 })}
|
||||
>
|
||||
Add Item
|
||||
</Button>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Conditional Fields
|
||||
|
||||
```tsx
|
||||
const schema = z.object({
|
||||
role: z.enum(['user', 'admin']),
|
||||
adminKey: z.string().optional(),
|
||||
}).refine((data) => {
|
||||
if (data.role === 'admin') {
|
||||
return !!data.adminKey;
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: 'Admin key required',
|
||||
path: ['adminKey'],
|
||||
});
|
||||
|
||||
function ConditionalForm() {
|
||||
const form = useForm({ resolver: zodResolver(schema) });
|
||||
const role = form.watch('role');
|
||||
|
||||
return (
|
||||
<form className="space-y-4">
|
||||
<Select
|
||||
value={role}
|
||||
onValueChange={(val) => form.setValue('role', val as any)}
|
||||
>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{role === 'admin' && (
|
||||
<Input
|
||||
{...form.register('adminKey')}
|
||||
placeholder="Admin Key"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### File Upload
|
||||
|
||||
```tsx
|
||||
const schema = z.object({
|
||||
file: z.instanceof(FileList).refine((files) => files.length > 0, {
|
||||
message: 'File is required',
|
||||
}),
|
||||
});
|
||||
|
||||
<input
|
||||
type="file"
|
||||
{...form.register('file')}
|
||||
accept="image/*"
|
||||
/>
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
const file = data.file[0]; // FileList -> File
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
// Upload formData
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form Checklist
|
||||
|
||||
Before shipping a form, verify:
|
||||
|
||||
### Functionality
|
||||
- [ ] All fields register correctly
|
||||
- [ ] Validation works (test invalid inputs)
|
||||
- [ ] Submit handler fires
|
||||
- [ ] Loading state works
|
||||
- [ ] Error messages display
|
||||
- [ ] Success case redirects/shows success
|
||||
|
||||
### Accessibility
|
||||
- [ ] Labels associated with inputs (`htmlFor` + `id`)
|
||||
- [ ] Error messages use `aria-describedby`
|
||||
- [ ] Invalid inputs have `aria-invalid`
|
||||
- [ ] Focus order is logical (Tab through form)
|
||||
- [ ] Submit button disabled during submission
|
||||
|
||||
### UX
|
||||
- [ ] Field errors appear on blur or submit
|
||||
- [ ] Loading state prevents double-submit
|
||||
- [ ] Success message or redirect on success
|
||||
- [ ] Cancel button clears form or navigates away
|
||||
- [ ] Mobile-friendly (responsive layout)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Interactive Examples**: [Form examples](/dev/forms)
|
||||
- **Components**: [Form components](./02-components.md#form-components)
|
||||
- **Accessibility**: [Form accessibility](./07-accessibility.md#forms)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Components](./02-components.md) - Input, Label, Button, Select
|
||||
- [Layouts](./03-layouts.md) - Form layout patterns
|
||||
- [Accessibility](./07-accessibility.md) - ARIA attributes for forms
|
||||
|
||||
**External Resources:**
|
||||
- [react-hook-form Documentation](https://react-hook-form.com)
|
||||
- [Zod Documentation](https://zod.dev)
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
704
frontend/docs/design-system/07-accessibility.md
Normal file
704
frontend/docs/design-system/07-accessibility.md
Normal file
@@ -0,0 +1,704 @@
|
||||
# Accessibility Guide
|
||||
|
||||
**Build inclusive, accessible interfaces** that work for everyone. Learn WCAG AA standards, keyboard navigation, screen reader support, and testing strategies.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Accessibility Standards](#accessibility-standards)
|
||||
2. [Color Contrast](#color-contrast)
|
||||
3. [Keyboard Navigation](#keyboard-navigation)
|
||||
4. [Screen Reader Support](#screen-reader-support)
|
||||
5. [ARIA Attributes](#aria-attributes)
|
||||
6. [Focus Management](#focus-management)
|
||||
7. [Testing](#testing)
|
||||
8. [Accessibility Checklist](#accessibility-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Standards
|
||||
|
||||
### WCAG 2.1 Level AA
|
||||
|
||||
We follow **WCAG 2.1 Level AA** as the **minimum** standard.
|
||||
|
||||
**Why Level AA?**
|
||||
- ✅ Required for most legal compliance (ADA, Section 508)
|
||||
- ✅ Covers 95%+ of accessibility needs
|
||||
- ✅ Achievable without major UX compromises
|
||||
- ✅ Industry standard for modern web apps
|
||||
|
||||
**WCAG Principles (POUR):**
|
||||
1. **Perceivable** - Information can be perceived by users
|
||||
2. **Operable** - Interface can be operated by users
|
||||
3. **Understandable** - Information and operation are understandable
|
||||
4. **Robust** - Content works with current and future technologies
|
||||
|
||||
---
|
||||
|
||||
### Accessibility Decision Tree
|
||||
|
||||
```
|
||||
Creating a UI element?
|
||||
│
|
||||
├─ Is it interactive?
|
||||
│ ├─YES─> Can it be focused with Tab?
|
||||
│ │ ├─YES─> ✅ Good
|
||||
│ │ └─NO──> ❌ Add tabIndex or use button/link
|
||||
│ │
|
||||
│ └─NO──> Is it important information?
|
||||
│ ├─YES─> Does it have appropriate semantic markup?
|
||||
│ │ ├─YES─> ✅ Good
|
||||
│ │ └─NO──> ❌ Use h1-h6, p, ul, etc.
|
||||
│ │
|
||||
│ └─NO──> Is it purely decorative?
|
||||
│ ├─YES─> Add aria-hidden="true"
|
||||
│ └─NO──> Add alt text or ARIA label
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Color Contrast
|
||||
|
||||
### Minimum Contrast Ratios (WCAG AA)
|
||||
|
||||
| Content Type | Minimum Ratio | Example |
|
||||
|--------------|---------------|---------|
|
||||
| **Normal text** (< 18px) | **4.5:1** | Body paragraphs, form labels |
|
||||
| **Large text** (≥ 18px or ≥ 14px bold) | **3:1** | Headings, subheadings |
|
||||
| **UI components** | **3:1** | Buttons, form borders, icons |
|
||||
| **Graphical objects** | **3:1** | Chart elements, infographics |
|
||||
|
||||
**WCAG AAA (ideal, not required):**
|
||||
- Normal text: 7:1
|
||||
- Large text: 4.5:1
|
||||
|
||||
---
|
||||
|
||||
### Testing Color Contrast
|
||||
|
||||
**Tools:**
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- Chrome DevTools: Inspect element → Accessibility panel
|
||||
- [Contrast Ratio Tool](https://contrast-ratio.com)
|
||||
- Browser extensions: axe DevTools, WAVE
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - 4.7:1 contrast (WCAG AA pass)
|
||||
<p className="text-foreground"> // oklch(0.1529 0 0) on white
|
||||
Body text
|
||||
</p>
|
||||
|
||||
// ❌ BAD - 2.1:1 contrast (WCAG AA fail)
|
||||
<p className="text-gray-400"> // Too light
|
||||
Body text
|
||||
</p>
|
||||
|
||||
// ✅ GOOD - Using semantic tokens ensures contrast
|
||||
<p className="text-muted-foreground">
|
||||
Secondary text
|
||||
</p>
|
||||
```
|
||||
|
||||
**Our design system tokens are WCAG AA compliant:**
|
||||
- `text-foreground` on `bg-background`: 12.6:1 ✅
|
||||
- `text-primary-foreground` on `bg-primary`: 8.2:1 ✅
|
||||
- `text-destructive` on `bg-background`: 5.1:1 ✅
|
||||
- `text-muted-foreground` on `bg-background`: 4.6:1 ✅
|
||||
|
||||
---
|
||||
|
||||
### Color Blindness
|
||||
|
||||
**8% of men and 0.5% of women** have some form of color blindness.
|
||||
|
||||
**Best practices:**
|
||||
- ❌ Don't rely on color alone to convey information
|
||||
- ✅ Use icons, text labels, or patterns in addition to color
|
||||
- ✅ Test with color blindness simulators
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Color only
|
||||
<div className="text-green-600">Success</div>
|
||||
<div className="text-red-600">Error</div>
|
||||
|
||||
// ✅ GOOD - Color + icon + text
|
||||
<Alert variant="success">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Operation completed</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>Something went wrong</AlertDescription>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Navigation
|
||||
|
||||
### Core Requirements
|
||||
|
||||
All interactive elements must be:
|
||||
1. ✅ **Focusable** - Can be reached with Tab key
|
||||
2. ✅ **Activatable** - Can be triggered with Enter or Space
|
||||
3. ✅ **Navigable** - Can move between with arrow keys (where appropriate)
|
||||
4. ✅ **Escapable** - Can be closed/exited with Escape key
|
||||
|
||||
---
|
||||
|
||||
### Tab Order
|
||||
|
||||
**Natural tab order** follows DOM order (top to bottom, left to right).
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Natural tab order
|
||||
<form>
|
||||
<Input /> {/* Tab 1 */}
|
||||
<Input /> {/* Tab 2 */}
|
||||
<Button>Submit</Button> {/* Tab 3 */}
|
||||
</form>
|
||||
|
||||
// ❌ BAD - Using tabIndex to force order
|
||||
<form>
|
||||
<Input tabIndex={2} /> // Don't do this
|
||||
<Input tabIndex={1} />
|
||||
<Button tabIndex={3}>Submit</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
**When to use `tabIndex`:**
|
||||
- `tabIndex={0}` - Make non-interactive element focusable
|
||||
- `tabIndex={-1}` - Remove from tab order (for programmatic focus)
|
||||
- `tabIndex={1+}` - ❌ **Avoid** - Breaks natural order
|
||||
|
||||
---
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action | Example |
|
||||
|-----|--------|---------|
|
||||
| **Tab** | Move focus forward | Navigate through form fields |
|
||||
| **Shift + Tab** | Move focus backward | Go back to previous field |
|
||||
| **Enter** | Activate button/link | Submit form, follow link |
|
||||
| **Space** | Activate button/checkbox | Toggle checkbox, click button |
|
||||
| **Escape** | Close overlay | Close dialog, dropdown |
|
||||
| **Arrow keys** | Navigate within component | Navigate dropdown items |
|
||||
| **Home** | Jump to start | First item in list |
|
||||
| **End** | Jump to end | Last item in list |
|
||||
|
||||
---
|
||||
|
||||
### Implementing Keyboard Navigation
|
||||
|
||||
**Button (automatic):**
|
||||
```tsx
|
||||
// ✅ Button is keyboard accessible by default
|
||||
<Button onClick={handleClick}>
|
||||
Click me
|
||||
</Button>
|
||||
// Enter or Space triggers onClick
|
||||
```
|
||||
|
||||
**Custom clickable div (needs work):**
|
||||
```tsx
|
||||
// ❌ BAD - Not keyboard accessible
|
||||
<div onClick={handleClick}>
|
||||
Click me
|
||||
</div>
|
||||
|
||||
// ✅ GOOD - Make it accessible
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Click me
|
||||
</div>
|
||||
|
||||
// ✅ BETTER - Just use a button
|
||||
<button onClick={handleClick}>
|
||||
Click me
|
||||
</button>
|
||||
```
|
||||
|
||||
**Dropdown navigation:**
|
||||
```tsx
|
||||
<DropdownMenu>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem> {/* Arrow down */}
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem> {/* Arrow down */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
// shadcn/ui handles arrow key navigation automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Skip Links
|
||||
|
||||
**Allow keyboard users to skip navigation:**
|
||||
|
||||
```tsx
|
||||
// Add to layout
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
<nav>{/* Navigation */}</nav>
|
||||
|
||||
<main id="main-content">
|
||||
{/* Main content */}
|
||||
</main>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screen Reader Support
|
||||
|
||||
### Screen Reader Basics
|
||||
|
||||
**Popular screen readers:**
|
||||
- **NVDA** (Windows) - Free, most popular for testing
|
||||
- **JAWS** (Windows) - Industry standard, paid
|
||||
- **VoiceOver** (macOS/iOS) - Built-in to Apple devices
|
||||
- **TalkBack** (Android) - Built-in to Android
|
||||
|
||||
**What screen readers announce:**
|
||||
- Semantic element type (button, link, heading, etc.)
|
||||
- Element text content
|
||||
- Element state (expanded, selected, disabled)
|
||||
- ARIA labels and descriptions
|
||||
|
||||
---
|
||||
|
||||
### Semantic HTML
|
||||
|
||||
**Use the right HTML element for the job:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Semantic HTML
|
||||
<header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<article>
|
||||
<h1>Page Title</h1>
|
||||
<p>Content...</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2025 Company</p>
|
||||
</footer>
|
||||
|
||||
// ❌ BAD - Div soup
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div onClick={goHome}>Home</div>
|
||||
<div onClick={goAbout}>About</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div>Page Title</div>
|
||||
<div>Content...</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Semantic elements:**
|
||||
- `<header>` - Page header
|
||||
- `<nav>` - Navigation
|
||||
- `<main>` - Main content (only one per page)
|
||||
- `<article>` - Self-contained content
|
||||
- `<section>` - Thematic grouping
|
||||
- `<aside>` - Sidebar content
|
||||
- `<footer>` - Page footer
|
||||
- `<h1>` - `<h6>` - Headings (hierarchical)
|
||||
- `<button>` - Buttons
|
||||
- `<a>` - Links
|
||||
|
||||
---
|
||||
|
||||
### Alt Text for Images
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Descriptive alt text
|
||||
<img src="/chart.png" alt="Bar chart showing 20% increase in sales from January to February" />
|
||||
|
||||
// ✅ GOOD - Decorative images
|
||||
<img src="/decorative.png" alt="" /> // Empty alt for decorative
|
||||
// OR
|
||||
<img src="/decorative.png" aria-hidden="true" />
|
||||
|
||||
// ❌ BAD - Generic or missing alt
|
||||
<img src="/chart.png" alt="image" />
|
||||
<img src="/chart.png" /> // No alt
|
||||
```
|
||||
|
||||
**Icon-only buttons:**
|
||||
```tsx
|
||||
// ✅ GOOD - ARIA label
|
||||
<Button size="icon" aria-label="Close dialog">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
// ❌ BAD - No label
|
||||
<Button size="icon">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ARIA Attributes
|
||||
|
||||
### Common ARIA Attributes
|
||||
|
||||
**ARIA roles:**
|
||||
```tsx
|
||||
<div role="button" tabIndex={0}>Custom Button</div>
|
||||
<div role="alert">Error message</div>
|
||||
<div role="status">Loading...</div>
|
||||
<div role="navigation">...</div>
|
||||
```
|
||||
|
||||
**ARIA states:**
|
||||
```tsx
|
||||
<button aria-expanded={isOpen}>Toggle Menu</button>
|
||||
<button aria-pressed={isActive}>Toggle</button>
|
||||
<input aria-invalid={!!errors.email} />
|
||||
<div aria-disabled="true">Disabled Item</div>
|
||||
```
|
||||
|
||||
**ARIA properties:**
|
||||
```tsx
|
||||
<button aria-label="Close">×</button>
|
||||
<input aria-describedby="email-help" />
|
||||
<input aria-required="true" />
|
||||
<div aria-live="polite">Status updates</div>
|
||||
<div aria-hidden="true">Decorative content</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Form Accessibility
|
||||
|
||||
**Label association:**
|
||||
```tsx
|
||||
// ✅ GOOD - Explicit association
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" />
|
||||
|
||||
// ❌ BAD - No association
|
||||
<div>Email</div>
|
||||
<Input type="email" />
|
||||
```
|
||||
|
||||
**Error messages:**
|
||||
```tsx
|
||||
// ✅ GOOD - Linked with aria-describedby
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
aria-invalid={!!errors.password}
|
||||
aria-describedby={errors.password ? 'password-error' : undefined}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p id="password-error" className="text-sm text-destructive">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
// ❌ BAD - No association
|
||||
<Input type="password" />
|
||||
{errors.password && <p>{errors.password.message}</p>}
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
```tsx
|
||||
// ✅ GOOD - Marked as required
|
||||
<Label htmlFor="name">
|
||||
Name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input id="name" required aria-required="true" />
|
||||
|
||||
// Screen reader announces: "Name, required, edit text"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Live Regions
|
||||
|
||||
**Announce dynamic updates:**
|
||||
|
||||
```tsx
|
||||
// Polite (waits for user to finish)
|
||||
<div aria-live="polite" aria-atomic="true">
|
||||
{statusMessage}
|
||||
</div>
|
||||
|
||||
// Assertive (interrupts immediately)
|
||||
<div aria-live="assertive" role="alert">
|
||||
{errorMessage}
|
||||
</div>
|
||||
|
||||
// Example: Toast notifications (sonner uses this)
|
||||
toast.success('User created');
|
||||
// Announces: "Success. User created."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Focus Management
|
||||
|
||||
### Visible Focus Indicators
|
||||
|
||||
**All interactive elements must have visible focus:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - shadcn/ui components have focus rings
|
||||
<Button>Click me</Button>
|
||||
// Shows ring on focus
|
||||
|
||||
// ✅ GOOD - Custom focus styles
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Focusable content
|
||||
</div>
|
||||
|
||||
// ❌ BAD - Removing focus outline
|
||||
<button style={{ outline: 'none' }}>Bad</button>
|
||||
```
|
||||
|
||||
**Use `:focus-visible` instead of `:focus`:**
|
||||
- `:focus` - Shows on mouse click AND keyboard
|
||||
- `:focus-visible` - Shows only on keyboard (better UX)
|
||||
|
||||
---
|
||||
|
||||
### Focus Trapping
|
||||
|
||||
**Dialogs should trap focus:**
|
||||
|
||||
```tsx
|
||||
// shadcn/ui Dialog automatically traps focus
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
{/* Focus trapped inside */}
|
||||
<Input autoFocus /> {/* Focus first field */}
|
||||
<Button>Submit</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
// When dialog closes, focus returns to trigger button
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Programmatic Focus
|
||||
|
||||
**Set focus after actions:**
|
||||
|
||||
```tsx
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteUser();
|
||||
// Return focus to a relevant element
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
<Input ref={inputRef} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Automated Testing Tools
|
||||
|
||||
**Browser extensions:**
|
||||
- [axe DevTools](https://www.deque.com/axe/devtools/) - Free, comprehensive
|
||||
- [WAVE](https://wave.webaim.org/extension/) - Visual feedback
|
||||
- [Lighthouse](https://developer.chrome.com/docs/lighthouse/) - Built into Chrome
|
||||
|
||||
**CI/CD testing:**
|
||||
- [@axe-core/react](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/react) - Runtime accessibility testing
|
||||
- [jest-axe](https://github.com/nickcolley/jest-axe) - Jest integration
|
||||
- [Playwright accessibility testing](https://playwright.dev/docs/accessibility-testing)
|
||||
|
||||
---
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
#### Keyboard Testing
|
||||
1. [ ] Unplug mouse
|
||||
2. [ ] Tab through entire page
|
||||
3. [ ] All interactive elements focusable?
|
||||
4. [ ] Focus indicators visible?
|
||||
5. [ ] Can activate with Enter/Space?
|
||||
6. [ ] Can close modals with Escape?
|
||||
7. [ ] Tab order logical?
|
||||
|
||||
#### Screen Reader Testing
|
||||
1. [ ] Install NVDA (Windows) or VoiceOver (Mac)
|
||||
2. [ ] Navigate page with screen reader on
|
||||
3. [ ] All content announced?
|
||||
4. [ ] Interactive elements have labels?
|
||||
5. [ ] Form errors announced?
|
||||
6. [ ] Heading hierarchy correct?
|
||||
|
||||
#### Contrast Testing
|
||||
1. [ ] Use contrast checker on all text
|
||||
2. [ ] Check UI components (buttons, borders)
|
||||
3. [ ] Test in dark mode too
|
||||
4. [ ] All elements meet 4.5:1 (text) or 3:1 (UI)?
|
||||
|
||||
---
|
||||
|
||||
### Testing with Real Users
|
||||
|
||||
**Considerations:**
|
||||
- Test with actual users who rely on assistive technologies
|
||||
- Different screen readers behave differently
|
||||
- Mobile screen readers (VoiceOver, TalkBack) differ from desktop
|
||||
- Keyboard-only users have different needs than screen reader users
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Checklist
|
||||
|
||||
### General
|
||||
- [ ] Page has `<title>` and `<meta name="description">`
|
||||
- [ ] Page has proper heading hierarchy (h1 → h2 → h3)
|
||||
- [ ] Landmarks used (`<header>`, `<nav>`, `<main>`, `<footer>`)
|
||||
- [ ] Skip link present for keyboard users
|
||||
- [ ] No content relies on color alone
|
||||
|
||||
### Color & Contrast
|
||||
- [ ] Text has 4.5:1 contrast (normal) or 3:1 (large)
|
||||
- [ ] UI components have 3:1 contrast
|
||||
- [ ] Tested in both light and dark modes
|
||||
- [ ] Color blindness simulator used
|
||||
|
||||
### Keyboard
|
||||
- [ ] All interactive elements focusable
|
||||
- [ ] Focus indicators visible (ring, outline, etc.)
|
||||
- [ ] Tab order is logical
|
||||
- [ ] No keyboard traps
|
||||
- [ ] Enter/Space activates buttons
|
||||
- [ ] Escape closes dialogs/dropdowns
|
||||
- [ ] Arrow keys navigate lists/menus
|
||||
|
||||
### Screen Readers
|
||||
- [ ] All images have alt text
|
||||
- [ ] Icon-only buttons have aria-label
|
||||
- [ ] Form labels associated with inputs
|
||||
- [ ] Form errors use aria-describedby
|
||||
- [ ] Required fields marked with aria-required
|
||||
- [ ] Live regions for dynamic updates
|
||||
- [ ] ARIA roles used correctly
|
||||
|
||||
### Forms
|
||||
- [ ] Labels associated with inputs (`htmlFor` + `id`)
|
||||
- [ ] Error messages linked (`aria-describedby`)
|
||||
- [ ] Invalid inputs marked (`aria-invalid`)
|
||||
- [ ] Required fields indicated (`aria-required`)
|
||||
- [ ] Submit button disabled during submission
|
||||
|
||||
### Focus Management
|
||||
- [ ] Dialogs trap focus
|
||||
- [ ] Focus returns after dialog closes
|
||||
- [ ] Programmatic focus after actions
|
||||
- [ ] No focus outline removed without alternative
|
||||
|
||||
---
|
||||
|
||||
## Quick Wins for Accessibility
|
||||
|
||||
**Easy improvements with big impact:**
|
||||
|
||||
1. **Add alt text to images**
|
||||
```tsx
|
||||
<img src="/logo.png" alt="Company Logo" />
|
||||
```
|
||||
|
||||
2. **Associate labels with inputs**
|
||||
```tsx
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" />
|
||||
```
|
||||
|
||||
3. **Use semantic HTML**
|
||||
```tsx
|
||||
<button> instead of <div onClick>
|
||||
```
|
||||
|
||||
4. **Add aria-label to icon buttons**
|
||||
```tsx
|
||||
<Button aria-label="Close"><X /></Button>
|
||||
```
|
||||
|
||||
5. **Use semantic color tokens**
|
||||
```tsx
|
||||
className="text-foreground" // Auto contrast
|
||||
```
|
||||
|
||||
6. **Test with keyboard only**
|
||||
- Tab through page
|
||||
- Fix anything unreachable
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Test Now**: Run [axe DevTools](https://www.deque.com/axe/devtools/) on your app
|
||||
- **Learn More**: [W3C ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||||
- **Components**: [Review accessible components](./02-components.md)
|
||||
- **Forms**: [Accessible form patterns](./06-forms.md)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Forms](./06-forms.md) - Accessible form patterns
|
||||
- [Components](./02-components.md) - All components are accessible
|
||||
- [Foundations](./01-foundations.md) - Color contrast tokens
|
||||
|
||||
**External Resources:**
|
||||
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
|
||||
- [axe DevTools](https://www.deque.com/axe/devtools/)
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
574
frontend/docs/design-system/08-ai-guidelines.md
Normal file
574
frontend/docs/design-system/08-ai-guidelines.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# AI Code Generation Guidelines
|
||||
|
||||
**For AI Assistants**: This document contains strict rules for generating code in the FastNext Template project. Follow these rules to ensure generated code matches the design system perfectly.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Core Rules
|
||||
|
||||
### ALWAYS Do
|
||||
|
||||
1. ✅ **Import from `@/components/ui/*`**
|
||||
```tsx
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
```
|
||||
|
||||
2. ✅ **Use semantic color tokens**
|
||||
```tsx
|
||||
className="bg-primary text-primary-foreground"
|
||||
className="text-destructive"
|
||||
className="bg-muted text-muted-foreground"
|
||||
```
|
||||
|
||||
3. ✅ **Use `cn()` utility for className merging**
|
||||
```tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
className={cn("base-classes", conditional && "conditional-classes", className)}
|
||||
```
|
||||
|
||||
4. ✅ **Follow spacing scale** (multiples of 4: 0, 1, 2, 3, 4, 6, 8, 12, 16)
|
||||
```tsx
|
||||
className="p-4 space-y-6 mb-8"
|
||||
```
|
||||
|
||||
5. ✅ **Add accessibility attributes**
|
||||
```tsx
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
aria-invalid={!!errors.email}
|
||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
```
|
||||
|
||||
6. ✅ **Use component variants**
|
||||
```tsx
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Alert variant="destructive">Error message</Alert>
|
||||
```
|
||||
|
||||
7. ✅ **Compose from shadcn/ui primitives**
|
||||
```tsx
|
||||
// Don't create custom card components
|
||||
// Use Card + CardHeader + CardTitle + CardContent
|
||||
```
|
||||
|
||||
8. ✅ **Use mobile-first responsive design**
|
||||
```tsx
|
||||
className="text-2xl sm:text-3xl lg:text-4xl"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### NEVER Do
|
||||
|
||||
1. ❌ **NO arbitrary colors**
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
className="bg-blue-500 text-white"
|
||||
|
||||
// ✅ CORRECT
|
||||
className="bg-primary text-primary-foreground"
|
||||
```
|
||||
|
||||
2. ❌ **NO arbitrary spacing values**
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
className="p-[13px] mb-[17px]"
|
||||
|
||||
// ✅ CORRECT
|
||||
className="p-4 mb-4"
|
||||
```
|
||||
|
||||
3. ❌ **NO inline styles**
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
style={{ margin: '10px', color: '#3b82f6' }}
|
||||
|
||||
// ✅ CORRECT
|
||||
className="m-4 text-primary"
|
||||
```
|
||||
|
||||
4. ❌ **NO custom CSS classes** (use Tailwind utilities)
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<div className="my-custom-class">
|
||||
|
||||
// ✅ CORRECT
|
||||
<div className="flex items-center justify-between p-4">
|
||||
```
|
||||
|
||||
5. ❌ **NO mixing component libraries**
|
||||
```tsx
|
||||
// ❌ WRONG - Don't use Material-UI, Ant Design, etc.
|
||||
import { Button } from '@mui/material';
|
||||
|
||||
// ✅ CORRECT - Only shadcn/ui
|
||||
import { Button } from '@/components/ui/button';
|
||||
```
|
||||
|
||||
6. ❌ **NO skipping accessibility**
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<button><X /></button>
|
||||
|
||||
// ✅ CORRECT
|
||||
<Button size="icon" aria-label="Close">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
7. ❌ **NO creating custom variants without CVA**
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<Button className={type === 'danger' ? 'bg-red-500' : 'bg-blue-500'}>
|
||||
|
||||
// ✅ CORRECT
|
||||
<Button variant="destructive">Delete</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 Layout Patterns
|
||||
|
||||
### Page Container
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dashboard Grid
|
||||
|
||||
```tsx
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map(item => <Card key={item.id}>...</Card>)}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form Layout
|
||||
|
||||
```tsx
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Form Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Form fields */}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Centered Content
|
||||
|
||||
```tsx
|
||||
<div className="max-w-2xl mx-auto px-4">
|
||||
{/* Readable content width */}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Component Templates
|
||||
|
||||
### Custom Component Template
|
||||
|
||||
```tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
interface MyComponentProps {
|
||||
variant?: 'default' | 'compact';
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MyComponent({
|
||||
variant = 'default',
|
||||
className,
|
||||
children
|
||||
}: MyComponentProps) {
|
||||
return (
|
||||
<Card className={cn(
|
||||
"p-4", // base styles
|
||||
variant === 'compact' && "p-2",
|
||||
className // allow overrides
|
||||
)}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Component with CVA (class-variance-authority)
|
||||
|
||||
```tsx
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const componentVariants = cva(
|
||||
"base-classes-here", // base
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground",
|
||||
destructive: "bg-destructive text-destructive-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "h-8 px-3 text-xs",
|
||||
default: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-6 text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface ComponentProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof componentVariants> {}
|
||||
|
||||
export function Component({ variant, size, className, ...props }: ComponentProps) {
|
||||
return (
|
||||
<div className={cn(componentVariants({ variant, size, className }))} {...props} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Form Pattern Template
|
||||
|
||||
```tsx
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert } from '@/components/ui/alert';
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export function MyForm() {
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
// Handle submission
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Email Field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...form.register('email')}
|
||||
aria-invalid={!!form.formState.errors.email}
|
||||
aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{form.formState.errors.email && (
|
||||
<p id="email-error" className="text-sm text-destructive">
|
||||
{form.formState.errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Color Token Reference
|
||||
|
||||
**Always use these semantic tokens:**
|
||||
|
||||
| Token | Usage |
|
||||
|-------|-------|
|
||||
| `bg-primary text-primary-foreground` | Primary buttons, CTAs |
|
||||
| `bg-secondary text-secondary-foreground` | Secondary actions |
|
||||
| `bg-destructive text-destructive-foreground` | Delete, errors |
|
||||
| `bg-muted text-muted-foreground` | Disabled states |
|
||||
| `bg-accent text-accent-foreground` | Hover states |
|
||||
| `bg-card text-card-foreground` | Card backgrounds |
|
||||
| `text-foreground` | Body text |
|
||||
| `text-muted-foreground` | Secondary text |
|
||||
| `border-border` | Borders |
|
||||
| `ring-ring` | Focus rings |
|
||||
|
||||
---
|
||||
|
||||
## 📏 Spacing Reference
|
||||
|
||||
**Use these spacing values (multiples of 4px):**
|
||||
|
||||
| Class | Value | Pixels | Usage |
|
||||
|-------|-------|--------|-------|
|
||||
| `2` | 0.5rem | 8px | Tight spacing |
|
||||
| `4` | 1rem | 16px | Standard spacing |
|
||||
| `6` | 1.5rem | 24px | Section spacing |
|
||||
| `8` | 2rem | 32px | Large gaps |
|
||||
| `12` | 3rem | 48px | Section dividers |
|
||||
| `16` | 4rem | 64px | Page sections |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Decision Trees
|
||||
|
||||
### When to use Grid vs Flex?
|
||||
|
||||
```
|
||||
Need equal-width columns? → Use Grid
|
||||
className="grid grid-cols-3 gap-6"
|
||||
|
||||
Need flexible item sizes? → Use Flex
|
||||
className="flex gap-4"
|
||||
|
||||
Need 2D layout (rows + columns)? → Use Grid
|
||||
className="grid grid-cols-2 grid-rows-3 gap-4"
|
||||
|
||||
Need 1D layout (single row OR column)? → Use Flex
|
||||
className="flex flex-col gap-4"
|
||||
```
|
||||
|
||||
### When to use Margin vs Padding?
|
||||
|
||||
```
|
||||
Spacing between sibling elements? → Use gap or space-y
|
||||
className="flex gap-4"
|
||||
className="space-y-4"
|
||||
|
||||
Internal element spacing? → Use padding
|
||||
className="p-4"
|
||||
|
||||
External element spacing? → Avoid margins, use parent gap
|
||||
// ❌ Child with margin
|
||||
<div className="mb-4">
|
||||
|
||||
// ✅ Parent with gap
|
||||
<div className="space-y-4">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Common Mistakes to Avoid
|
||||
|
||||
### ❌ Mistake 1: Hardcoding colors
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<div className="bg-red-500 text-white">Error</div>
|
||||
|
||||
// ✅ CORRECT
|
||||
<Alert variant="destructive">Error message</Alert>
|
||||
```
|
||||
|
||||
### ❌ Mistake 2: Arbitrary spacing
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<div className="p-[15px] mb-[23px]">
|
||||
|
||||
// ✅ CORRECT
|
||||
<div className="p-4 mb-6">
|
||||
```
|
||||
|
||||
### ❌ Mistake 3: Missing accessibility
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<input type="email" />
|
||||
|
||||
// ✅ CORRECT
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" />
|
||||
```
|
||||
|
||||
### ❌ Mistake 4: Creating custom components unnecessarily
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Custom component for simple composition
|
||||
function MyCard({ title, children }) {
|
||||
return <div className="card">{children}</div>;
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Use shadcn/ui primitives
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### ❌ Mistake 5: Not using cn() utility
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<div className={`base-class ${isActive ? 'active-class' : ''} ${className}`}>
|
||||
|
||||
// ✅ CORRECT
|
||||
<div className={cn("base-class", isActive && "active-class", className)}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Reference Documentation
|
||||
|
||||
Before generating code, check these resources:
|
||||
|
||||
1. **[Quick Start](./00-quick-start.md)** - Essential patterns
|
||||
2. **[Components](./02-components.md)** - All shadcn/ui components
|
||||
3. **[Layouts](./03-layouts.md)** - Layout patterns
|
||||
4. **[Spacing](./04-spacing-philosophy.md)** - Spacing rules
|
||||
5. **[Forms](./06-forms.md)** - Form patterns
|
||||
6. **[Reference](./99-reference.md)** - Quick lookup tables
|
||||
|
||||
---
|
||||
|
||||
## ✅ Code Generation Checklist
|
||||
|
||||
Before outputting code, verify:
|
||||
|
||||
- [ ] All imports from `@/components/ui/*`
|
||||
- [ ] Using semantic color tokens (no `bg-blue-500`)
|
||||
- [ ] Using spacing scale (multiples of 4)
|
||||
- [ ] Using `cn()` for className merging
|
||||
- [ ] Accessibility attributes included
|
||||
- [ ] Mobile-first responsive design
|
||||
- [ ] Composing from shadcn/ui primitives
|
||||
- [ ] Following established patterns from docs
|
||||
- [ ] No inline styles
|
||||
- [ ] No arbitrary values
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI Assistant Configuration
|
||||
|
||||
### For Claude Code / Cursor
|
||||
|
||||
Add this to your project context:
|
||||
|
||||
```
|
||||
When generating React/Next.js components:
|
||||
1. Always import from @/components/ui/*
|
||||
2. Use semantic tokens (bg-primary, text-destructive)
|
||||
3. Use cn() utility for classNames
|
||||
4. Follow spacing scale (4, 8, 12, 16, 24, 32)
|
||||
5. Add accessibility (labels, ARIA)
|
||||
6. Use component variants (variant="destructive")
|
||||
7. Reference: /docs/design-system/08-ai-guidelines.md
|
||||
```
|
||||
|
||||
### For GitHub Copilot
|
||||
|
||||
Add to `.github/copilot-instructions.md`:
|
||||
|
||||
```markdown
|
||||
# Component Guidelines
|
||||
|
||||
- Import from @/components/ui/*
|
||||
- Use semantic colors: bg-primary, text-destructive
|
||||
- Spacing: multiples of 4 (p-4, mb-6, gap-8)
|
||||
- Use cn() for className merging
|
||||
- Add accessibility attributes
|
||||
- See /docs/design-system/08-ai-guidelines.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Examples
|
||||
|
||||
### ✅ Good Component (AI Generated)
|
||||
|
||||
```tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface DashboardCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
trend?: 'up' | 'down';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DashboardCard({ title, value, trend, className }: DashboardCardProps) {
|
||||
return (
|
||||
<Card className={cn("p-6", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{trend && (
|
||||
<p className={cn(
|
||||
"text-xs",
|
||||
trend === 'up' && "text-green-600",
|
||||
trend === 'down' && "text-destructive"
|
||||
)}>
|
||||
{trend === 'up' ? '↑' : '↓'} Trend
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's good:**
|
||||
- ✅ Imports from `@/components/ui/*`
|
||||
- ✅ Uses semantic tokens
|
||||
- ✅ Uses `cn()` utility
|
||||
- ✅ Follows spacing scale
|
||||
- ✅ Composes from shadcn/ui primitives
|
||||
- ✅ TypeScript interfaces
|
||||
- ✅ Allows className override
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Path for AI
|
||||
|
||||
1. Read [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
2. Read this document - Rules and templates
|
||||
3. Reference [Component Guide](./02-components.md) - All components
|
||||
4. Check [Reference Tables](./99-reference.md) - Token lookups
|
||||
|
||||
With these guidelines, you can generate code that perfectly matches the design system. Always prioritize consistency over creativity.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
**For AI Assistants**: Follow these rules strictly for optimal code generation.
|
||||
599
frontend/docs/design-system/99-reference.md
Normal file
599
frontend/docs/design-system/99-reference.md
Normal file
@@ -0,0 +1,599 @@
|
||||
# Quick Reference
|
||||
|
||||
**Bookmark this page** for instant lookups of colors, spacing, typography, components, and common patterns. Your go-to cheat sheet for the FastNext design system.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Color Tokens](#color-tokens)
|
||||
2. [Typography Scale](#typography-scale)
|
||||
3. [Spacing Scale](#spacing-scale)
|
||||
4. [Component Variants](#component-variants)
|
||||
5. [Layout Patterns](#layout-patterns)
|
||||
6. [Common Class Combinations](#common-class-combinations)
|
||||
7. [Decision Trees](#decision-trees)
|
||||
|
||||
---
|
||||
|
||||
## Color Tokens
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
| Token | Usage | Example |
|
||||
|-------|-------|---------|
|
||||
| `bg-primary text-primary-foreground` | CTAs, primary actions | Primary button |
|
||||
| `bg-secondary text-secondary-foreground` | Secondary actions | Secondary button |
|
||||
| `bg-destructive text-destructive-foreground` | Delete, errors | Delete button, error alert |
|
||||
| `bg-muted text-muted-foreground` | Disabled, subtle | Disabled button, TabsList |
|
||||
| `bg-accent text-accent-foreground` | Hover states | Dropdown hover |
|
||||
| `bg-card text-card-foreground` | Cards, elevated surfaces | Card component |
|
||||
| `bg-popover text-popover-foreground` | Popovers, dropdowns | Dropdown content |
|
||||
| `bg-background text-foreground` | Page background | Body |
|
||||
| `text-foreground` | Body text | Paragraphs |
|
||||
| `text-muted-foreground` | Secondary text | Captions, helper text |
|
||||
| `border-border` | Borders, dividers | Card borders, separators |
|
||||
| `border-input` | Input borders | Text input border |
|
||||
| `ring-ring` | Focus indicators | Focus ring |
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```tsx
|
||||
// Primary button
|
||||
<Button className="bg-primary text-primary-foreground">Save</Button>
|
||||
|
||||
// Destructive button
|
||||
<Button className="bg-destructive text-destructive-foreground">Delete</Button>
|
||||
|
||||
// Secondary text
|
||||
<p className="text-muted-foreground text-sm">Helper text</p>
|
||||
|
||||
// Card
|
||||
<Card className="bg-card text-card-foreground border-border">...</Card>
|
||||
|
||||
// Focus ring
|
||||
<div className="focus-visible:ring-2 focus-visible:ring-ring">...</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography Scale
|
||||
|
||||
### Font Sizes
|
||||
|
||||
| Class | rem | px | Use Case | Common |
|
||||
|-------|-----|----|----|:------:|
|
||||
| `text-xs` | 0.75rem | 12px | Labels, fine print | |
|
||||
| `text-sm` | 0.875rem | 14px | Secondary text, captions | ⭐ |
|
||||
| `text-base` | 1rem | 16px | Body text (default) | ⭐ |
|
||||
| `text-lg` | 1.125rem | 18px | Subheadings | |
|
||||
| `text-xl` | 1.25rem | 20px | Card titles | ⭐ |
|
||||
| `text-2xl` | 1.5rem | 24px | Section headings | ⭐ |
|
||||
| `text-3xl` | 1.875rem | 30px | Page titles | ⭐ |
|
||||
| `text-4xl` | 2.25rem | 36px | Large headings | |
|
||||
| `text-5xl` | 3rem | 48px | Hero text | |
|
||||
|
||||
⭐ = Most commonly used
|
||||
|
||||
### Font Weights
|
||||
|
||||
| Class | Value | Use Case | Common |
|
||||
|-------|-------|----------|:------:|
|
||||
| `font-light` | 300 | De-emphasized text | |
|
||||
| `font-normal` | 400 | Body text (default) | ⭐ |
|
||||
| `font-medium` | 500 | Labels, menu items | ⭐ |
|
||||
| `font-semibold` | 600 | Subheadings, buttons | ⭐ |
|
||||
| `font-bold` | 700 | Headings, emphasis | ⭐ |
|
||||
|
||||
⭐ = Most commonly used
|
||||
|
||||
### Common Typography Combinations
|
||||
|
||||
```tsx
|
||||
// Page title
|
||||
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||
|
||||
// Section heading
|
||||
<h2 className="text-2xl font-semibold mb-4">Section Heading</h2>
|
||||
|
||||
// Card title
|
||||
<h3 className="text-xl font-semibold">Card Title</h3>
|
||||
|
||||
// Body text (default)
|
||||
<p className="text-base text-foreground">Regular paragraph</p>
|
||||
|
||||
// Secondary text
|
||||
<p className="text-sm text-muted-foreground">Helper text</p>
|
||||
|
||||
// Label
|
||||
<Label className="text-sm font-medium">Field Label</Label>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
### Spacing Values
|
||||
|
||||
| Token | rem | px | Use Case | Common |
|
||||
|-------|-----|----|----|:------:|
|
||||
| `0` | 0 | 0px | No spacing | |
|
||||
| `px` | - | 1px | Borders | |
|
||||
| `0.5` | 0.125rem | 2px | Very tight | |
|
||||
| `1` | 0.25rem | 4px | Icon gaps | |
|
||||
| `2` | 0.5rem | 8px | Tight spacing (label → input) | ⭐ |
|
||||
| `3` | 0.75rem | 12px | Component padding | |
|
||||
| `4` | 1rem | 16px | Standard spacing (form fields) | ⭐ |
|
||||
| `5` | 1.25rem | 20px | Medium spacing | |
|
||||
| `6` | 1.5rem | 24px | Section spacing (cards) | ⭐ |
|
||||
| `8` | 2rem | 32px | Large gaps | ⭐ |
|
||||
| `10` | 2.5rem | 40px | Very large gaps | |
|
||||
| `12` | 3rem | 48px | Section dividers | ⭐ |
|
||||
| `16` | 4rem | 64px | Page sections | |
|
||||
|
||||
⭐ = Most commonly used
|
||||
|
||||
### Spacing Methods
|
||||
|
||||
| Method | Use Case | Example |
|
||||
|--------|----------|---------|
|
||||
| `gap-4` | Flex/grid spacing | `flex gap-4` |
|
||||
| `space-y-4` | Vertical stack spacing | `space-y-4` |
|
||||
| `space-x-4` | Horizontal stack spacing | `space-x-4` |
|
||||
| `p-4` | Padding (all sides) | `p-4` |
|
||||
| `px-4` | Horizontal padding | `px-4` |
|
||||
| `py-4` | Vertical padding | `py-4` |
|
||||
| `m-4` | Margin (exceptions only!) | `mt-8` |
|
||||
|
||||
### Common Spacing Patterns
|
||||
|
||||
```tsx
|
||||
// Form vertical spacing
|
||||
<form className="space-y-4">...</form>
|
||||
|
||||
// Field group spacing (label → input)
|
||||
<div className="space-y-2">
|
||||
<Label>...</Label>
|
||||
<Input />
|
||||
</div>
|
||||
|
||||
// Button group horizontal spacing
|
||||
<div className="flex gap-4">
|
||||
<Button>Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
// Card grid spacing
|
||||
<div className="grid grid-cols-3 gap-6">...</div>
|
||||
|
||||
// Page padding
|
||||
<div className="container mx-auto px-4 py-8">...</div>
|
||||
|
||||
// Card padding
|
||||
<Card className="p-6">...</Card>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Variants
|
||||
|
||||
### Button Variants
|
||||
|
||||
```tsx
|
||||
<Button variant="default">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="icon"><Icon /></Button>
|
||||
```
|
||||
|
||||
### Badge Variants
|
||||
|
||||
```tsx
|
||||
<Badge variant="default">New</Badge>
|
||||
<Badge variant="secondary">Draft</Badge>
|
||||
<Badge variant="outline">Pending</Badge>
|
||||
<Badge variant="destructive">Critical</Badge>
|
||||
```
|
||||
|
||||
### Alert Variants
|
||||
|
||||
```tsx
|
||||
<Alert variant="default">Info alert</Alert>
|
||||
<Alert variant="destructive">Error alert</Alert>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### Grid Columns
|
||||
|
||||
```tsx
|
||||
// 1 → 2 → 3 progression (most common)
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
|
||||
// 1 → 2 → 4 progression
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
|
||||
|
||||
// 1 → 2 progression (simple)
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
|
||||
// 1 → 3 progression (skip 2)
|
||||
className="grid grid-cols-1 lg:grid-cols-3 gap-6"
|
||||
```
|
||||
|
||||
### Container Widths
|
||||
|
||||
```tsx
|
||||
// Standard container
|
||||
className="container mx-auto px-4"
|
||||
|
||||
// Constrained widths
|
||||
className="max-w-md mx-auto" // 448px - Forms
|
||||
className="max-w-lg mx-auto" // 512px - Modals
|
||||
className="max-w-2xl mx-auto" // 672px - Articles
|
||||
className="max-w-4xl mx-auto" // 896px - Wide layouts
|
||||
className="max-w-7xl mx-auto" // 1280px - Full page
|
||||
```
|
||||
|
||||
### Flex Patterns
|
||||
|
||||
```tsx
|
||||
// Horizontal flex
|
||||
className="flex gap-4"
|
||||
|
||||
// Vertical flex
|
||||
className="flex flex-col gap-4"
|
||||
|
||||
// Center items
|
||||
className="flex items-center justify-center"
|
||||
|
||||
// Space between
|
||||
className="flex items-center justify-between"
|
||||
|
||||
// Wrap items
|
||||
className="flex flex-wrap gap-4"
|
||||
|
||||
// Responsive: stack on mobile, row on desktop
|
||||
className="flex flex-col sm:flex-row gap-4"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Class Combinations
|
||||
|
||||
### Page Container
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Card Header with Action
|
||||
|
||||
```tsx
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<div>
|
||||
<CardTitle>Title</CardTitle>
|
||||
<CardDescription>Description</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Action</Button>
|
||||
</CardHeader>
|
||||
```
|
||||
|
||||
### Dashboard Metric Card Header
|
||||
|
||||
```tsx
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Metric Title</CardTitle>
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
```
|
||||
|
||||
### Form Field
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="field">Label</Label>
|
||||
<Input id="field" />
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Centered Form Card
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Form Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Fields */}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Button Group
|
||||
|
||||
```tsx
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
|
||||
// Or right-aligned
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Icon with Text
|
||||
|
||||
```tsx
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>Text</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Responsive Text
|
||||
|
||||
```tsx
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
|
||||
Responsive Title
|
||||
</h1>
|
||||
```
|
||||
|
||||
### Responsive Padding
|
||||
|
||||
```tsx
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
Responsive padding
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Trees
|
||||
|
||||
### Grid vs Flex
|
||||
|
||||
```
|
||||
Need equal-width columns? → Grid
|
||||
Example: grid grid-cols-3 gap-6
|
||||
|
||||
Need flexible item sizes? → Flex
|
||||
Example: flex gap-4
|
||||
|
||||
Need 2D layout (rows + columns)? → Grid
|
||||
Example: grid grid-cols-2 grid-rows-3 gap-4
|
||||
|
||||
Need 1D layout (row OR column)? → Flex
|
||||
Example: flex flex-col gap-4
|
||||
```
|
||||
|
||||
### Margin vs Padding vs Gap
|
||||
|
||||
```
|
||||
Spacing between siblings?
|
||||
├─ Flex/Grid parent? → gap
|
||||
└─ Regular parent? → space-y or space-x
|
||||
|
||||
Inside component? → padding
|
||||
|
||||
Exception case (one child different)? → margin
|
||||
```
|
||||
|
||||
### Button Variant
|
||||
|
||||
```
|
||||
What's the action?
|
||||
├─ Primary action (save, submit) → variant="default"
|
||||
├─ Secondary action (cancel, back) → variant="secondary"
|
||||
├─ Alternative action (view, edit) → variant="outline"
|
||||
├─ Subtle action (icon in list) → variant="ghost"
|
||||
├─ In-text action (learn more) → variant="link"
|
||||
└─ Delete/remove action → variant="destructive"
|
||||
```
|
||||
|
||||
### Form Field Error Display
|
||||
|
||||
```
|
||||
Has error?
|
||||
├─YES─> Add aria-invalid={true}
|
||||
│ Add aria-describedby="field-error"
|
||||
│ Add border-destructive class
|
||||
│ Show <p id="field-error" className="text-sm text-destructive">
|
||||
│
|
||||
└─NO──> Normal state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action | Context |
|
||||
|-----|--------|---------|
|
||||
| `Tab` | Move focus forward | All |
|
||||
| `Shift + Tab` | Move focus backward | All |
|
||||
| `Enter` | Activate button/link | Buttons, links |
|
||||
| `Space` | Activate button/checkbox | Buttons, checkboxes |
|
||||
| `Escape` | Close overlay | Dialogs, dropdowns |
|
||||
| `Arrow keys` | Navigate items | Dropdowns, lists |
|
||||
| `Home` | Jump to start | Lists |
|
||||
| `End` | Jump to end | Lists |
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Quick Checks
|
||||
|
||||
### Contrast Ratios
|
||||
|
||||
- **Normal text (< 18px)**: 4.5:1 minimum
|
||||
- **Large text (≥ 18px or ≥ 14px bold)**: 3:1 minimum
|
||||
- **UI components**: 3:1 minimum
|
||||
|
||||
### ARIA Attributes
|
||||
|
||||
```tsx
|
||||
// Icon-only button
|
||||
<Button size="icon" aria-label="Close">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
// Form field error
|
||||
<Input
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? 'field-error' : undefined}
|
||||
/>
|
||||
{error && <p id="field-error">{error.message}</p>}
|
||||
|
||||
// Required field
|
||||
<Input aria-required="true" required />
|
||||
|
||||
// Live region
|
||||
<div aria-live="polite">{statusMessage}</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Import Cheat Sheet
|
||||
|
||||
```tsx
|
||||
// Components
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
|
||||
// Utilities
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Form
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Toast
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Icons
|
||||
import { Check, X, AlertCircle, Loader2 } from 'lucide-react';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zod Validation Patterns
|
||||
|
||||
```tsx
|
||||
// Required string
|
||||
z.string().min(1, 'Required')
|
||||
|
||||
// Email
|
||||
z.string().email('Invalid email')
|
||||
|
||||
// Min/max length
|
||||
z.string().min(8, 'Min 8 chars').max(100, 'Max 100 chars')
|
||||
|
||||
// Optional
|
||||
z.string().optional()
|
||||
|
||||
// Number
|
||||
z.coerce.number().min(0).max(100)
|
||||
|
||||
// Enum
|
||||
z.enum(['admin', 'user', 'guest'])
|
||||
|
||||
// Boolean
|
||||
z.boolean().refine(val => val === true, { message: 'Must accept' })
|
||||
|
||||
// Password confirmation
|
||||
z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string()
|
||||
}).refine(data => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive Breakpoints
|
||||
|
||||
| Breakpoint | Min Width | Typical Device |
|
||||
|------------|-----------|----------------|
|
||||
| `sm:` | 640px | Large phones, small tablets |
|
||||
| `md:` | 768px | Tablets |
|
||||
| `lg:` | 1024px | Laptops, desktops |
|
||||
| `xl:` | 1280px | Large desktops |
|
||||
| `2xl:` | 1536px | Extra large screens |
|
||||
|
||||
```tsx
|
||||
// Mobile-first (default → sm → md → lg)
|
||||
className="text-sm sm:text-base md:text-lg lg:text-xl"
|
||||
className="p-4 sm:p-6 lg:p-8"
|
||||
className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shadows & Radius
|
||||
|
||||
### Shadows
|
||||
|
||||
```tsx
|
||||
shadow-sm // Cards, panels
|
||||
shadow-md // Dropdowns, tooltips
|
||||
shadow-lg // Modals, popovers
|
||||
shadow-xl // Floating notifications
|
||||
```
|
||||
|
||||
### Border Radius
|
||||
|
||||
```tsx
|
||||
rounded-sm // 2px - Tags, small badges
|
||||
rounded-md // 4px - Inputs, small buttons
|
||||
rounded-lg // 6px - Cards, buttons (default)
|
||||
rounded-xl // 10px - Large cards, modals
|
||||
rounded-full // Pills, avatars, icon buttons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **For detailed info**: Navigate to specific guides from [README](./README.md)
|
||||
- **For examples**: Visit [/dev/components](/dev/components)
|
||||
- **For AI**: See [AI Guidelines](./08-ai-guidelines.md)
|
||||
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
- [Quick Start](./00-quick-start.md) - 5-minute crash course
|
||||
- [Foundations](./01-foundations.md) - Detailed color, typography, spacing
|
||||
- [Components](./02-components.md) - All component variants
|
||||
- [Layouts](./03-layouts.md) - Layout patterns
|
||||
- [Forms](./06-forms.md) - Form patterns
|
||||
- [Accessibility](./07-accessibility.md) - WCAG compliance
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
304
frontend/docs/design-system/README.md
Normal file
304
frontend/docs/design-system/README.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Design System Documentation
|
||||
|
||||
**FastNext Template Design System** - A comprehensive guide to building consistent, accessible, and beautiful user interfaces.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Navigation
|
||||
|
||||
| For... | Start Here | Time |
|
||||
|--------|-----------|------|
|
||||
| **Quick Start** | [⚡ 5-Minute Crash Course](./00-quick-start.md) | 5 min |
|
||||
| **Component Development** | [🧩 Components](./02-components.md) → [🔨 Creation Guide](./05-component-creation.md) | 15 min |
|
||||
| **Layout Design** | [📐 Layouts](./03-layouts.md) → [📏 Spacing](./04-spacing-philosophy.md) | 20 min |
|
||||
| **AI Code Generation** | [🤖 AI Guidelines](./08-ai-guidelines.md) | 3 min |
|
||||
| **Quick Reference** | [📚 Reference Tables](./99-reference.md) | Instant |
|
||||
| **Complete Guide** | Read all docs in order | 1 hour |
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentation Structure
|
||||
|
||||
### Getting Started
|
||||
- **[00. Quick Start](./00-quick-start.md)** ⚡
|
||||
- 5-minute crash course
|
||||
- Essential components and patterns
|
||||
- Copy-paste ready examples
|
||||
|
||||
### Fundamentals
|
||||
- **[01. Foundations](./01-foundations.md)** 🎨
|
||||
- Color system (OKLCH)
|
||||
- Typography scale
|
||||
- Spacing tokens
|
||||
- Shadows & radius
|
||||
|
||||
- **[02. Components](./02-components.md)** 🧩
|
||||
- shadcn/ui component library
|
||||
- All variants documented
|
||||
- Usage examples
|
||||
- Composition patterns
|
||||
|
||||
### Layouts & Spacing
|
||||
- **[03. Layouts](./03-layouts.md)** 📐
|
||||
- Grid vs Flex decision tree
|
||||
- Common layout patterns
|
||||
- Responsive strategies
|
||||
- Before/after examples
|
||||
|
||||
- **[04. Spacing Philosophy](./04-spacing-philosophy.md)** 📏
|
||||
- Parent vs child spacing rules
|
||||
- Margin vs padding strategy
|
||||
- Gap vs margin for flex/grid
|
||||
- Consistency patterns
|
||||
|
||||
### Building Components
|
||||
- **[05. Component Creation](./05-component-creation.md)** 🔨
|
||||
- When to create vs compose
|
||||
- Component templates
|
||||
- Variant patterns (CVA)
|
||||
- Testing checklist
|
||||
|
||||
- **[06. Forms](./06-forms.md)** 📝
|
||||
- Form patterns & validation
|
||||
- Error state UI
|
||||
- Loading states
|
||||
- Multi-field examples
|
||||
|
||||
### Best Practices
|
||||
- **[07. Accessibility](./07-accessibility.md)** ♿
|
||||
- WCAG AA compliance
|
||||
- Keyboard navigation
|
||||
- Screen reader support
|
||||
- ARIA attributes
|
||||
|
||||
- **[08. AI Guidelines](./08-ai-guidelines.md)** 🤖
|
||||
- Rules for AI code generation
|
||||
- Required patterns
|
||||
- Forbidden practices
|
||||
- Component templates
|
||||
|
||||
### Reference
|
||||
- **[99. Reference Tables](./99-reference.md)** 📚
|
||||
- Quick lookup tables
|
||||
- All tokens at a glance
|
||||
- Cheat sheet
|
||||
|
||||
---
|
||||
|
||||
## 🎪 Interactive Examples
|
||||
|
||||
Explore live examples and copy-paste code:
|
||||
|
||||
- **[Component Showcase](/dev/components)** - All shadcn/ui components with variants
|
||||
- **[Layout Patterns](/dev/layouts)** - Before/after comparisons of layouts
|
||||
- **[Spacing Examples](/dev/spacing)** - Visual spacing demonstrations
|
||||
- **[Form Patterns](/dev/forms)** - Complete form examples
|
||||
|
||||
Each demo page includes:
|
||||
- ✅ Live, interactive examples
|
||||
- ✅ Click-to-copy code snippets
|
||||
- ✅ Before/after comparisons
|
||||
- ✅ Links to documentation
|
||||
|
||||
---
|
||||
|
||||
## 🛤️ Learning Paths
|
||||
|
||||
### Path 1: Speedrun (5 minutes)
|
||||
**Goal**: Start building immediately
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
2. [Reference](./99-reference.md) - Bookmark for lookup
|
||||
3. Start coding!
|
||||
|
||||
**When to use**: You need to build something NOW and will learn deeply later.
|
||||
|
||||
---
|
||||
|
||||
### Path 2: Component Developer (15 minutes)
|
||||
**Goal**: Master component building
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Basics
|
||||
2. [Components](./02-components.md) - shadcn/ui library
|
||||
3. [Component Creation](./05-component-creation.md) - Building custom components
|
||||
4. [Reference](./99-reference.md) - Bookmark
|
||||
|
||||
**When to use**: You're building reusable components or UI library.
|
||||
|
||||
---
|
||||
|
||||
### Path 3: Layout Specialist (20 minutes)
|
||||
**Goal**: Master layouts and spacing
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Basics
|
||||
2. [Foundations](./01-foundations.md) - Spacing tokens
|
||||
3. [Layouts](./03-layouts.md) - Grid vs Flex patterns
|
||||
4. [Spacing Philosophy](./04-spacing-philosophy.md) - Margin/padding rules
|
||||
5. [Reference](./99-reference.md) - Bookmark
|
||||
|
||||
**When to use**: You're designing page layouts or dashboard UIs.
|
||||
|
||||
---
|
||||
|
||||
### Path 4: Form Specialist (15 minutes)
|
||||
**Goal**: Master forms and validation
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Basics
|
||||
2. [Components](./02-components.md) - Form components
|
||||
3. [Forms](./06-forms.md) - Patterns & validation
|
||||
4. [Accessibility](./07-accessibility.md) - ARIA for forms
|
||||
5. [Reference](./99-reference.md) - Bookmark
|
||||
|
||||
**When to use**: You're building forms with complex validation.
|
||||
|
||||
---
|
||||
|
||||
### Path 5: AI Setup (3 minutes)
|
||||
**Goal**: Configure AI for perfect code generation
|
||||
|
||||
1. [AI Guidelines](./08-ai-guidelines.md) - Read once, code forever
|
||||
2. Reference this in your AI context/prompts
|
||||
|
||||
**When to use**: You're using AI assistants (Claude, GitHub Copilot, etc.) to generate code.
|
||||
|
||||
---
|
||||
|
||||
### Path 6: Comprehensive Mastery (1 hour)
|
||||
**Goal**: Complete understanding of the design system
|
||||
|
||||
Read all documents in order:
|
||||
1. [Quick Start](./00-quick-start.md)
|
||||
2. [Foundations](./01-foundations.md)
|
||||
3. [Components](./02-components.md)
|
||||
4. [Layouts](./03-layouts.md)
|
||||
5. [Spacing Philosophy](./04-spacing-philosophy.md)
|
||||
6. [Component Creation](./05-component-creation.md)
|
||||
7. [Forms](./06-forms.md)
|
||||
8. [Accessibility](./07-accessibility.md)
|
||||
9. [AI Guidelines](./08-ai-guidelines.md)
|
||||
10. [Reference](./99-reference.md)
|
||||
|
||||
Explore all [interactive demos](/dev).
|
||||
|
||||
**When to use**: You're the design system maintainer or want complete mastery.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Principles
|
||||
|
||||
Our design system is built on these core principles:
|
||||
|
||||
1. **🎨 Semantic First** - Use `bg-primary`, not `bg-blue-500`
|
||||
2. **♿ Accessible by Default** - WCAG AA minimum, keyboard-first
|
||||
3. **📐 Consistent Spacing** - Multiples of 4px (0.25rem)
|
||||
4. **🧩 Compose, Don't Create** - Use shadcn/ui primitives
|
||||
5. **🌗 Dark Mode Ready** - All components work in light/dark
|
||||
6. **⚡ Pareto Efficient** - 80% of needs with 20% of patterns
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Technology Stack
|
||||
|
||||
- **Framework**: Next.js 15 + React 19
|
||||
- **Styling**: Tailwind CSS 4 (CSS-first configuration)
|
||||
- **Components**: shadcn/ui (New York style)
|
||||
- **Color Space**: OKLCH (perceptually uniform)
|
||||
- **Icons**: lucide-react
|
||||
- **Fonts**: Geist Sans + Geist Mono
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing to the Design System
|
||||
|
||||
### Adding a New Component
|
||||
1. Read [Component Creation Guide](./05-component-creation.md)
|
||||
2. Follow the template
|
||||
3. Add to [Component Showcase](/dev/components)
|
||||
4. Document in [Components](./02-components.md)
|
||||
|
||||
### Adding a New Pattern
|
||||
1. Validate it solves a real need (used 3+ times)
|
||||
2. Document in appropriate guide
|
||||
3. Add to [Reference](./99-reference.md)
|
||||
4. Create example in `/dev/`
|
||||
|
||||
### Updating Colors/Tokens
|
||||
1. Edit `src/app/globals.css`
|
||||
2. Test in both light and dark modes
|
||||
3. Verify WCAG AA contrast
|
||||
4. Update [Foundations](./01-foundations.md)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Quick Reference
|
||||
|
||||
### Most Common Patterns
|
||||
|
||||
```tsx
|
||||
// Button
|
||||
<Button variant="default">Action</Button>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
|
||||
// Card
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>Content</CardContent>
|
||||
</Card>
|
||||
|
||||
// Form Input
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" {...field} />
|
||||
{errors.email && (
|
||||
<p className="text-sm text-destructive">{errors.email.message}</p>
|
||||
)}
|
||||
|
||||
// Layout
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Content */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Grid
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map(item => <Card key={item.id}>...</Card>)}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Need Help?
|
||||
|
||||
1. **Quick Answer**: Check [Reference](./99-reference.md)
|
||||
2. **Pattern Question**: Search relevant doc (Layouts, Components, etc.)
|
||||
3. **Can't Find It**: Browse [Interactive Examples](/dev)
|
||||
4. **Still Stuck**: Read [Quick Start](./00-quick-start.md) or [Comprehensive Guide](#path-6-comprehensive-mastery-1-hour)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Design System Metrics
|
||||
|
||||
- **Components**: 20+ shadcn/ui components
|
||||
- **Color Tokens**: 25+ semantic color variables
|
||||
- **Layout Patterns**: 5 essential patterns (80% coverage)
|
||||
- **Spacing Scale**: 14 token sizes (0-16)
|
||||
- **Typography Scale**: 9 sizes (xs-9xl)
|
||||
- **Test Coverage**: All patterns demonstrated in /dev/
|
||||
|
||||
---
|
||||
|
||||
## 📚 External Resources
|
||||
|
||||
- [shadcn/ui Documentation](https://ui.shadcn.com)
|
||||
- [Tailwind CSS 4 Documentation](https://tailwindcss.com/docs)
|
||||
- [OKLCH Color Picker](https://oklch.com)
|
||||
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [Radix UI Primitives](https://www.radix-ui.com/primitives)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: November 2, 2025
|
||||
**Version**: 1.0
|
||||
**Maintainer**: Design System Team
|
||||
Reference in New Issue
Block a user