fix: prevent infinite loop in reactive sliders by tracking base color

Refactor manipulation panel to track a base color and apply all manipulations
sequentially from that base, rather than continuously stacking manipulations.
Sliders now reset to 0 when color changes externally (e.g., from color picker).

🤖 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:13:49 +01:00
parent 6ce16388f2
commit ad3996b3d2

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@@ -19,11 +19,13 @@ interface ManipulationPanelProps {
} }
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) { export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
const [lightenAmount, setLightenAmount] = useState(0.2); // Track base color and reset sliders when color changes externally
const [darkenAmount, setDarkenAmount] = useState(0.2); const [baseColor, setBaseColor] = useState(color);
const [saturateAmount, setSaturateAmount] = useState(0.2); const [lightenAmount, setLightenAmount] = useState(0);
const [desaturateAmount, setDesaturateAmount] = useState(0.2); const [darkenAmount, setDarkenAmount] = useState(0);
const [rotateAmount, setRotateAmount] = useState(30); const [saturateAmount, setSaturateAmount] = useState(0);
const [desaturateAmount, setDesaturateAmount] = useState(0);
const [rotateAmount, setRotateAmount] = useState(0);
const lightenMutation = useLighten(); const lightenMutation = useLighten();
const darkenMutation = useDarken(); const darkenMutation = useDarken();
@@ -32,112 +34,100 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const rotateMutation = useRotate(); const rotateMutation = useRotate();
const complementMutation = useComplement(); const complementMutation = useComplement();
// Debounce timers // Debounce timer
const lightenTimer = useRef<NodeJS.Timeout | undefined>(undefined); const debounceTimer = useRef<NodeJS.Timeout | undefined>(undefined);
const darkenTimer = useRef<NodeJS.Timeout | undefined>(undefined);
const saturateTimer = useRef<NodeJS.Timeout | undefined>(undefined);
const desaturateTimer = useRef<NodeJS.Timeout | undefined>(undefined);
const rotateTimer = useRef<NodeJS.Timeout | undefined>(undefined);
// Reactive lighten // Reset sliders when color changes from outside (e.g., color picker)
useEffect(() => { useEffect(() => {
if (lightenTimer.current) clearTimeout(lightenTimer.current); setBaseColor(color);
lightenTimer.current = setTimeout(async () => { 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) { if (lightenAmount > 0) {
try { const result = await lightenMutation.mutateAsync({
const result = await lightenMutation.mutateAsync({ colors: [currentColor],
colors: [color], amount: lightenAmount,
amount: lightenAmount, });
}); if (result.colors[0]) {
if (result.colors[0]) { currentColor = result.colors[0].output;
onColorChange(result.colors[0].output);
}
} catch (error) {
// Silent error - user is still adjusting
} }
} }
}, 300);
}, [lightenAmount]);
// Reactive darken // Apply darken
useEffect(() => {
if (darkenTimer.current) clearTimeout(darkenTimer.current);
darkenTimer.current = setTimeout(async () => {
if (darkenAmount > 0) { if (darkenAmount > 0) {
try { const result = await darkenMutation.mutateAsync({
const result = await darkenMutation.mutateAsync({ colors: [currentColor],
colors: [color], amount: darkenAmount,
amount: darkenAmount, });
}); if (result.colors[0]) {
if (result.colors[0]) { currentColor = result.colors[0].output;
onColorChange(result.colors[0].output);
}
} catch (error) {
// Silent error - user is still adjusting
} }
} }
}, 300);
}, [darkenAmount]);
// Reactive saturate // Apply saturate
useEffect(() => {
if (saturateTimer.current) clearTimeout(saturateTimer.current);
saturateTimer.current = setTimeout(async () => {
if (saturateAmount > 0) { if (saturateAmount > 0) {
try { const result = await saturateMutation.mutateAsync({
const result = await saturateMutation.mutateAsync({ colors: [currentColor],
colors: [color], amount: saturateAmount,
amount: saturateAmount, });
}); if (result.colors[0]) {
if (result.colors[0]) { currentColor = result.colors[0].output;
onColorChange(result.colors[0].output);
}
} catch (error) {
// Silent error - user is still adjusting
} }
} }
}, 300);
}, [saturateAmount]);
// Reactive desaturate // Apply desaturate
useEffect(() => {
if (desaturateTimer.current) clearTimeout(desaturateTimer.current);
desaturateTimer.current = setTimeout(async () => {
if (desaturateAmount > 0) { if (desaturateAmount > 0) {
try { const result = await desaturateMutation.mutateAsync({
const result = await desaturateMutation.mutateAsync({ colors: [currentColor],
colors: [color], amount: desaturateAmount,
amount: desaturateAmount, });
}); if (result.colors[0]) {
if (result.colors[0]) { currentColor = result.colors[0].output;
onColorChange(result.colors[0].output);
}
} catch (error) {
// Silent error - user is still adjusting
} }
} }
}, 300);
}, [desaturateAmount]);
// Reactive rotate // Apply rotate
useEffect(() => {
if (rotateTimer.current) clearTimeout(rotateTimer.current);
rotateTimer.current = setTimeout(async () => {
if (rotateAmount !== 0) { if (rotateAmount !== 0) {
try { const result = await rotateMutation.mutateAsync({
const result = await rotateMutation.mutateAsync({ colors: [currentColor],
colors: [color], amount: rotateAmount,
amount: rotateAmount, });
}); if (result.colors[0]) {
if (result.colors[0]) { currentColor = result.colors[0].output;
onColorChange(result.colors[0].output);
}
} catch (error) {
// Silent error - user is still adjusting
} }
} }
// Only update if color changed
if (currentColor !== baseColor) {
onColorChange(currentColor);
}
} catch (error) {
// Silent error during manipulation
}
}, [baseColor, lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount, lightenMutation, darkenMutation, saturateMutation, desaturateMutation, rotateMutation, onColorChange]);
// Debounced effect to apply manipulations
useEffect(() => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
debounceTimer.current = setTimeout(() => {
applyManipulations();
}, 300); }, 300);
}, [rotateAmount]);
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, [applyManipulations]);
const handleComplement = async () => { const handleComplement = async () => {
try { try {