diff --git a/app/(app)/figlet/page.tsx b/app/(app)/figlet/page.tsx new file mode 100644 index 0000000..64d807a --- /dev/null +++ b/app/(app)/figlet/page.tsx @@ -0,0 +1,13 @@ +import { FigletConverter } from '@/components/figlet/FigletConverter'; + +export default function FigletPage() { + return ( +
+
+

Figlet UI

+

ASCII Art Text Generator with 373 Fonts

+
+ +
+ ); +} diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx new file mode 100644 index 0000000..65c0b2e --- /dev/null +++ b/app/(app)/layout.tsx @@ -0,0 +1,16 @@ +import { AppShell } from '@/components/layout/AppShell'; +import { Providers } from '@/components/providers/Providers'; + +export default function AppLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/app/(app)/pastel/accessibility/colorblind/page.tsx b/app/(app)/pastel/accessibility/colorblind/page.tsx new file mode 100644 index 0000000..6cfc231 --- /dev/null +++ b/app/(app)/pastel/accessibility/colorblind/page.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { useState } from 'react'; +import { ColorPicker } from '@/components/pastel/color/ColorPicker'; +import { ColorDisplay } from '@/components/pastel/color/ColorDisplay'; +import { Button } from '@/components/ui/Button'; +import { Select } from '@/components/ui/Select'; +import { useSimulateColorBlindness } from '@/lib/pastel/api/queries'; +import { Loader2, Eye, Plus, X } from 'lucide-react'; +import { toast } from 'sonner'; + +type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia'; + +export default function ColorBlindPage() { + const [colors, setColors] = useState(['#ff0099']); + const [blindnessType, setBlindnessType] = useState('protanopia'); + const [simulations, setSimulations] = useState< + Array<{ input: string; output: string; difference_percentage: number }> + >([]); + + const simulateMutation = useSimulateColorBlindness(); + + const handleSimulate = async () => { + try { + const result = await simulateMutation.mutateAsync({ + colors, + type: blindnessType, + }); + setSimulations(result.colors); + toast.success(`Simulated ${blindnessType}`); + } catch (error) { + toast.error('Failed to simulate color blindness'); + console.error(error); + } + }; + + const addColor = () => { + if (colors.length < 10) { + setColors([...colors, '#000000']); + } + }; + + const removeColor = (index: number) => { + if (colors.length > 1) { + setColors(colors.filter((_, i) => i !== index)); + } + }; + + const updateColor = (index: number, color: string) => { + const newColors = [...colors]; + newColors[index] = color; + setColors(newColors); + }; + + const typeDescriptions: Record = { + protanopia: 'Red-blind (affects ~1% of males)', + deuteranopia: 'Green-blind (affects ~1% of males)', + tritanopia: 'Blue-blind (rare, affects ~0.001%)', + }; + + return ( +
+
+
+

Color Blindness Simulator

+

+ Simulate how colors appear with different types of color blindness +

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

Colors to Test

+ +
+ +
+ {colors.map((color, index) => ( +
+
+ updateColor(index, newColor)} + /> +
+ {colors.length > 1 && ( + + )} +
+ ))} +
+
+ +
+

Blindness Type

+
+ + +

+ {typeDescriptions[blindnessType]} +

