Files
pastel-ui/components/tools/ManipulationPanel.tsx
Sebastian Krüger d09ecd17d5 fix: use ref flag to prevent feedback loop in reactive sliders
Track when applying our own changes via isApplyingRef to prevent the color
update effect from resetting sliders when we trigger the change ourselves.
This properly breaks the infinite loop while maintaining reactive behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:51 +01:00

238 lines
6.3 KiB
TypeScript

'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { Slider } from '@/components/ui/slider';
import { Button } from '@/components/ui/button';
import {
useLighten,
useDarken,
useSaturate,
useDesaturate,
useRotate,
useComplement
} from '@/lib/api/queries';
import { toast } from 'sonner';
interface ManipulationPanelProps {
color: string;
onColorChange: (color: string) => void;
}
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
const [lightenAmount, setLightenAmount] = useState(0);
const [darkenAmount, setDarkenAmount] = useState(0);
const [saturateAmount, setSaturateAmount] = useState(0);
const [desaturateAmount, setDesaturateAmount] = useState(0);
const [rotateAmount, setRotateAmount] = useState(0);
const lightenMutation = useLighten();
const darkenMutation = useDarken();
const saturateMutation = useSaturate();
const desaturateMutation = useDesaturate();
const rotateMutation = useRotate();
const complementMutation = useComplement();
// Track if we're applying our own changes to prevent feedback loop
const isApplyingRef = useRef(false);
const baseColorRef = useRef(color);
// Update base color only when not applying our own changes
useEffect(() => {
if (!isApplyingRef.current) {
baseColorRef.current = color;
// Reset sliders when color changes externally
setLightenAmount(0);
setDarkenAmount(0);
setSaturateAmount(0);
setDesaturateAmount(0);
setRotateAmount(0);
}
}, [color]);
// Debounced effect to apply manipulations
useEffect(() => {
const timer = setTimeout(async () => {
// Skip if all sliders are at neutral position
if (lightenAmount === 0 && darkenAmount === 0 && saturateAmount === 0 &&
desaturateAmount === 0 && rotateAmount === 0) {
return;
}
isApplyingRef.current = true;
let currentColor = baseColorRef.current;
try {
// Apply lighten
if (lightenAmount > 0) {
const result = await lightenMutation.mutateAsync({
colors: [currentColor],
amount: lightenAmount,
});
if (result.colors[0]) {
currentColor = result.colors[0].output;
}
}
// Apply darken
if (darkenAmount > 0) {
const result = await darkenMutation.mutateAsync({
colors: [currentColor],
amount: darkenAmount,
});
if (result.colors[0]) {
currentColor = result.colors[0].output;
}
}
// Apply saturate
if (saturateAmount > 0) {
const result = await saturateMutation.mutateAsync({
colors: [currentColor],
amount: saturateAmount,
});
if (result.colors[0]) {
currentColor = result.colors[0].output;
}
}
// Apply desaturate
if (desaturateAmount > 0) {
const result = await desaturateMutation.mutateAsync({
colors: [currentColor],
amount: desaturateAmount,
});
if (result.colors[0]) {
currentColor = result.colors[0].output;
}
}
// Apply rotate
if (rotateAmount !== 0) {
const result = await rotateMutation.mutateAsync({
colors: [currentColor],
amount: rotateAmount,
});
if (result.colors[0]) {
currentColor = result.colors[0].output;
}
}
onColorChange(currentColor);
} catch (error) {
// Silent error during manipulation
} finally {
isApplyingRef.current = false;
}
}, 300);
return () => clearTimeout(timer);
}, [lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount]);
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 ||
saturateMutation.isPending ||
desaturateMutation.isPending ||
rotateMutation.isPending ||
complementMutation.isPending;
return (
<div className="space-y-6">
{/* Lighten */}
<div>
<Slider
label="Lighten"
min={0}
max={1}
step={0.05}
value={lightenAmount}
onChange={(e) => setLightenAmount(parseFloat(e.target.value))}
suffix="%"
showValue
/>
</div>
{/* Darken */}
<div>
<Slider
label="Darken"
min={0}
max={1}
step={0.05}
value={darkenAmount}
onChange={(e) => setDarkenAmount(parseFloat(e.target.value))}
suffix="%"
showValue
/>
</div>
{/* Saturate */}
<div>
<Slider
label="Saturate"
min={0}
max={1}
step={0.05}
value={saturateAmount}
onChange={(e) => setSaturateAmount(parseFloat(e.target.value))}
suffix="%"
showValue
/>
</div>
{/* Desaturate */}
<div>
<Slider
label="Desaturate"
min={0}
max={1}
step={0.05}
value={desaturateAmount}
onChange={(e) => setDesaturateAmount(parseFloat(e.target.value))}
suffix="%"
showValue
/>
</div>
{/* Rotate Hue */}
<div>
<Slider
label="Rotate Hue"
min={-180}
max={180}
step={5}
value={rotateAmount}
onChange={(e) => setRotateAmount(parseInt(e.target.value))}
suffix="°"
showValue
/>
</div>
{/* Quick Actions */}
<div className="pt-4 border-t space-y-2">
<h3 className="text-sm font-medium mb-3">Quick Actions</h3>
<Button
onClick={handleComplement}
disabled={isLoading}
variant="outline"
className="w-full"
>
Get Complementary Color
</Button>
</div>
</div>
);
}