refactor: use shadcn Card component in pastel app

This commit is contained in:
2026-02-25 13:35:29 +01:00
parent 57ba63aa32
commit 4ccf316184
9 changed files with 608 additions and 494 deletions

View File

@@ -10,6 +10,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PaletteGrid } from '@/components/pastel/PaletteGrid'; import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu'; import { ExportMenu } from '@/components/pastel/ExportMenu';
import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/pastel/api/queries'; import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/pastel/api/queries';
@@ -102,27 +103,33 @@ export default function BatchPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Input */} {/* Input */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Input Colors</h2> <CardHeader>
<p className="text-sm text-muted-foreground mb-4"> <CardTitle className="text-sm font-medium">Input Colors</CardTitle>
Enter colors (one per line or comma-separated). Supports hex format </CardHeader>
</p> <CardContent>
<p className="text-sm text-muted-foreground mb-4">
Enter colors (one per line or comma-separated). Supports hex format
</p>
<textarea <textarea
value={inputColors} value={inputColors}
onChange={(e) => setInputColors(e.target.value)} onChange={(e) => setInputColors(e.target.value)}
placeholder="#ff0099, #00ff99, #9900ff&#10;#ff5533&#10;#3355ff" placeholder="#ff0099, #00ff99, #9900ff&#10;#ff5533&#10;#3355ff"
className="w-full h-48 p-3 border border-border rounded-xl bg-input font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary/50 transition-all duration-200" className="w-full h-48 p-3 border border-border rounded-xl bg-input font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary/50 transition-all duration-200"
/> />
<p className="text-xs text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-2">
{parseColors(inputColors).length} valid colors found {parseColors(inputColors).length} valid colors found
</p> </p>
</div> </CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Operation</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">Operation</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select <Select
value={operation} value={operation}
onValueChange={(value) => setOperation(value as Operation)} onValueChange={(value) => setOperation(value as Operation)}
@@ -170,30 +177,38 @@ export default function BatchPage() {
</> </>
)} )}
</Button> </Button>
</div> </CardContent>
</div> </Card>
</div> </div>
{/* Output */} {/* Output */}
<div className="space-y-6"> <div className="space-y-6">
{outputColors.length > 0 ? ( {outputColors.length > 0 ? (
<> <>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4"> <CardHeader>
Output Colors ({outputColors.length}) <CardTitle className="text-sm font-medium">
</h2> Output Colors ({outputColors.length})
<PaletteGrid colors={outputColors} /> </CardTitle>
</div> </CardHeader>
<CardContent>
<PaletteGrid colors={outputColors} />
</CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<ExportMenu colors={outputColors} /> <CardContent className="pt-6">
</div> <ExportMenu colors={outputColors} />
</CardContent>
</Card>
</> </>
) : ( ) : (
<div className="p-12 border rounded-lg bg-card text-center text-muted-foreground"> <Card>
<Download className="h-12 w-12 mx-auto mb-4 opacity-50" /> <CardContent className="p-12 text-center text-muted-foreground">
<p>Enter colors and click Process to see results</p> <Download className="h-12 w-12 mx-auto mb-4 opacity-50" />
</div> <p>Enter colors and click Process to see results</p>
</CardContent>
</Card>
)} )}
</div> </div>
</div> </div>

View File

