Files
pastel-ui/CLAUDE.md
Sebastian Krüger 4aed0d4bf9
Some checks failed
Docker Build & Push / build-and-push (push) Failing after 24s
feat: integrate WebAssembly for zero-latency, offline-first color operations
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>
2025-11-17 09:06:25 +01:00

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 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:

// 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 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

// 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

# 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

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