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

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { X, ExternalLink } from "lucide-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 { applyTheme, getStoredThemeId } from "@/lib/themes/engine";
import { ANIMATION_IDS, ANIMATION_LABELS, type AnimationId } from "@/lib/animations"; 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" }, { 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 = [ const animOptions = [
{ id: "shuffle", color: "text-red-bright", activeBg: "bg-red-bright/15", activeBorder: "border-red-bright/40" }, { 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" }, { 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" }, { 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 }) { export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
const [currentTheme, setCurrentTheme] = useState(getStoredThemeId()); const [currentTheme, setCurrentTheme] = useState(getStoredThemeId());
const [currentAnim, setCurrentAnim] = useState<string>("shuffle"); const [currentAnim, setCurrentAnim] = useState<string>("shuffle");
@@ -37,7 +42,6 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
const handleTheme = (id: string) => { const handleTheme = (id: string) => {
applyTheme(id); applyTheme(id);
setCurrentTheme(id); setCurrentTheme(id);
onClose();
}; };
const handleAnim = (id: string) => { const handleAnim = (id: string) => {
@@ -48,6 +52,8 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
onClose(); onClose();
}; };
const currentFamily = THEMES[currentTheme]?.family ?? FAMILIES[0].id;
return ( return (
<> <>
{/* Backdrop */} {/* 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 ${ 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" 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"> <div className="flex items-center justify-between px-5 pt-4 pb-2">
<span className="text-foreground/80 font-bold text-lg">Settings</span> <span className="text-foreground/80 font-bold text-lg">Settings</span>
@@ -76,20 +82,42 @@ export function SettingsSheet({ open, onClose }: { open: boolean; onClose: () =>
{/* Theme */} {/* Theme */}
<div> <div>
<div className="text-foreground/50 text-xs uppercase tracking-wider mb-2">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 <button
key={opt.id} key={family.id}
onClick={() => handleTheme(opt.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"
}`}
>
{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 ${ className={`flex-1 py-2.5 rounded-lg text-sm font-medium border transition-colors duration-200 ${
currentTheme === opt.id currentTheme === theme.id
? `${opt.activeBg} ${opt.color} ${opt.activeBorder}` ? `${style.activeBg} ${style.color} ${style.activeBorder}`
: "bg-foreground/5 text-foreground/40 border-transparent hover:text-foreground/60" : "bg-foreground/5 text-foreground/40 border-transparent hover:text-foreground/60"
}`} }`}
> >
{opt.label} {theme.label}
</button> </button>
))} );
})}
</div> </div>
</div> </div>

View File

