darkmode #41

Merged
schn33fuchs merged 2 commits from darkmode into styles-nino 2026-05-28 17:29:23 +02:00
8 changed files with 175 additions and 9 deletions

View File

@@ -16,7 +16,8 @@ serde = { workspace = true }
wasm-bindgen-futures = "0.4.70" wasm-bindgen-futures = "0.4.70"
web-sys = { version = "0.3.95", features = [ web-sys = { version = "0.3.95", features = [
"Window","Document","Request","Response","Headers", "HtmlSelectElement", "RequestCredentials", "Window","Document","Request","Response","Headers", "HtmlSelectElement", "RequestCredentials",
"SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement", "MouseEvent" "SubmitEvent","InputEvent","HtmlInputElement","Event", "HtmlFormElement", "MouseEvent",
"Element", "MediaQueryList", "DomTokenList"
] } ] }
gloo-net = "0.7.0" gloo-net = "0.7.0"
gloo-storage = "0.4.0" gloo-storage = "0.4.0"

65
frontend/src/dark_mode.rs Normal file
View File

@@ -0,0 +1,65 @@
use gloo_storage::{LocalStorage, Storage};
use web_sys::window;
use yew::prelude::*;
#[function_component(DarkModeToggle)]
pub fn dark_mode_toggle() -> Html {
// 1. Initialize state from LocalStorage or system preference
let is_dark = use_state(|| {
LocalStorage::get::<bool>("dark-mode").unwrap_or_else(|_| {
// Fallback to system preference if not set
window()
.and_then(|w| w.match_media("(prefers-color-scheme: dark)").ok().flatten())
.map(|m| m.matches())
.unwrap_or(false)
})
});
// 2. Synchronize the class on the body when the state changes
{
let is_dark = is_dark.clone();
use_effect_with(is_dark, |is_dark| {
if let Some(win) = window() {
if let Some(doc) = win.document() {
if let Some(body) = doc.body() {
if **is_dark {
let _ = body.class_list().add_1("theme-dark");
let _ = body.class_list().remove_1("theme-light");
let _ = LocalStorage::set("dark-mode", true);
} else {
let _ = body.class_list().add_1("theme-light");
let _ = body.class_list().remove_1("theme-dark");
let _ = LocalStorage::set("dark-mode", false);
}
}
}
}
|| ()
});
}
// 3. Toggle action
let onclick = {
let is_dark = is_dark.clone();
Callback::from(move |_| {
is_dark.set(!*is_dark);
})
};
html! {
<button class="dark-mode-toggle" {onclick}>
if *is_dark {
// Sun Icon for switching to light mode
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
</svg>
} else {
// Moon Icon for switching to dark mode
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
</svg>
}
</button>
}
}

View File

