use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::{BsMoonStars, BsSun}; use dioxus_free_icons::Icon; #[cfg(feature = "web")] const STORAGE_KEY: &str = "compliance-scanner.theme"; /// Sidebar-footer theme toggle. Reads the initial state on mount from /// localStorage (explicit user choice) or `prefers-color-scheme` (OS default), /// then writes back to both the `` attribute and /// localStorage on every click. #[component] pub fn ThemeToggle(collapsed: bool) -> Element { // `None` until the on-mount effect resolves the real value, so SSR doesn't // render the wrong icon for the user's actual theme. let mut is_dark = use_signal(|| None::); use_effect(move || { let (dark, from_storage) = initial_theme(); is_dark.set(Some(dark)); // If the user already made an explicit choice (in localStorage), assert it // on the DOM so an OS-vs-stored mismatch can't briefly show the wrong theme. if from_storage { apply_theme(dark); } }); let label = if collapsed { "" } else if is_dark().unwrap_or(true) { "Light mode" } else { "Dark mode" }; let title = if is_dark().unwrap_or(true) { "Switch to light mode" } else { "Switch to dark mode" }; rsx! { button { class: "theme-toggle", r#type: "button", title: "{title}", "aria-label": "{title}", onclick: move |_| { let next_dark = !is_dark().unwrap_or(true); is_dark.set(Some(next_dark)); apply_theme(next_dark); }, if is_dark().unwrap_or(true) { Icon { icon: BsSun, width: 16, height: 16 } } else { Icon { icon: BsMoonStars, width: 16, height: 16 } } if !collapsed { span { class: "theme-toggle-label", "{label}" } } } } } /// Returns `(is_dark, from_storage)`. `from_storage` is true when an explicit /// user choice is in localStorage; false when we fell back to OS preference /// (or to the dark default). #[cfg(feature = "web")] fn initial_theme() -> (bool, bool) { if let Some(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { if let Ok(Some(value)) = storage.get_item(STORAGE_KEY) { return (value == "dark", true); } } if let Ok(Some(mql)) = window.match_media("(prefers-color-scheme: dark)") { return (mql.matches(), false); } } (true, false) } #[cfg(not(feature = "web"))] fn initial_theme() -> (bool, bool) { (true, false) } #[cfg(feature = "web")] fn apply_theme(dark: bool) { let theme = if dark { "dark" } else { "light" }; if let Some(window) = web_sys::window() { if let Some(document) = window.document() { if let Some(root) = document.document_element() { let _ = root.set_attribute("data-theme", theme); } } if let Ok(Some(storage)) = window.local_storage() { let _ = storage.set_item(STORAGE_KEY, theme); } } } #[cfg(not(feature = "web"))] fn apply_theme(_dark: bool) {}