refactor: streamline, refine and polish

This commit is contained in:
2026-02-27 12:35:02 +01:00
parent efe3c81576
commit ee7e5ec06c
21 changed files with 606 additions and 735 deletions

View File

@@ -12,12 +12,25 @@ import {
useComplement
} from '@/lib/color/api/queries';
import { toast } from 'sonner';
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
interface ManipulationPanelProps {
color: string;
onColorChange: (color: string) => void;
}
interface ManipulationRow {
label: string;
icon: React.ReactNode;
value: number;
setValue: (v: number) => void;
format: (v: number) => string;
min: number;
max: number;
step: number;
onApply: () => Promise<void>;
}
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
const [lightenAmount, setLightenAmount] = useState(0.2);
const [darkenAmount, setDarkenAmount] = useState(0.2);
@@ -32,93 +45,6 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const rotateMutation = useRotate();
const complementMutation = useComplement();
const handleLighten = async () => {
try {
const result = await lightenMutation.mutateAsync({
colors: [color],
amount: lightenAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to lighten color');
}
};
const handleDarken = async () => {
try {
const result = await darkenMutation.mutateAsync({
colors: [color],
amount: darkenAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to darken color');
}
};
const handleSaturate = async () => {
try {
const result = await saturateMutation.mutateAsync({
colors: [color],
amount: saturateAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to saturate color');
}
};
const handleDesaturate = async () => {
try {
const result = await desaturateMutation.mutateAsync({
colors: [color],
amount: desaturateAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to desaturate color');
}
};
const handleRotate = async () => {
try {
const result = await rotateMutation.mutateAsync({
colors: [color],
amount: rotateAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Rotated hue by ${rotateAmount}°`);
}
} catch (error) {
toast.error('Failed to rotate hue');
}
};
const handleComplement = async () => {
try {
const result = await complementMutation.mutateAsync([color]);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success('Generated complementary color');
}
} catch (error) {
toast.error('Failed to generate complement');
}
};
const isLoading =
lightenMutation.isPending ||
darkenMutation.isPending ||
@@ -127,108 +53,151 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
rotateMutation.isPending ||
complementMutation.isPending;
const handleMutation = async (
mutationFn: (params: any) => Promise<any>,
params: any,
successMsg: string,
errorMsg: string
) => {
try {
const result = await mutationFn(params);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(successMsg);
}
} catch {
toast.error(errorMsg);
}
};
const rows: ManipulationRow[] = [
{
label: 'Lighten',
icon: <Sun className="h-3.5 w-3.5" />,
value: lightenAmount,
setValue: setLightenAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
lightenMutation.mutateAsync,
{ colors: [color], amount: lightenAmount },
`Lightened by ${(lightenAmount * 100).toFixed(0)}%`,
'Failed to lighten color'
),
},
{
label: 'Darken',
icon: <Moon className="h-3.5 w-3.5" />,
value: darkenAmount,
setValue: setDarkenAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
darkenMutation.mutateAsync,
{ colors: [color], amount: darkenAmount },
`Darkened by ${(darkenAmount * 100).toFixed(0)}%`,
'Failed to darken color'
),
},
{
label: 'Saturate',
icon: <Droplets className="h-3.5 w-3.5" />,
value: saturateAmount,
setValue: setSaturateAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
saturateMutation.mutateAsync,
{ colors: [color], amount: saturateAmount },
`Saturated by ${(saturateAmount * 100).toFixed(0)}%`,
'Failed to saturate color'
),
},
{
label: 'Desaturate',
icon: <Droplet className="h-3.5 w-3.5" />,
value: desaturateAmount,
setValue: setDesaturateAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
desaturateMutation.mutateAsync,
{ colors: [color], amount: desaturateAmount },
`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`,
'Failed to desaturate color'
),
},
{
label: 'Rotate',
icon: <RotateCcw className="h-3.5 w-3.5" />,
value: rotateAmount,
setValue: setRotateAmount,
format: (v) => `${v}°`,
min: -180, max: 180, step: 5,
onApply: () => handleMutation(
rotateMutation.mutateAsync,
{ colors: [color], amount: rotateAmount },
`Rotated hue by ${rotateAmount}°`,
'Failed to rotate hue'
),
},
];
const handleComplement = async () => {
try {
const result = await complementMutation.mutateAsync([color]);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success('Generated complementary color');
}
} catch {
toast.error('Failed to generate complement');
}
};
return (
<div className="space-y-6">
{/* Lighten */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Lighten</label>
<span className="text-xs text-muted-foreground">{(lightenAmount * 100).toFixed(0)}%</span>
<div className="space-y-4">
{rows.map((row) => (
<div key={row.label} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-xs font-medium">
{row.icon}
<span>{row.label}</span>
</div>
<span className="text-[10px] text-muted-foreground tabular-nums">{row.format(row.value)}</span>
</div>
<div className="flex items-center gap-2">
<Slider
min={row.min}
max={row.max}
step={row.step}
value={[row.value]}
onValueChange={(vals) => row.setValue(vals[0])}
className="flex-1"
/>
<Button
onClick={row.onApply}
disabled={isLoading}
size="sm"
variant="outline"
className="shrink-0 w-16"
>
Apply
</Button>
</div>
</div>
<Slider
min={0}
max={1}
step={0.05}
value={[lightenAmount]}
onValueChange={(vals) => setLightenAmount(vals[0])}
/>
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
Apply Lighten
</Button>
</div>
))}
{/* Darken */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Darken</label>
<span className="text-xs text-muted-foreground">{(darkenAmount * 100).toFixed(0)}%</span>
</div>
<Slider
min={0}
max={1}
step={0.05}
value={[darkenAmount]}
onValueChange={(vals) => setDarkenAmount(vals[0])}
/>
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
Apply Darken
</Button>
</div>
{/* Saturate */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Saturate</label>
<span className="text-xs text-muted-foreground">{(saturateAmount * 100).toFixed(0)}%</span>
</div>
<Slider
min={0}
max={1}
step={0.05}
value={[saturateAmount]}
onValueChange={(vals) => setSaturateAmount(vals[0])}
/>
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
Apply Saturate
</Button>
</div>
{/* Desaturate */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Desaturate</label>
<span className="text-xs text-muted-foreground">{(desaturateAmount * 100).toFixed(0)}%</span>
</div>
<Slider
min={0}
max={1}
step={0.05}
value={[desaturateAmount]}
onValueChange={(vals) => setDesaturateAmount(vals[0])}
/>
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
Apply Desaturate
</Button>
</div>
{/* Rotate Hue */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Rotate Hue</label>
<span className="text-xs text-muted-foreground">{rotateAmount}°</span>
</div>
<Slider
min={-180}
max={180}
step={5}
value={[rotateAmount]}
onValueChange={(vals) => setRotateAmount(vals[0])}
/>
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
Apply Rotation
</Button>
</div>
{/* Quick Actions */}
<div className="pt-4 border-t space-y-2">
<h3 className="text-sm font-medium mb-3">Quick Actions</h3>
<div className="pt-3 border-t">
<Button
onClick={handleComplement}
disabled={isLoading}
variant="outline"
className="w-full"
size="sm"
>
Get Complementary Color
<ArrowLeftRight className="h-3.5 w-3.5 mr-1.5" />
Complementary Color
</Button>
</div>
</div>