From ad3996b3d2a6dc7ad6c8d243552203f69eb6117a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 22:13:49 +0100 Subject: [PATCH] fix: prevent infinite loop in reactive sliders by tracking base color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/tools/ManipulationPanel.tsx | 170 ++++++++++++------------- 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/components/tools/ManipulationPanel.tsx b/components/tools/ManipulationPanel.tsx index 52b41af..a3fee22 100644 --- a/components/tools/ManipulationPanel.tsx +++ b/components/tools/ManipulationPanel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { Slider } from '@/components/ui/slider'; import { Button } from '@/components/ui/button'; import { @@ -19,11 +19,13 @@ interface ManipulationPanelProps { } export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) { - const [lightenAmount, setLightenAmount] = useState(0.2); - const [darkenAmount, setDarkenAmount] = useState(0.2); - const [saturateAmount, setSaturateAmount] = useState(0.2); - const [desaturateAmount, setDesaturateAmount] = useState(0.2); - const [rotateAmount, setRotateAmount] = useState(30); + // 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); + const [desaturateAmount, setDesaturateAmount] = useState(0); + const [rotateAmount, setRotateAmount] = useState(0); const lightenMutation = useLighten(); const darkenMutation = useDarken(); @@ -32,112 +34,100 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro const rotateMutation = useRotate(); const complementMutation = useComplement(); - // Debounce timers - const lightenTimer = useRef(undefined); - const darkenTimer = useRef(undefined); - const saturateTimer = useRef(undefined); - const desaturateTimer = useRef(undefined); - const rotateTimer = useRef(undefined); + // Debounce timer + const debounceTimer = useRef(undefined); - // Reactive lighten + // Reset sliders when color changes from outside (e.g., color picker) useEffect(() => { - if (lightenTimer.current) clearTimeout(lightenTimer.current); - lightenTimer.current = setTimeout(async () => { + 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) { - try { - const result = await lightenMutation.mutateAsync({ - colors: [color], - amount: lightenAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - } - } catch (error) { - // Silent error - user is still adjusting + const result = await lightenMutation.mutateAsync({ + colors: [currentColor], + amount: lightenAmount, + }); + if (result.colors[0]) { + currentColor = result.colors[0].output; } } - }, 300); - }, [lightenAmount]); - // Reactive darken - useEffect(() => { - if (darkenTimer.current) clearTimeout(darkenTimer.current); - darkenTimer.current = setTimeout(async () => { + // Apply darken if (darkenAmount > 0) { - try { - const result = await darkenMutation.mutateAsync({ - colors: [color], - amount: darkenAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - } - } catch (error) { - // Silent error - user is still adjusting + const result = await darkenMutation.mutateAsync({ + colors: [currentColor], + amount: darkenAmount, + }); + if (result.colors[0]) { + currentColor = result.colors[0].output; } } - }, 300); - }, [darkenAmount]); - // Reactive saturate - useEffect(() => { - if (saturateTimer.current) clearTimeout(saturateTimer.current); - saturateTimer.current = setTimeout(async () => { + // Apply saturate if (saturateAmount > 0) { - try { - const result = await saturateMutation.mutateAsync({ - colors: [color], - amount: saturateAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - } - } catch (error) { - // Silent error - user is still adjusting + const result = await saturateMutation.mutateAsync({ + colors: [currentColor], + amount: saturateAmount, + }); + if (result.colors[0]) { + currentColor = result.colors[0].output; } } - }, 300); - }, [saturateAmount]); - // Reactive desaturate - useEffect(() => { - if (desaturateTimer.current) clearTimeout(desaturateTimer.current); - desaturateTimer.current = setTimeout(async () => { + // Apply desaturate if (desaturateAmount > 0) { - try { - const result = await desaturateMutation.mutateAsync({ - colors: [color], - amount: desaturateAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - } - } catch (error) { - // Silent error - user is still adjusting + const result = await desaturateMutation.mutateAsync({ + colors: [currentColor], + amount: desaturateAmount, + }); + if (result.colors[0]) { + currentColor = result.colors[0].output; } } - }, 300); - }, [desaturateAmount]); - // Reactive rotate - useEffect(() => { - if (rotateTimer.current) clearTimeout(rotateTimer.current); - rotateTimer.current = setTimeout(async () => { + // Apply rotate if (rotateAmount !== 0) { - try { - const result = await rotateMutation.mutateAsync({ - colors: [color], - amount: rotateAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - } - } catch (error) { - // Silent error - user is still adjusting + 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 + } + }, [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); - }, [rotateAmount]); + + return () => { + if (debounceTimer.current) clearTimeout(debounceTimer.current); + }; + }, [applyManipulations]); const handleComplement = async () => { try {