featL: theme
This commit is contained in:
@@ -9,8 +9,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Frontend (vite)",
|
"name": "Frontend (vite)",
|
||||||
"runtimeExecutable": "/Users/dtoro/Projects/micronomicon/frontend/node_modules/.bin/vite",
|
"runtimeExecutable": "/opt/homebrew/bin/node",
|
||||||
"runtimeArgs": ["/Users/dtoro/Projects/micronomicon/frontend"],
|
"runtimeArgs": ["/Users/dtoro/Projects/micronomicon/frontend/node_modules/.bin/vite", "/Users/dtoro/Projects/micronomicon/frontend"],
|
||||||
"port": 5173
|
"port": 5173
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
38
frontend/src/assets/frame.azure.svg
Normal file
38
frontend/src/assets/frame.azure.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 496 KiB |
@@ -2,22 +2,30 @@ import { useEffect, useState, type ReactNode } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
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 = `
|
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 {
|
function toRoman(n: number): string {
|
||||||
const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
|
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"];
|
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;
|
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() {
|
function RomanClock() {
|
||||||
const [time, setTime] = useState(new Date());
|
const [time, setTime] = useState(new Date());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,12 +70,35 @@ function RomanClock() {
|
|||||||
const m = time.getMinutes();
|
const m = time.getMinutes();
|
||||||
const s = time.getSeconds();
|
const s = time.getSeconds();
|
||||||
return (
|
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 }) {
|
export default function AppShell({ children }: { children: ReactNode }) {
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<TooltipProvider>
|
<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">
|
<div className="relative max-w-5xl min-w-5xl mx-auto min-h-full">
|
||||||
{/* Frame SVG — background */}
|
{/* Frame SVG — background */}
|
||||||
<img
|
<img
|
||||||
|
key={theme}
|
||||||
src={frameSvg}
|
src={frameSvg}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden
|
aria-hidden
|
||||||
@@ -85,6 +141,14 @@ export default function AppShell({ children }: { children: ReactNode }) {
|
|||||||
</main>
|
</main>
|
||||||
<footer className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground py-4 shrink-0 tracking-widest uppercase">
|
<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>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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@keyframes romanSlideIn {
|
||||||
|
from { transform: translateY(100%); }
|
||||||
|
to { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-heading: var(--font-mono);
|
--font-heading: var(--font-mono);
|
||||||
--font-sans: 'JetBrains Mono Variable', 'Courier New', monospace;
|
--font-sans: 'JetBrains Mono Variable', 'Courier New', monospace;
|
||||||
@@ -122,6 +127,41 @@
|
|||||||
--sidebar-ring: oklch(0.7 0.13 55);
|
--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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
@@ -137,10 +177,10 @@
|
|||||||
@apply font-mono;
|
@apply font-mono;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Terracotta scrollbars */
|
/* Theme-aware scrollbars */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: oklch(0.35 0.05 45) transparent;
|
scrollbar-color: var(--border) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
@@ -153,12 +193,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background: oklch(0.35 0.05 45);
|
background: var(--border);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb:hover {
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
background: oklch(0.5 0.08 55);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-corner {
|
*::-webkit-scrollbar-corner {
|
||||||
|
|||||||
Reference in New Issue
Block a user