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:
38
components/color/ColorDisplay.tsx
Normal file
38
components/color/ColorDisplay.tsx
Normal 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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
92
components/color/ColorInfo.tsx
Normal file
92
components/color/ColorInfo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
components/color/ColorPicker.tsx
Normal file
38
components/color/ColorPicker.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user