Files
2025-11-30 08:25:20 +08:00

654 lines
15 KiB
Markdown

# UI Components: [Project Name]
**Framework**: React 19
**Component Library**: shadcn/ui (Radix UI primitives)
**Styling**: Tailwind v4
**Forms**: React Hook Form + Zod
**State**: TanStack Query (server) + Zustand (client)
**Last Updated**: [Date]
---
## Overview
This document outlines the component hierarchy, reusable components, forms, and UI patterns.
**Component Philosophy**:
- **Composition over configuration** - Build complex UIs from simple parts
- **Accessibility first** - All components keyboard navigable, ARIA compliant
- **shadcn/ui ownership** - Components copied to codebase, not npm dependency
- **Tailwind utility classes** - Minimal custom CSS
- **Dark mode support** - All components adapt to theme
---
## Component Hierarchy
```
App
├── Providers
│ ├── ClerkProvider (auth)
│ ├── ThemeProvider (dark/light mode)
│ └── QueryClientProvider (TanStack Query)
├── Layout
│ ├── Header
│ │ ├── Logo
│ │ ├── Navigation
│ │ └── UserMenu
│ ├── Main (page content)
│ └── Footer (optional)
└── Pages (routes)
├── HomePage
│ ├── HeroSection
│ ├── FeaturesSection
│ └── CTASection
├── DashboardPage
│ ├── Sidebar
│ │ └── NavLinks
│ └── DashboardContent
│ ├── StatsCards
│ └── [ResourceList]
├── [Resource]Page
│ ├── [Resource]List
│ │ ├── [Resource]Card (for each item)
│ │ └── Pagination
│ └── [Resource]CreateDialog
└── [Resource]DetailPage
├── [Resource]Header
├── [Resource]Info
└── [Resource]Actions
```
---
## Core Layout Components
### `App.tsx`
**Purpose**: Root component with providers
**Structure**:
```tsx
import { ClerkProvider } from '@clerk/clerk-react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ThemeProvider } from '@/components/theme-provider'
import { Router } from '@/router'
const queryClient = new QueryClient()
export function App() {
return (
<ClerkProvider publishableKey={...}>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="app-theme">
<Router />
</ThemeProvider>
</QueryClientProvider>
</ClerkProvider>
)
}
```
---
### `Layout.tsx`
**Purpose**: Common layout wrapper for authenticated pages
**Props**: None (uses `Outlet` from react-router)
**Structure**:
```tsx
export function Layout() {
return (
<div className="min-h-screen bg-background">
<Header />
<main className="container mx-auto px-4 py-8">
<Outlet />
</main>
<Footer />
</div>
)
}
```
**Files**: `src/components/layout/Layout.tsx`, `Header.tsx`, `Footer.tsx`
---
### `Header.tsx`
**Purpose**: Top navigation bar
**Features**:
- Logo/brand
- Navigation links
- User menu (authenticated) or Sign In button (unauthenticated)
- Theme toggle (dark/light mode)
**Structure**:
```tsx
export function Header() {
return (
<header className="border-b bg-background">
<div className="container mx-auto flex h-16 items-center justify-between px-4">
<div className="flex items-center gap-6">
<Logo />
<Navigation />
</div>
<div className="flex items-center gap-4">
<ThemeToggle />
<UserButton />
</div>
</div>
</header>
)
}
```
**Files**: `src/components/layout/Header.tsx`
---
## shadcn/ui Components Used
### Installed Components
Run these to add components:
```bash
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add dialog
pnpm dlx shadcn@latest add dropdown-menu
pnpm dlx shadcn@latest add form
pnpm dlx shadcn@latest add input
pnpm dlx shadcn@latest add label
pnpm dlx shadcn@latest add select
pnpm dlx shadcn@latest add textarea
pnpm dlx shadcn@latest add card
pnpm dlx shadcn@latest add badge
pnpm dlx shadcn@latest add avatar
pnpm dlx shadcn@latest add skeleton
pnpm dlx shadcn@latest add toast
```
**Location**: `src/components/ui/`
**Ownership**: These are now part of our codebase, modify as needed
---
## Custom Components
### `[Resource]List.tsx`
**Purpose**: Display list of [resources] with loading/error states
**Props**:
```typescript
interface [Resource]ListProps {
// No props - uses TanStack Query internally
}
```
**Structure**:
```tsx
export function [Resource]List() {
const { data, isLoading, error } = useQuery({
queryKey: ['[resources]'],
queryFn: fetch[Resources]
})
if (isLoading) return <[Resource]ListSkeleton />
if (error) return <ErrorMessage error={error} />
return (
<div className="space-y-4">
{data.map(item => (
<[Resource]Card key={item.id} item={item} />
))}
</div>
)
}
```
**Files**: `src/components/[resource]/[Resource]List.tsx`
---
### `[Resource]Card.tsx`
**Purpose**: Display single [resource] item
**Props**:
```typescript
interface [Resource]CardProps {
item: [Resource]
onEdit?: (id: number) => void
onDelete?: (id: number) => void
}
```
**Structure**:
```tsx
export function [Resource]Card({ item, onEdit, onDelete }: [Resource]CardProps) {
return (
<Card>
<CardHeader>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{item.description}</p>
</CardContent>
<CardFooter>
<Button onClick={() => onEdit?.(item.id)}>Edit</Button>
<Button variant="destructive" onClick={() => onDelete?.(item.id)}>Delete</Button>
</CardFooter>
</Card>
)
}
```
**Files**: `src/components/[resource]/[Resource]Card.tsx`
---
### `[Resource]Form.tsx`
**Purpose**: Create or edit [resource]
**Props**:
```typescript
interface [Resource]FormProps {
item?: [Resource] // undefined for create, populated for edit
onSuccess?: () => void
}
```
**Structure** (using React Hook Form + Zod):
```tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { [resource]Schema } from '@/lib/schemas'
export function [Resource]Form({ item, onSuccess }: [Resource]FormProps) {
const form = useForm({
resolver: zodResolver([resource]Schema),
defaultValues: item || {
field1: '',
field2: ''
}
})
const mutation = useMutation({
mutationFn: item ? update[Resource] : create[Resource],
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['[resources]'] })
onSuccess?.()
}
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(data => mutation.mutate(data))}>
<FormField name="field1" control={form.control} render={({ field }) => (
<FormItem>
<FormLabel>Field 1</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)} />
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : 'Save'}
</Button>
</form>
</Form>
)
}
```
**Files**: `src/components/[resource]/[Resource]Form.tsx`
---
### `[Resource]CreateDialog.tsx`
**Purpose**: Modal dialog for creating new [resource]
**Structure**:
```tsx
export function [Resource]CreateDialog() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Create [Resource]</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New [Resource]</DialogTitle>
</DialogHeader>
<[Resource]Form onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
)
}
```
**Files**: `src/components/[resource]/[Resource]CreateDialog.tsx`
---
## Loading and Error States
### `[Resource]ListSkeleton.tsx`
**Purpose**: Loading state for [resource] list
**Structure**:
```tsx
export function [Resource]ListSkeleton() {
return (
<div className="space-y-4">
{[1, 2, 3].map(i => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-6 w-48" />
</CardHeader>
<CardContent>
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4 mt-2" />
</CardContent>
</Card>
))}
</div>
)
}
```
---
### `ErrorMessage.tsx`
**Purpose**: Reusable error display
**Props**:
```typescript
interface ErrorMessageProps {
error: Error
retry?: () => void
}
```
**Structure**:
```tsx
export function ErrorMessage({ error, retry }: ErrorMessageProps) {
return (
<div className="rounded-lg border border-destructive bg-destructive/10 p-4">
<h3 className="font-semibold text-destructive">Error</h3>
<p className="text-sm text-muted-foreground">{error.message}</p>
{retry && (
<Button variant="outline" onClick={retry} className="mt-2">
Retry
</Button>
)}
</div>
)
}
```
---
## State Management Patterns
### Server State (TanStack Query)
**Use for**: Data from API (users, tasks, analytics, etc)
**Example**:
```tsx
// In component
const { data, isLoading, error } = useQuery({
queryKey: ['tasks'],
queryFn: () => api.get('/api/tasks').then(res => res.data)
})
// Mutation
const mutation = useMutation({
mutationFn: (newTask) => api.post('/api/tasks', newTask),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
}
})
```
**Files**: Queries/mutations defined in `src/lib/api.ts` or component files
---
### Client State (Zustand)
**Use for**: UI state, preferences, filters
**Example Store**:
```tsx
// src/stores/ui-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface UIStore {
sidebarOpen: boolean
setSidebarOpen: (open: boolean) => void
theme: 'light' | 'dark' | 'system'
setTheme: (theme: 'light' | 'dark' | 'system') => void
}
export const useUIStore = create<UIStore>()(
persist(
(set) => ({
sidebarOpen: true,
setSidebarOpen: (open) => set({ sidebarOpen: open }),
theme: 'system',
setTheme: (theme) => set({ theme })
}),
{ name: 'ui-store' }
)
)
```
**Usage**:
```tsx
const { sidebarOpen, setSidebarOpen } = useUIStore()
```
---
## Theme System
### ThemeProvider
**Purpose**: Manage dark/light/system theme
**Structure**:
```tsx
// src/components/theme-provider.tsx
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props}
>
{children}
</NextThemesProvider>
)
}
export function useTheme() {
return useNextTheme()
}
```
**Files**: `src/components/theme-provider.tsx`
---
### ThemeToggle
**Purpose**: Toggle between light/dark/system themes
**Structure**:
```tsx
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
```
---
## Form Patterns
All forms use **React Hook Form + Zod** for validation.
**Schema Definition** (shared client + server):
```typescript
// src/lib/schemas.ts
import { z } from 'zod'
export const taskSchema = z.object({
title: z.string().min(1, 'Title required').max(100),
description: z.string().optional(),
dueDate: z.string().optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium')
})
export type TaskFormData = z.infer<typeof taskSchema>
```
**Form Component**:
```tsx
const form = useForm<TaskFormData>({
resolver: zodResolver(taskSchema),
defaultValues: { ... }
})
```
---
## Responsive Design
**Breakpoints** (Tailwind defaults):
- `sm`: 640px
- `md`: 768px
- `lg`: 1024px
- `xl`: 1280px
- `2xl`: 1536px
**Patterns**:
```tsx
// Stack on mobile, grid on desktop
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
// Hide on mobile, show on desktop
<div className="hidden md:block">
// Different padding on mobile vs desktop
<div className="px-4 md:px-8">
```
---
## Accessibility
**Requirements**:
- All interactive elements keyboard navigable
- Focus states visible (use `ring` classes)
- Images have alt text
- Forms have associated labels
- Color contrast meets WCAG AA
- Screen reader friendly (ARIA labels)
**shadcn/ui benefits**: All components built with Radix UI (accessible by default)
---
## File Structure
```
src/
├── components/
│ ├── ui/ # shadcn/ui components
│ │ ├── button.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ └── ...
│ ├── layout/ # Layout components
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ └── Layout.tsx
│ ├── [resource]/ # Feature-specific components
│ │ ├── [Resource]List.tsx
│ │ ├── [Resource]Card.tsx
│ │ ├── [Resource]Form.tsx
│ │ └── [Resource]CreateDialog.tsx
│ ├── theme-provider.tsx
│ └── error-message.tsx
├── lib/
│ ├── schemas.ts # Zod schemas
│ ├── api.ts # API client
│ └── utils.ts # cn() helper
├── stores/
│ └── ui-store.ts # Zustand stores
└── pages/
├── HomePage.tsx
├── DashboardPage.tsx
└── ...
```
---
## Design Tokens
See `src/index.css` for theme configuration.
**Colors** (semantic):
- `background` / `foreground`
- `primary` / `primary-foreground`
- `secondary` / `secondary-foreground`
- `destructive` / `destructive-foreground`
- `muted` / `muted-foreground`
- `accent` / `accent-foreground`
- `border`
- `ring`
**Usage**: These adapt to light/dark mode automatically.
---
## Future Components
Components to build:
- [ ] `[Component]` - [Description]
- [ ] `[Component]` - [Description]
---
## Revision History
**v1.0** ([Date]): Initial component structure
**v1.1** ([Date]): [Changes made]