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

11
.mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
]
}
}
}

77
CLAUDE.md Normal file
View File

@@ -0,0 +1,77 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
Kit UI is a static-export toolkit (Next.js 16, React 19, TypeScript strict) with five browser-based tools: Color, Units, ASCII, Media, and Favicon. All heavy processing runs client-side via WebAssembly. Deployed at kit.pivoine.art.
## Commands
```bash
pnpm dev # Dev server with Turbopack (localhost:3000)
pnpm build # Static export to /out
pnpm lint # ESLint (next/core-web-vitals)
pnpm postinstall # Copies WASM binaries to public/wasm/ (runs automatically on install)
```
There are no test suites. Use `pnpm build` to verify changes compile correctly.
## Architecture
### Routing (App Router)
```
app/
├── page.tsx # Landing page (dark mode forced)
├── (app)/layout.tsx # Wraps all tools with Providers + AppShell
├── (app)/color/ # Color manipulation (@valknarthing/pastel-wasm)
├── (app)/units/ # Unit converter (187+ units, 23 categories)
├── (app)/ascii/ # ASCII art (373 figlet fonts)
├── (app)/media/ # File converter (FFmpeg + ImageMagick WASM)
└── (app)/favicon/ # Favicon/PWA asset generator
```
### Code Organization
Each tool follows a mirrored structure across three directories:
- **`app/(app)/{tool}/page.tsx`** — Route entry point
- **`components/{tool}/`** — UI components for the tool
- **`lib/{tool}/`** — Business logic, WASM wrappers, stores, and utilities
- **`types/`** — Shared TypeScript definitions
Shared UI primitives live in `components/ui/` (shadcn/ui, customized with glassmorphic styling). Layout shell in `components/layout/`.
### State Management (three layers)
1. **URL params** — Primary for shareable state (`useSearchParams` / `useRouter().push`)
2. **React Query** — Async WASM computations with caching
3. **Zustand** — Per-tool client stores in `lib/{tool}/`
4. **localStorage** — Persistence for theme, favorites, history
### WASM Integration
WASM modules (FFmpeg, ImageMagick, pastel-wasm) are lazy-loaded on first use. Binaries live in `public/wasm/` and are copied there by the postinstall script — don't move them manually. WASM logic is browser-only; do not attempt to run it in Node.
## Conventions
- **`'use client'`** only where needed (WASM, browser APIs, interactive state). Default to RSC.
- **Styling**: Tailwind CSS 4 with CSS-first config in `app/globals.css`. Use `cn()` from `lib/utils/cn.ts` for conditional classes. Use `@utility glass` for glassmorphic effects and gradient utilities (`gradient-purple-blue`, etc.).
- **Icons**: Lucide React exclusively.
- **Imports**: Use `@/` path alias (resolves to project root).
- **Components**: shadcn/ui from `components/ui/` as building blocks.
- **Logic/UI separation**: Business logic in `lib/`, UI in `components/`. Keep them separate.
## Deployment
Static export (`output: 'export'` in next.config.ts) served by Nginx via Docker. No Node.js runtime in production.
```bash
docker build -t kit-ui .
docker run -p 80:80 kit-ui
```
## PWA
Service worker at `public/sw.js` pre-caches core assets and WASM binaries. Manifest generated from `app/manifest.ts` (force-static). Cache version: `kit-ui-v1`.

View File

@@ -1,3 +1,4 @@
import AnimatedBackground from '@/components/AnimatedBackground';
import { AppShell } from '@/components/layout/AppShell';
import { Providers } from '@/components/providers/Providers';

View File

