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:
2025-11-10 11:03:45 +01:00
parent 464a6140c4
commit 96df7edf88
208 changed files with 4056 additions and 4556 deletions

View File

@@ -110,6 +110,7 @@ src/lib/api/generated/
### 2.3 When to Regenerate
Regenerate the API client when:
- Backend API changes (new endpoints, updated models)
- After pulling backend changes from git
- When types don't match backend responses
@@ -122,6 +123,7 @@ Regenerate the API client when:
### 3.1 Using Generated Services
**Example: Fetching users**
```typescript
import { UsersService } from '@/lib/api/generated';
@@ -129,19 +131,20 @@ async function getUsers() {
const users = await UsersService.getUsers({
page: 1,
pageSize: 20,
search: 'john'
search: 'john',
});
return users;
}
```
**Example: Creating a user**
```typescript
import { AdminService } from '@/lib/api/generated';
async function createUser(data: CreateUserDto) {
const newUser = await AdminService.createUser({
requestBody: data
requestBody: data,
});
return newUser;
}
@@ -156,19 +159,19 @@ import { apiClient } from '@/lib/api/client';
// GET request
const response = await apiClient.get<User[]>('/users', {
params: { page: 1, search: 'john' }
params: { page: 1, search: 'john' },
});
// POST request
const response = await apiClient.post<User>('/admin/users', {
email: 'user@example.com',
first_name: 'John',
password: 'secure123'
password: 'secure123',
});
// PATCH request
const response = await apiClient.patch<User>(`/users/${userId}`, {
first_name: 'Jane'
first_name: 'Jane',
});
// DELETE request
@@ -178,27 +181,30 @@ await apiClient.delete(`/users/${userId}`);
### 3.3 Request Configuration
**Timeouts:**
```typescript
const response = await apiClient.get('/users', {
timeout: 5000 // 5 seconds
timeout: 5000, // 5 seconds
});
```
**Custom Headers:**
```typescript
const response = await apiClient.post('/users', data, {
headers: {
'X-Custom-Header': 'value'
}
'X-Custom-Header': 'value',
},
});
```
**Request Cancellation:**
```typescript
const controller = new AbortController();
const response = await apiClient.get('/users', {
signal: controller.signal
signal: controller.signal,
});
// Cancel the request
@@ -215,15 +221,13 @@ The Axios client automatically adds the Authorization header to all requests:
```typescript
// src/lib/api/client.ts
apiClient.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
);
return config;
});
```
You don't need to manually add auth headers - they're added automatically!
@@ -246,7 +250,7 @@ apiClient.interceptors.response.use(
// Refresh tokens
const refreshToken = getRefreshToken();
const { access_token, refresh_token } = await AuthService.refreshToken({
requestBody: { refresh_token: refreshToken }
requestBody: { refresh_token: refreshToken },
});
// Update stored tokens
@@ -255,7 +259,6 @@ apiClient.interceptors.response.use(
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return apiClient.request(originalRequest);
} catch (refreshError) {
// Refresh failed - logout user
useAuthStore.getState().clearAuth();
@@ -278,14 +281,11 @@ import { useAuthStore } from '@/stores/authStore';
async function login(email: string, password: string) {
try {
const response = await AuthService.login({
requestBody: { email, password }
requestBody: { email, password },
});
// Store tokens and user
useAuthStore.getState().setTokens(
response.access_token,
response.refresh_token
);
useAuthStore.getState().setTokens(response.access_token, response.refresh_token);
useAuthStore.getState().setUser(response.user);
return response.user;
@@ -319,6 +319,7 @@ The backend returns structured errors:
### 5.2 Parsing Errors
**Error Parser** (`src/lib/api/errors.ts`):
```typescript
import type { AxiosError } from 'axios';
@@ -341,80 +342,93 @@ export function parseAPIError(error: AxiosError<APIErrorResponse>): APIError[] {
// Network errors
if (!error.response) {
return [{
code: 'NETWORK_ERROR',
message: 'Network error. Please check your connection.',
}];
return [
{
code: 'NETWORK_ERROR',
message: 'Network error. Please check your connection.',
},
];
}
// HTTP status errors
const status = error.response.status;
if (status === 403) {
return [{
code: 'FORBIDDEN',
message: "You don't have permission to perform this action.",
}];
return [
{
code: 'FORBIDDEN',
message: "You don't have permission to perform this action.",
},
];
}
if (status === 404) {
return [{
code: 'NOT_FOUND',
message: 'The requested resource was not found.',
}];
return [
{
code: 'NOT_FOUND',
message: 'The requested resource was not found.',
},
];
}
if (status === 429) {
return [{
code: 'RATE_LIMIT',
message: 'Too many requests. Please slow down.',
}];
return [
{
code: 'RATE_LIMIT',
message: 'Too many requests. Please slow down.',
},
];
}
if (status >= 500) {
return [{
code: 'SERVER_ERROR',
message: 'A server error occurred. Please try again later.',
}];
return [
{
code: 'SERVER_ERROR',
message: 'A server error occurred. Please try again later.',
},
];
}
// Fallback
return [{
code: 'UNKNOWN',
message: error.message || 'An unexpected error occurred.',
}];
return [
{
code: 'UNKNOWN',
message: error.message || 'An unexpected error occurred.',
},
];
}
```
### 5.3 Error Code Mapping
**Error Messages** (`src/lib/api/errorMessages.ts`):
```typescript
export const ERROR_MESSAGES: Record<string, string> = {
// Authentication errors (AUTH_xxx)
'AUTH_001': 'Invalid email or password',
'AUTH_002': 'Account is inactive',
'AUTH_003': 'Invalid or expired token',
AUTH_001: 'Invalid email or password',
AUTH_002: 'Account is inactive',
AUTH_003: 'Invalid or expired token',
// User errors (USER_xxx)
'USER_001': 'User not found',
'USER_002': 'This email is already registered',
'USER_003': 'Invalid user data',
USER_001: 'User not found',
USER_002: 'This email is already registered',
USER_003: 'Invalid user data',
// Validation errors (VAL_xxx)
'VAL_001': 'Invalid input. Please check your data.',
'VAL_002': 'Email format is invalid',
'VAL_003': 'Password does not meet requirements',
VAL_001: 'Invalid input. Please check your data.',
VAL_002: 'Email format is invalid',
VAL_003: 'Password does not meet requirements',
// Organization errors (ORG_xxx)
'ORG_001': 'Organization name already exists',
'ORG_002': 'Organization not found',
ORG_001: 'Organization name already exists',
ORG_002: 'Organization not found',
// Permission errors (PERM_xxx)
'PERM_001': 'Insufficient permissions',
'PERM_002': 'Admin access required',
PERM_001: 'Insufficient permissions',
PERM_002: 'Admin access required',
// Rate limiting (RATE_xxx)
'RATE_001': 'Too many requests. Please try again later.',
RATE_001: 'Too many requests. Please try again later.',
};
export function getErrorMessage(code: string): string {
@@ -425,6 +439,7 @@ export function getErrorMessage(code: string): string {
### 5.4 Displaying Errors
**In React Query:**
```typescript
import { toast } from 'sonner';
import { parseAPIError, getErrorMessage } from '@/lib/api/errors';
@@ -442,6 +457,7 @@ export function useUpdateUser() {
```
**In Forms:**
```typescript
const onSubmit = async (data: FormData) => {
try {
@@ -459,9 +475,9 @@ const onSubmit = async (data: FormData) => {
});
// Set general error
if (errors.some(err => !err.field)) {
if (errors.some((err) => !err.field)) {
form.setError('root', {
message: errors.find(err => !err.field)?.message || 'An error occurred',
message: errors.find((err) => !err.field)?.message || 'An error occurred',
});
}
}
@@ -505,8 +521,7 @@ export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserDto) =>
AdminService.createUser({ requestBody: data }),
mutationFn: (data: CreateUserDto) => AdminService.createUser({ requestBody: data }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success('User created successfully');
@@ -542,8 +557,7 @@ export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: string) =>
AdminService.deleteUser({ userId }),
mutationFn: (userId: string) => AdminService.deleteUser({ userId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success('User deleted successfully');
@@ -614,7 +628,7 @@ export function useToggleUserActive() {
mutationFn: ({ userId, isActive }: { userId: string; isActive: boolean }) =>
AdminService.updateUser({
userId,
requestBody: { is_active: isActive }
requestBody: { is_active: isActive },
}),
onMutate: async ({ userId, isActive }) => {
// Cancel outgoing refetches
@@ -687,9 +701,7 @@ export const handlers = [
}),
rest.delete('/api/v1/admin/users/:userId', (req, res, ctx) => {
return res(
ctx.json({ success: true, message: 'User deleted' })
);
return res(ctx.json({ success: true, message: 'User deleted' }));
}),
];
```
@@ -844,6 +856,7 @@ function UserDetail({ userId }: { userId: string }) {
**Symptom**: `Access-Control-Allow-Origin` error in console
**Solution**: Ensure backend CORS is configured for frontend URL:
```python
# backend/app/main.py
BACKEND_CORS_ORIGINS = ["http://localhost:3000"]
@@ -854,12 +867,14 @@ BACKEND_CORS_ORIGINS = ["http://localhost:3000"]
**Symptom**: All API calls return 401
**Possible Causes**:
1. No token in store: Check `useAuthStore.getState().accessToken`
2. Token expired: Check token expiration
3. Token invalid: Try logging in again
4. Interceptor not working: Check interceptor configuration
**Debug**:
```typescript
// Log token in interceptor
apiClient.interceptors.request.use((config) => {
@@ -877,6 +892,7 @@ apiClient.interceptors.request.use((config) => {
**Symptom**: TypeScript errors about response types
**Solution**: Regenerate API client to sync with backend
```bash
npm run generate:api
```
@@ -886,10 +902,11 @@ npm run generate:api
**Symptom**: UI shows old data after mutation
**Solution**: Invalidate queries after mutations
```typescript
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
};
```
### 9.5 Network Timeout
@@ -897,6 +914,7 @@ onSuccess: () => {
**Symptom**: Requests timeout
**Solution**: Increase timeout or check backend performance
```typescript
const apiClient = axios.create({
timeout: 60000, // 60 seconds
@@ -908,6 +926,7 @@ const apiClient = axios.create({
## Conclusion
This guide covers the essential patterns for integrating with the FastAPI backend. For more advanced use cases, refer to:
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [Axios Documentation](https://axios-http.com/)
- Backend API documentation at `/docs` endpoint

View File

@@ -88,6 +88,7 @@ This frontend template provides a production-ready foundation for building moder
### 2.1 Core Framework
**Next.js 15.x (App Router)**
- **Why**: Modern React framework with RSC, excellent DX, optimized performance
- **App Router**: Preferred over Pages Router for better data fetching, layouts, and streaming
- **Server Components**: Default for better performance, client components for interactivity
@@ -96,11 +97,13 @@ This frontend template provides a production-ready foundation for building moder
### 2.2 State Management
**TanStack Query (React Query v5)**
- **Purpose**: Server state management (all API data)
- **Why**: Automatic caching, background refetching, request deduplication, optimistic updates
- **Usage**: All data fetching goes through React Query hooks
**Zustand 4.x**
- **Purpose**: Client-only state (authentication, UI preferences)
- **Why**: Minimal boilerplate, no Context API overhead, simple API
- **Usage**: Auth store, UI store (sidebar, theme, modals)
@@ -109,27 +112,32 @@ This frontend template provides a production-ready foundation for building moder
### 2.3 UI Layer
**shadcn/ui**
- **Why**: Accessible components (Radix UI), customizable, copy-paste (not npm dependency)
- **Components**: Button, Card, Dialog, Form, Input, Table, Toast, etc.
- **Customization**: Tailwind-based, easy to adapt to design system
**Tailwind CSS 4.x**
- **Why**: Utility-first, excellent DX, small bundle size, dark mode support
- **Strategy**: Class-based dark mode, mobile-first responsive design
- **Customization**: Custom theme colors, design tokens
**Recharts 2.x**
- **Purpose**: Charts for admin dashboard
- **Why**: React-native, composable, responsive, themed with Tailwind colors
### 2.4 API Layer
**@hey-api/openapi-ts**
- **Purpose**: Generate TypeScript client from backend OpenAPI spec
- **Why**: Type-safe API calls, auto-generated types matching backend
- **Alternative**: Considered `openapi-typescript-codegen` but this is more actively maintained
**Axios 1.x**
- **Purpose**: HTTP client for API calls
- **Why**: Interceptor support for auth, better error handling than fetch
- **Usage**: Wrapped in generated API client, configured with auth interceptors
@@ -137,10 +145,12 @@ This frontend template provides a production-ready foundation for building moder
### 2.5 Forms & Validation
**react-hook-form 7.x**
- **Purpose**: Form state management
- **Why**: Excellent performance, minimal re-renders, great DX
**Zod 3.x**
- **Purpose**: Runtime type validation and schema definition
- **Why**: Type inference, composable schemas, integrates with react-hook-form
- **Usage**: All forms use Zod schemas with `zodResolver`
@@ -148,10 +158,12 @@ This frontend template provides a production-ready foundation for building moder
### 2.6 Testing
**Jest + React Testing Library**
- **Purpose**: Unit and component tests
- **Why**: Industry standard, excellent React support, accessibility-focused
**Playwright**
- **Purpose**: End-to-end testing
- **Why**: Fast, reliable, multi-browser, great debugging tools
- **Coverage Target**: 90%+ for template robustness
@@ -208,6 +220,7 @@ Inspired by backend's 5-layer architecture, frontend follows similar separation
```
**Key Rules:**
- Pages/Layouts should NOT contain business logic
- Components should NOT call API client directly (use hooks)
- Hooks should NOT contain display logic
@@ -217,6 +230,7 @@ Inspired by backend's 5-layer architecture, frontend follows similar separation
### 3.2 Component Patterns
**Server Components by Default:**
```typescript
// app/(authenticated)/admin/users/page.tsx
// Server Component - can fetch data directly
@@ -232,6 +246,7 @@ export default async function UsersPage() {
```
**Client Components for Interactivity:**
```typescript
// components/admin/UserTable.tsx
'use client';
@@ -245,6 +260,7 @@ export function UserTable() {
```
**Composition Over Prop Drilling:**
```typescript
// Good: Use composition
<Card>
@@ -358,6 +374,7 @@ Each module has one clear responsibility:
```
**Token Refresh Flow (Automatic):**
```
API Request → 401 Response → Check if refresh token exists
↓ Yes ↓ No
@@ -369,11 +386,13 @@ New Tokens → Update Store → Retry Original Request
### 4.3 State Updates
**Server State (React Query):**
- Automatic background refetch
- Cache invalidation on mutations
- Optimistic updates where appropriate
**Client State (Zustand):**
- Direct store updates
- No actions/reducers boilerplate
- Subscriptions for components
@@ -385,6 +404,7 @@ New Tokens → Update Store → Retry Original Request
### 5.1 Philosophy
**Use the Right Tool for the Right Job:**
- Server data → TanStack Query
- Auth & tokens → Zustand
- UI state → Zustand (minimal)
@@ -392,6 +412,7 @@ New Tokens → Update Store → Retry Original Request
- Component state → useState/useReducer
**Avoid Redundancy:**
- DON'T duplicate server data in Zustand
- DON'T store API responses in global state
- DO keep state as local as possible
@@ -399,34 +420,36 @@ New Tokens → Update Store → Retry Original Request
### 5.2 TanStack Query Configuration
**Global Config** (`src/config/queryClient.ts`):
```typescript
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1 minute
cacheTime: 300000, // 5 minutes
retry: 3, // Retry failed requests
refetchOnWindowFocus: true, // Refetch on tab focus
refetchOnReconnect: true, // Refetch on network reconnect
staleTime: 60000, // 1 minute
cacheTime: 300000, // 5 minutes
retry: 3, // Retry failed requests
refetchOnWindowFocus: true, // Refetch on tab focus
refetchOnReconnect: true, // Refetch on network reconnect
},
mutations: {
retry: 1, // Retry mutations once
retry: 1, // Retry mutations once
},
},
});
```
**Query Key Structure:**
```typescript
['users'] // List all users
['users', userId] // Single user
['users', { page: 1, search: 'john' }] // Filtered list
['organizations', orgId, 'members'] // Nested resource
['users'][('users', userId)][('users', { page: 1, search: 'john' })][ // List all users // Single user // Filtered list
('organizations', orgId, 'members')
]; // Nested resource
```
### 5.3 Zustand Stores
**Auth Store** (`src/stores/authStore.ts`):
```typescript
interface AuthStore {
user: User | null;
@@ -443,6 +466,7 @@ interface AuthStore {
```
**UI Store** (`src/stores/uiStore.ts`):
```typescript
interface UIStore {
sidebarOpen: boolean;
@@ -454,6 +478,7 @@ interface UIStore {
```
**Store Guidelines:**
- Keep stores small and focused
- Use selectors for computed values
- Persist to localStorage where appropriate
@@ -480,6 +505,7 @@ Component → useAuth() hook → AuthContext → Zustand Store → Storage Layer
**Why This Pattern?**
**Benefits:**
- **Testable**: E2E tests can inject mock stores without backend
- **Performant**: Zustand handles state efficiently, Context is just a thin wrapper
- **Type-safe**: Full TypeScript inference throughout
@@ -488,6 +514,7 @@ Component → useAuth() hook → AuthContext → Zustand Store → Storage Layer
- **React-idiomatic**: Follows React best practices
**Key Design Principles:**
1. **Thin Context Layer**: Context only provides dependency injection, no business logic
2. **Zustand for State**: All state management stays in Zustand (no duplicated state)
3. **Backward Compatible**: Internal refactor only, no API changes
@@ -516,6 +543,7 @@ window.__TEST_AUTH_STORE__ = mockStoreHook;
```
**Implementation Details:**
- Stores Zustand hook function (not state) in Context
- Priority: explicit prop → E2E test store → production singleton
- Type-safe window global extension for E2E injection
@@ -530,23 +558,25 @@ window.__TEST_AUTH_STORE__ = mockStoreHook;
const { user, isAuthenticated } = useAuth();
// Pattern 2: Selector (optimized for performance)
const user = useAuth(state => state.user);
const user = useAuth((state) => state.user);
```
**Why Polymorphic?**
- Simple pattern for most use cases
- Optimized pattern available when needed
- Type-safe with function overloads
- No performance overhead
**Critical Implementation Detail:**
```typescript
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
throw new Error('useAuth must be used within AuthProvider');
}
// CRITICAL: Call the hook internally (follows React Rules of Hooks)
return selector ? storeHook(selector) : storeHook();
@@ -580,6 +610,7 @@ function MyComponent() {
```
**Why?**
- Component re-renders when auth state changes
- Type-safe access to all state properties
- Clean, idiomatic React code
@@ -605,6 +636,7 @@ export function useLogin() {
```
**Why?**
- Event handlers run outside React render cycle
- Don't need to re-render when state changes
- Using `getState()` directly is cleaner
@@ -694,6 +726,7 @@ test.describe('Protected Pages', () => {
```
**Why This Order?**
- AuthProvider must wrap AuthInitializer (AuthInitializer uses auth state)
- AuthProvider should wrap all app providers (auth available everywhere)
- Keep provider tree shallow for performance
@@ -701,15 +734,18 @@ test.describe('Protected Pages', () => {
### 6.6 Token Management Strategy
**Two-Token System:**
- **Access Token**: Short-lived (15 min), stored in memory/sessionStorage
- **Refresh Token**: Long-lived (7 days), stored in httpOnly cookie (preferred) or localStorage
**Token Storage Decision:**
- **Primary**: httpOnly cookies (most secure, prevents XSS)
- **Fallback**: localStorage with encryption wrapper (if cookies not feasible)
- **Access Token**: sessionStorage or React state (short-lived, acceptable risk)
**Token Rotation:**
- On refresh, both tokens are rotated
- Old refresh token is invalidated immediately
- Prevents token replay attacks
@@ -717,6 +753,7 @@ test.describe('Protected Pages', () => {
### 6.2 Per-Device Session Tracking
Backend tracks sessions per device:
- Each login creates a unique session with device info
- Users can view all active sessions
- Users can revoke individual sessions
@@ -724,6 +761,7 @@ Backend tracks sessions per device:
- "Logout All" deactivates all sessions
Frontend Implementation:
- Session list page at `/settings/sessions`
- Display device name, IP, location, last used
- Highlight current session
@@ -732,6 +770,7 @@ Frontend Implementation:
### 6.3 Auth Guard Implementation
**Layout-Based Protection:**
```typescript
// app/(authenticated)/layout.tsx
export default function AuthenticatedLayout({ children }) {
@@ -746,6 +785,7 @@ export default function AuthenticatedLayout({ children }) {
```
**Permission Checks:**
```typescript
// app/(authenticated)/admin/layout.tsx
export default function AdminLayout({ children }) {
@@ -776,12 +816,14 @@ export default function AdminLayout({ children }) {
### 7.1 OpenAPI Client Generation
**Workflow:**
```
Backend OpenAPI Spec → @hey-api/openapi-ts → TypeScript Client
(/api/v1/openapi.json) (src/lib/api/generated/)
```
**Generation Script** (`scripts/generate-api-client.sh`):
```bash
#!/bin/bash
API_URL="${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8000}"
@@ -792,6 +834,7 @@ npx @hey-api/openapi-ts \
```
**Benefits:**
- Type-safe API calls
- Auto-completion in IDE
- Compile-time error checking
@@ -801,6 +844,7 @@ npx @hey-api/openapi-ts \
### 7.2 Axios Configuration
**Base Instance** (`src/lib/api/client.ts`):
```typescript
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
@@ -812,6 +856,7 @@ export const apiClient = axios.create({
```
**Request Interceptor:**
```typescript
apiClient.interceptors.request.use(
(config) => {
@@ -826,6 +871,7 @@ apiClient.interceptors.request.use(
```
**Response Interceptor:**
```typescript
apiClient.interceptors.response.use(
(response) => response,
@@ -850,6 +896,7 @@ apiClient.interceptors.response.use(
### 7.3 Error Handling
**Backend Error Format:**
```typescript
{
success: false,
@@ -864,24 +911,28 @@ apiClient.interceptors.response.use(
```
**Frontend Error Parsing:**
```typescript
export function parseAPIError(error: AxiosError): APIError {
if (error.response?.data?.errors) {
return error.response.data.errors;
}
return [{
code: 'UNKNOWN',
message: 'An unexpected error occurred'
}];
return [
{
code: 'UNKNOWN',
message: 'An unexpected error occurred',
},
];
}
```
**Error Code Mapping:**
```typescript
const ERROR_MESSAGES = {
'AUTH_001': 'Invalid email or password',
'USER_002': 'This email is already registered',
'VAL_001': 'Please check your input',
AUTH_001: 'Invalid email or password',
USER_002: 'This email is already registered',
VAL_001: 'Please check your input',
// ... all backend error codes
};
```
@@ -889,6 +940,7 @@ const ERROR_MESSAGES = {
### 7.4 React Query Hooks Pattern
**Standard Pattern:**
```typescript
// lib/api/hooks/useUsers.ts
export function useUsers(filters?: UserFilters) {
@@ -955,6 +1007,7 @@ app/
```
**Route Groups** (parentheses in folder name):
- Organize routes without affecting URL
- Apply different layouts to route subsets
- Example: `(auth)` and `(authenticated)` have different layouts
@@ -962,23 +1015,27 @@ app/
### 8.2 Layout Strategy
**Root Layout** (`app/layout.tsx`):
- HTML structure
- React Query provider
- Theme provider
- Global metadata
**Auth Layout** (`app/(auth)/layout.tsx`):
- Centered form container
- No header/footer
- Minimal styling
**Authenticated Layout** (`app/(authenticated)/layout.tsx`):
- Auth guard (redirect if not authenticated)
- Header with user menu
- Main content area
- Footer
**Admin Layout** (`app/(authenticated)/admin/layout.tsx`):
- Admin sidebar
- Breadcrumbs
- Admin permission check (is_superuser)
@@ -1036,11 +1093,13 @@ components/
### 9.2 Component Guidelines
**Naming:**
- PascalCase for components: `UserTable.tsx`
- Match file name with component name
- One component per file
**Structure:**
```typescript
// 1. Imports
import { useState } from 'react';
@@ -1078,6 +1137,7 @@ export function UserTable({ filters }: UserTableProps) {
```
**Best Practices:**
- Prefer named exports over default exports
- Destructure props in function signature
- Extract complex logic to hooks
@@ -1087,6 +1147,7 @@ export function UserTable({ filters }: UserTableProps) {
### 9.3 Styling Strategy
**Tailwind Utility Classes:**
```typescript
<button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90">
Click Me
@@ -1094,6 +1155,7 @@ export function UserTable({ filters }: UserTableProps) {
```
**Conditional Classes with cn():**
```typescript
import { cn } from '@/lib/utils/cn';
@@ -1105,6 +1167,7 @@ import { cn } from '@/lib/utils/cn';
```
**Dark Mode:**
```typescript
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
Content
@@ -1134,18 +1197,21 @@ import { cn } from '@/lib/utils/cn';
### 10.2 Test Categories
**Unit Tests** (60% of suite):
- Utilities (`lib/utils/`)
- Custom hooks (`hooks/`)
- Services (`services/`)
- Pure functions
**Component Tests** (30% of suite):
- Reusable components (`components/`)
- Forms with validation
- User interactions
- Accessibility
**Integration Tests** (E2E with Playwright, 10% of suite):
- Critical user flows:
- Login → Dashboard
- Admin: Create/Edit/Delete User
@@ -1157,6 +1223,7 @@ import { cn } from '@/lib/utils/cn';
### 10.3 Testing Tools
**Jest + React Testing Library:**
```typescript
// UserTable.test.tsx
import { render, screen } from '@testing-library/react';
@@ -1169,6 +1236,7 @@ test('renders user table with data', async () => {
```
**Playwright E2E:**
```typescript
// tests/e2e/auth.spec.ts
test('user can login', async ({ page }) => {
@@ -1183,11 +1251,13 @@ test('user can login', async ({ page }) => {
### 10.4 Coverage Target
**Goal: 90%+ Overall Coverage**
- Unit tests: 95%+
- Component tests: 85%+
- Integration tests: Critical paths only
**Justification for 90%:**
- This is a template for production projects
- High coverage ensures robustness
- Confidence for extension and customization
@@ -1199,6 +1269,7 @@ test('user can login', async ({ page }) => {
### 11.1 Optimization Strategies
**Code Splitting:**
```typescript
// Dynamic imports for heavy components
const AdminDashboard = dynamic(() => import('./AdminDashboard'), {
@@ -1207,6 +1278,7 @@ const AdminDashboard = dynamic(() => import('./AdminDashboard'), {
```
**Image Optimization:**
```typescript
import Image from 'next/image';
@@ -1220,11 +1292,13 @@ import Image from 'next/image';
```
**React Query Caching:**
- Stale time: 1 minute (reduce unnecessary refetches)
- Cache time: 5 minutes (keep data in memory)
- Background refetch: Yes (keep data fresh)
**Bundle Size Monitoring:**
```bash
npm run build && npm run analyze
# Use webpack-bundle-analyzer to identify large dependencies
@@ -1233,12 +1307,14 @@ npm run build && npm run analyze
### 11.2 Performance Targets
**Lighthouse Scores:**
- Performance: >90
- Accessibility: 100
- Best Practices: >90
- SEO: >90
**Core Web Vitals:**
- LCP (Largest Contentful Paint): <2.5s
- FID (First Input Delay): <100ms
- CLS (Cumulative Layout Shift): <0.1
@@ -1250,16 +1326,19 @@ npm run build && npm run analyze
### 12.1 Client-Side Security
**XSS Prevention:**
- React's default escaping (JSX)
- Sanitize user input if rendering HTML
- CSP headers (configured in backend)
**Token Security:**
- Access token: sessionStorage or memory (15 min expiry mitigates risk)
- Refresh token: httpOnly cookie (preferred) or encrypted localStorage
- Never log tokens to console in production
**HTTPS Only:**
- All production requests over HTTPS
- Cookies with Secure flag
- No mixed content
@@ -1267,11 +1346,13 @@ npm run build && npm run analyze
### 12.2 Input Validation
**Client-Side Validation:**
- Zod schemas for all forms
- Immediate feedback to users
- Prevent malformed requests
**Remember:**
- Client validation is for UX
- Backend validation is for security
- Always trust backend, not client
@@ -1279,12 +1360,14 @@ npm run build && npm run analyze
### 12.3 Dependency Security
**Regular Audits:**
```bash
npm audit
npm audit fix
```
**Automated Scanning:**
- Dependabot (GitHub)
- Snyk (CI/CD integration)
@@ -1295,12 +1378,14 @@ npm audit fix
### 13.1 Why Next.js App Router?
**Pros:**
- Server Components reduce client bundle
- Better data fetching patterns
- Streaming and Suspense built-in
- Simpler layouts and error handling
**Cons:**
- Newer, less mature than Pages Router
- Learning curve for team
@@ -1309,11 +1394,13 @@ npm audit fix
### 13.2 Why TanStack Query?
**Alternatives Considered:**
- SWR: Similar but less features
- Redux Toolkit Query: Too much boilerplate for our use case
- Apollo Client: Overkill for REST API
**Why TanStack Query:**
- Best-in-class caching and refetching
- Framework-agnostic (not tied to Next.js)
- Excellent DevTools
@@ -1322,11 +1409,13 @@ npm audit fix
### 13.3 Why Zustand over Redux?
**Why NOT Redux:**
- Too much boilerplate (actions, reducers, middleware)
- We don't need time-travel debugging
- Most state is server state (handled by React Query)
**Why Zustand:**
- Minimal API (easy to learn)
- No Context API overhead
- Can use outside React (interceptors)
@@ -1335,11 +1424,13 @@ npm audit fix
### 13.4 Why shadcn/ui over Component Libraries?
**Alternatives Considered:**
- Material-UI: Heavy, opinionated styling
- Chakra UI: Good, but still an npm dependency
- Ant Design: Too opinionated for template
**Why shadcn/ui:**
- Copy-paste (full control)
- Accessible (Radix UI primitives)
- Tailwind-based (consistent with our stack)
@@ -1348,11 +1439,13 @@ npm audit fix
### 13.5 Why Axios over Fetch?
**Why NOT Fetch:**
- No request/response interceptors
- Manual timeout handling
- Less ergonomic error handling
**Why Axios:**
- Interceptors (essential for auth)
- Automatic JSON parsing
- Better error handling
@@ -1364,11 +1457,13 @@ npm audit fix
**Decision: httpOnly Cookies (Primary), localStorage (Fallback)**
**Why httpOnly Cookies:**
- Most secure (not accessible to JavaScript)
- Prevents XSS token theft
- Automatic sending with requests (if CORS configured)
**Why Fallback to localStorage:**
- Simpler initial setup (no backend cookie handling)
- Still secure with proper measures:
- Short access token expiry (15 min)
@@ -1377,6 +1472,7 @@ npm audit fix
- Encrypted wrapper (optional)
**Implementation:**
- Try httpOnly cookies first
- Fall back to localStorage if not feasible
- Document choice in code
@@ -1388,12 +1484,14 @@ npm audit fix
### 14.1 Production Deployment
**Recommended Platform: Vercel**
- Native Next.js support
- Edge functions for middleware
- Automatic preview deployments
- CDN with global edge network
**Alternative: Docker**
```dockerfile
FROM node:20-alpine
WORKDIR /app
@@ -1408,18 +1506,21 @@ CMD ["npm", "start"]
### 14.2 Environment Configuration
**Development:**
```env
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
NODE_ENV=development
```
**Production:**
```env
NEXT_PUBLIC_API_URL=https://api.example.com/api/v1
NODE_ENV=production
```
**Secrets:**
- Never commit `.env.local`
- Use platform-specific secret management (Vercel Secrets, Docker Secrets)
@@ -1453,6 +1554,7 @@ jobs:
This architecture document provides a comprehensive overview of the frontend system design, patterns, and decisions. It should serve as a reference for developers working on the project and guide future architectural decisions.
For specific implementation details, refer to:
- **CODING_STANDARDS.md**: Code style and conventions
- **COMPONENT_GUIDE.md**: Component usage and patterns
- **FEATURE_EXAMPLES.md**: Step-by-step feature implementation

View File

@@ -31,6 +31,7 @@
### 1.1 General Principles
**✅ DO:**
- Enable strict mode in `tsconfig.json`
- Define explicit types for all function parameters and return values
- Use TypeScript's type inference where obvious
@@ -38,6 +39,7 @@
- Use generics for reusable, type-safe components and functions
**❌ DON'T:**
- Use `any` (use `unknown` if type is truly unknown)
- Use `as any` casts (refactor to proper types)
- Use `@ts-ignore` or `@ts-nocheck` (fix the underlying issue)
@@ -46,6 +48,7 @@
### 1.2 Interfaces vs Types
**Use `interface` for object shapes:**
```typescript
// ✅ Good
interface User {
@@ -63,6 +66,7 @@ interface UserFormProps {
```
**Use `type` for unions, intersections, and primitives:**
```typescript
// ✅ Good
type UserRole = 'admin' | 'user' | 'guest';
@@ -74,6 +78,7 @@ type UserWithPermissions = User & { permissions: string[] };
### 1.3 Function Signatures
**Always type function parameters and return values:**
```typescript
// ✅ Good
function formatUserName(user: User): string {
@@ -86,7 +91,8 @@ async function fetchUser(id: string): Promise<User> {
}
// ❌ Bad
function formatUserName(user) { // Implicit any
function formatUserName(user) {
// Implicit any
return `${user.firstName} ${user.lastName}`;
}
```
@@ -94,6 +100,7 @@ function formatUserName(user) { // Implicit any
### 1.4 Generics
**Use generics for reusable, type-safe code:**
```typescript
// ✅ Good: Generic data table
interface DataTableProps<T> {
@@ -113,6 +120,7 @@ export function DataTable<T>({ data, columns, onRowClick }: DataTableProps<T>) {
### 1.5 Unknown vs Any
**Use `unknown` for truly unknown types:**
```typescript
// ✅ Good: Force type checking
function parseJSON(jsonString: string): unknown {
@@ -134,15 +142,11 @@ function parseJSON(jsonString: string): any {
### 1.6 Type Guards
**Create type guards for runtime type checking:**
```typescript
// ✅ Good
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'email' in value
);
return typeof value === 'object' && value !== null && 'id' in value && 'email' in value;
}
// Usage
@@ -155,6 +159,7 @@ if (isUser(data)) {
### 1.7 Utility Types
**Use TypeScript utility types:**
```typescript
// Partial - make all properties optional
type UserUpdate = Partial<User>;
@@ -182,6 +187,7 @@ type NonAdminRole = Exclude<UserRole, 'admin'>;
### 2.1 Component Structure
**Standard component template:**
```typescript
// 1. Imports (React, external libs, internal, types, styles)
'use client'; // If client component
@@ -240,6 +246,7 @@ export function UserList({
### 2.2 Server Components vs Client Components
**Server Components by default:**
```typescript
// ✅ Good: Server Component (default)
// app/(authenticated)/users/page.tsx
@@ -255,6 +262,7 @@ export default async function UsersPage() {
```
**Client Components only when needed:**
```typescript
// ✅ Good: Client Component (interactive)
'use client';
@@ -276,6 +284,7 @@ export function UserList() {
```
**When to use Client Components:**
- Using React hooks (useState, useEffect, etc.)
- Event handlers (onClick, onChange, etc.)
- Browser APIs (window, document, localStorage)
@@ -284,6 +293,7 @@ export function UserList() {
### 2.3 Props
**Always type props explicitly:**
```typescript
// ✅ Good: Explicit interface
interface ButtonProps {
@@ -300,13 +310,14 @@ export function Button({
onClick,
variant = 'primary',
disabled = false,
className
className,
}: ButtonProps) {
// Implementation
}
```
**Destructure props in function signature:**
```typescript
// ✅ Good
function UserCard({ user, onEdit }: UserCardProps) {
@@ -320,6 +331,7 @@ function UserCard(props: UserCardProps) {
```
**Allow className override:**
```typescript
// ✅ Good: Allow consumers to add classes
interface CardProps {
@@ -339,6 +351,7 @@ export function Card({ title, className }: CardProps) {
### 2.4 Composition Over Prop Drilling
**Use composition patterns:**
```typescript
// ✅ Good: Composition
<Card>
@@ -366,6 +379,7 @@ export function Card({ title, className }: CardProps) {
### 2.5 Named Exports vs Default Exports
**Prefer named exports:**
```typescript
// ✅ Good: Named export
export function UserList() {
@@ -385,6 +399,7 @@ import WhateverName from './UserList';
```
**Exception: Next.js pages must use default export**
```typescript
// pages and route handlers require default export
export default function UsersPage() {
@@ -395,6 +410,7 @@ export default function UsersPage() {
### 2.6 Custom Hooks
**Extract reusable logic:**
```typescript
// ✅ Good: Custom hook
function useDebounce<T>(value: T, delay: number): T {
@@ -425,15 +441,16 @@ function SearchInput() {
```
**Hook naming: Always prefix with "use":**
```typescript
// ✅ Good
function useAuth() { }
function useUsers() { }
function useDebounce() { }
function useAuth() {}
function useUsers() {}
function useDebounce() {}
// ❌ Bad
function auth() { }
function getUsers() { }
function auth() {}
function getUsers() {}
```
---
@@ -442,20 +459,21 @@ function getUsers() { }
### 3.1 Files and Directories
| Type | Convention | Example |
|------|------------|---------|
| Components | PascalCase | `UserTable.tsx`, `LoginForm.tsx` |
| Hooks | camelCase with `use` prefix | `useAuth.ts`, `useDebounce.ts` |
| Utilities | camelCase | `formatDate.ts`, `parseError.ts` |
| Types | camelCase | `user.ts`, `api.ts` |
| Constants | camelCase or UPPER_SNAKE_CASE | `constants.ts`, `API_ENDPOINTS.ts` |
| Stores | camelCase with `Store` suffix | `authStore.ts`, `uiStore.ts` |
| Services | camelCase with `Service` suffix | `authService.ts`, `adminService.ts` |
| Pages (Next.js) | lowercase | `page.tsx`, `layout.tsx`, `loading.tsx` |
| Type | Convention | Example |
| --------------- | ------------------------------- | --------------------------------------- |
| Components | PascalCase | `UserTable.tsx`, `LoginForm.tsx` |
| Hooks | camelCase with `use` prefix | `useAuth.ts`, `useDebounce.ts` |
| Utilities | camelCase | `formatDate.ts`, `parseError.ts` |
| Types | camelCase | `user.ts`, `api.ts` |
| Constants | camelCase or UPPER_SNAKE_CASE | `constants.ts`, `API_ENDPOINTS.ts` |
| Stores | camelCase with `Store` suffix | `authStore.ts`, `uiStore.ts` |
| Services | camelCase with `Service` suffix | `authService.ts`, `adminService.ts` |
| Pages (Next.js) | lowercase | `page.tsx`, `layout.tsx`, `loading.tsx` |
### 3.2 Variables and Functions
**Variables:**
```typescript
// ✅ Good: camelCase
const userName = 'John';
@@ -463,36 +481,39 @@ const isAuthenticated = true;
const userList = [];
// ❌ Bad
const UserName = 'John'; // PascalCase for variable
const UserName = 'John'; // PascalCase for variable
const user_name = 'John'; // snake_case
```
**Functions:**
```typescript
// ✅ Good: camelCase, descriptive verb + noun
function getUserById(id: string): User { }
function formatDate(date: Date): string { }
function handleSubmit(data: FormData): void { }
function getUserById(id: string): User {}
function formatDate(date: Date): string {}
function handleSubmit(data: FormData): void {}
// ❌ Bad
function User(id: string) { } // Looks like a class
function get_user(id: string) { } // snake_case
function gub(id: string) { } // Not descriptive
function User(id: string) {} // Looks like a class
function get_user(id: string) {} // snake_case
function gub(id: string) {} // Not descriptive
```
**Event Handlers:**
```typescript
// ✅ Good: handle + EventName
const handleClick = () => { };
const handleSubmit = () => { };
const handleInputChange = () => { };
const handleClick = () => {};
const handleSubmit = () => {};
const handleInputChange = () => {};
// ❌ Bad
const onClick = () => { }; // Confusing with prop name
const submit = () => { };
const onClick = () => {}; // Confusing with prop name
const submit = () => {};
```
**Boolean Variables:**
```typescript
// ✅ Good: is/has/should prefix
const isLoading = true;
@@ -508,6 +529,7 @@ const error = false;
### 3.3 Constants
**Use UPPER_SNAKE_CASE for true constants:**
```typescript
// ✅ Good
const MAX_RETRY_ATTEMPTS = 3;
@@ -525,16 +547,17 @@ const USER_ROLES = {
### 3.4 Types and Interfaces
**PascalCase for types and interfaces:**
```typescript
// ✅ Good
interface User { }
interface UserFormProps { }
interface User {}
interface UserFormProps {}
type UserId = string;
type UserRole = 'admin' | 'user';
// ❌ Bad
interface user { }
interface user_form_props { }
interface user {}
interface user_form_props {}
type userId = string;
```
@@ -545,6 +568,7 @@ type userId = string;
### 4.1 Import Order
**Organize imports in this order:**
```typescript
// 1. React and Next.js
import { useState, useEffect } from 'react';
@@ -576,6 +600,7 @@ import styles from './Component.module.css';
### 4.2 Co-location
**Group related files together:**
```
components/admin/
├── UserTable/
@@ -587,6 +612,7 @@ components/admin/
```
**Or flat structure for simpler components:**
```
components/admin/
├── UserTable.tsx
@@ -598,6 +624,7 @@ components/admin/
### 4.3 Barrel Exports
**Use index.ts for clean imports:**
```typescript
// components/ui/index.ts
export { Button } from './button';
@@ -615,6 +642,7 @@ import { Button, Card, Input } from '@/components/ui';
### 5.1 State Placement
**Keep state as local as possible:**
```typescript
// ✅ Good: Local state
function UserFilter() {
@@ -629,6 +657,7 @@ function UserFilter() {
### 5.2 TanStack Query Usage
**Standard query pattern:**
```typescript
// lib/api/hooks/useUsers.ts
export function useUsers(filters?: UserFilters) {
@@ -651,6 +680,7 @@ function UserList() {
```
**Standard mutation pattern:**
```typescript
// lib/api/hooks/useUsers.ts
export function useUpdateUser() {
@@ -691,22 +721,21 @@ function UserForm({ userId }: { userId: string }) {
```
**Query key structure:**
```typescript
// ✅ Good: Consistent query keys
['users'] // List all
['users', userId] // Single user
['users', { search: 'john', page: 1 }] // Filtered list
['organizations', orgId, 'members'] // Nested resource
// ❌ Bad: Inconsistent
['userList']
['user-' + userId]
['getUsersBySearch', 'john']
['users'][('users', userId)][('users', { search: 'john', page: 1 })][ // List all // Single user // Filtered list
('organizations', orgId, 'members')
][ // Nested resource
// ❌ Bad: Inconsistent
'userList'
]['user-' + userId][('getUsersBySearch', 'john')];
```
### 5.3 Zustand Store Pattern
**Auth store example:**
```typescript
// stores/authStore.ts
import { create } from 'zustand';
@@ -757,6 +786,7 @@ function UserAvatar() {
```
**UI store example:**
```typescript
// stores/uiStore.ts
interface UIStore {
@@ -790,6 +820,7 @@ export const useUIStore = create<UIStore>()(
### 6.1 API Client Structure
**Axios instance configuration:**
```typescript
// lib/api/client.ts
import axios from 'axios';
@@ -837,6 +868,7 @@ apiClient.interceptors.response.use(
### 6.2 Error Handling
**Parse API errors:**
```typescript
// lib/api/errors.ts
export interface APIError {
@@ -850,18 +882,20 @@ export function parseAPIError(error: AxiosError): APIError[] {
return error.response.data.errors;
}
return [{
code: 'UNKNOWN',
message: error.message || 'An unexpected error occurred',
}];
return [
{
code: 'UNKNOWN',
message: error.message || 'An unexpected error occurred',
},
];
}
// Error code mapping
export const ERROR_MESSAGES: Record<string, string> = {
'AUTH_001': 'Invalid email or password',
'USER_002': 'This email is already registered',
'VAL_001': 'Please check your input',
'ORG_001': 'Organization name already exists',
AUTH_001: 'Invalid email or password',
USER_002: 'This email is already registered',
VAL_001: 'Please check your input',
ORG_001: 'Organization name already exists',
};
export function getErrorMessage(code: string): string {
@@ -872,18 +906,19 @@ export function getErrorMessage(code: string): string {
### 6.3 Hook Organization
**One hook file per resource:**
```typescript
// lib/api/hooks/useUsers.ts
export function useUsers(filters?: UserFilters) { }
export function useUser(userId: string) { }
export function useCreateUser() { }
export function useUpdateUser() { }
export function useDeleteUser() { }
export function useUsers(filters?: UserFilters) {}
export function useUser(userId: string) {}
export function useCreateUser() {}
export function useUpdateUser() {}
export function useDeleteUser() {}
// lib/api/hooks/useOrganizations.ts
export function useOrganizations() { }
export function useOrganization(orgId: string) { }
export function useCreateOrganization() { }
export function useOrganizations() {}
export function useOrganization(orgId: string) {}
export function useCreateOrganization() {}
// ...
```
@@ -894,6 +929,7 @@ export function useCreateOrganization() { }
### 7.1 Form Pattern with react-hook-form + Zod
**Standard form implementation:**
```typescript
// components/auth/LoginForm.tsx
'use client';
@@ -961,30 +997,34 @@ export function LoginForm() {
### 7.2 Form Validation
**Complex validation with Zod:**
```typescript
const userSchema = z.object({
email: z.string().email('Invalid email'),
password: z
.string()
.min(8, 'Min 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain number'),
confirmPassword: z.string(),
firstName: z.string().min(1, 'Required'),
lastName: z.string().optional(),
phoneNumber: z
.string()
.regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
.optional(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
const userSchema = z
.object({
email: z.string().email('Invalid email'),
password: z
.string()
.min(8, 'Min 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain number'),
confirmPassword: z.string(),
firstName: z.string().min(1, 'Required'),
lastName: z.string().optional(),
phoneNumber: z
.string()
.regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
.optional(),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
```
### 7.3 Form Accessibility
**Always include labels and error messages:**
```typescript
<div>
<Label htmlFor="email">Email</Label>
@@ -1009,6 +1049,7 @@ const userSchema = z.object({
### 8.1 Tailwind CSS Usage
**Use utility classes:**
```typescript
// ✅ Good
<button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90">
@@ -1022,6 +1063,7 @@ const userSchema = z.object({
```
**Use cn() for conditional classes:**
```typescript
import { cn } from '@/lib/utils/cn';
@@ -1036,6 +1078,7 @@ import { cn } from '@/lib/utils/cn';
### 8.2 Responsive Design
**Mobile-first approach:**
```typescript
<div className="
w-full p-4 /* Mobile */
@@ -1050,6 +1093,7 @@ import { cn } from '@/lib/utils/cn';
### 8.3 Dark Mode
**Use dark mode classes:**
```typescript
<div className="
bg-white text-black
@@ -1079,6 +1123,7 @@ src/
### 10.2 Test Naming
**Use descriptive test names:**
```typescript
// ✅ Good
test('displays user list when data is loaded', async () => {});
@@ -1095,6 +1140,7 @@ test('renders', () => {});
### 10.3 Component Testing
**Test user interactions, not implementation:**
```typescript
// UserTable.test.tsx
import { render, screen, userEvent } from '@testing-library/react';
@@ -1115,6 +1161,7 @@ test('allows user to search for users', async () => {
### 10.4 Accessibility Testing
**Test with accessibility queries:**
```typescript
// Prefer getByRole over getByTestId
const button = screen.getByRole('button', { name: 'Submit' });
@@ -1156,6 +1203,7 @@ const textbox = screen.getByRole('textbox', { name: 'Email' });
### 11.2 ARIA Labels
**Use ARIA when semantic HTML isn't enough:**
```typescript
<button aria-label="Close dialog">
<X className="w-4 h-4" />
@@ -1169,6 +1217,7 @@ const textbox = screen.getByRole('textbox', { name: 'Email' });
### 11.3 Keyboard Navigation
**Ensure all interactive elements are keyboard accessible:**
```typescript
<div
role="button"
@@ -1191,6 +1240,7 @@ const textbox = screen.getByRole('textbox', { name: 'Email' });
### 12.1 Code Splitting
**Dynamic imports for heavy components:**
```typescript
import dynamic from 'next/dynamic';
@@ -1203,6 +1253,7 @@ const HeavyChart = dynamic(() => import('./HeavyChart'), {
### 12.2 Memoization
**Use React.memo for expensive renders:**
```typescript
export const UserCard = React.memo(function UserCard({ user }: UserCardProps) {
return <div>{user.name}</div>;
@@ -1210,6 +1261,7 @@ export const UserCard = React.memo(function UserCard({ user }: UserCardProps) {
```
**Use useMemo for expensive calculations:**
```typescript
const sortedUsers = useMemo(() => {
return users.sort((a, b) => a.name.localeCompare(b.name));
@@ -1219,6 +1271,7 @@ const sortedUsers = useMemo(() => {
### 12.3 Image Optimization
**Always use Next.js Image component:**
```typescript
import Image from 'next/image';
@@ -1238,6 +1291,7 @@ import Image from 'next/image';
### 13.1 Input Sanitization
**Never render raw HTML:**
```typescript
// ✅ Good: React escapes by default
<div>{userInput}</div>
@@ -1249,6 +1303,7 @@ import Image from 'next/image';
### 13.2 Environment Variables
**Never commit secrets:**
```typescript
// ✅ Good: Use env variables
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
@@ -1258,6 +1313,7 @@ const apiUrl = 'https://api.example.com';
```
**Public vs Private:**
- `NEXT_PUBLIC_*`: Exposed to browser
- Other vars: Server-side only
@@ -1266,6 +1322,7 @@ const apiUrl = 'https://api.example.com';
## 14. Code Review Checklist
**Before submitting PR:**
- [ ] All tests pass
- [ ] No TypeScript errors
- [ ] ESLint passes
@@ -1285,6 +1342,7 @@ const apiUrl = 'https://api.example.com';
These standards ensure consistency, maintainability, and quality across the codebase. Follow them rigorously, and update this document as the project evolves.
For specific patterns and examples, refer to:
- **ARCHITECTURE.md**: System design and patterns
- **COMPONENT_GUIDE.md**: Component usage and examples
- **FEATURE_EXAMPLES.md**: Step-by-step implementation guides

View File

@@ -27,44 +27,48 @@
### Pitfall 1.1: Returning Hook Function Instead of Calling It
**❌ WRONG:**
```typescript
// Custom hook that wraps Zustand
export function useAuth() {
const storeHook = useContext(AuthContext);
return storeHook; // Returns the hook function itself!
return storeHook; // Returns the hook function itself!
}
// Consumer component
function MyComponent() {
const authHook = useAuth(); // Got the hook function
const { user } = authHook(); // Have to call it here ❌ Rules of Hooks violation!
const authHook = useAuth(); // Got the hook function
const { user } = authHook(); // Have to call it here ❌ Rules of Hooks violation!
}
```
**Why It's Wrong:**
- Violates React Rules of Hooks (hook called conditionally/in wrong place)
- Confusing API for consumers
- Can't use in conditionals or callbacks safely
- Type inference breaks
**✅ CORRECT:**
```typescript
// Custom hook that calls the wrapped hook internally
export function useAuth() {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
throw new Error('useAuth must be used within AuthProvider');
}
return storeHook(); // Call the hook HERE, return the state
return storeHook(); // Call the hook HERE, return the state
}
// Consumer component
function MyComponent() {
const { user } = useAuth(); // Direct access to state ✅
const { user } = useAuth(); // Direct access to state ✅
}
```
**✅ EVEN BETTER (Polymorphic):**
```typescript
// Support both patterns
export function useAuth(): AuthState;
@@ -72,17 +76,18 @@ export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
throw new Error('useAuth must be used within AuthProvider');
}
return selector ? storeHook(selector) : storeHook();
}
// Usage - both work!
const { user } = useAuth(); // Full state
const user = useAuth(s => s.user); // Optimized selector
const { user } = useAuth(); // Full state
const user = useAuth((s) => s.user); // Optimized selector
```
**Key Takeaway:**
- **Always call hooks internally in custom hooks**
- Return state/values, not hook functions
- Support selectors for performance optimization
@@ -92,6 +97,7 @@ const user = useAuth(s => s.user); // Optimized selector
### Pitfall 1.2: Calling Hooks Conditionally
**❌ WRONG:**
```typescript
function MyComponent({ showUser }) {
if (showUser) {
@@ -103,6 +109,7 @@ function MyComponent({ showUser }) {
```
**✅ CORRECT:**
```typescript
function MyComponent({ showUser }) {
const { user } = useAuth(); // ✅ Always call at top level
@@ -116,6 +123,7 @@ function MyComponent({ showUser }) {
```
**Key Takeaway:**
- **Always call hooks at the top level of your component**
- Never call hooks inside conditionals, loops, or nested functions
- Return early after hooks are called
@@ -127,6 +135,7 @@ function MyComponent({ showUser }) {
### Pitfall 2.1: Creating New Context Value on Every Render
**❌ WRONG:**
```typescript
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
@@ -139,11 +148,13 @@ export function AuthProvider({ children }) {
```
**Why It's Wrong:**
- Every render creates a new object
- All consumers re-render even if values unchanged
- Performance nightmare in large apps
**✅ CORRECT:**
```typescript
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
@@ -156,6 +167,7 @@ export function AuthProvider({ children }) {
```
**✅ EVEN BETTER (Zustand + Context):**
```typescript
export function AuthProvider({ children, store }) {
// Zustand hook function is stable (doesn't change)
@@ -167,6 +179,7 @@ export function AuthProvider({ children, store }) {
```
**Key Takeaway:**
- **Use `useMemo` for Context values that are objects**
- Or use stable references (Zustand hooks, refs)
- Monitor re-renders with React DevTools
@@ -176,6 +189,7 @@ export function AuthProvider({ children, store }) {
### Pitfall 2.2: Prop Drilling Instead of Context
**❌ WRONG:**
```typescript
// Passing through 5 levels
<Layout user={user}>
@@ -190,6 +204,7 @@ export function AuthProvider({ children, store }) {
```
**✅ CORRECT:**
```typescript
// Provider at top
<AuthProvider>
@@ -206,6 +221,7 @@ export function AuthProvider({ children, store }) {
```
**Key Takeaway:**
- **Use Context for data needed by many components**
- Avoid prop drilling beyond 2-3 levels
- But don't overuse - local state is often better
@@ -217,6 +233,7 @@ export function AuthProvider({ children, store }) {
### Pitfall 3.1: Mixing Render State Access and Mutation Logic
**❌ WRONG (Mixing patterns):**
```typescript
function MyComponent() {
// Using hook for render state
@@ -231,6 +248,7 @@ function MyComponent() {
```
**✅ CORRECT (Separate patterns):**
```typescript
function MyComponent() {
// Hook for render state (subscribes to changes)
@@ -245,12 +263,14 @@ function MyComponent() {
```
**Why This Pattern?**
- **Render state**: Use hook → component re-renders on changes
- **Mutations**: Use `getState()` → no subscription, no re-renders
- **Performance**: Event handlers don't need to subscribe
- **Clarity**: Clear distinction between read and write
**Key Takeaway:**
- **Use hooks for state that affects rendering**
- **Use `getState()` for mutations in callbacks**
- Don't subscribe when you don't need to
@@ -260,6 +280,7 @@ function MyComponent() {
### Pitfall 3.2: Not Using Selectors for Optimization
**❌ SUBOPTIMAL:**
```typescript
function UserAvatar() {
// Re-renders on ANY auth state change! ❌
@@ -270,6 +291,7 @@ function UserAvatar() {
```
**✅ OPTIMIZED:**
```typescript
function UserAvatar() {
// Only re-renders when user changes ✅
@@ -280,6 +302,7 @@ function UserAvatar() {
```
**Key Takeaway:**
- **Use selectors for components that only need subset of state**
- Reduces unnecessary re-renders
- Especially important in frequently updating stores
@@ -291,13 +314,16 @@ function UserAvatar() {
### Pitfall 4.1: Using `any` Type
**❌ WRONG:**
```typescript
function processUser(user: any) { // ❌ Loses all type safety
return user.name.toUpperCase(); // No error if user.name is undefined
function processUser(user: any) {
// ❌ Loses all type safety
return user.name.toUpperCase(); // No error if user.name is undefined
}
```
**✅ CORRECT:**
```typescript
function processUser(user: User | null) {
if (!user?.name) {
@@ -308,6 +334,7 @@ function processUser(user: User | null) {
```
**Key Takeaway:**
- **Never use `any` - use `unknown` if type is truly unknown**
- Define proper types for all function parameters
- Use type guards for runtime checks
@@ -317,15 +344,17 @@ function processUser(user: User | null) {
### Pitfall 4.2: Implicit Types Leading to Errors
**❌ WRONG:**
```typescript
// No explicit return type - type inference can be wrong
export function useAuth() {
const context = useContext(AuthContext);
return context; // What type is this? ❌
return context; // What type is this? ❌
}
```
**✅ CORRECT:**
```typescript
// Explicit return type with overloads
export function useAuth(): AuthState;
@@ -333,13 +362,14 @@ export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
throw new Error('useAuth must be used within AuthProvider');
}
return selector ? context(selector) : context();
}
```
**Key Takeaway:**
- **Always provide explicit return types for public APIs**
- Use function overloads for polymorphic functions
- Document types in JSDoc comments
@@ -349,16 +379,19 @@ export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
### Pitfall 4.3: Not Using `import type` for Type-Only Imports
**❌ SUBOPTIMAL:**
```typescript
import { ReactNode } from 'react'; // Might be bundled even if only used for types
import { ReactNode } from 'react'; // Might be bundled even if only used for types
```
**✅ CORRECT:**
```typescript
import type { ReactNode } from 'react'; // Guaranteed to be stripped from bundle
import type { ReactNode } from 'react'; // Guaranteed to be stripped from bundle
```
**Key Takeaway:**
- **Use `import type` for type-only imports**
- Smaller bundle size
- Clearer intent
@@ -370,6 +403,7 @@ import type { ReactNode } from 'react'; // Guaranteed to be stripped from bundl
### Pitfall 5.1: Forgetting Optional Chaining for Nullable Values
**❌ WRONG:**
```typescript
function UserProfile() {
const { user } = useAuth();
@@ -378,6 +412,7 @@ function UserProfile() {
```
**✅ CORRECT:**
```typescript
function UserProfile() {
const { user } = useAuth();
@@ -397,6 +432,7 @@ function UserProfile() {
```
**Key Takeaway:**
- **Always handle null/undefined cases**
- Use optional chaining (`?.`) and nullish coalescing (`??`)
- Provide fallback UI for missing data
@@ -406,6 +442,7 @@ function UserProfile() {
### Pitfall 5.2: Mixing Concerns in Components
**❌ WRONG:**
```typescript
function UserDashboard() {
const [users, setUsers] = useState([]);
@@ -429,6 +466,7 @@ function UserDashboard() {
```
**✅ CORRECT:**
```typescript
// Custom hook for data fetching
function useUsers() {
@@ -460,6 +498,7 @@ function UserDashboard() {
```
**Key Takeaway:**
- **Separate concerns: data fetching, business logic, rendering**
- Extract logic to custom hooks
- Keep components focused on UI
@@ -471,6 +510,7 @@ function UserDashboard() {
### Pitfall 6.1: Wrong Provider Order
**❌ WRONG:**
```typescript
// AuthInitializer outside AuthProvider ❌
function RootLayout({ children }) {
@@ -486,6 +526,7 @@ function RootLayout({ children }) {
```
**✅ CORRECT:**
```typescript
function RootLayout({ children }) {
return (
@@ -500,6 +541,7 @@ function RootLayout({ children }) {
```
**Key Takeaway:**
- **Providers must wrap components that use them**
- Order matters when there are dependencies
- Keep provider tree shallow (performance)
@@ -509,6 +551,7 @@ function RootLayout({ children }) {
### Pitfall 6.2: Creating Too Many Providers
**❌ WRONG:**
```typescript
// Separate provider for every piece of state ❌
<UserProvider>
@@ -525,6 +568,7 @@ function RootLayout({ children }) {
```
**✅ BETTER:**
```typescript
// Combine related state, use Zustand for most things
<AuthProvider> {/* Only for auth DI */}
@@ -541,6 +585,7 @@ const useUserPreferences = create(...); // User settings
```
**Key Takeaway:**
- **Use Context only when necessary** (DI, third-party integrations)
- **Use Zustand for most global state** (no provider needed)
- Avoid provider hell
@@ -552,6 +597,7 @@ const useUserPreferences = create(...); // User settings
### Pitfall 7.1: Using Hooks in Event Handlers
**❌ WRONG:**
```typescript
function MyComponent() {
const handleClick = () => {
@@ -564,6 +610,7 @@ function MyComponent() {
```
**✅ CORRECT:**
```typescript
function MyComponent() {
const { user } = useAuth(); // ✅ Hook at component top level
@@ -587,6 +634,7 @@ function MyComponent() {
```
**Key Takeaway:**
- **Never call hooks inside event handlers**
- For render state: Call hook at top level, access in closure
- For mutations: Use `store.getState().method()`
@@ -596,13 +644,15 @@ function MyComponent() {
### Pitfall 7.2: Not Handling Async Errors in Event Handlers
**❌ WRONG:**
```typescript
const handleSubmit = async (data: FormData) => {
await apiCall(data); // ❌ No error handling!
await apiCall(data); // ❌ No error handling!
};
```
**✅ CORRECT:**
```typescript
const handleSubmit = async (data: FormData) => {
try {
@@ -616,6 +666,7 @@ const handleSubmit = async (data: FormData) => {
```
**Key Takeaway:**
- **Always wrap async calls in try/catch**
- Provide user feedback for both success and errors
- Log errors for debugging
@@ -627,6 +678,7 @@ const handleSubmit = async (data: FormData) => {
### Pitfall 8.1: Not Mocking Context Providers in Tests
**❌ WRONG:**
```typescript
// Test without provider ❌
test('renders user name', () => {
@@ -636,6 +688,7 @@ test('renders user name', () => {
```
**✅ CORRECT:**
```typescript
// Mock the hook
jest.mock('@/lib/stores', () => ({
@@ -654,6 +707,7 @@ test('renders user name', () => {
```
**Key Takeaway:**
- **Mock hooks at module level in tests**
- Provide necessary return values for each test case
- Test both success and error states
@@ -663,6 +717,7 @@ test('renders user name', () => {
### Pitfall 8.2: Testing Implementation Details
**❌ WRONG:**
```typescript
test('calls useAuthStore hook', () => {
const spy = jest.spyOn(require('@/lib/stores'), 'useAuthStore');
@@ -672,6 +727,7 @@ test('calls useAuthStore hook', () => {
```
**✅ CORRECT:**
```typescript
test('displays user name when authenticated', () => {
(useAuth as jest.Mock).mockReturnValue({
@@ -685,6 +741,7 @@ test('displays user name when authenticated', () => {
```
**Key Takeaway:**
- **Test behavior, not implementation**
- Focus on what the user sees/does
- Don't test internal API calls unless critical
@@ -696,6 +753,7 @@ test('displays user name when authenticated', () => {
### Pitfall 9.1: Not Using React.memo for Expensive Components
**❌ SUBOPTIMAL:**
```typescript
// Re-renders every time parent re-renders ❌
function ExpensiveChart({ data }) {
@@ -705,6 +763,7 @@ function ExpensiveChart({ data }) {
```
**✅ OPTIMIZED:**
```typescript
// Only re-renders when data changes ✅
export const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
@@ -713,6 +772,7 @@ export const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
```
**Key Takeaway:**
- **Use `React.memo` for expensive components**
- Especially useful for list items, charts, heavy UI
- Profile with React DevTools to identify candidates
@@ -722,6 +782,7 @@ export const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
### Pitfall 9.2: Creating Functions Inside Render
**❌ SUBOPTIMAL:**
```typescript
function MyComponent() {
return (
@@ -733,6 +794,7 @@ function MyComponent() {
```
**✅ OPTIMIZED:**
```typescript
function MyComponent() {
const handleClick = useCallback(() => {
@@ -744,15 +806,18 @@ function MyComponent() {
```
**When to Optimize:**
- **For memoized child components** (memo, PureComponent)
- **For expensive event handlers**
- **When profiling shows performance issues**
**When NOT to optimize:**
- **Simple components with cheap operations** (premature optimization)
- **One-off event handlers**
**Key Takeaway:**
- **Use `useCallback` for functions passed to memoized children**
- But don't optimize everything - profile first
@@ -763,6 +828,7 @@ function MyComponent() {
### Pitfall 10.1: Not Using Barrel Exports
**❌ INCONSISTENT:**
```typescript
// Deep imports all over the codebase
import { useAuth } from '@/lib/auth/AuthContext';
@@ -771,6 +837,7 @@ import { User } from '@/lib/stores/authStore';
```
**✅ CONSISTENT:**
```typescript
// Barrel exports in stores/index.ts
export { useAuth, AuthProvider } from '../auth/AuthContext';
@@ -781,6 +848,7 @@ import { useAuth, useAuthStore, User } from '@/lib/stores';
```
**Key Takeaway:**
- **Create barrel exports (index.ts) for public APIs**
- Easier to refactor internal structure
- Consistent import paths across codebase
@@ -790,31 +858,44 @@ import { useAuth, useAuthStore, User } from '@/lib/stores';
### Pitfall 10.2: Circular Dependencies
**❌ WRONG:**
```typescript
// fileA.ts
import { functionB } from './fileB';
export function functionA() { return functionB(); }
export function functionA() {
return functionB();
}
// fileB.ts
import { functionA } from './fileA'; // ❌ Circular!
export function functionB() { return functionA(); }
import { functionA } from './fileA'; // ❌ Circular!
export function functionB() {
return functionA();
}
```
**✅ CORRECT:**
```typescript
// utils.ts
export function sharedFunction() { /* shared logic */ }
export function sharedFunction() {
/* shared logic */
}
// fileA.ts
import { sharedFunction } from './utils';
export function functionA() { return sharedFunction(); }
export function functionA() {
return sharedFunction();
}
// fileB.ts
import { sharedFunction } from './utils';
export function functionB() { return sharedFunction(); }
export function functionB() {
return sharedFunction();
}
```
**Key Takeaway:**
- **Avoid circular imports**
- Extract shared code to separate modules
- Keep dependency graph acyclic
@@ -840,6 +921,7 @@ npm run build
```
**In browser:**
- [ ] No console errors or warnings
- [ ] Components render correctly
- [ ] No infinite loops or excessive re-renders (React DevTools)

View File

@@ -93,12 +93,14 @@ npm run coverage:view
### Pros & Cons
**Pros:**
- ✅ Native browser coverage (most accurate)
- ✅ No build instrumentation needed (faster)
- ✅ Works with source maps
- ✅ Zero performance overhead
**Cons:**
- ❌ Chromium only (V8 engine specific)
- ❌ Requires v8-to-istanbul conversion
@@ -168,11 +170,13 @@ This generates: `coverage-combined/index.html`
### Pros & Cons
**Pros:**
- ✅ Works on all browsers (Firefox, Safari, etc.)
- ✅ Industry standard tooling
- ✅ No conversion needed
**Cons:**
- ❌ Requires code instrumentation (slower builds)
- ❌ More complex setup
- ❌ Slight test performance overhead
@@ -196,11 +200,9 @@ module.exports = {
presets: ['next/babel'],
env: {
test: {
plugins: [
process.env.E2E_COVERAGE && 'istanbul'
].filter(Boolean)
}
}
plugins: [process.env.E2E_COVERAGE && 'istanbul'].filter(Boolean),
},
},
};
```
@@ -442,12 +444,14 @@ Add to your CI pipeline (e.g., `.github/workflows/test.yml`):
### Problem: No coverage files generated
**Symptoms:**
```bash
npm run coverage:convert
# ❌ No V8 coverage found at: coverage-e2e/raw
```
**Solutions:**
1. Verify `E2E_COVERAGE=true` is set when running tests
2. Check coverage helpers are imported: `import { withCoverage } from './helpers/coverage'`
3. Verify `beforeEach` and `afterEach` hooks are added
@@ -456,12 +460,14 @@ npm run coverage:convert
### Problem: V8 conversion fails
**Symptoms:**
```bash
npm run coverage:convert
# ❌ v8-to-istanbul not installed
```
**Solution:**
```bash
npm install -D v8-to-istanbul
```
@@ -469,6 +475,7 @@ npm install -D v8-to-istanbul
### Problem: Coverage lower than expected
**Symptoms:**
```
Combined: 85% (expected 99%)
```
@@ -490,12 +497,14 @@ Combined: 85% (expected 99%)
### Problem: Istanbul coverage is empty
**Symptoms:**
```typescript
await saveIstanbulCoverage(page, testName);
// ⚠️ No Istanbul coverage found
```
**Solutions:**
1. Verify `babel-plugin-istanbul` is configured
2. Check `window.__coverage__` exists:
```typescript
@@ -507,12 +516,14 @@ await saveIstanbulCoverage(page, testName);
### Problem: Merge script fails
**Symptoms:**
```bash
npm run coverage:merge
# ❌ Error: Cannot find module 'istanbul-lib-coverage'
```
**Solution:**
```bash
npm install -D istanbul-lib-coverage istanbul-lib-report istanbul-reports
```
@@ -524,11 +535,13 @@ npm install -D istanbul-lib-coverage istanbul-lib-report istanbul-reports
### Q: Should I use V8 or Istanbul coverage?
**A: V8 coverage (Approach 1)** if:
- ✅ You only test in Chromium
- ✅ You want zero instrumentation overhead
- ✅ You want the most accurate coverage
**Istanbul (Approach 2)** if:
- ✅ You need cross-browser coverage
- ✅ You already use Istanbul tooling
- ✅ You need complex coverage transformations
@@ -545,6 +558,7 @@ npm install -D istanbul-lib-coverage istanbul-lib-report istanbul-reports
### Q: Can I run coverage only for specific tests?
**Yes:**
```bash
# Single file
E2E_COVERAGE=true npx playwright test homepage.spec.ts
@@ -559,10 +573,7 @@ Edit `.nycrc.json` and add to `exclude` array:
```json
{
"exclude": [
"src/app/dev/**",
"src/lib/utils/debug.ts"
]
"exclude": ["src/app/dev/**", "src/lib/utils/debug.ts"]
}
```
@@ -571,6 +582,7 @@ Edit `.nycrc.json` and add to `exclude` array:
Not directly in the HTML report, but you can:
1. Generate separate reports:
```bash
npx nyc report --reporter=html --report-dir=coverage-unit --temp-dir=coverage/.nyc_output
npx nyc report --reporter=html --report-dir=coverage-e2e-only --temp-dir=coverage-e2e/.nyc_output
@@ -581,6 +593,7 @@ Not directly in the HTML report, but you can:
### Q: What's the performance impact on CI?
Typical impact:
- V8 coverage: +2-3 minutes (conversion time)
- Istanbul coverage: +5-7 minutes (build instrumentation)
- Merge step: ~10 seconds
@@ -594,6 +607,7 @@ Total CI time increase: **3-8 minutes**
### After Phase 1 (Infrastructure - DONE ✅)
You've completed:
- ✅ `.nycrc.json` configuration
- ✅ Merge script (`scripts/merge-coverage.ts`)
- ✅ Conversion script (`scripts/convert-v8-to-istanbul.ts`)
@@ -603,6 +617,7 @@ You've completed:
### Phase 2: Activation (When Ready)
1. **Install dependencies:**
```bash
npm install -D v8-to-istanbul istanbul-lib-coverage istanbul-lib-report istanbul-reports
```

View File

@@ -6,20 +6,24 @@
## Bottleneck Analysis
### 1. Authentication Overhead (HIGHEST IMPACT)
**Problem**: Each test logs in fresh via UI
- **Impact**: 5-7s per test × 133 admin tests = ~700s wasted
- **Root Cause**: Using `loginViaUI(page)` in every `beforeEach`
**Example of current slow pattern:**
```typescript
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page); // ← 5-7s UI login EVERY test
await loginViaUI(page); // ← 5-7s UI login EVERY test
await page.goto('/admin');
});
```
**Solution: Playwright Storage State** (SAVE ~600-700s)
```typescript
// auth.setup.ts - Run ONCE per worker
import { test as setup } from '@playwright/test';
@@ -62,7 +66,7 @@ export default defineConfig({
// admin-users.spec.ts - NO MORE loginViaUI!
test.beforeEach(async ({ page }) => {
// Auth already loaded from storageState
await page.goto('/admin/users'); // ← Direct navigation, ~1-2s
await page.goto('/admin/users'); // ← Direct navigation, ~1-2s
});
```
@@ -71,28 +75,32 @@ test.beforeEach(async ({ page }) => {
---
### 2. Redundant Navigation Tests (MEDIUM IMPACT)
**Problem**: Separate tests for "navigate to X" and "display X page"
- **Impact**: 3-5s per redundant test × ~15 tests = ~60s wasted
**Current slow pattern:**
```typescript
test('should navigate to users page', async ({ page }) => {
await page.goto('/admin/users'); // 3s
await page.goto('/admin/users'); // 3s
await expect(page).toHaveURL('/admin/users');
await expect(page.locator('h1')).toContainText('User Management');
});
test('should display user management page', async ({ page }) => {
await page.goto('/admin/users'); // 3s DUPLICATE
await page.goto('/admin/users'); // 3s DUPLICATE
await expect(page.locator('h1')).toContainText('User Management');
await expect(page.getByText(/manage users/i)).toBeVisible();
});
```
**Optimized pattern:**
```typescript
test('should navigate to users page and display content', async ({ page }) => {
await page.goto('/admin/users'); // 3s ONCE
await page.goto('/admin/users'); // 3s ONCE
// Navigation assertions
await expect(page).toHaveURL('/admin/users');
@@ -109,18 +117,22 @@ test('should navigate to users page and display content', async ({ page }) => {
---
### 3. Flaky Test Fix (CRITICAL)
**Problem**: Test #218 failed once, passed on retry
```
Test: settings-password.spec.ts:24:7 Password Change should display password change form
Failed: 12.8s → Retry passed: 8.3s
```
**Root Cause Options**:
1. Race condition in form rendering
2. Slow network request not properly awaited
3. Animation/transition timing issue
**Investigation needed:**
```typescript
// Current test (lines 24-35)
test('should display password change form', async ({ page }) => {
@@ -134,11 +146,12 @@ test('should display password change form', async ({ page }) => {
```
**Temporary Solution: Skip until fixed**
```typescript
test.skip('should display password change form', async ({ page }) => {
// TODO: Fix race condition (issue #XXX)
await page.goto('/settings/password');
await page.waitForLoadState('networkidle'); // ← Add this
await page.waitForLoadState('networkidle'); // ← Add this
await expect(page.getByLabel(/current password/i)).toBeVisible();
});
```
@@ -148,23 +161,27 @@ test.skip('should display password change form', async ({ page }) => {
---
### 4. Optimize Wait Timeouts (LOW IMPACT)
**Problem**: Default timeout is 10s for all assertions
- **Impact**: Tests wait unnecessarily when elements load faster
**Current global timeout:**
```typescript
// playwright.config.ts
export default defineConfig({
timeout: 30000, // Per test
expect: { timeout: 10000 }, // Per assertion
timeout: 30000, // Per test
expect: { timeout: 10000 }, // Per assertion
});
```
**Optimized for fast-loading pages:**
```typescript
export default defineConfig({
timeout: 20000, // Reduce from 30s
expect: { timeout: 5000 }, // Reduce from 10s (most elements load <2s)
timeout: 20000, // Reduce from 30s
expect: { timeout: 5000 }, // Reduce from 10s (most elements load <2s)
});
```
@@ -175,6 +192,7 @@ export default defineConfig({
## Implementation Priority
### Phase 1: Quick Wins (1-2 hours work)
1.**Skip flaky test #218** temporarily
2.**Reduce timeout defaults** (5s for expects, 20s for tests)
3.**Combine 5 most obvious redundant navigation tests**
@@ -184,6 +202,7 @@ export default defineConfig({
---
### Phase 2: Auth State Caching (2-4 hours work)
1. ✅ Create `e2e/auth.setup.ts` with storage state setup
2. ✅ Update `playwright.config.ts` with projects + dependencies
3. ✅ Remove `loginViaUI` from all admin test `beforeEach` hooks
@@ -194,6 +213,7 @@ export default defineConfig({
---
### Phase 3: Deep Optimization (4-8 hours work)
1. ✅ Investigate and fix flaky test root cause
2. ✅ Audit all navigation tests for redundancy
3. ✅ Combine related assertions (e.g., all stat cards in one test)
@@ -205,12 +225,12 @@ export default defineConfig({
## Total Expected Improvement
| Phase | Time Investment | Time Saved | % Improvement |
|-------|----------------|------------|---------------|
| Phase 1 | 1-2 hours | ~150s | 7% |
| Phase 2 | 2-4 hours | ~700s | 35% |
| Phase 3 | 4-8 hours | ~200s | 10% |
| **Total** | **7-14 hours** | **~1050s** | **50-60%** |
| Phase | Time Investment | Time Saved | % Improvement |
| --------- | --------------- | ---------- | ------------- |
| Phase 1 | 1-2 hours | ~150s | 7% |
| Phase 2 | 2-4 hours | ~700s | 35% |
| Phase 3 | 4-8 hours | ~200s | 10% |
| **Total** | **7-14 hours** | **~1050s** | **50-60%** |
**Final target**: 2100s → 1050s = **~17-18 minutes** (currently ~35 minutes)
@@ -219,6 +239,7 @@ export default defineConfig({
## Risks and Considerations
### Storage State Caching Risks:
1. **Test isolation**: Shared auth state could cause cross-test pollution
- **Mitigation**: Use separate storage files per role, clear cookies between tests
2. **Stale auth tokens**: Mock tokens might expire
@@ -227,6 +248,7 @@ export default defineConfig({
- **Mitigation**: Keep `loginViaUI` tests for auth flow verification
### Recommended Safeguards:
```typescript
// Clear non-auth state between tests
test.beforeEach(async ({ page }) => {
@@ -249,15 +271,18 @@ test.beforeEach(async ({ page }) => {
## Next Steps
**Immediate Actions (Do Now):**
1. Skip flaky test #218 with TODO comment
2. Reduce timeout defaults in playwright.config.ts
3. Create this optimization plan issue/ticket
**Short-term (This Week):**
1. Implement auth storage state (Phase 2)
2. Combine obvious redundant tests (Phase 1)
**Medium-term (Next Sprint):**
1. Investigate flaky test root cause
2. Audit all tests for redundancy
3. Measure and report improvements
@@ -267,18 +292,21 @@ test.beforeEach(async ({ page }) => {
## Metrics to Track
Before optimization:
- Total time: ~2100s (35 minutes)
- Avg test time: 9.1s
- Slowest test: 20.1s (settings navigation)
- Flaky tests: 1
After Phase 1+2 target:
- Total time: <1200s (20 minutes) ✅
- Avg test time: <5.5s ✅
- Slowest test: <12s ✅
- Flaky tests: 0 ✅
After Phase 3 target:
- Total time: <1050s (17 minutes) 🎯
- Avg test time: <4.8s 🎯
- Slowest test: <10s 🎯

View File

@@ -69,8 +69,7 @@ export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserDto) =>
AdminService.createUser({ requestBody: data }),
mutationFn: (data: CreateUserDto) => AdminService.createUser({ requestBody: data }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success('User created successfully');
@@ -106,8 +105,7 @@ export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: string) =>
AdminService.deleteUser({ userId }),
mutationFn: (userId: string) => AdminService.deleteUser({ userId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success('User deleted successfully');
@@ -145,7 +143,7 @@ export function useBulkUserAction() {
return useMutation({
mutationFn: ({ action, userIds }: { action: string; userIds: string[] }) =>
AdminService.bulkUserAction({
requestBody: { action, user_ids: userIds }
requestBody: { action, user_ids: userIds },
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
@@ -592,6 +590,7 @@ export default function NewUserPage() {
### Testing the Feature
**Test the user management flow:**
1. Navigate to `/admin/users`
2. Search for users
3. Click "Create User" and fill the form
@@ -630,8 +629,7 @@ export function useRevokeSession() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (sessionId: string) =>
SessionsService.revokeSession({ sessionId }),
mutationFn: (sessionId: string) => SessionsService.revokeSession({ sessionId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sessions'] });
toast.success('Session revoked successfully');
@@ -1041,11 +1039,13 @@ export default function AdminDashboardPage() {
## Conclusion
These examples demonstrate:
1. **Complete CRUD operations** (User Management)
2. **Real-time data with polling** (Session Management)
3. **Data visualization** (Admin Dashboard Charts)
Each example follows the established patterns:
- API hooks for data fetching
- Reusable components
- Proper error handling
@@ -1053,6 +1053,7 @@ Each example follows the established patterns:
- Type safety with TypeScript
For more patterns and best practices, refer to:
- **ARCHITECTURE.md**: System design
- **CODING_STANDARDS.md**: Code style
- **COMPONENT_GUIDE.md**: Component usage

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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/)

View File

@@ -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

View File

@@ -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

View File

@@ -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/`

View File

@@ -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