refactor: update UI component usage to match latest shadcn APIs
This commit is contained in:
@@ -1,9 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/Select';
|
import {
|
||||||
import { Input } from '@/components/ui/Input';
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
||||||
import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/pastel/api/queries';
|
import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/pastel/api/queries';
|
||||||
@@ -118,15 +124,19 @@ export default function BatchPage() {
|
|||||||
<h2 className="text-sm font-medium mb-4">Operation</h2>
|
<h2 className="text-sm font-medium mb-4">Operation</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Select
|
<Select
|
||||||
label="Operation"
|
|
||||||
value={operation}
|
value={operation}
|
||||||
onChange={(e) => setOperation(e.target.value as Operation)}
|
onValueChange={(value) => setOperation(value as Operation)}
|
||||||
>
|
>
|
||||||
<option value="lighten">Lighten</option>
|
<SelectTrigger>
|
||||||
<option value="darken">Darken</option>
|
<SelectValue placeholder="Select operation" />
|
||||||
<option value="saturate">Saturate</option>
|
</SelectTrigger>
|
||||||
<option value="desaturate">Desaturate</option>
|
<SelectContent>
|
||||||
<option value="rotate">Rotate Hue</option>
|
<SelectItem value="lighten">Lighten</SelectItem>
|
||||||
|
<SelectItem value="darken">Darken</SelectItem>
|
||||||
|
<SelectItem value="saturate">Saturate</SelectItem>
|
||||||
|
<SelectItem value="desaturate">Desaturate</SelectItem>
|
||||||
|
<SelectItem value="rotate">Rotate Hue</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -3,8 +3,14 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
||||||
import { ColorDisplay } from '@/components/pastel/color/ColorDisplay';
|
import { ColorDisplay } from '@/components/pastel/color/ColorDisplay';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/Select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
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';
|
||||||
@@ -113,13 +119,17 @@ export default function ColorBlindPage() {
|
|||||||
<h2 className="text-sm font-medium mb-4">Blindness Type</h2>
|
<h2 className="text-sm font-medium mb-4">Blindness Type</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Select
|
<Select
|
||||||
label="Type"
|
|
||||||
value={blindnessType}
|
value={blindnessType}
|
||||||
onChange={(e) => setBlindnessType(e.target.value as ColorBlindnessType)}
|
onValueChange={(value) => setBlindnessType(value as ColorBlindnessType)}
|
||||||
>
|
>
|
||||||
<option value="protanopia">Protanopia (Red-blind)</option>
|
<SelectTrigger>
|
||||||
<option value="deuteranopia">Deuteranopia (Green-blind)</option>
|
<SelectValue placeholder="Select type" />
|
||||||
<option value="tritanopia">Tritanopia (Blue-blind)</option>
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="protanopia">Protanopia (Red-blind)</SelectItem>
|
||||||
|
<SelectItem value="deuteranopia">Deuteranopia (Green-blind)</SelectItem>
|
||||||
|
<SelectItem value="tritanopia">Tritanopia (Blue-blind)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
import { ColorPicker } from '@/components/pastel/color/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 { 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';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export default function ContrastPage() {
|
|||||||
}) => (
|
}) => (
|
||||||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||||||
<span className="text-sm">{label}</span>
|
<span className="text-sm">{label}</span>
|
||||||
<Badge variant={passed ? 'success' : 'destructive'}>
|
<Badge variant={passed ? 'secondary' : 'destructive'}>
|
||||||
{passed ? (
|
{passed ? (
|
||||||
<>
|
<>
|
||||||
<Check className="h-3 w-3 mr-1" />
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
|||||||
@@ -3,9 +3,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
import { ExportMenu } from '@/components/pastel/tools/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 { Select } from '@/components/ui/Select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
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';
|
||||||
@@ -65,14 +71,23 @@ export default function DistinctPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium block">
|
||||||
|
Distance Metric
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
label="Distance Metric"
|
|
||||||
value={metric}
|
value={metric}
|
||||||
onChange={(e) => setMetric(e.target.value as 'cie76' | 'ciede2000')}
|
onValueChange={(value) => setMetric(value as 'cie76' | 'ciede2000')}
|
||||||
>
|
>
|
||||||
<option value="cie76">CIE76 (Faster)</option>
|
<SelectTrigger>
|
||||||
<option value="ciede2000">CIEDE2000 (More Accurate)</option>
|
<SelectValue placeholder="Select metric" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cie76">CIE76 (Faster)</SelectItem>
|
||||||
|
<SelectItem value="ciede2000">CIEDE2000 (More Accurate)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useState } from 'react';
|
|||||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
||||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
import { ExportMenu } from '@/components/pastel/tools/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 { 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';
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ import { useState } from 'react';
|
|||||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
||||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/Select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
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';
|
||||||
@@ -73,16 +79,20 @@ export default function HarmonyPage() {
|
|||||||
<h2 className="text-sm font-medium mb-4">Harmony Type</h2>
|
<h2 className="text-sm font-medium mb-4">Harmony Type</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Select
|
<Select
|
||||||
label="Harmony"
|
|
||||||
value={harmonyType}
|
value={harmonyType}
|
||||||
onChange={(e) => setHarmonyType(e.target.value as HarmonyType)}
|
onValueChange={(value) => setHarmonyType(value as HarmonyType)}
|
||||||
>
|
>
|
||||||
<option value="monochromatic">Monochromatic</option>
|
<SelectTrigger>
|
||||||
<option value="analogous">Analogous</option>
|
<SelectValue placeholder="Select harmony" />
|
||||||
<option value="complementary">Complementary</option>
|
</SelectTrigger>
|
||||||
<option value="split-complementary">Split-Complementary</option>
|
<SelectContent>
|
||||||
<option value="triadic">Triadic</option>
|
<SelectItem value="monochromatic">Monochromatic</SelectItem>
|
||||||
<option value="tetradic">Tetradic (Square)</option>
|
<SelectItem value="analogous">Analogous</SelectItem>
|
||||||
|
<SelectItem value="complementary">Complementary</SelectItem>
|
||||||
|
<SelectItem value="split-complementary">Split-Complementary</SelectItem>
|
||||||
|
<SelectItem value="triadic">Triadic</SelectItem>
|
||||||
|
<SelectItem value="tetradic">Tetradic (Square)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ColorSwatch } from '@/components/pastel/color/ColorSwatch';
|
import { ColorSwatch } from '@/components/pastel/color/ColorSwatch';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select } from '@/components/ui/Select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
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';
|
||||||
@@ -57,9 +63,14 @@ export default function NamedColorsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-48">
|
<div className="w-full sm:w-48">
|
||||||
<Select value={sortBy} onChange={(e) => setSortBy(e.target.value as 'name' | 'hue')}>
|
<Select value={sortBy} onValueChange={(value) => setSortBy(value as 'name' | 'hue')}>
|
||||||
<option value="name">Sort by Name</option>
|
<SelectTrigger>
|
||||||
<option value="hue">Sort by Hue</option>
|
<SelectValue placeholder="Sort by..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="name">Sort by Name</SelectItem>
|
||||||
|
<SelectItem value="hue">Sort by Hue</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ManipulationPanel } from '@/components/pastel/tools/ManipulationPanel';
|
|||||||
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';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
function PlaygroundContent() {
|
function PlaygroundContent() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
||||||
import { ColorDisplay } from '@/components/pastel/color/ColorDisplay';
|
import { ColorDisplay } from '@/components/pastel/color/ColorDisplay';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
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';
|
||||||
|
|||||||
114
app/globals.css
114
app/globals.css
@@ -1,4 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@source "../components/*.{js,ts,jsx,tsx}";
|
@source "../components/*.{js,ts,jsx,tsx}";
|
||||||
@source "../components/ui/*.{js,ts,jsx,tsx}";
|
@source "../components/ui/*.{js,ts,jsx,tsx}";
|
||||||
@@ -222,3 +226,113 @@ html {
|
|||||||
@utility gradient-brand {
|
@utility gradient-brand {
|
||||||
background: linear-gradient(to right, #a78bfa, #f472b6, #22d3ee);
|
background: linear-gradient(to right, #a78bfa, #f472b6, #22d3ee);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
23
components.json
Normal file
23
components.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { toPng } from 'html-to-image';
|
import { toPng } from 'html-to-image';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/Skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from "@/components/ui/empty"
|
||||||
import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type } from 'lucide-react';
|
import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -190,12 +196,15 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare
|
|||||||
{text}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<Empty>
|
||||||
icon={Type}
|
<EmptyHeader>
|
||||||
title="Start typing to see your ASCII art"
|
<EmptyMedia variant="icon">
|
||||||
description="Enter text in the input field above to generate ASCII art with the selected font"
|
<Type />
|
||||||
className="py-8"
|
</EmptyMedia>
|
||||||
/>
|
<EmptyTitle>Start typing to see your ASCII art</EmptyTitle>
|
||||||
|
<EmptyDescription>Enter text in the input field above to generate ASCII art with the selected font</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from "@/components/ui/empty"
|
||||||
import { Search, X, Heart, Clock, List, Shuffle } from 'lucide-react';
|
import { Search, X, Heart, Clock, List, Shuffle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { FigletFont } from '@/types/figlet';
|
import type { FigletFont } from '@/types/figlet';
|
||||||
import { getFavorites, getRecentFonts, toggleFavorite, isFavorite } from '@/lib/storage/favorites';
|
import { getFavorites, getRecentFonts, toggleFavorite, isFavorite } from '@/lib/storage/favorites';
|
||||||
|
|
||||||
@@ -162,16 +168,20 @@ export function FontSelector({
|
|||||||
{/* Font List */}
|
{/* Font List */}
|
||||||
<div className="flex-1 overflow-y-auto space-y-1 pr-2">
|
<div className="flex-1 overflow-y-auto space-y-1 pr-2">
|
||||||
{filteredFonts.length === 0 ? (
|
{filteredFonts.length === 0 ? (
|
||||||
<EmptyState
|
<Empty>
|
||||||
icon={filter === 'favorites' ? Heart : (filter === 'recent' ? Clock : Search)}
|
<EmptyHeader>
|
||||||
title={
|
<EmptyMedia variant="icon">
|
||||||
|
{filter === 'favorites' ? <Heart /> : (filter === 'recent' ? <Clock /> : <Search />)}
|
||||||
|
</EmptyMedia>
|
||||||
|
<EmptyTitle>{
|
||||||
filter === 'favorites'
|
filter === 'favorites'
|
||||||
? 'No favorite fonts yet'
|
? 'No favorite fonts yet'
|
||||||
: filter === 'recent'
|
: filter === 'recent'
|
||||||
? 'No recent fonts'
|
? 'No recent fonts'
|
||||||
: 'No fonts found'
|
: 'No fonts found'
|
||||||
}
|
}</EmptyTitle>
|
||||||
description={
|
<EmptyDescription>
|
||||||
|
{
|
||||||
filter === 'favorites'
|
filter === 'favorites'
|
||||||
? 'Click the heart icon on any font to add it to your favorites'
|
? 'Click the heart icon on any font to add it to your favorites'
|
||||||
: filter === 'recent'
|
: filter === 'recent'
|
||||||
@@ -180,8 +190,9 @@ export function FontSelector({
|
|||||||
? 'Try a different search term'
|
? 'Try a different search term'
|
||||||
: 'Loading fonts...'
|
: 'Loading fonts...'
|
||||||
}
|
}
|
||||||
className="py-8"
|
</EmptyDescription>
|
||||||
/>
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
) : (
|
) : (
|
||||||
filteredFonts.map((font) => (
|
filteredFonts.map((font) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { Menu, Search, Bell, ChevronRight, Moon, Sun, X } from 'lucide-react';
|
import { Menu, Search, Bell, ChevronRight, Moon, Sun, X } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useTheme } from '@/components/providers/ThemeProvider';
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { useSidebar } from './SidebarProvider';
|
import { useSidebar } from './SidebarProvider';
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { useSidebar } from './SidebarProvider';
|
import { useSidebar } from './SidebarProvider';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ColorInfo as ColorInfoType } from '@/lib/pastel/api/types';
|
import { ColorInfo as ColorInfoType } from '@/lib/pastel/api/types';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Copy } from 'lucide-react';
|
import { Copy } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { HexColorPicker } from 'react-colorful';
|
import { HexColorPicker } from 'react-colorful';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
|
|
||||||
interface ColorPickerProps {
|
interface ColorPickerProps {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Moon, Sun } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from '@/components/providers/ThemeProvider';
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/Select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Download, Copy, Check } from 'lucide-react';
|
import { Download, Copy, Check } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -84,13 +90,18 @@ export function ExportMenu({ colors, className }: ExportMenuProps) {
|
|||||||
<h3 className="text-sm font-medium mb-2">Export Palette</h3>
|
<h3 className="text-sm font-medium mb-2">Export Palette</h3>
|
||||||
<Select
|
<Select
|
||||||
value={format}
|
value={format}
|
||||||
onChange={(e) => setFormat(e.target.value as ExportFormat)}
|
onValueChange={(value) => setFormat(value as ExportFormat)}
|
||||||
>
|
>
|
||||||
<option value="css">CSS Variables</option>
|
<SelectTrigger>
|
||||||
<option value="scss">SCSS Variables</option>
|
<SelectValue placeholder="Select format" />
|
||||||
<option value="tailwind">Tailwind Config</option>
|
</SelectTrigger>
|
||||||
<option value="json">JSON</option>
|
<SelectContent>
|
||||||
<option value="javascript">JavaScript Array</option>
|
<SelectItem value="css">CSS Variables</SelectItem>
|
||||||
|
<SelectItem value="scss">SCSS Variables</SelectItem>
|
||||||
|
<SelectItem value="tailwind">Tailwind Config</SelectItem>
|
||||||
|
<SelectItem value="json">JSON</SelectItem>
|
||||||
|
<SelectItem value="javascript">JavaScript Array</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Slider } from '@/components/ui/Slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
useLighten,
|
useLighten,
|
||||||
useDarken,
|
useDarken,
|
||||||
@@ -131,15 +131,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Lighten */}
|
{/* Lighten */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Lighten</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{(lightenAmount * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
label="Lighten"
|
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.05}
|
step={0.05}
|
||||||
value={lightenAmount}
|
value={[lightenAmount]}
|
||||||
onChange={(e) => setLightenAmount(parseFloat(e.target.value))}
|
onValueChange={(vals) => setLightenAmount(vals[0])}
|
||||||
suffix="%"
|
|
||||||
showValue
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
|
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
|
||||||
Apply Lighten
|
Apply Lighten
|
||||||
@@ -148,15 +149,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
|
|
||||||
{/* Darken */}
|
{/* Darken */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Darken</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{(darkenAmount * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
label="Darken"
|
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.05}
|
step={0.05}
|
||||||
value={darkenAmount}
|
value={[darkenAmount]}
|
||||||
onChange={(e) => setDarkenAmount(parseFloat(e.target.value))}
|
onValueChange={(vals) => setDarkenAmount(vals[0])}
|
||||||
suffix="%"
|
|
||||||
showValue
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
|
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
|
||||||
Apply Darken
|
Apply Darken
|
||||||
@@ -165,15 +167,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
|
|
||||||
{/* Saturate */}
|
{/* Saturate */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Saturate</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{(saturateAmount * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
label="Saturate"
|
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.05}
|
step={0.05}
|
||||||
value={saturateAmount}
|
value={[saturateAmount]}
|
||||||
onChange={(e) => setSaturateAmount(parseFloat(e.target.value))}
|
onValueChange={(vals) => setSaturateAmount(vals[0])}
|
||||||
suffix="%"
|
|
||||||
showValue
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
|
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
|
||||||
Apply Saturate
|
Apply Saturate
|
||||||
@@ -182,15 +185,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
|
|
||||||
{/* Desaturate */}
|
{/* Desaturate */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Desaturate</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{(desaturateAmount * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
label="Desaturate"
|
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.05}
|
step={0.05}
|
||||||
value={desaturateAmount}
|
value={[desaturateAmount]}
|
||||||
onChange={(e) => setDesaturateAmount(parseFloat(e.target.value))}
|
onValueChange={(vals) => setDesaturateAmount(vals[0])}
|
||||||
suffix="%"
|
|
||||||
showValue
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
|
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
|
||||||
Apply Desaturate
|
Apply Desaturate
|
||||||
@@ -199,15 +203,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
|
|
||||||
{/* Rotate Hue */}
|
{/* Rotate Hue */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Rotate Hue</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{rotateAmount}°</span>
|
||||||
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
label="Rotate Hue"
|
|
||||||
min={-180}
|
min={-180}
|
||||||
max={180}
|
max={180}
|
||||||
step={5}
|
step={5}
|
||||||
value={rotateAmount}
|
value={[rotateAmount]}
|
||||||
onChange={(e) => setRotateAmount(parseInt(e.target.value))}
|
onValueChange={(vals) => setRotateAmount(vals[0])}
|
||||||
suffix="°"
|
|
||||||
showValue
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
|
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
|
||||||
Apply Rotation
|
Apply Rotation
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
variant?: 'default' | 'success' | 'warning' | 'destructive' | 'outline';
|
|
||||||
}
|
|
||||||
|
|
||||||
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
|
||||||
({ className, variant = 'default', ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
|
||||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
||||||
{
|
|
||||||
'bg-primary text-primary-foreground': variant === 'default',
|
|
||||||
'bg-green-500 text-white': variant === 'success',
|
|
||||||
'bg-yellow-500 text-white': variant === 'warning',
|
|
||||||
'bg-destructive text-destructive-foreground': variant === 'destructive',
|
|
||||||
'border border-input': variant === 'outline',
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Badge.displayName = 'Badge';
|
|
||||||
|
|
||||||
export { Badge };
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
||||||
variant?: 'default' | 'outline' | 'ghost' | 'destructive' | 'secondary';
|
|
||||||
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, variant = 'default', size = 'default', ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center justify-center rounded-xl font-semibold',
|
|
||||||
'transition-all duration-300',
|
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background',
|
|
||||||
'disabled:pointer-events-none disabled:opacity-50',
|
|
||||||
'active:scale-[0.98]',
|
|
||||||
{
|
|
||||||
'bg-primary text-primary-foreground shadow-[0_0_20px_rgba(139,92,246,0.3)] hover:bg-primary/90 hover:shadow-[0_0_25px_rgba(139,92,246,0.5)] hover:-translate-y-0.5':
|
|
||||||
variant === 'default',
|
|
||||||
'glass hover:bg-accent/10 hover:border-primary/20 text-foreground':
|
|
||||||
variant === 'outline',
|
|
||||||
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
|
||||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-lg hover:shadow-destructive/20':
|
|
||||||
variant === 'destructive',
|
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80':
|
|
||||||
variant === 'secondary',
|
|
||||||
'h-10 px-5 py-2 text-sm': size === 'default',
|
|
||||||
'h-9 px-4 text-xs': size === 'sm',
|
|
||||||
'h-12 px-8 text-base': size === 'lg',
|
|
||||||
'h-10 w-10': size === 'icon',
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
|
||||||
|
|
||||||
export { Button };
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn('bg-card text-card-foreground border rounded-lg transition-all duration-300', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Card.displayName = 'Card';
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
CardHeader.displayName = 'CardHeader';
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
CardTitle.displayName = 'CardTitle';
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
CardDescription.displayName = 'CardDescription';
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
CardContent.displayName = 'CardContent';
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
CardFooter.displayName = 'CardFooter';
|
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
|
||||||
icon?: LucideIcon;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
action?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmptyState({
|
|
||||||
icon: Icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
action,
|
|
||||||
className,
|
|
||||||
}: EmptyStateProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}>
|
|
||||||
{Icon && (
|
|
||||||
<div className="mb-4 rounded-full bg-muted p-3">
|
|
||||||
<Icon className="h-6 w-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<h3 className="mb-2 text-sm font-semibold">{title}</h3>
|
|
||||||
{description && (
|
|
||||||
<p className="mb-4 text-sm text-muted-foreground max-w-sm">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{action}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
||||||
({ className, type, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
className={cn(
|
|
||||||
'flex h-10 w-full rounded-lg border border-border bg-input px-4 py-2',
|
|
||||||
'text-sm ring-offset-background file:border-0 file:bg-transparent',
|
|
||||||
'file:text-sm file:font-medium placeholder:text-muted-foreground',
|
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary/50',
|
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Input.displayName = 'Input';
|
|
||||||
|
|
||||||
export { Input };
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
||||||
({ className, label, children, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{label && (
|
|
||||||
<label htmlFor={props.id} className="text-sm font-medium">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<select
|
|
||||||
className={cn(
|
|
||||||
'flex h-10 w-full rounded-lg border border-border bg-input px-3 py-2',
|
|
||||||
'text-sm ring-offset-background',
|
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary/50',
|
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Select.displayName = 'Select';
|
|
||||||
|
|
||||||
export { Select };
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
||||||
|
|
||||||
export function Skeleton({ className, ...props }: SkeletonProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
|
|
||||||
export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
||||||
label?: string;
|
|
||||||
showValue?: boolean;
|
|
||||||
suffix?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
|
|
||||||
({ className, label, showValue = true, suffix = '', ...props }, ref) => {
|
|
||||||
const [value, setValue] = React.useState(props.value || props.defaultValue || 0);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setValue(e.target.value);
|
|
||||||
props.onChange?.(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{(label || showValue) && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{label && (
|
|
||||||
<label htmlFor={props.id} className="text-sm font-medium">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
{showValue && (
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{value}
|
|
||||||
{suffix}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
className={cn(
|
|
||||||
'w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer',
|
|
||||||
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4',
|
|
||||||
'[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary',
|
|
||||||
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-all',
|
|
||||||
'[&::-webkit-slider-thumb]:hover:scale-110',
|
|
||||||
'[&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full',
|
|
||||||
'[&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0',
|
|
||||||
'[&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:transition-all',
|
|
||||||
'[&::-moz-range-thumb]:hover:scale-110',
|
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Slider.displayName = 'Slider';
|
|
||||||
|
|
||||||
export { Slider };
|
|
||||||
48
components/ui/badge.tsx
Normal file
48
components/ui/badge.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils/index"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
64
components/ui/button.tsx
Normal file
64
components/ui/button.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
92
components/ui/card.tsx
Normal file
92
components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
104
components/ui/empty.tsx
Normal file
104
components/ui/empty.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils/index"
|
||||||
|
|
||||||
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty"
|
||||||
|
className={cn(
|
||||||
|
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-header"
|
||||||
|
className={cn(
|
||||||
|
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMediaVariants = cva(
|
||||||
|
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function EmptyMedia({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-icon"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-title"
|
||||||
|
className={cn("text-lg font-medium tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="empty-content"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Empty,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyTitle,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyMedia,
|
||||||
|
}
|
||||||
21
components/ui/input.tsx
Normal file
21
components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
190
components/ui/select.tsx
Normal file
190
components/ui/select.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
import { Select as SelectPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="select-item-indicator"
|
||||||
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
13
components/ui/skeleton.tsx
Normal file
13
components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
63
components/ui/slider.tsx
Normal file
63
components/ui/slider.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slider as SliderPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Slider({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Slider }
|
||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Copy, Star, Check, ArrowLeftRight, BarChart3 } from 'lucide-react';
|
import { Copy, Star, Check, ArrowLeftRight, BarChart3 } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select } from '@/components/ui/Select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import SearchUnits from './SearchUnits';
|
import SearchUnits from './SearchUnits';
|
||||||
import VisualComparison from './VisualComparison';
|
import VisualComparison from './VisualComparison';
|
||||||
import CommandPalette from '@/components/units/ui/CommandPalette';
|
import CommandPalette from '@/components/units/ui/CommandPalette';
|
||||||
@@ -127,17 +133,23 @@ export default function MainConverter() {
|
|||||||
<div className="w-full md:w-64 shrink-0">
|
<div className="w-full md:w-64 shrink-0">
|
||||||
<Select
|
<Select
|
||||||
value={selectedMeasure}
|
value={selectedMeasure}
|
||||||
onChange={(e) => setSelectedMeasure(e.target.value as Measure)}
|
onValueChange={(value) => setSelectedMeasure(value as Measure)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
className="h-10 text-sm"
|
className="h-10 text-sm"
|
||||||
style={{
|
style={{
|
||||||
borderLeft: `4px solid ${getCategoryColorHex(selectedMeasure)}`,
|
borderLeft: `4px solid ${getCategoryColorHex(selectedMeasure)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<SelectValue placeholder="Measure" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
{measures.map((measure) => (
|
{measures.map((measure) => (
|
||||||
<option key={measure} value={measure}>
|
<SelectItem key={measure} value={measure}>
|
||||||
{formatMeasureName(measure)}
|
{formatMeasureName(measure)}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,13 +179,18 @@ export default function MainConverter() {
|
|||||||
<label className="text-sm font-medium mb-2 block">From</label>
|
<label className="text-sm font-medium mb-2 block">From</label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedUnit}
|
value={selectedUnit}
|
||||||
onChange={(e) => setSelectedUnit(e.target.value)}
|
onValueChange={(value) => setSelectedUnit(value)}
|
||||||
>
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="From" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
{units.map((unit) => (
|
{units.map((unit) => (
|
||||||
<option key={unit} value={unit}>
|
<SelectItem key={unit} value={unit}>
|
||||||
{unit}
|
{unit}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{/* Swap Button */}
|
{/* Swap Button */}
|
||||||
@@ -191,13 +208,18 @@ export default function MainConverter() {
|
|||||||
<label className="text-sm font-medium mb-2 block">To</label>
|
<label className="text-sm font-medium mb-2 block">To</label>
|
||||||
<Select
|
<Select
|
||||||
value={targetUnit}
|
value={targetUnit}
|
||||||
onChange={(e) => setTargetUnit(e.target.value)}
|
onValueChange={(value) => setTargetUnit(value)}
|
||||||
>
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="To" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
{units.map((unit) => (
|
{units.map((unit) => (
|
||||||
<option key={unit} value={unit}>
|
<SelectItem key={unit} value={unit}>
|
||||||
{unit}
|
{unit}
|
||||||
</option>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
getAllMeasures,
|
getAllMeasures,
|
||||||
getUnitsForMeasure,
|
getUnitsForMeasure,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@valknarthing/pastel-wasm": "^0.1.0",
|
"@valknarthing/pastel-wasm": "^0.1.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"convert-units": "^2.3.4",
|
"convert-units": "^2.3.4",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
@@ -37,7 +39,9 @@
|
|||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-config-next": "^15.1.7",
|
"eslint-config-next": "^15.1.7",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"shadcn": "^3.8.5",
|
||||||
"tailwindcss": "^4.2.0",
|
"tailwindcss": "^4.2.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3472
pnpm-lock.yaml
generated
3472
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user