refactor: refactor color tool to match calculate blueprint

Rewrites all color components to use the glass panel design language,
fixed-height two-panel layout, and tab-based navigation.

- ColorManipulation: lg:grid-cols-5 split — left 2/5 shows ColorPicker
  + ColorInfo always; right 3/5 has Info/Adjust/Harmony/Gradient tabs;
  mobile 'Pick | Explore' switcher
- ColorPicker: removes shadcn Input/Label, native input with dynamic
  contrast color matching the picked hue
- ColorInfo: removes shadcn Button, native copy buttons on hover,
  metadata chips with bg-primary/5 background
- ManipulationPanel: keeps Slider, replaces Button with glass action
  buttons, tighter spacing and muted labels
- ExportMenu: keeps Select, replaces Buttons with glass action buttons,
  code preview in dark terminal box (#06060e)
- ColorSwatch: rectangular full-width design for palette grids,
  hover reveals copy icon, hex label at bottom
- PaletteGrid: denser grid (4→5 cols), smaller swatch height

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 08:15:33 +01:00
parent 50dc009fdf
commit 0727ec7675
7 changed files with 496 additions and 652 deletions

View File

@@ -13,54 +13,43 @@ interface ColorSwatchProps {
className?: string;
}
export function ColorSwatch({
color,
size = 'md',
showLabel = true,
onClick,
className,
}: ColorSwatchProps) {
export function ColorSwatch({ color, size = 'md', showLabel = true, onClick, className }: ColorSwatchProps) {
const [copied, setCopied] = useState(false);
const sizeClasses = {
sm: 'h-12 w-12',
md: 'h-16 w-16',
lg: 'h-24 w-24',
};
const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation();
const handleClick = () => {
if (onClick) { onClick(); return; }
navigator.clipboard.writeText(color);
setCopied(true);
toast.success(`Copied ${color}`);
setTimeout(() => setCopied(false), 2000);
setTimeout(() => setCopied(false), 1500);
};
return (
<div className={cn('flex flex-col items-center gap-2', className)}>
<button
className={cn(
'relative rounded-lg ring-2 ring-border transition-all duration-200',
'hover:scale-110 hover:ring-primary hover:shadow-lg',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'group active:scale-95',
sizeClasses[size]
)}
style={{ backgroundColor: color }}
onClick={onClick || handleCopy}
aria-label={`Color ${color}`}
>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all duration-200 bg-black/30 rounded-lg backdrop-blur-sm">
{copied ? (
<Check className="h-5 w-5 text-white animate-scale-in" />
) : (
<Copy className="h-5 w-5 text-white" />
)}
</div>
</button>
{showLabel && (
<span className="text-xs font-mono text-muted-foreground">{color}</span>
<button
onClick={handleClick}
title={color}
aria-label={`Color ${color}`}
className={cn(
'group relative w-full rounded-lg overflow-hidden border border-white/8 transition-all',
'hover:scale-[1.04] hover:border-white/20 hover:shadow-lg hover:shadow-black/20',
size === 'sm' && 'h-10',
size === 'md' && 'h-14',
size === 'lg' && 'h-20',
className
)}
</div>
style={{ backgroundColor: color }}
>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/25">
{copied
? <Check className="w-3.5 h-3.5 text-white drop-shadow" />
: <Copy className="w-3.5 h-3.5 text-white drop-shadow" />
}
</div>
{showLabel && (
<div className="absolute bottom-0 inset-x-0 px-1 py-0.5 text-[9px] font-mono text-white/70 bg-black/25 truncate text-center leading-tight">
{color}
</div>
)}
</button>
);
}