Replaced REST API dependency with @valknarthing/pastel-wasm (130KB) for complete browser-based color operations. The application is now fully static (2.2MB total) with zero network latency and offline support. **Key Changes:** 1. **WASM Integration:** - Added @valknarthing/pastel-wasm dependency (0.1.0) - Created lib/api/wasm-client.ts wrapper matching API interface - Updated lib/api/client.ts to use WASM client by default - All 18 color operations now run locally in browser 2. **Static Export Configuration:** - Changed next.config.ts output from 'standalone' to 'export' - Disabled image optimization for static export - Removed API proxy route (app/api/pastel/[...path]/route.ts) - Updated package.json scripts (removed dev:api, added serve) 3. **Docker Optimization:** - Migrated from Node.js standalone to nginx-alpine - Created nginx.conf with SPA routing and WASM mime types - Updated Dockerfile for static file serving - Reduced image size from ~150MB to ~25MB - Changed port from 3000 to 80 (standard HTTP) - Simplified docker-compose.yml (removed pastel-api service) 4. **Documentation Updates:** - Updated README.md with WASM benefits and deployment options - Added Key Benefits section highlighting zero-latency features - Rewrote deployment section for static hosting platforms - Updated CLAUDE.md tech stack and architecture - Removed obsolete docs: DEV_SETUP.md, DOCKER.md, IMPLEMENTATION_PLAN.md **Benefits:** - 🚀 Zero Latency - All operations run locally via WebAssembly - 📱 Offline First - Works completely offline after initial load - 🌐 No Backend - Fully static, deploy anywhere - ⚡ Fast - Native-speed color operations in browser - 📦 Small - 2.2MB total (130KB WASM, 2.07MB HTML/CSS/JS) **Deployment:** Can now be deployed to any static hosting platform: - Vercel, Netlify, Cloudflare Pages (zero config) - GitHub Pages, S3, CDN - Self-hosted nginx/Apache - Docker (optional, nginx-based) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
26 KiB
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 runs entirely in the browser using WebAssembly for zero-latency color operations.
Technology Stack:
- Framework: Next.js 16 (App Router, React 19, Static Export)
- Styling: Tailwind CSS 4 (CSS-first configuration)
- Language: TypeScript (strict mode)
- Color Engine:
@valknarthing/pastel-wasm(WebAssembly, 130KB) - State Management:
@tanstack/react-query(server state)zustand(client state)
- Animation:
framer-motion - Icons:
lucide-react - UI Components: Custom components + shadcn/ui patterns
- Deployment: Fully static (2.2MB total)
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
├── 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 componentColorDisplay- Large preview areaColorInfo- Tabbed format informationManipulationPanel- 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 UIGradientCreator- Gradient builderDistinctGenerator- Distinct colors with progressPaletteGrid- Visual palette displayExportMenu- 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 interfaceContrastAnalyzer- WCAG contrast checkerColorBlindSimulator- Simulation previewsComplianceBadge- 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 displayColorSearch- Search and filterNearestColorFinder- 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 interfaceBatchOperations- Operation selectionBatchPreview- Results previewBatchExporter- 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:
// 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:
// 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
# 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
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:3000 # Pastel API URL
NEXT_PUBLIC_APP_URL=http://localhost:3000 # This app URL
Available Scripts
# 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
anytypes (useunknownif 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:
camelCasewithuseprefix (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
// 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)
/* 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
// 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
// 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
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
// 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
// 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
<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
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)
// 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
// 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)
// 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)
# Install Vercel CLI
pnpm add -g vercel
# Deploy
vercel
# Production deployment
vercel --prod
Environment Variables (Production)
# Vercel dashboard or .env.production
NEXT_PUBLIC_API_URL=https://api.pastel.example.com
NEXT_PUBLIC_APP_URL=https://pastel.example.com
Build Optimization
// 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
# .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
// 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
// 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:
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
Color Science
Tools
- Figma - Design mockups
- Playwright - E2E testing
- Vitest - 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