refactor: streamline, refine and polish

This commit is contained in:
2026-02-27 12:35:02 +01:00
parent efe3c81576
commit ee7e5ec06c
21 changed files with 606 additions and 735 deletions

View File

@@ -13,6 +13,7 @@ import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/col
import { Loader2, Share2, Palette, Plus, X, Layers } from 'lucide-react'; import { Loader2, Share2, Palette, Plus, X, Layers } from 'lucide-react';
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 { Label } from '@/components/ui/label';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -137,37 +138,36 @@ function PlaygroundContent() {
title="Color" title="Color"
description="Interactive color manipulation and analysis tool" description="Interactive color manipulation and analysis tool"
> >
<div className="space-y-8"> <div className="space-y-6">
{/* Row 1: Workspace */} {/* Row 1: Workspace */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Main Workspace: Color Picker and Information */} {/* Main Workspace: Color Picker and Information */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="h-full"> <Card className="h-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle>Color Picker</CardTitle> <CardTitle>Color Picker</CardTitle>
<Button onClick={handleShare} variant="outline" size="sm"> <Button onClick={handleShare} variant="outline" size="xs">
<Share2 className="h-4 w-4 mr-2" /> <Share2 className="h-3 w-3 mr-1" />
Share Share
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col md:flex-row gap-12"> <div className="flex flex-col md:flex-row gap-8">
<div className="flex-shrink-0 mx-auto md:mx-0"> <div className="flex-shrink-0 mx-auto md:mx-0">
<ColorPicker color={color} onChange={setColor} /> <ColorPicker color={color} onChange={setColor} />
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="text-sm font-medium mb-4 text-muted-foreground uppercase tracking-wider">Color Information</h3>
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
)} )}
{isError && ( {isError && (
<div className="p-4 bg-destructive/10 text-destructive rounded-lg"> <div className="p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
<p className="font-medium">Error loading color information</p> <p className="font-medium">Error loading color information</p>
<p className="text-sm mt-1">{error?.message || 'Unknown error'}</p> <p className="mt-1">{error?.message || 'Unknown error'}</p>
</div> </div>
)} )}
@@ -182,7 +182,7 @@ function PlaygroundContent() {
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<Card className="h-full"> <Card className="h-full">
<CardHeader> <CardHeader>
<CardTitle>Color Manipulation</CardTitle> <CardTitle>Adjustments</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ManipulationPanel color={color} onColorChange={setColor} /> <ManipulationPanel color={color} onColorChange={setColor} />
@@ -192,15 +192,12 @@ function PlaygroundContent() {
</div> </div>
{/* Row 2: Harmony Generator */} {/* Row 2: Harmony Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Harmony Controls */} {/* Harmony Controls */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<Card className="h-full"> <Card className="h-full">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle>Harmony</CardTitle>
<Palette className="h-5 w-5" />
Harmony Type
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<Select <Select
@@ -219,7 +216,7 @@ function PlaygroundContent() {
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-sm text-muted-foreground"> <p className="text-xs text-muted-foreground">
{harmonyDescriptions[harmonyType]} {harmonyDescriptions[harmonyType]}
</p> </p>
@@ -230,14 +227,11 @@ function PlaygroundContent() {
> >
{paletteMutation.isPending ? ( {paletteMutation.isPending ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating.. Generating...
</> </>
) : ( ) : (
<> 'Generate'
<Palette className="mr-2 h-4 w-4" />
Generate Harmony
</>
)} )}
</Button> </Button>
</CardContent> </CardContent>
@@ -249,21 +243,21 @@ function PlaygroundContent() {
<Card className="h-full"> <Card className="h-full">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
Generated Palette {palette.length > 0 && `(${palette.length} colors)`} Palette {palette.length > 0 && <span className="text-muted-foreground font-normal text-sm ml-1">({palette.length})</span>}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{palette.length > 0 ? ( {palette.length > 0 ? (
<div className="space-y-6"> <div className="space-y-5">
<PaletteGrid colors={palette} onColorClick={setColor} /> <PaletteGrid colors={palette} onColorClick={setColor} />
<div className="pt-4 border-t"> <div className="pt-3 border-t">
<ExportMenu colors={palette} /> <ExportMenu colors={palette} />
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-12 text-center text-muted-foreground"> <div className="py-8 text-center text-muted-foreground text-xs">
<Palette className="h-12 w-12 mx-auto mb-4 opacity-50" /> <Palette className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p>Select a harmony type and click Generate to create your palette based on the current color</p> <p>Generate a harmony palette from the current color</p>
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -272,74 +266,72 @@ function PlaygroundContent() {
</div> </div>
{/* Row 3: Gradient Generator */} {/* Row 3: Gradient Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Gradient Controls */} {/* Gradient Controls */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<Card className="h-full"> <Card className="h-full">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle>Gradient</CardTitle>
<Layers className="h-5 w-5" />
Gradient Controls
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-4">
<div className="space-y-4"> <div className="space-y-2">
<div className="space-y-3"> <Label className="text-xs">Color Stops</Label>
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Color Stops</h4> {stops.map((stop, index) => (
{stops.map((stop, index) => ( <div key={index} className="flex items-center gap-2">
<div key={index} className="flex items-center gap-2"> <Input
<div className="flex-1"> type="color"
<Input value={stop}
type="color" onChange={(e) => updateStop(index, e.target.value)}
value={stop} className="w-9 h-9 p-1 shrink-0 cursor-pointer"
onChange={(e) => updateStop(index, e.target.value)} />
className="h-10 w-full cursor-pointer p-1" <Input
/> type="text"
</div> value={stop}
{index !== 0 && stops.length > 2 && ( onChange={(e) => updateStop(index, e.target.value)}
<Button className="font-mono text-xs flex-1"
variant="ghost" />
size="icon" {index !== 0 && stops.length > 2 && (
onClick={() => removeStop(index)} <Button
className="shrink-0" variant="ghost"
> size="icon-xs"
<X className="h-4 w-4" /> onClick={() => removeStop(index)}
</Button> >
)} <X className="h-3.5 w-3.5" />
</div> </Button>
))} )}
<Button onClick={addStop} variant="outline" size="sm" className="w-full"> </div>
<Plus className="h-4 w-4 mr-2" /> ))}
Add Stop <Button onClick={addStop} variant="outline" size="sm" className="w-full">
</Button> <Plus className="h-3.5 w-3.5 mr-1.5" />
</div> Add Stop
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Steps</h4>
<Input
type="number"
min={2}
max={100}
value={gradientCount}
onChange={(e) => setGradientCount(parseInt(e.target.value))}
/>
</div>
<Button
onClick={generateGradient}
disabled={gradientMutation.isPending}
className="w-full"
>
{gradientMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating..
</>
) : (
'Generate Gradient'
)}
</Button> </Button>
</div> </div>
<div className="space-y-2">
<Label className="text-xs">Steps</Label>
<Input
type="number"
min={2}
max={100}
value={gradientCount}
onChange={(e) => setGradientCount(parseInt(e.target.value))}
/>
</div>
<Button
onClick={generateGradient}
disabled={gradientMutation.isPending}
className="w-full"
>
{gradientMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating...
</>
) : (
'Generate'
)}
</Button>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -349,27 +341,27 @@ function PlaygroundContent() {
<Card className="h-full"> <Card className="h-full">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
Generated Gradient {gradientResult.length > 0 && `(${gradientResult.length} colors)`} Gradient {gradientResult.length > 0 && <span className="text-muted-foreground font-normal text-sm ml-1">({gradientResult.length})</span>}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{gradientResult.length > 0 ? ( {gradientResult.length > 0 ? (
<div className="space-y-6"> <div className="space-y-5">
<div <div
className="h-24 w-full rounded-lg border shadow-inner" className="h-16 w-full rounded-lg border"
style={{ style={{
background: `linear-gradient(to right, ${gradientResult.join(', ')})`, background: `linear-gradient(to right, ${gradientResult.join(', ')})`,
}} }}
/> />
<PaletteGrid colors={gradientResult} onColorClick={setColor} /> <PaletteGrid colors={gradientResult} onColorClick={setColor} />
<div className="pt-4 border-t"> <div className="pt-3 border-t">
<ExportMenu colors={gradientResult} /> <ExportMenu colors={gradientResult} />
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-12 text-center text-muted-foreground"> <div className="py-8 text-center text-muted-foreground text-xs">
<Layers className="h-12 w-12 mx-auto mb-4 opacity-50" /> <Layers className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p>Add color stops and click Generate to create your smooth gradient</p> <p>Add color stops and generate a smooth gradient</p>
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -3,7 +3,6 @@
import * as React from 'react'; import * as React from 'react';
import { AppPage } from '@/components/layout/AppPage'; import { AppPage } from '@/components/layout/AppPage';
import { FaviconGenerator } from '@/components/favicon/FaviconGenerator'; import { FaviconGenerator } from '@/components/favicon/FaviconGenerator';
import { Globe, Shield, Zap } from 'lucide-react';
export default function FaviconPage() { export default function FaviconPage() {
return ( return (
@@ -11,9 +10,7 @@ export default function FaviconPage() {
title="Favicon Generator" title="Favicon Generator"
description="Create a complete set of icons for your website including PWA manifest and HTML code." description="Create a complete set of icons for your website including PWA manifest and HTML code."
> >
<div className="w-full max-w-7xl mx-auto space-y-8 pb-12"> <FaviconGenerator />
<FaviconGenerator />
</div>
</AppPage> </AppPage>
); );
} }

View File

@@ -9,7 +9,7 @@ import BackToTop from '@/components/BackToTop';
export default function Home() { export default function Home() {
return ( return (
<main className="relative min-h-screen dark text-foreground"> <main className="relative min-h-screen text-foreground">
<AnimatedBackground /> <AnimatedBackground />
<BackToTop /> <BackToTop />
<Hero /> <Hero />

View File

@@ -11,7 +11,6 @@ import { addRecentFont } from '@/lib/storage/favorites';
import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing'; import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { ASCIIFont } from '@/types/ascii'; import type { ASCIIFont } from '@/types/ascii';
import { Car } from 'lucide-react';
import { Card, CardContent } from '../ui/card'; import { Card, CardContent } from '../ui/card';
export function ASCIIConverter() { export function ASCIIConverter() {

View File

@@ -61,111 +61,95 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle>Preview</CardTitle> <CardTitle>Preview</CardTitle>
{font && ( {font && (
<span className="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-md font-mono"> <span className="text-[10px] px-1.5 py-0.5 bg-primary/10 text-primary rounded font-mono">
{font} {font}
</span> </span>
)} )}
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-1.5 flex-wrap">
{onCopy && ( {onCopy && (
<Button variant="outline" size="sm" onClick={onCopy}> <Button variant="outline" size="xs" onClick={onCopy}>
<Copy className="h-3 w-3 mr-2" /> <Copy className="h-3 w-3 mr-1" />
Copy Copy
</Button> </Button>
)} )}
{onShare && ( {onShare && (
<Button variant="outline" size="sm" onClick={onShare} title="Copy shareable URL"> <Button variant="outline" size="xs" onClick={onShare} title="Copy shareable URL">
<Share2 className="h-3 w-3 mr-2" /> <Share2 className="h-3 w-3 mr-1" />
Share Share
</Button> </Button>
)} )}
<Button variant="outline" size="sm" onClick={handleExportPNG} title="Export as PNG"> <Button variant="outline" size="xs" onClick={handleExportPNG} title="Export as PNG">
<ImageIcon className="h-3 w-3 mr-2" /> <ImageIcon className="h-3 w-3 mr-1" />
PNG PNG
</Button> </Button>
{onDownload && ( {onDownload && (
<Button variant="outline" size="sm" onClick={onDownload}> <Button variant="outline" size="xs" onClick={onDownload}>
<Download className="h-3 w-3 mr-2" /> <Download className="h-3 w-3 mr-1" />
TXT TXT
</Button> </Button>
)} )}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3">
{/* Controls */} {/* Controls */}
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1 border rounded-md p-1"> <div className="flex items-center border rounded-md p-0.5">
<button <button
onClick={() => setTextAlign('left')} onClick={() => setTextAlign('left')}
className={cn( className={cn(
'p-1.5 rounded transition-colors', 'p-1 rounded transition-colors',
textAlign === 'left' ? 'bg-accent' : 'hover:bg-accent/50' textAlign === 'left' ? 'bg-accent' : 'hover:bg-accent/50'
)} )}
title="Align left" title="Align left"
> >
<AlignLeft className="h-3.5 w-3.5" /> <AlignLeft className="h-3 w-3" />
</button> </button>
<button <button
onClick={() => setTextAlign('center')} onClick={() => setTextAlign('center')}
className={cn( className={cn(
'p-1.5 rounded transition-colors', 'p-1 rounded transition-colors',
textAlign === 'center' ? 'bg-accent' : 'hover:bg-accent/50' textAlign === 'center' ? 'bg-accent' : 'hover:bg-accent/50'
)} )}
title="Align center" title="Align center"
> >
<AlignCenter className="h-3.5 w-3.5" /> <AlignCenter className="h-3 w-3" />
</button> </button>
<button <button
onClick={() => setTextAlign('right')} onClick={() => setTextAlign('right')}
className={cn( className={cn(
'p-1.5 rounded transition-colors', 'p-1 rounded transition-colors',
textAlign === 'right' ? 'bg-accent' : 'hover:bg-accent/50' textAlign === 'right' ? 'bg-accent' : 'hover:bg-accent/50'
)} )}
title="Align right" title="Align right"
> >
<AlignRight className="h-3.5 w-3.5" /> <AlignRight className="h-3 w-3" />
</button> </button>
</div> </div>
<div className="flex items-center gap-1 border rounded-md p-1"> <div className="flex items-center border rounded-md p-0.5">
<button {(['xs', 'sm', 'base'] as const).map((s) => (
onClick={() => setFontSize('xs')} <button
className={cn( key={s}
'px-2 py-1 text-xs rounded transition-colors', onClick={() => setFontSize(s)}
fontSize === 'xs' ? 'bg-accent' : 'hover:bg-accent/50' className={cn(
)} 'px-1.5 py-0.5 text-[10px] rounded transition-colors uppercase',
> fontSize === s ? 'bg-accent' : 'hover:bg-accent/50'
XS )}
</button> >
<button {s === 'base' ? 'md' : s}
onClick={() => setFontSize('sm')} </button>
className={cn( ))}
'px-2 py-1 text-xs rounded transition-colors',
fontSize === 'sm' ? 'bg-accent' : 'hover:bg-accent/50'
)}
>
SM
</button>
<button
onClick={() => setFontSize('base')}
className={cn(
'px-2 py-1 text-xs rounded transition-colors',
fontSize === 'base' ? 'bg-accent' : 'hover:bg-accent/50'
)}
>
MD
</button>
</div> </div>
{!isLoading && text && (
<div className="flex gap-2 text-[10px] text-muted-foreground ml-auto">
<span>{lineCount} lines</span>
<span>{charCount} chars</span>
</div>
)}
</div> </div>
{!isLoading && text && (
<div className="flex gap-4 text-xs text-muted-foreground">
<span>{lineCount} lines</span>
<span></span>
<span>{charCount} chars</span>
</div>
)}
<div <div
ref={previewRef} ref={previewRef}
className={cn( className={cn(

View File

@@ -95,15 +95,15 @@ export function FontSelector({
return ( return (
<Card className={cn("flex flex-col min-h-0 overflow-hidden", className)}> <Card className={cn("flex flex-col min-h-0 overflow-hidden", className)}>
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2 space-y-0"> <CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2 space-y-0">
<CardTitle>Select Font</CardTitle> <CardTitle>Fonts</CardTitle>
{onRandomFont && ( {onRandomFont && (
<Button <Button
variant="outline" variant="outline"
size="sm" size="xs"
onClick={onRandomFont} onClick={onRandomFont}
title="Random font" title="Random font"
> >
<Shuffle className="h-3 w-3 mr-2" /> <Shuffle className="h-3 w-3 mr-1" />
Random Random
</Button> </Button>
)} )}
@@ -112,34 +112,34 @@ export function FontSelector({
<Tabs <Tabs
value={filter} value={filter}
onValueChange={(v) => setFilter(v as FilterType)} onValueChange={(v) => setFilter(v as FilterType)}
className="mb-4 shrink-0" className="mb-3 shrink-0"
> >
<TabsList className="w-full"> <TabsList className="w-full">
<TabsTrigger value="all" className="flex-1"> <TabsTrigger value="all" className="flex-1">
<List className="h-3.5 w-3.5" /> <List className="h-3 w-3" />
All All
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="favorites" className="flex-1"> <TabsTrigger value="favorites" className="flex-1">
<Heart className="h-3.5 w-3.5" /> <Heart className="h-3 w-3" />
Favorites Fav
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="recent" className="flex-1"> <TabsTrigger value="recent" className="flex-1">
<Clock className="h-3.5 w-3.5" /> <Clock className="h-3 w-3" />
Recent Recent
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
{/* Search Input */} {/* Search Input */}
<div className="relative mb-4 shrink-0"> <div className="relative mb-3 shrink-0">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
<Input <Input
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
placeholder="Search fonts..." placeholder="Search fonts..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-9" className="pl-8 pr-8 h-8 text-sm"
/> />
{searchQuery && ( {searchQuery && (
<button <button
@@ -147,13 +147,13 @@ export function FontSelector({
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label="Clear search" aria-label="Clear search"
> >
<X className="h-4 w-4" /> <X className="h-3.5 w-3.5" />
</button> </button>
)} )}
</div> </div>
{/* Font List */} {/* Font List */}
<div className="flex-1 overflow-y-auto space-y-1 pr-2 scrollbar"> <div className="flex-1 overflow-y-auto space-y-0.5 pr-1 scrollbar">
{filteredFonts.length === 0 ? ( {filteredFonts.length === 0 ? (
<Empty> <Empty>
<EmptyHeader> <EmptyHeader>
@@ -185,26 +185,26 @@ export function FontSelector({
<div <div
key={font.name} key={font.name}
className={cn( className={cn(
'group flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors', 'group flex items-center gap-1 px-2 py-1.5 rounded text-xs transition-colors',
'hover:bg-accent hover:text-accent-foreground', 'hover:bg-accent hover:text-accent-foreground',
selectedFont === font.name && 'bg-accent text-accent-foreground font-medium' selectedFont === font.name && 'bg-accent text-accent-foreground font-medium'
)} )}
> >
<button <button
onClick={() => onSelectFont(font.name)} onClick={() => onSelectFont(font.name)}
className="flex-1 text-left" className="flex-1 text-left truncate"
> >
{font.name} {font.name}
</button> </button>
<button <button
onClick={(e) => handleToggleFavorite(font.name, e)} onClick={(e) => handleToggleFavorite(font.name, e)}
className="p-1" className="p-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
aria-label={isFavorite(font.name) ? 'Remove from favorites' : 'Add to favorites'} aria-label={isFavorite(font.name) ? 'Remove from favorites' : 'Add to favorites'}
> >
<Heart <Heart
className={cn( className={cn(
'h-4 w-4 transition-colors', 'h-3 w-3 transition-colors',
isFavorite(font.name) ? 'fill-red-500 text-red-500' : 'text-muted-foreground/30 hover:text-red-500/50' isFavorite(font.name) ? 'fill-red-500 text-red-500 !opacity-100' : 'text-muted-foreground/50 hover:text-red-500/50'
)} )}
/> />
</button> </button>
@@ -214,10 +214,10 @@ export function FontSelector({
</div> </div>
{/* Stats */} {/* Stats */}
<div className="mt-4 pt-4 border-t text-xs text-muted-foreground shrink-0"> <div className="mt-3 pt-3 border-t text-[10px] text-muted-foreground shrink-0">
{filteredFonts.length} font{filteredFonts.length !== 1 ? 's' : ''} {filteredFonts.length} font{filteredFonts.length !== 1 ? 's' : ''}
{filter === 'favorites' && ` ${favorites.length} total favorites`} {filter === 'favorites' && ` · ${favorites.length} favorites`}
{filter === 'recent' && ` ${recentFonts.length} recent`} {filter === 'recent' && ` · ${recentFonts.length} recent`}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -48,48 +48,43 @@ export function ColorInfo({ info, className }: ColorInfoProps) {
]; ];
return ( return (
<div className={cn('space-y-4', className)}> <div className={cn('space-y-3', className)}>
<div className="grid grid-cols-1 gap-3"> <div className="grid grid-cols-1 gap-1.5">
{formats.map((format) => ( {formats.map((format) => (
<div <div
key={format.label} key={format.label}
className="flex items-center justify-between p-3 bg-muted rounded-lg" className="flex items-center justify-between px-3 py-2 bg-muted/50 rounded-md group"
> >
<div className="flex-1"> <div className="flex items-baseline gap-2 min-w-0 flex-1">
<div className="text-xs text-muted-foreground mb-1">{format.label}</div> <span className="text-[10px] uppercase tracking-wider text-muted-foreground w-10 shrink-0">{format.label}</span>
<div className="font-mono text-sm">{format.value}</div> <span className="font-mono text-xs truncate">{format.value}</span>
</div> </div>
<Button <Button
size="icon" size="icon-xs"
variant="ghost" variant="ghost"
onClick={() => copyToClipboard(format.value, format.label)} onClick={() => copyToClipboard(format.value, format.label)}
aria-label={`Copy ${format.label} value`} aria-label={`Copy ${format.label} value`}
className="opacity-0 group-hover:opacity-100 transition-opacity"
> >
<Copy className="h-4 w-4" /> <Copy className="h-3 w-3" />
</Button> </Button>
</div> </div>
))} ))}
</div> </div>
<div className="grid grid-cols-2 gap-3 pt-2 border-t"> <div className="grid grid-cols-3 gap-3 pt-2 border-t text-xs">
<div className="space-y-1"> <div>
<div className="text-xs text-muted-foreground">Brightness</div> <div className="text-muted-foreground mb-0.5">Brightness</div>
<div className="text-sm font-medium">{(info.brightness * 100).toFixed(1)}%</div> <div className="font-medium">{(info.brightness * 100).toFixed(1)}%</div>
</div> </div>
<div className="space-y-1"> <div>
<div className="text-xs text-muted-foreground">Luminance</div> <div className="text-muted-foreground mb-0.5">Luminance</div>
<div className="text-sm font-medium">{(info.luminance * 100).toFixed(1)}%</div> <div className="font-medium">{(info.luminance * 100).toFixed(1)}%</div>
</div> </div>
<div className="space-y-1"> <div>
<div className="text-xs text-muted-foreground">Type</div> <div className="text-muted-foreground mb-0.5">{info.name && typeof info.name === 'string' ? 'Name' : 'Type'}</div>
<div className="text-sm font-medium">{info.is_light ? 'Light' : 'Dark'}</div> <div className="font-medium">{info.name && typeof info.name === 'string' ? info.name : (info.is_light ? 'Light' : 'Dark')}</div>
</div> </div>
{info.name && typeof info.name === 'string' && (
<div className="space-y-1">
<div className="text-xs text-muted-foreground">Named</div>
<div className="text-sm font-medium">{info.name}</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -2,6 +2,7 @@
import { HexColorPicker } from 'react-colorful'; import { HexColorPicker } from 'react-colorful';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { hexToRgb } from '@/lib/color/utils/color'; import { hexToRgb } from '@/lib/color/utils/color';
@@ -29,20 +30,20 @@ export function ColorPicker({ color, onChange, className }: ColorPickerProps) {
const textColor = getContrastColor(color); const textColor = getContrastColor(color);
return ( return (
<div className={cn('flex flex-col items-center justify-center space-y-4', className)}> <div className={cn('flex flex-col items-center justify-center space-y-3', className)}>
<div className="w-full max-w-[200px] space-y-4"> <div className="w-full max-w-[200px] space-y-3">
<HexColorPicker color={color} onChange={onChange} className="!w-full" /> <HexColorPicker color={color} onChange={onChange} className="!w-full" />
<div className="space-y-2"> <div className="space-y-1.5">
<label htmlFor="color-input" className="text-sm font-medium"> <Label htmlFor="color-input" className="text-xs">
Color Value Hex Value
</label> </Label>
<Input <Input
id="color-input" id="color-input"
type="text" type="text"
value={color} value={color}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="#ff0099 or rgb(255, 0, 153)" placeholder="#ff0099"
className="font-mono transition-colors duration-200" className="font-mono text-xs transition-colors duration-200"
style={{ style={{
backgroundColor: color, backgroundColor: color,
color: textColor, color: textColor,

View File

@@ -118,76 +118,70 @@ export function ExportMenu({ colors, className }: ExportMenuProps) {
return ( return (
<div className={className}> <div className={className}>
<div className="space-y-4"> <div className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="flex gap-3">
<div className="space-y-2"> <Select
<h3 className="text-sm font-medium">Export Format</h3> value={format}
<Select onValueChange={(value) => setFormat(value as ExportFormat)}
value={format} >
onValueChange={(value) => setFormat(value as ExportFormat)} <SelectTrigger className="flex-1">
> <SelectValue placeholder="Format" />
<SelectTrigger className="w-full"> </SelectTrigger>
<SelectValue placeholder="Select format" /> <SelectContent>
</SelectTrigger> <SelectItem value="css">CSS Variables</SelectItem>
<SelectContent> <SelectItem value="scss">SCSS Variables</SelectItem>
<SelectItem value="css">CSS Variables</SelectItem> <SelectItem value="tailwind">Tailwind Config</SelectItem>
<SelectItem value="scss">SCSS Variables</SelectItem> <SelectItem value="json">JSON</SelectItem>
<SelectItem value="tailwind">Tailwind Config</SelectItem> <SelectItem value="javascript">JavaScript Array</SelectItem>
<SelectItem value="json">JSON</SelectItem> </SelectContent>
<SelectItem value="javascript">JavaScript Array</SelectItem> </Select>
</SelectContent>
</Select>
</div>
<div className="space-y-2"> <Select
<h3 className="text-sm font-medium">Color Space</h3> value={colorSpace}
<Select onValueChange={(value) => setColorSpace(value as ColorSpace)}
value={colorSpace} >
onValueChange={(value) => setColorSpace(value as ColorSpace)} <SelectTrigger className="flex-1">
> <SelectValue placeholder="Space" />
<SelectTrigger className="w-full"> </SelectTrigger>
<SelectValue placeholder="Select space" /> <SelectContent>
</SelectTrigger> <SelectItem value="hex">Hex</SelectItem>
<SelectContent> <SelectItem value="rgb">RGB</SelectItem>
<SelectItem value="hex">Hex</SelectItem> <SelectItem value="hsl">HSL</SelectItem>
<SelectItem value="rgb">RGB</SelectItem> <SelectItem value="lab">Lab</SelectItem>
<SelectItem value="hsl">HSL</SelectItem> <SelectItem value="oklab">OkLab</SelectItem>
<SelectItem value="lab">Lab</SelectItem> <SelectItem value="lch">LCH</SelectItem>
<SelectItem value="oklab">OkLab</SelectItem> <SelectItem value="oklch">OkLCH</SelectItem>
<SelectItem value="lch">LCH</SelectItem> </SelectContent>
<SelectItem value="oklch">OkLCH</SelectItem> </Select>
</SelectContent>
</Select>
</div>
</div> </div>
<div className="p-4 bg-muted rounded-lg relative min-h-[100px]"> <div className="p-3 bg-muted/50 rounded-lg relative min-h-[80px]">
{isConverting ? ( {isConverting ? (
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm rounded-lg z-10"> <div className="absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm rounded-lg z-10">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div> </div>
) : null} ) : null}
<pre className="text-xs overflow-x-auto"> <pre className="text-[11px] overflow-x-auto leading-relaxed">
<code>{getExportContent()}</code> <code>{getExportContent()}</code>
</pre> </pre>
</div> </div>
<div className="flex gap-2 flex-col md:flex-row"> <div className="flex gap-2">
<Button onClick={handleCopy} variant="outline" className="w-full md:flex-1" disabled={isConverting}> <Button onClick={handleCopy} variant="outline" size="sm" className="flex-1" disabled={isConverting}>
{copied ? ( {copied ? (
<> <>
<Check className="h-4 w-4 mr-2" /> <Check className="h-3.5 w-3.5 mr-1.5" />
Copied! Copied
</> </>
) : ( ) : (
<> <>
<Copy className="h-4 w-4 mr-2" /> <Copy className="h-3.5 w-3.5 mr-1.5" />
Copy Copy
</> </>
)} )}
</Button> </Button>
<Button onClick={handleDownload} variant="default" className="w-full md:flex-1" disabled={isConverting}> <Button onClick={handleDownload} variant="default" size="sm" className="flex-1" disabled={isConverting}>
<Download className="h-4 w-4 mr-2" /> <Download className="h-3.5 w-3.5 mr-1.5" />
Download Download
</Button> </Button>
</div> </div>

View File

@@ -12,12 +12,25 @@ import {
useComplement useComplement
} from '@/lib/color/api/queries'; } from '@/lib/color/api/queries';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
interface ManipulationPanelProps { interface ManipulationPanelProps {
color: string; color: string;
onColorChange: (color: string) => void; onColorChange: (color: string) => void;
} }
interface ManipulationRow {
label: string;
icon: React.ReactNode;
value: number;
setValue: (v: number) => void;
format: (v: number) => string;
min: number;
max: number;
step: number;
onApply: () => Promise<void>;
}
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) { export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
const [lightenAmount, setLightenAmount] = useState(0.2); const [lightenAmount, setLightenAmount] = useState(0.2);
const [darkenAmount, setDarkenAmount] = useState(0.2); const [darkenAmount, setDarkenAmount] = useState(0.2);
@@ -32,93 +45,6 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const rotateMutation = useRotate(); const rotateMutation = useRotate();
const complementMutation = useComplement(); const complementMutation = useComplement();
const handleLighten = async () => {
try {
const result = await lightenMutation.mutateAsync({
colors: [color],
amount: lightenAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to lighten color');
}
};
const handleDarken = async () => {
try {
const result = await darkenMutation.mutateAsync({
colors: [color],
amount: darkenAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to darken color');
}
};
const handleSaturate = async () => {
try {
const result = await saturateMutation.mutateAsync({
colors: [color],
amount: saturateAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to saturate color');
}
};
const handleDesaturate = async () => {
try {
const result = await desaturateMutation.mutateAsync({
colors: [color],
amount: desaturateAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`);
}
} catch (error) {
toast.error('Failed to desaturate color');
}
};
const handleRotate = async () => {
try {
const result = await rotateMutation.mutateAsync({
colors: [color],
amount: rotateAmount,
});
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(`Rotated hue by ${rotateAmount}°`);
}
} catch (error) {
toast.error('Failed to rotate hue');
}
};
const handleComplement = async () => {
try {
const result = await complementMutation.mutateAsync([color]);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success('Generated complementary color');
}
} catch (error) {
toast.error('Failed to generate complement');
}
};
const isLoading = const isLoading =
lightenMutation.isPending || lightenMutation.isPending ||
darkenMutation.isPending || darkenMutation.isPending ||
@@ -127,108 +53,151 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
rotateMutation.isPending || rotateMutation.isPending ||
complementMutation.isPending; complementMutation.isPending;
const handleMutation = async (
mutationFn: (params: any) => Promise<any>,
params: any,
successMsg: string,
errorMsg: string
) => {
try {
const result = await mutationFn(params);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success(successMsg);
}
} catch {
toast.error(errorMsg);
}
};
const rows: ManipulationRow[] = [
{
label: 'Lighten',
icon: <Sun className="h-3.5 w-3.5" />,
value: lightenAmount,
setValue: setLightenAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
lightenMutation.mutateAsync,
{ colors: [color], amount: lightenAmount },
`Lightened by ${(lightenAmount * 100).toFixed(0)}%`,
'Failed to lighten color'
),
},
{
label: 'Darken',
icon: <Moon className="h-3.5 w-3.5" />,
value: darkenAmount,
setValue: setDarkenAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
darkenMutation.mutateAsync,
{ colors: [color], amount: darkenAmount },
`Darkened by ${(darkenAmount * 100).toFixed(0)}%`,
'Failed to darken color'
),
},
{
label: 'Saturate',
icon: <Droplets className="h-3.5 w-3.5" />,
value: saturateAmount,
setValue: setSaturateAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
saturateMutation.mutateAsync,
{ colors: [color], amount: saturateAmount },
`Saturated by ${(saturateAmount * 100).toFixed(0)}%`,
'Failed to saturate color'
),
},
{
label: 'Desaturate',
icon: <Droplet className="h-3.5 w-3.5" />,
value: desaturateAmount,
setValue: setDesaturateAmount,
format: (v) => `${(v * 100).toFixed(0)}%`,
min: 0, max: 1, step: 0.05,
onApply: () => handleMutation(
desaturateMutation.mutateAsync,
{ colors: [color], amount: desaturateAmount },
`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`,
'Failed to desaturate color'
),
},
{
label: 'Rotate',
icon: <RotateCcw className="h-3.5 w-3.5" />,
value: rotateAmount,
setValue: setRotateAmount,
format: (v) => `${v}°`,
min: -180, max: 180, step: 5,
onApply: () => handleMutation(
rotateMutation.mutateAsync,
{ colors: [color], amount: rotateAmount },
`Rotated hue by ${rotateAmount}°`,
'Failed to rotate hue'
),
},
];
const handleComplement = async () => {
try {
const result = await complementMutation.mutateAsync([color]);
if (result.colors[0]) {
onColorChange(result.colors[0].output);
toast.success('Generated complementary color');
}
} catch {
toast.error('Failed to generate complement');
}
};
return ( return (
<div className="space-y-6"> <div className="space-y-4">
{/* Lighten */} {rows.map((row) => (
<div className="space-y-3"> <div key={row.label} className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium">Lighten</label> <div className="flex items-center gap-1.5 text-xs font-medium">
<span className="text-xs text-muted-foreground">{(lightenAmount * 100).toFixed(0)}%</span> {row.icon}
<span>{row.label}</span>
</div>
<span className="text-[10px] text-muted-foreground tabular-nums">{row.format(row.value)}</span>
</div>
<div className="flex items-center gap-2">
<Slider
min={row.min}
max={row.max}
step={row.step}
value={[row.value]}
onValueChange={(vals) => row.setValue(vals[0])}
className="flex-1"
/>
<Button
onClick={row.onApply}
disabled={isLoading}
size="sm"
variant="outline"
className="shrink-0 w-16"
>
Apply
</Button>
</div>
</div> </div>
<Slider ))}
min={0}
max={1}
step={0.05}
value={[lightenAmount]}
onValueChange={(vals) => setLightenAmount(vals[0])}
/>
<Button onClick={handleLighten} disabled={isLoading} className="w-full">
Apply Lighten
</Button>
</div>
{/* Darken */} <div className="pt-3 border-t">
<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
min={0}
max={1}
step={0.05}
value={[darkenAmount]}
onValueChange={(vals) => setDarkenAmount(vals[0])}
/>
<Button onClick={handleDarken} disabled={isLoading} className="w-full">
Apply Darken
</Button>
</div>
{/* 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
min={0}
max={1}
step={0.05}
value={[saturateAmount]}
onValueChange={(vals) => setSaturateAmount(vals[0])}
/>
<Button onClick={handleSaturate} disabled={isLoading} className="w-full">
Apply Saturate
</Button>
</div>
{/* 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
min={0}
max={1}
step={0.05}
value={[desaturateAmount]}
onValueChange={(vals) => setDesaturateAmount(vals[0])}
/>
<Button onClick={handleDesaturate} disabled={isLoading} className="w-full">
Apply Desaturate
</Button>
</div>
{/* 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
min={-180}
max={180}
step={5}
value={[rotateAmount]}
onValueChange={(vals) => setRotateAmount(vals[0])}
/>
<Button onClick={handleRotate} disabled={isLoading} className="w-full">
Apply Rotation
</Button>
</div>
{/* Quick Actions */}
<div className="pt-4 border-t space-y-2">
<h3 className="text-sm font-medium mb-3">Quick Actions</h3>
<Button <Button
onClick={handleComplement} onClick={handleComplement}
disabled={isLoading} disabled={isLoading}
variant="outline" variant="outline"
className="w-full" className="w-full"
size="sm"
> >
Get Complementary Color <ArrowLeftRight className="h-3.5 w-3.5 mr-1.5" />
Complementary Color
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -88,14 +88,14 @@ export function FaviconFileUpload({
/> />
{selectedFile ? ( {selectedFile ? (
<div className="border border-border rounded-lg p-4 bg-card/50 backdrop-blur-sm"> <div className="border border-border rounded-xl p-4 bg-card/50 backdrop-blur-sm">
<div className="flex items-start gap-4"> <div className="flex items-start gap-3">
<div className="p-2 bg-primary/10 rounded-lg"> <div className="p-2 bg-primary/10 rounded-lg shrink-0">
<FileImage className="h-6 w-6 text-primary" /> <FileImage className="h-5 w-5 text-primary" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p className="text-sm font-semibold text-foreground truncate" title={selectedFile.name}> <p className="text-sm font-medium text-foreground truncate" title={selectedFile.name}>
{selectedFile.name} {selectedFile.name}
</p> </p>
<Button <Button
@@ -103,18 +103,18 @@ export function FaviconFileUpload({
size="icon-xs" size="icon-xs"
onClick={onFileRemove} onClick={onFileRemove}
disabled={disabled} disabled={disabled}
className="rounded-full hover:bg-destructive/10 hover:text-destructive" className="rounded-full hover:bg-destructive/10 hover:text-destructive shrink-0"
> >
<X className="h-3.5 w-3.5" /> <X className="h-3.5 w-3.5" />
</Button> </Button>
</div> </div>
<div className="mt-2 flex gap-4 text-[10px] text-muted-foreground uppercase tracking-wider font-bold"> <div className="mt-1.5 flex gap-3 text-[10px] text-muted-foreground">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<HardDrive className="h-3 w-3" /> <HardDrive className="h-3 w-3" />
<span>{(selectedFile.size / 1024).toFixed(1)} KB</span> <span>{(selectedFile.size / 1024).toFixed(1)} KB</span>
</div> </div>
{dimensions && ( {dimensions && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<FileImage className="h-3 w-3" /> <FileImage className="h-3 w-3" />
<span>{dimensions}</span> <span>{dimensions}</span>
</div> </div>
@@ -131,23 +131,23 @@ export function FaviconFileUpload({
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
className={cn( className={cn(
'border-2 border-dashed rounded-xl p-10 text-center cursor-pointer transition-all duration-300', 'border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200',
'hover:border-primary/50 hover:bg-primary/5', 'hover:border-primary/40 hover:bg-primary/5',
{ {
'border-primary bg-primary/10 scale-[0.98]': isDragging, 'border-primary bg-primary/10 scale-[0.98]': isDragging,
'border-border bg-muted/30': !isDragging, 'border-border/50': !isDragging,
'opacity-50 cursor-not-allowed': disabled, 'opacity-50 cursor-not-allowed': disabled,
} }
)} )}
> >
<div className="bg-primary/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="bg-primary/10 w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3">
<Upload className="h-8 w-8 text-primary" /> <Upload className="h-6 w-6 text-primary" />
</div> </div>
<p className="text-sm font-semibold text-foreground mb-1"> <p className="text-sm font-medium text-foreground mb-0.5">
Drop icon source here Drop icon source here
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
Recommended: 512x512 PNG or SVG 512x512 PNG or SVG recommended
</p> </p>
</div> </div>
)} )}

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { Download, Loader2, RefreshCw, Code2, Globe, Layout, Palette } from 'lucide-react'; import { Download, Loader2, Code2, Globe, Layout } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } 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 { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
@@ -76,22 +76,16 @@ export function FaviconGenerator() {
}; };
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Settings Column */} {/* Settings Column */}
<div className="lg:col-span-4 space-y-6"> <div className="lg:col-span-4 space-y-6">
<Card className="glass"> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle>App Details</CardTitle>
<Layout className="h-5 w-5 text-primary" />
App Details
</CardTitle>
<CardDescription>
Configure how your app appears on devices
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3">
<div className="space-y-2"> <div className="space-y-1.5">
<Label htmlFor="app-name">Application Name</Label> <Label htmlFor="app-name" className="text-xs">Application Name</Label>
<Input <Input
id="app-name" id="app-name"
value={options.name} value={options.name}
@@ -99,8 +93,8 @@ export function FaviconGenerator() {
placeholder="e.g. My Awesome Website" placeholder="e.g. My Awesome Website"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-1.5">
<Label htmlFor="short-name">Short Name</Label> <Label htmlFor="short-name" className="text-xs">Short Name</Label>
<Input <Input
id="short-name" id="short-name"
value={options.shortName} value={options.shortName}
@@ -112,22 +106,19 @@ export function FaviconGenerator() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="glass"> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle>Theme Colors</CardTitle>
<Palette className="h-5 w-5 text-primary" />
Theme Colors
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-2"> <div className="space-y-1.5">
<Label htmlFor="bg-color">Background</Label> <Label htmlFor="bg-color" className="text-xs">Background</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="bg-color" id="bg-color"
type="color" type="color"
className="w-10 p-1 h-10 shrink-0" className="w-9 p-1 h-9 shrink-0"
value={options.backgroundColor} value={options.backgroundColor}
onChange={(e) => setOptions({ ...options, backgroundColor: e.target.value })} onChange={(e) => setOptions({ ...options, backgroundColor: e.target.value })}
/> />
@@ -138,13 +129,13 @@ export function FaviconGenerator() {
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-1.5">
<Label htmlFor="theme-color">Theme</Label> <Label htmlFor="theme-color" className="text-xs">Theme</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="theme-color" id="theme-color"
type="color" type="color"
className="w-10 p-1 h-10 shrink-0" className="w-9 p-1 h-9 shrink-0"
value={options.themeColor} value={options.themeColor}
onChange={(e) => setOptions({ ...options, themeColor: e.target.value })} onChange={(e) => setOptions({ ...options, themeColor: e.target.value })}
/> />
@@ -159,8 +150,8 @@ export function FaviconGenerator() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="glass overflow-hidden"> <Card className="overflow-hidden">
<CardContent className="p-6"> <CardContent>
<FaviconFileUpload <FaviconFileUpload
selectedFile={sourceFile} selectedFile={sourceFile}
onFileSelect={setSourceFile} onFileSelect={setSourceFile}
@@ -168,21 +159,17 @@ export function FaviconGenerator() {
disabled={isGenerating} disabled={isGenerating}
/> />
<Button <Button
className="w-full mt-6" className="w-full mt-4"
size="lg"
disabled={!sourceFile || isGenerating} disabled={!sourceFile || isGenerating}
onClick={handleGenerate} onClick={handleGenerate}
> >
{isGenerating ? ( {isGenerating ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating... {progress}% Generating... {progress}%
</> </>
) : ( ) : (
<> 'Generate Favicons'
<RefreshCw className="mr-2 h-4 w-4" />
Generate Favicons
</>
)} )}
</Button> </Button>
{result && ( {result && (
@@ -200,58 +187,51 @@ export function FaviconGenerator() {
{/* Results Column */} {/* Results Column */}
<div className="lg:col-span-8 space-y-6"> <div className="lg:col-span-8 space-y-6">
{!result && !isGenerating ? ( {isGenerating ? (
<Card className="glass h-full flex flex-col items-center justify-center p-12 text-center text-muted-foreground border-dashed border-2"> <Card className="h-full flex flex-col items-center justify-center p-10 space-y-4">
<Globe className="h-12 w-12 mb-4 opacity-20" /> <Loader2 className="h-6 w-6 text-primary animate-spin" />
<h3 className="text-lg font-semibold text-foreground">Ready to generate</h3> <div className="w-full max-w-xs space-y-2">
<p className="max-w-xs text-sm">Upload a square image (PNG or SVG recommended) and configure your app details to get started.</p> <div className="flex items-center justify-between text-[10px] text-muted-foreground">
</Card> <div className="flex items-center gap-1.5">
) : isGenerating ? ( <span className="font-medium">Processing...</span>
<Card className="glass h-full flex flex-col items-center justify-center p-12 space-y-6"> </div>
<div className="relative"> <span className="tabular-nums">{progress}%</span>
<div className="h-24 w-24 rounded-full border-4 border-primary/20 animate-pulse" />
<Loader2 className="h-12 w-12 text-primary animate-spin absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
<div className="w-full max-w-sm space-y-2">
<div className="flex justify-between text-xs font-bold uppercase tracking-widest text-muted-foreground">
<span>Processing Icons</span>
<span>{progress}%</span>
</div> </div>
<Progress value={progress} className="h-1.5" /> <Progress value={progress} className="h-1" />
</div> </div>
</Card> </Card>
) : ( ) : result ? (
<div className="space-y-6 animate-fadeIn"> <div className="space-y-5 animate-fade-in">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h2 className="text-2xl font-bold tracking-tight">Generated Assets</h2> <h2 className="text-lg font-bold">Generated Assets</h2>
<Button onClick={handleDownloadAll} variant="default" className="shadow-lg shadow-primary/20"> <Button onClick={handleDownloadAll}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-1.5 h-3.5 w-3.5" />
Download All (ZIP) Download ZIP
</Button> </Button>
</div> </div>
<Tabs defaultValue="icons" className="w-full"> <Tabs defaultValue="icons" className="w-full">
<TabsList className="w-full"> <TabsList className="w-full">
<TabsTrigger value="icons" className="flex items-center gap-2"> <TabsTrigger value="icons" className="flex items-center gap-1.5">
<Layout className="h-4 w-4" /> <Layout className="h-3.5 w-3.5" />
Icons Icons
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="html" className="flex items-center gap-2"> <TabsTrigger value="html" className="flex items-center gap-1.5">
<Code2 className="h-4 w-4" /> <Code2 className="h-3.5 w-3.5" />
HTML Code HTML
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="manifest" className="flex items-center gap-2"> <TabsTrigger value="manifest" className="flex items-center gap-1.5">
<Globe className="h-4 w-4" /> <Globe className="h-3.5 w-3.5" />
Manifest Manifest
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="icons" className="mt-6"> <TabsContent value="icons" className="mt-4">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{result?.icons.map((icon) => ( {result?.icons.map((icon) => (
<Card key={icon.name} className="glass group overflow-hidden"> <Card key={icon.name} className="group overflow-hidden">
<div className="p-4 flex flex-col items-center text-center space-y-3"> <div className="p-3 flex flex-col items-center text-center space-y-2">
<div className="relative h-20 w-20 flex items-center justify-center bg-black/20 rounded-lg p-2 border border-border group-hover:scale-110 transition-transform duration-300"> <div className="relative h-16 w-16 flex items-center justify-center bg-muted/50 rounded-lg p-1.5 border border-border/50 group-hover:scale-105 transition-transform duration-200">
{icon.previewUrl && ( {icon.previewUrl && (
<img <img
src={icon.previewUrl} src={icon.previewUrl}
@@ -260,12 +240,12 @@ export function FaviconGenerator() {
/> />
)} )}
</div> </div>
<div className="space-y-1 w-full"> <div className="space-y-0.5 w-full">
<p className="text-[10px] font-bold text-foreground truncate uppercase tracking-tighter" title={icon.name}> <p className="text-[10px] font-medium text-foreground truncate" title={icon.name}>
{icon.name} {icon.name}
</p> </p>
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
{icon.width}x{icon.height} {(icon.size / 1024).toFixed(1)} KB {icon.width}x{icon.height} · {(icon.size / 1024).toFixed(1)} KB
</p> </p>
</div> </div>
</div> </div>
@@ -274,27 +254,27 @@ export function FaviconGenerator() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="html" className="mt-6 space-y-4"> <TabsContent value="html" className="mt-4 space-y-3">
<div className="space-y-2"> <div className="space-y-1.5">
<Label>Embed in your &lt;head&gt;</Label> <Label className="text-xs">Embed in your &lt;head&gt;</Label>
{result && <CodeSnippet code={result.htmlCode} />} {result && <CodeSnippet code={result.htmlCode} />}
</div> </div>
<div className="p-4 rounded-lg bg-info/5 border border-info/20"> <div className="p-3 rounded-lg bg-primary/5 border border-primary/10">
<p className="text-xs text-info leading-relaxed"> <p className="text-[11px] text-muted-foreground leading-relaxed">
<strong>Note:</strong> Make sure to place the generated files in your website&apos;s root directory or update the <code>href</code> paths accordingly. Place generated files in your site root or update the <code className="text-primary">href</code> paths.
</p> </p>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="manifest" className="mt-6"> <TabsContent value="manifest" className="mt-4">
<div className="space-y-2"> <div className="space-y-1.5">
<Label>site.webmanifest</Label> <Label className="text-xs">site.webmanifest</Label>
{result && <CodeSnippet code={result.manifest} />} {result && <CodeSnippet code={result.manifest} />}
</div> </div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
)} ) : null}
</div> </div>
</div> </div>
); );

View File

@@ -3,7 +3,7 @@
import * as React from 'react'; 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, ChevronRight, X } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { useSidebar } from './SidebarProvider'; import { useSidebar } from './SidebarProvider';
@@ -17,7 +17,7 @@ export function AppHeader() {
const pathSegments = pathname.split('/').filter(Boolean); const pathSegments = pathname.split('/').filter(Boolean);
return ( return (
<header className="h-16 border-b border-border bg-background/10 backdrop-blur-xl sticky top-0 z-40 flex items-center justify-between pl-8 pr-5 md:pr-9"> <header className="h-16 border-b border-border bg-background/10 backdrop-blur-xl sticky top-0 z-40 flex items-center justify-between pl-8 pr-5 md:pr-9 shadow-[0_1px_3px_0_rgb(0_0_0/0.05)]">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<nav className="flex items-center text-sm font-medium"> <nav className="flex items-center text-sm font-medium">
<Link href="/" className="flex items-center gap-2"> <Link href="/" className="flex items-center gap-2">
@@ -32,7 +32,7 @@ export function AppHeader() {
return ( return (
<React.Fragment key={href}> <React.Fragment key={href}>
<ChevronRight className="h-4 w-4 mx-1 text-muted-foreground/30" /> <ChevronRight className="h-4 w-4 mx-1 text-muted-foreground/50" />
<Link <Link
href={href} href={href}
className={cn( className={cn(

View File

@@ -12,12 +12,12 @@ interface AppPageProps {
export function AppPage({ title, description, children, className }: AppPageProps) { export function AppPage({ title, description, children, className }: AppPageProps) {
return ( return (
<div className={cn("min-h-screen py-12", className)}> <div className={cn("min-h-screen py-8", className)}>
<div className="max-w-7xl mx-auto px-8 space-y-8"> <div className="max-w-7xl mx-auto px-8 space-y-6 animate-fade-in">
<div> <div>
<h1 className="text-4xl font-bold mb-2">{title}</h1> <h1 className="text-2xl font-bold mb-1">{title}</h1>
{description && ( {description && (
<p className="text-muted-foreground"> <p className="text-sm text-muted-foreground">
{description} {description}
</p> </p>
)} )}

View File

@@ -74,7 +74,7 @@ export function AppSidebar() {
)} )}
<aside className={cn( <aside className={cn(
"fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-background/10 backdrop-blur-2xl transition-all duration-300 ease-in-out lg:relative lg:h-full", "fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-background/20 backdrop-blur-2xl transition-all duration-300 ease-in-out lg:relative lg:h-full",
isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0", isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0",
isCollapsed ? "lg:w-20" : "w-64" isCollapsed ? "lg:w-20" : "w-64"
)}> )}>
@@ -101,15 +101,10 @@ export function AppSidebar() {
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 overflow-y-auto px-4 py-2 space-y-8 mt-4 overflow-x-hidden"> <nav className="flex-1 overflow-y-auto px-4 py-2 space-y-6 mt-4 overflow-x-hidden">
{navigation.map((group) => ( {navigation.map((group) => (
<div key={group.label} className="space-y-2"> <div key={group.label}>
{!isCollapsed && ( <div className="space-y-0.5">
<h4 className="px-3 text-xs font-semibold text-muted-foreground/50 uppercase tracking-wider">
{group.label}
</h4>
)}
<div className="space-y-1">
{group.items.map((item) => { {group.items.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href)); const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
@@ -119,9 +114,9 @@ export function AppSidebar() {
href={item.href} href={item.href}
onClick={() => { if (window.innerWidth < 1024) close(); }} onClick={() => { if (window.innerWidth < 1024) close(); }}
className={cn( className={cn(
"flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300 relative group/item", "flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-300 relative group/item",
isActive isActive
? "bg-primary/10 text-primary ring-1 ring-primary/20" ? "bg-primary/10 text-primary border-l-2 border-primary"
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground", : "text-foreground/80 hover:bg-accent/50 hover:text-foreground",
isCollapsed ? "justify-center" : "justify-between" isCollapsed ? "justify-center" : "justify-between"
)} )}
@@ -185,7 +180,7 @@ export function AppSidebar() {
) : ( ) : (
<> <>
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
<span className="text-xs font-semibold uppercase tracking-wider">Collapse Sidebar</span> <span className="text-xs font-semibold uppercase tracking-wider">Collapse</span>
</> </>
)} )}
</Button> </Button>

View File

@@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -37,7 +38,7 @@ export function ConversionOptionsPanel({
<div className="space-y-4"> <div className="space-y-4">
{/* Video Codec */} {/* Video Codec */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Video Codec</label> <Label>Video Codec</Label>
<Select <Select
value={options.videoCodec || 'default'} value={options.videoCodec || 'default'}
onValueChange={(value) => handleOptionChange('videoCodec', value === 'default' ? undefined : value)} onValueChange={(value) => handleOptionChange('videoCodec', value === 'default' ? undefined : value)}
@@ -59,7 +60,7 @@ export function ConversionOptionsPanel({
{/* Video Bitrate */} {/* Video Bitrate */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Video Bitrate</label> <Label>Video Bitrate</Label>
<span className="text-xs text-muted-foreground">{options.videoBitrate || '2M'}</span> <span className="text-xs text-muted-foreground">{options.videoBitrate || '2M'}</span>
</div> </div>
<Slider <Slider
@@ -75,7 +76,7 @@ export function ConversionOptionsPanel({
{/* Resolution */} {/* Resolution */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Resolution</label> <Label>Resolution</Label>
<Select <Select
value={options.videoResolution || 'original'} value={options.videoResolution || 'original'}
onValueChange={(value) => handleOptionChange('videoResolution', value === 'original' ? undefined : value)} onValueChange={(value) => handleOptionChange('videoResolution', value === 'original' ? undefined : value)}
@@ -96,7 +97,7 @@ export function ConversionOptionsPanel({
{/* FPS */} {/* FPS */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Frame Rate (FPS)</label> <Label>Frame Rate (FPS)</Label>
<Select <Select
value={options.videoFps?.toString() || 'original'} value={options.videoFps?.toString() || 'original'}
onValueChange={(value) => handleOptionChange('videoFps', value === 'original' ? undefined : parseInt(value))} onValueChange={(value) => handleOptionChange('videoFps', value === 'original' ? undefined : parseInt(value))}
@@ -118,7 +119,7 @@ export function ConversionOptionsPanel({
{/* Audio Bitrate */} {/* Audio Bitrate */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Audio Bitrate</label> <Label>Audio Bitrate</Label>
<span className="text-xs text-muted-foreground">{options.audioBitrate || '128k'}</span> <span className="text-xs text-muted-foreground">{options.audioBitrate || '128k'}</span>
</div> </div>
<Slider <Slider
@@ -137,7 +138,7 @@ export function ConversionOptionsPanel({
<div className="space-y-4"> <div className="space-y-4">
{/* Audio Codec */} {/* Audio Codec */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Audio Codec</label> <Label>Audio Codec</Label>
<Select <Select
value={options.audioCodec || 'default'} value={options.audioCodec || 'default'}
onValueChange={(value) => handleOptionChange('audioCodec', value === 'default' ? undefined : value)} onValueChange={(value) => handleOptionChange('audioCodec', value === 'default' ? undefined : value)}
@@ -160,7 +161,7 @@ export function ConversionOptionsPanel({
{/* Bitrate */} {/* Bitrate */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Bitrate</label> <Label>Bitrate</Label>
<span className="text-xs text-muted-foreground">{options.audioBitrate || '192k'}</span> <span className="text-xs text-muted-foreground">{options.audioBitrate || '192k'}</span>
</div> </div>
<Slider <Slider
@@ -175,7 +176,7 @@ export function ConversionOptionsPanel({
{/* Sample Rate */} {/* Sample Rate */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Sample Rate</label> <Label>Sample Rate</Label>
<Select <Select
value={options.audioSampleRate?.toString() || 'original'} value={options.audioSampleRate?.toString() || 'original'}
onValueChange={(value) => handleOptionChange('audioSampleRate', value === 'original' ? undefined : parseInt(value))} onValueChange={(value) => handleOptionChange('audioSampleRate', value === 'original' ? undefined : parseInt(value))}
@@ -195,7 +196,7 @@ export function ConversionOptionsPanel({
{/* Channels */} {/* Channels */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Channels</label> <Label>Channels</Label>
<Select <Select
value={options.audioChannels?.toString() || 'original'} value={options.audioChannels?.toString() || 'original'}
onValueChange={(value) => handleOptionChange('audioChannels', value === 'original' ? undefined : parseInt(value))} onValueChange={(value) => handleOptionChange('audioChannels', value === 'original' ? undefined : parseInt(value))}
@@ -219,7 +220,7 @@ export function ConversionOptionsPanel({
{/* Quality */} {/* Quality */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Quality</label> <Label>Quality</Label>
<span className="text-xs text-muted-foreground">{options.imageQuality || 85}%</span> <span className="text-xs text-muted-foreground">{options.imageQuality || 85}%</span>
</div> </div>
<Slider <Slider
@@ -234,7 +235,7 @@ export function ConversionOptionsPanel({
{/* Width */} {/* Width */}
<div> <div>
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label> <Label className="mb-2">Width (px)</Label>
<Input <Input
type="number" type="number"
value={options.imageWidth || ''} value={options.imageWidth || ''}
@@ -247,7 +248,7 @@ export function ConversionOptionsPanel({
{/* Height */} {/* Height */}
<div> <div>
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label> <Label className="mb-2">Height (px)</Label>
<Input <Input
type="number" type="number"
value={options.imageHeight || ''} value={options.imageHeight || ''}

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, FileCheck2, ArrowRight, RefreshCw } from 'lucide-react'; import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, RefreshCw } 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -85,7 +85,7 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
switch (category) { switch (category) {
case 'image': case 'image':
return ( return (
<div className="mt-4 rounded-lg overflow-hidden bg-muted/30 flex items-center justify-center p-4"> <div className="mt-3 rounded-lg overflow-hidden bg-muted/30 flex items-center justify-center p-4">
<img <img
src={previewUrl} src={previewUrl}
alt="Converted image preview" alt="Converted image preview"
@@ -107,7 +107,7 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
case 'video': case 'video':
return ( return (
<div className="mt-4 rounded-lg overflow-hidden bg-muted/30"> <div className="mt-3 rounded-lg overflow-hidden bg-muted/30">
<video src={previewUrl} controls className="w-full max-h-64"> <video src={previewUrl} controls className="w-full max-h-64">
Your browser does not support video playback. Your browser does not support video playback.
</video> </video>
@@ -140,38 +140,38 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
switch (job.status) { switch (job.status) {
case 'loading': case 'loading':
return ( return (
<div className="space-y-3"> <div className="space-y-2">
<div className="flex items-center gap-2 text-info"> <div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
<span className="text-sm font-medium">Loading WASM converter...</span> <span className="text-xs font-medium">Loading converter...</span>
</div> </div>
<div className="flex items-center gap-2 text-xs text-muted-foreground"> <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
<Clock className="h-3.5 w-3.5" /> <Clock className="h-3 w-3" />
<span>Elapsed: {formatTime(elapsedTime)}</span> <span>{formatTime(elapsedTime)}</span>
</div> </div>
</div> </div>
); );
case 'processing': case 'processing':
return ( return (
<div className="space-y-3"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-info"> <div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
<span className="text-sm font-medium">Converting...</span> <span className="text-xs font-medium">Converting...</span>
</div> </div>
<span className="text-xs text-muted-foreground">{job.progress}%</span> <span className="text-[10px] text-muted-foreground tabular-nums">{job.progress}%</span>
</div> </div>
<Progress value={job.progress} /> <Progress value={job.progress} className="h-1" />
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-3 text-[10px] text-muted-foreground">
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" /> <Clock className="h-3 w-3" />
<span>Elapsed: {formatTime(elapsedTime)}</span> <span>{formatTime(elapsedTime)}</span>
</div> </div>
{estimatedTimeRemaining && ( {estimatedTimeRemaining && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<TrendingUp className="h-3.5 w-3.5" /> <TrendingUp className="h-3 w-3" />
<span>~{formatTime(estimatedTimeRemaining)} remaining</span> <span>~{formatTime(estimatedTimeRemaining)} left</span>
</div> </div>
)} )}
</div> </div>
@@ -184,39 +184,27 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
const sizeReduction = inputSize > 0 ? ((inputSize - outputSize) / inputSize) * 100 : 0; const sizeReduction = inputSize > 0 ? ((inputSize - outputSize) / inputSize) * 100 : 0;
return ( return (
<div className="space-y-3"> <div className="space-y-2">
<div className="flex items-center gap-2 text-success"> <div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5" /> <CheckCircle className="h-3.5 w-3.5 text-primary" />
<span className="text-sm font-medium">Conversion complete!</span> <span className="text-xs font-medium">Complete</span>
</div> </div>
{/* File size comparison */} <div className="bg-muted/50 rounded-lg p-2.5 space-y-1">
<div className="bg-muted/50 rounded-lg p-3 space-y-2"> <div className="flex items-center justify-between text-xs">
<div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Input</span>
<div className="flex items-center gap-2"> <span className="font-medium tabular-nums">{formatFileSize(inputSize)}</span>
<FileCheck2 className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Input:</span>
</div>
<span className="font-medium">{formatFileSize(inputSize)}</span>
</div> </div>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center justify-center py-1"> <span className="text-muted-foreground">Output</span>
<ArrowRight className="h-4 w-4 text-muted-foreground" /> <div className="flex items-center gap-1.5">
</div> <span className="font-medium tabular-nums">{formatFileSize(outputSize)}</span>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<FileCheck2 className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Output:</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">{formatFileSize(outputSize)}</span>
{Math.abs(sizeReduction) > 1 && ( {Math.abs(sizeReduction) > 1 && (
<span className={cn( <span className={cn(
"text-xs px-2 py-0.5 rounded-full", "text-[10px] px-1.5 py-0.5 rounded-full",
sizeReduction > 0 sizeReduction > 0
? "bg-success/10 text-success" ? "bg-primary/10 text-primary"
: "bg-info/10 text-info" : "bg-muted text-muted-foreground"
)}> )}>
{sizeReduction > 0 ? '-' : '+'}{Math.abs(sizeReduction).toFixed(0)}% {sizeReduction > 0 ? '-' : '+'}{Math.abs(sizeReduction).toFixed(0)}%
</span> </span>
@@ -230,8 +218,8 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
case 'error': case 'error':
return ( return (
<div className="flex items-center gap-2 text-destructive"> <div className="flex items-center gap-2 text-destructive">
<XCircle className="h-5 w-5" /> <XCircle className="h-3.5 w-3.5" />
<span className="text-sm font-medium">Conversion failed</span> <span className="text-xs font-medium">Conversion failed</span>
</div> </div>
); );
@@ -245,48 +233,41 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
} }
return ( return (
<Card className="animate-fadeIn"> <Card className="animate-fade-in">
<CardHeader> <CardHeader>
<CardTitle className="text-lg">Conversion Status</CardTitle> <CardTitle className="text-sm">Conversion</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-3">
{/* Status */}
{renderStatus()} {renderStatus()}
{/* Error message */}
{job.error && ( {job.error && (
<div className="bg-destructive/10 border border-destructive/20 rounded-md p-3"> <div className="bg-destructive/10 border border-destructive/20 rounded-md p-2.5">
<p className="text-sm text-destructive">{job.error}</p> <p className="text-xs text-destructive">{job.error}</p>
</div> </div>
)} )}
{/* Retry button */}
{job.status === 'error' && onRetry && ( {job.status === 'error' && onRetry && (
<Button onClick={onRetry} variant="outline" className="w-full gap-2"> <Button onClick={onRetry} variant="outline" className="w-full">
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-3.5 w-3.5 mr-1.5" />
Retry Conversion Retry
</Button> </Button>
)} )}
{/* Preview */}
{job.status === 'completed' && renderPreview()} {job.status === 'completed' && renderPreview()}
{/* Download button */}
{job.status === 'completed' && job.result && ( {job.status === 'completed' && job.result && (
<Button onClick={handleDownload} className="w-full" variant="default" size="lg"> <Button onClick={handleDownload} className="w-full">
<Download className="h-4 w-4 shrink-0" /> <Download className="h-3.5 w-3.5 shrink-0 mr-1.5" />
<span className="truncate min-w-0"> <span className="truncate min-w-0">
Download{' '}
{generateOutputFilename(job.inputFile.name, job.outputFormat.extension)} {generateOutputFilename(job.inputFile.name, job.outputFormat.extension)}
</span> </span>
</Button> </Button>
)} )}
{/* Duration */}
{job.status === 'completed' && job.startTime && job.endTime && ( {job.status === 'completed' && job.startTime && job.endTime && (
<p className="text-xs text-muted-foreground text-center"> <p className="text-[10px] text-muted-foreground text-center">
Completed in {((job.endTime - job.startTime) / 1000).toFixed(2)}s {((job.endTime - job.startTime) / 1000).toFixed(1)}s
</p> </p>
)} )}
</div> </div>

View File

@@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
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 { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { import {
@@ -366,13 +367,12 @@ export function FileConverter() {
const completedCount = conversionJobs.filter(job => job.status === 'completed').length; const completedCount = conversionJobs.filter(job => job.status === 'completed').length;
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 w-full"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
{/* Left Column: Upload and Conversion Options */} {/* Left Column: Upload and Conversion Options */}
<div className="space-y-6"> <div className="space-y-6">
{/* Upload Card */} {/* Upload Card */}
<Card className="glass"> <Card>
<CardContent className="p-6"> <CardContent>
{/* File upload */}
<FileUpload <FileUpload
onFileSelect={handleFileSelect} onFileSelect={handleFileSelect}
onFileRemove={handleFileRemove} onFileRemove={handleFileRemove}
@@ -386,14 +386,14 @@ export function FileConverter() {
{/* Conversion Options Card */} {/* Conversion Options Card */}
{inputFormat && compatibleFormats.length > 0 && ( {inputFormat && compatibleFormats.length > 0 && (
<Card className="glass"> <Card>
<CardHeader> <CardHeader>
<CardTitle>Conversion Options</CardTitle> <CardTitle>Options</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-4">
{/* Output Format Select */} {/* Output Format Select */}
<div className="space-y-2"> <div className="space-y-1.5">
<label className="text-sm font-medium text-foreground block">Output Format</label> <Label className="text-xs">Output Format</Label>
<Select <Select
value={outputFormat?.id || ''} value={outputFormat?.id || ''}
onValueChange={(formatId) => { onValueChange={(formatId) => {
@@ -422,7 +422,7 @@ export function FileConverter() {
<div className="space-y-4 pt-4 border-t border-border"> <div className="space-y-4 pt-4 border-t border-border">
{/* Video Codec */} {/* Video Codec */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Video Codec</label> <Label className="text-xs">Video Codec</Label>
<Select <Select
value={conversionOptions.videoCodec || 'default'} value={conversionOptions.videoCodec || 'default'}
onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoCodec: value === 'default' ? undefined : value })} onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoCodec: value === 'default' ? undefined : value })}
@@ -444,7 +444,7 @@ export function FileConverter() {
{/* Video Bitrate */} {/* Video Bitrate */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Video Bitrate</label> <Label className="text-xs">Video Bitrate</Label>
<span className="text-xs text-muted-foreground">{conversionOptions.videoBitrate || '2M'}</span> <span className="text-xs text-muted-foreground">{conversionOptions.videoBitrate || '2M'}</span>
</div> </div>
<Slider <Slider
@@ -460,7 +460,7 @@ export function FileConverter() {
{/* Resolution */} {/* Resolution */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Resolution</label> <Label className="text-xs">Resolution</Label>
<Select <Select
value={conversionOptions.videoResolution || 'original'} value={conversionOptions.videoResolution || 'original'}
onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoResolution: value === 'original' ? undefined : value })} onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoResolution: value === 'original' ? undefined : value })}
@@ -481,7 +481,7 @@ export function FileConverter() {
{/* FPS */} {/* FPS */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Frame Rate (FPS)</label> <Label className="text-xs">Frame Rate (FPS)</Label>
<Select <Select
value={conversionOptions.videoFps?.toString() || 'original'} value={conversionOptions.videoFps?.toString() || 'original'}
onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoFps: value === 'original' ? undefined : parseInt(value) })} onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoFps: value === 'original' ? undefined : parseInt(value) })}
@@ -503,7 +503,7 @@ export function FileConverter() {
{/* Audio Bitrate */} {/* Audio Bitrate */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Audio Bitrate</label> <Label className="text-xs">Audio Bitrate</Label>
<span className="text-xs text-muted-foreground">{conversionOptions.audioBitrate || '128k'}</span> <span className="text-xs text-muted-foreground">{conversionOptions.audioBitrate || '128k'}</span>
</div> </div>
<Slider <Slider
@@ -522,7 +522,7 @@ export function FileConverter() {
<div className="space-y-4 pt-4 border-t border-border"> <div className="space-y-4 pt-4 border-t border-border">
{/* Audio Codec */} {/* Audio Codec */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Audio Codec</label> <Label className="text-xs">Audio Codec</Label>
<Select <Select
value={conversionOptions.audioCodec || 'default'} value={conversionOptions.audioCodec || 'default'}
onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioCodec: value === 'default' ? undefined : value })} onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioCodec: value === 'default' ? undefined : value })}
@@ -545,7 +545,7 @@ export function FileConverter() {
{/* Bitrate */} {/* Bitrate */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Bitrate</label> <Label className="text-xs">Bitrate</Label>
<span className="text-xs text-muted-foreground">{conversionOptions.audioBitrate || '192k'}</span> <span className="text-xs text-muted-foreground">{conversionOptions.audioBitrate || '192k'}</span>
</div> </div>
<Slider <Slider
@@ -560,7 +560,7 @@ export function FileConverter() {
{/* Sample Rate */} {/* Sample Rate */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Sample Rate</label> <Label className="text-xs">Sample Rate</Label>
<Select <Select
value={conversionOptions.audioSampleRate?.toString() || 'original'} value={conversionOptions.audioSampleRate?.toString() || 'original'}
onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioSampleRate: value === 'original' ? undefined : parseInt(value) })} onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioSampleRate: value === 'original' ? undefined : parseInt(value) })}
@@ -580,7 +580,7 @@ export function FileConverter() {
{/* Channels */} {/* Channels */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground block">Channels</label> <Label className="text-xs">Channels</Label>
<Select <Select
value={conversionOptions.audioChannels?.toString() || 'original'} value={conversionOptions.audioChannels?.toString() || 'original'}
onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioChannels: value === 'original' ? undefined : parseInt(value) })} onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioChannels: value === 'original' ? undefined : parseInt(value) })}
@@ -604,7 +604,7 @@ export function FileConverter() {
{/* Quality */} {/* Quality */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Quality</label> <Label className="text-xs">Quality</Label>
<span className="text-xs text-muted-foreground">{conversionOptions.imageQuality || 85}%</span> <span className="text-xs text-muted-foreground">{conversionOptions.imageQuality || 85}%</span>
</div> </div>
<Slider <Slider
@@ -619,7 +619,7 @@ export function FileConverter() {
{/* Width */} {/* Width */}
<div> <div>
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label> <Label className="text-xs mb-1.5">Width (px)</Label>
<Input <Input
type="number" type="number"
value={conversionOptions.imageWidth || ''} value={conversionOptions.imageWidth || ''}
@@ -632,7 +632,7 @@ export function FileConverter() {
{/* Height */} {/* Height */}
<div> <div>
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label> <Label className="text-xs mb-1.5">Height (px)</Label>
<Input <Input
type="number" type="number"
value={conversionOptions.imageHeight || ''} value={conversionOptions.imageHeight || ''}
@@ -652,13 +652,12 @@ export function FileConverter() {
onClick={handleConvert} onClick={handleConvert}
disabled={isConvertDisabled} disabled={isConvertDisabled}
className="w-full" className="w-full"
size="lg"
> >
{isConverting {isConverting
? 'Converting...' ? 'Converting...'
: `Convert ${selectedFiles.length} File${selectedFiles.length > 1 ? 's' : ''}`} : `Convert ${selectedFiles.length} File${selectedFiles.length > 1 ? 's' : ''}`}
</Button> </Button>
<Button onClick={handleReset} variant="outline" size="lg" className="w-full"> <Button onClick={handleReset} variant="outline" className="w-full">
Reset Reset
</Button> </Button>
</CardContent> </CardContent>
@@ -670,13 +669,11 @@ export function FileConverter() {
<div className="space-y-6"> <div className="space-y-6">
{/* Download All Button */} {/* Download All Button */}
{completedCount > 0 && ( {completedCount > 0 && (
<Card className="glass"> <Card>
<CardContent className="p-6"> <CardContent>
<Button <Button
onClick={handleDownloadAll} onClick={handleDownloadAll}
className="w-full" className="w-full"
size="lg"
variant="default"
> >
Download All ({completedCount} file{completedCount > 1 ? 's' : ''}) Download All ({completedCount} file{completedCount > 1 ? 's' : ''})
{completedCount > 1 && ' as ZIP'} {completedCount > 1 && ' as ZIP'}

View File

@@ -246,13 +246,13 @@ export function FileUpload({
const metadata = fileMetadata[index]; const metadata = fileMetadata[index];
return ( return (
<div key={`${file.name}-${index}`} className="border border-border rounded-xl p-4 bg-card/50 backdrop-blur-sm"> <div key={`${file.name}-${index}`} className="border border-border rounded-xl p-4 bg-card/50 backdrop-blur-sm">
<div className="flex items-start gap-4"> <div className="flex items-start gap-3">
<div className="p-2 bg-primary/10 rounded-lg shrink-0"> <div className="p-2 bg-primary/10 rounded-lg shrink-0">
{getCategoryIcon()} {getCategoryIcon()}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p className="text-sm font-semibold text-foreground truncate" title={file.name}> <p className="text-sm font-medium text-foreground truncate" title={file.name}>
{file.name} {file.name}
</p> </p>
<Button <Button
@@ -267,22 +267,22 @@ export function FileUpload({
</Button> </Button>
</div> </div>
{metadata && ( {metadata && (
<div className="mt-2 flex flex-wrap gap-4 text-[10px] text-muted-foreground uppercase tracking-wider font-bold"> <div className="mt-1.5 flex flex-wrap gap-3 text-[10px] text-muted-foreground">
{/* File Size */} {/* File Size */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<HardDrive className="h-3 w-3" /> <HardDrive className="h-3 w-3" />
<span>{metadata.size}</span> <span>{metadata.size}</span>
</div> </div>
{/* Type */} {/* Type */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<File className="h-3 w-3" /> <File className="h-3 w-3" />
<span>{metadata.type}</span> <span>{metadata.type}</span>
</div> </div>
{/* Duration (for video/audio) */} {/* Duration (for video/audio) */}
{metadata.duration && ( {metadata.duration && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
<span>{metadata.duration}</span> <span>{metadata.duration}</span>
</div> </div>
@@ -290,7 +290,7 @@ export function FileUpload({
{/* Dimensions */} {/* Dimensions */}
{metadata.dimensions && ( {metadata.dimensions && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
{inputFormat?.category === 'video' ? ( {inputFormat?.category === 'video' ? (
<Film className="h-3 w-3" /> <Film className="h-3 w-3" />
) : ( ) : (
@@ -312,10 +312,9 @@ export function FileUpload({
variant="outline" variant="outline"
onClick={handleClick} onClick={handleClick}
disabled={disabled} disabled={disabled}
className="w-full rounded-xl" className="w-full"
size="lg"
> >
<Upload className="h-4 w-4 mr-2" /> <Upload className="h-3.5 w-3.5 mr-1.5" />
Add More Files Add More Files
</Button> </Button>
</div> </div>
@@ -327,23 +326,23 @@ export function FileUpload({
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
className={cn( className={cn(
'border-2 border-dashed rounded-xl p-10 text-center cursor-pointer transition-all duration-300', 'border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200',
'hover:border-primary/50 hover:bg-primary/5', 'hover:border-primary/40 hover:bg-primary/5',
{ {
'border-primary bg-primary/10 scale-[0.98]': isDragging, 'border-primary bg-primary/10 scale-[0.98]': isDragging,
'border-border bg-muted/30': !isDragging, 'border-border/50': !isDragging,
'opacity-50 cursor-not-allowed': disabled, 'opacity-50 cursor-not-allowed': disabled,
} }
)} )}
> >
<div className="bg-primary/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="bg-primary/10 w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3">
<Upload className="h-8 w-8 text-primary" /> <Upload className="h-6 w-6 text-primary" />
</div> </div>
<p className="text-sm font-semibold text-foreground mb-1"> <p className="text-sm font-medium text-foreground mb-0.5">
Drop your files here or click to browse Drop files here or click to browse
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
Maximum file size: {maxSizeMB}MB per file Max {maxSizeMB}MB per file
</p> </p>
</div> </div>
)} )}

View File

@@ -5,6 +5,7 @@ import Fuse from 'fuse.js';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import type { ConversionFormat } from '@/types/media'; import type { ConversionFormat } from '@/types/media';
@@ -62,7 +63,7 @@ export function FormatSelector({
return ( return (
<div className="w-full"> <div className="w-full">
<label className="text-sm font-medium text-foreground mb-2 block">{label}</label> <Label className="mb-2">{label}</Label>
{/* Search input */} {/* Search input */}
<div className="relative mb-3"> <div className="relative mb-3">

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { ArrowLeftRight, BarChart3 } from 'lucide-react'; import { 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 { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Select, Select,
@@ -90,16 +91,15 @@ export default function MainConverter() {
}, [selectedUnit]); }, [selectedUnit]);
return ( return (
<div className="w-full space-y-8"> <div className="w-full space-y-6">
{/* Quick Access Row */} {/* Quick Access Row */}
<Card> <Card>
<CardContent className="flex flex-col md:flex-row md:items-center gap-4 justify-between"> <CardContent className="flex flex-col md:flex-row md:items-center gap-3 justify-between">
<div className="flex-1"> <div className="flex-1">
<SearchUnits onSelectUnit={handleSearchSelect} /> <SearchUnits onSelectUnit={handleSearchSelect} />
</div> </div>
<div className="w-full md:w-56 shrink-0">
<div className="w-full md:w-64 shrink-0">
<Select <Select
value={selectedMeasure} value={selectedMeasure}
onValueChange={(value) => setSelectedMeasure(value as Measure)} onValueChange={(value) => setSelectedMeasure(value as Measure)}
@@ -124,12 +124,10 @@ export default function MainConverter() {
<CardHeader> <CardHeader>
<CardTitle>Convert {formatMeasureName(selectedMeasure)}</CardTitle> <CardTitle>Convert {formatMeasureName(selectedMeasure)}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-4">
{/* Input row, stacks vertically on mobile, horizontal on desktop */} <div className="flex flex-col gap-3 md:flex-row md:items-end md:gap-2">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:gap-2">
{/* Value Input */}
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<label className="text-sm font-medium mb-2 block">Value</label> <Label className="text-xs mb-1.5">Value</Label>
<Input <Input
type="text" type="text"
inputMode="decimal" inputMode="decimal"
@@ -139,9 +137,8 @@ export default function MainConverter() {
className={cn("text-lg", "w-full", "max-w-full")} className={cn("text-lg", "w-full", "max-w-full")}
/> />
</div> </div>
{/* From Unit Select */} <div className="w-full md:w-36">
<div className="w-full md:w-40"> <Label className="text-xs mb-1.5">From</Label>
<label className="text-sm font-medium mb-2 block">From</label>
<Select <Select
value={selectedUnit} value={selectedUnit}
onValueChange={(value) => setSelectedUnit(value)} onValueChange={(value) => setSelectedUnit(value)}
@@ -158,19 +155,17 @@ export default function MainConverter() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* Swap Button */}
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon-xs"
onClick={handleSwapUnits} onClick={handleSwapUnits}
className="flex-shrink-0 w-full md:w-40" // Make button full width on mobile too className="shrink-0 w-full md:w-7"
title="Swap units" title="Swap units"
> >
<ArrowLeftRight className="h-4 w-4" /> <ArrowLeftRight className="h-3.5 w-3.5" />
</Button> </Button>
{/* To Unit Select */} <div className="w-full md:w-36">
<div className="w-full md:w-40"> <Label className="text-xs mb-1.5">To</Label>
<label className="text-sm font-medium mb-2 block">To</label>
<Select <Select
value={targetUnit} value={targetUnit}
onValueChange={(value) => setTargetUnit(value)} onValueChange={(value) => setTargetUnit(value)}
@@ -189,12 +184,11 @@ export default function MainConverter() {
</div> </div>
</div> </div>
{/* Quick result */}
{parseNumberInput(inputValue) !== null && ( {parseNumberInput(inputValue) !== null && (
<div className="p-4 rounded-lg bg-accent/50 border-l-4 border-primary"> <div className="p-3 rounded-lg bg-primary/5 border border-primary/15">
<div className="text-sm text-muted-foreground">Result</div> <div className="text-xs text-muted-foreground mb-0.5">Result</div>
<div className="text-3xl font-bold mt-1 text-primary"> <div className="text-2xl font-bold text-primary tabular-nums">
{formatNumber(convertUnit(parseNumberInput(inputValue)!, selectedUnit, targetUnit))} {targetUnit} {formatNumber(convertUnit(parseNumberInput(inputValue)!, selectedUnit, targetUnit))} <span className="text-base font-medium text-muted-foreground">{targetUnit}</span>
</div> </div>
</div> </div>
)} )}
@@ -208,11 +202,11 @@ export default function MainConverter() {
<CardTitle>All Conversions</CardTitle> <CardTitle>All Conversions</CardTitle>
<Button <Button
variant="outline" variant="outline"
size="sm" size="xs"
onClick={() => setShowVisualComparison(!showVisualComparison)} onClick={() => setShowVisualComparison(!showVisualComparison)}
> >
<BarChart3 className="h-4 w-4 mr-2" /> <BarChart3 className="h-3 w-3 mr-1" />
{showVisualComparison ? 'Grid View' : 'Chart View'} {showVisualComparison ? 'Grid' : 'Chart'}
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
@@ -223,25 +217,17 @@ export default function MainConverter() {
onValueChange={handleValueChange} onValueChange={handleValueChange}
/> />
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{conversions.map((conversion) => { {conversions.map((conversion) => (
return (
<div <div
key={conversion.unit} key={conversion.unit}
className="group relative p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors border-l-4 border-l-primary/30" className="p-3 rounded-lg border border-border/50 hover:border-primary/30 transition-colors"
> >
<div className="text-sm text-muted-foreground mb-1"> <div className="text-xs text-muted-foreground">{conversion.unitInfo.plural}</div>
{conversion.unitInfo.plural} <div className="text-lg font-bold tabular-nums mt-0.5">{formatNumber(conversion.value)}</div>
</div> <div className="text-xs text-muted-foreground">{conversion.unit}</div>
<div className="text-2xl font-bold">
{formatNumber(conversion.value)}
</div>
<div className="text-sm text-muted-foreground mt-1">
{conversion.unit}
</div>
</div> </div>
); ))}
})}
</div> </div>
)} )}
</CardContent> </CardContent>