- {formats.map((format) => (
+ {/* Format rows */}
+
+ {formats.map((fmt) => (
- {format.label}
- {format.value}
+
+ {fmt.label}
+
+ {fmt.value}
-
))}
-
-
-
Brightness
-
{(info.brightness * 100).toFixed(1)}%
-
-
-
Luminance
-
{(info.luminance * 100).toFixed(1)}%
-
-
-
{info.name && typeof info.name === 'string' ? 'Name' : 'Type'}
-
{info.name && typeof info.name === 'string' ? info.name : (info.is_light ? 'Light' : 'Dark')}
-
+ {/* Metadata row */}
+
+ {[
+ { label: 'Brightness', value: `${(info.brightness * 100).toFixed(1)}%` },
+ { label: 'Luminance', value: `${(info.luminance * 100).toFixed(1)}%` },
+ {
+ label: info.name && typeof info.name === 'string' ? 'Name' : 'Type',
+ value: info.name && typeof info.name === 'string' ? info.name : (info.is_light ? 'Light' : 'Dark'),
+ },
+ ].map((m) => (
+
+
{m.label}
+
{m.value}
+
+ ))}
);
diff --git a/components/color/ColorManipulation.tsx b/components/color/ColorManipulation.tsx
index 88adf23..5e611d4 100644
--- a/components/color/ColorManipulation.tsx
+++ b/components/color/ColorManipulation.tsx
@@ -7,27 +7,32 @@ import { ColorInfo } from '@/components/color/ColorInfo';
import { ManipulationPanel } from '@/components/color/ManipulationPanel';
import { PaletteGrid } from '@/components/color/PaletteGrid';
import { ExportMenu } from '@/components/color/ExportMenu';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/color/api/queries';
-import { Loader2, Share2, Palette, Plus, X, Layers } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
+import { Loader2, Share2, Plus, X, Palette, Layers } from 'lucide-react';
import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
-type HarmonyType =
- | 'monochromatic'
- | 'analogous'
- | 'complementary'
- | 'triadic'
- | 'tetradic';
+type HarmonyType = 'monochromatic' | 'analogous' | 'complementary' | 'triadic' | 'tetradic';
+type RightTab = 'info' | 'adjust' | 'harmony' | 'gradient';
+type MobileTab = 'pick' | 'explore';
+
+const HARMONY_OPTS: { value: HarmonyType; label: string; desc: string }[] = [
+ { value: 'monochromatic', label: 'Mono', desc: 'Single hue, varied lightness' },
+ { value: 'analogous', label: 'Analogous', desc: 'Adjacent colors ±30°' },
+ { value: 'complementary', label: 'Complement', desc: 'Opposite on wheel 180°' },
+ { value: 'triadic', label: 'Triadic', desc: 'Three equal 120° steps' },
+ { value: 'tetradic', label: 'Tetradic', desc: 'Four equal 90° steps' },
+];
+
+const RIGHT_TABS: { value: RightTab; label: string }[] = [
+ { value: 'info', label: 'Info' },
+ { value: 'adjust', label: 'Adjust' },
+ { value: 'harmony', label: 'Harmony' },
+ { value: 'gradient', label: 'Gradient' },
+];
+
+const actionBtn =
+ 'flex items-center gap-1 px-2.5 py-1 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed';
function ColorManipulationContent() {
const searchParams = useSearchParams();
@@ -37,24 +42,23 @@ function ColorManipulationContent() {
return urlColor ? `#${urlColor.replace('#', '')}` : '#ff0099';
});
- // Harmony state
+ const [rightTab, setRightTab] = useState
('info');
+ const [mobileTab, setMobileTab] = useState('pick');
+
+ // Harmony
const [harmonyType, setHarmonyType] = useState('complementary');
const [palette, setPalette] = useState([]);
const paletteMutation = useGeneratePalette();
- // Gradient state
+ // Gradient
const [stops, setStops] = useState(['#ff0099', '#0099ff']);
const [gradientCount, setGradientCount] = useState(10);
const [gradientResult, setGradientResult] = useState([]);
const gradientMutation = useGenerateGradient();
- const { data, isLoading, isError, error } = useColorInfo({
- colors: [color],
- });
-
+ const { data, isLoading } = useColorInfo({ colors: [color] });
const colorInfo = data?.colors[0];
- // Update URL when color changes
useEffect(() => {
const hex = color.replace('#', '');
if (hex.length === 6 || hex.length === 3) {
@@ -64,301 +68,289 @@ function ColorManipulationContent() {
// Sync first gradient stop with active color
useEffect(() => {
- const newStops = [...stops];
- newStops[0] = color;
- setStops(newStops);
+ setStops((prev) => [color, ...prev.slice(1)]);
}, [color]);
const handleShare = () => {
- const url = `${window.location.origin}/color?color=${color.replace('#', '')}`;
- navigator.clipboard.writeText(url);
- toast.success('Link copied to clipboard!');
+ navigator.clipboard.writeText(`${window.location.origin}/color?color=${color.replace('#', '')}`);
+ toast.success('Link copied!');
};
const generateHarmony = async () => {
try {
- const result = await paletteMutation.mutateAsync({
- base: color,
- scheme: harmonyType,
- });
-
- const colors = [result.palette.primary, ...result.palette.secondary];
- setPalette(colors);
- toast.success(`Generated ${harmonyType} harmony palette`);
- } catch (error) {
- toast.error('Failed to generate harmony palette');
- console.error(error);
- }
+ const result = await paletteMutation.mutateAsync({ base: color, scheme: harmonyType });
+ setPalette([result.palette.primary, ...result.palette.secondary]);
+ toast.success(`Generated ${harmonyType} palette`);
+ } catch { toast.error('Failed to generate palette'); }
};
const generateGradient = async () => {
try {
- const result = await gradientMutation.mutateAsync({
- stops,
- count: gradientCount,
- });
+ const result = await gradientMutation.mutateAsync({ stops, count: gradientCount });
setGradientResult(result.gradient);
toast.success(`Generated ${result.gradient.length} colors`);
- } catch (error) {
- toast.error('Failed to generate gradient');
- }
+ } catch { toast.error('Failed to generate gradient'); }
};
- const addStop = () => {
- setStops([...stops, '#000000']);
- };
-
- const removeStop = (index: number) => {
- if (index === 0) return;
- if (stops.length > 2) {
- setStops(stops.filter((_, i) => i !== index));
- }
- };
-
- const updateStop = (index: number, colorValue: string) => {
- const newStops = [...stops];
- newStops[index] = colorValue;
- setStops(newStops);
- if (index === 0) setColor(colorValue);
- };
-
- const harmonyDescriptions: Record = {
- monochromatic: 'Single color with variations',
- analogous: 'Colors adjacent on the color wheel (±30°)',
- complementary: 'Colors opposite on the color wheel (180°)',
- triadic: 'Three colors evenly spaced on the color wheel (120°)',
- tetradic: 'Four colors evenly spaced on the color wheel (90°)',
+ const updateStop = (i: number, v: string) => {
+ const next = [...stops];
+ next[i] = v;
+ setStops(next);
+ if (i === 0) setColor(v);
};
return (
-
- {/* Row 1: Workspace */}
-
- {/* Main Workspace: Color Picker and Information */}
-
-
-
- Color Picker
-
-
- Share
-
-
-
-
-
-
-
+
-
- {isLoading && (
-
-
-
- )}
-
- {isError && (
-
-
Error loading color information
-
{error?.message || 'Unknown error'}
-
- )}
-
- {colorInfo &&
}
-
-
-
-
-
-
- {/* Sidebar: Color Manipulation */}
-
-
-
- Adjustments
-
-
-
-
-
-
+ {/* ── Mobile tab switcher ────────────────────────────────── */}
+
+ {(['pick', 'explore'] as MobileTab[]).map((t) => (
+ setMobileTab(t)}
+ className={cn(
+ 'flex-1 py-2.5 rounded-lg text-sm font-medium capitalize transition-all',
+ mobileTab === t
+ ? 'bg-primary text-primary-foreground shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ )}
+ >
+ {t === 'pick' ? 'Pick' : 'Explore'}
+
+ ))}
- {/* Row 2: Harmony Generator */}
-
- {/* Harmony Controls */}
-
-
-
- Harmony
-
-
-
+ {/* ── Main layout ────────────────────────────────────────── */}
+
-
- {harmonyDescriptions[harmonyType]}
-
+ {/* Left panel: Picker + ColorInfo */}
+
+ {/* Color picker card */}
+
+
+
+ Color
+
+
+ Share
+
+
+
+
-
- {paletteMutation.isPending ? (
- <>
-
- Generating...
- >
- ) : (
- 'Generate'
- )}
-
-
-
-
-
- {/* Harmony Results */}
-
-
-
-
- Palette {palette.length > 0 && ({palette.length})}
-
-
-
- {palette.length > 0 ? (
-
-
-
-
-
+ {/* Color info card */}
+
+
+ Info
+
+
+ {isLoading ? (
+
+
- ) : (
-
-
-
Generate a harmony palette from the current color
-
- )}
-
-
-
-
-
- {/* Row 3: Gradient Generator */}
-
- {/* Gradient Controls */}
-
-
-
- Gradient
-
-
-
-
-
-
- setGradientCount(parseInt(e.target.value))}
- />
-
-
-
- {gradientMutation.isPending ? (
- <>
-
- Generating...
- >
- ) : (
- 'Generate'
- )}
-
-
-
+ ) : colorInfo ? (
+
+ ) : null}
+
+
- {/* Gradient Results */}
-
-
-
-
- Gradient {gradientResult.length > 0 && ({gradientResult.length})}
-
-
-
- {gradientResult.length > 0 ? (
-
+ {/* Right panel: tabbed tools */}
+
+
+
+ {/* Tab switcher */}
+
+ {RIGHT_TABS.map(({ value, label }) => (
+ setRightTab(value)}
+ className={cn(
+ 'flex-1 py-1.5 rounded-md text-xs font-medium transition-all',
+ rightTab === value
+ ? 'bg-primary text-primary-foreground shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ )}
+ >
+ {label}
+
+ ))}
+
+
+ {/* Tab content */}
+
+
+ {/* ── Info tab ─────────────────────────────── */}
+ {rightTab === 'info' && (
+
+ {/* Large color preview */}
-
-
-
-
-
- ) : (
-
-
-
Add color stops and generate a smooth gradient
+ {isLoading ? (
+
+
+
+ ) : colorInfo ? (
+
+ ) : null}
)}
-
-
+
+ {/* ── Adjust tab ───────────────────────────── */}
+ {rightTab === 'adjust' && (
+
+ )}
+
+ {/* ── Harmony tab ──────────────────────────── */}
+ {rightTab === 'harmony' && (
+
+ {/* Scheme selector */}
+
+
+ Scheme
+
+
+ {HARMONY_OPTS.map((opt) => (
+ setHarmonyType(opt.value)}
+ className={cn(
+ 'px-2.5 py-1 rounded-lg border text-xs font-mono transition-all',
+ harmonyType === opt.value
+ ? 'bg-primary/10 border-primary/40 text-primary'
+ : 'border-border/30 text-muted-foreground hover:border-primary/30 hover:text-foreground'
+ )}
+ >
+ {opt.label}
+
+ ))}
+
+
+ {HARMONY_OPTS.find((o) => o.value === harmonyType)?.desc}
+
+
+
+
+ {paletteMutation.isPending
+ ? <> Generating…>
+ : <> Generate Palette>
+ }
+
+
+ {palette.length > 0 && (
+
+ )}
+
+ )}
+
+ {/* ── Gradient tab ─────────────────────────── */}
+ {rightTab === 'gradient' && (
+
+ {/* Color stops */}
+
+
+ {/* Steps */}
+
+
+ Steps
+
+ setGradientCount(parseInt(e.target.value))}
+ className="w-20 bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono text-center outline-none focus:border-primary/50 transition-colors"
+ />
+
+
+
+ {gradientMutation.isPending
+ ? <> Generating…>
+ : <> Generate Gradient>
+ }
+
+
+ {gradientResult.length > 0 && (
+
+ {/* Gradient preview bar */}
+
+
+
+
+
+
+ )}
+
+ )}
+
+
+
@@ -369,7 +361,7 @@ export function ColorManipulation() {
return (
-
+
}>
diff --git a/components/color/ColorPicker.tsx b/components/color/ColorPicker.tsx
index a0147df..509d4bb 100644
--- a/components/color/ColorPicker.tsx
+++ b/components/color/ColorPicker.tsx
@@ -1,8 +1,6 @@
'use client';
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';
@@ -13,45 +11,23 @@ interface ColorPickerProps {
}
export function ColorPicker({ color, onChange, className }: ColorPickerProps) {
- const handleInputChange = (e: React.ChangeEvent
) => {
- const value = e.target.value;
- // Allow partial input while typing
- onChange(value);
- };
-
- // Determine text color based on background brightness
- const getContrastColor = (hex: string) => {
- const rgb = hexToRgb(hex);
- if (!rgb) return 'inherit';
- const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
- return brightness > 128 ? '#000000' : '#ffffff';
- };
-
- const textColor = getContrastColor(color);
+ const rgb = hexToRgb(color);
+ const brightness = rgb ? (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000 : 0;
+ const textColor = brightness > 128 ? '#000000' : '#ffffff';
+ const borderColor = brightness > 128 ? 'rgba(0,0,0,0.12)' : 'rgba(255,255,255,0.2)';
return (
-
-
-
-
-
-
-
-
+
+
+ onChange(e.target.value)}
+ placeholder="#ff0099"
+ className="w-full font-mono text-xs rounded-lg px-3 py-2 outline-none transition-colors duration-200 border"
+ style={{ backgroundColor: color, color: textColor, borderColor }}
+ spellCheck={false}
+ />
);
}
diff --git a/components/color/ColorSwatch.tsx b/components/color/ColorSwatch.tsx
index 32083ea..4433818 100644
--- a/components/color/ColorSwatch.tsx
+++ b/components/color/ColorSwatch.tsx
@@ -13,54 +13,43 @@ interface ColorSwatchProps {
className?: string;
}
-export function ColorSwatch({
- color,
- size = 'md',
- showLabel = true,
- onClick,
- className,
-}: ColorSwatchProps) {
+export function ColorSwatch({ color, size = 'md', showLabel = true, onClick, className }: ColorSwatchProps) {
const [copied, setCopied] = useState(false);
- const sizeClasses = {
- sm: 'h-12 w-12',
- md: 'h-16 w-16',
- lg: 'h-24 w-24',
- };
-
- const handleCopy = (e: React.MouseEvent) => {
- e.stopPropagation();
+ const handleClick = () => {
+ if (onClick) { onClick(); return; }
navigator.clipboard.writeText(color);
setCopied(true);
toast.success(`Copied ${color}`);
- setTimeout(() => setCopied(false), 2000);
+ setTimeout(() => setCopied(false), 1500);
};
return (
-
-
-
- {copied ? (
-
- ) : (
-
- )}
-
-
- {showLabel && (
-
{color}
+
+ style={{ backgroundColor: color }}
+ >
+
+ {copied
+ ?
+ :
+ }
+
+ {showLabel && (
+
+ {color}
+
+ )}
+
);
}
diff --git a/components/color/ExportMenu.tsx b/components/color/ExportMenu.tsx
index 9f22c48..7da319f 100644
--- a/components/color/ExportMenu.tsx
+++ b/components/color/ExportMenu.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Button } from '@/components/ui/button';
+import { useState, useEffect } from 'react';
import {
Select,
SelectContent,
@@ -8,7 +8,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
-import { useState, useEffect } from 'react';
import { Download, Copy, Check, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import {
@@ -21,6 +20,7 @@ import {
type ExportColor,
} from '@/lib/color/utils/export';
import { colorAPI } from '@/lib/color/api/client';
+import { cn } from '@/lib/utils/cn';
interface ExportMenuProps {
colors: string[];
@@ -30,6 +30,9 @@ interface ExportMenuProps {
type ExportFormat = 'css' | 'scss' | 'tailwind' | 'json' | 'javascript';
type ColorSpace = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklab' | 'lch' | 'oklch';
+const actionBtn =
+ 'flex items-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed';
+
export function ExportMenu({ colors, className }: ExportMenuProps) {
const [format, setFormat] = useState
('css');
const [colorSpace, setColorSpace] = useState('hex');
@@ -39,152 +42,105 @@ export function ExportMenu({ colors, className }: ExportMenuProps) {
useEffect(() => {
async function convertColors() {
- if (colorSpace === 'hex') {
- setConvertedColors(colors);
- return;
- }
-
+ if (colorSpace === 'hex') { setConvertedColors(colors); return; }
setIsConverting(true);
try {
- const response = await colorAPI.convertFormat({
- colors,
- format: colorSpace,
- });
-
+ const response = await colorAPI.convertFormat({ colors, format: colorSpace });
if (response.success) {
- setConvertedColors(response.data.conversions.map(c => c.output));
+ setConvertedColors(response.data.conversions.map((c) => c.output));
}
- } catch (error) {
- console.error('Failed to convert colors:', error);
- toast.error('Failed to convert colors to selected space');
+ } catch {
+ toast.error('Failed to convert colors');
} finally {
setIsConverting(false);
}
}
-
convertColors();
}, [colors, colorSpace]);
const exportColors: ExportColor[] = convertedColors.map((value) => ({ value }));
- const getExportContent = (): string => {
+ const getContent = (): string => {
switch (format) {
- case 'css':
- return exportAsCSS(exportColors);
- case 'scss':
- return exportAsSCSS(exportColors);
- case 'tailwind':
- return exportAsTailwind(exportColors);
- case 'json':
- return exportAsJSON(exportColors);
- case 'javascript':
- return exportAsJavaScript(exportColors);
+ case 'css': return exportAsCSS(exportColors);
+ case 'scss': return exportAsSCSS(exportColors);
+ case 'tailwind': return exportAsTailwind(exportColors);
+ case 'json': return exportAsJSON(exportColors);
+ case 'javascript': return exportAsJavaScript(exportColors);
}
};
- const getFileExtension = (): string => {
- switch (format) {
- case 'css':
- return 'css';
- case 'scss':
- return 'scss';
- case 'tailwind':
- return 'js';
- case 'json':
- return 'json';
- case 'javascript':
- return 'js';
- }
- };
+ const getExt = () => ({ css: 'css', scss: 'scss', tailwind: 'js', json: 'json', javascript: 'js' }[format]);
const handleCopy = () => {
- const content = getExportContent();
- navigator.clipboard.writeText(content);
+ navigator.clipboard.writeText(getContent());
setCopied(true);
- toast.success('Copied to clipboard!');
+ toast.success('Copied!');
setTimeout(() => setCopied(false), 2000);
};
const handleDownload = () => {
- const content = getExportContent();
- const extension = getFileExtension();
- downloadAsFile(content, `palette.${extension}`, 'text/plain');
+ downloadAsFile(getContent(), `palette.${getExt()}`, 'text/plain');
toast.success('Downloaded!');
};
- if (colors.length === 0) {
- return null;
- }
+ if (colors.length === 0) return null;
return (
-
-
-
-
+
+ Export
-
-
+ {/* Selectors */}
+
+
+
+
-
- {isConverting ? (
-
-
-
- ) : null}
-
- {getExportContent()}
-
-
+ {/* Code preview */}
+
+ {isConverting && (
+
+
+
+ )}
+
+ {getContent()}
+
+
-
-
- {copied ? (
- <>
-
- Copied
- >
- ) : (
- <>
-
- Copy
- >
- )}
-
-
-
- Download
-
-
+ {/* Actions */}
+
+
+ {copied ? : }
+ {copied ? 'Copied' : 'Copy'}
+
+
+
+ Download
+
);
diff --git a/components/color/ManipulationPanel.tsx b/components/color/ManipulationPanel.tsx
index bc443cf..a8f531a 100644
--- a/components/color/ManipulationPanel.tsx
+++ b/components/color/ManipulationPanel.tsx
@@ -2,34 +2,25 @@
import { useState } from 'react';
import { Slider } from '@/components/ui/slider';
-import { Button } from '@/components/ui/button';
import {
useLighten,
useDarken,
useSaturate,
useDesaturate,
useRotate,
- useComplement
+ useComplement,
} from '@/lib/color/api/queries';
import { toast } from 'sonner';
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
+import { cn } from '@/lib/utils/cn';
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
;
-}
+const actionBtn =
+ 'shrink-0 px-3 py-1 text-[10px] font-mono glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed';
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
const [lightenAmount, setLightenAmount] = useState(0.2);
@@ -53,150 +44,104 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
rotateMutation.isPending ||
complementMutation.isPending;
- const handleMutation = async (
- mutationFn: (params: any) => Promise,
+ const applyMutation = async (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mutationFn: (p: any) => Promise<{ colors: { output: string }[] }>,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any,
- successMsg: string,
- errorMsg: string
+ msg: string
) => {
try {
const result = await mutationFn(params);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
- toast.success(successMsg);
+ toast.success(msg);
}
} catch {
- toast.error(errorMsg);
+ toast.error('Failed to apply');
}
};
- const rows: ManipulationRow[] = [
+ const rows = [
{
- label: 'Lighten',
- icon: ,
- value: lightenAmount,
- setValue: setLightenAmount,
- format: (v) => `${(v * 100).toFixed(0)}%`,
+ label: 'Lighten', icon: ,
+ value: lightenAmount, setValue: setLightenAmount,
+ display: `${(lightenAmount * 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'
- ),
+ onApply: () => applyMutation(lightenMutation.mutateAsync, { colors: [color], amount: lightenAmount }, `Lightened ${(lightenAmount * 100).toFixed(0)}%`),
},
{
- label: 'Darken',
- icon: ,
- value: darkenAmount,
- setValue: setDarkenAmount,
- format: (v) => `${(v * 100).toFixed(0)}%`,
+ label: 'Darken', icon: ,
+ value: darkenAmount, setValue: setDarkenAmount,
+ display: `${(darkenAmount * 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'
- ),
+ onApply: () => applyMutation(darkenMutation.mutateAsync, { colors: [color], amount: darkenAmount }, `Darkened ${(darkenAmount * 100).toFixed(0)}%`),
},
{
- label: 'Saturate',
- icon: ,
- value: saturateAmount,
- setValue: setSaturateAmount,
- format: (v) => `${(v * 100).toFixed(0)}%`,
+ label: 'Saturate', icon: ,
+ value: saturateAmount, setValue: setSaturateAmount,
+ display: `${(saturateAmount * 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'
- ),
+ onApply: () => applyMutation(saturateMutation.mutateAsync, { colors: [color], amount: saturateAmount }, `Saturated ${(saturateAmount * 100).toFixed(0)}%`),
},
{
- label: 'Desaturate',
- icon: ,
- value: desaturateAmount,
- setValue: setDesaturateAmount,
- format: (v) => `${(v * 100).toFixed(0)}%`,
+ label: 'Desaturate', icon: ,
+ value: desaturateAmount, setValue: setDesaturateAmount,
+ display: `${(desaturateAmount * 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'
- ),
+ onApply: () => applyMutation(desaturateMutation.mutateAsync, { colors: [color], amount: desaturateAmount }, `Desaturated ${(desaturateAmount * 100).toFixed(0)}%`),
},
{
- label: 'Rotate',
- icon: ,
- value: rotateAmount,
- setValue: setRotateAmount,
- format: (v) => `${v}°`,
+ label: 'Rotate Hue', icon: ,
+ value: rotateAmount, setValue: setRotateAmount,
+ display: `${rotateAmount}°`,
min: -180, max: 180, step: 5,
- onApply: () => handleMutation(
- rotateMutation.mutateAsync,
- { colors: [color], amount: rotateAmount },
- `Rotated hue by ${rotateAmount}°`,
- 'Failed to rotate hue'
- ),
+ onApply: () => applyMutation(rotateMutation.mutateAsync, { colors: [color], amount: rotateAmount }, `Rotated ${rotateAmount}°`),
},
];
- 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 (
{rows.map((row) => (
-
+
-
+
{row.icon}
{row.label}
-
{row.format(row.value)}
+
{row.display}
row.setValue(vals[0])}
className="flex-1"
/>
-
+
Apply
-
+
))}
-
-
+ {
+ try {
+ const result = await complementMutation.mutateAsync([color]);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
+ toast.success('Complementary color applied');
+ }
+ } catch { toast.error('Failed'); }
+ }}
disabled={isLoading}
- variant="outline"
- className="w-full"
+ className={cn(actionBtn, 'w-full justify-center flex items-center gap-1.5 py-2')}
>
-
+
Complementary Color
-
+
);
diff --git a/components/color/PaletteGrid.tsx b/components/color/PaletteGrid.tsx
index 50e68c7..ecdf54f 100644
--- a/components/color/PaletteGrid.tsx
+++ b/components/color/PaletteGrid.tsx
@@ -19,16 +19,12 @@ export function PaletteGrid({ colors, onColorClick, className }: PaletteGridProp
}
return (
-
+
{colors.map((color, index) => (
onColorClick(color) : undefined}
/>
))}