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:
@@ -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,80 +32,112 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
const rotateMutation = useRotate();
|
const rotateMutation = useRotate();
|
||||||
const complementMutation = useComplement();
|
const complementMutation = useComplement();
|
||||||
|
|
||||||
const handleLighten = async () => {
|
// Debounce timers
|
||||||
try {
|
const lightenTimer = useRef<NodeJS.Timeout>();
|
||||||
const result = await lightenMutation.mutateAsync({
|
const darkenTimer = useRef<NodeJS.Timeout>();
|
||||||
colors: [color],
|
const saturateTimer = useRef<NodeJS.Timeout>();
|
||||||
amount: lightenAmount,
|
const desaturateTimer = useRef<NodeJS.Timeout>();
|
||||||
});
|
const rotateTimer = useRef<NodeJS.Timeout>();
|
||||||
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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDarken = async () => {
|
// Reactive lighten
|
||||||
try {
|
useEffect(() => {
|
||||||
const result = await darkenMutation.mutateAsync({
|
if (lightenTimer.current) clearTimeout(lightenTimer.current);
|
||||||
colors: [color],
|
lightenTimer.current = setTimeout(async () => {
|
||||||
amount: darkenAmount,
|
if (lightenAmount > 0) {
|
||||||
});
|
try {
|
||||||
if (result.colors[0]) {
|
const result = await lightenMutation.mutateAsync({
|
||||||
onColorChange(result.colors[0].output);
|
colors: [color],
|
||||||
toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`);
|
amount: lightenAmount,
|
||||||
|
});
|
||||||
|
if (result.colors[0]) {
|
||||||
|
onColorChange(result.colors[0].output);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silent error - user is still adjusting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, 300);
|
||||||
toast.error('Failed to darken color');
|
}, [lightenAmount]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaturate = async () => {
|
// Reactive darken
|
||||||
try {
|
useEffect(() => {
|
||||||
const result = await saturateMutation.mutateAsync({
|
if (darkenTimer.current) clearTimeout(darkenTimer.current);
|
||||||
colors: [color],
|
darkenTimer.current = setTimeout(async () => {
|
||||||
amount: saturateAmount,
|
if (darkenAmount > 0) {
|
||||||
});
|
try {
|
||||||
if (result.colors[0]) {
|
const result = await darkenMutation.mutateAsync({
|
||||||
onColorChange(result.colors[0].output);
|
colors: [color],
|
||||||
toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`);
|
amount: darkenAmount,
|
||||||
|
});
|
||||||
|
if (result.colors[0]) {
|
||||||
|
onColorChange(result.colors[0].output);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silent error - user is still adjusting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, 300);
|
||||||
toast.error('Failed to saturate color');
|
}, [darkenAmount]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDesaturate = async () => {
|
// Reactive saturate
|
||||||
try {
|
useEffect(() => {
|
||||||
const result = await desaturateMutation.mutateAsync({
|
if (saturateTimer.current) clearTimeout(saturateTimer.current);
|
||||||
colors: [color],
|
saturateTimer.current = setTimeout(async () => {
|
||||||
amount: desaturateAmount,
|
if (saturateAmount > 0) {
|
||||||
});
|
try {
|
||||||
if (result.colors[0]) {
|
const result = await saturateMutation.mutateAsync({
|
||||||
onColorChange(result.colors[0].output);
|
colors: [color],
|
||||||
toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`);
|
amount: saturateAmount,
|
||||||
|
});
|
||||||
|
if (result.colors[0]) {
|
||||||
|
onColorChange(result.colors[0].output);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silent error - user is still adjusting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, 300);
|
||||||
toast.error('Failed to desaturate color');
|
}, [saturateAmount]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRotate = async () => {
|
// Reactive desaturate
|
||||||
try {
|
useEffect(() => {
|
||||||
const result = await rotateMutation.mutateAsync({
|
if (desaturateTimer.current) clearTimeout(desaturateTimer.current);
|
||||||
colors: [color],
|
desaturateTimer.current = setTimeout(async () => {
|
||||||
amount: rotateAmount,
|
if (desaturateAmount > 0) {
|
||||||
});
|
try {
|
||||||
if (result.colors[0]) {
|
const result = await desaturateMutation.mutateAsync({
|
||||||
onColorChange(result.colors[0].output);
|
colors: [color],
|
||||||
toast.success(`Rotated hue by ${rotateAmount}°`);
|
amount: desaturateAmount,
|
||||||
|
});
|
||||||
|
if (result.colors[0]) {
|
||||||
|
onColorChange(result.colors[0].output);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silent error - user is still adjusting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, 300);
|
||||||
toast.error('Failed to rotate hue');
|
}, [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 () => {
|
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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user