refactor(features, widgets): update ThemeManager, FontSampler, FontSearch, and SampleList

This commit is contained in:
Ilia Mashkov
2026-03-02 22:20:48 +03:00
parent 0fa3437661
commit 55e2efc222
14 changed files with 1512 additions and 115 deletions

View File

@@ -1,14 +1,55 @@
/**
* Theme management with system preference detection
*
* Manages light/dark theme state with localStorage persistence
* and automatic system preference detection. Themes are applied
* via CSS class on the document element.
*
* Features:
* - Persists user preference to localStorage
* - Detects OS-level theme preference
* - Responds to OS theme changes when in "system" mode
* - Toggle between light/dark themes
* - Reset to follow system preference
*
* @example
* ```svelte
* <script lang="ts">
* import { themeManager } from '$features/ChangeAppTheme';
*
* // Initialize once on app mount
* onMount(() => themeManager.init());
* onDestroy(() => themeManager.destroy());
* </script>
*
* <button on:click={() => themeManager.toggle()}>
* {themeManager.isDark ? 'Switch to Light' : 'Switch to Dark'}
* </button>
* ```
*/
import { createPersistentStore } from '$shared/lib';
type Theme = 'light' | 'dark';
type ThemeSource = 'system' | 'user';
/**
* Theme manager singleton
*
* Call init() on app mount and destroy() on app unmount.
* Use isDark property to conditionally apply styles.
*/
class ThemeManager {
// Private reactive state
/** Current theme value ('light' or 'dark') */
#theme = $state<Theme>('light');
/** Whether theme is controlled by user or follows system */
#source = $state<ThemeSource>('system');
/** MediaQueryList for detecting system theme changes */
#mediaQuery: MediaQueryList | null = null;
/** Persistent storage for user's theme preference */
#store = createPersistentStore<Theme | null>('glyphdiff:theme', null);
/** Bound handler for system theme change events */
#systemChangeHandler = this.#onSystemChange.bind(this);
constructor() {
@@ -23,35 +64,56 @@ class ThemeManager {
}
}
/** Current theme value */
get value(): Theme {
return this.#theme;
}
/** Source of current theme ('system' or 'user') */
get source(): ThemeSource {
return this.#source;
}
/** Whether dark theme is active */
get isDark(): boolean {
return this.#theme === 'dark';
}
/** Whether theme is controlled by user (not following system) */
get isUserControlled(): boolean {
return this.#source === 'user';
}
/** Call once in root onMount */
/**
* Initialize theme manager
*
* Applies current theme to DOM and sets up system preference listener.
* Call once in root component onMount.
*/
init(): void {
this.#applyToDom(this.#theme);
this.#mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.#mediaQuery.addEventListener('change', this.#systemChangeHandler);
}
/** Call in root onDestroy */
/**
* Clean up theme manager
*
* Removes system preference listener.
* Call in root component onDestroy.
*/
destroy(): void {
this.#mediaQuery?.removeEventListener('change', this.#systemChangeHandler);
this.#mediaQuery = null;
}
/**
* Set theme explicitly
*
* Switches to user control and applies specified theme.
*
* @param theme - Theme to apply ('light' or 'dark')
*/
setTheme(theme: Theme): void {
this.#source = 'user';
this.#theme = theme;
@@ -59,11 +121,18 @@ class ThemeManager {
this.#applyToDom(theme);
}
/**
* Toggle between light and dark themes
*/
toggle(): void {
this.setTheme(this.value === 'dark' ? 'light' : 'dark');
}
/** Hand control back to OS */
/**
* Reset to follow system preference
*
* Clears user preference and switches to system theme.
*/
resetToSystem(): void {
this.#store.clear();
this.#theme = this.#getSystemTheme();
@@ -71,10 +140,12 @@ class ThemeManager {
this.#applyToDom(this.#theme);
}
// -------------------------
// Private helpers
// -------------------------
/**
* Detect system theme preference
* @returns 'dark' if system prefers dark mode, 'light' otherwise
*/
#getSystemTheme(): Theme {
if (typeof window === 'undefined') {
return 'light';
@@ -83,10 +154,18 @@ class ThemeManager {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* Apply theme to DOM
* @param theme - Theme to apply
*/
#applyToDom(theme: Theme): void {
document.documentElement.classList.toggle('dark', theme === 'dark');
}
/**
* Handle system theme change
* Only updates if currently following system preference
*/
#onSystemChange(e: MediaQueryListEvent): void {
if (this.#source === 'system') {
this.#theme = e.matches ? 'dark' : 'light';
@@ -95,5 +174,15 @@ class ThemeManager {
}
}
// Export a singleton — one instance for the whole app
/**
* Singleton theme manager instance
*
* Use throughout the app for consistent theme state.
*/
export const themeManager = new ThemeManager();
/**
* ThemeManager class exported for testing purposes
* Use the singleton `themeManager` in application code.
*/
export { ThemeManager };