feat: unify pastel application into single playground and remove standalone pages

This commit is contained in:
2026-02-26 12:07:21 +01:00
parent 225a9ad7fb
commit 061ea1d806
21 changed files with 519 additions and 2127 deletions

View File

@@ -1,219 +0,0 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Slider } from '@/components/ui/slider';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu';
import { AppPage } from '@/components/layout/AppPage';
import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/pastel/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;
}
// Extract output colors from the result
const processed = result.colors.map((c) => c.output);
setOutputColors(processed);
toast.success(`Processed ${processed.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 (
<AppPage
title="Batch Operations"
description="Process multiple colors at once with manipulation operations"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Input */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Input Colors</CardTitle>
</CardHeader>
<CardContent>
<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&#10;#ff5533&#10;#3355ff"
className="h-48 font-mono"
/>
<p className="text-xs text-muted-foreground mt-2">
{parseColors(inputColors).length} valid colors found
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Operation</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select
value={operation}
onValueChange={(value) => setOperation(value as Operation)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select operation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="lighten">Lighten</SelectItem>
<SelectItem value="darken">Darken</SelectItem>
<SelectItem value="saturate">Saturate</SelectItem>
<SelectItem value="desaturate">Desaturate</SelectItem>
<SelectItem value="rotate">Rotate Hue</SelectItem>
</SelectContent>
</Select>
<div className="space-y-4 pt-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">
Amount
</label>
<span className="text-xs text-muted-foreground">
{operation === 'rotate' ? (amount * 360).toFixed(0) + '°' : (amount * 100).toFixed(0) + '%'}
</span>
</div>
<Slider
min={0}
max={1}
step={0.01}
value={[amount]}
onValueChange={(vals) => setAmount(vals[0])}
/>
</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>
</CardContent>
</Card>
</div>
{/* Output */}
<div className="space-y-6">
{outputColors.length > 0 ? (
<>
<Card>
<CardHeader>
<CardTitle>
Output Colors ({outputColors.length})
</CardTitle>
</CardHeader>
<CardContent>
<PaletteGrid colors={outputColors} />
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<ExportMenu colors={outputColors} />
</CardContent>
</Card>
</>
) : (
<Card>
<CardContent className="p-12 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>
</CardContent>
</Card>
)}
</div>
</div>
</AppPage>
);
}

View File

@@ -1,230 +0,0 @@
'use client';
import { useState } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker';
import { ColorDisplay } from '@/components/pastel/ColorDisplay';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useSimulateColorBlindness } from '@/lib/pastel/api/queries';
import { Loader2, Eye, Plus, X } from 'lucide-react';
import { toast } from 'sonner';
type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia';
export default function ColorBlindPage() {
const [colors, setColors] = useState<string[]>(['#ff0099']);
const [blindnessType, setBlindnessType] = useState<ColorBlindnessType>('protanopia');
const [simulations, setSimulations] = useState<
Array<{ input: string; output: string; difference_percentage: number }>
>([]);
const simulateMutation = useSimulateColorBlindness();
const handleSimulate = async () => {
try {
const result = await simulateMutation.mutateAsync({
colors,
type: blindnessType,
});
setSimulations(result.colors);
toast.success(`Simulated ${blindnessType}`);
} catch (error) {
toast.error('Failed to simulate color blindness');
console.error(error);
}
};
const addColor = () => {
if (colors.length < 10) {
setColors([...colors, '#000000']);
}
};
const removeColor = (index: number) => {
if (colors.length > 1) {
setColors(colors.filter((_, i) => i !== index));
}
};
const updateColor = (index: number, color: string) => {
const newColors = [...colors];
newColors[index] = color;
setColors(newColors);
};
const typeDescriptions: Record<ColorBlindnessType, string> = {
protanopia: 'Red-blind (affects ~1% of males)',
deuteranopia: 'Green-blind (affects ~1% of males)',
tritanopia: 'Blue-blind (rare, affects ~0.001%)',
};
return (
<AppPage
title="Color Blindness Simulator"
description="Simulate how colors appear with different types of color blindness"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Controls */}
<div className="space-y-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle>Colors to Test</CardTitle>
<Button
onClick={addColor}
variant="outline"
size="sm"
disabled={colors.length >= 10}
>
<Plus className="h-4 w-4 mr-2" />
Add Color
</Button>
</CardHeader>
<CardContent className="space-y-4">
{colors.map((color, index) => (
<div key={index} className="flex items-start gap-3">
<div className="flex-1">
<ColorPicker
color={color}
onChange={(newColor) => updateColor(index, newColor)}
/>
</div>
{colors.length > 1 && (
<Button
variant="ghost"
size="icon"
onClick={() => removeColor(index)}
className="mt-8"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Blindness Type</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select
value={blindnessType}
onValueChange={(value) => setBlindnessType(value as ColorBlindnessType)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="protanopia">Protanopia (Red-blind)</SelectItem>
<SelectItem value="deuteranopia">Deuteranopia (Green-blind)</SelectItem>
<SelectItem value="tritanopia">Tritanopia (Blue-blind)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{typeDescriptions[blindnessType]}
</p>
<Button
onClick={handleSimulate}
disabled={simulateMutation.isPending || colors.length === 0}
className="w-full"
>
{simulateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Simulating..
</>
) : (
<>
<Eye className="mr-2 h-4 w-4" />
Simulate
</>
)}
</Button>
</CardContent>
</Card>
</div>
{/* Results */}
<div className="space-y-6">
{simulations.length > 0 ? (
<>
<Card>
<CardHeader>
<CardTitle>Simulation Results</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground mb-2">
Compare original colors (left) with how they appear to people with{' '}
{blindnessType} (right)
</p>
<div className="space-y-4">
{simulations.map((sim, index) => (
<div
key={index}
className="grid grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg"
>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Original
</p>
<div className="flex items-center gap-3">
<ColorDisplay color={sim.input} size="md" />
<code className="text-sm font-mono">{sim.input}</code>
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
As Seen ({sim.difference_percentage.toFixed(1)}% difference)
</p>
<div className="flex items-center gap-3">
<ColorDisplay color={sim.output} size="md" />
<code className="text-sm font-mono">{sim.output}</code>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="bg-blue-50 dark:bg-blue-950/20 border-blue-100 dark:border-blue-900/30 shadow-none">
<CardContent className="pt-6">
<h3 className="font-semibold mb-2 flex items-center gap-2">
<Eye className="h-5 w-5" />
Accessibility Tip
</h3>
<p className="text-sm text-muted-foreground">
Ensure important information isn&apos;t conveyed by color alone. Use text
labels, patterns, or icons to make your design accessible to everyone
</p>
</CardContent>
</Card>
</>
) : (
<Card>
<CardContent className="p-12 text-center text-muted-foreground">
<Eye className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Add colors and click Simulate to see how they appear</p>
<p className="text-sm mt-2">with different types of color blindness</p>
</CardContent>
</Card>
)}
</div>
</div>
</AppPage>
);
}

View File

@@ -1,184 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { getContrastRatio, hexToRgb, checkWCAGCompliance } from '@/lib/pastel/utils/color';
import { ArrowLeftRight, Check, X } from 'lucide-react';
export default function ContrastPage() {
const [foreground, setForeground] = useState('#000000');
const [background, setBackground] = useState('#ffffff');
const [ratio, setRatio] = useState<number | null>(null);
const [compliance, setCompliance] = useState<any>(null);
useEffect(() => {
const fgRgb = hexToRgb(foreground);
const bgRgb = hexToRgb(background);
if (fgRgb && bgRgb) {
const contrastRatio = getContrastRatio(fgRgb, bgRgb);
setRatio(contrastRatio);
setCompliance(checkWCAGCompliance(contrastRatio));
}
}, [foreground, background]);
const swapColors = () => {
const temp = foreground;
setForeground(background);
setBackground(temp);
};
const ComplianceItem = ({
label,
passed,
}: {
label: string;
passed: boolean;
}) => (
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
<span className="text-sm">{label}</span>
<Badge variant={passed ? 'default' : 'destructive'}>
{passed ? (
<>
<Check className="h-3 w-3 mr-1" />
Pass
</>
) : (
<>
<X className="h-3 w-3 mr-1" />
Fail
</>
)}
</Badge>
</div>
);
return (
<AppPage
title="Contrast Checker"
description="Test color combinations for WCAG 2.1 compliance"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Color Pickers */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Foreground Color</CardTitle>
</CardHeader>
<CardContent>
<ColorPicker color={foreground} onChange={setForeground} />
</CardContent>
</Card>
<div className="flex justify-center">
<Button
onClick={swapColors}
variant="outline"
size="icon"
className="rounded-full"
>
<ArrowLeftRight className="h-4 w-4" />
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Background Color</CardTitle>
</CardHeader>
<CardContent>
<ColorPicker color={background} onChange={setBackground} />
</CardContent>
</Card>
</div>
{/* Results */}
<div className="space-y-6">
{/* Preview */}
<Card>
<CardHeader>
<CardTitle>Preview</CardTitle>
</CardHeader>
<CardContent>
<div
className="rounded-lg p-8 text-center"
style={{ backgroundColor: background, color: foreground }}
>
<p className="text-xl font-bold mb-2">Normal Text (16px)</p>
<p className="text-3xl font-bold">Large Text (24px)</p>
</div>
</CardContent>
</Card>
{/* Contrast Ratio */}
{ratio !== null && (
<Card>
<CardHeader>
<CardTitle>Contrast Ratio</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center mb-6">
<div className="text-5xl font-bold">{ratio.toFixed(2)}:1</div>
<p className="text-sm text-muted-foreground mt-2">
{ratio >= 7
? 'Excellent contrast'
: ratio >= 4.5
? 'Good contrast'
: ratio >= 3
? 'Minimum contrast'
: 'Poor contrast'}
</p>
</div>
</CardContent>
</Card>
)}
{/* WCAG Compliance */}
{compliance && (
<Card>
<CardHeader>
<CardTitle>WCAG 2.1 Compliance</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="text-sm font-semibold mb-2">Level AA</h3>
<div className="space-y-2">
<ComplianceItem
label="Normal Text (4.5:1)"
passed={compliance.aa.normalText}
/>
<ComplianceItem
label="Large Text (3:1)"
passed={compliance.aa.largeText}
/>
<ComplianceItem
label="UI Components (3:1)"
passed={compliance.aa.ui}
/>
</div>
</div>
<div>
<h3 className="text-sm font-semibold mb-2">Level AAA</h3>
<div className="space-y-2">
<ComplianceItem
label="Normal Text (7:1)"
passed={compliance.aaa.normalText}
/>
<ComplianceItem
label="Large Text (4.5:1)"
passed={compliance.aaa.largeText}
/>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</AppPage>
);
}

View File

@@ -1,138 +0,0 @@
'use client';
import { useState } from 'react';
import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useGenerateDistinct } from '@/lib/pastel/api/queries';
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
export default function DistinctPage() {
const [count, setCount] = useState(8);
const [metric, setMetric] = useState<'cie76' | 'ciede2000'>('ciede2000');
const [colors, setColors] = useState<string[]>([]);
const generateMutation = useGenerateDistinct();
const handleGenerate = async () => {
try {
const result = await generateMutation.mutateAsync({
count,
metric,
});
setColors(result.colors);
toast.success(`Generated ${result.colors.length} distinct colors`);
} catch (error) {
toast.error('Failed to generate distinct colors');
}
};
return (
<AppPage
title="Distinct Colors Generator"
description="Generate visually distinct colors using simulated annealing"
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Controls */}
<div className="lg:col-span-1">
<Card>
<CardHeader>
<CardTitle>Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<label htmlFor="count" className="text-sm font-medium mb-2 block">
Number of Colors
</label>
<Input
id="count"
type="number"
min={2}
max={100}
value={count}
onChange={(e) => setCount(parseInt(e.target.value))}
/>
<p className="text-xs text-muted-foreground mt-1">
Higher counts take longer to generate
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium block">
Distance Metric
</label>
<Select
value={metric}
onValueChange={(value) => setMetric(value as 'cie76' | 'ciede2000')}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select metric" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cie76">CIE76 (Faster)</SelectItem>
<SelectItem value="ciede2000">CIEDE2000 (More Accurate)</SelectItem>
</SelectContent>
</Select>
</div>
<Button
onClick={handleGenerate}
disabled={generateMutation.isPending}
className="w-full"
>
{generateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
'Generate Colors'
)}
</Button>
{generateMutation.isPending && (
<div className="text-sm text-muted-foreground text-center">
This may take a few moments..
</div>
)}
</CardContent>
</Card>
</div>
{/* Results */}
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle>
Generated Colors {colors.length > 0 && `(${colors.length})`}
</CardTitle>
</CardHeader>
<CardContent>
<PaletteGrid colors={colors} />
</CardContent>
</Card>
{colors.length > 0 && (
<Card>
<CardContent className="pt-6">
<ExportMenu colors={colors} />
</CardContent>
</Card>
)}
</div>
</div>
</AppPage>
);
}

View File

@@ -1,170 +0,0 @@
'use client';
import { useState } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker';
import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useGenerateGradient } from '@/lib/pastel/api/queries';
import { Loader2, Plus, X } from 'lucide-react';
import { toast } from 'sonner';
export default function GradientPage() {
const [stops, setStops] = useState<string[]>(['#ff0099', '#0099ff']);
const [count, setCount] = useState(10);
const [gradient, setGradient] = useState<string[]>([]);
const generateMutation = useGenerateGradient();
const handleGenerate = async () => {
try {
const result = await generateMutation.mutateAsync({
stops,
count,
});
setGradient(result.gradient);
toast.success(`Generated ${result.gradient.length} colors`);
} catch (error) {
toast.error('Failed to generate gradient');
}
};
const addStop = () => {
setStops([...stops, '#000000']);
};
const removeStop = (index: number) => {
if (stops.length > 2) {
setStops(stops.filter((_, i) => i !== index));
}
};
const updateStop = (index: number, color: string) => {
const newStops = [...stops];
newStops[index] = color;
setStops(newStops);
};
return (
<AppPage
title="Gradient Creator"
description="Create smooth color gradients with multiple stops"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Controls */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Color Stops</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
{stops.map((stop, index) => (
<div key={index} className="flex items-start gap-3">
<div className="flex-1">
<ColorPicker
color={stop}
onChange={(color) => updateStop(index, color)}
/>
</div>
{stops.length > 2 && (
<Button
variant="ghost"
size="icon"
onClick={() => removeStop(index)}
className="mt-8"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button onClick={addStop} variant="outline" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Stop
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label htmlFor="count" className="text-sm font-medium mb-2 block">
Number of Colors
</label>
<Input
id="count"
type="number"
min={2}
max={1000}
value={count}
onChange={(e) => setCount(parseInt(e.target.value))}
/>
</div>
<Button
onClick={handleGenerate}
disabled={generateMutation.isPending || stops.length < 2}
className="w-full"
>
{generateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
'Generate Gradient'
)}
</Button>
</CardContent>
</Card>
</div>
{/* Preview */}
<div className="space-y-6">
{gradient && gradient.length > 0 && (
<>
<Card>
<CardHeader>
<CardTitle>Gradient Preview</CardTitle>
</CardHeader>
<CardContent>
<div
className="h-32 rounded-lg"
style={{
background: `linear-gradient(to right, ${gradient.join(', ')})`,
}}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
Colors ({gradient.length})
</CardTitle>
</CardHeader>
<CardContent>
<PaletteGrid colors={gradient} />
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<ExportMenu colors={gradient} />
</CardContent>
</Card>
</>
)}
</div>
</div>
</AppPage>
);
}

View File

@@ -1,161 +0,0 @@
'use client';
import { useState } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker';
import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useGeneratePalette } from '@/lib/pastel/api/queries';
import { Loader2, Palette } from 'lucide-react';
import { toast } from 'sonner';
type HarmonyType =
| 'monochromatic'
| 'analogous'
| 'complementary'
| 'split-complementary'
| 'triadic'
| 'tetradic';
export default function HarmonyPage() {
const [baseColor, setBaseColor] = useState('#ff0099');
const [harmonyType, setHarmonyType] = useState<HarmonyType>('complementary');
const [palette, setPalette] = useState<string[]>([]);
const paletteMutation = useGeneratePalette();
const generateHarmony = async () => {
try {
const result = await paletteMutation.mutateAsync({
base: baseColor,
scheme: harmonyType,
});
// Combine primary and secondary colors into flat array
const colors = [result.palette.primary, ...result.palette.secondary];
setPalette(colors);
toast.success(`Generated ${harmonyType} harmony palette`);
} catch (error) {
toast.error('Failed to generate harmony palette');
console.error(error);
}
};
const harmonyDescriptions: Record<HarmonyType, string> = {
monochromatic: 'Single color with variations',
analogous: 'Colors adjacent on the color wheel (±30°)',
complementary: 'Colors opposite on the color wheel (180°)',
'split-complementary': 'Base color + two colors flanking its complement',
triadic: 'Three colors evenly spaced on the color wheel (120°)',
tetradic: 'Four colors evenly spaced on the color wheel (90°)',
};
return (
<AppPage
title="Harmony Palette Generator"
description="Create color harmonies based on color theory principles"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Controls */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Base Color</CardTitle>
</CardHeader>
<CardContent>
<ColorPicker color={baseColor} onChange={setBaseColor} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Harmony Type</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select
value={harmonyType}
onValueChange={(value) => setHarmonyType(value as HarmonyType)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select harmony" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochromatic">Monochromatic</SelectItem>
<SelectItem value="analogous">Analogous</SelectItem>
<SelectItem value="complementary">Complementary</SelectItem>
<SelectItem value="split-complementary">Split-Complementary</SelectItem>
<SelectItem value="triadic">Triadic</SelectItem>
<SelectItem value="tetradic">Tetradic (Square)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{harmonyDescriptions[harmonyType]}
</p>
<Button
onClick={generateHarmony}
disabled={paletteMutation.isPending}
className="w-full"
>
{paletteMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
<>
<Palette className="mr-2 h-4 w-4" />
Generate Harmony
</>
)}
</Button>
</CardContent>
</Card>
</div>
{/* Results */}
<div className="space-y-6">
{palette.length > 0 && (
<>
<Card>
<CardHeader>
<CardTitle>
Generated Palette ({palette.length} colors)
</CardTitle>
</CardHeader>
<CardContent>
<PaletteGrid colors={palette} />
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<ExportMenu colors={palette} />
</CardContent>
</Card>
</>
)}
{palette.length === 0 && (
<Card>
<CardContent className="p-12 text-center text-muted-foreground">
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Select a harmony type and click Generate to create your palette</p>
</CardContent>
</Card>
)}
</div>
</div>
</AppPage>
);
}

View File

@@ -1,122 +0,0 @@
'use client';
import { useState, useMemo } from 'react';
import { ColorSwatch } from '@/components/pastel/ColorSwatch';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useNamedColors } from '@/lib/pastel/api/queries';
import { Loader2 } from 'lucide-react';
import { parse_color } from '@valknarthing/pastel-wasm';
export default function NamedColorsPage() {
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'hue'>('name');
const { data, isLoading, isError } = useNamedColors();
const filteredColors = useMemo(() => {
if (!data) return [];
let colors = data.colors.filter(
(color) =>
color.name.toLowerCase().includes(search.toLowerCase()) ||
color.hex.toLowerCase().includes(search.toLowerCase())
);
if (sortBy === 'name') {
colors.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortBy === 'hue') {
colors.sort((a, b) => {
const infoA = parse_color(a.hex);
const infoB = parse_color(b.hex);
return infoA.hsl[0] - infoB.hsl[0];
});
}
return colors;
}, [data, search, sortBy]);
return (
<AppPage
title="Named Colors"
description="Explore 148 CSS/X11 named colors"
>
<div className="space-y-8">
{/* Search and Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
type="text"
placeholder="Search by name or hex..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="w-full sm:w-48">
<Select value={sortBy} onValueChange={(value) => setSortBy(value as 'name' | 'hue')}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">Sort by Name</SelectItem>
<SelectItem value="hue">Sort by Hue</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Colors Grid */}
<Card>
<CardContent className="pt-6">
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{isError && (
<div className="text-center py-12 text-destructive">
Failed to load named colors
</div>
)}
{filteredColors.length > 0 && (
<>
<div className="mb-4 text-sm text-muted-foreground">
Showing {filteredColors.length} colors
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
{filteredColors.map((color) => (
<div key={color.name} className="flex flex-col items-center gap-2">
<ColorSwatch color={color.hex} showLabel={false} />
<div className="text-center">
<div className="text-sm font-medium">{color.name}</div>
<div className="text-xs font-mono text-muted-foreground">
{color.hex}
</div>
</div>
</div>
))}
</div>
</>
)}
{filteredColors.length === 0 && !isLoading && !isError && (
<div className="text-center py-12 text-muted-foreground">
No colors match your search
</div>
)}
</CardContent>
</Card>
</div>
</AppPage>
);
}

View File

@@ -3,17 +3,32 @@
import { useState, useEffect, Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { ColorPicker } from '@/components/pastel/ColorPicker';
import { ColorDisplay } from '@/components/pastel/ColorDisplay';
import { ColorInfo } from '@/components/pastel/ColorInfo';
import { ManipulationPanel } from '@/components/pastel/ManipulationPanel';
import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useColorInfo } from '@/lib/pastel/api/queries';
import { useColorHistory } from '@/lib/pastel/stores/historyStore';
import { Loader2, Share2, History, X } from 'lucide-react';
import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/pastel/api/queries';
import { Loader2, Share2, Palette, Plus, X, Layers } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner';
type HarmonyType =
| 'monochromatic'
| 'analogous'
| 'complementary'
| 'triadic'
| 'tetradic';
function PlaygroundContent() {
const searchParams = useSearchParams();
const router = useRouter();
@@ -23,16 +38,23 @@ function PlaygroundContent() {
return urlColor ? `#${urlColor.replace('#', '')}` : '#ff0099';
});
// Harmony state
const [harmonyType, setHarmonyType] = useState<HarmonyType>('complementary');
const [palette, setPalette] = useState<string[]>([]);
const paletteMutation = useGeneratePalette();
// Gradient state
const [stops, setStops] = useState<string[]>(['#ff0099', '#0099ff']);
const [gradientCount, setGradientCount] = useState(10);
const [gradientResult, setGradientResult] = useState<string[]>([]);
const gradientMutation = useGenerateGradient();
const { data, isLoading, isError, error } = useColorInfo({
colors: [color],
});
const colorInfo = data?.colors[0];
// Color history
const { history, addColor, removeColor, clearHistory, getRecent } = useColorHistory();
const recentColors = getRecent(10);
// Update URL when color changes
useEffect(() => {
const hex = color.replace('#', '');
@@ -41,18 +63,12 @@ function PlaygroundContent() {
}
}, [color, router]);
// Debounced history update
// Sync first gradient stop with active color
useEffect(() => {
const timer = setTimeout(() => {
const hex = color.replace('#', '');
// Only add valid hex colors to history
if (hex.length === 6 || hex.length === 3) {
addColor(color);
}
}, 1000); // Wait 1 second before adding to history
return () => clearTimeout(timer);
}, [color, addColor]);
const newStops = [...stops];
newStops[0] = color;
setStops(newStops);
}, [color]);
// Share color via URL
const handleShare = () => {
@@ -61,16 +77,59 @@ function PlaygroundContent() {
toast.success('Link copied to clipboard!');
};
// Copy color to clipboard
const handleCopyColor = () => {
navigator.clipboard.writeText(color);
toast.success('Color copied to clipboard!');
const generateHarmony = async () => {
try {
const result = await paletteMutation.mutateAsync({
base: color,
scheme: harmonyType,
});
const colors = [result.palette.primary, ...result.palette.secondary];
setPalette(colors);
toast.success(`Generated ${harmonyType} harmony palette`);
} catch (error) {
toast.error('Failed to generate harmony palette');
console.error(error);
}
};
// Random color generation
const handleRandomColor = () => {
const randomHex = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
setColor(randomHex);
const generateGradient = async () => {
try {
const result = await gradientMutation.mutateAsync({
stops,
count: gradientCount,
});
setGradientResult(result.gradient);
toast.success(`Generated ${result.gradient.length} colors`);
} catch (error) {
toast.error('Failed to generate gradient');
}
};
const addStop = () => {
setStops([...stops, '#000000']);
};
const removeStop = (index: number) => {
if (index === 0) return; // Prevent deleting the first stop (synchronized with picker)
if (stops.length > 2) {
setStops(stops.filter((_, i) => i !== index));
}
};
const updateStop = (index: number, colorValue: string) => {
const newStops = [...stops];
newStops[index] = colorValue;
setStops(newStops);
if (index === 0) setColor(colorValue);
};
const harmonyDescriptions: Record<HarmonyType, string> = {
monochromatic: 'Single color with variations',
analogous: 'Colors adjacent on the color wheel (±30°)',
complementary: 'Colors opposite on the color wheel (180°)',
triadic: 'Three colors evenly spaced on the color wheel (120°)',
tetradic: 'Four colors evenly spaced on the color wheel (90°)',
};
return (
@@ -78,112 +137,50 @@ function PlaygroundContent() {
title="Pastel"
description="Interactive color manipulation and analysis tool"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column: Color Picker and Display */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Color Picker</CardTitle>
</CardHeader>
<CardContent>
<ColorPicker color={color} onChange={setColor} />
</CardContent>
</Card>
<Card>
<div className="space-y-8">
{/* Row 1: Workspace */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch">
{/* Main Workspace: Color Picker and Information */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle>Preview</CardTitle>
<CardTitle>Color Picker</CardTitle>
<Button onClick={handleShare} variant="outline" size="sm">
<Share2 className="h-4 w-4 mr-2" />
Share
</Button>
</CardHeader>
<CardContent>
<div className="flex justify-center">
<ColorDisplay color={color} size="xl" />
<div className="flex flex-col md:flex-row gap-12">
<div className="flex-shrink-0 mx-auto md:mx-0">
<ColorPicker color={color} onChange={setColor} />
</div>
<div className="flex-1">
<h3 className="text-sm font-medium mb-4 text-muted-foreground uppercase tracking-wider">Color Information</h3>
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{isError && (
<div className="p-4 bg-destructive/10 text-destructive rounded-lg">
<p className="font-medium">Error loading color information</p>
<p className="text-sm mt-1">{error?.message || 'Unknown error'}</p>
</div>
)}
{colorInfo && <ColorInfo info={colorInfo} />}
</div>
</div>
</CardContent>
</Card>
{recentColors.length > 0 && (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div className="flex items-center gap-2">
<History className="h-5 w-5" />
<CardTitle>Recent Colors</CardTitle>
</div>
<Button
onClick={clearHistory}
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground"
>
Clear
</Button>
</CardHeader>
<CardContent>
<div className="grid grid-cols-5 gap-2">
{recentColors.map((entry) => (
<div
key={entry.timestamp}
className="group relative aspect-square rounded-lg border-2 border-border hover:border-primary transition-all hover:scale-110 cursor-pointer"
style={{ backgroundColor: entry.color }}
onClick={() => setColor(entry.color)}
title={entry.color}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setColor(entry.color);
}
}}
>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30 rounded-lg">
<button
onClick={(e) => {
e.stopPropagation();
removeColor(entry.color);
toast.success('Color removed from history');
}}
className="p-1 bg-destructive rounded-full hover:bg-destructive/80"
aria-label="Remove color"
>
<X className="h-3 w-3 text-destructive-foreground" />
</button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
{/* Right Column: Color Information */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Color Information</CardTitle>
</CardHeader>
<CardContent>
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{isError && (
<div className="p-4 bg-destructive/10 text-destructive rounded-lg">
<p className="font-medium">Error loading color information</p>
<p className="text-sm mt-1">{error?.message || 'Unknown error'}</p>
</div>
)}
{colorInfo && <ColorInfo info={colorInfo} />}
</CardContent>
</Card>
<Card>
{/* Sidebar: Color Manipulation */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Color Manipulation</CardTitle>
</CardHeader>
@@ -193,6 +190,193 @@ function PlaygroundContent() {
</Card>
</div>
</div>
{/* Row 2: Harmony Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch">
{/* Harmony Controls */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="h-5 w-5" />
Harmony Type
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select
value={harmonyType}
onValueChange={(value) => setHarmonyType(value as HarmonyType)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select harmony" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochromatic">Monochromatic</SelectItem>
<SelectItem value="analogous">Analogous</SelectItem>
<SelectItem value="complementary">Complementary</SelectItem>
<SelectItem value="triadic">Triadic</SelectItem>
<SelectItem value="tetradic">Tetradic (Square)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{harmonyDescriptions[harmonyType]}
</p>
<Button
onClick={generateHarmony}
disabled={paletteMutation.isPending}
className="w-full"
>
{paletteMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
<>
<Palette className="mr-2 h-4 w-4" />
Generate Harmony
</>
)}
</Button>
</CardContent>
</Card>
</div>
{/* Harmony Results */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>
Generated Palette {palette.length > 0 && `(${palette.length} colors)`}
</CardTitle>
</CardHeader>
<CardContent>
{palette.length > 0 ? (
<div className="space-y-6">
<PaletteGrid colors={palette} onColorClick={setColor} />
<div className="pt-4 border-t">
<ExportMenu colors={palette} />
</div>
</div>
) : (
<div className="p-12 text-center text-muted-foreground">
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Select a harmony type and click Generate to create your palette based on the current color</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
{/* Row 3: Gradient Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch">
{/* Gradient Controls */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Layers className="h-5 w-5" />
Gradient Controls
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Color Stops</h4>
{stops.map((stop, index) => (
<div key={index} className="flex items-center gap-2">
<div className="flex-1">
<Input
type="color"
value={stop}
onChange={(e) => updateStop(index, e.target.value)}
className="h-10 w-full cursor-pointer p-1"
/>
</div>
{index !== 0 && stops.length > 2 && (
<Button
variant="ghost"
size="icon"
onClick={() => removeStop(index)}
className="shrink-0"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button onClick={addStop} variant="outline" size="sm" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Stop
</Button>
</div>
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Steps</h4>
<Input
type="number"
min={2}
max={100}
value={gradientCount}
onChange={(e) => setGradientCount(parseInt(e.target.value))}
/>
</div>
<Button
onClick={generateGradient}
disabled={gradientMutation.isPending}
className="w-full"
>
{gradientMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
'Generate Gradient'
)}
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Gradient Results */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>
Generated Gradient {gradientResult.length > 0 && `(${gradientResult.length} colors)`}
</CardTitle>
</CardHeader>
<CardContent>
{gradientResult.length > 0 ? (
<div className="space-y-6">
<div
className="h-24 w-full rounded-lg border shadow-inner"
style={{
background: `linear-gradient(to right, ${gradientResult.join(', ')})`,
}}
/>
<PaletteGrid colors={gradientResult} onColorClick={setColor} />
<div className="pt-4 border-t">
<ExportMenu colors={gradientResult} />
</div>
</div>
) : (
<div className="p-12 text-center text-muted-foreground">
<Layers className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Add color stops and click Generate to create your smooth gradient</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
</AppPage>
);
}

View File

@@ -1,227 +0,0 @@
'use client';
import { useState } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker';
import { ColorDisplay } from '@/components/pastel/ColorDisplay';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppPage } from '@/components/layout/AppPage';
import { useTextColor } from '@/lib/pastel/api/queries';
import { Loader2, Palette, Plus, X, CheckCircle2, XCircle } from 'lucide-react';
import { toast } from 'sonner';
export default function TextColorPage() {
const [backgrounds, setBackgrounds] = useState<string[]>(['#ff0099']);
const [results, setResults] = useState<
Array<{
background: string;
textcolor: string;
contrast_ratio: number;
wcag_aa: boolean;
wcag_aaa: boolean;
}>
>([]);
const textColorMutation = useTextColor();
const handleOptimize = async () => {
try {
const result = await textColorMutation.mutateAsync({
backgrounds,
});
setResults(result.colors);
toast.success(`Optimized text colors for ${result.colors.length} background(s)`);
} catch (error) {
toast.error('Failed to optimize text colors');
console.error(error);
}
};
const addBackground = () => {
if (backgrounds.length < 10) {
setBackgrounds([...backgrounds, '#000000']);
}
};
const removeBackground = (index: number) => {
if (backgrounds.length > 1) {
setBackgrounds(backgrounds.filter((_, i) => i !== index));
}
};
const updateBackground = (index: number, color: string) => {
const newBackgrounds = [...backgrounds];
newBackgrounds[index] = color;
setBackgrounds(newBackgrounds);
};
return (
<AppPage
title="Text Color Optimizer"
description="Automatically find the best text color (black or white) for any background color"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Input */}
<div className="space-y-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle>Background Colors</CardTitle>
<Button
onClick={addBackground}
disabled={backgrounds.length >= 10}
variant="outline"
size="sm"
>
<Plus className="h-4 w-4 mr-2" />
Add
</Button>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
{backgrounds.map((color, index) => (
<div key={index} className="flex items-center gap-3">
<div className="flex-1">
<ColorPicker
color={color}
onChange={(newColor) => updateBackground(index, newColor)}
/>
</div>
{backgrounds.length > 1 && (
<Button
onClick={() => removeBackground(index)}
variant="ghost"
size="icon"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
<Button
onClick={handleOptimize}
disabled={textColorMutation.isPending || backgrounds.length === 0}
className="w-full mt-4"
>
{textColorMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Optimizing..
</>
) : (
<>
<Palette className="mr-2 h-4 w-4" />
Optimize Text Colors
</>
)}
</Button>
</CardContent>
</Card>
<Card className="bg-blue-50 dark:bg-blue-950/20 border-blue-100 dark:border-blue-900/30 shadow-none">
<CardContent className="pt-6">
<h3 className="font-semibold mb-2">How it works</h3>
<p className="text-sm text-muted-foreground">
This tool analyzes each background color and automatically selects either black
or white text to ensure maximum readability. The algorithm guarantees WCAG AA
compliance (4.5:1 contrast ratio) for normal text
</p>
</CardContent>
</Card>
</div>
{/* Results */}
<div className="space-y-6">
{results.length > 0 ? (
<>
<Card>
<CardHeader>
<CardTitle>Optimized Results</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{results.map((result, index) => (
<Card
key={index}
className="overflow-hidden shadow-none"
style={{ backgroundColor: result.background }}
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<ColorDisplay color={result.background} size="sm" />
<code className="text-sm font-mono text-inherit">
{result.background}
</code>
</div>
</div>
<div
className="p-4 rounded border-2"
style={{
backgroundColor: result.background,
color: result.textcolor,
borderColor: result.textcolor,
}}
>
<p className="font-semibold mb-2" style={{ color: result.textcolor }}>
Sample Text Preview
</p>
<p className="text-sm" style={{ color: result.textcolor }}>
The quick brown fox jumps over the lazy dog. This is how your text
will look on this background color
</p>
</div>
<div className="mt-3 grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-muted-foreground">Text Color: </span>
<code className="font-mono">{result.textcolor}</code>
</div>
<div>
<span className="text-muted-foreground">Contrast: </span>
<span className="font-medium">
{result.contrast_ratio.toFixed(2)}:1
</span>
</div>
<div className="flex items-center gap-2">
{result.wcag_aa ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className={result.wcag_aa ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
WCAG AA
</span>
</div>
<div className="flex items-center gap-2">
{result.wcag_aaa ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-yellow-500" />
)}
<span className={result.wcag_aaa ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'}>
WCAG AAA
</span>
</div>
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
</>
) : (
<Card>
<CardContent className="p-12 text-center text-muted-foreground">
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Add background colors and click Optimize to see results</p>
</CardContent>
</Card>
)}
</div>
</div>
</AppPage>
);
}