Skip to main content

dioxus_docs_kit/components/
shared.rs

1//! Hooks and components shared between the docs and blog surfaces.
2
3use dioxus::prelude::*;
4use dioxus_free_icons::Icon;
5use dioxus_free_icons::icons::ld_icons::{LdMoon, LdSun};
6
7use super::docs_layout::CurrentTheme;
8use crate::config::ThemeConfig;
9
10/// Provide the [`CurrentTheme`] context and apply the persisted theme on mount.
11///
12/// Reads the stored preference from localStorage (falling back to the config's
13/// default theme), sets `data-theme` on `<html>`, and keeps the returned signal
14/// in sync. Called by `DocsLayout`/`BlogLayout`; call it yourself when you
15/// render kit components (e.g. [`ThemeToggle`](super::ThemeToggle)) outside
16/// those layouts, e.g. in a landing-page navbar.
17pub fn use_theme_provider(theme: Option<ThemeConfig>) -> Signal<String> {
18    let theme_default = theme
19        .as_ref()
20        .map(|t| t.default_theme.clone())
21        .unwrap_or_default();
22    let storage_key = theme
23        .as_ref()
24        .map(|t| t.storage_key.clone())
25        .unwrap_or_default();
26    let has_theme = theme.is_some();
27
28    let mut current_theme = use_signal(|| theme_default.clone());
29    use_context_provider(|| CurrentTheme(current_theme));
30
31    // On mount: read stored preference and apply data-theme
32    use_effect(move || {
33        if !has_theme {
34            return;
35        }
36        let key = storage_key.clone();
37        let fallback = theme_default.clone();
38        spawn(async move {
39            let mut eval = document::eval(&format!(
40                r#"
41                let theme = null;
42                try {{ theme = localStorage.getItem('{key}'); }} catch(e) {{}}
43                theme = theme || '{fallback}';
44                document.documentElement.setAttribute('data-theme', theme);
45                dioxus.send(theme);
46                "#
47            ));
48            if let Ok(stored) = eval.recv::<String>().await {
49                current_theme.set(stored);
50            }
51        });
52    });
53
54    current_theme
55}
56
57/// Register a document-level Cmd/Ctrl+K listener that toggles `search_open`.
58///
59/// The handler is stored on `window` and any previous one is removed before
60/// registering, so layout remounts never accumulate listeners.
61pub(crate) fn use_search_hotkey(mut search_open: Signal<bool>) {
62    let owner = dioxus::core::current_scope_id().0;
63    use_effect(move || {
64        spawn(async move {
65            let mut eval = document::eval(&format!(
66                r#"
67                window.__dkSearchCleanup?.stop();
68                const handler = (e) => {{
69                    if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') {{
70                        e.preventDefault();
71                        dioxus.send(true);
72                    }}
73                }};
74                window.__dkSearchHotkey = handler;
75                document.addEventListener('keydown', handler);
76                await new Promise(resolve => {{
77                    window.__dkSearchCleanup = {{ owner: {owner}, stop: () => {{
78                        document.removeEventListener('keydown', handler);
79                        if (window.__dkSearchHotkey === handler) {{
80                            delete window.__dkSearchHotkey;
81                            delete window.__dkSearchCleanup;
82                        }}
83                        resolve();
84                    }} }};
85                }});
86                "#,
87            ));
88            // A disposed evaluator returns Err immediately. Retrying forever
89            // would spin without yielding when the docs/blog layout unmounts.
90            while eval.recv::<bool>().await.is_ok() {
91                search_open.toggle();
92            }
93        });
94    });
95    use_drop(move || {
96        let _ = document::eval(&format!(
97            "if (window.__dkSearchCleanup?.owner === {owner}) window.__dkSearchCleanup.stop();"
98        ));
99    });
100}
101
102/// Light/dark theme toggle button shared by [`ThemeToggle`](super::ThemeToggle)
103/// and [`BlogThemeToggle`](super::blog::BlogThemeToggle).
104///
105/// Renders nothing if `theme` has no `toggle_themes` configured.
106#[component]
107pub(crate) fn ThemeToggleButton(theme: Option<ThemeConfig>) -> Element {
108    let toggle = match theme.as_ref().and_then(|t| t.toggle_themes.as_ref()) {
109        Some(t) => t.clone(),
110        None => return rsx! {},
111    };
112
113    let storage_key = theme
114        .as_ref()
115        .map(|t| t.storage_key.clone())
116        .unwrap_or_default();
117
118    let CurrentTheme(mut current_theme) = use_context::<CurrentTheme>();
119
120    let (light, dark) = toggle;
121    let is_dark = current_theme() == dark;
122
123    rsx! {
124        button {
125            class: "btn btn-ghost btn-sm btn-square",
126            title: if is_dark { "Switch to light mode" } else { "Switch to dark mode" },
127            onclick: move |_| {
128                let new_theme = if (current_theme)() == dark { light.clone() } else { dark.clone() };
129                current_theme.set(new_theme.clone());
130                let key = storage_key.clone();
131                spawn(async move {
132                    let _ = document::eval(&format!(
133                        r#"document.documentElement.setAttribute('data-theme', '{new_theme}');
134                        try {{ localStorage.setItem('{key}', '{new_theme}'); }} catch(e) {{}}"#
135                    ));
136                });
137            },
138            if is_dark {
139                Icon { class: "size-5", icon: LdSun }
140            } else {
141                Icon { class: "size-5", icon: LdMoon }
142            }
143        }
144    }
145}