Add comprehensive feature set and fixes:
**Theme Improvements:**
- Fix theme flickering by adding blocking script in layout
- Prevents FOUC (Flash of Unstyled Content)
- Smooth transitions between light and dark modes
**Tailwind CSS v4 Migration:**
- Convert globals.css to Tailwind CSS v4 format
- Use @import "tailwindcss" instead of @tailwind directives
- Implement @theme block with OkLCH color space
- Add @plugin directives for forms and typography
- Use :root and .dark class-based theming
- Add all custom animations in CSS
- Create postcss.config.mjs with @tailwindcss/postcss
**Dev Environment:**
- Add .env.local with API on port 3001
- Add dev:api and dev:all scripts to package.json
- Create .env for API with port 3001 configuration
- Enable running both UI and API simultaneously
**New Features Implemented:**
1. **Harmony Palettes** (app/palettes/harmony/page.tsx)
- Generate color harmonies based on color theory
- Support for 6 harmony types:
- Monochromatic
- Analogous (±30°)
- Complementary (180°)
- Split-complementary
- Triadic (120° spacing)
- Tetradic/Square (90° spacing)
- Uses complement and rotate API endpoints
- Export harmonies in multiple formats
2. **Color Blindness Simulator** (app/accessibility/colorblind/page.tsx)
- Simulate 3 types of color blindness:
- Protanopia (red-blind, ~1% males)
- Deuteranopia (green-blind, ~1% males)
- Tritanopia (blue-blind, rare)
- Side-by-side comparison of original vs simulated
- Support for multiple colors (up to 10)
- Educational information about each type
- Accessibility tips and best practices
3. **Batch Operations** (app/batch/page.tsx)
- Process up to 100 colors at once
- Text input (line-separated or comma-separated)
- 5 operations supported:
- Lighten/Darken
- Saturate/Desaturate
- Rotate hue
- Adjustable amount slider
- Export processed colors
- Live validation and color count
**API Query Hooks:**
- Add useSimulateColorBlindness hook
- Add useTextColor hook
- Export ColorBlindnessRequest and TextColorRequest types
**Files Added:**
- postcss.config.mjs
- .env.local
- ../pastel-api/.env
- app/accessibility/colorblind/page.tsx
- app/palettes/harmony/page.tsx
**Files Modified:**
- app/globals.css (Tailwind v4 migration)
- app/layout.tsx (theme flicker fix)
- app/batch/page.tsx (functional implementation)
- lib/api/queries.ts (new hooks)
- package.json (dev scripts)
- tailwind.config.ts (simplified, CSS-first)
All features build successfully and are ready for testing.
Development server can now run with API via `pnpm dev:all`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
192 lines
6.6 KiB
TypeScript
192 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Select } from '@/components/ui/select';
|
|
import { Input } from '@/components/ui/input';
|
|
import { PaletteGrid } from '@/components/color/PaletteGrid';
|
|
import { ExportMenu } from '@/components/tools/ExportMenu';
|
|
import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/api/queries';
|
|
import { Loader2, Upload, Download } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
|
|
type Operation = 'lighten' | 'darken' | 'saturate' | 'desaturate' | 'rotate';
|
|
|
|
export default function BatchPage() {
|
|
const [inputColors, setInputColors] = useState('');
|
|
const [operation, setOperation] = useState<Operation>('lighten');
|
|
const [amount, setAmount] = useState(0.2);
|
|
const [outputColors, setOutputColors] = useState<string[]>([]);
|
|
|
|
const lightenMutation = useLighten();
|
|
const darkenMutation = useDarken();
|
|
const saturateMutation = useSaturate();
|
|
const desaturateMutation = useDesaturate();
|
|
const rotateMutation = useRotate();
|
|
|
|
const parseColors = (text: string): string[] => {
|
|
// Parse colors from text (one per line, or comma-separated)
|
|
return text
|
|
.split(/[\n,]/)
|
|
.map((c) => c.trim())
|
|
.filter((c) => c.length > 0 && c.match(/^#?[0-9a-fA-F]{3,8}$/));
|
|
};
|
|
|
|
const handleProcess = async () => {
|
|
const colors = parseColors(inputColors);
|
|
|
|
if (colors.length === 0) {
|
|
toast.error('No valid colors found');
|
|
return;
|
|
}
|
|
|
|
if (colors.length > 100) {
|
|
toast.error('Maximum 100 colors allowed');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let result;
|
|
|
|
switch (operation) {
|
|
case 'lighten':
|
|
result = await lightenMutation.mutateAsync({ colors, amount });
|
|
break;
|
|
case 'darken':
|
|
result = await darkenMutation.mutateAsync({ colors, amount });
|
|
break;
|
|
case 'saturate':
|
|
result = await saturateMutation.mutateAsync({ colors, amount });
|
|
break;
|
|
case 'desaturate':
|
|
result = await desaturateMutation.mutateAsync({ colors, amount });
|
|
break;
|
|
case 'rotate':
|
|
result = await rotateMutation.mutateAsync({ colors, amount: amount * 360 });
|
|
break;
|
|
}
|
|
|
|
setOutputColors(result.colors);
|
|
toast.success(`Processed ${result.colors.length} colors`);
|
|
} catch (error) {
|
|
toast.error('Failed to process colors');
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const isPending =
|
|
lightenMutation.isPending ||
|
|
darkenMutation.isPending ||
|
|
saturateMutation.isPending ||
|
|
desaturateMutation.isPending ||
|
|
rotateMutation.isPending;
|
|
|
|
return (
|
|
<div className="min-h-screen p-8">
|
|
<div className="max-w-7xl mx-auto space-y-8">
|
|
<div>
|
|
<h1 className="text-4xl font-bold mb-2">Batch Operations</h1>
|
|
<p className="text-muted-foreground">
|
|
Process multiple colors at once with manipulation operations
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Input */}
|
|
<div className="space-y-6">
|
|
<div className="p-6 border rounded-lg bg-card">
|
|
<h2 className="text-xl font-semibold mb-4">Input Colors</h2>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Enter colors (one per line or comma-separated). Supports hex format.
|
|
</p>
|
|
|
|
<textarea
|
|
value={inputColors}
|
|
onChange={(e) => setInputColors(e.target.value)}
|
|
placeholder="#ff0099, #00ff99, #9900ff #ff5533 #3355ff"
|
|
className="w-full h-48 p-3 border rounded-lg bg-background font-mono text-sm"
|
|
/>
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{parseColors(inputColors).length} valid colors found
|
|
</p>
|
|
</div>
|
|
|
|
<div className="p-6 border rounded-lg bg-card">
|
|
<h2 className="text-xl font-semibold mb-4">Operation</h2>
|
|
<div className="space-y-4">
|
|
<Select
|
|
label="Operation"
|
|
value={operation}
|
|
onChange={(e) => setOperation(e.target.value as Operation)}
|
|
>
|
|
<option value="lighten">Lighten</option>
|
|
<option value="darken">Darken</option>
|
|
<option value="saturate">Saturate</option>
|
|
<option value="desaturate">Desaturate</option>
|
|
<option value="rotate">Rotate Hue</option>
|
|
</Select>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium mb-2 block">
|
|
Amount: {operation === 'rotate' ? (amount * 360).toFixed(0) + '°' : (amount * 100).toFixed(0) + '%'}
|
|
</label>
|
|
<Input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={amount}
|
|
onChange={(e) => setAmount(parseFloat(e.target.value))}
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleProcess}
|
|
disabled={isPending || parseColors(inputColors).length === 0}
|
|
className="w-full"
|
|
>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Processing...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
Process Colors
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Output */}
|
|
<div className="space-y-6">
|
|
{outputColors.length > 0 ? (
|
|
<>
|
|
<div className="p-6 border rounded-lg bg-card">
|
|
<h2 className="text-xl font-semibold mb-4">
|
|
Output Colors ({outputColors.length})
|
|
</h2>
|
|
<PaletteGrid colors={outputColors} />
|
|
</div>
|
|
|
|
<div className="p-6 border rounded-lg bg-card">
|
|
<ExportMenu colors={outputColors} />
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="p-12 border rounded-lg bg-card text-center text-muted-foreground">
|
|
<Download className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>Enter colors and click Process to see results</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|