@@ -1,31 +1,35 @@
import { useRef, useState, useEffect } from "react"; 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 FADE_DURATION = 300;
const LABELS: Record<string, string> = {
darkbox: "classic",
"darkbox-retro": "retro",
"darkbox-dim": "dim",
};
export default function ThemeSwitcher() { export default function ThemeSwitcher() {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const [currentLabel, setCurrentLabel] = useState(""); const [familyName, setFamilyName] = useState("");
const [variantLabel, setVariantLabel] = useState("");
const maskRef = useRef<HTMLDivElement>(null); const maskRef = useRef<HTMLDivElement>(null);
const animatingRef = useRef(false); const animatingRef = useRef(false);
const committedRef = useRef(""); 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(() => { useEffect(() => {
committedRef.current = getStoredThemeId(); committedRef.current = getStoredThemeId();
setCurrentLabel(LABELS[committedRef.current] ?? ""); syncLabels(committedRef.current);
const handleSwap = () => { const handleSwap = () => {
const id = getStoredThemeId(); const id = getStoredThemeId();
applyTheme(id); applyTheme(id);
committedRef.current = id; committedRef.current = id;
setCurrentLabel(LABELS[id] ?? ""); syncLabels(id);
}; };
document.addEventListener("astro:after-swap", handleSwap); document.addEventListener("astro:after-swap", handleSwap);
@@ -34,7 +38,7 @@ export default function ThemeSwitcher() {
}; };
}, []); }, []);
const handleClick = () => { function animateTransition(nextId: string) {
if (animatingRef.current) return; if (animatingRef.current) return;
animatingRef.current = true; animatingRef.current = true;
@@ -51,10 +55,9 @@ export default function ThemeSwitcher() {
mask.style.visibility = "visible"; mask.style.visibility = "visible";
mask.style.transition = "none"; mask.style.transition = "none";
const next = getNextTheme(committedRef.current); applyTheme(nextId);
applyTheme(next.id); committedRef.current = nextId;
committedRef.current = next.id; syncLabels(nextId);
setCurrentLabel(LABELS[next.id] ?? "");
mask.offsetHeight; mask.offsetHeight;
@@ -69,6 +72,18 @@ export default function ThemeSwitcher() {
}; };
mask.addEventListener("transitionend", onEnd); 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 ( return (
@@ -77,14 +92,24 @@ export default function ThemeSwitcher() {
className="fixed bottom-4 right-4 z-[101] pointer-events-auto hidden lg:block" className="fixed bottom-4 right-4 z-[101] pointer-events-auto hidden lg:block"
onMouseEnter={() => setHovering(true)} onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)} onMouseLeave={() => setHovering(false)}
onClick={handleClick}
style={{ cursor: "pointer" }}
> >
<span <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 }} 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> </span>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { THEMES, DEFAULT_THEME_ID } from "@/lib/themes"; import { THEMES, FAMILIES, DEFAULT_THEME_ID } from "@/lib/themes";
import { CSS_PROPS } from "@/lib/themes/props"; import { CSS_PROPS } from "@/lib/themes/props";
import type { Theme } from "@/lib/themes/types"; import type { Theme } from "@/lib/themes/types";
@@ -11,6 +11,26 @@ export function saveTheme(id: string): void {
localStorage.setItem("theme", id); localStorage.setItem("theme", id);
} }
/** Cycle to the next theme family, jumping to its default variant. */
export function getNextFamily(currentId: string): Theme {
const current = THEMES[currentId];
const familyId = current?.family ?? FAMILIES[0].id;
const idx = FAMILIES.findIndex((f) => f.id === familyId);
const next = FAMILIES[(idx + 1) % FAMILIES.length];
return THEMES[next.default];
}
/** Cycle to the next variant within the current family. */
export function getNextVariant(currentId: string): Theme {
const current = THEMES[currentId];
if (!current) return Object.values(THEMES)[0];
const family = FAMILIES.find((f) => f.id === current.family);
if (!family) return current;
const idx = family.themes.findIndex((t) => t.id === currentId);
return family.themes[(idx + 1) % family.themes.length];
}
// Keep for backward compat (cycles all themes linearly)
export function getNextTheme(currentId: string): Theme { export function getNextTheme(currentId: string): Theme {
const list = Object.values(THEMES); const list = Object.values(THEMES);
const idx = list.findIndex((t) => t.id === currentId); const idx = list.findIndex((t) => t.id === currentId);
@@ -27,7 +47,6 @@ export function previewTheme(id: string): void {
root.style.setProperty(prop, theme.colors[key]); root.style.setProperty(prop, theme.colors[key]);
} }
document.dispatchEvent(new CustomEvent("theme-changed", { detail: { id } })); document.dispatchEvent(new CustomEvent("theme-changed", { detail: { id } }));
} }

View File

