Skip to main content

dioxus_docs_kit/components/blog/
blog_layout.rs

1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::{LdMenu, LdSearch};
4
5use crate::blog::registry::BlogRegistry;
6use crate::components::docs_layout::{CurrentTheme, DrawerOpen};
7
8use super::mobile_drawer::BlogMobileDrawer;
9use super::search_modal::BlogSearchModal;
10use super::theme_toggle::BlogThemeToggle;
11
12/// Blog layout shell.
13///
14/// # Context requirements
15///
16/// - `&'static BlogRegistry` — provided by consumer
17/// - `BlogContext` — provided by consumer
18#[component]
19pub fn BlogLayout(
20    header: Option<Element>,
21    #[props(default = true)] show_header: bool,
22    children: Element,
23) -> Element {
24    let registry = use_context::<&'static BlogRegistry>();
25
26    let parent_search: Option<Signal<bool>> = try_use_context();
27    let parent_drawer: Option<DrawerOpen> = try_use_context();
28
29    let local_search = use_signal(|| false);
30    let local_drawer = use_signal(|| false);
31
32    let mut search_open = parent_search.unwrap_or(local_search);
33    let mut drawer_open = parent_drawer.map(|d| d.0).unwrap_or(local_drawer);
34
35    use_context_provider(|| search_open);
36    use_context_provider(|| DrawerOpen(drawer_open));
37
38    // Theme state
39    let theme_default = registry
40        .theme
41        .as_ref()
42        .map(|t| t.default_theme.clone())
43        .unwrap_or_default();
44    let theme_storage_key = registry
45        .theme
46        .as_ref()
47        .map(|t| t.storage_key.clone())
48        .unwrap_or_default();
49    let has_theme = registry.theme.is_some();
50
51    let mut current_theme = use_signal(|| theme_default.clone());
52    use_context_provider(|| CurrentTheme(current_theme));
53
54    use_effect(move || {
55        if !has_theme {
56            return;
57        }
58        let key = theme_storage_key.clone();
59        let fallback = theme_default.clone();
60        spawn(async move {
61            let mut eval = document::eval(&format!(
62                r#"
63                let theme = null;
64                try {{ theme = localStorage.getItem('{key}'); }} catch(e) {{}}
65                theme = theme || '{fallback}';
66                document.documentElement.setAttribute('data-theme', theme);
67                dioxus.send(theme);
68                "#
69            ));
70            if let Ok(stored) = eval.recv::<String>().await {
71                current_theme.set(stored);
72            }
73        });
74    });
75
76    // Keyboard shortcut: Cmd/Ctrl+K
77    use_effect(move || {
78        spawn(async move {
79            let mut eval = document::eval(
80                r#"
81                document.addEventListener('keydown', (e) => {
82                    if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
83                        e.preventDefault();
84                        dioxus.send(true);
85                    }
86                });
87                while (true) { await new Promise(r => setTimeout(r, 1000000)); }
88                "#,
89            );
90            loop {
91                if (eval.recv::<bool>().await).is_ok() {
92                    search_open.toggle();
93                }
94            }
95        });
96    });
97
98    rsx! {
99        div { class: "min-h-screen bg-base-100",
100            if show_header {
101                div { class: "sticky top-0 z-50",
102                    if let Some(hdr) = header {
103                        {hdr}
104                    } else {
105                        div { class: "navbar bg-base-200 border-b border-base-300 px-4 lg:px-8",
106                            div { class: "flex-1 gap-2",
107                                button {
108                                    class: "btn btn-ghost btn-sm btn-square lg:hidden",
109                                    onclick: move |_| drawer_open.toggle(),
110                                    Icon { class: "size-5", icon: LdMenu }
111                                }
112                            }
113                            div { class: "flex-none gap-1",
114                                BlogSearchButton { search_open }
115                                BlogThemeToggle {}
116                            }
117                        }
118                    }
119                }
120            }
121
122            div { class: "flex-1 min-w-0",
123                {children}
124            }
125        }
126
127        BlogMobileDrawer { open: drawer_open }
128        BlogSearchModal {}
129    }
130}
131
132/// Reusable search button for blog headers.
133#[component]
134pub fn BlogSearchButton(search_open: Signal<bool>) -> Element {
135    rsx! {
136        button {
137            class: "btn btn-ghost btn-sm gap-2",
138            onclick: move |_| search_open.set(true),
139            Icon { class: "size-4", icon: LdSearch }
140            span { class: "hidden sm:inline text-base-content/60 text-sm", "Search" }
141            kbd { class: "kbd kbd-xs hidden sm:inline-flex", "\u{2318}K" }
142        }
143    }
144}