Add theme families

This commit is contained in:
2026-04-06 15:27:40 -07:00
parent 1cd76b03df
commit e640e87d3f
12 changed files with 599 additions and 97 deletions
+44 -19
View File
@@ -1,31 +1,35 @@
import { useRef, useState, useEffect } from "react";
import { getStoredThemeId, getNextTheme, applyTheme } from "@/lib/themes/engine";
import { THEMES, FAMILIES } from "@/lib/themes";
import { getStoredThemeId, getNextFamily, getNextVariant, applyTheme } from "@/lib/themes/engine";
const FADE_DURATION = 300;
const LABELS: Record<string, string> = {
darkbox: "classic",
"darkbox-retro": "retro",
"darkbox-dim": "dim",
};
export default function ThemeSwitcher() {
const [hovering, setHovering] = useState(false);
const [currentLabel, setCurrentLabel] = useState("");
const [familyName, setFamilyName] = useState("");
const [variantLabel, setVariantLabel] = useState("");
const maskRef = useRef<HTMLDivElement>(null);
const animatingRef = useRef(false);
const committedRef = useRef("");
function syncLabels(id: string) {
const theme = THEMES[id];
if (!theme) return;
const family = FAMILIES.find((f) => f.id === theme.family);
setFamilyName(family?.name.toLowerCase() ?? theme.family);
setVariantLabel(theme.label);
}
useEffect(() => {
committedRef.current = getStoredThemeId();
setCurrentLabel(LABELS[committedRef.current] ?? "");
syncLabels(committedRef.current);
const handleSwap = () => {
const id = getStoredThemeId();
applyTheme(id);
committedRef.current = id;
setCurrentLabel(LABELS[id] ?? "");
syncLabels(id);
};
document.addEventListener("astro:after-swap", handleSwap);
@@ -34,7 +38,7 @@ export default function ThemeSwitcher() {
};
}, []);
const handleClick = () => {
function animateTransition(nextId: string) {
if (animatingRef.current) return;
animatingRef.current = true;
@@ -51,10 +55,9 @@ export default function ThemeSwitcher() {
mask.style.visibility = "visible";
mask.style.transition = "none";
const next = getNextTheme(committedRef.current);
applyTheme(next.id);
committedRef.current = next.id;
setCurrentLabel(LABELS[next.id] ?? "");
applyTheme(nextId);
committedRef.current = nextId;
syncLabels(nextId);
mask.offsetHeight;
@@ -69,6 +72,18 @@ export default function ThemeSwitcher() {
};
mask.addEventListener("transitionend", onEnd);
}
const handleFamilyClick = (e: React.MouseEvent) => {
e.stopPropagation();
const next = getNextFamily(committedRef.current);
animateTransition(next.id);
};
const handleVariantClick = (e: React.MouseEvent) => {
e.stopPropagation();
const next = getNextVariant(committedRef.current);
animateTransition(next.id);
};
return (
@@ -77,14 +92,24 @@ export default function ThemeSwitcher() {
className="fixed bottom-4 right-4 z-[101] pointer-events-auto hidden lg:block"
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
<span
className="text-foreground font-bold text-sm select-none transition-opacity duration-200"
className="text-foreground font-bold text-sm select-none transition-opacity duration-200 inline-flex items-center gap-0"
style={{ opacity: hovering ? 0.8 : 0.15 }}
>
{currentLabel}
<button
onClick={handleFamilyClick}
className="hover:text-yellow-bright transition-colors duration-150 cursor-pointer bg-transparent border-none p-0 font-bold text-sm text-inherit"
>
{familyName}
</button>
<span className="mx-1 opacity-40">·</span>
<button
onClick={handleVariantClick}
className="hover:text-blue-bright transition-colors duration-150 cursor-pointer bg-transparent border-none p-0 font-bold text-sm text-inherit"
>
{variantLabel}
</button>
</span>
</div>