feat: implement color playground with interactive picker and info display

Add complete color manipulation interface:

**Color Components:**
- ColorPicker - Interactive hex color picker with text input
  - Uses react-colorful for visual selection
  - Manual input support for any color format
  - Real-time updates

- ColorDisplay - Large color preview swatch
  - Configurable sizes (sm, md, lg, xl)
  - Optional border styling
  - Accessible with ARIA labels

- ColorInfo - Comprehensive color information display
  - Shows all formats: Hex, RGB, HSL, Lab, OkLab
  - Copy to clipboard functionality for each format
  - Displays brightness, luminance, light/dark type
  - Shows named color matches

**API Integration:**
- React Query hooks for all Pastel API endpoints
  - useColorInfo - Get color information
  - useConvertFormat - Format conversion
  - useLighten, useDarken, useSaturate, etc. - Manipulations
  - useGenerateRandom, useGenerateDistinct, useGenerateGradient
  - useNamedColors - Cached named colors list
  - Automatic error handling and loading states

**Playground Page (/playground):**
- Two-column layout: picker + info
- Live color preview with large swatch
- Real-time API integration with loading states
- Error handling with user-friendly messages
- Quick action buttons (ready for implementation)
- Responsive grid layout

**Features:**
- Toast notifications for clipboard copy
- Loading spinners during API calls
- Error display with helpful messages
- Accessible keyboard navigation
- Smooth transitions and animations

Build successful with playground page rendering! Ready for color manipulation actions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
valknarness
2025-11-07 11:05:23 +01:00
parent 6c1ec9b44f
commit 173d81d9d4
5 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
'use client';
import { cn } from '@/lib/utils/cn';
interface ColorDisplayProps {
color: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
showBorder?: boolean;
}
export function ColorDisplay({
color,
size = 'lg',
className,
showBorder = true,
}: ColorDisplayProps) {
const sizeClasses = {
sm: 'h-16 w-16',
md: 'h-32 w-32',
lg: 'h-48 w-48',
xl: 'h-64 w-64',
};
return (
<div
className={cn(
'rounded-lg transition-all',
showBorder && 'ring-2 ring-border',
sizeClasses[size],
className
)}
style={{ backgroundColor: color }}
role="img"
aria-label={`Color swatch: ${color}`}
/>
);
}

View File

@@ -0,0 +1,92 @@
'use client';
import { ColorInfo as ColorInfoType } from '@/lib/api/types';
import { Button } from '@/components/ui/button';
import { Copy } from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils/cn';
interface ColorInfoProps {
info: ColorInfoType;
className?: string;
}
export function ColorInfo({ info, className }: ColorInfoProps) {
const copyToClipboard = (value: string, label: string) => {
navigator.clipboard.writeText(value);
toast.success(`Copied ${label} to clipboard`);
};
const formatRgb = (rgb: { r: number; g: number; b: number; a?: number }) => {
if (rgb.a !== undefined && rgb.a < 1) {
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
}
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
};
const formatHsl = (hsl: { h: number; s: number; l: number; a?: number }) => {
if (hsl.a !== undefined && hsl.a < 1) {
return `hsla(${Math.round(hsl.h)}°, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`;
}
return `hsl(${Math.round(hsl.h)}°, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`;
};
const formatLab = (lab: { l: number; a: number; b: number }) => {
return `lab(${lab.l.toFixed(1)}, ${lab.a.toFixed(1)}, ${lab.b.toFixed(1)})`;
};
const formats = [
{ label: 'Hex', value: info.hex },
{ label: 'RGB', value: formatRgb(info.rgb) },
{ label: 'HSL', value: formatHsl(info.hsl) },
{ label: 'Lab', value: formatLab(info.lab) },
{ label: 'OkLab', value: formatLab(info.oklab) },
];
return (
<div className={cn('space-y-4', className)}>
<div className="grid grid-cols-1 gap-3">
{formats.map((format) => (
<div
key={format.label}
className="flex items-center justify-between p-3 bg-muted rounded-lg"
>
<div className="flex-1">
<div className="text-xs text-muted-foreground mb-1">{format.label}</div>
<div className="font-mono text-sm">{format.value}</div>
</div>
<Button
size="icon"
variant="ghost"
onClick={() => copyToClipboard(format.value, format.label)}
aria-label={`Copy ${format.label} value`}
>
<Copy className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-3 pt-2 border-t">
<div className="space-y-1">
<div className="text-xs text-muted-foreground">Brightness</div>
<div className="text-sm font-medium">{(info.brightness * 100).toFixed(1)}%</div>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">Luminance</div>
<div className="text-sm font-medium">{(info.luminance * 100).toFixed(1)}%</div>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">Type</div>
<div className="text-sm font-medium">{info.is_light ? 'Light' : 'Dark'}</div>
</div>
{info.name && (
<div className="space-y-1">
<div className="text-xs text-muted-foreground">Named</div>
<div className="text-sm font-medium">{info.name}</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
'use client';
import { HexColorPicker } from 'react-colorful';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils/cn';
interface ColorPickerProps {
color: string;
onChange: (color: string) => void;
className?: string;
}
export function ColorPicker({ color, onChange, className }: ColorPickerProps) {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// Allow partial input while typing
onChange(value);
};
return (
<div className={cn('space-y-4', className)}>
<HexColorPicker color={color} onChange={onChange} className="w-full" />
<div className="space-y-2">
<label htmlFor="color-input" className="text-sm font-medium">
Color Value
</label>
<Input
id="color-input"
type="text"
value={color}
onChange={handleInputChange}
placeholder="#ff0099 or rgb(255, 0, 153)"
className="font-mono"
/>
</div>
</div>
);
}