+ + +
+
+
+ + {/* Results */} +
+ {simulations.length > 0 ? ( + <> +
+

Simulation Results

+

+ Compare original colors (left) with how they appear to people with{' '} + {blindnessType} (right) +

+ +
+ {simulations.map((sim, index) => ( +
+
+

+ Original +

+
+ + {sim.input} +
+
+ +
+

+ As Seen ({sim.difference_percentage.toFixed(1)}% difference) +

+
+ + {sim.output} +
+
+
+ ))} +
+
+ +
+

+ + Accessibility Tip +

+

+ Ensure important information isn't conveyed by color alone. Use text + labels, patterns, or icons to make your design accessible to everyone +

+
+ + ) : ( +
+ +

Add colors and click Simulate to see how they appear

+

with different types of color blindness

+
+ )} +
+
+
+
+ ); +} diff --git a/app/(app)/pastel/accessibility/contrast/page.tsx b/app/(app)/pastel/accessibility/contrast/page.tsx new file mode 100644 index 0000000..a8010c3 --- /dev/null +++ b/app/(app)/pastel/accessibility/contrast/page.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ColorPicker } from '@/components/pastel/color/ColorPicker'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; +import { getContrastRatio, hexToRgb, checkWCAGCompliance } from '@/lib/pastel/utils/color'; +import { ArrowLeftRight, Check, X } from 'lucide-react'; + +export default function ContrastPage() { + const [foreground, setForeground] = useState('#000000'); + const [background, setBackground] = useState('#ffffff'); + const [ratio, setRatio] = useState(null); + const [compliance, setCompliance] = useState(null); + + useEffect(() => { + const fgRgb = hexToRgb(foreground); + const bgRgb = hexToRgb(background); + + if (fgRgb && bgRgb) { + const contrastRatio = getContrastRatio(fgRgb, bgRgb); + setRatio(contrastRatio); + setCompliance(checkWCAGCompliance(contrastRatio)); + } + }, [foreground, background]); + + const swapColors = () => { + const temp = foreground; + setForeground(background); + setBackground(temp); + }; + + const ComplianceItem = ({ + label, + passed, + }: { + label: string; + passed: boolean; + }) => ( +
+ {label} + + {passed ? ( + <> + + Pass + + ) : ( + <> + + Fail + + )} + +
+ ); + + return ( +
+
+
+

Contrast Checker

+

+ Test color combinations for WCAG 2.1 compliance +

+
+ +
+ {/* Color Pickers */} +
+
+
+

Foreground Color

+
+ +
+ +
+ +
+ +
+

Background Color

+ +
+
+ + {/* Results */} +
+ {/* Preview */} +
+

Preview

+
+

Normal Text (16px)

+

Large Text (24px)

+
+
+ + {/* Contrast Ratio */} + {ratio !== null && ( +
+

Contrast Ratio

+
+
{ratio.toFixed(2)}:1
+

+ {ratio >= 7 + ? 'Excellent contrast' + : ratio >= 4.5 + ? 'Good contrast' + : ratio >= 3 + ? 'Minimum contrast' + : 'Poor contrast'} +

+
+
+ )} + + {/* WCAG Compliance */} + {compliance && ( +
+

WCAG 2.1 Compliance

+
+
+

Level AA

+
+ + + +
+
+ +
+

Level AAA

+
+ + +
+
+
+
+ )} +
+
+
+
+ ); +} diff --git a/app/(app)/pastel/accessibility/page.tsx b/app/(app)/pastel/accessibility/page.tsx new file mode 100644 index 0000000..ec8c370 --- /dev/null +++ b/app/(app)/pastel/accessibility/page.tsx @@ -0,0 +1,96 @@ +import Link from 'next/link'; +import { Contrast, Eye, Palette } from 'lucide-react'; + +export default function AccessibilityPage() { + const tools = [ + { + title: 'Contrast Checker', + description: 'Test color combinations for WCAG 2.1 AA and AAA compliance', + href: '/pastel/accessibility/contrast', + icon: Contrast, + features: ['WCAG 2.1 standards', 'AA/AAA ratings', 'Live preview'], + }, + { + title: 'Color Blindness Simulator', + description: 'Simulate how colors appear with different types of color blindness', + href: '/pastel/accessibility/colorblind', + icon: Eye, + features: ['Protanopia', 'Deuteranopia', 'Tritanopia'], + }, + { + title: 'Text Color Optimizer', + description: 'Find the best text color for any background automatically', + href: '/pastel/accessibility/textcolor', + icon: Palette, + features: ['Automatic optimization', 'WCAG guaranteed', 'Light/dark options'], + }, + ]; + + return ( +
+
+
+

Accessibility Tools

+

+ Ensure your colors are accessible to everyone +

+
+ +
+ {tools.map((tool) => { + const Icon = tool.icon; + return ( + +
+ +
+

{tool.title}

+

{tool.description}

+
    + {tool.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + ); + })} +
+ + {/* Educational Content */} +
+

About WCAG 2.1

+
+

+ The Web Content Accessibility Guidelines (WCAG) 2.1 provide standards for making web + content more accessible to people with disabilities +

+
+
+

Level AA (Minimum)

+
    +
  • • Normal text: 4.5:1 contrast ratio
  • +
  • • Large text: 3:1 contrast ratio
  • +
  • • UI components: 3:1 contrast ratio
  • +
+
+
+

Level AAA (Enhanced)

+
    +
  • • Normal text: 7:1 contrast ratio
  • +
  • • Large text: 4.5:1 contrast ratio
  • +
+
+
+
+
+
+
+ ); +} diff --git a/app/(app)/pastel/accessibility/textcolor/page.tsx b/app/(app)/pastel/accessibility/textcolor/page.tsx new file mode 100644 index 0000000..1fcf489 --- /dev/null +++ b/app/(app)/pastel/accessibility/textcolor/page.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { useState } from 'react'; +import { ColorPicker } from '@/components/pastel/color/ColorPicker'; +import { ColorDisplay } from '@/components/pastel/color/ColorDisplay'; +import { Button } from '@/components/ui/Button'; +import { useTextColor } from '@/lib/pastel/api/queries'; +import { Loader2, Palette, Plus, X, CheckCircle2, XCircle } from 'lucide-react'; +import { toast } from 'sonner'; + +export default function TextColorPage() { + const [backgrounds, setBackgrounds] = useState(['#ff0099']); + const [results, setResults] = useState< + Array<{ + background: string; + textcolor: string; + contrast_ratio: number; + wcag_aa: boolean; + wcag_aaa: boolean; + }> + >([]); + + const textColorMutation = useTextColor(); + + const handleOptimize = async () => { + try { + const result = await textColorMutation.mutateAsync({ + backgrounds, + }); + setResults(result.colors); + toast.success(`Optimized text colors for ${result.colors.length} background(s)`); + } catch (error) { + toast.error('Failed to optimize text colors'); + console.error(error); + } + }; + + const addBackground = () => { + if (backgrounds.length < 10) { + setBackgrounds([...backgrounds, '#000000']); + } + }; + + const removeBackground = (index: number) => { + if (backgrounds.length > 1) { + setBackgrounds(backgrounds.filter((_, i) => i !== index)); + } + }; + + const updateBackground = (index: number, color: string) => { + const newBackgrounds = [...backgrounds]; + newBackgrounds[index] = color; + setBackgrounds(newBackgrounds); + }; + + return ( +
+
+
+

Text Color Optimizer

+

+ Automatically find the best text color (black or white) for any background color +

+
+ +
+ {/* Input */} +
+
+
+

Background Colors

+ +
+ +
+ {backgrounds.map((color, index) => ( +
+
+ updateBackground(index, newColor)} + /> +
+ {backgrounds.length > 1 && ( + + )} +
+ ))} +
+ + +
+ +
+

How it works

+

+ This tool analyzes each background color and automatically selects either black + or white text to ensure maximum readability. The algorithm guarantees WCAG AA + compliance (4.5:1 contrast ratio) for normal text +

+
+
+ + {/* Results */} +
+ {results.length > 0 ? ( + <> +
+

Optimized Results

+
+ {results.map((result, index) => ( +
+
+
+ + + {result.background} + +
+
+ +
+

+ Sample Text Preview +

+

+ The quick brown fox jumps over the lazy dog. This is how your text + will look on this background color +

+
+ +
+
+ Text Color: + {result.textcolor} +
+
+ Contrast: + + {result.contrast_ratio.toFixed(2)}:1 + +
+
+ {result.wcag_aa ? ( + + ) : ( + + )} + + WCAG AA + +
+
+ {result.wcag_aaa ? ( + + ) : ( + + )} + + WCAG AAA + +
+
+
+ ))} +
+
+ + ) : ( +
+ +

Add background colors and click Optimize to see results

+
+ )} +
+
+
+
+ ); +} diff --git a/app/(app)/pastel/batch/page.tsx b/app/(app)/pastel/batch/page.tsx new file mode 100644 index 0000000..fc96210 --- /dev/null +++ b/app/(app)/pastel/batch/page.tsx @@ -0,0 +1,193 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/Button'; +import { Select } from '@/components/ui/Select'; +import { Input } from '@/components/ui/Input'; +import { PaletteGrid } from '@/components/pastel/color/PaletteGrid'; +import { ExportMenu } from '@/components/pastel/tools/ExportMenu'; +import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/pastel/api/queries'; +import { Loader2, Upload, Download } from 'lucide-react'; +import { toast } from 'sonner'; + +type Operation = 'lighten' | 'darken' | 'saturate' | 'desaturate' | 'rotate'; + +export default function BatchPage() { + const [inputColors, setInputColors] = useState(''); + const [operation, setOperation] = useState('lighten'); + const [amount, setAmount] = useState(0.2); + const [outputColors, setOutputColors] = useState([]); + + const lightenMutation = useLighten(); + const darkenMutation = useDarken(); + const saturateMutation = useSaturate(); + const desaturateMutation = useDesaturate(); + const rotateMutation = useRotate(); + + const parseColors = (text: string): string[] => { + // Parse colors from text (one per line, or comma-separated) + return text + .split(/[\n,]/) + .map((c) => c.trim()) + .filter((c) => c.length > 0 && c.match(/^#?[0-9a-fA-F]{3,8}$/)); + }; + + const handleProcess = async () => { + const colors = parseColors(inputColors); + + if (colors.length === 0) { + toast.error('No valid colors found'); + return; + } + + if (colors.length > 100) { + toast.error('Maximum 100 colors allowed'); + return; + } + + try { + let result; + + switch (operation) { + case 'lighten': + result = await lightenMutation.mutateAsync({ colors, amount }); + break; + case 'darken': + result = await darkenMutation.mutateAsync({ colors, amount }); + break; + case 'saturate': + result = await saturateMutation.mutateAsync({ colors, amount }); + break; + case 'desaturate': + result = await desaturateMutation.mutateAsync({ colors, amount }); + break; + case 'rotate': + result = await rotateMutation.mutateAsync({ colors, amount: amount * 360 }); + break; + } + + // Extract output colors from the result + const processed = result.colors.map((c) => c.output); + setOutputColors(processed); + toast.success(`Processed ${processed.length} colors`); + } catch (error) { + toast.error('Failed to process colors'); + console.error(error); + } + }; + + const isPending = + lightenMutation.isPending || + darkenMutation.isPending || + saturateMutation.isPending || + desaturateMutation.isPending || + rotateMutation.isPending; + + return ( +
+
+
+

Batch Operations

+

+ Process multiple colors at once with manipulation operations +

+
+ +
+ {/* Input */} +
+
+

Input Colors

+

+ Enter colors (one per line or comma-separated). Supports hex format +

+ +