feat: refactor theme, add tailwind-scrollbar, and improve UI components

- Removed manual theme switching logic and ThemeProvider
- Installed and configured tailwind-scrollbar plugin
- Updated FileConverter and ConversionOptions to use shadcn Input
- Refactored FontSelector to use shadcn Tabs
- Simplified global styles and adjusted glassmorphic effects
This commit is contained in:
2026-02-26 22:22:32 +01:00
parent a3ef948600
commit 782923f2e0
20 changed files with 178 additions and 248 deletions

View File

@@ -5,7 +5,7 @@ export default function AnimatedBackground() {
<div className="fixed inset-0 -z-10 overflow-hidden bg-background transition-colors duration-500">
{/* Animated gradient background */}
<div
className="absolute inset-0 opacity-[0.08] dark:opacity-50"
className="absolute inset-0 opacity-50"
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #667eea 100%)',
backgroundSize: '400% 400%',
@@ -15,7 +15,7 @@ export default function AnimatedBackground() {
{/* Signature Grid pattern overlay - Original landing page specification */}
<div
className="absolute inset-0 opacity-[0.05] dark:opacity-10"
className="absolute inset-0 opacity-10"
style={{
backgroundImage: `
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
@@ -26,9 +26,9 @@ export default function AnimatedBackground() {
/>
{/* Floating orbs */}
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-purple-500 rounded-full mix-blend-multiply dark:mix-blend-normal filter blur-3xl opacity-[0.03] dark:opacity-20 animate-float" />
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-cyan-500 rounded-full mix-blend-multiply dark:mix-blend-normal filter blur-3xl opacity-[0.03] dark:opacity-20 animate-float" style={{ animationDelay: '2s' }} />
<div className="absolute bottom-1/4 left-1/3 w-96 h-96 bg-pink-500 rounded-full mix-blend-multiply dark:mix-blend-normal filter blur-3xl opacity-[0.03] dark:opacity-20 animate-float" style={{ animationDelay: '4s' }} />
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-purple-500 rounded-full mix-blend-normal filter blur-3xl opacity-20 animate-float" />
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-cyan-500 rounded-full mix-blend-normal filter blur-3xl opacity-20 animate-float" style={{ animationDelay: '2s' }} />
<div className="absolute bottom-1/4 left-1/3 w-96 h-96 bg-pink-500 rounded-full mix-blend-normal filter blur-3xl opacity-20 animate-float" style={{ animationDelay: '4s' }} />
</div>
);
}

View File

@@ -31,12 +31,12 @@ export default function ToolCard({ title, description, icon, url, gradient, acce
<div className="glass relative overflow-hidden rounded-2xl p-8 h-full transition-all duration-300 group-hover:shadow-2xl group-hover:bg-card/80">
{/* Gradient overlay on hover */}
<div
className={`absolute inset-0 opacity-0 group-hover:opacity-10 dark:group-hover:opacity-15 transition-opacity duration-300 ${gradient}`}
className={`absolute inset-0 opacity-0 group-hover:opacity-15 transition-opacity duration-300 ${gradient}`}
/>
{/* Glow effect */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl -z-10">
<div className={`w-full h-full ${gradient} opacity-20 dark:opacity-30`} />
<div className={`w-full h-full ${gradient} opacity-30`} />
</div>
{/* Icon */}

View File

@@ -14,6 +14,7 @@ import {
import { Search, X, Heart, Clock, List, Shuffle } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { ASCIIFont } from '@/types/ascii';
import { getFavorites, getRecentFonts, toggleFavorite, isFavorite } from '@/lib/storage/favorites';
@@ -108,39 +109,26 @@ export function FontSelector({
)}
</CardHeader>
<CardContent className="flex flex-col flex-1 min-h-0 pt-0">
{/* Filter Tabs */}
<div className="flex gap-1 mb-4 p-1 bg-muted rounded-lg shrink-0">
<button
onClick={() => setFilter('all')}
className={cn(
'flex-1 flex items-center justify-center px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
filter === 'all' ? 'bg-background shadow-sm' : 'hover:bg-background/50'
)}
>
<List className="h-3 w-3 mr-1.5" />
All
</button>
<button
onClick={() => setFilter('favorites')}
className={cn(
'flex-1 flex items-center justify-center px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
filter === 'favorites' ? 'bg-background shadow-sm' : 'hover:bg-background/50'
)}
>
<Heart className="h-3 w-3 mr-1.5" />
Favorites
</button>
<button
onClick={() => setFilter('recent')}
className={cn(
'flex-1 flex items-center justify-center px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
filter === 'recent' ? 'bg-background shadow-sm' : 'hover:bg-background/50'
)}
>
<Clock className="h-3 w-3 mr-1.5" />
Recent
</button>
</div>
<Tabs
value={filter}
onValueChange={(v) => setFilter(v as FilterType)}
className="mb-4 shrink-0"
>
<TabsList className="w-full">
<TabsTrigger value="all" className="flex-1">
<List className="h-3.5 w-3.5" />
All
</TabsTrigger>
<TabsTrigger value="favorites" className="flex-1">
<Heart className="h-3.5 w-3.5" />
Favorites
</TabsTrigger>
<TabsTrigger value="recent" className="flex-1">
<Clock className="h-3.5 w-3.5" />
Recent
</TabsTrigger>
</TabsList>
</Tabs>
{/* Search Input */}
<div className="relative mb-4 shrink-0">
@@ -165,7 +153,7 @@ export function FontSelector({
</div>
{/* Font List */}
<div className="flex-1 overflow-y-auto space-y-1 pr-2">
<div className="flex-1 overflow-y-auto space-y-1 pr-2 scrollbar">
{filteredFonts.length === 0 ? (
<Empty>
<EmptyHeader>

View File

@@ -5,7 +5,6 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Menu, Search, Bell, ChevronRight, Moon, Sun, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTheme } from '@/components/providers/ThemeProvider';
import { cn } from '@/lib/utils/cn';
import { useSidebar } from './SidebarProvider';
import Logo from '@/components/Logo';
@@ -23,7 +22,7 @@ export function AppHeader() {
<nav className="flex items-center text-sm font-medium">
<Link href="/" className="flex items-center gap-2">
<Logo size={20} className="lg:hidden" />
<span className="font-medium transition-colors text-foreground">
<span className="font-medium transition-colors text-muted-foreground">
Kit
</span>
</Link>
@@ -37,7 +36,7 @@ export function AppHeader() {
<Link
href={href}
className={cn(
"capitalize transition-colors text-foreground",
"capitalize transition-colors text-muted-foreground",
isLast ? "font-semibold" : "font-medium"
)}
>
@@ -50,7 +49,6 @@ export function AppHeader() {
</div>
<div className="flex items-center gap-2">
<ThemeToggleComponent />
<Button
variant="ghost"
size="icon"
@@ -62,24 +60,4 @@ export function AppHeader() {
</div>
</header>
);
}
function ThemeToggleComponent() {
const { resolvedTheme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className="text-muted-foreground hover:text-foreground hover:bg-accent/50"
title={`Switch to ${resolvedTheme === 'dark' ? 'light' : 'dark'} mode`}
>
{resolvedTheme === 'dark' ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</Button>
);
}
}

View File

@@ -13,12 +13,12 @@ interface AppShellProps {
export function AppShell({ children }: AppShellProps) {
return (
<SidebarProvider>
<div className="flex h-screen overflow-hidden bg-background text-foreground relative">
<div className="flex h-screen overflow-hidden bg-transparent text-foreground relative">
<AnimatedBackground />
<AppSidebar />
<div className="flex-1 flex flex-col min-w-0 relative z-10">
<AppHeader />
<main className="flex-1 overflow-y-auto">
<main className="flex-1 overflow-y-auto scrollbar">
{children}
</main>
</div>

View File

@@ -5,11 +5,6 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
ChevronRight,
MousePointer2,
Palette,
Eye,
Languages,
Layers,
ChevronLeft,
X
} from 'lucide-react';
@@ -73,13 +68,13 @@ export function AppSidebar() {
{/* Mobile Overlay Backdrop */}
{isOpen && (
<div
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40 lg:hidden"
className="fixed inset-0 bg-transparent backdrop-blur-sm z-40 lg:hidden"
onClick={close}
/>
)}
<aside className={cn(
"fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-background/40 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/10 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",
isCollapsed ? "lg:w-20" : "w-64"
)}>
@@ -106,7 +101,7 @@ export function AppSidebar() {
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto px-4 py-2 space-y-8 mt-4 scrollbar-hide">
<nav className="flex-1 overflow-y-auto px-4 py-2 space-y-8 mt-4 overflow-x-hidden">
{navigation.map((group) => (
<div key={group.label} className="space-y-2">
{!isCollapsed && (

View File

@@ -1,10 +1,8 @@
'use client';
import * as React from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
@@ -237,12 +235,11 @@ export function ConversionOptionsPanel({
{/* Width */}
<div>
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label>
<input
<Input
type="number"
value={options.imageWidth || ''}
onChange={(e) => handleOptionChange('imageWidth', e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="Original"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
disabled={disabled}
/>
<p className="text-xs text-muted-foreground mt-1">Leave empty to keep original</p>
@@ -251,12 +248,11 @@ export function ConversionOptionsPanel({
{/* Height */}
<div>
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label>
<input
<Input
type="number"
value={options.imageHeight || ''}
onChange={(e) => handleOptionChange('imageHeight', e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="Original"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
disabled={disabled}
/>
<p className="text-xs text-muted-foreground mt-1">Leave empty to maintain aspect ratio</p>

View File

@@ -2,6 +2,7 @@
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider';
import {
@@ -619,12 +620,11 @@ export function FileConverter() {
{/* Width */}
<div>
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label>
<input
<Input
type="number"
value={conversionOptions.imageWidth || ''}
onChange={(e) => setConversionOptions({ ...conversionOptions, imageWidth: e.target.value ? parseInt(e.target.value) : undefined })}
placeholder="Original"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
disabled={isConverting}
/>
<p className="text-xs text-muted-foreground mt-1">Leave empty to keep original</p>
@@ -633,12 +633,11 @@ export function FileConverter() {
{/* Height */}
<div>
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label>
<input
<Input
type="number"
value={conversionOptions.imageHeight || ''}
onChange={(e) => setConversionOptions({ ...conversionOptions, imageHeight: e.target.value ? parseInt(e.target.value) : undefined })}
placeholder="Original"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
disabled={isConverting}
/>
<p className="text-xs text-muted-foreground mt-1">Leave empty to maintain aspect ratio</p>

View File

@@ -3,7 +3,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'sonner';
import { useState } from 'react';
import { ThemeProvider } from './ThemeProvider';
import { TooltipProvider } from '@/components/ui/tooltip';
import { SWRegistration } from './SWRegistration';
@@ -21,14 +20,12 @@ export function Providers({ children }: { children: React.ReactNode }) {
);
return (
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<SWRegistration />
{children}
</TooltipProvider>
<Toaster position="top-right" richColors />
</QueryClientProvider>
</ThemeProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<SWRegistration />
{children}
</TooltipProvider>
<Toaster position="top-right" richColors />
</QueryClientProvider>
);
}

View File

@@ -1,85 +0,0 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('dark');
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('dark');
const [mounted, setMounted] = useState(false);
// Load theme from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem('theme') as Theme | null;
if (stored) {
setTheme(stored);
}
setMounted(true);
}, []);
// Apply theme to document element and save to localStorage
useEffect(() => {
if (!mounted) return;
const root = window.document.documentElement;
// Remove previous theme classes
root.classList.remove('light', 'dark');
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
root.classList.add(systemTheme);
setResolvedTheme(systemTheme);
} else {
root.classList.add(theme);
setResolvedTheme(theme);
}
// Save to localStorage
localStorage.setItem('theme', theme);
}, [theme, mounted]);
// Listen for system theme changes
useEffect(() => {
if (!mounted) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (theme === 'system') {
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
setResolvedTheme(systemTheme);
window.document.documentElement.classList.remove('light', 'dark');
window.document.documentElement.classList.add(systemTheme);
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme, mounted]);
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -134,7 +134,7 @@ export default function SearchUnits({ onSelectUnit, className }: SearchUnitsProp
{/* Results dropdown */}
{isOpen && results.length > 0 && (
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-lg shadow-lg max-h-80 overflow-y-auto">
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-lg shadow-lg max-h-80 overflow-y-auto scrollbar">
{results.map((result, index) => (
<button
key={`${result.measure}-${result.unitInfo.abbr}`}