- Fix reactivity bug: use SvelteMap instead of Map in sessions store - Fix theme listener memory leak: guard against double-init, return cleanup function - Fix transport singleton ignoring different endpoints - Fix form/button type mismatch in MessageInput - Add safer retry validation in chat page - Extract shared utilities: date formatting, session config check, result source config - Extract shared components: Backdrop, PageHeader - Extract orchestration composable from chat page (310→85 lines of script) - Consolidate AuditTimeline switch functions into config record - Move sample data to dedicated module - Add dark mode support for LineageTree SVG colors - Memoize leaf count computation in LineageTree - Update README with usage guide and project structure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
|
|
const STORAGE_KEY = 'llm-multiverse-theme';
|
|
|
|
function createThemeStore() {
|
|
let mode: ThemeMode = $state('system');
|
|
let resolvedDark = $state(false);
|
|
let initialized = false;
|
|
let mediaQuery: MediaQueryList | null = null;
|
|
let mediaListener: ((e: MediaQueryListEvent) => void) | null = null;
|
|
|
|
function applyTheme(isDark: boolean) {
|
|
if (typeof document === 'undefined') return;
|
|
const root = document.documentElement;
|
|
// Add transition class for smooth switching
|
|
root.classList.add('theme-transition');
|
|
if (isDark) {
|
|
root.classList.add('dark');
|
|
} else {
|
|
root.classList.remove('dark');
|
|
}
|
|
// Remove transition class after animation completes
|
|
setTimeout(() => {
|
|
root.classList.remove('theme-transition');
|
|
}, 300);
|
|
}
|
|
|
|
function getSystemPreference(): boolean {
|
|
if (typeof window === 'undefined') return false;
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
}
|
|
|
|
function init(): (() => void) | undefined {
|
|
if (typeof window === 'undefined') return;
|
|
if (initialized) return;
|
|
initialized = true;
|
|
|
|
// Load saved preference
|
|
const saved = localStorage.getItem(STORAGE_KEY) as ThemeMode | null;
|
|
if (saved === 'light' || saved === 'dark' || saved === 'system') {
|
|
mode = saved;
|
|
} else {
|
|
mode = 'system';
|
|
}
|
|
|
|
// Listen for system preference changes
|
|
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
mediaListener = (e: MediaQueryListEvent) => {
|
|
if (mode === 'system') {
|
|
resolvedDark = e.matches;
|
|
applyTheme(resolvedDark);
|
|
}
|
|
};
|
|
mediaQuery.addEventListener('change', mediaListener);
|
|
|
|
// Apply initial theme
|
|
resolvedDark =
|
|
mode === 'dark' ? true : mode === 'light' ? false : getSystemPreference();
|
|
applyTheme(resolvedDark);
|
|
|
|
return () => {
|
|
if (mediaQuery && mediaListener) {
|
|
mediaQuery.removeEventListener('change', mediaListener);
|
|
}
|
|
initialized = false;
|
|
};
|
|
}
|
|
|
|
function setMode(newMode: ThemeMode) {
|
|
mode = newMode;
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(STORAGE_KEY, newMode);
|
|
}
|
|
resolvedDark =
|
|
newMode === 'dark' ? true : newMode === 'light' ? false : getSystemPreference();
|
|
applyTheme(resolvedDark);
|
|
}
|
|
|
|
function cycle() {
|
|
const order: ThemeMode[] = ['system', 'light', 'dark'];
|
|
const idx = order.indexOf(mode);
|
|
const next = order[(idx + 1) % order.length];
|
|
setMode(next);
|
|
}
|
|
|
|
return {
|
|
get mode() {
|
|
return mode;
|
|
},
|
|
get isDark() {
|
|
return resolvedDark;
|
|
},
|
|
init,
|
|
setMode,
|
|
cycle
|
|
};
|
|
}
|
|
|
|
export const themeStore = createThemeStore();
|