featL: theme
This commit is contained in:
@@ -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 (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
overflow: "hidden",
|
||||
height: "1em",
|
||||
lineHeight: "1em",
|
||||
verticalAlign: "bottom",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
key={value}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
animation: "romanSlideIn 0.25s cubic-bezier(0.22, 1, 0.36, 1)",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function RomanClock() {
|
||||
const [time, setTime] = useState(new Date());
|
||||
useEffect(() => {
|
||||
@@ -38,12 +70,35 @@ function RomanClock() {
|
||||
const m = time.getMinutes();
|
||||
const s = time.getSeconds();
|
||||
return (
|
||||
<span>{toRoman(h || 12)}:{toRoman(m || 1).padStart(2, " ")}:{toRoman(s || 1).padStart(3, " ")}</span>
|
||||
<span style={{ display: "inline-flex", alignItems: "baseline", gap: 0 }}>
|
||||
<RomanDrum value={toRoman(h || 12)} />
|
||||
<span>:</span>
|
||||
<RomanDrum value={toRoman(m || 1)} />
|
||||
<span>:</span>
|
||||
<RomanDrum value={toRoman(s || 1)} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppShell({ children }: { children: ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
const [theme, setTheme] = useState<Theme>(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 (
|
||||
<TooltipProvider>
|
||||
@@ -52,6 +107,7 @@ export default function AppShell({ children }: { children: ReactNode }) {
|
||||
<div className="relative max-w-5xl min-w-5xl mx-auto min-h-full">
|
||||
{/* Frame SVG — background */}
|
||||
<img
|
||||
key={theme}
|
||||
src={frameSvg}
|
||||
alt=""
|
||||
aria-hidden
|
||||
@@ -85,6 +141,14 @@ export default function AppShell({ children }: { children: ReactNode }) {
|
||||
</main>
|
||||
<footer className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground py-4 shrink-0 tracking-widest uppercase">
|
||||
<span>Becoming with hubris · MMXXVI</span>
|
||||
<span className="text-muted-foreground/50">│</span>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="hover:text-foreground transition-colors cursor-pointer"
|
||||
title={`Switch to ${theme === "terra" ? "Azure" : "Terracotta"} theme`}
|
||||
>
|
||||
{theme === "terra" ? "◐ AZURE" : "◑ TERRA"}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
<Toaster />
|
||||
|
||||
Reference in New Issue
Block a user