feat(web): add theme system with Obsidian, Paper, and Midnight themes

Three holistic design directions with CSS custom properties, a theme
store persisted to localStorage, and a live switcher in both the header
(cycle button) and settings page (card selector). Also fixes checkbox
checkmark alignment and adds back navigation from settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 22:36:07 +01:00
parent 6c2fc6960a
commit 0352c22b4f
9 changed files with 462 additions and 67 deletions
+57
View File
@@ -0,0 +1,57 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
/** @typedef {'obsidian' | 'paper' | 'midnight'} ThemeName */
const STORAGE_KEY = 'opal-theme';
/** @type {ThemeName} */
const DEFAULT_THEME = 'obsidian';
/** @type {ThemeName[]} */
export const THEMES = ['obsidian', 'paper', 'midnight'];
/**
* Read stored theme, falling back to default
* @returns {ThemeName}
*/
function getInitial() {
if (!browser) return DEFAULT_THEME;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && THEMES.includes(/** @type {ThemeName} */ (stored))) {
return /** @type {ThemeName} */ (stored);
}
return DEFAULT_THEME;
}
function createThemeStore() {
const { subscribe, set, update } = writable(getInitial());
/** Apply theme to the document */
function apply(/** @type {ThemeName} */ theme) {
if (browser) {
document.documentElement.dataset.theme = theme;
localStorage.setItem(STORAGE_KEY, theme);
}
}
// Apply on every change
subscribe(apply);
return {
subscribe,
/** @param {ThemeName} theme */
set(theme) {
set(theme);
},
/** Cycle to the next theme */
cycle() {
update(current => {
const idx = THEMES.indexOf(current);
return THEMES[(idx + 1) % THEMES.length];
});
}
};
}
export const themeStore = createThemeStore();