@@ -11,6 +11,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useSimulateColorBlindness } from '@/lib/pastel/api/queries'; import { useSimulateColorBlindness } from '@/lib/pastel/api/queries';
import { Loader2, Eye, Plus, X } from 'lucide-react'; import { Loader2, Eye, Plus, X } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -77,9 +78,9 @@ export default function ColorBlindPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Controls */} {/* Controls */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<div className="flex items-center justify-between mb-4"> <CardHeader className="flex flex-row items-center justify-between space-y-0">
<h2 className="text-sm font-medium">Colors to Test</h2> <CardTitle className="text-sm font-medium">Colors to Test</CardTitle>
<Button <Button
onClick={addColor} onClick={addColor}
variant="outline" variant="outline"
@@ -89,9 +90,9 @@ export default function ColorBlindPage() {
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Color Add Color
</Button> </Button>
</div> </CardHeader>
<div className="space-y-4"> <CardContent className="space-y-4">
{colors.map((color, index) => ( {colors.map((color, index) => (
<div key={index} className="flex items-start gap-3"> <div key={index} className="flex items-start gap-3">
<div className="flex-1"> <div className="flex-1">
@@ -112,12 +113,14 @@ export default function ColorBlindPage() {
)} )}
</div> </div>
))} ))}
</div> </CardContent>
</div> </Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Blindness Type</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">Blindness Type</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select <Select
value={blindnessType} value={blindnessType}
onValueChange={(value) => setBlindnessType(value as ColorBlindnessType)} onValueChange={(value) => setBlindnessType(value as ColorBlindnessType)}
@@ -153,68 +156,76 @@ export default function ColorBlindPage() {
</> </>
)} )}
</Button> </Button>
</div> </CardContent>
</div> </Card>
</div> </div>
{/* Results */} {/* Results */}
<div className="space-y-6"> <div className="space-y-6">
{simulations.length > 0 ? ( {simulations.length > 0 ? (
<> <>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Simulation Results</h2> <CardHeader>
<p className="text-sm text-muted-foreground mb-6"> <CardTitle className="text-sm font-medium">Simulation Results</CardTitle>
Compare original colors (left) with how they appear to people with{' '} </CardHeader>
{blindnessType} (right) <CardContent className="space-y-4">
</p> <p className="text-sm text-muted-foreground mb-2">
Compare original colors (left) with how they appear to people with{' '}
{blindnessType} (right)
</p>
<div className="space-y-4"> <div className="space-y-4">
{simulations.map((sim, index) => ( {simulations.map((sim, index) => (
<div <div
key={index} key={index}
className="grid grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg" className="grid grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg"
> >
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground"> <p className="text-xs font-medium text-muted-foreground">
Original Original
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ColorDisplay color={sim.input} size="md" /> <ColorDisplay color={sim.input} size="md" />
<code className="text-sm font-mono">{sim.input}</code> <code className="text-sm font-mono">{sim.input}</code>
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
As Seen ({sim.difference_percentage.toFixed(1)}% difference)
</p>
<div className="flex items-center gap-3">
<ColorDisplay color={sim.output} size="md" />
<code className="text-sm font-mono">{sim.output}</code>
</div>
</div> </div>
</div> </div>
))}
</div>
</CardContent>
</Card>
<div className="space-y-2"> <Card className="bg-blue-50 dark:bg-blue-950/20 border-blue-100 dark:border-blue-900/30 shadow-none">
<p className="text-xs font-medium text-muted-foreground"> <CardContent className="pt-6">
As Seen ({sim.difference_percentage.toFixed(1)}% difference) <h3 className="font-semibold mb-2 flex items-center gap-2">
</p> <Eye className="h-5 w-5" />
<div className="flex items-center gap-3"> Accessibility Tip
<ColorDisplay color={sim.output} size="md" /> </h3>
<code className="text-sm font-mono">{sim.output}</code> <p className="text-sm text-muted-foreground">
</div> Ensure important information isn&apos;t conveyed by color alone. Use text
</div> labels, patterns, or icons to make your design accessible to everyone
</div> </p>
))} </CardContent>
</div> </Card>
</div>
<div className="p-6 border rounded-lg bg-card bg-blue-50 dark:bg-blue-950/20">
<h3 className="font-semibold mb-2 flex items-center gap-2">
<Eye className="h-5 w-5" />
Accessibility Tip
</h3>
<p className="text-sm text-muted-foreground">
Ensure important information isn&apos;t conveyed by color alone. Use text
labels, patterns, or icons to make your design accessible to everyone
</p>
</div>
</> </>
) : ( ) : (
<div className="p-12 border rounded-lg bg-card text-center text-muted-foreground"> <Card>
<Eye className="h-12 w-12 mx-auto mb-4 opacity-50" /> <CardContent className="p-12 text-center text-muted-foreground">
<p>Add colors and click Simulate to see how they appear</p> <Eye className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="text-sm mt-2">with different types of color blindness</p> <p>Add colors and click Simulate to see how they appear</p>
</div> <p className="text-sm mt-2">with different types of color blindness</p>
</CardContent>
</Card>
)} )}
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker'; import { ColorPicker } from '@/components/pastel/ColorPicker';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { getContrastRatio, hexToRgb, checkWCAGCompliance } from '@/lib/pastel/utils/color'; import { getContrastRatio, hexToRgb, checkWCAGCompliance } from '@/lib/pastel/utils/color';
import { ArrowLeftRight, Check, X } from 'lucide-react'; import { ArrowLeftRight, Check, X } from 'lucide-react';
@@ -68,12 +69,14 @@ export default function ContrastPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Color Pickers */} {/* Color Pickers */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<div className="flex items-center justify-between mb-4"> <CardHeader>
<h2 className="text-sm font-medium">Foreground Color</h2> <CardTitle className="text-sm font-medium">Foreground Color</CardTitle>
</div> </CardHeader>
<ColorPicker color={foreground} onChange={setForeground} /> <CardContent>
</div> <ColorPicker color={foreground} onChange={setForeground} />
</CardContent>
</Card>
<div className="flex justify-center"> <div className="flex justify-center">
<Button <Button
@@ -86,50 +89,64 @@ export default function ContrastPage() {
</Button> </Button>
</div> </div>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Background Color</h2> <CardHeader>
<ColorPicker color={background} onChange={setBackground} /> <CardTitle className="text-sm font-medium">Background Color</CardTitle>
</div> </CardHeader>
<CardContent>
<ColorPicker color={background} onChange={setBackground} />
</CardContent>
</Card>
</div> </div>
{/* Results */} {/* Results */}
<div className="space-y-6"> <div className="space-y-6">
{/* Preview */} {/* Preview */}
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Preview</h2> <CardHeader>
<div <CardTitle className="text-sm font-medium">Preview</CardTitle>
className="rounded-lg p-8 text-center" </CardHeader>
style={{ backgroundColor: background, color: foreground }} <CardContent>
> <div
<p className="text-xl font-bold mb-2">Normal Text (16px)</p> className="rounded-lg p-8 text-center"
<p className="text-3xl font-bold">Large Text (24px)</p> style={{ backgroundColor: background, color: foreground }}
</div> >
</div> <p className="text-xl font-bold mb-2">Normal Text (16px)</p>
<p className="text-3xl font-bold">Large Text (24px)</p>
</div>
</CardContent>
</Card>
{/* Contrast Ratio */} {/* Contrast Ratio */}
{ratio !== null && ( {ratio !== null && (
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Contrast Ratio</h2> <CardHeader>
<div className="text-center mb-6"> <CardTitle className="text-sm font-medium">Contrast Ratio</CardTitle>
<div className="text-5xl font-bold">{ratio.toFixed(2)}:1</div> </CardHeader>
<p className="text-sm text-muted-foreground mt-2"> <CardContent>
{ratio >= 7 <div className="text-center mb-6">
? 'Excellent contrast' <div className="text-5xl font-bold">{ratio.toFixed(2)}:1</div>
: ratio >= 4.5 <p className="text-sm text-muted-foreground mt-2">
? 'Good contrast' {ratio >= 7
: ratio >= 3 ? 'Excellent contrast'
? 'Minimum contrast' : ratio >= 4.5
: 'Poor contrast'} ? 'Good contrast'
</p> : ratio >= 3
</div> ? 'Minimum contrast'
</div> : 'Poor contrast'}
</p>
</div>
</CardContent>
</Card>
)} )}
{/* WCAG Compliance */} {/* WCAG Compliance */}
{compliance && ( {compliance && (
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">WCAG 2.1 Compliance</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">WCAG 2.1 Compliance</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div> <div>
<h3 className="text-sm font-semibold mb-2">Level AA</h3> <h3 className="text-sm font-semibold mb-2">Level AA</h3>
<div className="space-y-2"> <div className="space-y-2">
@@ -161,8 +178,8 @@ export default function ContrastPage() {
/> />
</div> </div>
</div> </div>
</div> </CardContent>
</div> </Card>
)} )}
</div> </div>
</div> </div>

View File

@@ -12,6 +12,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useGenerateDistinct } from '@/lib/pastel/api/queries'; import { useGenerateDistinct } from '@/lib/pastel/api/queries';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -49,82 +50,90 @@ export default function DistinctPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Controls */} {/* Controls */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<div className="p-6 border rounded-lg bg-card space-y-6"> <Card>
<div> <CardHeader>
<h2 className="text-sm font-medium mb-4">Settings</h2> <CardTitle className="text-sm font-medium">Settings</CardTitle>
</div> </CardHeader>
<div> <CardContent className="space-y-6">
<label htmlFor="count" className="text-sm font-medium mb-2 block"> <div>
Number of Colors <label htmlFor="count" className="text-sm font-medium mb-2 block">
</label> Number of Colors
<Input </label>
id="count" <Input
type="number" id="count"
min={2} type="number"
max={100} min={2}
value={count} max={100}
onChange={(e) => setCount(parseInt(e.target.value))} value={count}
/> onChange={(e) => setCount(parseInt(e.target.value))}
<p className="text-xs text-muted-foreground mt-1"> />
Higher counts take longer to generate <p className="text-xs text-muted-foreground mt-1">
</p> Higher counts take longer to generate
</div> </p>
<div className="space-y-2">
<label className="text-sm font-medium block">
Distance Metric
</label>
<Select
value={metric}
onValueChange={(value) => setMetric(value as 'cie76' | 'ciede2000')}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select metric" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cie76">CIE76 (Faster)</SelectItem>
<SelectItem value="ciede2000">CIEDE2000 (More Accurate)</SelectItem>
</SelectContent>
</Select>
</div>
<Button
onClick={handleGenerate}
disabled={generateMutation.isPending}
className="w-full"
>
{generateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
'Generate Colors'
)}
</Button>
{generateMutation.isPending && (
<div className="text-sm text-muted-foreground text-center">
This may take a few moments..
</div> </div>
)}
</div> <div className="space-y-2">
<label className="text-sm font-medium block">
Distance Metric
</label>
<Select
value={metric}
onValueChange={(value) => setMetric(value as 'cie76' | 'ciede2000')}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select metric" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cie76">CIE76 (Faster)</SelectItem>
<SelectItem value="ciede2000">CIEDE2000 (More Accurate)</SelectItem>
</SelectContent>
</Select>
</div>
<Button
onClick={handleGenerate}
disabled={generateMutation.isPending}
className="w-full"
>
{generateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
'Generate Colors'
)}
</Button>
{generateMutation.isPending && (
<div className="text-sm text-muted-foreground text-center">
This may take a few moments..
</div>
)}
</CardContent>
</Card>
</div> </div>
{/* Results */} {/* Results */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4"> <CardHeader>
Generated Colors {colors.length > 0 && `(${colors.length})`} <CardTitle className="text-sm font-medium">
</h2> Generated Colors {colors.length > 0 && `(${colors.length})`}
<PaletteGrid colors={colors} /> </CardTitle>
</div> </CardHeader>
<CardContent>
<PaletteGrid colors={colors} />
</CardContent>
</Card>
{colors.length > 0 && ( {colors.length > 0 && (
<div className="p-6 border rounded-lg bg-card"> <Card>
<ExportMenu colors={colors} /> <CardContent className="pt-6">
</div> <ExportMenu colors={colors} />
</CardContent>
</Card>
)} )}
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { PaletteGrid } from '@/components/pastel/PaletteGrid';
import { ExportMenu } from '@/components/pastel/ExportMenu'; import { ExportMenu } from '@/components/pastel/ExportMenu';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useGenerateGradient } from '@/lib/pastel/api/queries'; import { useGenerateGradient } from '@/lib/pastel/api/queries';
import { Loader2, Plus, X } from 'lucide-react'; import { Loader2, Plus, X } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -59,39 +60,45 @@ export default function GradientPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Controls */} {/* Controls */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Color Stops</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">Color Stops</CardTitle>
{stops.map((stop, index) => ( </CardHeader>
<div key={index} className="flex items-start gap-3"> <CardContent className="space-y-4">
<div className="flex-1"> <div className="space-y-4">
<ColorPicker {stops.map((stop, index) => (
color={stop} <div key={index} className="flex items-start gap-3">
onChange={(color) => updateStop(index, color)} <div className="flex-1">
/> <ColorPicker
color={stop}
onChange={(color) => updateStop(index, color)}
/>
</div>
{stops.length > 2 && (
<Button
variant="ghost"
size="icon"
onClick={() => removeStop(index)}
className="mt-8"
>
<X className="h-4 w-4" />
</Button>
)}
</div> </div>
{stops.length > 2 && ( ))}
<Button <Button onClick={addStop} variant="outline" className="w-full">
variant="ghost" <Plus className="h-4 w-4 mr-2" />
size="icon" Add Stop
onClick={() => removeStop(index)} </Button>
className="mt-8" </div>
> </CardContent>
<X className="h-4 w-4" /> </Card>
</Button>
)}
</div>
))}
<Button onClick={addStop} variant="outline" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Stop
</Button>
</div>
</div>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Settings</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div> <div>
<label htmlFor="count" className="text-sm font-medium mb-2 block"> <label htmlFor="count" className="text-sm font-medium mb-2 block">
Number of Colors Number of Colors
@@ -120,34 +127,44 @@ export default function GradientPage() {
'Generate Gradient' 'Generate Gradient'
)} )}
</Button> </Button>
</div> </CardContent>
</div> </Card>
</div> </div>
{/* Preview */} {/* Preview */}
<div className="space-y-6"> <div className="space-y-6">
{gradient && gradient.length > 0 && ( {gradient && gradient.length > 0 && (
<> <>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Gradient Preview</h2> <CardHeader>
<div <CardTitle className="text-sm font-medium">Gradient Preview</CardTitle>
className="h-32 rounded-lg" </CardHeader>
style={{ <CardContent>
background: `linear-gradient(to right, ${gradient.join(', ')})`, <div
}} className="h-32 rounded-lg"
/> style={{
</div> background: `linear-gradient(to right, ${gradient.join(', ')})`,
}}
/>
</CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4"> <CardHeader>
Colors ({gradient.length}) <CardTitle className="text-sm font-medium">
</h2> Colors ({gradient.length})
<PaletteGrid colors={gradient} /> </CardTitle>
</div> </CardHeader>
<CardContent>
<PaletteGrid colors={gradient} />
</CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<ExportMenu colors={gradient} /> <CardContent className="pt-6">
</div> <ExportMenu colors={gradient} />
</CardContent>
</Card>
</> </>
)} )}
</div> </div>