@@ -0,0 +1,94 @@
import type { Theme, ThemeFamily } from "../types";
// Catppuccin — warm pastel palette. Each flavor has its own accent colors.
const mocha: Theme = {
id: "catppuccin-mocha",
family: "catppuccin",
label: "mocha",
name: "Catppuccin Mocha",
type: "dark",
colors: {
background: "30 30 46",
foreground: "205 214 244",
red: "243 139 168", redBright: "246 166 190",
orange: "250 179 135", orangeBright: "252 200 170",
green: "166 227 161", greenBright: "190 236 186",
yellow: "249 226 175", yellowBright: "251 235 200",
blue: "137 180 250", blueBright: "172 202 251",
purple: "203 166 247", purpleBright: "220 192 249",
aqua: "148 226 213", aquaBright: "180 236 228",
surface: "49 50 68",
},
canvasPalette: [[243,139,168],[166,227,161],[249,226,175],[137,180,250],[203,166,247],[148,226,213]],
};
const macchiato: Theme = {
id: "catppuccin-macchiato",
family: "catppuccin",
label: "macchiato",
name: "Catppuccin Macchiato",
type: "dark",
colors: {
background: "36 39 58",
foreground: "202 211 245",
red: "237 135 150", redBright: "242 167 180",
orange: "245 169 127", orangeBright: "248 192 165",
green: "166 218 149", greenBright: "190 232 180",
yellow: "238 212 159", yellowBright: "243 226 190",
blue: "138 173 244", blueBright: "170 198 247",
purple: "198 160 246", purpleBright: "218 190 249",
aqua: "139 213 202", aquaBright: "175 228 220",
surface: "54 58 79",
},
canvasPalette: [[237,135,150],[166,218,149],[238,212,159],[138,173,244],[198,160,246],[139,213,202]],
};
const frappe: Theme = {
id: "catppuccin-frappe",
family: "catppuccin",
label: "frappé",
name: "Catppuccin Frappé",
type: "dark",
colors: {
background: "48 52 70",
foreground: "198 208 245",
red: "231 130 132", redBright: "238 160 162",
orange: "239 159 118", orangeBright: "244 185 158",
green: "166 209 137", greenBright: "190 222 172",
yellow: "229 200 144", yellowBright: "237 216 178",
blue: "140 170 238", blueBright: "172 196 242",
purple: "202 158 230", purpleBright: "218 186 238",
aqua: "129 200 190", aquaBright: "168 216 208",
surface: "65 69 89",
},
canvasPalette: [[231,130,132],[166,209,137],[229,200,144],[140,170,238],[202,158,230],[129,200,190]],
};
const latte: Theme = {
id: "catppuccin-latte",
family: "catppuccin",
label: "latte",
name: "Catppuccin Latte",
type: "light",
colors: {
background: "239 241 245",
foreground: "76 79 105",
red: "210 15 57", redBright: "228 50 82",
orange: "254 100 11", orangeBright: "254 135 60",
green: "64 160 43", greenBright: "85 180 65",
yellow: "223 142 29", yellowBright: "236 170 60",
blue: "30 102 245", blueBright: "70 130 248",
purple: "136 57 239", purpleBright: "162 95 244",
aqua: "23 146 153", aquaBright: "55 168 175",
surface: "204 208 218",
},
canvasPalette: [[210,15,57],[64,160,43],[223,142,29],[30,102,245],[136,57,239],[23,146,153]],
};
export const catppuccin: ThemeFamily = {
id: "catppuccin",
name: "Catppuccin",
themes: [mocha, macchiato, frappe, latte],
default: "catppuccin-mocha",
};

View File

