refactor: update UI component usage to match latest shadcn APIs
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
'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 { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} 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';
|
||||
@@ -118,15 +124,19 @@ export default function BatchPage() {
|
||||
<h2 className="text-sm font-medium mb-4">Operation</h2>
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
label="Operation"
|
||||
value={operation}
|
||||
onChange={(e) => setOperation(e.target.value as Operation)}
|
||||
onValueChange={(value) => setOperation(value as Operation)}
|
||||
>
|
||||
<option value="lighten">Lighten</option>
|
||||
<option value="darken">Darken</option>
|
||||
<option value="saturate">Saturate</option>
|
||||
<option value="desaturate">Desaturate</option>
|
||||
<option value="rotate">Rotate Hue</option>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select operation" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
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 { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useSimulateColorBlindness } from '@/lib/pastel/api/queries';
|
||||
import { Loader2, Eye, Plus, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -113,13 +119,17 @@ export default function ColorBlindPage() {
|
||||
<h2 className="text-sm font-medium mb-4">Blindness Type</h2>
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
label="Type"
|
||||
value={blindnessType}
|
||||
onChange={(e) => setBlindnessType(e.target.value as ColorBlindnessType)}
|
||||
onValueChange={(value) => setBlindnessType(value as ColorBlindnessType)}
|
||||
>
|
||||
<option value="protanopia">Protanopia (Red-blind)</option>
|
||||
<option value="deuteranopia">Deuteranopia (Green-blind)</option>
|
||||
<option value="tritanopia">Tritanopia (Blue-blind)</option>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</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>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
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 { 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';
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function ContrastPage() {
|
||||
}) => (
|
||||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||||
<span className="text-sm">{label}</span>
|
||||
<Badge variant={passed ? 'success' : 'destructive'}>
|
||||
<Badge variant={passed ? 'secondary' : 'destructive'}>
|
||||
{passed ? (
|
||||
<>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
import { useState } from 'react';
|
||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useGenerateDistinct } from '@/lib/pastel/api/queries';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -65,14 +71,23 @@ export default function DistinctPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium block">
|
||||
Distance Metric
|
||||
</label>
|
||||
<Select
|
||||
label="Distance 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>
|
||||
<option value="ciede2000">CIEDE2000 (More Accurate)</option>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select metric" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cie76">CIE76 (Faster)</SelectItem>
|
||||
<SelectItem value="ciede2000">CIEDE2000 (More Accurate)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useState } from 'react';
|
||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useGenerateGradient } from '@/lib/pastel/api/queries';
|
||||
import { Loader2, Plus, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -4,8 +4,14 @@ import { useState } from 'react';
|
||||
import { ColorPicker } from '@/components/pastel/color/ColorPicker';
|
||||
import { PaletteGrid } from '@/components/pastel/color/PaletteGrid';
|
||||
import { ExportMenu } from '@/components/pastel/tools/ExportMenu';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useGeneratePalette } from '@/lib/pastel/api/queries';
|
||||
import { Loader2, Palette } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -73,16 +79,20 @@ export default function HarmonyPage() {
|
||||
<h2 className="text-sm font-medium mb-4">Harmony Type</h2>
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
label="Harmony"
|
||||
value={harmonyType}
|
||||
onChange={(e) => setHarmonyType(e.target.value as HarmonyType)}
|
||||
onValueChange={(value) => setHarmonyType(value as HarmonyType)}
|
||||
>
|
||||
<option value="monochromatic">Monochromatic</option>
|
||||
<option value="analogous">Analogous</option>
|
||||
<option value="complementary">Complementary</option>
|
||||
<option value="split-complementary">Split-Complementary</option>
|
||||
<option value="triadic">Triadic</option>
|
||||
<option value="tetradic">Tetradic (Square)</option>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select harmony" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monochromatic">Monochromatic</SelectItem>
|
||||
<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>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ColorSwatch } from '@/components/pastel/color/ColorSwatch';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useNamedColors } from '@/lib/pastel/api/queries';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { parse_color } from '@valknarthing/pastel-wasm';
|
||||
@@ -57,9 +63,14 @@ export default function NamedColorsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:w-48">
|
||||
<Select value={sortBy} onChange={(e) => setSortBy(e.target.value as 'name' | 'hue')}>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="hue">Sort by Hue</option>
|
||||
<Select value={sortBy} onValueChange={(value) => setSortBy(value as 'name' | 'hue')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Sort by Name</SelectItem>
|
||||
<SelectItem value="hue">Sort by Hue</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ManipulationPanel } from '@/components/pastel/tools/ManipulationPanel';
|
||||
import { useColorInfo } from '@/lib/pastel/api/queries';
|
||||
import { useColorHistory } from '@/lib/pastel/stores/historyStore';
|
||||
import { Loader2, Share2, History, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
function PlaygroundContent() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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 { 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';
|
||||
|
||||
114
app/globals.css
114
app/globals.css
@@ -1,4 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@source "../components/*.{js,ts,jsx,tsx}";
|
||||
@source "../components/ui/*.{js,ts,jsx,tsx}";
|
||||
@@ -222,3 +226,113 @@ html {
|
||||
@utility gradient-brand {
|
||||
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 { toPng } from 'html-to-image';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Skeleton } from '@/components/ui/Skeleton';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
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 { cn } from '@/lib/utils/cn';
|
||||
import { toast } from 'sonner';
|
||||
@@ -190,12 +196,15 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare
|
||||
{text}
|
||||
</pre>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={Type}
|
||||
title="Start typing to see your ASCII art"
|
||||
description="Enter text in the input field above to generate ASCII art with the selected font"
|
||||
className="py-8"
|
||||
/>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Type />
|
||||
</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>
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty"
|
||||
import { Search, X, Heart, Clock, List, Shuffle } from 'lucide-react';
|
||||
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 { getFavorites, getRecentFonts, toggleFavorite, isFavorite } from '@/lib/storage/favorites';
|
||||
|
||||
@@ -162,16 +168,20 @@ export function FontSelector({
|
||||
{/* Font List */}
|
||||
<div className="flex-1 overflow-y-auto space-y-1 pr-2">
|
||||
{filteredFonts.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={filter === 'favorites' ? Heart : (filter === 'recent' ? Clock : Search)}
|
||||
title={
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
{filter === 'favorites' ? <Heart /> : (filter === 'recent' ? <Clock /> : <Search />)}
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>{
|
||||
filter === 'favorites'
|
||||
? 'No favorite fonts yet'
|
||||
: filter === 'recent'
|
||||
? 'No recent fonts'
|
||||
: 'No fonts found'
|
||||
}
|
||||
description={
|
||||
}</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
{
|
||||
filter === 'favorites'
|
||||
? 'Click the heart icon on any font to add it to your favorites'
|
||||
: filter === 'recent'
|
||||
@@ -180,8 +190,9 @@ export function FontSelector({
|
||||
? 'Try a different search term'
|
||||
: 'Loading fonts...'
|
||||
}
|
||||
className="py-8"
|
||||
/>
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
) : (
|
||||
filteredFonts.map((font) => (
|
||||
<div
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
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 { cn } from '@/lib/utils/cn';
|
||||
import { useSidebar } from './SidebarProvider';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSidebar } from './SidebarProvider';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
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 { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface ColorPickerProps {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from '@/components/providers/ThemeProvider';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useState } from 'react';
|
||||
import { Download, Copy, Check } from 'lucide-react';
|
||||
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>
|
||||
<Select
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value as ExportFormat)}
|
||||
onValueChange={(value) => setFormat(value as ExportFormat)}
|
||||
>
|
||||
<option value="css">CSS Variables</option>
|
||||
<option value="scss">SCSS Variables</option>
|
||||
<option value="tailwind">Tailwind Config</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="javascript">JavaScript Array</option>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Slider } from '@/components/ui/Slider';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
useLighten,
|
||||
useDarken,
|
||||
@@ -131,15 +131,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
||||
<div className="space-y-6">
|
||||
{/* Lighten */}
|
||||
<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
|
||||
label="Lighten"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={lightenAmount}
|
||||
onChange={(e) => setLightenAmount(parseFloat(e.target.value))}
|
||||
suffix="%"
|
||||
showValue
|
||||
value={[lightenAmount]}
|
||||
onValueChange={(vals) => setLightenAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
|
||||
Apply Lighten
|
||||
@@ -148,15 +149,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
||||
|
||||
{/* Darken */}
|
||||
<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
|
||||
label="Darken"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={darkenAmount}
|
||||
onChange={(e) => setDarkenAmount(parseFloat(e.target.value))}
|
||||
suffix="%"
|
||||
showValue
|
||||
value={[darkenAmount]}
|
||||
onValueChange={(vals) => setDarkenAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
|
||||
Apply Darken
|
||||
@@ -165,15 +167,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
||||
|
||||
{/* Saturate */}
|
||||
<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
|
||||
label="Saturate"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={saturateAmount}
|
||||
onChange={(e) => setSaturateAmount(parseFloat(e.target.value))}
|
||||
suffix="%"
|
||||
showValue
|
||||
value={[saturateAmount]}
|
||||
onValueChange={(vals) => setSaturateAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
|
||||
Apply Saturate
|
||||
@@ -182,15 +185,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
||||
|
||||
{/* Desaturate */}
|
||||
<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
|
||||
label="Desaturate"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={desaturateAmount}
|
||||
onChange={(e) => setDesaturateAmount(parseFloat(e.target.value))}
|
||||
suffix="%"
|
||||
showValue
|
||||
value={[desaturateAmount]}
|
||||
onValueChange={(vals) => setDesaturateAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
|
||||
Apply Desaturate
|
||||
@@ -199,15 +203,16 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
||||
|
||||
{/* Rotate Hue */}
|
||||
<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
|
||||
label="Rotate Hue"
|
||||
min={-180}
|
||||
max={180}
|
||||
step={5}
|
||||
value={rotateAmount}
|
||||
onChange={(e) => setRotateAmount(parseInt(e.target.value))}
|
||||
suffix="°"
|
||||
showValue
|
||||
value={[rotateAmount]}
|
||||
onValueChange={(vals) => setRotateAmount(vals[0])}
|
||||
/>
|
||||
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
|
||||
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 { Copy, Star, Check, ArrowLeftRight, BarChart3 } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import SearchUnits from './SearchUnits';
|
||||
import VisualComparison from './VisualComparison';
|
||||
import CommandPalette from '@/components/units/ui/CommandPalette';
|
||||
@@ -127,17 +133,23 @@ export default function MainConverter() {
|
||||
<div className="w-full md:w-64 shrink-0">
|
||||
<Select
|
||||
value={selectedMeasure}
|
||||
onChange={(e) => setSelectedMeasure(e.target.value as Measure)}
|
||||
onValueChange={(value) => setSelectedMeasure(value as Measure)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-10 text-sm"
|
||||
style={{
|
||||
borderLeft: `4px solid ${getCategoryColorHex(selectedMeasure)}`,
|
||||
}}
|
||||
>
|
||||
<SelectValue placeholder="Measure" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{measures.map((measure) => (
|
||||
<option key={measure} value={measure}>
|
||||
<SelectItem key={measure} value={measure}>
|
||||
{formatMeasureName(measure)}
|
||||
</option>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,13 +179,18 @@ export default function MainConverter() {
|
||||
<label className="text-sm font-medium mb-2 block">From</label>
|
||||
<Select
|
||||
value={selectedUnit}
|
||||
onChange={(e) => setSelectedUnit(e.target.value)}
|
||||
onValueChange={(value) => setSelectedUnit(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="From" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{units.map((unit) => (
|
||||
<option key={unit} value={unit}>
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</option>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Swap Button */}
|
||||
@@ -191,13 +208,18 @@ export default function MainConverter() {
|
||||
<label className="text-sm font-medium mb-2 block">To</label>
|
||||
<Select
|
||||
value={targetUnit}
|
||||
onChange={(e) => setTargetUnit(e.target.value)}
|
||||
onValueChange={(value) => setTargetUnit(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="To" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{units.map((unit) => (
|
||||
<option key={unit} value={unit}>
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</option>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
getAllMeasures,
|
||||
getUnitsForMeasure,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@valknarthing/pastel-wasm": "^0.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"convert-units": "^2.3.4",
|
||||
@@ -20,6 +21,7 @@
|
||||
"html-to-image": "^1.11.13",
|
||||
"lucide-react": "^0.575.0",
|
||||
"next": "^16.1.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -37,7 +39,9 @@
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-next": "^15.1.7",
|
||||
"postcss": "^8.5.6",
|
||||
"shadcn": "^3.8.5",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"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