View File

@@ -12,6 +12,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useGeneratePalette } from '@/lib/pastel/api/queries'; import { useGeneratePalette } from '@/lib/pastel/api/queries';
import { Loader2, Palette } from 'lucide-react'; import { Loader2, Palette } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -70,14 +71,20 @@ export default function HarmonyPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Controls */} {/* Controls */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Base Color</h2> <CardHeader>
<ColorPicker color={baseColor} onChange={setBaseColor} /> <CardTitle className="text-sm font-medium">Base Color</CardTitle>
</div> </CardHeader>
<CardContent>
<ColorPicker color={baseColor} onChange={setBaseColor} />
</CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Harmony Type</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">Harmony Type</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select <Select
value={harmonyType} value={harmonyType}
onValueChange={(value) => setHarmonyType(value as HarmonyType)} onValueChange={(value) => setHarmonyType(value as HarmonyType)}
@@ -116,32 +123,40 @@ export default function HarmonyPage() {
</> </>
)} )}
</Button> </Button>
</div> </CardContent>
</div> </Card>
</div> </div>
{/* Results */} {/* Results */}
<div className="space-y-6"> <div className="space-y-6">
{palette.length > 0 && ( {palette.length > 0 && (
<> <>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4"> <CardHeader>
Generated Palette ({palette.length} colors) <CardTitle className="text-sm font-medium">
</h2> Generated Palette ({palette.length} colors)
<PaletteGrid colors={palette} /> </CardTitle>
</div> </CardHeader>
<CardContent>
<PaletteGrid colors={palette} />
</CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<ExportMenu colors={palette} /> <CardContent className="pt-6">
</div> <ExportMenu colors={palette} />
</CardContent>
</Card>
</> </>
)} )}
{palette.length === 0 && ( {palette.length === 0 && (
<div className="p-12 border rounded-lg bg-card text-center text-muted-foreground"> <Card>
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" /> <CardContent className="p-12 text-center text-muted-foreground">
<p>Select a harmony type and click Generate to create your palette</p> <Palette className="h-12 w-12 mx-auto mb-4 opacity-50" />
</div> <p>Select a harmony type and click Generate to create your palette</p>
</CardContent>
</Card>
)} )}
</div> </div>
</div> </div>

