From 072851a762abacd23ca48cbbb4adc7c09101c4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 22:02:57 +0100 Subject: [PATCH] feat: make color manipulation sliders reactive with auto-apply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove redundant "Apply" buttons and make all manipulation sliders (lighten, darken, saturate, desaturate, rotate) update the color in real-time with 300ms debouncing. This provides a smoother, more intuitive user experience. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/tools/ManipulationPanel.tsx | 191 ++++++++++++++----------- 1 file changed, 104 insertions(+), 87 deletions(-) diff --git a/components/tools/ManipulationPanel.tsx b/components/tools/ManipulationPanel.tsx index 6ae7344..c9e7def 100644 --- a/components/tools/ManipulationPanel.tsx +++ b/components/tools/ManipulationPanel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Slider } from '@/components/ui/slider'; import { Button } from '@/components/ui/button'; import { @@ -32,80 +32,112 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro const rotateMutation = useRotate(); const complementMutation = useComplement(); - const handleLighten = async () => { - try { - const result = await lightenMutation.mutateAsync({ - colors: [color], - amount: lightenAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`); - } - } catch (error) { - toast.error('Failed to lighten color'); - } - }; + // Debounce timers + const lightenTimer = useRef(); + const darkenTimer = useRef(); + const saturateTimer = useRef(); + const desaturateTimer = useRef(); + const rotateTimer = useRef(); - const handleDarken = async () => { - try { - const result = await darkenMutation.mutateAsync({ - colors: [color], - amount: darkenAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`); + // Reactive lighten + useEffect(() => { + if (lightenTimer.current) clearTimeout(lightenTimer.current); + lightenTimer.current = setTimeout(async () => { + 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 + } } - } catch (error) { - toast.error('Failed to darken color'); - } - }; + }, 300); + }, [lightenAmount]); - const handleSaturate = async () => { - try { - const result = await saturateMutation.mutateAsync({ - colors: [color], - amount: saturateAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`); + // Reactive darken + useEffect(() => { + if (darkenTimer.current) clearTimeout(darkenTimer.current); + darkenTimer.current = setTimeout(async () => { + 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 + } } - } catch (error) { - toast.error('Failed to saturate color'); - } - }; + }, 300); + }, [darkenAmount]); - const handleDesaturate = async () => { - try { - const result = await desaturateMutation.mutateAsync({ - colors: [color], - amount: desaturateAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`); + // Reactive saturate + useEffect(() => { + if (saturateTimer.current) clearTimeout(saturateTimer.current); + saturateTimer.current = setTimeout(async () => { + 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 + } } - } catch (error) { - toast.error('Failed to desaturate color'); - } - }; + }, 300); + }, [saturateAmount]); - const handleRotate = async () => { - try { - const result = await rotateMutation.mutateAsync({ - colors: [color], - amount: rotateAmount, - }); - if (result.colors[0]) { - onColorChange(result.colors[0].output); - toast.success(`Rotated hue by ${rotateAmount}°`); + // Reactive desaturate + useEffect(() => { + if (desaturateTimer.current) clearTimeout(desaturateTimer.current); + desaturateTimer.current = setTimeout(async () => { + 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 + } } - } catch (error) { - toast.error('Failed to rotate hue'); - } - }; + }, 300); + }, [desaturateAmount]); + + // Reactive rotate + useEffect(() => { + if (rotateTimer.current) clearTimeout(rotateTimer.current); + rotateTimer.current = setTimeout(async () => { + 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 + } + } + }, 300); + }, [rotateAmount]); const handleComplement = async () => { try { @@ -130,7 +162,7 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro return (
{/* Lighten */} -
+
-
{/* Darken */} -
+
-
{/* Saturate */} -
+
-
{/* Desaturate */} -
+
-
{/* Rotate Hue */} -
+
-
{/* Quick Actions */}