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 next state in the toggle cycle: auto → light → dark → auto.
24 pub const fn next(self) -> Theme {
25 match self {
26 Theme::Auto => Theme::Light,
27 Theme::Light => Theme::Dark,
28 Theme::Dark => Theme::Auto,
29 }
30 }
31
32 /// Lowercase name, used in tooltips and as the localStorage value.
33 pub const fn label(self) -> &'static str {
34 match self {
35 Theme::Auto => "auto",
36 Theme::Light => "light",
37 Theme::Dark => "dark",
38 }
39 }
40
41 /// The glyph the toggle button shows for this state.
42 pub const fn icon(self) -> IconName {
43 match self {
44 Theme::Auto => IconName::ThemeAuto,
45 Theme::Light => IconName::ThemeLight,
46 Theme::Dark => IconName::ThemeDark,
47 }
48 }
49
50 /// Persist the choice. Auto stores nothing (like most sites: "no entry"
51 /// means "follow the OS"), so clearing the key returns to auto.
52 pub fn store(self) {
53 let Some(store) = local_storage() else {
54 return;
55 };
56 match self {
57 Theme::Auto => {
58 let _ = store.remove_item(STORAGE_KEY);
59 }
60 t => {
61 let _ = store.set_item(STORAGE_KEY, t.label());
62 }
63 }
64 }
65
66 /// The persisted choice; anything missing or invalid is
67 /// [`Theme::Auto`].
68 pub fn load() -> Theme {
69 let value = local_storage().and_then(|s| s.get_item(STORAGE_KEY).ok().flatten());
70 match value.as_deref() {
71 Some("light") => Theme::Light,
72 Some("dark") => Theme::Dark,
73 _ => Theme::Auto,
74 }
75 }
76
77 /// Apply to `<html>`: auto clears the class (the stylesheet falls back
78 /// to `prefers-color-scheme`), light/dark force a palette.
79 pub fn apply(self) {
80 let Some(html) = web_sys::window()
81 .and_then(|w| w.document())
82 .and_then(|d| d.document_element())
83 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok())
84 else {
85 return;
86 };
87 let classes = match self {
88 Theme::Auto => "",
89 t => t.label(),
90 };
91 html.set_class_name(classes);
92 }
93}
94
95/// localStorage, when the browser exposes it (private mode can deny it).
96fn local_storage() -> Option<web_sys::Storage> {
97 web_sys::window()?.local_storage().ok()?
98}
99