refactor: use shadcn Card component in pastel app
This commit is contained in:
@@ -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 #ff5533 #3355ff"
|
placeholder="#ff0099, #00ff99, #9900ff #ff5533 #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>
|
||||||
|
|||||||
@@ -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'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'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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user