@@ -1,5 +1,6 @@
mod auth; mod auth;
mod pages; mod pages;
mod dark_mode;
use crate::auth::ProtectedRoute; use crate::auth::ProtectedRoute;
use crate::pages::*; use crate::pages::*;
use gloo_net::http::Request; use gloo_net::http::Request;
@@ -271,6 +272,7 @@ fn switch(route: Route) -> Html {
pub fn app() -> Html { pub fn app() -> Html {
html! { html! {
<BrowserRouter> <BrowserRouter>
<dark_mode::DarkModeToggle />
<Switch<Route> render={switch} /> <Switch<Route> render={switch} />
</BrowserRouter> </BrowserRouter>
} }

View File

@@ -386,7 +386,12 @@ pub fn sidebar() -> Html {
<SidebarStateProvider> <SidebarStateProvider>
<nav class="sidebar user"> <nav class="sidebar user">
<ul> <ul>
<Link<crate::Route> to={crate::Route::Home}>{ "󰟒" }</Link<crate::Route>> <Link<crate::Route> to={crate::Route::Home} classes="home">
<svg xmlns="http://www.w3.org/2000/svg" class="home-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</Link<crate::Route>>
<TicketMenu/> <TicketMenu/>
<li class="logout-item"> <li class="logout-item">
<button <button
@@ -406,7 +411,12 @@ pub fn sidebar() -> Html {
<SidebarStateProvider> <SidebarStateProvider>
<nav class="sidebar admin"> <nav class="sidebar admin">
<ul> <ul>
<Link<crate::Route> to={crate::Route::Home}>{ "󰟒" }</Link<crate::Route>> <Link<crate::Route> to={crate::Route::Home} classes="home">
<svg xmlns="http://www.w3.org/2000/svg" class="home-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</Link<crate::Route>>
<TicketMenu/> <TicketMenu/>
<UsersMenu/> <UsersMenu/>
<Link<crate::Route> to={crate::Route::Diagnostics}>{ "Statistiken" }</Link<crate::Route>> <Link<crate::Route> to={crate::Route::Diagnostics}>{ "Statistiken" }</Link<crate::Route>>

View File

@@ -1,15 +1,15 @@
// Color Palette (Reference Style) // Color Palette (Reference Style)
$color-bg: #f0f2f5; $color-bg: var(--color-bg);
$color-bg-dark: #121212; $color-bg-dark: var(--color-bg-dark);
$color-container: #ffffff; $color-container: var(--color-container);
$color-container-dark: #333333; $color-container-dark: var(--color-container-dark);
$color-sidebar: #0f172a; $color-sidebar: #0f172a;
$color-primary: #2b79c2; $color-primary: #2b79c2;
$color-primary-hover: #1d5fa0; $color-primary-hover: #1d5fa0;
$color-accent: #2b79c2; $color-accent: #2b79c2;
$color-muted: #6b7280; $color-muted: #6b7280;
$color-text: #111827; $color-text: var(--color-text);
$color-text-dark: #e2e2e2; $color-text-dark: var(--color-text-dark);
// Status Colors // Status Colors
$color-status-todo: #ffcccc; $color-status-todo: #ffcccc;

View File

@@ -0,0 +1,75 @@
:root {
--color-bg: #f0f2f5;
--color-bg-dark: #121212;
--color-container: #ffffff;
--color-container-dark: #333333;
--color-text: #111827;
--color-text-dark: #e2e2e2;
}
// Automatically respect standard media query if no override is set
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #121212;
--color-container: #333333;
--color-text: #e2e2e2;
}
}
// Force Light Mode overrides
body.theme-light {
--color-bg: #f0f2f5;
--color-bg-dark: #f0f2f5;
--color-container: #ffffff;
--color-container-dark: #ffffff;
--color-text: #111827;
--color-text-dark: #111827;
}
// Force Dark Mode overrides
body.theme-dark {
--color-bg: #121212;
--color-bg-dark: #121212;
--color-container: #333333;
--color-container-dark: #333333;
--color-text: #e2e2e2;
--color-text-dark: #e2e2e2;
}
// Fixed positioning ensures NO other component's layout is ever altered
.dark-mode-toggle {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
width: 55px;
height: 55px;
border-radius: 50%;
border: 1px solid rgba(128, 128, 128, 0.2);
background-color: var(--color-container);
color: var(--color-text);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
svg {
width: 36px;
height: 36px;
stroke: var(--color-text);
transition: stroke 0.2s ease-in-out;
}
&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
background-color: var(--color-bg);
}
&:active {
transform: translateY(0);
}
}

View File

@@ -49,6 +49,18 @@
text-align: left; text-align: left;
} }
.home {
display: flex;
justify-content: center;
}
.home-svg {
width: 24px;
height: 24px;
vertical-align: middle;
margin-right: 8px;
}
&.user { width: 220px; background: darken($color-sidebar, 6%); } &.user { width: 220px; background: darken($color-sidebar, 6%); }
} }

View File

@@ -10,6 +10,7 @@
@use "components/diagnostics"; @use "components/diagnostics";
@use "components/pages"; @use "components/pages";
@use "components/setup"; @use "components/setup";
@use "components/dark_mode";
* { * {
box-sizing: border-box; box-sizing: border-box;