@@ -0,0 +1,71 @@
import type { Theme, ThemeFamily } from "../types";
const classic: Theme = {
id: "darkbox",
family: "darkbox",
label: "classic",
name: "Darkbox Classic",
type: "dark",
colors: {
background: "0 0 0",
foreground: "235 219 178",
red: "251 73 52", redBright: "255 110 85",
orange: "254 128 25", orangeBright: "255 165 65",
green: "184 187 38", greenBright: "210 215 70",
yellow: "250 189 47", yellowBright: "255 215 85",
blue: "131 165 152", blueBright: "165 195 180",
purple: "211 134 155", purpleBright: "235 165 180",
aqua: "142 192 124", aquaBright: "175 220 160",
surface: "60 56 54",
},
canvasPalette: [[251,73,52],[184,187,38],[250,189,47],[131,165,152],[211,134,155],[142,192,124]],
};
const retro: Theme = {
id: "darkbox-retro",
family: "darkbox",
label: "retro",
name: "Darkbox Retro",
type: "dark",
colors: {
background: "0 0 0",
foreground: "189 174 147",
red: "204 36 29", redBright: "251 73 52",
orange: "214 93 14", orangeBright: "254 128 25",
green: "152 151 26", greenBright: "184 187 38",
yellow: "215 153 33", yellowBright: "250 189 47",
blue: "69 133 136", blueBright: "131 165 152",
purple: "177 98 134", purpleBright: "211 134 155",
aqua: "104 157 106", aquaBright: "142 192 124",
surface: "60 56 54",
},
canvasPalette: [[204,36,29],[152,151,26],[215,153,33],[69,133,136],[177,98,134],[104,157,106]],
};
const dim: Theme = {
id: "darkbox-dim",
family: "darkbox",
label: "dim",
name: "Darkbox Dim",
type: "dark",
colors: {
background: "0 0 0",
foreground: "168 153 132",
red: "157 0 6", redBright: "204 36 29",
orange: "175 58 3", orangeBright: "214 93 14",
green: "121 116 14", greenBright: "152 151 26",
yellow: "181 118 20", yellowBright: "215 153 33",
blue: "7 102 120", blueBright: "69 133 136",
purple: "143 63 113", purpleBright: "177 98 134",
aqua: "66 123 88", aquaBright: "104 157 106",
surface: "60 56 54",
},
canvasPalette: [[157,0,6],[121,116,14],[181,118,20],[7,102,120],[143,63,113],[66,123,88]],
};
export const darkbox: ThemeFamily = {
id: "darkbox",
name: "Darkbox",
themes: [classic, retro, dim],
default: "darkbox-retro",
};

View File

@@ -0,0 +1,70 @@
import type { Theme, ThemeFamily } from "../types";
// Everforest — warm green-toned palette. Same accents across
// hard/medium/soft, only backgrounds change.
const accents = {
red: "230 126 128", redBright: "240 155 157",
orange: "230 152 117", orangeBright: "240 177 150",
green: "167 192 128", greenBright: "190 210 160",
yellow: "219 188 127", yellowBright: "233 208 163",
blue: "127 187 179", blueBright: "160 208 200",
purple: "214 153 182", purpleBright: "228 180 202",
aqua: "131 192 146", aquaBright: "162 212 176",
} as const;
const palette: [number, number, number][] = [
[230,126,128],[167,192,128],[219,188,127],[127,187,179],[214,153,182],[131,192,146],
];
const hard: Theme = {
id: "everforest-hard",
family: "everforest",
label: "hard",
name: "Everforest Hard",
type: "dark",
colors: {
background: "39 46 51",
foreground: "211 198 170",
...accents,
surface: "55 65 69",
},
canvasPalette: palette,
};
const medium: Theme = {
id: "everforest-medium",
family: "everforest",
label: "medium",
name: "Everforest Medium",
type: "dark",
colors: {
background: "45 53 59",
foreground: "211 198 170",
...accents,
surface: "61 72 77",
},
canvasPalette: palette,
};
const soft: Theme = {
id: "everforest-soft",
family: "everforest",
label: "soft",
name: "Everforest Soft",
type: "dark",
colors: {
background: "52 63 68",
foreground: "211 198 170",
...accents,
surface: "68 80 85",
},
canvasPalette: palette,
};
export const everforest: ThemeFamily = {
id: "everforest",
name: "Everforest",
themes: [hard, medium, soft],
default: "everforest-medium",
};

View File

