Extract shared actionBtn and iconBtn constants into lib/utils/styles.ts and replace all 11 local definitions across tool components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Slider } from '@/components/ui/slider';
|
|
import {
|
|
useLighten,
|
|
useDarken,
|
|
useSaturate,
|
|
useDesaturate,
|
|
useRotate,
|
|
useComplement,
|
|
} from '@/lib/color/api/queries';
|
|
import { toast } from 'sonner';
|
|
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
|
|
import { cn, actionBtn } from '@/lib/utils';
|
|
|
|
interface ManipulationPanelProps {
|
|
color: string;
|
|
onColorChange: (color: string) => void;
|
|
}
|
|
|
|
|
|
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
|
|
const [lightenAmount, setLightenAmount] = useState(0.2);
|
|
const [darkenAmount, setDarkenAmount] = useState(0.2);
|
|
const [saturateAmount, setSaturateAmount] = useState(0.2);
|
|
const [desaturateAmount, setDesaturateAmount] = useState(0.2);
|
|
const [rotateAmount, setRotateAmount] = useState(30);
|
|
|
|
const lightenMutation = useLighten();
|
|
const darkenMutation = useDarken();
|
|
const saturateMutation = useSaturate();
|
|
const desaturateMutation = useDesaturate();
|
|
const rotateMutation = useRotate();
|
|
const complementMutation = useComplement();
|
|
|
|
const isLoading =
|
|
lightenMutation.isPending ||
|
|
darkenMutation.isPending ||
|
|
saturateMutation.isPending ||
|
|
desaturateMutation.isPending ||
|
|
rotateMutation.isPending ||
|
|
complementMutation.isPending;
|
|
|
|
const applyMutation = async (
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mutationFn: (p: any) => Promise<{ colors: { output: string }[] }>,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
params: any,
|
|
msg: string
|
|
) => {
|
|
try {
|
|
const result = await mutationFn(params);
|
|
if (result.colors[0]) {
|
|
onColorChange(result.colors[0].output);
|
|
toast.success(msg);
|
|
}
|
|
} catch {
|
|
toast.error('Failed to apply');
|
|
}
|
|
};
|
|
|
|
const rows = [
|
|
{
|
|
label: 'Lighten', icon: <Sun className="w-3 h-3" />,
|
|
value: lightenAmount, setValue: setLightenAmount,
|
|
display: `${(lightenAmount * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => applyMutation(lightenMutation.mutateAsync, { colors: [color], amount: lightenAmount }, `Lightened ${(lightenAmount * 100).toFixed(0)}%`),
|
|
},
|
|
{
|
|
label: 'Darken', icon: <Moon className="w-3 h-3" />,
|
|
value: darkenAmount, setValue: setDarkenAmount,
|
|
display: `${(darkenAmount * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => applyMutation(darkenMutation.mutateAsync, { colors: [color], amount: darkenAmount }, `Darkened ${(darkenAmount * 100).toFixed(0)}%`),
|
|
},
|
|
{
|
|
label: 'Saturate', icon: <Droplets className="w-3 h-3" />,
|
|
value: saturateAmount, setValue: setSaturateAmount,
|
|
display: `${(saturateAmount * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => applyMutation(saturateMutation.mutateAsync, { colors: [color], amount: saturateAmount }, `Saturated ${(saturateAmount * 100).toFixed(0)}%`),
|
|
},
|
|
{
|
|
label: 'Desaturate', icon: <Droplet className="w-3 h-3" />,
|
|
value: desaturateAmount, setValue: setDesaturateAmount,
|
|
display: `${(desaturateAmount * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => applyMutation(desaturateMutation.mutateAsync, { colors: [color], amount: desaturateAmount }, `Desaturated ${(desaturateAmount * 100).toFixed(0)}%`),
|
|
},
|
|
{
|
|
label: 'Rotate Hue', icon: <RotateCcw className="w-3 h-3" />,
|
|
value: rotateAmount, setValue: setRotateAmount,
|
|
display: `${rotateAmount}°`,
|
|
min: -180, max: 180, step: 5,
|
|
onApply: () => applyMutation(rotateMutation.mutateAsync, { colors: [color], amount: rotateAmount }, `Rotated ${rotateAmount}°`),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{rows.map((row) => (
|
|
<div key={row.label} className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5 text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
|
{row.icon}
|
|
<span>{row.label}</span>
|
|
</div>
|
|
<span className="text-[10px] text-muted-foreground/40 font-mono tabular-nums">{row.display}</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} className={cn(actionBtn, 'shrink-0')}>
|
|
Apply
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div className="pt-3 border-t border-border/25">
|
|
<button
|
|
onClick={async () => {
|
|
try {
|
|
const result = await complementMutation.mutateAsync([color]);
|
|
if (result.colors[0]) {
|
|
onColorChange(result.colors[0].output);
|
|
toast.success('Complementary color applied');
|
|
}
|
|
} catch { toast.error('Failed'); }
|
|
}}
|
|
disabled={isLoading}
|
|
className={cn(actionBtn, 'w-full justify-center py-2')}
|
|
>
|
|
<ArrowLeftRight className="w-3 h-3" />
|
|
Complementary Color
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|