app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 42884064df5846d1ba214c3e5c6608ee62b20ef9
parent b34e8f4d84f7424116d9348af3889bbd26253f0b
Author: triesap <triesap@radroots.dev>
Date:   Thu, 22 Jan 2026 17:00:53 +0000

app: initialize daisyui theme

- add theme helper with os_light/os_dark mapping

- apply data-theme and color-scheme on startup

- expose theme helpers for settings integration

- add unit tests for theme parsing

Diffstat:
Mapp/src/entry.rs | 3++-
Mapp/src/lib.rs | 11+++++++++++
Aapp/src/theme.rs | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 174 insertions(+), 1 deletion(-)

diff --git a/app/src/entry.rs b/app/src/entry.rs @@ -1,10 +1,11 @@ use leptos::mount::mount_to_body; use wasm_bindgen::prelude::wasm_bindgen; -use crate::{app_logging_init, RadrootsApp}; +use crate::{app_logging_init, app_theme_init, RadrootsApp}; #[wasm_bindgen(start)] pub fn start() { let _ = app_logging_init(None); + let _ = app_theme_init(); mount_to_body(RadrootsApp); } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -13,6 +13,7 @@ mod logs; mod notifications; mod settings; mod setup; +mod theme; mod tangle; mod ui_demo; mod entry; @@ -65,6 +66,16 @@ pub use keystore::{ pub use logs::RadrootsAppLogsPage; pub use settings::RadrootsAppSettingsPage; pub use ui_demo::RadrootsAppUiDemoPage; +pub use theme::{ + app_theme_apply_mode, + app_theme_init, + app_theme_mode_from_value, + app_theme_mode_to_name, + RadrootsAppThemeError, + RadrootsAppThemeMode, + RadrootsAppThemeResult, + APP_THEME_STORAGE_KEY, +}; pub use logging::{ app_log_entry_error, app_log_entry_emit, diff --git a/app/src/theme.rs b/app/src/theme.rs @@ -0,0 +1,161 @@ +#![forbid(unsafe_code)] + +pub const APP_THEME_STORAGE_KEY: &str = "app:theme:mode"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppThemeMode { + System, + Light, + Dark, +} + +impl RadrootsAppThemeMode { + pub const fn as_str(self) -> &'static str { + match self { + RadrootsAppThemeMode::System => "system", + RadrootsAppThemeMode::Light => "light", + RadrootsAppThemeMode::Dark => "dark", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppThemeError { + Unavailable, + Storage, +} + +pub type RadrootsAppThemeResult<T> = Result<T, RadrootsAppThemeError>; + +impl RadrootsAppThemeError { + pub const fn message(&self) -> &'static str { + match self { + RadrootsAppThemeError::Unavailable => "error.app.theme.unavailable", + RadrootsAppThemeError::Storage => "error.app.theme.storage", + } + } +} + +impl std::fmt::Display for RadrootsAppThemeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message()) + } +} + +impl std::error::Error for RadrootsAppThemeError {} + +pub fn app_theme_mode_from_value(value: &str) -> Option<RadrootsAppThemeMode> { + match value { + "system" => Some(RadrootsAppThemeMode::System), + "light" => Some(RadrootsAppThemeMode::Light), + "dark" => Some(RadrootsAppThemeMode::Dark), + _ => None, + } +} + +pub fn app_theme_mode_to_name(mode: RadrootsAppThemeMode, prefers_dark: bool) -> &'static str { + match mode { + RadrootsAppThemeMode::System => { + if prefers_dark { "os_dark" } else { "os_light" } + } + RadrootsAppThemeMode::Light => "os_light", + RadrootsAppThemeMode::Dark => "os_dark", + } +} + +#[cfg(target_arch = "wasm32")] +fn app_theme_prefers_dark() -> bool { + let Some(window) = web_sys::window() else { + return false; + }; + match window.match_media("(prefers-color-scheme: dark)") { + Ok(Some(query)) => query.matches(), + _ => false, + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn app_theme_prefers_dark() -> bool { + false +} + +#[cfg(target_arch = "wasm32")] +fn app_theme_apply_name(name: &str) -> RadrootsAppThemeResult<()> { + let Some(window) = web_sys::window() else { + return Err(RadrootsAppThemeError::Unavailable); + }; + let Some(document) = window.document() else { + return Err(RadrootsAppThemeError::Unavailable); + }; + let Some(root) = document.document_element() else { + return Err(RadrootsAppThemeError::Unavailable); + }; + root.set_attribute("data-theme", name) + .map_err(|_| RadrootsAppThemeError::Unavailable)?; + let color_scheme = if name == "os_dark" { "dark" } else { "light" }; + let style = root.style(); + style + .set_property("color-scheme", color_scheme) + .map_err(|_| RadrootsAppThemeError::Unavailable)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn app_theme_apply_name(_name: &str) -> RadrootsAppThemeResult<()> { + Ok(()) +} + +#[cfg(target_arch = "wasm32")] +fn app_theme_read_storage() -> Option<String> { + let window = web_sys::window()?; + let storage = window.local_storage().ok()??; + storage.get_item(APP_THEME_STORAGE_KEY).ok().flatten() +} + +#[cfg(not(target_arch = "wasm32"))] +fn app_theme_read_storage() -> Option<String> { + None +} + +pub fn app_theme_init() -> RadrootsAppThemeResult<&'static str> { + let prefers_dark = app_theme_prefers_dark(); + let mode = app_theme_read_storage() + .as_deref() + .and_then(app_theme_mode_from_value) + .unwrap_or(RadrootsAppThemeMode::System); + let theme_name = app_theme_mode_to_name(mode, prefers_dark); + app_theme_apply_name(theme_name)?; + Ok(theme_name) +} + +pub fn app_theme_apply_mode(mode: RadrootsAppThemeMode) -> RadrootsAppThemeResult<&'static str> { + let prefers_dark = app_theme_prefers_dark(); + let name = app_theme_mode_to_name(mode, prefers_dark); + app_theme_apply_name(name)?; + Ok(name) +} + +#[cfg(test)] +mod tests { + use super::{ + app_theme_mode_from_value, + app_theme_mode_to_name, + RadrootsAppThemeMode, + }; + + #[test] + fn theme_mode_from_value_parses_known_values() { + assert_eq!(app_theme_mode_from_value("system"), Some(RadrootsAppThemeMode::System)); + assert_eq!(app_theme_mode_from_value("light"), Some(RadrootsAppThemeMode::Light)); + assert_eq!(app_theme_mode_from_value("dark"), Some(RadrootsAppThemeMode::Dark)); + assert_eq!(app_theme_mode_from_value("other"), None); + } + + #[test] + fn theme_mode_to_name_respects_preference() { + assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::System, true), "os_dark"); + assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::System, false), "os_light"); + assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::Light, true), "os_light"); + assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::Dark, false), "os_dark"); + } +}