@@ -0,0 +1,70 @@
import type { Theme, ThemeFamily } from "../types";
// Original gruvbox palette — same accents across hard/medium/soft,
// only background and surface change.
const accents = {
red: "204 36 29", redBright: "251 73 52",
orange: "214 93 14", orangeBright: "254 128 25",
green: "152 151 26", greenBright: "184 187 38",
yellow: "215 153 33", yellowBright: "250 189 47",
blue: "69 133 136", blueBright: "131 165 152",
purple: "177 98 134", purpleBright: "211 134 155",
aqua: "104 157 106", aquaBright: "142 192 124",
} as const;
const palette: [number, number, number][] = [
[204,36,29],[152,151,26],[215,153,33],[69,133,136],[177,98,134],[104,157,106],
];
const hard: Theme = {
id: "gruvbox-hard",
family: "gruvbox",
label: "hard",
name: "Gruvbox Hard",
type: "dark",
colors: {
background: "29 32 33",
foreground: "235 219 178",
...accents,
surface: "60 56 54",
},
canvasPalette: palette,
};
const medium: Theme = {
id: "gruvbox-medium",
family: "gruvbox",
label: "medium",
name: "Gruvbox Medium",
type: "dark",
colors: {
background: "40 40 40",
foreground: "235 219 178",
...accents,
surface: "60 56 54",
},
canvasPalette: palette,
};
const soft: Theme = {
id: "gruvbox-soft",
family: "gruvbox",
label: "soft",
name: "Gruvbox Soft",
type: "dark",
colors: {
background: "50 48 47",
foreground: "235 219 178",
...accents,
surface: "80 73 69",
},
canvasPalette: palette,
};
export const gruvbox: ThemeFamily = {
id: "gruvbox",
name: "Gruvbox",
themes: [hard, medium, soft],
default: "gruvbox-medium",
};

View File

@@ -0,0 +1,74 @@
import type { Theme, ThemeFamily } from "../types";
// Kanagawa — inspired by Katsushika Hokusai's paintings.
// Each variant has its own distinct palette.
const wave: Theme = {
id: "kanagawa-wave",
family: "kanagawa",
label: "wave",
name: "Kanagawa Wave",
type: "dark",
colors: {
background: "31 31 40",
foreground: "220 215 186",
red: "195 64 67", redBright: "255 93 98",
orange: "255 160 102", orangeBright: "255 184 140",
green: "118 148 106", greenBright: "152 187 108",
yellow: "192 163 110", yellowBright: "230 195 132",
blue: "126 156 216", blueBright: "127 180 202",
purple: "149 127 184", purpleBright: "175 158 206",
aqua: "106 149 137", aquaBright: "122 168 159",
surface: "42 42 55",
},
canvasPalette: [[195,64,67],[118,148,106],[192,163,110],[126,156,216],[149,127,184],[106,149,137]],
};
const dragon: Theme = {
id: "kanagawa-dragon",
family: "kanagawa",
label: "dragon",
name: "Kanagawa Dragon",
type: "dark",
colors: {
background: "24 22 22",
foreground: "197 201 197",
red: "196 116 110", redBright: "195 64 67",
orange: "182 146 123", orangeBright: "255 160 102",
green: "135 169 135", greenBright: "152 187 108",
yellow: "196 178 138", yellowBright: "230 195 132",
blue: "139 164 176", blueBright: "126 156 216",
purple: "162 146 163", purpleBright: "149 127 184",
aqua: "142 164 162", aquaBright: "122 168 159",
surface: "40 39 39",
},
canvasPalette: [[196,116,110],[135,169,135],[196,178,138],[139,164,176],[162,146,163],[142,164,162]],
};
const lotus: Theme = {
id: "kanagawa-lotus",
family: "kanagawa",
label: "lotus",
name: "Kanagawa Lotus",
type: "light",
colors: {
background: "242 236 188",
foreground: "84 84 100",
red: "200 64 83", redBright: "215 71 75",
orange: "233 138 0", orangeBright: "245 160 30",
green: "111 137 78", greenBright: "130 158 98",
yellow: "222 152 0", yellowBright: "240 178 40",
blue: "77 105 155", blueBright: "93 87 163",
purple: "98 76 131", purpleBright: "118 100 155",
aqua: "89 123 117", aquaBright: "110 145 138",
surface: "228 215 148",
},
canvasPalette: [[200,64,83],[111,137,78],[222,152,0],[77,105,155],[98,76,131],[89,123,117]],
};
export const kanagawa: ThemeFamily = {
id: "kanagawa",
name: "Kanagawa",
themes: [wave, dragon, lotus],
default: "kanagawa-wave",
};

