feat(dashboard): add light/dark theme with sidebar toggle (#81)
This commit was merged in pull request #81.
This commit is contained in:
@@ -12,4 +12,5 @@ pub mod pentest_wizard;
|
||||
pub mod severity_badge;
|
||||
pub mod sidebar;
|
||||
pub mod stat_card;
|
||||
pub mod theme_toggle;
|
||||
pub mod toast;
|
||||
|
||||
@@ -4,6 +4,7 @@ use dioxus_free_icons::icons::bs_icons::*;
|
||||
use dioxus_free_icons::Icon;
|
||||
|
||||
use crate::app::Route;
|
||||
use crate::components::theme_toggle::ThemeToggle;
|
||||
|
||||
struct NavItem {
|
||||
label: &'static str,
|
||||
@@ -106,6 +107,7 @@ pub fn Sidebar() -> Element {
|
||||
}
|
||||
// Spacer pushes footer to the bottom
|
||||
div { class: "sidebar-spacer" }
|
||||
ThemeToggle { collapsed: collapsed() }
|
||||
button {
|
||||
class: "sidebar-toggle",
|
||||
onclick: move |_| collapsed.set(!collapsed()),
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
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) {}
|
||||
Reference in New Issue
Block a user