Files
Website/src/lib/stores/theme.ts
2025-11-29 08:20:13 +00:00

160 lines
4.9 KiB
TypeScript

import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
import { type ThemeColorMap, defaultColorMap } from '$lib/components/tui/utils';
// Import theme JSON files
import archTheme from '$lib/assets/themes/arch.theme.json';
import catppuccinTheme from '$lib/assets/themes/catppuccin.theme.json';
import wintryTheme from '$lib/assets/themes/wintry.theme.json';
import roseTheme from '$lib/assets/themes/rose.theme.json';
import cerberusTheme from '$lib/assets/themes/cerberus.theme.json';
export type ColorTheme = 'arch' | 'catppuccin' | 'wintry' | 'rose' | 'cerberus';
export type Mode = 'dark' | 'light';
// Theme JSON structure
export interface ThemeJson {
name: string;
icon: string;
dark: {
colors: Record<string, string>;
colorMap: ThemeColorMap;
};
light: {
colors: Record<string, string>;
colorMap: ThemeColorMap;
};
}
export interface ThemeColors {
primary: string;
secondary: string;
accent: string;
background: string;
backgroundLight: string;
text: string;
textMuted: string;
border: string;
terminal: string;
terminalPrompt: string;
terminalUser: string;
terminalPath: string;
colorMap: ThemeColorMap;
}
// Load themes from JSON files
const themes: Record<ColorTheme, ThemeJson> = {
arch: archTheme as ThemeJson,
catppuccin: catppuccinTheme as ThemeJson,
wintry: wintryTheme as ThemeJson,
rose: roseTheme as ThemeJson,
cerberus: cerberusTheme as ThemeJson
};
// Export themes for external access
export { themes };
// Build theme colors from JSON - merges colors into colorMap so formatter can use both
function buildThemeColors(theme: ThemeJson, mode: Mode): ThemeColors {
const modeData = theme[mode];
// Merge: defaults -> theme palette -> theme colors (so text, primary, etc. are correct hex values)
const mergedColorMap: ThemeColorMap = {
...defaultColorMap, // Fallback defaults (hex values)
...(modeData.colorMap ?? {}), // Theme's color palette (hex values)
// Also add theme colors so (&text), (&primary), etc. resolve to correct hex for this mode
'text': modeData.colors.text,
'textMuted': modeData.colors.textMuted,
'primary': modeData.colors.primary,
'secondary': modeData.colors.secondary,
'accent': modeData.colors.accent,
'background': modeData.colors.background,
'backgroundLight': modeData.colors.backgroundLight,
'border': modeData.colors.border,
'terminal': modeData.colors.terminal,
'terminalPrompt': modeData.colors.terminalPrompt,
'terminalUser': modeData.colors.terminalUser,
'terminalPath': modeData.colors.terminalPath,
'muted': modeData.colors.textMuted, // alias
};
return {
primary: modeData.colors.primary,
secondary: modeData.colors.secondary,
accent: modeData.colors.accent,
background: modeData.colors.background,
backgroundLight: modeData.colors.backgroundLight,
text: modeData.colors.text,
textMuted: modeData.colors.textMuted,
border: modeData.colors.border,
terminal: modeData.colors.terminal,
terminalPrompt: modeData.colors.terminalPrompt,
terminalUser: modeData.colors.terminalUser,
terminalPath: modeData.colors.terminalPath,
colorMap: mergedColorMap
};
}
// Build the theme colors map from JSON themes
const themeColorsMap: Record<ColorTheme, { dark: ThemeColors; light: ThemeColors }> =
Object.fromEntries(
Object.entries(themes).map(([key, theme]) => [
key,
{
dark: buildThemeColors(theme, 'dark'),
light: buildThemeColors(theme, 'light')
}
])
) as Record<ColorTheme, { dark: ThemeColors; light: ThemeColors }>;
function getInitialMode(): Mode {
if (browser) {
const stored = localStorage.getItem('mode');
if (stored === 'dark' || stored === 'light') return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'dark';
}
function getInitialTheme(): ColorTheme {
if (browser) {
const stored = localStorage.getItem('colorTheme');
if (stored && stored in themeColorsMap) return stored as ColorTheme;
}
return 'arch';
}
export const mode = writable<Mode>(getInitialMode());
export const colorTheme = writable<ColorTheme>(getInitialTheme());
export const themeColors = derived([mode, colorTheme], ([$mode, $colorTheme]) => {
return themeColorsMap[$colorTheme][$mode];
});
// Subscribe to persist changes
if (browser) {
mode.subscribe((value) => {
localStorage.setItem('mode', value);
document.documentElement.setAttribute('data-mode', value);
});
colorTheme.subscribe((value) => {
localStorage.setItem('colorTheme', value);
document.documentElement.setAttribute('data-theme', value);
});
}
export function toggleMode() {
mode.update((m) => (m === 'dark' ? 'light' : 'dark'));
}
export function setColorTheme(theme: ColorTheme) {
colorTheme.set(theme);
}
// Generate theme options from loaded themes
export const themeOptions: { value: ColorTheme; label: string; icon: string }[] =
Object.entries(themes).map(([key, theme]) => ({
value: key as ColorTheme,
label: theme.name,
icon: theme.icon
}));