style: format all files with prettier

This commit is contained in:
Seth Hobson
2026-01-19 17:07:03 -05:00
parent 8d37048deb
commit 56848874a2
355 changed files with 15215 additions and 10241 deletions

View File

@@ -20,13 +20,13 @@ Comprehensive patterns for Next.js 14+ App Router architecture, Server Component
### 1. Rendering Modes
| Mode | Where | When to Use |
|------|-------|-------------|
| **Server Components** | Server only | Data fetching, heavy computation, secrets |
| **Client Components** | Browser | Interactivity, hooks, browser APIs |
| **Static** | Build time | Content that rarely changes |
| **Dynamic** | Request time | Personalized or real-time data |
| **Streaming** | Progressive | Large pages, slow data sources |
| Mode | Where | When to Use |
| --------------------- | ------------ | ----------------------------------------- |
| **Server Components** | Server only | Data fetching, heavy computation, secrets |
| **Client Components** | Browser | Interactivity, hooks, browser APIs |
| **Static** | Build time | Content that rarely changes |
| **Dynamic** | Request time | Personalized or real-time data |
| **Streaming** | Progressive | Large pages, slow data sources |
### 2. File Conventions
@@ -199,18 +199,18 @@ export function AddToCartButton({ productId }: { productId: string }) {
```typescript
// app/actions/cart.ts
'use server'
"use server";
import { revalidateTag } from 'next/cache'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { revalidateTag } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export async function addToCart(productId: string) {
const cookieStore = await cookies()
const sessionId = cookieStore.get('session')?.value
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
if (!sessionId) {
redirect('/login')
redirect("/login");
}
try {
@@ -218,29 +218,29 @@ export async function addToCart(productId: string) {
where: { sessionId_productId: { sessionId, productId } },
update: { quantity: { increment: 1 } },
create: { sessionId, productId, quantity: 1 },
})
});
revalidateTag('cart')
return { success: true }
revalidateTag("cart");
return { success: true };
} catch (error) {
return { error: 'Failed to add item to cart' }
return { error: "Failed to add item to cart" };
}
}
export async function checkout(formData: FormData) {
const address = formData.get('address') as string
const payment = formData.get('payment') as string
const address = formData.get("address") as string;
const payment = formData.get("payment") as string;
// Validate
if (!address || !payment) {
return { error: 'Missing required fields' }
return { error: "Missing required fields" };
}
// Process order
const order = await processOrder({ address, payment })
const order = await processOrder({ address, payment });
// Redirect to confirmation
redirect(`/orders/${order.id}/confirmation`)
redirect(`/orders/${order.id}/confirmation`);
}
```
@@ -401,46 +401,43 @@ async function Recommendations({ productId }: { productId: string }) {
```typescript
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const category = searchParams.get('category')
const searchParams = request.nextUrl.searchParams;
const category = searchParams.get("category");
const products = await db.product.findMany({
where: category ? { category } : undefined,
take: 20,
})
});
return NextResponse.json(products)
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const body = await request.json()
const body = await request.json();
const product = await db.product.create({
data: body,
})
});
return NextResponse.json(product, { status: 201 })
return NextResponse.json(product, { status: 201 });
}
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
const product = await db.product.findUnique({ where: { id } })
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
return NextResponse.json({ error: "Product not found" }, { status: 404 });
}
return NextResponse.json(product)
return NextResponse.json(product);
}
```
@@ -499,31 +496,32 @@ export default async function ProductPage({ params }: Props) {
```typescript
// No cache (always fresh)
fetch(url, { cache: 'no-store' })
fetch(url, { cache: "no-store" });
// Cache forever (static)
fetch(url, { cache: 'force-cache' })
fetch(url, { cache: "force-cache" });
// ISR - revalidate after 60 seconds
fetch(url, { next: { revalidate: 60 } })
fetch(url, { next: { revalidate: 60 } });
// Tag-based invalidation
fetch(url, { next: { tags: ['products'] } })
fetch(url, { next: { tags: ["products"] } });
// Invalidate via Server Action
'use server'
import { revalidateTag, revalidatePath } from 'next/cache'
("use server");
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({ where: { id }, data })
revalidateTag('products')
revalidatePath('/products')
await db.product.update({ where: { id }, data });
revalidateTag("products");
revalidatePath("/products");
}
```
## Best Practices
### Do's
- **Start with Server Components** - Add 'use client' only when needed
- **Colocate data fetching** - Fetch data where it's used
- **Use Suspense boundaries** - Enable streaming for slow data
@@ -531,6 +529,7 @@ export async function updateProduct(id: string, data: ProductData) {
- **Use Server Actions** - For mutations with progressive enhancement
### Don'ts
- **Don't pass serializable data** - Server → Client boundary limitations
- **Don't use hooks in Server Components** - No useState, useEffect
- **Don't fetch in Client Components** - Use Server Components or React Query

View File

@@ -38,13 +38,13 @@ src/
### 2. Expo vs Bare React Native
| Feature | Expo | Bare RN |
|---------|------|---------|
| Setup complexity | Low | High |
| Native modules | EAS Build | Manual linking |
| OTA updates | Built-in | Manual setup |
| Build service | EAS | Custom CI |
| Custom native code | Config plugins | Direct access |
| Feature | Expo | Bare RN |
| ------------------ | -------------- | -------------- |
| Setup complexity | Low | High |
| Native modules | EAS Build | Manual linking |
| OTA updates | Built-in | Manual setup |
| Build service | EAS | Custom CI |
| Custom native code | Config plugins | Direct access |
## Quick Start
@@ -332,60 +332,60 @@ export function useCreateProduct() {
```typescript
// services/haptics.ts
import * as Haptics from 'expo-haptics'
import { Platform } from 'react-native'
import * as Haptics from "expo-haptics";
import { Platform } from "react-native";
export const haptics = {
light: () => {
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
if (Platform.OS !== "web") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
},
medium: () => {
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
if (Platform.OS !== "web") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
},
heavy: () => {
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
if (Platform.OS !== "web") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
}
},
success: () => {
if (Platform.OS !== 'web') {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
if (Platform.OS !== "web") {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
},
error: () => {
if (Platform.OS !== 'web') {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
if (Platform.OS !== "web") {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
},
}
};
// services/biometrics.ts
import * as LocalAuthentication from 'expo-local-authentication'
import * as LocalAuthentication from "expo-local-authentication";
export async function authenticateWithBiometrics(): Promise<boolean> {
const hasHardware = await LocalAuthentication.hasHardwareAsync()
if (!hasHardware) return false
const hasHardware = await LocalAuthentication.hasHardwareAsync();
if (!hasHardware) return false;
const isEnrolled = await LocalAuthentication.isEnrolledAsync()
if (!isEnrolled) return false
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) return false;
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to continue',
fallbackLabel: 'Use passcode',
promptMessage: "Authenticate to continue",
fallbackLabel: "Use passcode",
disableDeviceFallback: false,
})
});
return result.success
return result.success;
}
// services/notifications.ts
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import Constants from 'expo-constants'
import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
import Constants from "expo-constants";
Notifications.setNotificationHandler({
handleNotification: async () => ({
@@ -393,35 +393,35 @@ Notifications.setNotificationHandler({
shouldPlaySound: true,
shouldSetBadge: true,
}),
})
});
export async function registerForPushNotifications() {
let token: string | undefined
let token: string | undefined;
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
})
});
}
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return null
if (finalStatus !== "granted") {
return null;
}
const projectId = Constants.expoConfig?.extra?.eas?.projectId
token = (await Notifications.getExpoPushTokenAsync({ projectId })).data
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
return token
return token;
}
```
@@ -650,6 +650,7 @@ eas update --branch production --message "Bug fixes"
## Best Practices
### Do's
- **Use Expo** - Faster development, OTA updates, managed native code
- **FlashList over FlatList** - Better performance for long lists
- **Memoize components** - Prevent unnecessary re-renders
@@ -657,6 +658,7 @@ eas update --branch production --message "Bug fixes"
- **Test on real devices** - Simulators miss real-world issues
### Don'ts
- **Don't inline styles** - Use StyleSheet.create for performance
- **Don't fetch in render** - Use useEffect or React Query
- **Don't ignore platform differences** - Test on both iOS and Android

View File

@@ -20,13 +20,13 @@ Comprehensive guide to modern React state management patterns, from local compon
### 1. State Categories
| Type | Description | Solutions |
|------|-------------|-----------|
| **Local State** | Component-specific, UI state | useState, useReducer |
| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai |
| **Server State** | Remote data, caching | React Query, SWR, RTK Query |
| **URL State** | Route parameters, search | React Router, nuqs |
| **Form State** | Input values, validation | React Hook Form, Formik |
| Type | Description | Solutions |
| ---------------- | ---------------------------- | ----------------------------- |
| **Local State** | Component-specific, UI state | useState, useReducer |
| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai |
| **Server State** | Remote data, caching | React Query, SWR, RTK Query |
| **URL State** | Route parameters, search | React Router, nuqs |
| **Form State** | Input values, validation | React Hook Form, Formik |
### 2. Selection Criteria
@@ -87,10 +87,10 @@ function Header() {
```typescript
// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import userReducer from './slices/userSlice'
import cartReducer from './slices/cartSlice'
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import userReducer from "./slices/userSlice";
import cartReducer from "./slices/cartSlice";
export const store = configureStore({
reducer: {
@@ -100,99 +100,99 @@ export const store = configureStore({
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST'],
ignoredActions: ["persist/PERSIST"],
},
}),
})
});
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Typed hooks
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
```typescript
// store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
interface User {
id: string
email: string
name: string
id: string;
email: string;
name: string;
}
interface UserState {
current: User | null
status: 'idle' | 'loading' | 'succeeded' | 'failed'
error: string | null
current: User | null;
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
}
const initialState: UserState = {
current: null,
status: 'idle',
status: "idle",
error: null,
}
};
export const fetchUser = createAsyncThunk(
'user/fetchUser',
"user/fetchUser",
async (userId: string, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('Failed to fetch user')
return await response.json()
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error("Failed to fetch user");
return await response.json();
} catch (error) {
return rejectWithValue((error as Error).message)
return rejectWithValue((error as Error).message);
}
}
)
},
);
const userSlice = createSlice({
name: 'user',
name: "user",
initialState,
reducers: {
setUser: (state, action: PayloadAction<User>) => {
state.current = action.payload
state.status = 'succeeded'
state.current = action.payload;
state.status = "succeeded";
},
clearUser: (state) => {
state.current = null
state.status = 'idle'
state.current = null;
state.status = "idle";
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading'
state.error = null
state.status = "loading";
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded'
state.current = action.payload
state.status = "succeeded";
state.current = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload as string
})
state.status = "failed";
state.error = action.payload as string;
});
},
})
});
export const { setUser, clearUser } = userSlice.actions
export default userSlice.reducer
export const { setUser, clearUser } = userSlice.actions;
export default userSlice.reducer;
```
### Pattern 2: Zustand with Slices (Scalable)
```typescript
// store/slices/createUserSlice.ts
import { StateCreator } from 'zustand'
import { StateCreator } from "zustand";
export interface UserSlice {
user: User | null
isAuthenticated: boolean
login: (credentials: Credentials) => Promise<void>
logout: () => void
user: User | null;
isAuthenticated: boolean;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
export const createUserSlice: StateCreator<
@@ -204,31 +204,31 @@ export const createUserSlice: StateCreator<
user: null,
isAuthenticated: false,
login: async (credentials) => {
const user = await authApi.login(credentials)
set({ user, isAuthenticated: true })
const user = await authApi.login(credentials);
set({ user, isAuthenticated: true });
},
logout: () => {
set({ user: null, isAuthenticated: false })
set({ user: null, isAuthenticated: false });
// Can access other slices
// get().clearCart()
},
})
});
// store/index.ts
import { create } from 'zustand'
import { createUserSlice, UserSlice } from './slices/createUserSlice'
import { createCartSlice, CartSlice } from './slices/createCartSlice'
import { create } from "zustand";
import { createUserSlice, UserSlice } from "./slices/createUserSlice";
import { createCartSlice, CartSlice } from "./slices/createCartSlice";
type StoreState = UserSlice & CartSlice
type StoreState = UserSlice & CartSlice;
export const useStore = create<StoreState>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}))
}));
// Selective subscriptions (prevents unnecessary re-renders)
export const useUser = () => useStore((state) => state.user)
export const useCart = () => useStore((state) => state.cart)
export const useUser = () => useStore((state) => state.user);
export const useCart = () => useStore((state) => state.cart);
```
### Pattern 3: Jotai for Atomic State
@@ -280,16 +280,16 @@ function Profile() {
```typescript
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// Query keys factory
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
all: ["users"] as const,
lists: () => [...userKeys.all, "list"] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
details: () => [...userKeys.all, "detail"] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
};
// Fetch hook
export function useUsers(filters: UserFilters) {
@@ -298,7 +298,7 @@ export function useUsers(filters: UserFilters) {
queryFn: () => fetchUsers(filters),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
})
});
}
// Single user hook
@@ -307,39 +307,45 @@ export function useUser(id: string) {
queryKey: userKeys.detail(id),
queryFn: () => fetchUser(id),
enabled: !!id, // Don't fetch if no id
})
});
}
// Mutation with optimistic update
export function useUpdateUser() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) })
await queryClient.cancelQueries({
queryKey: userKeys.detail(newUser.id),
});
// Snapshot previous value
const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id))
const previousUser = queryClient.getQueryData(
userKeys.detail(newUser.id),
);
// Optimistically update
queryClient.setQueryData(userKeys.detail(newUser.id), newUser)
queryClient.setQueryData(userKeys.detail(newUser.id), newUser);
return { previousUser }
return { previousUser };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(
userKeys.detail(newUser.id),
context?.previousUser
)
context?.previousUser,
);
},
onSettled: (data, error, variables) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) })
queryClient.invalidateQueries({
queryKey: userKeys.detail(variables.id),
});
},
})
});
}
```
@@ -378,6 +384,7 @@ function Dashboard() {
## Best Practices
### Do's
- **Colocate state** - Keep state as close to where it's used as possible
- **Use selectors** - Prevent unnecessary re-renders with selective subscriptions
- **Normalize data** - Flatten nested structures for easier updates
@@ -385,6 +392,7 @@ function Dashboard() {
- **Separate concerns** - Server state (React Query) vs client state (Zustand)
### Don'ts
- **Don't over-globalize** - Not everything needs to be in global state
- **Don't duplicate server state** - Let React Query manage it
- **Don't mutate directly** - Always use immutable updates
@@ -397,28 +405,28 @@ function Dashboard() {
```typescript
// Before (legacy Redux)
const ADD_TODO = 'ADD_TODO'
const addTodo = (text) => ({ type: ADD_TODO, payload: text })
const ADD_TODO = "ADD_TODO";
const addTodo = (text) => ({ type: ADD_TODO, payload: text });
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, { text: action.payload, completed: false }]
return [...state, { text: action.payload, completed: false }];
default:
return state
return state;
}
}
// After (Redux Toolkit)
const todosSlice = createSlice({
name: 'todos',
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
// Immer allows "mutations"
state.push({ text: action.payload, completed: false })
state.push({ text: action.payload, completed: false });
},
},
})
});
```
## Resources

View File

@@ -39,51 +39,51 @@ Base styles → Variants → Sizes → States → Overrides
```typescript
// tailwind.config.ts
import type { Config } from 'tailwindcss'
import type { Config } from "tailwindcss";
const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
darkMode: 'class',
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
darkMode: "class",
theme: {
extend: {
colors: {
// Semantic color tokens
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
border: 'hsl(var(--border))',
ring: 'hsl(var(--ring))',
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
border: "hsl(var(--border))",
ring: "hsl(var(--ring))",
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require('tailwindcss-animate')],
}
plugins: [require("tailwindcss-animate")],
};
export default config
export default config;
```
```css
@@ -625,26 +625,27 @@ export function ThemeToggle() {
```typescript
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
// Focus ring utility
export const focusRing = cn(
'focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-ring focus-visible:ring-offset-2'
)
"focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-ring focus-visible:ring-offset-2",
);
// Disabled utility
export const disabled = 'disabled:pointer-events-none disabled:opacity-50'
export const disabled = "disabled:pointer-events-none disabled:opacity-50";
```
## Best Practices
### Do's
- **Use CSS variables** - Enable runtime theming
- **Compose with CVA** - Type-safe variants
- **Use semantic colors** - `primary` not `blue-500`
@@ -652,6 +653,7 @@ export const disabled = 'disabled:pointer-events-none disabled:opacity-50'
- **Add accessibility** - ARIA attributes, focus states
### Don'ts
- **Don't use arbitrary values** - Extend theme instead
- **Don't nest @apply** - Hurts readability
- **Don't skip focus states** - Keyboard users need them