tablo_ui/components/composites/theme.rs
1use std::fmt::Write as _;
2
3use topcoat::{
4 Result,
5 view::{View, component, view},
6};
7
8/// Reconciles the `dark` class on `<html>` before first paint.
9///
10/// A stored `light` removes a server-rendered `dark` class.
11///
12/// Uses nested `if`s instead of `&&`, which HTML escaping would rewrite into a syntax error inside
13/// `<script>`.
14fn theme_init_script_body(default_dark: bool) -> String {
15 let mut script = String::from(
16 "(function(){var t=null;try{t=localStorage.getItem('theme')}catch(e){}\
17if(!t){var m=document.cookie.match(/theme=([^;]+)/);if(m)t=m[1]}\
18if(t!=='light'){if(t!=='dark')t=",
19 );
20 let _ = write!(script, "'{}'", if default_dark { "dark" } else { "light" });
21 script.push_str(
22 ";}if(t==='dark')document.documentElement.classList.add('dark');\
23else document.documentElement.classList.remove('dark')})();",
24 );
25 script
26}
27
28/// Renders in the `<head>` of every layout, before the stylesheet link.
29///
30/// `default_dark` is the server's build-time preference, used only when the visitor has no stored
31/// choice.
32///
33/// ```rust
34/// # use tablo_ui::theme_init_script;
35/// # use topcoat::{context::Cx, view::{BoxView, ViewExt, view}};
36/// # fn head<'a>(cx: &'a Cx, default_dark: bool, stylesheet: &'a str) -> BoxView<'a> {
37/// # view! { cx =>
38/// <head>
39/// theme_init_script(default_dark: default_dark)
40/// <link rel="stylesheet" href=(stylesheet)>
41/// </head>
42/// # }
43/// # .boxed()
44/// # }
45/// ```
46#[component]
47pub async fn theme_init_script(default_dark: bool) -> Result<impl View> {
48 let body = theme_init_script_body(default_dark);
49 Ok(view! { <script>(body)</script> })
50}
51
52#[cfg(test)]
53mod tests;