docs: initial project setup and documentation
Add comprehensive documentation and planning for Pastel UI: - Complete architecture design using Next.js 16 and Tailwind CSS 4 - Detailed development guide (CLAUDE.md) with all patterns and conventions - Feature-rich README with keyboard shortcuts and export formats - Project structure and technology stack decisions Features planned: - Color playground with multi-format support - Palette generation (harmony, distinct, gradient) - Accessibility tools (WCAG, color blindness simulation) - Named colors explorer (148 CSS/X11 colors) - Batch operations with CSV/JSON import/export - Command palette (Cmd+K) and keyboard shortcuts - Dark/light mode with system preference detection - Shareable links with URL state Tech stack: - Next.js 16 with App Router and Server Components - React 19 with latest concurrent features - Tailwind CSS 4 with CSS-first configuration - TypeScript strict mode - React Query for server state - Zustand for client state - Framer Motion for animations Ready for implementation phase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
988
CLAUDE.md
Normal file
988
CLAUDE.md
Normal file
@@ -0,0 +1,988 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
**Pastel UI** is a modern, interactive web application for color manipulation, palette generation, and accessibility analysis. Built with Next.js 16 and Tailwind CSS 4, it provides a beautiful interface for all features of the [Pastel API](https://github.com/valknarness/pastel-api).
|
||||
|
||||
**Technology Stack:**
|
||||
- **Framework**: Next.js 16 (App Router, React 19)
|
||||
- **Styling**: Tailwind CSS 4 (CSS-first configuration)
|
||||
- **Language**: TypeScript (strict mode)
|
||||
- **State Management**:
|
||||
- `@tanstack/react-query` (server state)
|
||||
- `zustand` (client state)
|
||||
- **Animation**: `framer-motion`
|
||||
- **Icons**: `lucide-react`
|
||||
- **UI Components**: Custom components + shadcn/ui patterns
|
||||
- **API Client**: Type-safe Pastel API integration
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pastel-ui/
|
||||
├── app/ # Next.js 16 App Router
|
||||
│ ├── layout.tsx # Root layout with providers
|
||||
│ ├── page.tsx # Home/main playground
|
||||
│ ├── globals.css # Tailwind CSS imports
|
||||
│ ├── playground/ # Main color manipulation tool
|
||||
│ │ └── page.tsx
|
||||
│ ├── palettes/ # Palette generation tools
|
||||
│ │ ├── page.tsx # Palette dashboard
|
||||
│ │ ├── harmony/page.tsx # Harmony-based palettes
|
||||
│ │ ├── distinct/page.tsx # Distinct colors generator
|
||||
│ │ └── gradient/page.tsx # Gradient creator
|
||||
│ ├── accessibility/ # Accessibility tools
|
||||
│ │ ├── page.tsx # A11y dashboard
|
||||
│ │ ├── contrast/page.tsx # Contrast checker
|
||||
│ │ └── colorblind/page.tsx # Colorblindness simulator
|
||||
│ ├── names/ # Named colors explorer
|
||||
│ │ └── page.tsx
|
||||
│ ├── batch/ # Batch operations
|
||||
│ │ └── page.tsx
|
||||
│ ├── docs/ # Documentation
|
||||
│ │ └── page.tsx
|
||||
│ └── api/ # API route handlers (proxy)
|
||||
│ └── proxy/[...path]/route.ts
|
||||
├── components/
|
||||
│ ├── ui/ # Base UI components (shadcn/ui style)
|
||||
│ │ ├── button.tsx
|
||||
│ │ ├── input.tsx
|
||||
│ │ ├── slider.tsx
|
||||
│ │ ├── tabs.tsx
|
||||
│ │ ├── dialog.tsx
|
||||
│ │ ├── dropdown-menu.tsx
|
||||
│ │ ├── command.tsx
|
||||
│ │ ├── toast.tsx
|
||||
│ │ └── ...
|
||||
│ ├── color/ # Color-specific components
|
||||
│ │ ├── ColorPicker.tsx # Main color input
|
||||
│ │ ├── ColorDisplay.tsx # Large preview swatch
|
||||
│ │ ├── ColorInfo.tsx # Detailed color information
|
||||
│ │ ├── FormatConverter.tsx # Format conversion tabs
|
||||
│ │ ├── ColorSwatch.tsx # Small color preview
|
||||
│ │ └── ColorInput.tsx # Text input with validation
|
||||
│ ├── tools/ # Tool-specific components
|
||||
│ │ ├── ManipulationPanel.tsx # Lighten/darken/etc controls
|
||||
│ │ ├── PaletteGenerator.tsx # Palette creation UI
|
||||
│ │ ├── AccessibilityChecker.tsx # Contrast/WCAG analysis
|
||||
│ │ ├── GradientCreator.tsx # Gradient builder
|
||||
│ │ ├── ColorBlindSimulator.tsx # Colorblindness preview
|
||||
│ │ └── DistinctGenerator.tsx # Distinct colors UI
|
||||
│ ├── layout/ # Layout components
|
||||
│ │ ├── Navbar.tsx # Top navigation
|
||||
│ │ ├── Sidebar.tsx # Side navigation
|
||||
│ │ ├── Footer.tsx # Footer
|
||||
│ │ ├── ThemeToggle.tsx # Dark/light mode
|
||||
│ │ └── CommandPalette.tsx # Cmd+K interface
|
||||
│ └── providers/ # Context providers
|
||||
│ ├── Providers.tsx # Root provider wrapper
|
||||
│ ├── ThemeProvider.tsx # Theme context
|
||||
│ └── QueryProvider.tsx # React Query provider
|
||||
├── lib/
|
||||
│ ├── api/ # API client
|
||||
│ │ ├── client.ts # PastelAPIClient class
|
||||
│ │ ├── types.ts # API type definitions
|
||||
│ │ ├── endpoints.ts # Endpoint definitions
|
||||
│ │ └── queries.ts # React Query hooks
|
||||
│ ├── utils/ # Utilities
|
||||
│ │ ├── color.ts # Color helpers
|
||||
│ │ ├── format.ts # Format converters
|
||||
│ │ ├── export.ts # Export utilities (CSS, JSON, etc.)
|
||||
│ │ ├── validation.ts # Input validation
|
||||
│ │ └── cn.ts # Class name utility
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── useColor.ts # Color state management
|
||||
│ │ ├── usePalette.ts # Palette state
|
||||
│ │ ├── useColorHistory.ts # History tracking
|
||||
│ │ ├── useKeyboard.ts # Keyboard shortcuts
|
||||
│ │ └── useClipboard.ts # Copy to clipboard
|
||||
│ ├── stores/ # Zustand stores
|
||||
│ │ ├── colorStore.ts # Color state
|
||||
│ │ ├── historyStore.ts # Color history
|
||||
│ │ └── preferencesStore.ts # User preferences
|
||||
│ └── constants/ # Constants
|
||||
│ ├── colors.ts # Named colors
|
||||
│ ├── shortcuts.ts # Keyboard shortcuts
|
||||
│ └── exports.ts # Export templates
|
||||
├── styles/
|
||||
│ └── globals.css # Global styles + Tailwind
|
||||
├── public/
|
||||
│ ├── favicon.ico
|
||||
│ ├── og-image.png
|
||||
│ └── icons/
|
||||
├── tests/
|
||||
│ ├── unit/ # Vitest unit tests
|
||||
│ │ ├── utils/
|
||||
│ │ └── components/
|
||||
│ └── e2e/ # Playwright E2E tests
|
||||
│ ├── playground.spec.ts
|
||||
│ └── palettes.spec.ts
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ └── ci.yml # CI/CD pipeline
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── tailwind.config.ts # Tailwind CSS 4 config
|
||||
├── next.config.ts # Next.js 16 config
|
||||
├── vitest.config.ts # Vitest config
|
||||
├── playwright.config.ts # Playwright config
|
||||
├── .env.example
|
||||
├── .env.local # Local environment variables
|
||||
├── .gitignore
|
||||
├── .eslintrc.json
|
||||
├── .prettierrc
|
||||
├── CLAUDE.md # This file
|
||||
└── README.md # Project overview
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Color Playground
|
||||
**Location**: `/` or `/playground`
|
||||
|
||||
Interactive color manipulation interface with:
|
||||
- Real-time color picker
|
||||
- Support for all input formats (hex, rgb, hsl, hsv, lab, oklab, lch, oklch, cmyk, gray, named)
|
||||
- Live color information display (all format representations)
|
||||
- Format conversion with copy-to-clipboard
|
||||
- Color manipulation controls (lighten, darken, saturate, desaturate, rotate, complement, grayscale)
|
||||
- Color mixing with ratio slider
|
||||
- Visual preview with large swatch
|
||||
|
||||
**Key Components**:
|
||||
- `ColorPicker` - Main input component
|
||||
- `ColorDisplay` - Large preview area
|
||||
- `ColorInfo` - Tabbed format information
|
||||
- `ManipulationPanel` - Slider controls for adjustments
|
||||
|
||||
### 2. Palette Generation
|
||||
**Location**: `/palettes/*`
|
||||
|
||||
Multiple palette generation tools:
|
||||
|
||||
**a) Harmony Palettes** (`/palettes/harmony`)
|
||||
- Monochromatic (1 base color)
|
||||
- Analogous (3 adjacent colors)
|
||||
- Complementary (2 opposite colors)
|
||||
- Split-complementary (3 colors)
|
||||
- Triadic (3 evenly spaced colors)
|
||||
- Tetradic (4 colors in rectangle)
|
||||
- Custom harmony angles
|
||||
|
||||
**b) Distinct Colors** (`/palettes/distinct`)
|
||||
- Generate N visually distinct colors
|
||||
- Configurable count (2-100)
|
||||
- Distance metric selection (CIE76, CIEDE2000)
|
||||
- Optional fixed colors to include
|
||||
- Progress indicator (can take 2-5 minutes)
|
||||
|
||||
**c) Gradient Creator** (`/palettes/gradient`)
|
||||
- Multiple color stops
|
||||
- Configurable step count
|
||||
- Color space selection (RGB, HSL, Lab, LCH, OkLab, OkLCH)
|
||||
- Live preview
|
||||
- Export as CSS gradient or color array
|
||||
|
||||
**Key Components**:
|
||||
- `PaletteGenerator` - Main palette UI
|
||||
- `GradientCreator` - Gradient builder
|
||||
- `DistinctGenerator` - Distinct colors with progress
|
||||
- `PaletteGrid` - Visual palette display
|
||||
- `ExportMenu` - Export in multiple formats
|
||||
|
||||
### 3. Accessibility Tools
|
||||
**Location**: `/accessibility/*`
|
||||
|
||||
**a) Contrast Checker** (`/accessibility/contrast`)
|
||||
- WCAG 2.1 compliance analysis
|
||||
- AA/AAA ratings for normal and large text
|
||||
- Foreground/background color inputs
|
||||
- Contrast ratio calculation
|
||||
- Recommendations for improvement
|
||||
|
||||
**b) Color Blindness Simulator** (`/accessibility/colorblind`)
|
||||
- Simulate protanopia (red-blind)
|
||||
- Simulate deuteranopia (green-blind)
|
||||
- Simulate tritanopia (blue-blind)
|
||||
- Side-by-side comparison
|
||||
- Batch simulation for palettes
|
||||
|
||||
**c) Text Color Optimizer**
|
||||
- Find optimal text color for background
|
||||
- WCAG compliance guarantee
|
||||
- Light/dark text selection
|
||||
|
||||
**Key Components**:
|
||||
- `AccessibilityChecker` - Main A11y interface
|
||||
- `ContrastAnalyzer` - WCAG contrast checker
|
||||
- `ColorBlindSimulator` - Simulation previews
|
||||
- `ComplianceBadge` - AA/AAA status indicators
|
||||
|
||||
### 4. Named Colors Explorer
|
||||
**Location**: `/names`
|
||||
|
||||
Browse and search 148 CSS/X11 named colors:
|
||||
- Searchable grid of color swatches
|
||||
- Filter by name, hex value
|
||||
- Sort by hue, brightness, saturation, name
|
||||
- Click to use in playground
|
||||
- Find nearest named color for any input
|
||||
|
||||
**Key Components**:
|
||||
- `NamedColorsGrid` - Visual grid display
|
||||
- `ColorSearch` - Search and filter
|
||||
- `NearestColorFinder` - Find closest named color
|
||||
|
||||
### 5. Batch Operations
|
||||
**Location**: `/batch`
|
||||
|
||||
Process multiple colors at once:
|
||||
- Upload CSV/JSON files with color lists
|
||||
- Apply operations to all colors (lighten, darken, etc.)
|
||||
- Bulk format conversion
|
||||
- Download results in multiple formats
|
||||
- Visual preview of batch results
|
||||
|
||||
**Key Components**:
|
||||
- `BatchUploader` - File upload interface
|
||||
- `BatchOperations` - Operation selection
|
||||
- `BatchPreview` - Results preview
|
||||
- `BatchExporter` - Download results
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Server vs Client Components
|
||||
|
||||
**Server Components** (default):
|
||||
- Page layouts
|
||||
- Static content
|
||||
- Data fetching (when possible)
|
||||
- SEO-critical content
|
||||
|
||||
**Client Components** (use 'use client'):
|
||||
- Interactive UI (color pickers, sliders)
|
||||
- State management components
|
||||
- Animation/transition components
|
||||
- Browser API usage (localStorage, clipboard)
|
||||
|
||||
### State Management Strategy
|
||||
|
||||
**Server State** (React Query):
|
||||
- API responses from Pastel API
|
||||
- Cached color operations
|
||||
- Automatic refetching and caching
|
||||
|
||||
**Client State** (Zustand):
|
||||
- Current selected colors
|
||||
- Color history (session)
|
||||
- User preferences (theme, shortcuts)
|
||||
- UI state (sidebar open/closed)
|
||||
|
||||
**Persistent State** (LocalStorage/IndexedDB):
|
||||
- Saved palettes
|
||||
- Long-term color history
|
||||
- User settings
|
||||
|
||||
### API Integration
|
||||
|
||||
**Type-Safe Client**:
|
||||
```typescript
|
||||
// lib/api/client.ts
|
||||
export class PastelAPIClient {
|
||||
private baseURL: string;
|
||||
|
||||
constructor(baseURL: string = process.env.NEXT_PUBLIC_API_URL!) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async getColorInfo(colors: string[]): Promise<ColorInfoResponse> {
|
||||
const response = await fetch(`${this.baseURL}/api/v1/colors/info`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ colors }),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('API request failed');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ... all 21 endpoints
|
||||
}
|
||||
|
||||
export const pastelAPI = new PastelAPIClient();
|
||||
```
|
||||
|
||||
**React Query Hooks**:
|
||||
```typescript
|
||||
// lib/api/queries.ts
|
||||
export const useColorInfo = (colors: string[]) => {
|
||||
return useQuery({
|
||||
queryKey: ['colorInfo', colors],
|
||||
queryFn: () => pastelAPI.getColorInfo(colors),
|
||||
enabled: colors.length > 0,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDistinctColors = () => {
|
||||
return useMutation({
|
||||
mutationFn: (params: DistinctColorsParams) =>
|
||||
pastelAPI.generateDistinct(params.count, params.metric),
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Routing Strategy
|
||||
|
||||
**Next.js 16 App Router**:
|
||||
- File-based routing in `app/` directory
|
||||
- Server Components by default
|
||||
- Nested layouts for shared UI
|
||||
- Loading states with `loading.tsx`
|
||||
- Error boundaries with `error.tsx`
|
||||
- Metadata API for SEO
|
||||
|
||||
**Example Route Structure**:
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Root layout (navbar, providers)
|
||||
├── page.tsx # Home page
|
||||
├── playground/
|
||||
│ ├── layout.tsx # Playground layout (sidebar)
|
||||
│ └── page.tsx # Playground content
|
||||
└── palettes/
|
||||
├── layout.tsx # Shared palette layout
|
||||
├── page.tsx # Palette dashboard
|
||||
└── harmony/
|
||||
└── page.tsx # Harmony palettes
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone git@github.com:valknarness/pastel-ui.git
|
||||
cd pastel-ui
|
||||
|
||||
# Install dependencies (requires Node.js 18+)
|
||||
pnpm install
|
||||
|
||||
# Set up environment variables
|
||||
cp .env.example .env.local
|
||||
# Edit .env.local with your Pastel API URL
|
||||
|
||||
# Run development server
|
||||
pnpm dev
|
||||
|
||||
# Open http://localhost:3000
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000 # Pastel API URL
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000 # This app URL
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev # Start dev server (localhost:3000)
|
||||
pnpm dev:turbo # Start with Turbopack
|
||||
|
||||
# Building
|
||||
pnpm build # Production build
|
||||
pnpm start # Start production server
|
||||
|
||||
# Code Quality
|
||||
pnpm lint # ESLint
|
||||
pnpm lint:fix # Fix ESLint issues
|
||||
pnpm format # Prettier formatting
|
||||
pnpm type-check # TypeScript check
|
||||
|
||||
# Testing
|
||||
pnpm test # Run Vitest unit tests
|
||||
pnpm test:ui # Vitest UI mode
|
||||
pnpm test:e2e # Playwright E2E tests
|
||||
pnpm test:e2e:ui # Playwright UI mode
|
||||
|
||||
# Analysis
|
||||
pnpm analyze # Bundle size analysis
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
**TypeScript**:
|
||||
- Strict mode enabled
|
||||
- No `any` types (use `unknown` if needed)
|
||||
- Explicit return types for functions
|
||||
- Interface over type for object shapes
|
||||
|
||||
**React**:
|
||||
- Functional components only
|
||||
- Custom hooks for reusable logic
|
||||
- Proper dependency arrays in hooks
|
||||
- Descriptive prop names
|
||||
|
||||
**Naming Conventions**:
|
||||
- Components: `PascalCase` (e.g., `ColorPicker.tsx`)
|
||||
- Hooks: `camelCase` with `use` prefix (e.g., `useColor.ts`)
|
||||
- Utilities: `camelCase` (e.g., `formatColor.ts`)
|
||||
- Constants: `SCREAMING_SNAKE_CASE` (e.g., `MAX_COLORS`)
|
||||
- Types/Interfaces: `PascalCase` (e.g., `ColorInfo`)
|
||||
|
||||
**File Organization**:
|
||||
- One component per file
|
||||
- Co-locate types with components
|
||||
- Group related utilities
|
||||
- Barrel exports (`index.ts`) for public APIs
|
||||
|
||||
## Tailwind CSS 4 Usage
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Custom color palette
|
||||
primary: 'oklch(var(--primary))',
|
||||
secondary: 'oklch(var(--secondary))',
|
||||
// ... using CSS variables for theming
|
||||
},
|
||||
animation: {
|
||||
// Custom animations
|
||||
'fade-in': 'fadeIn 0.2s ease-in',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### CSS-First Configuration (Tailwind 4)
|
||||
|
||||
```css
|
||||
/* app/globals.css */
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Custom design tokens */
|
||||
--color-primary: oklch(0.7 0.15 250);
|
||||
--color-secondary: oklch(0.6 0.12 180);
|
||||
|
||||
/* Spacing scale */
|
||||
--spacing-xs: 0.5rem;
|
||||
--spacing-sm: 0.75rem;
|
||||
--spacing-md: 1rem;
|
||||
|
||||
/* Animations */
|
||||
--transition-fast: 150ms;
|
||||
--transition-normal: 300ms;
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@theme {
|
||||
--color-primary: oklch(0.8 0.15 250);
|
||||
--color-secondary: oklch(0.7 0.12 180);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Styling Pattern
|
||||
|
||||
```tsx
|
||||
// components/color/ColorSwatch.tsx
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface ColorSwatchProps {
|
||||
color: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function ColorSwatch({ color, size = 'md', onClick }: ColorSwatchProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'rounded-lg border-2 border-gray-300 transition-all hover:scale-110',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
{
|
||||
'h-8 w-8': size === 'sm',
|
||||
'h-12 w-12': size === 'md',
|
||||
'h-16 w-16': size === 'lg',
|
||||
}
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={onClick}
|
||||
aria-label={`Color swatch: ${color}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Code Splitting
|
||||
|
||||
```tsx
|
||||
// Lazy load heavy components
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const DistinctGenerator = dynamic(
|
||||
() => import('@/components/tools/DistinctGenerator'),
|
||||
{
|
||||
loading: () => <LoadingSpinner />,
|
||||
ssr: false, // Client-only component
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Image Optimization
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
|
||||
<Image
|
||||
src="/og-image.png"
|
||||
alt="Pastel UI"
|
||||
width={1200}
|
||||
height={630}
|
||||
priority // Load eagerly for above-fold images
|
||||
/>
|
||||
```
|
||||
|
||||
### API Call Optimization
|
||||
|
||||
```tsx
|
||||
// Debounce API calls during slider interactions
|
||||
import { useDebouncedValue } from '@/lib/hooks/useDebouncedValue';
|
||||
|
||||
const [lightness, setLightness] = useState(0.5);
|
||||
const debouncedLightness = useDebouncedValue(lightness, 300);
|
||||
|
||||
const { data } = useColorInfo([color], {
|
||||
enabled: !!color && debouncedLightness !== lightness,
|
||||
});
|
||||
```
|
||||
|
||||
### Bundle Size Management
|
||||
|
||||
- Keep initial bundle < 200KB
|
||||
- Use dynamic imports for route-specific code
|
||||
- Analyze bundle with `pnpm analyze`
|
||||
- Tree-shake unused dependencies
|
||||
|
||||
## Accessibility
|
||||
|
||||
### WCAG Compliance
|
||||
|
||||
- **AA** minimum for all UI elements
|
||||
- **AAA** target for text content
|
||||
- Color contrast checker built into design system
|
||||
- Never rely on color alone for information
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
```tsx
|
||||
// All interactive elements must be keyboard accessible
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Click me
|
||||
</div>
|
||||
```
|
||||
|
||||
### ARIA Labels
|
||||
|
||||
```tsx
|
||||
<input
|
||||
type="text"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
aria-label="Color input (hex, rgb, hsl, etc.)"
|
||||
aria-describedby="color-format-hint"
|
||||
/>
|
||||
<span id="color-format-hint" className="sr-only">
|
||||
Enter a color in any format: hex, rgb, hsl, or named color
|
||||
</span>
|
||||
```
|
||||
|
||||
### Focus Management
|
||||
|
||||
```tsx
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function Dialog({ isOpen, onClose }: DialogProps) {
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
closeButtonRef.current?.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div role="dialog" aria-modal="true">
|
||||
{/* Dialog content */}
|
||||
<button ref={closeButtonRef} onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (Vitest)
|
||||
|
||||
```typescript
|
||||
// tests/unit/utils/color.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseColor, formatColor } from '@/lib/utils/color';
|
||||
|
||||
describe('color utilities', () => {
|
||||
it('should parse hex colors', () => {
|
||||
expect(parseColor('#ff0099')).toEqual({
|
||||
r: 255,
|
||||
g: 0,
|
||||
b: 153,
|
||||
});
|
||||
});
|
||||
|
||||
it('should format RGB to hex', () => {
|
||||
expect(formatColor({ r: 255, g: 0, b: 153 }, 'hex'))
|
||||
.toBe('#ff0099');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Component Tests
|
||||
|
||||
```typescript
|
||||
// tests/unit/components/ColorSwatch.test.tsx
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ColorSwatch } from '@/components/color/ColorSwatch';
|
||||
|
||||
describe('ColorSwatch', () => {
|
||||
it('should render with correct background color', () => {
|
||||
render(<ColorSwatch color="#ff0099" />);
|
||||
const swatch = screen.getByRole('button');
|
||||
expect(swatch).toHaveStyle({ backgroundColor: '#ff0099' });
|
||||
});
|
||||
|
||||
it('should call onClick when clicked', async () => {
|
||||
const onClick = vi.fn();
|
||||
render(<ColorSwatch color="#ff0099" onClick={onClick} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(onClick).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Tests (Playwright)
|
||||
|
||||
```typescript
|
||||
// tests/e2e/playground.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('color manipulation workflow', async ({ page }) => {
|
||||
await page.goto('/playground');
|
||||
|
||||
// Enter a color
|
||||
await page.fill('[aria-label="Color input"]', '#ff0099');
|
||||
|
||||
// Verify color info is displayed
|
||||
await expect(page.locator('text=rgb(255, 0, 153)')).toBeVisible();
|
||||
|
||||
// Adjust lightness
|
||||
await page.locator('[aria-label="Lightness slider"]').fill('0.7');
|
||||
|
||||
// Verify updated color
|
||||
await expect(page.locator('.color-display')).toHaveCSS(
|
||||
'background-color',
|
||||
/rgb\(255, \d+, \d+\)/
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Vercel (Recommended)
|
||||
|
||||
```bash
|
||||
# Install Vercel CLI
|
||||
pnpm add -g vercel
|
||||
|
||||
# Deploy
|
||||
vercel
|
||||
|
||||
# Production deployment
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
### Environment Variables (Production)
|
||||
|
||||
```bash
|
||||
# Vercel dashboard or .env.production
|
||||
NEXT_PUBLIC_API_URL=https://api.pastel.example.com
|
||||
NEXT_PUBLIC_APP_URL=https://pastel.example.com
|
||||
```
|
||||
|
||||
### Build Optimization
|
||||
|
||||
```typescript
|
||||
// next.config.ts
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Enable React Compiler (Next.js 16)
|
||||
experimental: {
|
||||
reactCompiler: true,
|
||||
},
|
||||
|
||||
// Image optimization
|
||||
images: {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
},
|
||||
|
||||
// Bundle analyzer (conditional)
|
||||
...(process.env.ANALYZE === 'true' && {
|
||||
webpack(config) {
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
config.plugins.push(
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
openAnalyzer: false,
|
||||
})
|
||||
);
|
||||
return config;
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm lint
|
||||
- run: pnpm type-check
|
||||
- run: pnpm test
|
||||
- run: pnpm build
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm build
|
||||
- run: pnpm test:e2e
|
||||
|
||||
deploy:
|
||||
needs: [test, e2e]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: amondnet/vercel-action@v25
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercel-args: '--prod'
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Custom Hook Example
|
||||
|
||||
```typescript
|
||||
// lib/hooks/useColor.ts
|
||||
import { useState, useCallback } from 'use';
|
||||
import { useColorInfo } from '@/lib/api/queries';
|
||||
import { parseColor } from '@/lib/utils/color';
|
||||
|
||||
export function useColor(initialColor?: string) {
|
||||
const [color, setColor] = useState(initialColor ?? '#000000');
|
||||
const [format, setFormat] = useState<ColorFormat>('hex');
|
||||
|
||||
const { data: colorInfo, isLoading } = useColorInfo([color]);
|
||||
|
||||
const updateColor = useCallback((newColor: string) => {
|
||||
try {
|
||||
const parsed = parseColor(newColor);
|
||||
if (parsed) {
|
||||
setColor(newColor);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid color:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
color,
|
||||
colorInfo: colorInfo?.data.colors[0],
|
||||
format,
|
||||
setFormat,
|
||||
updateColor,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Zustand Store Example
|
||||
|
||||
```typescript
|
||||
// lib/stores/historyStore.ts
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface ColorHistoryState {
|
||||
history: string[];
|
||||
addColor: (color: string) => void;
|
||||
clearHistory: () => void;
|
||||
}
|
||||
|
||||
export const useColorHistory = create<ColorHistoryState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
history: [],
|
||||
|
||||
addColor: (color) => set((state) => ({
|
||||
history: [color, ...state.history.filter(c => c !== color)].slice(0, 50),
|
||||
})),
|
||||
|
||||
clearHistory: () => set({ history: [] }),
|
||||
}),
|
||||
{
|
||||
name: 'color-history',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: API calls failing with CORS errors
|
||||
**Solution**: Ensure `NEXT_PUBLIC_API_URL` is set correctly and the Pastel API has CORS enabled
|
||||
|
||||
**Issue**: Tailwind classes not applying
|
||||
**Solution**: Check `content` paths in `tailwind.config.ts` include all component files
|
||||
|
||||
**Issue**: Hydration errors
|
||||
**Solution**: Ensure client components using browser APIs have proper checks:
|
||||
```tsx
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
if (!mounted) return null;
|
||||
```
|
||||
|
||||
**Issue**: Large bundle size
|
||||
**Solution**: Use dynamic imports for heavy components and run `pnpm analyze`
|
||||
|
||||
## Resources
|
||||
|
||||
### Documentation
|
||||
- [Next.js 16 Docs](https://nextjs.org/docs)
|
||||
- [React 19 Docs](https://react.dev)
|
||||
- [Tailwind CSS 4 Docs](https://tailwindcss.com/docs)
|
||||
- [React Query Docs](https://tanstack.com/query/latest)
|
||||
- [Pastel API Docs](https://github.com/valknarness/pastel-api)
|
||||
|
||||
### Color Science
|
||||
- [OkLab Color Space](https://bottosson.github.io/posts/oklab/)
|
||||
- [WCAG Contrast Guidelines](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)
|
||||
- [Color Blindness Simulation](https://www.color-blindness.com/coblis-color-blindness-simulator/)
|
||||
|
||||
### Tools
|
||||
- [Figma](https://www.figma.com) - Design mockups
|
||||
- [Playwright](https://playwright.dev) - E2E testing
|
||||
- [Vitest](https://vitest.dev) - Unit testing
|
||||
|
||||
## Contributing
|
||||
|
||||
### PR Checklist
|
||||
|
||||
- [ ] Tests pass (`pnpm test`)
|
||||
- [ ] E2E tests pass (`pnpm test:e2e`)
|
||||
- [ ] No ESLint errors (`pnpm lint`)
|
||||
- [ ] No TypeScript errors (`pnpm type-check`)
|
||||
- [ ] Code formatted (`pnpm format`)
|
||||
- [ ] Documentation updated (if needed)
|
||||
- [ ] Accessibility tested (keyboard, screen reader)
|
||||
- [ ] Performance checked (bundle size)
|
||||
|
||||
### Code Review Guidelines
|
||||
|
||||
- All interactive elements must be keyboard accessible
|
||||
- All images must have alt text
|
||||
- Color contrast must meet WCAG AA
|
||||
- No console.log in production code
|
||||
- Meaningful variable/function names
|
||||
- Comments for complex logic only
|
||||
|
||||
---
|
||||
|
||||
**Project Status**: Design phase complete, ready for implementation
|
||||
**Last Updated**: 2025-11-07
|
||||
Reference in New Issue
Block a user