diff --git a/app/playground/page.tsx b/app/playground/page.tsx index 4b33cb2..485a8af 100644 --- a/app/playground/page.tsx +++ b/app/playground/page.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { ColorPicker } from '@/components/color/ColorPicker'; import { ColorDisplay } from '@/components/color/ColorDisplay'; import { ColorInfo } from '@/components/color/ColorInfo'; -import { Button } from '@/components/ui/button'; +import { ManipulationPanel } from '@/components/tools/ManipulationPanel'; import { useColorInfo } from '@/lib/api/queries'; import { Loader2 } from 'lucide-react'; @@ -65,27 +65,8 @@ export default function PlaygroundPage() {
-

Quick Actions

-
- - - - - - -
+

Color Manipulation

+
diff --git a/components/tools/ManipulationPanel.tsx b/components/tools/ManipulationPanel.tsx new file mode 100644 index 0000000..54f910e --- /dev/null +++ b/components/tools/ManipulationPanel.tsx @@ -0,0 +1,231 @@ +'use client'; + +import { useState } from 'react'; +import { Slider } from '@/components/ui/slider'; +import { Button } from '@/components/ui/button'; +import { + useLighten, + useDarken, + useSaturate, + useDesaturate, + useRotate, + useComplement +} from '@/lib/api/queries'; +import { toast } from 'sonner'; + +interface ManipulationPanelProps { + color: string; + onColorChange: (color: string) => void; +} + +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); + + const lightenMutation = useLighten(); + const darkenMutation = useDarken(); + const saturateMutation = useSaturate(); + const desaturateMutation = useDesaturate(); + const rotateMutation = useRotate(); + const complementMutation = useComplement(); + + const handleLighten = async () => { + try { + const result = await lightenMutation.mutateAsync({ + colors: [color], + amount: lightenAmount, + }); + if (result.results[0]) { + onColorChange(result.results[0].output); + toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`); + } + } catch (error) { + toast.error('Failed to lighten color'); + } + }; + + const handleDarken = async () => { + try { + const result = await darkenMutation.mutateAsync({ + colors: [color], + amount: darkenAmount, + }); + if (result.results[0]) { + onColorChange(result.results[0].output); + toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`); + } + } catch (error) { + toast.error('Failed to darken color'); + } + }; + + const handleSaturate = async () => { + try { + const result = await saturateMutation.mutateAsync({ + colors: [color], + amount: saturateAmount, + }); + if (result.results[0]) { + onColorChange(result.results[0].output); + toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`); + } + } catch (error) { + toast.error('Failed to saturate color'); + } + }; + + const handleDesaturate = async () => { + try { + const result = await desaturateMutation.mutateAsync({ + colors: [color], + amount: desaturateAmount, + }); + if (result.results[0]) { + onColorChange(result.results[0].output); + toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`); + } + } catch (error) { + toast.error('Failed to desaturate color'); + } + }; + + const handleRotate = async () => { + try { + const result = await rotateMutation.mutateAsync({ + colors: [color], + amount: rotateAmount, + }); + if (result.results[0]) { + onColorChange(result.results[0].output); + toast.success(`Rotated hue by ${rotateAmount}°`); + } + } catch (error) { + toast.error('Failed to rotate hue'); + } + }; + + const handleComplement = async () => { + try { + const result = await complementMutation.mutateAsync([color]); + if (result.results[0]) { + onColorChange(result.results[0].output); + toast.success('Generated complementary color'); + } + } catch (error) { + toast.error('Failed to generate complement'); + } + }; + + const isLoading = + lightenMutation.isPending || + darkenMutation.isPending || + saturateMutation.isPending || + desaturateMutation.isPending || + rotateMutation.isPending || + complementMutation.isPending; + + return ( +
+ {/* Lighten */} +
+ setLightenAmount(parseFloat(e.target.value))} + suffix="%" + showValue + /> + +
+ + {/* Darken */} +
+ setDarkenAmount(parseFloat(e.target.value))} + suffix="%" + showValue + /> + +
+ + {/* Saturate */} +
+ setSaturateAmount(parseFloat(e.target.value))} + suffix="%" + showValue + /> + +
+ + {/* Desaturate */} +
+ setDesaturateAmount(parseFloat(e.target.value))} + suffix="%" + showValue + /> + +
+ + {/* Rotate Hue */} +
+ setRotateAmount(parseInt(e.target.value))} + suffix="°" + showValue + /> + +
+ + {/* Quick Actions */} +
+

Quick Actions

+ +
+
+ ); +} diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx new file mode 100644 index 0000000..f2da193 --- /dev/null +++ b/components/ui/slider.tsx @@ -0,0 +1,65 @@ +'use client'; + +import * as React from 'react'; +import { cn } from '@/lib/utils/cn'; + +export interface SliderProps extends Omit, 'type'> { + label?: string; + showValue?: boolean; + suffix?: string; +} + +const Slider = React.forwardRef( + ({ className, label, showValue = true, suffix = '', ...props }, ref) => { + const [value, setValue] = React.useState(props.value || props.defaultValue || 0); + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + props.onChange?.(e); + }; + + return ( +
+ {(label || showValue) && ( +
+ {label && ( + + )} + {showValue && ( + + {value} + {suffix} + + )} +
+ )} + +
+ ); + } +); + +Slider.displayName = 'Slider'; + +export { Slider };