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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`}
|
||||
|
||||
Reference in New Issue
Block a user