diff --git a/.claude/launch.json b/.claude/launch.json index 37b8065..744ef7a 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -9,8 +9,8 @@ }, { "name": "Frontend (vite)", - "runtimeExecutable": "/Users/dtoro/Projects/micronomicon/frontend/node_modules/.bin/vite", - "runtimeArgs": ["/Users/dtoro/Projects/micronomicon/frontend"], + "runtimeExecutable": "/opt/homebrew/bin/node", + "runtimeArgs": ["/Users/dtoro/Projects/micronomicon/frontend/node_modules/.bin/vite", "/Users/dtoro/Projects/micronomicon/frontend"], "port": 5173 } ] diff --git a/frontend/src/assets/frame.azure.svg b/frontend/src/assets/frame.azure.svg new file mode 100644 index 0000000..eca50e7 --- /dev/null +++ b/frontend/src/assets/frame.azure.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/src/components/shared/AppShell.tsx b/frontend/src/components/shared/AppShell.tsx index 10474e1..c0b9414 100644 --- a/frontend/src/components/shared/AppShell.tsx +++ b/frontend/src/components/shared/AppShell.tsx @@ -2,22 +2,30 @@ import { useEffect, useState, type ReactNode } from "react"; import { useNavigate } from "react-router-dom"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Toaster } from "@/components/ui/sonner"; -import frameSvg from "@/assets/frame.themed.svg"; +import frameTerraSvg from "@/assets/frame.themed.svg"; +import frameAzureSvg from "@/assets/frame.azure.svg"; const TITLE = ` - ▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄████████ ▄██████▄ ███▄▄▄▄ ▄██████▄ ▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄██████▄ ███▄▄▄▄ - ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███▀▀▀██▄ - ███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███ - ███ ███ ███ ███▌ ███ ▄███▄▄▄▄██▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███ - ███ ███ ███ ███▌ ███ ▀▀███▀▀▀▀▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███ - ███ ███ ███ ███ ███ █▄ ▀███████████ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▄ ███ ███ ███ ███ - ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ - ▀█ ███ █▀ █▀ ████████▀ ███ ███ ▀██████▀ ▀█ █▀ ▀██████▀ ▀█ ███ █▀ █▀ ████████▀ ▀██████▀ ▀█ █▀ - ███ ███ - + ▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄████████ ▄██████▄ ███▄▄▄▄ ▄██████▄ ▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄██████▄ ███▄▄▄▄ + ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███▀▀▀██▄ + ███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███ + ███ ███ ███ ███▌ ███ ▄███▄▄▄▄██▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███ + ███ ███ ███ ███▌ ███ ▀▀███▀▀▀▀▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ █▄ ▀███████████ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▄ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ + ▀█ ███ █▀ █▀ ████████▀ ███ ███ ▀██████▀ ▀█ █▀ ▀██████▀ ▀█ ███ █▀ █▀ ████████▀ ▀██████▀ ▀█ █▀ + ███ ███ + `; +type Theme = "terra" | "azure"; + +function getStoredTheme(): Theme { + try { return (localStorage.getItem("micronomicon-theme") as Theme) || "terra"; } + catch { return "terra"; } +} + function toRoman(n: number): string { const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]; const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]; @@ -28,6 +36,30 @@ function toRoman(n: number): string { return r; } +function RomanDrum({ value }: { value: string }) { + return ( + + + {value} + + + ); +} + function RomanClock() { const [time, setTime] = useState(new Date()); useEffect(() => { @@ -38,12 +70,35 @@ function RomanClock() { const m = time.getMinutes(); const s = time.getSeconds(); return ( - {toRoman(h || 12)}:{toRoman(m || 1).padStart(2, " ")}:{toRoman(s || 1).padStart(3, " ")} + + + : + + : + + ); } export default function AppShell({ children }: { children: ReactNode }) { const navigate = useNavigate(); + const [theme, setTheme] = useState(getStoredTheme); + + useEffect(() => { + const root = document.documentElement; + // Remove all theme classes, then apply + root.classList.remove("dark", "theme-azure"); + if (theme === "terra") { + root.classList.add("dark"); + } else { + root.classList.add("theme-azure"); + } + localStorage.setItem("micronomicon-theme", theme); + }, [theme]); + + const frameSvg = theme === "azure" ? frameAzureSvg : frameTerraSvg; + + const toggleTheme = () => setTheme(t => t === "terra" ? "azure" : "terra"); return ( @@ -52,6 +107,7 @@ export default function AppShell({ children }: { children: ReactNode }) { {/* Frame SVG — background */} diff --git a/frontend/src/index.css b/frontend/src/index.css index 7df959e..f8dbc88 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,6 +5,11 @@ @custom-variant dark (&:is(.dark *)); +@keyframes romanSlideIn { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + @theme inline { --font-heading: var(--font-mono); --font-sans: 'JetBrains Mono Variable', 'Courier New', monospace; @@ -122,6 +127,41 @@ --sidebar-ring: oklch(0.7 0.13 55); } +/* ── Azure Theme — blue and white, clean ── */ +.theme-azure { + --background: oklch(0.14 0.02 240); + --foreground: oklch(0.82 0.04 220); + --card: oklch(0.17 0.025 240); + --card-foreground: oklch(0.82 0.04 220); + --popover: oklch(0.17 0.025 240); + --popover-foreground: oklch(0.82 0.04 220); + --primary: oklch(0.72 0.12 230); + --primary-foreground: oklch(0.14 0.02 240); + --secondary: oklch(0.22 0.03 240); + --secondary-foreground: oklch(0.78 0.04 220); + --muted: oklch(0.22 0.025 240); + --muted-foreground: oklch(0.55 0.04 230); + --accent: oklch(0.24 0.035 240); + --accent-foreground: oklch(0.82 0.04 220); + --destructive: oklch(0.55 0.18 25); + --border: oklch(0.38 0.06 230); + --input: oklch(0.26 0.03 240); + --ring: oklch(0.72 0.12 230); + --chart-1: oklch(0.72 0.12 230); + --chart-2: oklch(0.65 0.1 200); + --chart-3: oklch(0.55 0.08 260); + --chart-4: oklch(0.6 0.06 180); + --chart-5: oklch(0.45 0.04 230); + --sidebar: oklch(0.16 0.02 240); + --sidebar-foreground: oklch(0.82 0.04 220); + --sidebar-primary: oklch(0.72 0.12 230); + --sidebar-primary-foreground: oklch(0.14 0.02 240); + --sidebar-accent: oklch(0.24 0.035 240); + --sidebar-accent-foreground: oklch(0.82 0.04 220); + --sidebar-border: oklch(0.38 0.06 230); + --sidebar-ring: oklch(0.72 0.12 230); +} + @layer base { * { @apply border-border outline-ring/50; @@ -137,10 +177,10 @@ @apply font-mono; } - /* Terracotta scrollbars */ + /* Theme-aware scrollbars */ * { scrollbar-width: thin; - scrollbar-color: oklch(0.35 0.05 45) transparent; + scrollbar-color: var(--border) transparent; } *::-webkit-scrollbar { @@ -153,12 +193,12 @@ } *::-webkit-scrollbar-thumb { - background: oklch(0.35 0.05 45); + background: var(--border); border-radius: 0; } *::-webkit-scrollbar-thumb:hover { - background: oklch(0.5 0.08 55); + background: var(--primary); } *::-webkit-scrollbar-corner {