View File

@@ -0,0 +1,74 @@
import type { Theme, ThemeFamily } from "../types";
// Rosé Pine — soft muted palette inspired by the natural world.
// Only 6 accent hues; aqua is derived between pine and foam.
const main: Theme = {
id: "rosepine-main",
family: "rosepine",
label: "main",
name: "Rosé Pine",
type: "dark",
colors: {
background: "25 23 36",
foreground: "224 222 244",
red: "235 111 146", redBright: "241 145 174",
orange: "235 188 186", orangeBright: "240 208 206",
green: "49 116 143", greenBright: "78 140 165",
yellow: "246 193 119", yellowBright: "249 212 160",
blue: "156 207 216", blueBright: "180 222 229",
purple: "196 167 231", purpleBright: "214 190 239",
aqua: "100 170 185", aquaBright: "135 192 205",
surface: "38 35 58",
},
canvasPalette: [[235,111,146],[49,116,143],[246,193,119],[156,207,216],[196,167,231],[235,188,186]],
};
const moon: Theme = {
id: "rosepine-moon",
family: "rosepine",
label: "moon",
name: "Rosé Pine Moon",
type: "dark",
colors: {
background: "35 33 54",
foreground: "224 222 244",
red: "235 111 146", redBright: "241 145 174",
orange: "234 154 151", orangeBright: "241 186 184",
green: "62 143 176", greenBright: "90 165 195",
yellow: "246 193 119", yellowBright: "249 212 160",
blue: "156 207 216", blueBright: "180 222 229",
purple: "196 167 231", purpleBright: "214 190 239",
aqua: "110 178 196", aquaBright: "140 196 210",
surface: "57 53 82",
},
canvasPalette: [[235,111,146],[62,143,176],[246,193,119],[156,207,216],[196,167,231],[234,154,151]],
};
const dawn: Theme = {
id: "rosepine-dawn",
family: "rosepine",
label: "dawn",
name: "Rosé Pine Dawn",
type: "light",
colors: {
background: "250 244 237",
foreground: "87 82 121",
red: "180 99 122", redBright: "200 120 142",
orange: "215 130 126", orangeBright: "230 155 152",
green: "40 105 131", greenBright: "60 125 150",
yellow: "234 157 52", yellowBright: "242 180 85",
blue: "86 148 159", blueBright: "110 168 178",
purple: "144 122 169", purpleBright: "168 148 188",
aqua: "62 128 146", aquaBright: "85 150 165",
surface: "242 233 225",
},
canvasPalette: [[180,99,122],[40,105,131],[234,157,52],[86,148,159],[144,122,169],[215,130,126]],
};
export const rosepine: ThemeFamily = {
id: "rosepine",
name: "Rosé Pine",
themes: [main, moon, dawn],
default: "rosepine-main",
};

View File

