989 lines
26 KiB
Markdown
989 lines
26 KiB
Markdown
|
|
# 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
|