feat: make color manipulation sliders reactive with auto-apply

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-17 22:02:57 +01:00
parent 61d2276fad
commit 072851a762

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect, useRef } 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 {
@@ -32,7 +32,18 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const rotateMutation = useRotate(); const rotateMutation = useRotate();
const complementMutation = useComplement(); const complementMutation = useComplement();
const handleLighten = async () => { // Debounce timers
const lightenTimer = useRef<NodeJS.Timeout>();
const darkenTimer = useRef<NodeJS.Timeout>();
const saturateTimer = useRef<NodeJS.Timeout>();
const desaturateTimer = useRef<NodeJS.Timeout>();
const rotateTimer = useRef<NodeJS.Timeout>();
// Reactive lighten
useEffect(() => {
if (lightenTimer.current) clearTimeout(lightenTimer.current);
lightenTimer.current = setTimeout(async () => {
if (lightenAmount > 0) {
try { try {
const result = await lightenMutation.mutateAsync({ const result = await lightenMutation.mutateAsync({
colors: [color], colors: [color],
@@ -40,14 +51,19 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
}); });
if (result.colors[0]) { if (result.colors[0]) {
onColorChange(result.colors[0].output); onColorChange(result.colors[0].output);
toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`);
} }
} catch (error) { } catch (error) {
toast.error('Failed to lighten color'); // Silent error - user is still adjusting
} }
}; }
}, 300);
}, [lightenAmount]);
const handleDarken = async () => { // Reactive darken
useEffect(() => {
if (darkenTimer.current) clearTimeout(darkenTimer.current);
darkenTimer.current = setTimeout(async () => {
if (darkenAmount > 0) {
try { try {
const result = await darkenMutation.mutateAsync({ const result = await darkenMutation.mutateAsync({
colors: [color], colors: [color],
@@ -55,14 +71,19 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
}); });
if (result.colors[0]) { if (result.colors[0]) {
onColorChange(result.colors[0].output); onColorChange(result.colors[0].output);
toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`);
} }
} catch (error) { } catch (error) {
toast.error('Failed to darken color'); // Silent error - user is still adjusting
} }
}; }
}, 300);
}, [darkenAmount]);
const handleSaturate = async () => { // Reactive saturate
useEffect(() => {
if (saturateTimer.current) clearTimeout(saturateTimer.current);
saturateTimer.current = setTimeout(async () => {
if (saturateAmount > 0) {
try { try {
const result = await saturateMutation.mutateAsync({ const result = await saturateMutation.mutateAsync({
colors: [color], colors: [color],
@@ -70,14 +91,19 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
}); });
if (result.colors[0]) { if (result.colors[0]) {
onColorChange(result.colors[0].output); onColorChange(result.colors[0].output);
toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`);
} }
} catch (error) { } catch (error) {
toast.error('Failed to saturate color'); // Silent error - user is still adjusting
} }
}; }
}, 300);
}, [saturateAmount]);
const handleDesaturate = async () => { // Reactive desaturate
useEffect(() => {
if (desaturateTimer.current) clearTimeout(desaturateTimer.current);
desaturateTimer.current = setTimeout(async () => {
if (desaturateAmount > 0) {
try { try {
const result = await desaturateMutation.mutateAsync({ const result = await desaturateMutation.mutateAsync({
colors: [color], colors: [color],
@@ -85,14 +111,19 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
}); });
if (result.colors[0]) { if (result.colors[0]) {
onColorChange(result.colors[0].output); onColorChange(result.colors[0].output);
toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`);
} }
} catch (error) { } catch (error) {
toast.error('Failed to desaturate color'); // Silent error - user is still adjusting
} }
}; }
}, 300);
}, [desaturateAmount]);
const handleRotate = async () => { // Reactive rotate
useEffect(() => {
if (rotateTimer.current) clearTimeout(rotateTimer.current);
rotateTimer.current = setTimeout(async () => {
if (rotateAmount !== 0) {
try { try {
const result = await rotateMutation.mutateAsync({ const result = await rotateMutation.mutateAsync({
colors: [color], colors: [color],
@@ -100,12 +131,13 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
}); });
if (result.colors[0]) { if (result.colors[0]) {
onColorChange(result.colors[0].output); onColorChange(result.colors[0].output);
toast.success(`Rotated hue by ${rotateAmount}°`);
} }
} catch (error) { } catch (error) {
toast.error('Failed to rotate hue'); // Silent error - user is still adjusting
} }
}; }
}, 300);
}, [rotateAmount]);
const handleComplement = async () => { const handleComplement = async () => {
try { try {
@@ -130,7 +162,7 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Lighten */} {/* Lighten */}
<div className="space-y-3"> <div>
<Slider <Slider
label="Lighten" label="Lighten"
min={0} min={0}
@@ -141,13 +173,10 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
suffix="%" suffix="%"
showValue showValue
/> />
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
Apply Lighten
</Button>
</div> </div>
{/* Darken */} {/* Darken */}
<div className="space-y-3"> <div>
<Slider <Slider
label="Darken" label="Darken"
min={0} min={0}
@@ -158,13 +187,10 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
suffix="%" suffix="%"
showValue showValue
/> />
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
Apply Darken
</Button>
</div> </div>
{/* Saturate */} {/* Saturate */}
<div className="space-y-3"> <div>
<Slider <Slider
label="Saturate" label="Saturate"
min={0} min={0}
@@ -175,13 +201,10 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
suffix="%" suffix="%"
showValue showValue
/> />
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
Apply Saturate
</Button>
</div> </div>
{/* Desaturate */} {/* Desaturate */}
<div className="space-y-3"> <div>
<Slider <Slider
label="Desaturate" label="Desaturate"
min={0} min={0}
@@ -192,13 +215,10 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
suffix="%" suffix="%"
showValue showValue
/> />
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
Apply Desaturate
</Button>
</div> </div>
{/* Rotate Hue */} {/* Rotate Hue */}
<div className="space-y-3"> <div>
<Slider <Slider
label="Rotate Hue" label="Rotate Hue"
min={-180} min={-180}
@@ -209,9 +229,6 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
suffix="°" suffix="°"
showValue showValue
/> />
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
Apply Rotation
</Button>
</div> </div>
{/* Quick Actions */} {/* Quick Actions */}