From c3745bd6b7f8aa70f6a90e9dbbfa56513be5f701 Mon Sep 17 00:00:00 2001 From: valknarness Date: Fri, 7 Nov 2025 11:38:20 +0100 Subject: [PATCH] feat: implement palette generation features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete palette generation system with multiple methods: **New Components:** - ColorSwatch component - Individual color display - Click to copy to clipboard - Hover effects with copy icon - Configurable sizes (sm, md, lg) - Optional color label display - Check icon on successful copy - PaletteGrid component - Grid display for color palettes - Responsive grid layout (3-6 columns) - Click handler for color selection - Empty state message - Flexible and reusable - Select component - Dropdown selector - Consistent styling with other inputs - Optional label support - Keyboard accessible - Focus ring styling **Palette Pages:** 1. Gradient Creator (/palettes/gradient) - Multiple color stop editor (add/remove) - Configurable step count (2-1000) - Color space selection (RGB, HSL, Lab, LCH, OkLab, OkLCH) - Live gradient preview bar - Full palette grid display - Interactive color pickers for each stop - Real-time generation 2. Distinct Colors (/palettes/distinct) - Count selector (2-100 colors) - Distance metric selection (CIE76, CIEDE2000) - Simulated annealing algorithm - Statistics display: - Minimum distance - Average distance - Generation time - Loading state with progress message - Quality metrics for generated palette 3. Palettes Dashboard (/palettes) - Overview of all palette generators - Feature cards with icons - "Coming Soon" badge for harmony palettes - Quick navigation to each generator - Feature highlights for each method **Features:** - All palette generators integrated with Pastel API - Toast notifications for success/error - Loading states during generation - Copy colors to clipboard - Responsive layouts - Empty states - Error handling **API Integration:** - useGenerateGradient mutation - useGenerateDistinct mutation - Proper error handling and loading states - Result caching with React Query Build successful! 3 new palette pages rendering. Next: Harmony palettes, accessibility tools, and named colors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/palettes/distinct/page.tsx | 140 +++++++++++++++++++++++ app/palettes/gradient/page.tsx | 183 +++++++++++++++++++++++++++++++ app/palettes/page.tsx | 74 +++++++++++++ components/color/ColorSwatch.tsx | 65 +++++++++++ components/color/PaletteGrid.tsx | 37 +++++++ components/ui/select.tsx | 39 +++++++ 6 files changed, 538 insertions(+) create mode 100644 app/palettes/distinct/page.tsx create mode 100644 app/palettes/gradient/page.tsx create mode 100644 app/palettes/page.tsx create mode 100644 components/color/ColorSwatch.tsx create mode 100644 components/color/PaletteGrid.tsx create mode 100644 components/ui/select.tsx diff --git a/app/palettes/distinct/page.tsx b/app/palettes/distinct/page.tsx new file mode 100644 index 0000000..9a184cd --- /dev/null +++ b/app/palettes/distinct/page.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { useState } from 'react'; +import { PaletteGrid } from '@/components/color/PaletteGrid'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select } from '@/components/ui/select'; +import { useGenerateDistinct } from '@/lib/api/queries'; +import { Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; + +export default function DistinctPage() { + const [count, setCount] = useState(8); + const [metric, setMetric] = useState<'cie76' | 'ciede2000'>('ciede2000'); + const [colors, setColors] = useState([]); + const [stats, setStats] = useState<{ + min_distance: number; + avg_distance: number; + generation_time_ms: number; + } | null>(null); + + const generateMutation = useGenerateDistinct(); + + const handleGenerate = async () => { + try { + const result = await generateMutation.mutateAsync({ + count, + metric, + }); + setColors(result.colors); + setStats(result.stats); + toast.success(`Generated ${result.colors.length} distinct colors`); + } catch (error) { + toast.error('Failed to generate distinct colors'); + } + }; + + return ( +
+
+
+

Distinct Colors Generator

+

+ Generate visually distinct colors using simulated annealing +

+
+ +
+ {/* Controls */} +
+
+
+

Settings

+
+ +
+ + setCount(parseInt(e.target.value))} + /> +

+ Higher counts take longer to generate +

+
+ + + + + + {generateMutation.isPending && ( +
+ This may take a few moments... +
+ )} + + {stats && ( +
+

Statistics

+
+
+ Min Distance: + {stats.min_distance.toFixed(2)} +
+
+ Avg Distance: + {stats.avg_distance.toFixed(2)} +
+
+ Generation Time: + + {(stats.generation_time_ms / 1000).toFixed(2)}s + +
+
+
+ )} +
+
+ + {/* Results */} +
+
+

+ Generated Colors {colors.length > 0 && `(${colors.length})`} +

+ +
+
+
+
+
+ ); +} diff --git a/app/palettes/gradient/page.tsx b/app/palettes/gradient/page.tsx new file mode 100644 index 0000000..6394476 --- /dev/null +++ b/app/palettes/gradient/page.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { useState } from 'react'; +import { ColorPicker } from '@/components/color/ColorPicker'; +import { PaletteGrid } from '@/components/color/PaletteGrid'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select } from '@/components/ui/select'; +import { useGenerateGradient } from '@/lib/api/queries'; +import { Loader2, Plus, X } from 'lucide-react'; +import { toast } from 'sonner'; + +export default function GradientPage() { + const [stops, setStops] = useState(['#ff0099', '#0099ff']); + const [count, setCount] = useState(10); + const [colorspace, setColorspace] = useState< + 'rgb' | 'hsl' | 'hsv' | 'lab' | 'oklab' | 'lch' | 'oklch' + >('lch'); + const [gradient, setGradient] = useState([]); + + const generateMutation = useGenerateGradient(); + + const handleGenerate = async () => { + try { + const result = await generateMutation.mutateAsync({ + stops, + count, + colorspace, + }); + setGradient(result.colors); + toast.success(`Generated ${result.colors.length} colors`); + } catch (error) { + toast.error('Failed to generate gradient'); + } + }; + + const addStop = () => { + setStops([...stops, '#000000']); + }; + + const removeStop = (index: number) => { + if (stops.length > 2) { + setStops(stops.filter((_, i) => i !== index)); + } + }; + + const updateStop = (index: number, color: string) => { + const newStops = [...stops]; + newStops[index] = color; + setStops(newStops); + }; + + return ( +
+
+
+

Gradient Creator

+

+ Create smooth color gradients with multiple stops +

+
+ +
+ {/* Controls */} +
+
+

Color Stops

+
+ {stops.map((stop, index) => ( +
+
+ updateStop(index, color)} + /> +
+ {stops.length > 2 && ( + + )} +
+ ))} + +
+
+ +
+

Settings

+
+
+ + setCount(parseInt(e.target.value))} + /> +
+ + + + +
+
+
+ + {/* Preview */} +
+ {gradient.length > 0 && ( + <> +
+

Gradient Preview

+
+
+ +
+

+ Colors ({gradient.length}) +

+ +
+ + )} +
+
+
+
+ ); +} diff --git a/app/palettes/page.tsx b/app/palettes/page.tsx new file mode 100644 index 0000000..cb9ae2c --- /dev/null +++ b/app/palettes/page.tsx @@ -0,0 +1,74 @@ +import Link from 'next/link'; +import { Palette, Sparkles, GraduationCap } from 'lucide-react'; + +export default function PalettesPage() { + const paletteTypes = [ + { + title: 'Gradient Creator', + description: 'Create smooth color gradients with multiple stops and color spaces', + href: '/palettes/gradient', + icon: GraduationCap, + features: ['Multiple color stops', 'Various color spaces', 'Live preview'], + }, + { + title: 'Distinct Colors', + description: 'Generate visually distinct colors using simulated annealing algorithm', + href: '/palettes/distinct', + icon: Sparkles, + features: ['Perceptual distance', 'Configurable count', 'Quality metrics'], + }, + { + title: 'Harmony Palettes', + description: 'Create color palettes based on color theory and harmony rules', + href: '/palettes/harmony', + icon: Palette, + features: ['Color theory', 'Multiple schemes', 'Instant generation'], + comingSoon: true, + }, + ]; + + return ( +
+
+
+

Palette Generation

+

+ Create beautiful color palettes using various generation methods +

+
+ +
+ {paletteTypes.map((type) => { + const Icon = type.icon; + return ( + +
+ + {type.comingSoon && ( + Coming Soon + )} +
+

{type.title}

+

{type.description}

+
    + {type.features.map((feature) => ( +
  • + • + {feature} +
  • + ))} +
+ + ); + })} +
+
+
+ ); +} diff --git a/components/color/ColorSwatch.tsx b/components/color/ColorSwatch.tsx new file mode 100644 index 0000000..b877110 --- /dev/null +++ b/components/color/ColorSwatch.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { cn } from '@/lib/utils/cn'; +import { Check, Copy } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +interface ColorSwatchProps { + color: string; + size?: 'sm' | 'md' | 'lg'; + showLabel?: boolean; + onClick?: () => void; + className?: string; +} + +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(); + navigator.clipboard.writeText(color); + setCopied(true); + toast.success(`Copied ${color}`); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ + {showLabel && ( + {color} + )} +
+ ); +} diff --git a/components/color/PaletteGrid.tsx b/components/color/PaletteGrid.tsx new file mode 100644 index 0000000..50e68c7 --- /dev/null +++ b/components/color/PaletteGrid.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { ColorSwatch } from './ColorSwatch'; +import { cn } from '@/lib/utils/cn'; + +interface PaletteGridProps { + colors: string[]; + onColorClick?: (color: string) => void; + className?: string; +} + +export function PaletteGrid({ colors, onColorClick, className }: PaletteGridProps) { + if (colors.length === 0) { + return ( +
+ No colors in palette yet +
+ ); + } + + return ( +
+ {colors.map((color, index) => ( + onColorClick(color) : undefined} + /> + ))} +
+ ); +} diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..c284baa --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,39 @@ +'use client'; + +import * as React from 'react'; +import { cn } from '@/lib/utils/cn'; + +export interface SelectProps extends React.SelectHTMLAttributes { + label?: string; +} + +const Select = React.forwardRef( + ({ className, label, children, ...props }, ref) => { + return ( +
+ {label && ( + + )} + +
+ ); + } +); + +Select.displayName = 'Select'; + +export { Select };