@@ -1,58 +1,26 @@
import type { Theme } from "./types"; import type { Theme, ThemeFamily } from "./types";
import { darkbox } from "./families/darkbox";
import { gruvbox } from "./families/gruvbox";
import { everforest } from "./families/everforest";
import { catppuccin } from "./families/catppuccin";
import { rosepine } from "./families/rosepine";
import { kanagawa } from "./families/kanagawa";
export const DEFAULT_THEME_ID = "darkbox-retro"; export const DEFAULT_THEME_ID = "darkbox-retro";
function theme( export const FAMILIES: ThemeFamily[] = [
id: string, darkbox,
name: string, gruvbox,
type: "dark" | "light", everforest,
colors: Theme["colors"], catppuccin,
palette: [number, number, number][] rosepine,
): Theme { kanagawa,
return { id, name, type, colors, canvasPalette: palette }; ];
// Flat lookup — backward compatible with all existing consumers
export const THEMES: Record<string, Theme> = {};
for (const family of FAMILIES) {
for (const theme of family.themes) {
THEMES[theme.id] = theme;
}
} }
// Three darkbox variants from darkbox.nvim
// Classic (vivid) → Retro (muted) → Dim (deep)
// Each variant's "bright" is the next level up's base.
export const THEMES: Record<string, Theme> = {
darkbox: theme("darkbox", "Darkbox Classic", "dark", {
background: "0 0 0",
foreground: "235 219 178",
red: "251 73 52", redBright: "255 110 85",
orange: "254 128 25", orangeBright: "255 165 65",
green: "184 187 38", greenBright: "210 215 70",
yellow: "250 189 47", yellowBright: "255 215 85",
blue: "131 165 152", blueBright: "165 195 180",
purple: "211 134 155", purpleBright: "235 165 180",
aqua: "142 192 124", aquaBright: "175 220 160",
surface: "60 56 54",
}, [[251,73,52],[184,187,38],[250,189,47],[131,165,152],[211,134,155],[142,192,124]]),
"darkbox-retro": theme("darkbox-retro", "Darkbox Retro", "dark", {
background: "0 0 0",
foreground: "189 174 147",
red: "204 36 29", redBright: "251 73 52",
orange: "214 93 14", orangeBright: "254 128 25",
green: "152 151 26", greenBright: "184 187 38",
yellow: "215 153 33", yellowBright: "250 189 47",
blue: "69 133 136", blueBright: "131 165 152",
purple: "177 98 134", purpleBright: "211 134 155",
aqua: "104 157 106", aquaBright: "142 192 124",
surface: "60 56 54",
}, [[204,36,29],[152,151,26],[215,153,33],[69,133,136],[177,98,134],[104,157,106]]),
"darkbox-dim": theme("darkbox-dim", "Darkbox Dim", "dark", {
background: "0 0 0",
foreground: "168 153 132",
red: "157 0 6", redBright: "204 36 29",
orange: "175 58 3", orangeBright: "214 93 14",
green: "121 116 14", greenBright: "152 151 26",
yellow: "181 118 20", yellowBright: "215 153 33",
blue: "7 102 120", blueBright: "69 133 136",
purple: "143 63 113", purpleBright: "177 98 134",
aqua: "66 123 88", aquaBright: "104 157 106",
surface: "60 56 54",
}, [[157,0,6],[121,116,14],[181,118,20],[7,102,120],[143,63,113],[66,123,88]]),
};

View File

@@ -20,8 +20,17 @@ export interface ThemeColors {
export interface Theme { export interface Theme {
id: string; id: string;
family: string;
label: string;
name: string; name: string;
type: "dark" | "light"; type: "dark" | "light";
colors: ThemeColors; colors: ThemeColors;
canvasPalette: [number, number, number][]; canvasPalette: [number, number, number][];
} }
export interface ThemeFamily {
id: string;
name: string;
themes: Theme[];
default: string;
}

View File

@@ -47,19 +47,19 @@
to bottom, to bottom,
transparent 0px, transparent 0px,
transparent 2px, transparent 2px,
rgba(0, 0, 0, 0.12) 2px, rgb(var(--color-foreground) / 0.06) 2px,
rgba(0, 0, 0, 0.12) 4px rgb(var(--color-foreground) / 0.06) 4px
); );
animation: crt-scroll 12s linear infinite; animation: crt-scroll 12s linear infinite;
z-index: 1; z-index: 1;
} }
.crt-bloom { .crt-bloom {
box-shadow: inset 0 0 100px 30px rgba(0, 0, 0, 0.3); box-shadow: inset 0 0 100px 30px rgb(var(--color-background) / 0.3);
background: radial-gradient( background: radial-gradient(
ellipse at center, ellipse at center,
transparent 50%, transparent 50%,
rgba(0, 0, 0, 0.25) 100% rgb(var(--color-background) / 0.25) 100%
); );
z-index: 2; z-index: 2;
} }