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>
This commit is contained in:
2025-11-17 22:19:51 +01:00
parent ad3996b3d2
commit d09ecd17d5

View File

@@ -19,8 +19,6 @@ interface ManipulationPanelProps {
}
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
// Track base color and reset sliders when color changes externally
const [baseColor, setBaseColor] = useState(color);
const [lightenAmount, setLightenAmount] = useState(0);
const [darkenAmount, setDarkenAmount] = useState(0);
const [saturateAmount, setSaturateAmount] = useState(0);
@@ -34,100 +32,101 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const rotateMutation = useRotate();
const complementMutation = useComplement();
// Debounce timer
const debounceTimer = useRef<NodeJS.Timeout | undefined>(undefined);
// Track if we're applying our own changes to prevent feedback loop
const isApplyingRef = useRef(false);
const baseColorRef = useRef(color);
// Reset sliders when color changes from outside (e.g., color picker)
// Update base color only when not applying our own changes
useEffect(() => {
setBaseColor(color);
setLightenAmount(0);
setDarkenAmount(0);
setSaturateAmount(0);
setDesaturateAmount(0);
setRotateAmount(0);
}, [color]);
// Apply all manipulations to base color
const applyManipulations = useCallback(async () => {
let currentColor = baseColor;
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;
}
}
// Only update if color changed
if (currentColor !== baseColor) {
onColorChange(currentColor);
}
} catch (error) {
// Silent error during manipulation
if (!isApplyingRef.current) {
baseColorRef.current = color;
// Reset sliders when color changes externally
setLightenAmount(0);
setDarkenAmount(0);
setSaturateAmount(0);
setDesaturateAmount(0);
setRotateAmount(0);
}
}, [baseColor, lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount, lightenMutation, darkenMutation, saturateMutation, desaturateMutation, rotateMutation, onColorChange]);
}, [color]);
// Debounced effect to apply manipulations
useEffect(() => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
const timer = setTimeout(async () => {
// Skip if all sliders are at neutral position
if (lightenAmount === 0 && darkenAmount === 0 && saturateAmount === 0 &&
desaturateAmount === 0 && rotateAmount === 0) {
return;
}
debounceTimer.current = setTimeout(() => {
applyManipulations();
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 () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, [applyManipulations]);
return () => clearTimeout(timer);
}, [lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount]);
const handleComplement = async () => {
try {