@@ -1,4 +1,5 @@
@import "tailwindcss";
@plugin "tailwind-scrollbar";
@source "../components/*.{js,ts,jsx,tsx}";
@source "../components/ui/*.{js,ts,jsx,tsx}";
@@ -85,14 +86,13 @@
}
}
:root, .dark {
color-scheme: dark;
:root {
/* CORPORATE DARK THEME (The Standard) */
--background: #0a0a0f;
--foreground: #ffffff;
--card: rgba(255, 255, 255, 0.03);
--card-foreground: #ffffff;
--popover: #0f0f15;
--popover: #5D429C;
--popover-foreground: #ffffff;
--primary: #8b5cf6;
--primary-foreground: #ffffff;
@@ -110,36 +110,12 @@
--radius: 1rem;
}
.light {
color-scheme: light;
/* LIGHT ADAPTATION (Keeping the "Glass" look) */
--background: oklch(98% 0.005 255);
--foreground: oklch(20% 0.04 255);
--card: rgba(255, 255, 255, 0.4);
--card-foreground: oklch(20% 0.04 255);
--popover: oklch(100% 0 0);
--popover-foreground: oklch(20% 0.04 255);
--primary: oklch(55% 0.22 270);
--primary-foreground: oklch(100% 0 0);
--secondary: rgba(0, 0, 0, 0.02);
--secondary-foreground: oklch(20% 0.04 255);
--muted: rgba(0, 0, 0, 0.02);
--muted-foreground: oklch(45% 0.04 255);
--accent: rgba(0, 0, 0, 0.03);
--accent-foreground: oklch(15% 0.05 255);
--destructive: oklch(60% 0.2 25);
--destructive-foreground: oklch(100% 0 0);
--border: rgba(0, 0, 0, 0.2);
--input: rgba(0, 0, 0, 0.08);
--ring: rgba(139, 92, 246, 0.4);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent hover:scrollbar-thumb-primary/40;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View File

@@ -1,6 +1,5 @@
import type { Metadata } from 'next';
import './globals.css';
import { Providers } from '@/components/providers/Providers';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
@@ -37,7 +36,7 @@ export default function RootLayout({
const isProd = process.env.NODE_ENV === 'production';
return (
<html lang="en" className="dark" suppressHydrationWarning>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="apple-mobile-web-app-capable" content="yes" />
@@ -49,28 +48,6 @@ export default function RootLayout({
{isProd && umamiScript && umamiId && (
<script defer src={umamiScript} data-website-id={umamiId}></script>
)}
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme');
var isLanding = window.location.pathname === '/';
if (isLanding) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else if (theme === 'light' || (!theme && window.matchMedia('(prefers-color-scheme: light)').matches)) {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
}
} catch (e) {}
})();
`,
}}
/>
</head>
<body className="antialiased">
{children}

View File

@@ -1,6 +1,5 @@
'use client';
import * as React from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import AnimatedBackground from '@/components/AnimatedBackground';
@@ -10,11 +9,6 @@ import { Button } from '@/components/ui/button';
import { Home } from 'lucide-react';
export default function NotFound() {
React.useEffect(() => {
// Force dark mode on html element for the 404 page
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
}, []);
return (
<main className="relative min-h-screen dark text-foreground flex flex-col">

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect } from 'react';
import AnimatedBackground from '@/components/AnimatedBackground';
import Hero from '@/components/Hero';
import Stats from '@/components/Stats';
@@ -9,12 +8,6 @@ import Footer from '@/components/Footer';
import BackToTop from '@/components/BackToTop';
export default function Home() {
useEffect(() => {
// Force dark mode on html element for the landing page
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
}, []);
return (
<main className="relative min-h-screen dark text-foreground">
<AnimatedBackground />

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}`}

View File

@@ -46,6 +46,7 @@
"eslint-config-next": "^15.1.7",
"postcss": "^8.5.6",
"shadcn": "^3.8.5",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.2.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"

32
pnpm-lock.yaml generated
View File

@@ -111,6 +111,9 @@ importers:
shadcn:
specifier: ^3.8.5
version: 3.8.5(@types/node@25.3.0)(typescript@5.9.3)
tailwind-scrollbar:
specifier: ^4.0.2
version: 4.0.2(react@19.2.4)(tailwindcss@4.2.0)
tailwindcss:
specifier: ^4.2.0
version: 4.2.0
@@ -1514,6 +1517,9 @@ packages:
'@types/node@25.3.0':
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
'@types/prismjs@1.26.6':
resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -3315,6 +3321,11 @@ packages:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'}
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
react: '>=16.0.0'
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -3691,6 +3702,12 @@ packages:
tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
tailwind-scrollbar@4.0.2:
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 4.x
tailwindcss@4.2.0:
resolution: {integrity: sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==}
@@ -5391,6 +5408,8 @@ snapshots:
dependencies:
undici-types: 7.18.2
'@types/prismjs@1.26.6': {}
'@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies:
'@types/react': 19.2.14
@@ -7312,6 +7331,12 @@ snapshots:
dependencies:
parse-ms: 4.0.0
prism-react-renderer@2.4.1(react@19.2.4):
dependencies:
'@types/prismjs': 1.26.6
clsx: 2.1.1
react: 19.2.4
process-nextick-args@2.0.1: {}
prompts@2.4.2:
@@ -7860,6 +7885,13 @@ snapshots:
tailwind-merge@3.5.0: {}
tailwind-scrollbar@4.0.2(react@19.2.4)(tailwindcss@4.2.0):
dependencies:
prism-react-renderer: 2.4.1(react@19.2.4)
tailwindcss: 4.2.0
transitivePeerDependencies:
- react
tailwindcss@4.2.0: {}
tapable@2.3.0: {}