From d09ecd17d5001b25e35f9226b8b3c9a7426889a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 22:19:51 +0100 Subject: [PATCH] fix: use ref flag to prevent feedback loop in reactive sliders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/tools/ManipulationPanel.tsx | 173 ++++++++++++------------- 1 file changed, 86 insertions(+), 87 deletions(-) diff --git a/components/tools/ManipulationPanel.tsx b/components/tools/ManipulationPanel.tsx index a3fee22..8cee83e 100644 --- a/components/tools/ManipulationPanel.tsx +++ b/components/tools/ManipulationPanel.tsx @@ -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(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 {