diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..bd98b4f
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "shadcn": {
+ "command": "npx",
+ "args": [
+ "shadcn@latest",
+ "mcp"
+ ]
+ }
+ }
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..e519bb8
--- /dev/null
+++ b/CLAUDE.md
@@ -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`.
diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx
index 65c0b2e..0f00bc5 100644
--- a/app/(app)/layout.tsx
+++ b/app/(app)/layout.tsx
@@ -1,3 +1,4 @@
+import AnimatedBackground from '@/components/AnimatedBackground';
import { AppShell } from '@/components/layout/AppShell';
import { Providers } from '@/components/providers/Providers';
diff --git a/app/globals.css b/app/globals.css
index 442d8b7..d1a6366 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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;
diff --git a/app/layout.tsx b/app/layout.tsx
index f70e274..91b64ee 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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 (
-
+
@@ -49,28 +48,6 @@ export default function RootLayout({
{isProd && umamiScript && umamiId && (
)}
-
{children}
diff --git a/app/not-found.tsx b/app/not-found.tsx
index 85c5c90..526602e 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -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 (
diff --git a/app/page.tsx b/app/page.tsx
index dde5ab2..51b0fe5 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -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 (
diff --git a/components/AnimatedBackground.tsx b/components/AnimatedBackground.tsx
index 4c56ca5..e7179ea 100644
--- a/components/AnimatedBackground.tsx
+++ b/components/AnimatedBackground.tsx
@@ -5,7 +5,7 @@ export default function AnimatedBackground() {
{/* Animated gradient background */}
{/* Floating orbs */}
-
-
-
+
+
+
);
}
diff --git a/components/ToolCard.tsx b/components/ToolCard.tsx
index f919e56..1fa1c68 100644
--- a/components/ToolCard.tsx
+++ b/components/ToolCard.tsx
@@ -31,12 +31,12 @@ export default function ToolCard({ title, description, icon, url, gradient, acce
{/* Gradient overlay on hover */}
{/* Glow effect */}
{/* Icon */}
diff --git a/components/ascii/FontSelector.tsx b/components/ascii/FontSelector.tsx
index 55b68df..8d0edab 100644
--- a/components/ascii/FontSelector.tsx
+++ b/components/ascii/FontSelector.tsx
@@ -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({
)}
- {/* Filter Tabs */}
-
-
-
-
-
+ setFilter(v as FilterType)}
+ className="mb-4 shrink-0"
+ >
+
+
+
+ All
+
+
+
+ Favorites
+
+
+
+ Recent
+
+
+
{/* Search Input */}
@@ -165,7 +153,7 @@ export function FontSelector({
{/* Font List */}
-
+
{filteredFonts.length === 0 ? (
diff --git a/components/layout/AppHeader.tsx b/components/layout/AppHeader.tsx
index c4a212a..692ad8a 100644
--- a/components/layout/AppHeader.tsx
+++ b/components/layout/AppHeader.tsx
@@ -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() {
-