refactor: streamline, refine and polish
This commit is contained in:
@@ -48,48 +48,43 @@ export function ColorInfo({ info, className }: ColorInfoProps) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{formats.map((format) => (
|
||||
<div
|
||||
key={format.label}
|
||||
className="flex items-center justify-between p-3 bg-muted rounded-lg"
|
||||
className="flex items-center justify-between px-3 py-2 bg-muted/50 rounded-md group"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted-foreground mb-1">{format.label}</div>
|
||||
<div className="font-mono text-sm">{format.value}</div>
|
||||
<div className="flex items-baseline gap-2 min-w-0 flex-1">
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground w-10 shrink-0">{format.label}</span>
|
||||
<span className="font-mono text-xs truncate">{format.value}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(format.value, format.label)}
|
||||
aria-label={`Copy ${format.label} value`}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 pt-2 border-t">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Brightness</div>
|
||||
<div className="text-sm font-medium">{(info.brightness * 100).toFixed(1)}%</div>
|
||||
<div className="grid grid-cols-3 gap-3 pt-2 border-t text-xs">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">Brightness</div>
|
||||
<div className="font-medium">{(info.brightness * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Luminance</div>
|
||||
<div className="text-sm font-medium">{(info.luminance * 100).toFixed(1)}%</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">Luminance</div>
|
||||
<div className="font-medium">{(info.luminance * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Type</div>
|
||||
<div className="text-sm font-medium">{info.is_light ? 'Light' : 'Dark'}</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">{info.name && typeof info.name === 'string' ? 'Name' : 'Type'}</div>
|
||||
<div className="font-medium">{info.name && typeof info.name === 'string' ? info.name : (info.is_light ? 'Light' : 'Dark')}</div>
|
||||
</div>
|
||||
{info.name && typeof info.name === 'string' && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Named</div>
|
||||
<div className="text-sm font-medium">{info.name}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { hexToRgb } from '@/lib/color/utils/color';
|
||||
|
||||
@@ -29,20 +30,20 @@ export function ColorPicker({ color, onChange, className }: ColorPickerProps) {
|
||||
const textColor = getContrastColor(color);
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center space-y-4', className)}>
|
||||
<div className="w-full max-w-[200px] space-y-4">
|
||||
<div className={cn('flex flex-col items-center justify-center space-y-3', className)}>
|
||||
<div className="w-full max-w-[200px] space-y-3">
|
||||
<HexColorPicker color={color} onChange={onChange} className="!w-full" />
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="color-input" className="text-sm font-medium">
|
||||
Color Value
|
||||
</label>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="color-input" className="text-xs">
|
||||
Hex Value
|
||||
</Label>
|
||||
<Input
|
||||
id="color-input"
|
||||
type="text"
|
||||
value={color}
|
||||
onChange={handleInputChange}
|
||||
placeholder="#ff0099 or rgb(255, 0, 153)"
|
||||
className="font-mono transition-colors duration-200"
|
||||
placeholder="#ff0099"
|
||||
className="font-mono text-xs transition-colors duration-200"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
color: textColor,
|
||||
|
||||
@@ -118,76 +118,70 @@ export function ExportMenu({ colors, className }: ExportMenuProps) {
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Export Format</h3>
|
||||
<Select
|
||||
value={format}
|
||||
onValueChange={(value) => setFormat(value as ExportFormat)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="css">CSS Variables</SelectItem>
|
||||
<SelectItem value="scss">SCSS Variables</SelectItem>
|
||||
<SelectItem value="tailwind">Tailwind Config</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
<SelectItem value="javascript">JavaScript Array</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<Select
|
||||
value={format}
|
||||
onValueChange={(value) => setFormat(value as ExportFormat)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="css">CSS Variables</SelectItem>
|
||||
<SelectItem value="scss">SCSS Variables</SelectItem>
|
||||
<SelectItem value="tailwind">Tailwind Config</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
<SelectItem value="javascript">JavaScript Array</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Color Space</h3>
|
||||
<Select
|
||||
value={colorSpace}
|
||||
onValueChange={(value) => setColorSpace(value as ColorSpace)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select space" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hex">Hex</SelectItem>
|
||||
<SelectItem value="rgb">RGB</SelectItem>
|
||||
<SelectItem value="hsl">HSL</SelectItem>
|
||||
<SelectItem value="lab">Lab</SelectItem>
|
||||
<SelectItem value="oklab">OkLab</SelectItem>
|
||||
<SelectItem value="lch">LCH</SelectItem>
|
||||
<SelectItem value="oklch">OkLCH</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
value={colorSpace}
|
||||
onValueChange={(value) => setColorSpace(value as ColorSpace)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Space" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hex">Hex</SelectItem>
|
||||
<SelectItem value="rgb">RGB</SelectItem>
|
||||
<SelectItem value="hsl">HSL</SelectItem>
|
||||
<SelectItem value="lab">Lab</SelectItem>
|
||||
<SelectItem value="oklab">OkLab</SelectItem>
|
||||
<SelectItem value="lch">LCH</SelectItem>
|
||||
<SelectItem value="oklch">OkLCH</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-muted rounded-lg relative min-h-[100px]">
|
||||
<div className="p-3 bg-muted/50 rounded-lg relative min-h-[80px]">
|
||||
{isConverting ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm rounded-lg z-10">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : null}
|
||||
<pre className="text-xs overflow-x-auto">
|
||||
<pre className="text-[11px] overflow-x-auto leading-relaxed">
|
||||
<code>{getExportContent()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-col md:flex-row">
|
||||
<Button onClick={handleCopy} variant="outline" className="w-full md:flex-1" disabled={isConverting}>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleCopy} variant="outline" size="sm" className="flex-1" disabled={isConverting}>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Copied!
|
||||
<Check className="h-3.5 w-3.5 mr-1.5" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
<Copy className="h-3.5 w-3.5 mr-1.5" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={handleDownload} variant="default" className="w-full md:flex-1" disabled={isConverting}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
<Button onClick={handleDownload} variant="default" size="sm" className="flex-1" disabled={isConverting}>
|
||||
<Download className="h-3.5 w-3.5 mr-1.5" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,25 @@ import {
|
||||
useComplement
|
||||
} from '@/lib/color/api/queries';
|
||||
import { toast } from 'sonner';
|
||||
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
|
||||
|
||||
interface ManipulationPanelProps {
|
||||
color: string;
|
||||
onColorChange: (color: string) => void;
|
||||
}
|
||||
|
||||
interface ManipulationRow {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
value: number;
|
||||
setValue: (v: number) => void;
|
||||
format: (v: number) => string;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
onApply: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
|
||||
const [lightenAmount, setLightenAmount] = useState(0.2);
|
||||
const [darkenAmount, setDarkenAmount] = useState(0.2);
|
||||
@@ -32,93 +45,6 @@ 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');
|
||||
}
|
||||
};
|
||||
|
||||
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)}%`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to darken color');
|
||||
}
|
||||
};
|
||||
|
||||
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)}%`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to saturate color');
|
||||
}
|
||||
};
|
||||
|
||||
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)}%`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to desaturate color');
|
||||
}
|
||||
};
|
||||
|
||||
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}°`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to rotate hue');
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplement = async () => {
|
||||
try {
|
||||
const result = await complementMutation.mutateAsync([color]);
|
||||
if (result.colors[0]) {
|
||||
onColorChange(result.colors[0].output);
|
||||
toast.success('Generated complementary color');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to generate complement');
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
lightenMutation.isPending ||
|
||||
darkenMutation.isPending ||
|
||||
@@ -127,108 +53,151 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
||||
rotateMutation.isPending ||
|
||||
complementMutation.isPending;
|
||||
|
||||
const handleMutation = async (
|
||||
mutationFn: (params: any) => Promise<any>,
|
||||
params: any,
|
||||
successMsg: string,
|
||||
errorMsg: string
|
||||
) => {
|
||||
try {
|
||||
const result = await mutationFn(params);
|
||||
if (result.colors[0]) {
|
||||
onColorChange(result.colors[0].output);
|
||||
toast.success(successMsg);
|
||||
}
|
||||
} catch {
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const rows: ManipulationRow[] = [
|
||||
{
|
||||
label: 'Lighten',
|
||||
icon: <Sun className="h-3.5 w-3.5" />,
|
||||
value: lightenAmount,
|
||||
setValue: setLightenAmount,
|
||||
format: (v) => `${(v * 100).toFixed(0)}%`,
|
||||
min: 0, max: 1, step: 0.05,
|
||||
onApply: () => handleMutation(
|
||||
lightenMutation.mutateAsync,
|
||||
{ colors: [color], amount: lightenAmount },
|
||||
`Lightened by ${(lightenAmount * 100).toFixed(0)}%`,
|
||||
'Failed to lighten color'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Darken',
|
||||
icon: <Moon className="h-3.5 w-3.5" />,
|
||||
value: darkenAmount,
|
||||
setValue: setDarkenAmount,
|
||||
format: (v) => `${(v * 100).toFixed(0)}%`,
|
||||
min: 0, max: 1, step: 0.05,
|
||||
onApply: () => handleMutation(
|
||||
darkenMutation.mutateAsync,
|
||||
{ colors: [color], amount: darkenAmount },
|
||||
`Darkened by ${(darkenAmount * 100).toFixed(0)}%`,
|
||||
'Failed to darken color'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Saturate',
|
||||
icon: <Droplets className="h-3.5 w-3.5" />,
|
||||
value: saturateAmount,
|
||||
setValue: setSaturateAmount,
|
||||
format: (v) => `${(v * 100).toFixed(0)}%`,
|
||||
min: 0, max: 1, step: 0.05,
|
||||
onApply: () => handleMutation(
|
||||
saturateMutation.mutateAsync,
|
||||
{ colors: [color], amount: saturateAmount },
|
||||
`Saturated by ${(saturateAmount * 100).toFixed(0)}%`,
|
||||
'Failed to saturate color'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Desaturate',
|
||||
icon: <Droplet className="h-3.5 w-3.5" />,
|
||||
value: desaturateAmount,
|
||||
setValue: setDesaturateAmount,
|
||||
format: (v) => `${(v * 100).toFixed(0)}%`,
|
||||
min: 0, max: 1, step: 0.05,
|
||||
onApply: () => handleMutation(
|
||||
desaturateMutation.mutateAsync,
|
||||
{ colors: [color], amount: desaturateAmount },
|
||||
`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`,
|
||||
'Failed to desaturate color'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Rotate',
|
||||
icon: <RotateCcw className="h-3.5 w-3.5" />,
|
||||
value: rotateAmount,
|
||||
setValue: setRotateAmount,
|
||||
format: (v) => `${v}°`,
|
||||
min: -180, max: 180, step: 5,
|
||||
onApply: () => handleMutation(
|
||||
rotateMutation.mutateAsync,
|
||||
{ colors: [color], amount: rotateAmount },
|
||||
`Rotated hue by ${rotateAmount}°`,
|
||||
'Failed to rotate hue'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleComplement = async () => {
|
||||
try {
|
||||
const result = await complementMutation.mutateAsync([color]);
|
||||
if (result.colors[0]) {
|
||||
onColorChange(result.colors[0].output);
|
||||
toast.success('Generated complementary color');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Failed to generate complement');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Lighten */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Lighten</label>
|
||||
<span className="text-xs text-muted-foreground">{(lightenAmount * 100).toFixed(0)}%</span>
|
||||
<div className="space-y-4">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium">
|
||||
{row.icon}
|
||||
<span>{row.label}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">{row.format(row.value)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Slider
|
||||
min={row.min}
|
||||
max={row.max}
|
||||
step={row.step}
|
||||
value={[row.value]}
|
||||
onValueChange={(vals) => row.setValue(vals[0])}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={row.onApply}
|
||||
disabled={isLoading}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 w-16"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={[lightenAmount]}
|
||||
onValueChange={(vals) => setLightenAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
|
||||
Apply Lighten
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Darken */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Darken</label>
|
||||
<span className="text-xs text-muted-foreground">{(darkenAmount * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={[darkenAmount]}
|
||||
onValueChange={(vals) => setDarkenAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
|
||||
Apply Darken
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Saturate */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Saturate</label>
|
||||
<span className="text-xs text-muted-foreground">{(saturateAmount * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={[saturateAmount]}
|
||||
onValueChange={(vals) => setSaturateAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
|
||||
Apply Saturate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Desaturate */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Desaturate</label>
|
||||
<span className="text-xs text-muted-foreground">{(desaturateAmount * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={[desaturateAmount]}
|
||||
onValueChange={(vals) => setDesaturateAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
|
||||
Apply Desaturate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Rotate Hue */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Rotate Hue</label>
|
||||
<span className="text-xs text-muted-foreground">{rotateAmount}°</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={-180}
|
||||
max={180}
|
||||
step={5}
|
||||
value={[rotateAmount]}
|
||||
onValueChange={(vals) => setRotateAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
|
||||
Apply Rotation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="pt-4 border-t space-y-2">
|
||||
<h3 className="text-sm font-medium mb-3">Quick Actions</h3>
|
||||
<div className="pt-3 border-t">
|
||||
<Button
|
||||
onClick={handleComplement}
|
||||
disabled={isLoading}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
>
|
||||
Get Complementary Color
|
||||
<ArrowLeftRight className="h-3.5 w-3.5 mr-1.5" />
|
||||
Complementary Color
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user