theme.rs
⎇
Raw
1//! Tri-state color theme: auto (follow the OS), light, dark.
2//!
3//! The choice is persisted in localStorage and applied as a class on
4//! `<html>`; `app.css` resolves the palette variables accordingly.
5//! "Auto" means no class at all, so the stylesheet falls back to
6//! `prefers-color-scheme`.
7
8use wasm_bindgen::JsCast;
9
10use crate::icons::IconName;
11
12const STORAGE_KEY: &str = "fb-theme";
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub enum Theme {
16 /// Follow the OS `prefers-color-scheme` (the default).
17 Auto,
18 Light,
19 Dark,
20}
21
22impl Theme {
23 /// The i18n key of this state's display name.
24 pub const fn name_key(self) -> &'static str {
25 match self {
26 Theme::Auto => crate::i18n::k::THEME_AUTO,
27 Theme::Light => crate::i18n::k::THEME_LIGHT,
28 Theme::Dark => crate::i18n::k::THEME_DARK,
29 }
30 }
31
32 /// The next state in the toggle cycle: auto → light → dark → auto.
33 pub const fn next(self) -> Theme {
34 match self {
35 Theme::Auto => Theme::Light,
36 Theme::Light => Theme::Dark,
37 Theme::Dark => Theme::Auto,
38 }
39 }
40
41 /// Lowercase name, used in tooltips and as the localStorage value.
42 pub const fn label(self) -> &'static str {
43 match self {
44 Theme::Auto => "auto",
45 Theme::Light => "light",
46 Theme::Dark => "dark",
47 }
48 }
49
50 /// The glyph the toggle button shows for this state.
51 pub const fn icon(self) -> IconName {
52 match self {
53 Theme::Auto => IconName::ThemeAuto,
54 Theme::Light => IconName::ThemeLight,
55 Theme::Dark => IconName::ThemeDark,
56 }
57 }
58
59 /// Persist the choice. Auto stores nothing (like most sites: "no entry"
60 /// means "follow the OS"), so clearing the key returns to auto.
61 pub fn store(self) {
62 let Some(store) = local_storage() else {
63 return;
64 };
65 match self {
66 Theme::Auto => {
67 let _ = store.remove_item(STORAGE_KEY);
68 }
69 t => {
70 let _ = store.set_item(STORAGE_KEY, t.label());
71 }
72 }
73 }
74
75 /// The persisted choice; anything missing or invalid is
76 /// [`Theme::Auto`].
77 pub fn load() -> Theme {
78 let value = local_storage().and_then(|s| s.get_item(STORAGE_KEY).ok().flatten());
79 match value.as_deref() {
80 Some("light") => Theme::Light,
81 Some("dark") => Theme::Dark,
82 _ => Theme::Auto,
83 }
84 }
85
86 /// Apply to `<html>`: auto clears the class (the stylesheet falls back
87 /// to `prefers-color-scheme`), light/dark force a palette.
88 pub fn apply(self) {
89 let Some(html) = web_sys::window()
90 .and_then(|w| w.document())
91 .and_then(|d| d.document_element())
92 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok())
93 else {
94 return;
95 };
96 let classes = match self {
97 Theme::Auto => "",
98 t => t.label(),
99 };
100 html.set_class_name(classes);
101 }
102}
103
104/// localStorage, when the browser exposes it (private mode can deny it).
105fn local_storage() -> Option<web_sys::Storage> {
106 web_sys::window()?.local_storage().ok()?
107}
108