Refactor useAuth hook, settings components, and docs for formatting and readability improvements
- Consolidated multi-line arguments into single lines where appropriate in `useAuth`. - Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`). - Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`). - Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
This commit is contained in:
@@ -66,7 +66,7 @@ import {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter
|
||||
CardFooter,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
<Card>
|
||||
@@ -80,7 +80,7 @@ import {
|
||||
<CardFooter>
|
||||
<Button>Save</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Card>;
|
||||
```
|
||||
|
||||
**[See card examples](/dev/components#card)**
|
||||
@@ -95,18 +95,9 @@ 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>
|
||||
<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)**
|
||||
@@ -123,7 +114,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTrigger
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
<Dialog>
|
||||
@@ -133,16 +124,14 @@ import {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Action</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to proceed?
|
||||
</DialogDescription>
|
||||
<DialogDescription>Are you sure you want to proceed?</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Confirm</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog>;
|
||||
```
|
||||
|
||||
**[See dialog examples](/dev/components#dialog)**
|
||||
@@ -197,7 +186,7 @@ import { AlertCircle } from 'lucide-react';
|
||||
```tsx
|
||||
// Responsive card grid
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{items.map(item => (
|
||||
{items.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
@@ -218,9 +207,7 @@ import { AlertCircle } from 'lucide-react';
|
||||
<CardTitle>Login</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Form fields */}
|
||||
</form>
|
||||
<form className="space-y-4">{/* Form fields */}</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -247,6 +234,7 @@ import { AlertCircle } from 'lucide-react';
|
||||
```
|
||||
|
||||
**Available tokens:**
|
||||
|
||||
- `primary` - Main brand color, CTAs
|
||||
- `destructive` - Errors, delete actions
|
||||
- `muted` - Disabled states, subtle backgrounds
|
||||
@@ -276,6 +264,7 @@ import { AlertCircle } from 'lucide-react';
|
||||
```
|
||||
|
||||
**Common spacing values:**
|
||||
|
||||
- `2` (8px) - Tight spacing
|
||||
- `4` (16px) - Standard spacing
|
||||
- `6` (24px) - Section spacing
|
||||
@@ -326,6 +315,7 @@ import { AlertCircle } from 'lucide-react';
|
||||
```
|
||||
|
||||
**Breakpoints:**
|
||||
|
||||
- `sm:` 640px+
|
||||
- `md:` 768px+
|
||||
- `lg:` 1024px+
|
||||
@@ -370,11 +360,9 @@ import { AlertCircle } from 'lucide-react';
|
||||
```tsx
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-12 w-full" />
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
{
|
||||
isLoading ? <Skeleton className="h-12 w-full" /> : <div>{content}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Dropdown Menu
|
||||
@@ -384,7 +372,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
<DropdownMenu>
|
||||
@@ -395,7 +383,7 @@ import {
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DropdownMenu>;
|
||||
```
|
||||
|
||||
### Badge/Tag
|
||||
@@ -415,17 +403,20 @@ import { Badge } from '@/components/ui/badge';
|
||||
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
|
||||
|
||||
@@ -449,6 +440,7 @@ Remember these and you'll be 95% compliant:
|
||||
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
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
### 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
|
||||
@@ -55,6 +56,7 @@ We use **OKLCH** (Oklab LCH) color space for:
|
||||
### Semantic Color Tokens
|
||||
|
||||
All colors follow the **background/foreground** convention:
|
||||
|
||||
- `background` - The background color
|
||||
- `foreground` - The text color that goes on that background
|
||||
|
||||
@@ -68,11 +70,12 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```css
|
||||
/* Light & Dark Mode */
|
||||
--primary: oklch(0.6231 0.1880 259.8145) /* Blue */
|
||||
--primary-foreground: oklch(1 0 0) /* White text */
|
||||
--primary: oklch(0.6231 0.188 259.8145) /* Blue */ --primary-foreground: oklch(1 0 0)
|
||||
/* White text */;
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
|
||||
```tsx
|
||||
// Primary button (most common)
|
||||
<Button>Save Changes</Button>
|
||||
@@ -87,12 +90,14 @@ All colors follow the **background/foreground** convention:
|
||||
```
|
||||
|
||||
**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`)
|
||||
@@ -105,15 +110,14 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```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 */
|
||||
--secondary: oklch(0.967 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>
|
||||
@@ -135,15 +139,12 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```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)
|
||||
--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>
|
||||
@@ -165,6 +166,7 @@ All colors follow the **background/foreground** convention:
|
||||
```
|
||||
|
||||
**Common use cases**:
|
||||
|
||||
- Disabled button backgrounds
|
||||
- Placeholder/skeleton loaders
|
||||
- TabsList backgrounds
|
||||
@@ -179,15 +181,12 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```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)
|
||||
--accent: oklch(0.9514 0.025 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>
|
||||
@@ -205,6 +204,7 @@ All colors follow the **background/foreground** convention:
|
||||
```
|
||||
|
||||
**Common use cases**:
|
||||
|
||||
- Dropdown menu item hover states
|
||||
- Command palette hover states
|
||||
- Highlighted sections
|
||||
@@ -218,11 +218,12 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```css
|
||||
/* Light & Dark Mode */
|
||||
--destructive: oklch(0.6368 0.2078 25.3313) /* Red */
|
||||
--destructive-foreground: oklch(1 0 0) /* White text */
|
||||
--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>
|
||||
@@ -246,6 +247,7 @@ All colors follow the **background/foreground** convention:
|
||||
```
|
||||
|
||||
**When to use**:
|
||||
|
||||
- ✅ Delete/remove actions
|
||||
- ✅ Error messages
|
||||
- ✅ Validation errors
|
||||
@@ -259,19 +261,15 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```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 */
|
||||
--card: oklch(1 0 0) /* White */ --card-foreground: oklch(0.1529 0 0) /* Dark text */
|
||||
--popover: oklch(1 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>
|
||||
@@ -296,15 +294,12 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```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)
|
||||
--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" />
|
||||
@@ -329,10 +324,11 @@ All colors follow the **background/foreground** convention:
|
||||
|
||||
```css
|
||||
/* Light & Dark Mode */
|
||||
--ring: oklch(0.6231 0.1880 259.8145) /* Primary blue */
|
||||
--ring: oklch(0.6231 0.188 259.8145) /* Primary blue */;
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
|
||||
```tsx
|
||||
// Button with focus ring (automatic)
|
||||
<Button>Click me</Button>
|
||||
@@ -355,14 +351,14 @@ All colors follow the **background/foreground** convention:
|
||||
**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 */
|
||||
--chart-1: oklch(0.6231 0.188 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 = [
|
||||
@@ -436,12 +432,13 @@ What's the purpose?
|
||||
### 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
|
||||
--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>
|
||||
@@ -457,21 +454,21 @@ What's the purpose?
|
||||
|
||||
### 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** |
|
||||
| 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**
|
||||
|
||||
@@ -479,13 +476,13 @@ What's the purpose?
|
||||
|
||||
### 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 |
|
||||
| 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**
|
||||
|
||||
@@ -494,35 +491,37 @@ What's the purpose?
|
||||
### 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>
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground">Helper text, timestamps, captions</p>
|
||||
```
|
||||
|
||||
#### Label
|
||||
|
||||
```tsx
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email Address
|
||||
@@ -533,16 +532,17 @@ What's the purpose?
|
||||
|
||||
### 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) |
|
||||
| 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">
|
||||
@@ -622,23 +622,23 @@ Tailwind uses a **0.25rem (4px) base unit**:
|
||||
|
||||
### 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 |
|
||||
| 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**
|
||||
|
||||
@@ -660,18 +660,18 @@ Tailwind uses a **0.25rem (4px) base unit**:
|
||||
|
||||
### 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 |
|
||||
| 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**
|
||||
@@ -729,27 +729,28 @@ Tailwind uses a **0.25rem (4px) base unit**:
|
||||
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-xs:
|
||||
0 1px 3px 0px hsl(0 0% 0% / 0.05) --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1),
|
||||
0 1px 2px -1px hsl(0 0% 0% / 0.1) --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1),
|
||||
0 1px 2px -1px hsl(0 0% 0% / 0.1) --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1),
|
||||
0 2px 4px -1px hsl(0 0% 0% / 0.1) --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1),
|
||||
0 4px 6px -1px hsl(0 0% 0% / 0.1) --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1),
|
||||
0 8px 10px -1px hsl(0 0% 0% / 0.1) --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) |
|
||||
| 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>
|
||||
@@ -779,26 +780,24 @@ Professional shadow system for depth and elevation:
|
||||
Consistent rounded corners across the application:
|
||||
|
||||
```css
|
||||
--radius: 0.375rem; /* 6px - base */
|
||||
--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 */
|
||||
--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** |
|
||||
| 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**
|
||||
|
||||
@@ -854,6 +853,7 @@ Consistent rounded corners across the application:
|
||||
### Most Used Tokens
|
||||
|
||||
**Colors**:
|
||||
|
||||
- `bg-primary text-primary-foreground` - CTAs
|
||||
- `bg-destructive text-destructive-foreground` - Delete/errors
|
||||
- `bg-muted text-muted-foreground` - Disabled/subtle
|
||||
@@ -862,6 +862,7 @@ Consistent rounded corners across the application:
|
||||
- `border-border` - Borders
|
||||
|
||||
**Typography**:
|
||||
|
||||
- `text-3xl font-bold` - Page titles
|
||||
- `text-2xl font-semibold` - Section headings
|
||||
- `text-xl font-semibold` - Card titles
|
||||
@@ -869,6 +870,7 @@ Consistent rounded corners across the application:
|
||||
- `text-sm text-muted-foreground` - Secondary text
|
||||
|
||||
**Spacing**:
|
||||
|
||||
- `p-4` - Standard padding (16px)
|
||||
- `p-6` - Card padding (24px)
|
||||
- `gap-4` - Standard gap (16px)
|
||||
@@ -877,6 +879,7 @@ Consistent rounded corners across the application:
|
||||
- `space-y-6` - Section spacing (24px)
|
||||
|
||||
**Shadows & Radius**:
|
||||
|
||||
- `shadow-sm` - Cards
|
||||
- `shadow-md` - Dropdowns
|
||||
- `shadow-lg` - Modals
|
||||
@@ -896,12 +899,14 @@ Consistent rounded corners across the application:
|
||||
---
|
||||
|
||||
**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)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
We use **[shadcn/ui](https://ui.shadcn.com)**, a collection of accessible, customizable components built on **Radix UI primitives**.
|
||||
|
||||
**Key features:**
|
||||
|
||||
- ✅ **Accessible** - WCAG AA compliant, keyboard navigation, screen reader support
|
||||
- ✅ **Customizable** - Components are copied into your project (not npm dependencies)
|
||||
- ✅ **Composable** - Build complex UIs from simple primitives
|
||||
@@ -41,6 +42,7 @@ npx shadcn@latest add
|
||||
```
|
||||
|
||||
**Installed components** (in `/src/components/ui/`):
|
||||
|
||||
- alert, avatar, badge, button, card, checkbox, dialog
|
||||
- dropdown-menu, input, label, popover, select, separator
|
||||
- sheet, skeleton, table, tabs, textarea, toast
|
||||
@@ -82,16 +84,17 @@ import { Button } from '@/components/ui/button';
|
||||
|
||||
**When to use each variant:**
|
||||
|
||||
| Variant | Use Case | Example |
|
||||
|---------|----------|---------|
|
||||
| `default` | Primary actions, CTAs | Save, Submit, Create |
|
||||
| `secondary` | Secondary actions | Cancel, Back |
|
||||
| `outline` | Alternative actions | View Details, Edit |
|
||||
| `ghost` | Subtle actions in lists | Icon buttons in table rows |
|
||||
| `link` | In-text actions | Read more, Learn more |
|
||||
| `destructive` | Delete, remove actions | Delete Account, Remove |
|
||||
| Variant | Use Case | Example |
|
||||
| ------------- | ----------------------- | -------------------------- |
|
||||
| `default` | Primary actions, CTAs | Save, Submit, Create |
|
||||
| `secondary` | Secondary actions | Cancel, Back |
|
||||
| `outline` | Alternative actions | View Details, Edit |
|
||||
| `ghost` | Subtle actions in lists | Icon buttons in table rows |
|
||||
| `link` | In-text actions | Read more, Learn more |
|
||||
| `destructive` | Delete, remove actions | Delete Account, Remove |
|
||||
|
||||
**Accessibility**:
|
||||
|
||||
- Always add `aria-label` for icon-only buttons
|
||||
- Use `disabled` for unavailable actions (not hidden)
|
||||
- Loading state prevents double-submission
|
||||
@@ -162,6 +165,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
```
|
||||
|
||||
**Pattern: User menu**:
|
||||
|
||||
```tsx
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
@@ -323,6 +327,7 @@ import { Label } from '@/components/ui/label';
|
||||
```
|
||||
|
||||
**Input types:**
|
||||
|
||||
- `text` - Default text input
|
||||
- `email` - Email address
|
||||
- `password` - Password field
|
||||
@@ -530,6 +535,7 @@ import { AlertCircle, CheckCircle, Info } from 'lucide-react';
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- ✅ Form-level errors
|
||||
- ✅ Important warnings
|
||||
- ✅ Success confirmations (inline)
|
||||
@@ -557,14 +563,11 @@ toast.info('Processing your request...');
|
||||
toast.warning('This action cannot be undone');
|
||||
|
||||
// Loading (with promise)
|
||||
toast.promise(
|
||||
saveChanges(),
|
||||
{
|
||||
loading: 'Saving changes...',
|
||||
success: 'Changes saved!',
|
||||
error: 'Failed to save changes',
|
||||
}
|
||||
);
|
||||
toast.promise(saveChanges(), {
|
||||
loading: 'Saving changes...',
|
||||
success: 'Changes saved!',
|
||||
error: 'Failed to save changes',
|
||||
});
|
||||
|
||||
// Custom with action
|
||||
toast('Event has been created', {
|
||||
@@ -580,6 +583,7 @@ toast.dismiss();
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- ✅ Action confirmations (saved, deleted)
|
||||
- ✅ Background task updates
|
||||
- ✅ Temporary errors
|
||||
@@ -629,12 +633,11 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||
```
|
||||
|
||||
**Pattern: Loading states**:
|
||||
|
||||
```tsx
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-48 w-full" />
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
{
|
||||
isLoading ? <Skeleton className="h-48 w-full" /> : <div>{content}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -654,7 +657,7 @@ import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTrigger,
|
||||
DialogClose
|
||||
DialogClose,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
// Basic dialog
|
||||
@@ -678,7 +681,7 @@ import {
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog>;
|
||||
|
||||
// Controlled dialog
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -695,10 +698,11 @@ const [isOpen, setIsOpen] = useState(false);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog>;
|
||||
```
|
||||
|
||||
**Accessibility:**
|
||||
|
||||
- Escape key closes dialog
|
||||
- Focus trapped inside dialog
|
||||
- Returns focus to trigger on close
|
||||
@@ -916,7 +920,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption
|
||||
TableCaption,
|
||||
} from '@/components/ui/table';
|
||||
|
||||
<Table>
|
||||
@@ -945,7 +949,7 @@ import {
|
||||
<TableCell className="text-right">$2,500.00</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</Table>;
|
||||
```
|
||||
|
||||
**For advanced tables** (sorting, filtering, pagination), use **TanStack Table** with react-hook-form.
|
||||
@@ -1014,12 +1018,14 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map(user => (
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm">Edit</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
Edit
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -1041,9 +1047,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New User</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new user to the system
|
||||
</DialogDescription>
|
||||
<DialogDescription>Add a new user to the system</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
@@ -1113,7 +1117,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
```tsx
|
||||
<Table>
|
||||
<TableBody>
|
||||
{users.map(user => (
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
@@ -1134,10 +1138,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(user)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => handleDelete(user)} className="text-destructive">
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
@@ -1187,6 +1188,7 @@ Need switchable panels? → Tabs
|
||||
### Component Variants Quick Reference
|
||||
|
||||
**Button**:
|
||||
|
||||
- `default` - Primary action
|
||||
- `secondary` - Secondary action
|
||||
- `outline` - Alternative action
|
||||
@@ -1195,12 +1197,14 @@ Need switchable panels? → Tabs
|
||||
- `destructive` - Delete/remove
|
||||
|
||||
**Badge**:
|
||||
|
||||
- `default` - Blue (new, active)
|
||||
- `secondary` - Gray (draft, inactive)
|
||||
- `outline` - Bordered (pending)
|
||||
- `destructive` - Red (critical, error)
|
||||
|
||||
**Alert**:
|
||||
|
||||
- `default` - Info
|
||||
- `destructive` - Error
|
||||
|
||||
@@ -1216,12 +1220,14 @@ Need switchable panels? → Tabs
|
||||
---
|
||||
|
||||
**Related Documentation:**
|
||||
|
||||
- [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
- [Foundations](./01-foundations.md) - Colors, typography, spacing
|
||||
- [Layouts](./03-layouts.md) - Layout patterns
|
||||
- [Forms](./06-forms.md) - Form validation and patterns
|
||||
|
||||
**External Resources:**
|
||||
|
||||
- [shadcn/ui Documentation](https://ui.shadcn.com)
|
||||
- [Radix UI Primitives](https://www.radix-ui.com)
|
||||
|
||||
|
||||
@@ -36,16 +36,16 @@ Use this flowchart to choose between Grid and 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`) |
|
||||
| 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`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -68,15 +68,14 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
<CardHeader>
|
||||
<CardTitle>Section Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
Page content goes here
|
||||
</CardContent>
|
||||
<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)
|
||||
@@ -85,6 +84,7 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
- `space-y-6` - Vertical spacing between children
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Blog posts
|
||||
- Documentation pages
|
||||
- Settings pages
|
||||
@@ -103,7 +103,7 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
<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 => (
|
||||
{items.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
@@ -119,11 +119,13 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -131,6 +133,7 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
- `gap-6` - Consistent spacing between items
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Dashboards
|
||||
- Product grids
|
||||
- Image galleries
|
||||
@@ -171,17 +174,20 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -220,6 +226,7 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- `flex` - Horizontal layout
|
||||
- `w-64` - Fixed sidebar width (256px)
|
||||
- `flex-1` - Main content takes remaining space
|
||||
@@ -249,6 +256,7 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Admin dashboards
|
||||
- Settings pages
|
||||
- Documentation sites
|
||||
@@ -277,17 +285,20 @@ These 5 patterns cover 80% of all layout needs. Master these first.
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -327,13 +338,13 @@ Always start with mobile layout, then enhance for larger screens:
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
|
||||
@@ -457,12 +468,8 @@ grid-cols-1 lg:grid-cols-3
|
||||
```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 className="col-span-2">Main content (2/3 width)</div>
|
||||
<div className="col-span-1">Sidebar (1/3 width)</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -482,12 +489,8 @@ grid-cols-1 lg:grid-cols-3
|
||||
|
||||
```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>
|
||||
<aside className="sticky top-6 h-fit w-64">{/* Stays in view while scrolling */}</aside>
|
||||
<main className="flex-1">{/* Scrollable content */}</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -579,6 +582,7 @@ w-full px-4
|
||||
---
|
||||
|
||||
**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
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
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
|
||||
@@ -40,6 +41,7 @@ These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
|
||||
- Eliminates "last child" edge cases
|
||||
- Makes components reusable (they work in any context)
|
||||
- Changes propagate from one place (parent)
|
||||
@@ -48,6 +50,7 @@ These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
---
|
||||
|
||||
### Rule 2: Use Gap for Siblings
|
||||
|
||||
**For flex and grid layouts, use `gap-*` to space siblings.**
|
||||
|
||||
```tsx
|
||||
@@ -73,6 +76,7 @@ These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
---
|
||||
|
||||
### Rule 3: Use Padding for Internal Spacing
|
||||
|
||||
**Padding is for spacing _inside_ a component, between the border and content.**
|
||||
|
||||
```tsx
|
||||
@@ -91,6 +95,7 @@ These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
---
|
||||
|
||||
### Rule 4: Use space-y for Vertical Stacks
|
||||
|
||||
**For vertical stacks (not flex/grid), use `space-y-*` utility.**
|
||||
|
||||
```tsx
|
||||
@@ -110,6 +115,7 @@ These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
```
|
||||
|
||||
**How space-y works:**
|
||||
|
||||
```css
|
||||
/* space-y-4 applies margin-top to all children except first */
|
||||
.space-y-4 > * + * {
|
||||
@@ -120,6 +126,7 @@ These 5 rules eliminate 90% of spacing inconsistencies:
|
||||
---
|
||||
|
||||
### Rule 5: Margins Only for Exceptions
|
||||
|
||||
**Use margin only when a specific child needs different spacing from its siblings.**
|
||||
|
||||
```tsx
|
||||
@@ -151,18 +158,19 @@ When children control their own margins:
|
||||
```tsx
|
||||
// ❌ ANTI-PATTERN
|
||||
function TodoItem({ className }: { className?: string }) {
|
||||
return <div className={cn("mb-4", className)}>Todo</div>;
|
||||
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>
|
||||
<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
|
||||
@@ -199,6 +207,7 @@ function TodoItem({ className }: { className?: string }) {
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
1. ✅ No edge cases (last child, first child, only child)
|
||||
2. ✅ Spacing controlled in one place
|
||||
3. ✅ Component works in any layout context
|
||||
@@ -265,6 +274,7 @@ Use this flowchart to choose the right spacing method:
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -288,6 +298,7 @@ Use this flowchart to choose the right spacing method:
|
||||
```
|
||||
|
||||
**Why gap over space-x:**
|
||||
|
||||
- Works with `flex-wrap`
|
||||
- Works with `flex-col` (changes direction)
|
||||
- Consistent spacing in all directions
|
||||
@@ -306,6 +317,7 @@ Use this flowchart to choose the right spacing method:
|
||||
```
|
||||
|
||||
**Why gap:**
|
||||
|
||||
- Consistent spacing between rows and columns
|
||||
- Works with responsive grid changes
|
||||
- No edge cases (first row, last column, etc.)
|
||||
@@ -332,6 +344,7 @@ Use this flowchart to choose the right spacing method:
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -364,6 +377,7 @@ Use this flowchart to choose the right spacing method:
|
||||
```
|
||||
|
||||
**Spacing breakdown:**
|
||||
|
||||
- `px-4`: Horizontal padding (prevents edge touching)
|
||||
- `py-8`: Vertical padding (top and bottom spacing)
|
||||
- `space-y-6`: 24px between sections
|
||||
@@ -376,25 +390,28 @@ Use this flowchart to choose the right spacing method:
|
||||
### Example 1: Button Group
|
||||
|
||||
#### ❌ Before (Child-Controlled)
|
||||
|
||||
```tsx
|
||||
function ActionButton({ children, className }: Props) {
|
||||
return <Button className={cn("mr-4", className)}>{children}</Button>;
|
||||
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>
|
||||
<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>;
|
||||
@@ -415,6 +432,7 @@ function ActionButton({ children, className }: Props) {
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- No edge cases
|
||||
- Reusable in any layout
|
||||
- Easy to change spacing
|
||||
@@ -424,6 +442,7 @@ function ActionButton({ children, className }: Props) {
|
||||
### Example 2: List Items
|
||||
|
||||
#### ❌ Before (Child-Controlled)
|
||||
|
||||
```tsx
|
||||
function ListItem({ title, description }: Props) {
|
||||
return (
|
||||
@@ -437,16 +456,18 @@ function ListItem({ title, description }: Props) {
|
||||
<div>
|
||||
<ListItem title="Item 1" description="..." />
|
||||
<ListItem title="Item 2" description="..." />
|
||||
<ListItem title="Item 3" description="..." /> {/* Unwanted mb-6 */}
|
||||
</div>
|
||||
<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 (
|
||||
@@ -473,6 +494,7 @@ function ListItem({ title, description }: Props) {
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- No unwanted margins
|
||||
- Internal spacing controlled by `space-y-2`
|
||||
- Reusable with different spacings
|
||||
@@ -482,6 +504,7 @@ function ListItem({ title, description }: Props) {
|
||||
### Example 3: Form Fields
|
||||
|
||||
#### ❌ Before (Mixed Strategy)
|
||||
|
||||
```tsx
|
||||
<form>
|
||||
<div className="mb-4">
|
||||
@@ -494,16 +517,20 @@ function ListItem({ title, description }: Props) {
|
||||
<Input id="email" className="mt-2" />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-6">Submit</Button>
|
||||
<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">
|
||||
@@ -516,11 +543,14 @@ function ListItem({ title, description }: Props) {
|
||||
<Input id="email" />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-2">Submit</Button>
|
||||
<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
|
||||
@@ -533,18 +563,20 @@ function ListItem({ title, description }: Props) {
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
{items.map((item, index) => (
|
||||
<Card key={item.id} className={index < items.length - 1 ? "mb-4" : ""}>
|
||||
{item.name}
|
||||
</Card>
|
||||
))}
|
||||
{
|
||||
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 => (
|
||||
{items.map((item) => (
|
||||
<Card key={item.id}>{item.name}</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -564,6 +596,7 @@ function ListItem({ title, description }: Props) {
|
||||
```
|
||||
|
||||
**Why negative margins are bad:**
|
||||
|
||||
- Indicates broken spacing strategy
|
||||
- Hard to maintain
|
||||
- Creates coupling between components
|
||||
@@ -618,26 +651,26 @@ function ListItem({ title, description }: Props) {
|
||||
|
||||
### 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` |
|
||||
| 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 |
|
||||
| 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)
|
||||
|
||||
@@ -682,7 +715,7 @@ Need spacing?
|
||||
Before implementing spacing, verify:
|
||||
|
||||
- [ ] **Parent controls children?** Using gap or space-y/x?
|
||||
- [ ] **No child margins?** Components don't have mb-* or mr-*?
|
||||
- [ ] **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?
|
||||
@@ -700,6 +733,7 @@ Before implementing spacing, verify:
|
||||
---
|
||||
|
||||
**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
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
**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
|
||||
@@ -74,6 +75,7 @@ Do you need a UI element?
|
||||
```
|
||||
|
||||
**Why this is good:**
|
||||
|
||||
- Simple and direct
|
||||
- Easy to customize per use case
|
||||
- No abstraction overhead
|
||||
@@ -103,10 +105,11 @@ function ContentCard({ title, description, content, actionLabel, onAction }: Pro
|
||||
}
|
||||
|
||||
// Used once... why did we create this?
|
||||
<ContentCard title="..." description="..." content="..." />
|
||||
<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)
|
||||
@@ -148,6 +151,7 @@ function DashboardMetricCard({
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- ✅ Pattern validated (used 3+ times)
|
||||
- ✅ Specific purpose (dashboard metrics)
|
||||
- ✅ Consistent structure across uses
|
||||
@@ -171,22 +175,23 @@ interface MyComponentProps {
|
||||
|
||||
export function MyComponent({ className, children }: MyComponentProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"base-classes-here", // Base styles
|
||||
className // Allow overrides
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'base-classes-here', // Base styles
|
||||
className // Allow overrides
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<MyComponent className="custom-overrides">
|
||||
Content
|
||||
</MyComponent>
|
||||
<MyComponent className="custom-overrides">Content</MyComponent>;
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Always accept `className` prop
|
||||
- Use `cn()` utility for merging
|
||||
- Base classes first, overrides last
|
||||
@@ -203,24 +208,24 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
const componentVariants = cva(
|
||||
// Base classes (always applied)
|
||||
"inline-flex items-center justify-center rounded-lg font-medium transition-colors",
|
||||
'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",
|
||||
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",
|
||||
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",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -231,25 +236,18 @@ interface MyComponentProps
|
||||
// Additional props here
|
||||
}
|
||||
|
||||
export function MyComponent({
|
||||
variant,
|
||||
size,
|
||||
className,
|
||||
...props
|
||||
}: MyComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(componentVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
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>
|
||||
<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
|
||||
@@ -273,13 +271,7 @@ interface StatCardProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatCard({
|
||||
title,
|
||||
value,
|
||||
description,
|
||||
icon,
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
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">
|
||||
@@ -288,9 +280,7 @@ export function StatCard({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -302,10 +292,11 @@ export function StatCard({
|
||||
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 `?`
|
||||
@@ -354,14 +345,17 @@ export function Toggle({
|
||||
}
|
||||
|
||||
// Uncontrolled usage
|
||||
<Toggle defaultValue={false}>Auto-save</Toggle>
|
||||
<Toggle defaultValue={false}>Auto-save</Toggle>;
|
||||
|
||||
// Controlled usage
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
<Toggle value={enabled} onChange={setEnabled}>Auto-save</Toggle>
|
||||
<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
|
||||
@@ -376,6 +370,7 @@ const [enabled, setEnabled] = useState(false);
|
||||
**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
|
||||
@@ -390,24 +385,23 @@ import { cva } from 'class-variance-authority';
|
||||
|
||||
const alertVariants = cva(
|
||||
// Base classes (always applied)
|
||||
"relative w-full rounded-lg border p-4",
|
||||
'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",
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Usage
|
||||
<div className={alertVariants({ variant: "destructive" })}>
|
||||
Alert content
|
||||
</div>
|
||||
<div className={alertVariants({ variant: 'destructive' })}>Alert content</div>;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -416,32 +410,32 @@ const alertVariants = cva(
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
|
||||
'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",
|
||||
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",
|
||||
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",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Usage
|
||||
<button className={buttonVariants({ variant: "outline", size: "lg" })}>
|
||||
<button className={buttonVariants({ variant: 'outline', size: 'lg' })}>
|
||||
Large Outline Button
|
||||
</button>
|
||||
</button>;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -451,28 +445,28 @@ const buttonVariants = cva(
|
||||
**Use case**: Different classes when specific variant combinations are used
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva("base-classes", {
|
||||
const buttonVariants = cva('base-classes', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary",
|
||||
destructive: "bg-destructive",
|
||||
default: 'bg-primary',
|
||||
destructive: 'bg-destructive',
|
||||
},
|
||||
size: {
|
||||
sm: "h-8",
|
||||
lg: "h-12",
|
||||
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
|
||||
variant: 'destructive',
|
||||
size: 'lg',
|
||||
class: 'text-lg font-bold', // Applied when BOTH are true
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "sm",
|
||||
variant: 'default',
|
||||
size: 'sm',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -484,6 +478,7 @@ const buttonVariants = cva("base-classes", {
|
||||
### Prop Naming Conventions
|
||||
|
||||
**DO**:
|
||||
|
||||
```tsx
|
||||
// ✅ Descriptive, semantic names
|
||||
interface UserCardProps {
|
||||
@@ -495,6 +490,7 @@ interface UserCardProps {
|
||||
```
|
||||
|
||||
**DON'T**:
|
||||
|
||||
```tsx
|
||||
// ❌ Generic, unclear names
|
||||
interface CardProps {
|
||||
@@ -510,6 +506,7 @@ interface CardProps {
|
||||
### Required vs Optional Props
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Required: Core functionality depends on it
|
||||
- Optional: Nice-to-have, has sensible default
|
||||
|
||||
@@ -531,7 +528,7 @@ interface AlertProps {
|
||||
|
||||
export function Alert({
|
||||
children,
|
||||
variant = 'default', // Default for optional prop
|
||||
variant = 'default', // Default for optional prop
|
||||
onClose,
|
||||
icon,
|
||||
className,
|
||||
@@ -545,6 +542,7 @@ export function Alert({
|
||||
### Prop Type Patterns
|
||||
|
||||
**Enum props** (limited options):
|
||||
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
variant: 'default' | 'destructive' | 'outline';
|
||||
@@ -553,6 +551,7 @@ interface ButtonProps {
|
||||
```
|
||||
|
||||
**Boolean flags**:
|
||||
|
||||
```tsx
|
||||
interface CardProps {
|
||||
isLoading?: boolean;
|
||||
@@ -562,6 +561,7 @@ interface CardProps {
|
||||
```
|
||||
|
||||
**Callback props**:
|
||||
|
||||
```tsx
|
||||
interface FormProps {
|
||||
onSubmit: (data: FormData) => void;
|
||||
@@ -571,6 +571,7 @@ interface FormProps {
|
||||
```
|
||||
|
||||
**Render props** (advanced customization):
|
||||
|
||||
```tsx
|
||||
interface ListProps<T> {
|
||||
items: T[];
|
||||
@@ -583,7 +584,7 @@ interface ListProps<T> {
|
||||
items={users}
|
||||
renderItem={(user, i) => <UserCard key={i} user={user} />}
|
||||
renderEmpty={() => <EmptyState />}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -593,6 +594,7 @@ interface ListProps<T> {
|
||||
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
|
||||
@@ -602,6 +604,7 @@ Before shipping a custom component, verify:
|
||||
- [ ] **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
|
||||
@@ -609,6 +612,7 @@ Before shipping a custom component, verify:
|
||||
- [ ] **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)
|
||||
@@ -616,6 +620,7 @@ Before shipping a custom component, verify:
|
||||
- [ ] **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
|
||||
@@ -644,13 +649,7 @@ interface StatCardProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
icon: Icon,
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
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">
|
||||
@@ -660,11 +659,9 @@ export function StatCard({
|
||||
<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 className={cn('text-xs', change >= 0 ? 'text-green-600' : 'text-destructive')}>
|
||||
{change >= 0 ? '+' : ''}
|
||||
{change}% from last month
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -678,10 +675,11 @@ export function StatCard({
|
||||
<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>
|
||||
</div>;
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- Specific purpose (dashboard metrics)
|
||||
- Reused 8+ times
|
||||
- Consistent structure
|
||||
@@ -747,18 +745,10 @@ export function ConfirmDialog({
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
variant={variant}
|
||||
onClick={handleConfirm}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button variant={variant} onClick={handleConfirm} disabled={isLoading}>
|
||||
{isLoading ? 'Processing...' : confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -781,10 +771,11 @@ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
await deleteUser(user.id);
|
||||
toast.success('User deleted');
|
||||
}}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- Common pattern (confirmations)
|
||||
- Handles loading states automatically
|
||||
- Consistent UX across app
|
||||
@@ -808,19 +799,12 @@ interface PageHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
}: PageHeaderProps) {
|
||||
export function PageHeader({ title, description, action, className }: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center justify-between", className)}>
|
||||
<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>
|
||||
)}
|
||||
{description && <p className="text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
{action && <div>{action}</div>}
|
||||
</div>
|
||||
@@ -837,7 +821,7 @@ export function PageHeader({
|
||||
Create User
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -866,6 +850,7 @@ Before creating a custom component, ask:
|
||||
---
|
||||
|
||||
**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
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- **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)
|
||||
@@ -80,11 +81,7 @@ export function SimpleForm() {
|
||||
<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')}
|
||||
/>
|
||||
<Input id="email" type="email" {...form.register('email')} />
|
||||
</div>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
@@ -180,6 +177,7 @@ export function LoginForm() {
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
1. Define Zod schema first
|
||||
2. Infer TypeScript type with `z.infer`
|
||||
3. Use `zodResolver` in `useForm`
|
||||
@@ -217,15 +215,9 @@ export function LoginForm() {
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
rows={4}
|
||||
{...form.register('description')}
|
||||
/>
|
||||
<Textarea id="description" rows={4} {...form.register('description')} />
|
||||
{form.formState.errors.description && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.description.message}
|
||||
</p>
|
||||
<p className="text-sm text-destructive">{form.formState.errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
@@ -237,10 +229,7 @@ export function LoginForm() {
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select
|
||||
value={form.watch('role')}
|
||||
onValueChange={(value) => form.setValue('role', value)}
|
||||
>
|
||||
<Select value={form.watch('role')} onValueChange={(value) => form.setValue('role', value)}>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
@@ -251,9 +240,7 @@ export function LoginForm() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{form.formState.errors.role && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.role.message}
|
||||
</p>
|
||||
<p className="text-sm text-destructive">{form.formState.errors.role.message}</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
@@ -272,12 +259,12 @@ export function LoginForm() {
|
||||
<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>
|
||||
)}
|
||||
</div>;
|
||||
{
|
||||
form.formState.errors.acceptTerms && (
|
||||
<p className="text-sm text-destructive">{form.formState.errors.acceptTerms.message}</p>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -289,22 +276,16 @@ export function LoginForm() {
|
||||
<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>
|
||||
<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>
|
||||
<input type="radio" id="sms" value="sms" {...form.register('notificationMethod')} />
|
||||
<Label htmlFor="sms" className="font-normal">
|
||||
SMS
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,65 +301,68 @@ export function LoginForm() {
|
||||
import { z } from 'zod';
|
||||
|
||||
// Email
|
||||
z.string().email('Invalid email address')
|
||||
z.string().email('Invalid email address');
|
||||
|
||||
// Min/max length
|
||||
z.string().min(8, 'Minimum 8 characters').max(100, 'Maximum 100 characters')
|
||||
z.string().min(8, 'Minimum 8 characters').max(100, 'Maximum 100 characters');
|
||||
|
||||
// Required field
|
||||
z.string().min(1, 'This field is required')
|
||||
z.string().min(1, 'This field is required');
|
||||
|
||||
// Optional field
|
||||
z.string().optional()
|
||||
z.string().optional();
|
||||
|
||||
// Number with range
|
||||
z.number().min(0).max(100)
|
||||
z.number().min(0).max(100);
|
||||
|
||||
// Number from string input
|
||||
z.coerce.number().min(0)
|
||||
z.coerce.number().min(0);
|
||||
|
||||
// Enum
|
||||
z.enum(['admin', 'user', 'guest'], {
|
||||
errorMap: () => ({ message: 'Invalid role' })
|
||||
})
|
||||
errorMap: () => ({ message: 'Invalid role' }),
|
||||
});
|
||||
|
||||
// URL
|
||||
z.string().url('Invalid 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')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number');
|
||||
|
||||
// Confirm password
|
||||
z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string()
|
||||
confirmPassword: z.string(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
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;
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.role === 'admin') {
|
||||
return !!data.adminKey;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Admin key required for admin role',
|
||||
path: ['adminKey'],
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: 'Admin key required for admin role',
|
||||
path: ['adminKey'],
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
@@ -448,6 +432,7 @@ type UserFormData = z.infer<typeof userFormSchema>;
|
||||
```
|
||||
|
||||
**Accessibility notes:**
|
||||
|
||||
- Use `aria-invalid` to indicate error state
|
||||
- Use `aria-describedby` to link error message
|
||||
- Error ID format: `{fieldName}-error`
|
||||
@@ -470,14 +455,14 @@ const onSubmit = async (data: FormData) => {
|
||||
};
|
||||
|
||||
// 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>
|
||||
)}
|
||||
{
|
||||
form.formState.errors.root && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{form.formState.errors.root.message}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -620,32 +605,26 @@ const onSubmit = async (data: FormData) => {
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground">Basic details about you</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
{/* Fields */}
|
||||
</div>
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground">Configure your account preferences</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
{/* Fields */}
|
||||
</div>
|
||||
<div className="space-y-4">{/* Fields */}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button type="button" variant="outline">Cancel</Button>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Save Changes</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -661,10 +640,14 @@ const onSubmit = async (data: FormData) => {
|
||||
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'),
|
||||
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() {
|
||||
@@ -684,30 +667,19 @@ function DynamicForm() {
|
||||
<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 {...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)}
|
||||
>
|
||||
<Button type="button" variant="destructive" onClick={() => remove(index)}>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => append({ name: '', quantity: 1 })}
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={() => append({ name: '', quantity: 1 })}>
|
||||
Add Item
|
||||
</Button>
|
||||
|
||||
@@ -722,18 +694,23 @@ function DynamicForm() {
|
||||
### 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'],
|
||||
});
|
||||
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) });
|
||||
@@ -741,23 +718,17 @@ function ConditionalForm() {
|
||||
|
||||
return (
|
||||
<form className="space-y-4">
|
||||
<Select
|
||||
value={role}
|
||||
onValueChange={(val) => form.setValue('role', val as any)}
|
||||
>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
{role === 'admin' && <Input {...form.register('adminKey')} placeholder="Admin Key" />}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -774,14 +745,10 @@ const schema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
<input
|
||||
type="file"
|
||||
{...form.register('file')}
|
||||
accept="image/*"
|
||||
/>
|
||||
<input type="file" {...form.register('file')} accept="image/*" />;
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
const file = data.file[0]; // FileList -> File
|
||||
const file = data.file[0]; // FileList -> File
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
// Upload formData
|
||||
@@ -795,6 +762,7 @@ const onSubmit = (data: FormData) => {
|
||||
Before shipping a form, verify:
|
||||
|
||||
### Functionality
|
||||
|
||||
- [ ] All fields register correctly
|
||||
- [ ] Validation works (test invalid inputs)
|
||||
- [ ] Submit handler fires
|
||||
@@ -803,6 +771,7 @@ Before shipping a form, verify:
|
||||
- [ ] Success case redirects/shows success
|
||||
|
||||
### Accessibility
|
||||
|
||||
- [ ] Labels associated with inputs (`htmlFor` + `id`)
|
||||
- [ ] Error messages use `aria-describedby`
|
||||
- [ ] Invalid inputs have `aria-invalid`
|
||||
@@ -810,6 +779,7 @@ Before shipping a form, verify:
|
||||
- [ ] Submit button disabled during submission
|
||||
|
||||
### UX
|
||||
|
||||
- [ ] Field errors appear on blur or submit
|
||||
- [ ] Loading state prevents double-submit
|
||||
- [ ] Success message or redirect on success
|
||||
@@ -827,11 +797,13 @@ Before shipping a form, verify:
|
||||
---
|
||||
|
||||
**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)
|
||||
|
||||
|
||||
@@ -24,12 +24,14 @@
|
||||
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
|
||||
@@ -63,14 +65,15 @@ Creating a UI element?
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
|
||||
@@ -79,6 +82,7 @@ Creating a UI element?
|
||||
### 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)
|
||||
@@ -104,6 +108,7 @@ Creating a UI element?
|
||||
```
|
||||
|
||||
**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 ✅
|
||||
@@ -116,6 +121,7 @@ Creating a UI element?
|
||||
**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
|
||||
@@ -148,6 +154,7 @@ Creating a UI element?
|
||||
### 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)
|
||||
@@ -176,6 +183,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -184,31 +192,31 @@ All interactive elements must be:
|
||||
|
||||
### 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 |
|
||||
| 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>
|
||||
<Button onClick={handleClick}>Click me</Button>
|
||||
// Enter or Space triggers onClick
|
||||
```
|
||||
|
||||
**Custom clickable div (needs work):**
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Not keyboard accessible
|
||||
<div onClick={handleClick}>
|
||||
@@ -237,11 +245,12 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**Dropdown navigation:**
|
||||
|
||||
```tsx
|
||||
<DropdownMenu>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem> {/* Arrow down */}
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem> {/* Arrow down */}
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem> {/* Arrow down */}
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem> {/* Arrow down */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
// shadcn/ui handles arrow key navigation automatically
|
||||
@@ -276,12 +285,14 @@ All interactive elements must be:
|
||||
### 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)
|
||||
@@ -334,6 +345,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**Semantic elements:**
|
||||
|
||||
- `<header>` - Page header
|
||||
- `<nav>` - Navigation
|
||||
- `<main>` - Main content (only one per page)
|
||||
@@ -364,6 +376,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**Icon-only buttons:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - ARIA label
|
||||
<Button size="icon" aria-label="Close dialog">
|
||||
@@ -383,6 +396,7 @@ All interactive elements must be:
|
||||
### Common ARIA Attributes
|
||||
|
||||
**ARIA roles:**
|
||||
|
||||
```tsx
|
||||
<div role="button" tabIndex={0}>Custom Button</div>
|
||||
<div role="alert">Error message</div>
|
||||
@@ -391,6 +405,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**ARIA states:**
|
||||
|
||||
```tsx
|
||||
<button aria-expanded={isOpen}>Toggle Menu</button>
|
||||
<button aria-pressed={isActive}>Toggle</button>
|
||||
@@ -399,6 +414,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**ARIA properties:**
|
||||
|
||||
```tsx
|
||||
<button aria-label="Close">×</button>
|
||||
<input aria-describedby="email-help" />
|
||||
@@ -412,6 +428,7 @@ All interactive elements must be:
|
||||
### Form Accessibility
|
||||
|
||||
**Label association:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Explicit association
|
||||
<Label htmlFor="email">Email</Label>
|
||||
@@ -423,6 +440,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**Error messages:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Linked with aria-describedby
|
||||
<Label htmlFor="password">Password</Label>
|
||||
@@ -444,6 +462,7 @@ All interactive elements must be:
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Marked as required
|
||||
<Label htmlFor="name">
|
||||
@@ -502,6 +521,7 @@ toast.success('User created');
|
||||
```
|
||||
|
||||
**Use `:focus-visible` instead of `:focus`:**
|
||||
|
||||
- `:focus` - Shows on mouse click AND keyboard
|
||||
- `:focus-visible` - Shows only on keyboard (better UX)
|
||||
|
||||
@@ -516,7 +536,7 @@ toast.success('User created');
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
{/* Focus trapped inside */}
|
||||
<Input autoFocus /> {/* Focus first field */}
|
||||
<Input autoFocus /> {/* Focus first field */}
|
||||
<Button>Submit</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -539,7 +559,7 @@ const handleDelete = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
<Input ref={inputRef} />
|
||||
<Input ref={inputRef} />;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -549,11 +569,13 @@ const handleDelete = () => {
|
||||
### 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)
|
||||
@@ -563,6 +585,7 @@ const handleDelete = () => {
|
||||
### Manual Testing Checklist
|
||||
|
||||
#### Keyboard Testing
|
||||
|
||||
1. [ ] Unplug mouse
|
||||
2. [ ] Tab through entire page
|
||||
3. [ ] All interactive elements focusable?
|
||||
@@ -572,6 +595,7 @@ const handleDelete = () => {
|
||||
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?
|
||||
@@ -580,6 +604,7 @@ const handleDelete = () => {
|
||||
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
|
||||
@@ -590,6 +615,7 @@ const handleDelete = () => {
|
||||
### 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
|
||||
@@ -600,6 +626,7 @@ const handleDelete = () => {
|
||||
## Accessibility Checklist
|
||||
|
||||
### General
|
||||
|
||||
- [ ] Page has `<title>` and `<meta name="description">`
|
||||
- [ ] Page has proper heading hierarchy (h1 → h2 → h3)
|
||||
- [ ] Landmarks used (`<header>`, `<nav>`, `<main>`, `<footer>`)
|
||||
@@ -607,12 +634,14 @@ const handleDelete = () => {
|
||||
- [ ] 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
|
||||
@@ -622,6 +651,7 @@ const handleDelete = () => {
|
||||
- [ ] Arrow keys navigate lists/menus
|
||||
|
||||
### Screen Readers
|
||||
|
||||
- [ ] All images have alt text
|
||||
- [ ] Icon-only buttons have aria-label
|
||||
- [ ] Form labels associated with inputs
|
||||
@@ -631,6 +661,7 @@ const handleDelete = () => {
|
||||
- [ ] ARIA roles used correctly
|
||||
|
||||
### Forms
|
||||
|
||||
- [ ] Labels associated with inputs (`htmlFor` + `id`)
|
||||
- [ ] Error messages linked (`aria-describedby`)
|
||||
- [ ] Invalid inputs marked (`aria-invalid`)
|
||||
@@ -638,6 +669,7 @@ const handleDelete = () => {
|
||||
- [ ] Submit button disabled during submission
|
||||
|
||||
### Focus Management
|
||||
|
||||
- [ ] Dialogs trap focus
|
||||
- [ ] Focus returns after dialog closes
|
||||
- [ ] Programmatic focus after actions
|
||||
@@ -650,29 +682,36 @@ const handleDelete = () => {
|
||||
**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>
|
||||
<Button aria-label="Close">
|
||||
<X />
|
||||
</Button>
|
||||
```
|
||||
|
||||
5. **Use semantic color tokens**
|
||||
|
||||
```tsx
|
||||
className="text-foreground" // Auto contrast
|
||||
className = 'text-foreground'; // Auto contrast
|
||||
```
|
||||
|
||||
6. **Test with keyboard only**
|
||||
@@ -691,11 +730,13 @@ const handleDelete = () => {
|
||||
---
|
||||
|
||||
**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/)
|
||||
|
||||
@@ -9,19 +9,22 @@
|
||||
### 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"
|
||||
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';
|
||||
|
||||
@@ -29,11 +32,13 @@
|
||||
```
|
||||
|
||||
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"
|
||||
className = 'p-4 space-y-6 mb-8';
|
||||
```
|
||||
|
||||
5. ✅ **Add accessibility attributes**
|
||||
|
||||
```tsx
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
@@ -44,12 +49,14 @@
|
||||
```
|
||||
|
||||
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
|
||||
@@ -57,8 +64,8 @@
|
||||
|
||||
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"
|
||||
className = 'text-2xl sm:text-3xl lg:text-4xl';
|
||||
className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3';
|
||||
```
|
||||
|
||||
---
|
||||
@@ -66,24 +73,27 @@
|
||||
### NEVER Do
|
||||
|
||||
1. ❌ **NO arbitrary colors**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
className="bg-blue-500 text-white"
|
||||
className = 'bg-blue-500 text-white';
|
||||
|
||||
// ✅ CORRECT
|
||||
className="bg-primary text-primary-foreground"
|
||||
className = 'bg-primary text-primary-foreground';
|
||||
```
|
||||
|
||||
2. ❌ **NO arbitrary spacing values**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
className="p-[13px] mb-[17px]"
|
||||
className = 'p-[13px] mb-[17px]';
|
||||
|
||||
// ✅ CORRECT
|
||||
className="p-4 mb-4"
|
||||
className = 'p-4 mb-4';
|
||||
```
|
||||
|
||||
3. ❌ **NO inline styles**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
style={{ margin: '10px', color: '#3b82f6' }}
|
||||
@@ -93,6 +103,7 @@
|
||||
```
|
||||
|
||||
4. ❌ **NO custom CSS classes** (use Tailwind utilities)
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<div className="my-custom-class">
|
||||
@@ -102,6 +113,7 @@
|
||||
```
|
||||
|
||||
5. ❌ **NO mixing component libraries**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Don't use Material-UI, Ant Design, etc.
|
||||
import { Button } from '@mui/material';
|
||||
@@ -111,6 +123,7 @@
|
||||
```
|
||||
|
||||
6. ❌ **NO skipping accessibility**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<button><X /></button>
|
||||
@@ -122,6 +135,7 @@
|
||||
```
|
||||
|
||||
7. ❌ **NO creating custom variants without CVA**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
<Button className={type === 'danger' ? 'bg-red-500' : 'bg-blue-500'}>
|
||||
@@ -138,9 +152,7 @@
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Content */}
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto space-y-6">{/* Content */}</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -148,7 +160,9 @@
|
||||
|
||||
```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>)}
|
||||
{items.map((item) => (
|
||||
<Card key={item.id}>...</Card>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -160,9 +174,7 @@
|
||||
<CardTitle>Form Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Form fields */}
|
||||
</form>
|
||||
<form className="space-y-4">{/* Form fields */}</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
@@ -170,9 +182,7 @@
|
||||
### Centered Content
|
||||
|
||||
```tsx
|
||||
<div className="max-w-2xl mx-auto px-4">
|
||||
{/* Readable content width */}
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto px-4">{/* Readable content width */}</div>
|
||||
```
|
||||
|
||||
---
|
||||
@@ -191,17 +201,15 @@ interface MyComponentProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MyComponent({
|
||||
variant = 'default',
|
||||
className,
|
||||
children
|
||||
}: MyComponentProps) {
|
||||
export function MyComponent({ variant = 'default', className, children }: MyComponentProps) {
|
||||
return (
|
||||
<Card className={cn(
|
||||
"p-4", // base styles
|
||||
variant === 'compact' && "p-2",
|
||||
className // allow overrides
|
||||
)}>
|
||||
<Card
|
||||
className={cn(
|
||||
'p-4', // base styles
|
||||
variant === 'compact' && 'p-2',
|
||||
className // allow overrides
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
@@ -215,22 +223,22 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const componentVariants = cva(
|
||||
"base-classes-here", // base
|
||||
'base-classes-here', // base
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground",
|
||||
destructive: "bg-destructive text-destructive-foreground",
|
||||
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",
|
||||
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",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -240,9 +248,7 @@ interface ComponentProps
|
||||
VariantProps<typeof componentVariants> {}
|
||||
|
||||
export function Component({ variant, size, className, ...props }: ComponentProps) {
|
||||
return (
|
||||
<div className={cn(componentVariants({ variant, size, className }))} {...props} />
|
||||
);
|
||||
return <div className={cn(componentVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -313,18 +319,18 @@ export function MyForm() {
|
||||
|
||||
**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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -332,14 +338,14 @@ export function MyForm() {
|
||||
|
||||
**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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -428,7 +434,7 @@ function MyCard({ title, children }) {
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
</Card>;
|
||||
```
|
||||
|
||||
### ❌ Mistake 5: Not using cn() utility
|
||||
@@ -497,7 +503,7 @@ Add to `.github/copilot-instructions.md`:
|
||||
```markdown
|
||||
# Component Guidelines
|
||||
|
||||
- Import from @/components/ui/*
|
||||
- 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
|
||||
@@ -525,20 +531,20 @@ interface DashboardCardProps {
|
||||
|
||||
export function DashboardCard({ title, value, trend, className }: DashboardCardProps) {
|
||||
return (
|
||||
<Card className={cn("p-6", className)}>
|
||||
<Card className={cn('p-6', className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
</CardTitle>
|
||||
<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"
|
||||
)}>
|
||||
<p
|
||||
className={cn(
|
||||
'text-xs',
|
||||
trend === 'up' && 'text-green-600',
|
||||
trend === 'down' && 'text-destructive'
|
||||
)}
|
||||
>
|
||||
{trend === 'up' ? '↑' : '↓'} Trend
|
||||
</p>
|
||||
)}
|
||||
@@ -549,6 +555,7 @@ export function DashboardCard({ title, value, trend, className }: DashboardCardP
|
||||
```
|
||||
|
||||
**Why it's good:**
|
||||
|
||||
- ✅ Imports from `@/components/ui/*`
|
||||
- ✅ Uses semantic tokens
|
||||
- ✅ Uses `cn()` utility
|
||||
|
||||
@@ -20,21 +20,21 @@
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
|
||||
@@ -61,29 +61,29 @@
|
||||
|
||||
### 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 | |
|
||||
| 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 | ⭐ |
|
||||
| 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
|
||||
|
||||
@@ -115,35 +115,35 @@
|
||||
|
||||
### 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 | |
|
||||
| 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` |
|
||||
| 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
|
||||
|
||||
@@ -217,52 +217,52 @@
|
||||
|
||||
```tsx
|
||||
// 1 → 2 → 3 progression (most common)
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
className = 'grid grid-cols-1 lg:grid-cols-3 gap-6';
|
||||
```
|
||||
|
||||
### Container Widths
|
||||
|
||||
```tsx
|
||||
// Standard container
|
||||
className="container mx-auto px-4"
|
||||
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
|
||||
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"
|
||||
className = 'flex gap-4';
|
||||
|
||||
// Vertical flex
|
||||
className="flex flex-col gap-4"
|
||||
className = 'flex flex-col gap-4';
|
||||
|
||||
// Center items
|
||||
className="flex items-center justify-center"
|
||||
className = 'flex items-center justify-center';
|
||||
|
||||
// Space between
|
||||
className="flex items-center justify-between"
|
||||
className = 'flex items-center justify-between';
|
||||
|
||||
// Wrap items
|
||||
className="flex flex-wrap gap-4"
|
||||
className = 'flex flex-wrap gap-4';
|
||||
|
||||
// Responsive: stack on mobile, row on desktop
|
||||
className="flex flex-col sm:flex-row gap-4"
|
||||
className = 'flex flex-col sm:flex-row gap-4';
|
||||
```
|
||||
|
||||
---
|
||||
@@ -273,9 +273,7 @@ className="flex flex-col sm:flex-row gap-4"
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Content */}
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto space-y-6">{/* Content */}</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -287,7 +285,9 @@ className="flex flex-col sm:flex-row gap-4"
|
||||
<CardTitle>Title</CardTitle>
|
||||
<CardDescription>Description</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Action</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Action
|
||||
</Button>
|
||||
</CardHeader>
|
||||
```
|
||||
|
||||
@@ -319,9 +319,7 @@ className="flex flex-col sm:flex-row gap-4"
|
||||
<CardTitle>Form Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
{/* Fields */}
|
||||
</form>
|
||||
<form className="space-y-4">{/* Fields */}</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -354,17 +352,13 @@ className="flex flex-col sm:flex-row gap-4"
|
||||
### Responsive Text
|
||||
|
||||
```tsx
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
|
||||
Responsive Title
|
||||
</h1>
|
||||
<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>
|
||||
<div className="p-4 sm:p-6 lg:p-8">Responsive padding</div>
|
||||
```
|
||||
|
||||
---
|
||||
@@ -427,16 +421,16 @@ Has error?
|
||||
|
||||
## 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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -482,7 +476,13 @@ 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
|
||||
// Utilities
|
||||
@@ -506,53 +506,53 @@ import { Check, X, AlertCircle, Loader2 } from 'lucide-react';
|
||||
|
||||
```tsx
|
||||
// Required string
|
||||
z.string().min(1, 'Required')
|
||||
z.string().min(1, 'Required');
|
||||
|
||||
// Email
|
||||
z.string().email('Invalid email')
|
||||
z.string().email('Invalid email');
|
||||
|
||||
// Min/max length
|
||||
z.string().min(8, 'Min 8 chars').max(100, 'Max 100 chars')
|
||||
z.string().min(8, 'Min 8 chars').max(100, 'Max 100 chars');
|
||||
|
||||
// Optional
|
||||
z.string().optional()
|
||||
z.string().optional();
|
||||
|
||||
// Number
|
||||
z.coerce.number().min(0).max(100)
|
||||
z.coerce.number().min(0).max(100);
|
||||
|
||||
// Enum
|
||||
z.enum(['admin', 'user', 'guest'])
|
||||
z.enum(['admin', 'user', 'guest']);
|
||||
|
||||
// Boolean
|
||||
z.boolean().refine(val => val === true, { message: 'Must accept' })
|
||||
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, {
|
||||
confirmPassword: z.string(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
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 |
|
||||
| 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"
|
||||
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';
|
||||
```
|
||||
|
||||
---
|
||||
@@ -562,20 +562,20 @@ className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
||||
### Shadows
|
||||
|
||||
```tsx
|
||||
shadow-sm // Cards, panels
|
||||
shadow-md // Dropdowns, tooltips
|
||||
shadow-lg // Modals, popovers
|
||||
shadow-xl // Floating notifications
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -589,6 +589,7 @@ rounded-full // Pills, avatars, icon buttons
|
||||
---
|
||||
|
||||
**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
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
## Project Overview
|
||||
|
||||
### Vision
|
||||
|
||||
Create a world-class design system documentation that:
|
||||
|
||||
- Follows Pareto principle (80% coverage with 20% content)
|
||||
- Includes AI-specific code generation guidelines
|
||||
- Provides interactive, copy-paste examples
|
||||
@@ -18,6 +20,7 @@ Create a world-class design system documentation that:
|
||||
- Maintains perfect internal coherence and link integrity
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Pareto-Efficiency** - 80/20 rule applied throughout
|
||||
2. **AI-Optimized** - Dedicated guidelines for AI code generation
|
||||
3. **Interconnected** - All docs cross-reference each other
|
||||
@@ -163,6 +166,7 @@ Create a world-class design system documentation that:
|
||||
### Documentation Review & Fixes ✅
|
||||
|
||||
#### Issues Found During Review:
|
||||
|
||||
1. **Time estimates in section headers** - Removed all (user request)
|
||||
- Removed "⏱️ Time to productive: 5 minutes" from header
|
||||
- Removed "(3 minutes)", "(30 seconds)" from all section headers
|
||||
@@ -178,6 +182,7 @@ Create a world-class design system documentation that:
|
||||
- Fixed: Added missing `SelectGroup` and `SelectLabel` to import statement in 02-components.md
|
||||
|
||||
#### Comprehensive Review Results:
|
||||
|
||||
- **✅ 100+ links checked**
|
||||
- **✅ 0 broken internal doc links**
|
||||
- **✅ 0 logic inconsistencies**
|
||||
@@ -200,7 +205,9 @@ Create a world-class design system documentation that:
|
||||
## Phase 2: Interactive Demos (PENDING)
|
||||
|
||||
### Objective
|
||||
|
||||
Create live, interactive demonstration pages at `/dev/*` routes with:
|
||||
|
||||
- Copy-paste ready code snippets
|
||||
- Before/after comparisons
|
||||
- Live component examples
|
||||
@@ -272,12 +279,14 @@ Create live, interactive demonstration pages at `/dev/*` routes with:
|
||||
### Overall Progress: 100% Complete ✅
|
||||
|
||||
**Phase 1: Documentation** ✅ 100% (14/14 tasks)
|
||||
|
||||
- All documentation files created (~7,600 lines)
|
||||
- All issues fixed (4 issues resolved)
|
||||
- Comprehensive review completed (100+ links verified)
|
||||
- CLAUDE.md updated
|
||||
|
||||
**Phase 2: Interactive Demos** ✅ 100% (6/6 tasks)
|
||||
|
||||
- Utility components created (~470 lines)
|
||||
- Hub page created (~220 lines)
|
||||
- All demo pages created and enhanced (~2,388 lines)
|
||||
@@ -386,6 +395,7 @@ Create live, interactive demonstration pages at `/dev/*` routes with:
|
||||
### Technical Implementation
|
||||
|
||||
**Technologies Used:**
|
||||
|
||||
- Next.js 15 App Router
|
||||
- React 19 + TypeScript
|
||||
- shadcn/ui components (all)
|
||||
@@ -395,6 +405,7 @@ Create live, interactive demonstration pages at `/dev/*` routes with:
|
||||
- Responsive design (mobile-first)
|
||||
|
||||
**Architecture:**
|
||||
|
||||
- Server components for static pages (hub, layouts, spacing)
|
||||
- Client components for interactive pages (components, forms)
|
||||
- Reusable utility components in `/src/components/dev/`
|
||||
|
||||
@@ -6,26 +6,28 @@
|
||||
|
||||
## 🚀 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 |
|
||||
| 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
|
||||
@@ -39,6 +41,7 @@
|
||||
- Composition patterns
|
||||
|
||||
### Layouts & Spacing
|
||||
|
||||
- **[03. Layouts](./03-layouts.md)** 📐
|
||||
- Grid vs Flex decision tree
|
||||
- Common layout patterns
|
||||
@@ -52,6 +55,7 @@
|
||||
- Consistency patterns
|
||||
|
||||
### Building Components
|
||||
|
||||
- **[05. Component Creation](./05-component-creation.md)** 🔨
|
||||
- When to create vs compose
|
||||
- Component templates
|
||||
@@ -65,6 +69,7 @@
|
||||
- Multi-field examples
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **[07. Accessibility](./07-accessibility.md)** ♿
|
||||
- WCAG AA compliance
|
||||
- Keyboard navigation
|
||||
@@ -78,6 +83,7 @@
|
||||
- Component templates
|
||||
|
||||
### Reference
|
||||
|
||||
- **[99. Reference Tables](./99-reference.md)** 📚
|
||||
- Quick lookup tables
|
||||
- All tokens at a glance
|
||||
@@ -95,6 +101,7 @@ Explore live examples and copy-paste code:
|
||||
- **[Form Patterns](/dev/forms)** - Complete form examples
|
||||
|
||||
Each demo page includes:
|
||||
|
||||
- ✅ Live, interactive examples
|
||||
- ✅ Click-to-copy code snippets
|
||||
- ✅ Before/after comparisons
|
||||
@@ -105,6 +112,7 @@ Each demo page includes:
|
||||
## 🛤️ Learning Paths
|
||||
|
||||
### Path 1: Speedrun (5 minutes)
|
||||
|
||||
**Goal**: Start building immediately
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Essential patterns
|
||||
@@ -116,6 +124,7 @@ Each demo page includes:
|
||||
---
|
||||
|
||||
### Path 2: Component Developer (15 minutes)
|
||||
|
||||
**Goal**: Master component building
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Basics
|
||||
@@ -128,6 +137,7 @@ Each demo page includes:
|
||||
---
|
||||
|
||||
### Path 3: Layout Specialist (20 minutes)
|
||||
|
||||
**Goal**: Master layouts and spacing
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Basics
|
||||
@@ -141,6 +151,7 @@ Each demo page includes:
|
||||
---
|
||||
|
||||
### Path 4: Form Specialist (15 minutes)
|
||||
|
||||
**Goal**: Master forms and validation
|
||||
|
||||
1. [Quick Start](./00-quick-start.md) - Basics
|
||||
@@ -154,6 +165,7 @@ Each demo page includes:
|
||||
---
|
||||
|
||||
### Path 5: AI Setup (3 minutes)
|
||||
|
||||
**Goal**: Configure AI for perfect code generation
|
||||
|
||||
1. [AI Guidelines](./08-ai-guidelines.md) - Read once, code forever
|
||||
@@ -164,9 +176,11 @@ Each demo page includes:
|
||||
---
|
||||
|
||||
### 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)
|
||||
@@ -211,18 +225,21 @@ Our design system is built on these core principles:
|
||||
## 🤝 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
|
||||
|
||||
Reference in New Issue
Block a user