105 lines
3.2 KiB
Rust
105 lines
3.2 KiB
Rust
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 `<html data-theme="...">` 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::<bool>);
|
|
|
|
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) {}
|