dioxus_docs_kit/components/
docs_layout.rs1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::LdMenu;
4
5use crate::DocsContext;
6use crate::registry::DocsRegistry;
7use dioxus_mdx::syntax_highlight_css;
8
9#[derive(Clone, Debug)]
15pub struct LayoutOffsets {
16 pub sticky_top: &'static str,
18 pub scroll_mt: &'static str,
20 pub sidebar_height: &'static str,
22}
23
24#[derive(Clone, Copy)]
25pub struct CurrentTheme(pub Signal<String>);
26
27#[derive(Clone, Copy)]
33pub struct DrawerOpen(pub Signal<bool>);
34
35use super::mobile_drawer::MobileDrawer;
36use super::search_modal::SearchModal;
37use super::sidebar::DocsSidebar;
38use super::theme_toggle::ThemeToggle;
39
40#[component]
59pub fn DocsLayout(
60 header: Option<Element>,
61 #[props(default = true)] show_header: bool,
62 children: Element,
63) -> Element {
64 let registry = use_context::<&'static DocsRegistry>();
65 let ctx = use_context::<DocsContext>();
66 let nav = ®istry.nav;
67
68 let parent_search: Option<Signal<bool>> = try_use_context();
70 let parent_drawer: Option<DrawerOpen> = try_use_context();
71
72 let local_search = use_signal(|| false);
74 let local_drawer = use_signal(|| false);
75
76 let mut search_open = parent_search.unwrap_or(local_search);
78 let mut drawer_open = parent_drawer.map(|d| d.0).unwrap_or(local_drawer);
79
80 use_context_provider(|| search_open);
82 use_context_provider(|| DrawerOpen(drawer_open));
83
84 let theme_default = registry
86 .theme
87 .as_ref()
88 .map(|t| t.default_theme.clone())
89 .unwrap_or_default();
90 let theme_storage_key = registry
91 .theme
92 .as_ref()
93 .map(|t| t.storage_key.clone())
94 .unwrap_or_default();
95 let has_theme = registry.theme.is_some();
96
97 let mut current_theme = use_signal(|| theme_default.clone());
98 use_context_provider(|| CurrentTheme(current_theme));
99
100 use_effect(move || {
102 if !has_theme {
103 return;
104 }
105 let key = theme_storage_key.clone();
106 let fallback = theme_default.clone();
107 spawn(async move {
108 let mut eval = document::eval(&format!(
109 r#"
110 let theme = null;
111 try {{ theme = localStorage.getItem('{key}'); }} catch(e) {{}}
112 theme = theme || '{fallback}';
113 document.documentElement.setAttribute('data-theme', theme);
114 dioxus.send(theme);
115 "#
116 ));
117 if let Ok(stored) = eval.recv::<String>().await {
118 current_theme.set(stored);
119 }
120 });
121 });
122
123 let mut active_tab = use_signal(|| nav.tabs.first().cloned().unwrap_or_default());
125 use_context_provider(|| active_tab);
126
127 let current_path = ctx.current_path;
129 let registry_for_effect = registry;
130 use_effect(move || {
131 let path = current_path();
132 if let Some(tab) = registry_for_effect.tab_for_path(&path) {
133 active_tab.set(tab);
134 }
135 });
136
137 use_effect(move || {
139 spawn(async move {
140 let mut eval = document::eval(
141 r#"
142 document.addEventListener('keydown', (e) => {
143 if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
144 e.preventDefault();
145 dioxus.send(true);
146 }
147 });
148 while (true) { await new Promise(r => setTimeout(r, 1000000)); }
149 "#,
150 );
151 loop {
152 if (eval.recv::<bool>().await).is_ok() {
153 search_open.toggle();
154 }
155 }
156 });
157 });
158
159 let has_tabs = nav.has_tabs();
160 let offsets = if !show_header {
161 LayoutOffsets {
162 sticky_top: "top-0",
163 scroll_mt: "scroll-mt-0",
164 sidebar_height: "h-screen",
165 }
166 } else if has_tabs {
167 LayoutOffsets {
168 sticky_top: "top-[6.5rem]",
169 scroll_mt: "scroll-mt-[6.5rem]",
170 sidebar_height: "h-[calc(100vh-6.5rem)]",
171 }
172 } else {
173 LayoutOffsets {
174 sticky_top: "top-16",
175 scroll_mt: "scroll-mt-16",
176 sidebar_height: "h-[calc(100vh-4rem)]",
177 }
178 };
179 use_context_provider(|| offsets.clone());
180
181 rsx! {
182 SyntaxStyles {}
184
185 div { class: "min-h-screen bg-base-100",
186 if show_header {
188 div { class: "sticky top-0 z-50",
189 if let Some(hdr) = header {
191 {hdr}
192 } else {
193 div { class: "navbar bg-base-200 border-b border-base-300 px-4 lg:px-8",
195 div { class: "flex-1 gap-2",
196 button {
197 class: "btn btn-ghost btn-sm btn-square lg:hidden",
198 onclick: move |_| drawer_open.toggle(),
199 Icon { class: "size-5", icon: LdMenu }
200 }
201 }
202 div { class: "flex-none gap-1",
203 SearchButton { search_open }
204 ThemeToggle {}
205 }
206 }
207 }
208
209 if has_tabs {
211 div { class: "bg-base-200/80 backdrop-blur border-b border-base-300 px-4 lg:px-8",
212 div { class: "flex gap-6",
213 for tab in nav.tabs.iter() {
214 {
215 let is_active = *tab == active_tab();
216 let tab_clone = tab.clone();
217 let style = if is_active {
218 "text-primary border-b-2 border-primary font-medium"
219 } else {
220 "text-base-content/60 hover:text-base-content border-b-2 border-transparent"
221 };
222 rsx! {
223 button {
224 class: "px-1 py-2.5 text-sm transition-colors -mb-px {style}",
225 onclick: move |_| {
226 active_tab.set(tab_clone.clone());
227 let groups = nav.groups_for_tab(&tab_clone);
228 if let Some(first_page) = groups.first().and_then(|g| g.pages.first()) {
229 (ctx.navigate)(first_page.clone());
230 }
231 },
232 "{tab}"
233 }
234 }
235 }
236 }
237 }
238 }
239 }
240 }
241 }
242
243 div { class: "flex",
245 aside { class: "w-64 shrink-0 border-r border-base-300 bg-base-200/30 hidden lg:block",
247 div { class: "sticky {offsets.sticky_top} {offsets.sidebar_height} overflow-y-auto p-6",
248 DocsSidebar {}
249 }
250 }
251
252 div { class: "flex-1 min-w-0",
254 {children}
255 }
256 }
257 }
258
259 MobileDrawer { open: drawer_open }
261 SearchModal {}
262 }
263}
264
265#[component]
270fn SyntaxStyles() -> Element {
271 use std::sync::atomic::{AtomicBool, Ordering};
272 static INJECTED: AtomicBool = AtomicBool::new(false);
273
274 if INJECTED.swap(true, Ordering::Relaxed) {
275 return rsx! {};
276 }
277
278 let css = syntax_highlight_css();
279 rsx! { document::Style { {css} } }
280}
281
282#[component]
284pub fn SearchButton(search_open: Signal<bool>) -> Element {
285 use dioxus_free_icons::icons::ld_icons::LdSearch;
286
287 rsx! {
288 button {
289 class: "btn btn-ghost btn-sm gap-2",
290 onclick: move |_| search_open.set(true),
291 Icon { class: "size-4", icon: LdSearch }
292 span { class: "hidden sm:inline text-base-content/60 text-sm", "Search" }
293 kbd { class: "kbd kbd-xs hidden sm:inline-flex", "\u{2318}K" }
294 }
295 }
296}