//! Tri-state color theme: auto (follow the OS), light, dark. //! //! The choice is persisted in localStorage and applied as a class on //! ``; `app.css` resolves the palette variables accordingly. //! "Auto" means no class at all, so the stylesheet falls back to //! `prefers-color-scheme`. use wasm_bindgen::JsCast; use crate::icons::IconName; const STORAGE_KEY: &str = "fb-theme"; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Theme { /// Follow the OS `prefers-color-scheme` (the default). Auto, Light, Dark, } impl Theme { /// The i18n key of this state's display name. pub const fn name_key(self) -> &'static str { match self { Theme::Auto => crate::i18n::k::THEME_AUTO, Theme::Light => crate::i18n::k::THEME_LIGHT, Theme::Dark => crate::i18n::k::THEME_DARK, } } /// The next state in the toggle cycle: auto → light → dark → auto. pub const fn next(self) -> Theme { match self { Theme::Auto => Theme::Light, Theme::Light => Theme::Dark, Theme::Dark => Theme::Auto, } } /// Lowercase name, used in tooltips and as the localStorage value. pub const fn label(self) -> &'static str { match self { Theme::Auto => "auto", Theme::Light => "light", Theme::Dark => "dark", } } /// The glyph the toggle button shows for this state. pub const fn icon(self) -> IconName { match self { Theme::Auto => IconName::ThemeAuto, Theme::Light => IconName::ThemeLight, Theme::Dark => IconName::ThemeDark, } } /// Persist the choice. Auto stores nothing (like most sites: "no entry" /// means "follow the OS"), so clearing the key returns to auto. pub fn store(self) { let Some(store) = local_storage() else { return; }; match self { Theme::Auto => { let _ = store.remove_item(STORAGE_KEY); } t => { let _ = store.set_item(STORAGE_KEY, t.label()); } } } /// The persisted choice; anything missing or invalid is /// [`Theme::Auto`]. pub fn load() -> Theme { let value = local_storage().and_then(|s| s.get_item(STORAGE_KEY).ok().flatten()); match value.as_deref() { Some("light") => Theme::Light, Some("dark") => Theme::Dark, _ => Theme::Auto, } } /// Apply to ``: auto clears the class (the stylesheet falls back /// to `prefers-color-scheme`), light/dark force a palette. pub fn apply(self) { let Some(html) = web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.document_element()) .and_then(|e| e.dyn_into::().ok()) else { return; }; let classes = match self { Theme::Auto => "", t => t.label(), }; html.set_class_name(classes); } } /// localStorage, when the browser exposes it (private mode can deny it). fn local_storage() -> Option { web_sys::window()?.local_storage().ok()? }