View File

@@ -10,6 +10,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Card, CardContent } from '@/components/ui/card';
import { useNamedColors } from '@/lib/pastel/api/queries'; import { useNamedColors } from '@/lib/pastel/api/queries';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { parse_color } from '@valknarthing/pastel-wasm'; import { parse_color } from '@valknarthing/pastel-wasm';
@@ -76,46 +77,48 @@ export default function NamedColorsPage() {
</div> </div>
{/* Colors Grid */} {/* Colors Grid */}
<div className="p-6 border rounded-lg bg-card"> <Card>
{isLoading && ( <CardContent className="pt-6">
<div className="flex items-center justify-center py-12"> {isLoading && (
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> <div className="flex items-center justify-center py-12">
</div> <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
)}
{isError && (
<div className="text-center py-12 text-destructive">
Failed to load named colors
</div>
)}
{filteredColors.length > 0 && (
<>
<div className="mb-4 text-sm text-muted-foreground">
Showing {filteredColors.length} colors
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6"> )}
{filteredColors.map((color) => (
<div key={color.name} className="flex flex-col items-center gap-2"> {isError && (
<ColorSwatch color={color.hex} showLabel={false} /> <div className="text-center py-12 text-destructive">
<div className="text-center"> Failed to load named colors
<div className="text-sm font-medium">{color.name}</div> </div>
<div className="text-xs font-mono text-muted-foreground"> )}
{color.hex}
{filteredColors.length > 0 && (
<>
<div className="mb-4 text-sm text-muted-foreground">
Showing {filteredColors.length} colors
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
{filteredColors.map((color) => (
<div key={color.name} className="flex flex-col items-center gap-2">
<ColorSwatch color={color.hex} showLabel={false} />
<div className="text-center">
<div className="text-sm font-medium">{color.name}</div>
<div className="text-xs font-mono text-muted-foreground">
{color.hex}
</div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> </>
</> )}
)}
{filteredColors.length === 0 && !isLoading && !isError && ( {filteredColors.length === 0 && !isLoading && !isError && (
<div className="text-center py-12 text-muted-foreground"> <div className="text-center py-12 text-muted-foreground">
No colors match your search No colors match your search
</div> </div>
)} )}
</div> </CardContent>
</Card>
</div> </div>
</div> </div>
); );

View File

@@ -6,6 +6,7 @@ import { ColorPicker } from '@/components/pastel/ColorPicker';
import { ColorDisplay } from '@/components/pastel/ColorDisplay'; import { ColorDisplay } from '@/components/pastel/ColorDisplay';
import { ColorInfo } from '@/components/pastel/ColorInfo'; import { ColorInfo } from '@/components/pastel/ColorInfo';
import { ManipulationPanel } from '@/components/pastel/ManipulationPanel'; import { ManipulationPanel } from '@/components/pastel/ManipulationPanel';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useColorInfo } from '@/lib/pastel/api/queries'; import { useColorInfo } from '@/lib/pastel/api/queries';
import { useColorHistory } from '@/lib/pastel/stores/historyStore'; import { useColorHistory } from '@/lib/pastel/stores/historyStore';
import { Loader2, Share2, History, X } from 'lucide-react'; import { Loader2, Share2, History, X } from 'lucide-react';
@@ -84,30 +85,36 @@ function PlaygroundContent() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column: Color Picker and Display */} {/* Left Column: Color Picker and Display */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Color Picker</h2> <CardHeader>
<ColorPicker color={color} onChange={setColor} /> <CardTitle className="text-sm font-medium">Color Picker</CardTitle>
</div> </CardHeader>
<CardContent>
<ColorPicker color={color} onChange={setColor} />
</CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card"> <Card>
<div className="flex items-center justify-between mb-4"> <CardHeader className="flex flex-row items-center justify-between space-y-0">
<h2 className="text-sm font-medium">Preview</h2> <CardTitle className="text-sm font-medium">Preview</CardTitle>
<Button onClick={handleShare} variant="outline" size="sm"> <Button onClick={handleShare} variant="outline" size="sm">
<Share2 className="h-4 w-4 mr-2" /> <Share2 className="h-4 w-4 mr-2" />
Share Share
</Button> </Button>
</div> </CardHeader>
<div className="flex justify-center"> <CardContent>
<ColorDisplay color={color} size="xl" /> <div className="flex justify-center">
</div> <ColorDisplay color={color} size="xl" />
</div> </div>
</CardContent>
</Card>
{recentColors.length > 0 && ( {recentColors.length > 0 && (
<div className="p-6 border rounded-lg bg-card"> <Card>
<div className="flex items-center justify-between mb-4"> <CardHeader className="flex flex-row items-center justify-between space-y-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<History className="h-5 w-5" /> <History className="h-5 w-5" />
<h2 className="text-sm font-medium">Recent Colors</h2> <CardTitle className="text-sm font-medium">Recent Colors</CardTitle>
</div> </div>
<Button <Button
onClick={clearHistory} onClick={clearHistory}
@@ -117,68 +124,77 @@ function PlaygroundContent() {
> >
Clear Clear
</Button> </Button>
</div> </CardHeader>
<div className="grid grid-cols-5 gap-2"> <CardContent>
{recentColors.map((entry) => ( <div className="grid grid-cols-5 gap-2">
<div {recentColors.map((entry) => (
key={entry.timestamp} <div
className="group relative aspect-square rounded-lg border-2 border-border hover:border-primary transition-all hover:scale-110 cursor-pointer" key={entry.timestamp}
style={{ backgroundColor: entry.color }} className="group relative aspect-square rounded-lg border-2 border-border hover:border-primary transition-all hover:scale-110 cursor-pointer"
onClick={() => setColor(entry.color)} style={{ backgroundColor: entry.color }}
title={entry.color} onClick={() => setColor(entry.color)}
role="button" title={entry.color}
tabIndex={0} role="button"
onKeyDown={(e) => { tabIndex={0}
if (e.key === 'Enter' || e.key === ' ') { onKeyDown={(e) => {
setColor(entry.color); if (e.key === 'Enter' || e.key === ' ') {
} setColor(entry.color);
}} }
> }}
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30 rounded-lg"> >
<button <div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/30 rounded-lg">
onClick={(e) => { <button
e.stopPropagation(); onClick={(e) => {
removeColor(entry.color); e.stopPropagation();
toast.success('Color removed from history'); removeColor(entry.color);
}} toast.success('Color removed from history');
className="p-1 bg-destructive rounded-full hover:bg-destructive/80" }}
aria-label="Remove color" className="p-1 bg-destructive rounded-full hover:bg-destructive/80"
> aria-label="Remove color"
<X className="h-3 w-3 text-destructive-foreground" /> >
</button> <X className="h-3 w-3 text-destructive-foreground" />
</button>
</div>
</div> </div>
</div> ))}
))} </div>
</div> </CardContent>
</div> </Card>
)} )}
</div> </div>
{/* Right Column: Color Information */} {/* Right Column: Color Information */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Color Information</h2> <CardHeader>
<CardTitle className="text-sm font-medium">Color Information</CardTitle>
</CardHeader>
<CardContent>
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{isLoading && ( {isError && (
<div className="flex items-center justify-center py-12"> <div className="p-4 bg-destructive/10 text-destructive rounded-lg">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> <p className="font-medium">Error loading color information</p>
</div> <p className="text-sm mt-1">{error?.message || 'Unknown error'}</p>
)} </div>
)}
{isError && ( {colorInfo && <ColorInfo info={colorInfo} />}
<div className="p-4 bg-destructive/10 text-destructive rounded-lg"> </CardContent>
<p className="font-medium">Error loading color information</p> </Card>
<p className="text-sm mt-1">{error?.message || 'Unknown error'}</p>
</div>
)}
{colorInfo && <ColorInfo info={colorInfo} />} <Card>
</div> <CardHeader>
<CardTitle className="text-sm font-medium">Color Manipulation</CardTitle>
<div className="p-6 border rounded-lg bg-card"> </CardHeader>
<h2 className="text-sm font-medium mb-4">Color Manipulation</h2> <CardContent>
<ManipulationPanel color={color} onColorChange={setColor} /> <ManipulationPanel color={color} onColorChange={setColor} />
</div> </CardContent>
</Card>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { ColorPicker } from '@/components/pastel/ColorPicker'; import { ColorPicker } from '@/components/pastel/ColorPicker';
import { ColorDisplay } from '@/components/pastel/ColorDisplay'; import { ColorDisplay } from '@/components/pastel/ColorDisplay';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useTextColor } from '@/lib/pastel/api/queries'; import { useTextColor } from '@/lib/pastel/api/queries';
import { Loader2, Palette, Plus, X, CheckCircle2, XCircle } from 'lucide-react'; import { Loader2, Palette, Plus, X, CheckCircle2, XCircle } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -66,9 +67,9 @@ export default function TextColorPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Input */} {/* Input */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 border rounded-lg bg-card"> <Card>
<div className="flex items-center justify-between mb-4"> <CardHeader className="flex flex-row items-center justify-between space-y-0">
<h2 className="text-sm font-medium">Background Colors</h2> <CardTitle className="text-sm font-medium">Background Colors</CardTitle>
<Button <Button
onClick={addBackground} onClick={addBackground}
disabled={backgrounds.length >= 10} disabled={backgrounds.length >= 10}
@@ -78,140 +79,150 @@ export default function TextColorPage() {
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Add
</Button> </Button>
</div> </CardHeader>
<div className="space-y-4"> <CardContent className="space-y-4">
{backgrounds.map((color, index) => ( <div className="space-y-4">
<div key={index} className="flex items-center gap-3"> {backgrounds.map((color, index) => (
<div className="flex-1"> <div key={index} className="flex items-center gap-3">
<ColorPicker <div className="flex-1">
color={color} <ColorPicker
onChange={(newColor) => updateBackground(index, newColor)} color={color}
/> onChange={(newColor) => updateBackground(index, newColor)}
/>
</div>
{backgrounds.length > 1 && (
<Button
onClick={() => removeBackground(index)}
variant="ghost"
size="icon"
>
<X className="h-4 w-4" />
</Button>
)}
</div> </div>
{backgrounds.length > 1 && ( ))}
<Button </div>
onClick={() => removeBackground(index)}
variant="ghost"
size="icon"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
<Button <Button
onClick={handleOptimize} onClick={handleOptimize}
disabled={textColorMutation.isPending || backgrounds.length === 0} disabled={textColorMutation.isPending || backgrounds.length === 0}
className="w-full mt-4" className="w-full mt-4"
> >
{textColorMutation.isPending ? ( {textColorMutation.isPending ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Optimizing.. Optimizing..
</> </>
) : ( ) : (
<> <>
<Palette className="mr-2 h-4 w-4" /> <Palette className="mr-2 h-4 w-4" />
Optimize Text Colors Optimize Text Colors
</> </>
)} )}
</Button> </Button>
</div> </CardContent>
</Card>
<div className="p-6 border rounded-lg bg-card bg-blue-50 dark:bg-blue-950/20"> <Card className="bg-blue-50 dark:bg-blue-950/20 border-blue-100 dark:border-blue-900/30 shadow-none">
<h3 className="font-semibold mb-2">How it works</h3> <CardContent className="pt-6">
<p className="text-sm text-muted-foreground"> <h3 className="font-semibold mb-2">How it works</h3>
This tool analyzes each background color and automatically selects either black <p className="text-sm text-muted-foreground">
or white text to ensure maximum readability. The algorithm guarantees WCAG AA This tool analyzes each background color and automatically selects either black
compliance (4.5:1 contrast ratio) for normal text or white text to ensure maximum readability. The algorithm guarantees WCAG AA
</p> compliance (4.5:1 contrast ratio) for normal text
</div> </p>
</CardContent>
</Card>
</div> </div>
{/* Results */} {/* Results */}
<div className="space-y-6"> <div className="space-y-6">
{results.length > 0 ? ( {results.length > 0 ? (
<> <>
<div className="p-6 border rounded-lg bg-card"> <Card>
<h2 className="text-sm font-medium mb-4">Optimized Results</h2> <CardHeader>
<div className="space-y-4"> <CardTitle className="text-sm font-medium">Optimized Results</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{results.map((result, index) => ( {results.map((result, index) => (
<div <Card
key={index} key={index}
className="p-4 border rounded-lg" className="overflow-hidden shadow-none"
style={{ backgroundColor: result.background }} style={{ backgroundColor: result.background }}
> >
<div className="flex items-center justify-between mb-3"> <CardContent className="p-4">
<div className="flex items-center gap-3"> <div className="flex items-center justify-between mb-3">
<ColorDisplay color={result.background} size="sm" /> <div className="flex items-center gap-3">
<code className="text-sm font-mono text-inherit"> <ColorDisplay color={result.background} size="sm" />
{result.background} <code className="text-sm font-mono text-inherit">
</code> {result.background}
</code>
</div>
</div> </div>
</div>
<div <div
className="p-4 rounded border-2" className="p-4 rounded border-2"
style={{ style={{
backgroundColor: result.background, backgroundColor: result.background,
color: result.textcolor, color: result.textcolor,
borderColor: result.textcolor, borderColor: result.textcolor,
}} }}
> >
<p className="font-semibold mb-2" style={{ color: result.textcolor }}> <p className="font-semibold mb-2" style={{ color: result.textcolor }}>
Sample Text Preview Sample Text Preview
</p> </p>
<p className="text-sm" style={{ color: result.textcolor }}> <p className="text-sm" style={{ color: result.textcolor }}>
The quick brown fox jumps over the lazy dog. This is how your text The quick brown fox jumps over the lazy dog. This is how your text
will look on this background color will look on this background color
</p> </p>
</div> </div>
<div className="mt-3 grid grid-cols-2 gap-3 text-sm"> <div className="mt-3 grid grid-cols-2 gap-3 text-sm">
<div> <div>
<span className="text-muted-foreground">Text Color: </span> <span className="text-muted-foreground">Text Color: </span>
<code className="font-mono">{result.textcolor}</code> <code className="font-mono">{result.textcolor}</code>
</div>
<div>
<span className="text-muted-foreground">Contrast: </span>
<span className="font-medium">
{result.contrast_ratio.toFixed(2)}:1
</span>
</div>
<div className="flex items-center gap-2">
{result.wcag_aa ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className={result.wcag_aa ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
WCAG AA
</span>
</div>
<div className="flex items-center gap-2">
{result.wcag_aaa ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-yellow-500" />
)}
<span className={result.wcag_aaa ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'}>
WCAG AAA
</span>
</div>
</div> </div>
<div> </CardContent>
<span className="text-muted-foreground">Contrast: </span> </Card>
<span className="font-medium">
{result.contrast_ratio.toFixed(2)}:1
</span>
</div>
<div className="flex items-center gap-2">
{result.wcag_aa ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className={result.wcag_aa ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
WCAG AA
</span>
</div>
<div className="flex items-center gap-2">
{result.wcag_aaa ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-yellow-500" />
)}
<span className={result.wcag_aaa ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'}>
WCAG AAA
</span>
</div>
</div>
</div>
))} ))}
</div> </CardContent>
</div> </Card>
</> </>
) : ( ) : (
<div className="p-12 border rounded-lg bg-card text-center text-muted-foreground"> <Card>
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" /> <CardContent className="p-12 text-center text-muted-foreground">
<p>Add background colors and click Optimize to see results</p> <Palette className="h-12 w-12 mx-auto mb-4 opacity-50" />
</div> <p>Add background colors and click Optimize to see results</p>
</CardContent>
</Card>
)} )}
</div> </div>
</div> </div>