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) { 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 [lightenAmount, setLightenAmount] = useState(0);
const [darkenAmount, setDarkenAmount] = useState(0); const [darkenAmount, setDarkenAmount] = useState(0);
const [saturateAmount, setSaturateAmount] = useState(0); const [saturateAmount, setSaturateAmount] = useState(0);
@@ -34,22 +32,34 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const rotateMutation = useRotate(); const rotateMutation = useRotate();
const complementMutation = useComplement(); const complementMutation = useComplement();
// Debounce timer // Track if we're applying our own changes to prevent feedback loop
const debounceTimer = useRef<NodeJS.Timeout | undefined>(undefined); 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(() => { useEffect(() => {
setBaseColor(color); if (!isApplyingRef.current) {
baseColorRef.current = color;
// Reset sliders when color changes externally
setLightenAmount(0); setLightenAmount(0);
setDarkenAmount(0); setDarkenAmount(0);
setSaturateAmount(0); setSaturateAmount(0);
setDesaturateAmount(0); setDesaturateAmount(0);
setRotateAmount(0); setRotateAmount(0);
}
}, [color]); }, [color]);
// Apply all manipulations to base color // Debounced effect to apply manipulations
const applyManipulations = useCallback(async () => { useEffect(() => {
let currentColor = baseColor; 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 { try {
// Apply lighten // Apply lighten
@@ -107,27 +117,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
} }
} }
// Only update if color changed
if (currentColor !== baseColor) {
onColorChange(currentColor); onColorChange(currentColor);
}
} catch (error) { } catch (error) {
// Silent error during manipulation // Silent error during manipulation
} finally {
isApplyingRef.current = false;
} }
}, [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);
return () => { return () => clearTimeout(timer);
if (debounceTimer.current) clearTimeout(debounceTimer.current); }, [lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount]);
};
}, [applyManipulations]);
const handleComplement = async () => { const handleComplement = async () => {
try { try {