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:
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ColorInfo as ColorInfoType } from '@/lib/color/api/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
@@ -12,79 +11,70 @@ interface ColorInfoProps {
|
||||
}
|
||||
|
||||
export function ColorInfo({ info, className }: ColorInfoProps) {
|
||||
const copyToClipboard = (value: string, label: string) => {
|
||||
const copy = (value: string, label: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success(`Copied ${label} to clipboard`);
|
||||
toast.success(`Copied ${label}`);
|
||||
};
|
||||
|
||||
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 formatRgb = (rgb: { r: number; g: number; b: number; a?: number }) =>
|
||||
rgb.a !== undefined && rgb.a < 1
|
||||
? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||
: `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 formatOkLab = (oklab: { l: number; a: number; b: number }) => {
|
||||
return `oklab(${(oklab.l * 100).toFixed(1)}% ${oklab.a.toFixed(3)} ${oklab.b.toFixed(3)})`;
|
||||
};
|
||||
const formatHsl = (hsl: { h: number; s: number; l: number; a?: number }) =>
|
||||
hsl.a !== undefined && hsl.a < 1
|
||||
? `hsla(${Math.round(hsl.h)}°, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`
|
||||
: `hsl(${Math.round(hsl.h)}°, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`;
|
||||
|
||||
const formats = [
|
||||
{ label: 'Hex', value: info.hex },
|
||||
{ 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: formatOkLab(info.oklab) },
|
||||
{ label: 'Lab', value: `lab(${info.lab.l.toFixed(1)} ${info.lab.a.toFixed(1)} ${info.lab.b.toFixed(1)})` },
|
||||
{ label: 'OkLab', value: `oklab(${(info.oklab.l * 100).toFixed(1)}% ${info.oklab.a.toFixed(3)} ${info.oklab.b.toFixed(3)})` },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{formats.map((format) => (
|
||||
{/* Format rows */}
|
||||
<div className="space-y-1">
|
||||
{formats.map((fmt) => (
|
||||
<div
|
||||
key={format.label}
|
||||
className="flex items-center justify-between px-3 py-2 bg-muted/50 rounded-md group"
|
||||
key={fmt.label}
|
||||
className="group flex items-center justify-between px-2.5 py-1.5 rounded-lg border border-transparent hover:border-border/30 hover:bg-primary/5 transition-all"
|
||||
>
|
||||
<div className="flex items-baseline gap-2 min-w-0 flex-1">
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground w-10 shrink-0">{format.label}</span>
|
||||
<span className="font-mono text-xs truncate">{format.value}</span>
|
||||
<span className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-widest w-9 shrink-0">
|
||||
{fmt.label}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-foreground/80 truncate">{fmt.value}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(format.value, format.label)}
|
||||
aria-label={`Copy ${format.label} value`}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
<button
|
||||
onClick={() => copy(fmt.value, fmt.label)}
|
||||
aria-label={`Copy ${fmt.label}`}
|
||||
className="shrink-0 ml-2 p-1 rounded text-muted-foreground/30 hover:text-primary opacity-0 group-hover:opacity-100 transition-all"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
<Copy className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 pt-2 border-t text-xs">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">Brightness</div>
|
||||
<div className="font-medium">{(info.brightness * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">Luminance</div>
|
||||
<div className="font-medium">{(info.luminance * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">{info.name && typeof info.name === 'string' ? 'Name' : 'Type'}</div>
|
||||
<div className="font-medium">{info.name && typeof info.name === 'string' ? info.name : (info.is_light ? 'Light' : 'Dark')}</div>
|
||||
</div>
|
||||
{/* Metadata row */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-border/25">
|
||||
{[
|
||||
{ label: 'Brightness', value: `${(info.brightness * 100).toFixed(1)}%` },
|
||||
{ label: 'Luminance', value: `${(info.luminance * 100).toFixed(1)}%` },
|
||||
{
|
||||
label: info.name && typeof info.name === 'string' ? 'Name' : 'Type',
|
||||
value: info.name && typeof info.name === 'string' ? info.name : (info.is_light ? 'Light' : 'Dark'),
|
||||
},
|
||||
].map((m) => (
|
||||
<div key={m.label} className="px-2.5 py-2 rounded-lg bg-primary/5 border border-border/20">
|
||||
<div className="text-[10px] text-muted-foreground/40 font-mono mb-0.5">{m.label}</div>
|
||||
<div className="text-xs font-mono font-medium text-foreground/75 truncate">{m.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user