mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
Add theme families
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { X, ExternalLink } from "lucide-react";
|
||||
import { THEMES } from "@/lib/themes";
|
||||
import { FAMILIES, THEMES } from "@/lib/themes";
|
||||
import { applyTheme, getStoredThemeId } from "@/lib/themes/engine";
|
||||
import { ANIMATION_IDS, ANIMATION_LABELS, type AnimationId } from "@/lib/animations";
|
||||
|
||||
@@ -11,12 +11,6 @@ const footerLinks = [
|
||||
{ href: "https://github.com/timmypidashev/web", label: "Source", color: "text-purple" },
|
||||
];
|
||||
|
||||
const themeOptions = [
|
||||
{ id: "darkbox", label: "classic", color: "text-yellow-bright", activeBg: "bg-yellow-bright/15", activeBorder: "border-yellow-bright/40" },
|
||||
{ id: "darkbox-retro", label: "retro", color: "text-orange-bright", activeBg: "bg-orange-bright/15", activeBorder: "border-orange-bright/40" },
|
||||
{ id: "darkbox-dim", label: "dim", color: "text-purple-bright", activeBg: "bg-purple-bright/15", activeBorder: "border-purple-bright/40" },
|
||||
];
|
||||
|
||||
const animOptions = [
|
||||
{ id: "shuffle", color: "text-red-bright", activeBg: "bg-red-bright/15", activeBorder: "border-red-bright/40" },
|
||||
{ id: "game-of-life", color: "text-green-bright", activeBg: "bg-green-bright/15", activeBorder: "border-green-bright/40" },
|
||||
@@ -26,6 +20,17 @@ const animOptions = [
|
||||
{ id: "pipes", color: "text-blue-bright", activeBg: "bg-blue-bright/15", activeBorder: "border-blue-bright/40" },
|
||||
];
|
||||
|
||||
// Cycle through accent colors for variant buttons
|
||||
const variantColors = [
|
||||
{ color: "text-yellow-bright", activeBg: "bg-yellow-bright/15", activeBorder: "border-yellow-bright/40" },
|
||||
{ color: "text-orange-bright", activeBg: "bg-orange-bright/15", activeBorder: "border-orange-bright/40" },
|
||||
{ color: "text-purple-bright", activeBg: "bg-purple-bright/15", activeBorder: "border-purple-bright/40" },
|
||||
{ color: "text-blue-bright", activeBg: "bg-blue-bright/15", activeBorder: "border-blue-bright/40" },
|
||||
{ color: "text-green-bright", activeBg: "bg-green-bright/15", activeBorder: "border-green-bright/40" },
|
||||
{ color: "text-red-bright", activeBg: "bg-red-bright/15", activeBorder: "border-red-bright/40" },
|
||||
{ color: "text-aqua-bright", activeBg: "bg-aqua-bright/15", activeBorder: "border-aqua-bright/40" },
|
||||
];
|
||||
|
||||
export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const [currentTheme, setCurrentTheme] = useState(getStoredThemeId());
|
||||
const [currentAnim, setCurrentAnim] = useState<string>("shuffle");
|
||||
@@ -37,7 +42,6 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
|
||||
const handleTheme = (id: string) => {
|
||||
applyTheme(id);
|
||||
setCurrentTheme(id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleAnim = (id: string) => {
|
||||
@@ -48,6 +52,8 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
|
||||
onClose();
|
||||
};
|
||||
|
||||
const currentFamily = THEMES[currentTheme]?.family ?? FAMILIES[0].id;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
@@ -63,7 +69,7 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
|
||||
className={`fixed left-0 right-0 bottom-0 z-[70] bg-background border-t border-foreground/10 rounded-t-2xl transition-transform duration-300 ease-out ${
|
||||
open ? "translate-y-0" : "translate-y-full"
|
||||
}`}
|
||||
style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)" }}
|
||||
style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)", maxHeight: "80vh", overflowY: "auto" }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 pt-4 pb-2">
|
||||
<span className="text-foreground/80 font-bold text-lg">Settings</span>
|
||||
@@ -76,21 +82,43 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
|
||||
{/* Theme */}
|
||||
<div>
|
||||
<div className="text-foreground/50 text-xs uppercase tracking-wider mb-2">Theme</div>
|
||||
<div className="flex gap-2">
|
||||
{themeOptions.map((opt) => (
|
||||
|
||||
{/* Family selector */}
|
||||
<div className="flex gap-2 mb-3 overflow-x-auto pb-1">
|
||||
{FAMILIES.map((family) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
onClick={() => handleTheme(opt.id)}
|
||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium border transition-colors duration-200 ${
|
||||
currentTheme === opt.id
|
||||
? `${opt.activeBg} ${opt.color} ${opt.activeBorder}`
|
||||
: "bg-foreground/5 text-foreground/40 border-transparent hover:text-foreground/60"
|
||||
key={family.id}
|
||||
onClick={() => handleTheme(family.default)}
|
||||
className={`flex-shrink-0 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors duration-200 ${
|
||||
currentFamily === family.id
|
||||
? "bg-foreground/10 text-foreground/80 border-foreground/20"
|
||||
: "bg-foreground/5 text-foreground/30 border-transparent hover:text-foreground/50"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
{family.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Variant selector for current family */}
|
||||
<div className="flex gap-2">
|
||||
{FAMILIES.find((f) => f.id === currentFamily)?.themes.map((theme, i) => {
|
||||
const style = variantColors[i % variantColors.length];
|
||||
return (
|
||||
<button
|
||||
key={theme.id}
|
||||
onClick={() => handleTheme(theme.id)}
|
||||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium border transition-colors duration-200 ${
|
||||
currentTheme === theme.id
|
||||
? `${style.activeBg} ${style.color} ${style.activeBorder}`
|
||||
: "bg-foreground/5 text-foreground/40 border-transparent hover:text-foreground/60"
|
||||
}`}
|
||||
>
|
||||
{theme.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animation */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user