Initial commit
This commit is contained in:
653
templates/UI_COMPONENTS.md
Normal file
653
templates/UI_COMPONENTS.md
Normal file
@@ -0,0 +1,653 @@
|
||||
# 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]
|
||||
Reference in New Issue
Block a user