Skip to main content

dioxus_docs_kit/components/
docs_page.rs

1use dioxus::prelude::*;
2use dioxus_free_icons::{Icon, icons::ld_icons::*};
3use dioxus_mdx::{DocContent, DocTableOfContents, EndpointPage, extract_headers};
4
5use crate::DocsContext;
6use crate::registry::DocsRegistry;
7
8use super::docs_layout::LayoutOffsets;
9use super::page_nav::DocsPageNav;
10
11/// Documentation page content renderer.
12///
13/// Checks if the path is an API endpoint page or a regular MDX page and renders accordingly.
14#[component]
15pub fn DocsPageContent(path: String) -> Element {
16    let registry = use_context::<&'static DocsRegistry>();
17    let ctx = use_context::<DocsContext>();
18
19    // Check if this is an API endpoint page
20    if let Some(operation) = registry.get_api_operation(&path)
21        && let Some(spec) = registry.get_first_api_spec()
22    {
23        return rsx! {
24            div { class: "flex flex-col",
25                EndpointPage { operation: operation.clone(), spec: spec.clone() }
26                main { class: "px-8 lg:px-12 pb-12",
27                    div { class: "max-w-2xl",
28                        DocsPageNav { current_path: path.clone() }
29                    }
30                }
31            }
32        };
33    }
34
35    let offsets = try_use_context::<LayoutOffsets>().unwrap_or(LayoutOffsets {
36        sticky_top: "top-20",
37        scroll_mt: "scroll-mt-20",
38        sidebar_height: "h-[calc(100vh-5rem)]",
39    });
40
41    let doc = match registry.get_parsed_doc(&path) {
42        Some(d) => d,
43        None => {
44            let base = ctx.base_path.clone();
45            return rsx! {
46                div { class: "container mx-auto px-8 py-12 max-w-4xl",
47                    div { class: "text-center",
48                        h1 { class: "text-4xl font-bold mb-4", "404" }
49                        p { class: "text-base-content/70 mb-8",
50                            "Page not found: {path}"
51                        }
52                        Link {
53                            to: NavigationTarget::Internal(base),
54                            class: "btn btn-primary",
55                            "Go to Documentation"
56                        }
57                    }
58                }
59            };
60        }
61    };
62
63    let headers = extract_headers(&doc.raw_markdown);
64
65    rsx! {
66        div { class: "flex",
67            // Main content
68            main { class: "flex-1 min-w-0 px-8 py-12 lg:px-12",
69                article { class: "max-w-3xl mx-auto",
70                    // Page header
71                    header { class: "mb-8 pb-8 border-b border-base-300",
72                        div { class: "flex items-start justify-between gap-4",
73                            h1 { class: "text-4xl font-bold tracking-tight mb-3",
74                                "{doc.frontmatter.title}"
75                            }
76                            CopyMdxButton { content: doc.raw_markdown.clone() }
77                        }
78                        if let Some(desc) = &doc.frontmatter.description {
79                            p { class: "text-lg text-base-content/70",
80                                "{desc}"
81                            }
82                        }
83                    }
84
85                    // MDX content
86                    div { class: "prose prose-base max-w-none
87                        prose-headings:{offsets.scroll_mt}
88                        prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10 prose-h2:mb-4
89                        prose-h3:text-xl prose-h3:font-medium prose-h3:mt-8 prose-h3:mb-3
90                        prose-p:text-base-content/80 prose-p:leading-relaxed
91                        prose-a:text-primary prose-a:no-underline hover:prose-a:underline
92                        prose-code:bg-base-200 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm
93                        prose-pre:bg-base-200 prose-pre:border prose-pre:border-base-300",
94                        DocContent { nodes: doc.content.clone() }
95                    }
96
97                    // Page navigation
98                    DocsPageNav { current_path: path.clone() }
99                }
100            }
101
102            // Table of Contents sidebar (right side)
103            if !headers.is_empty() {
104                aside { class: "w-56 shrink-0 hidden xl:block",
105                    div { class: "sticky {offsets.sticky_top} p-6",
106                        DocTableOfContents { headers }
107                    }
108                }
109            }
110        }
111    }
112}
113
114/// Copy MDX source button for doc pages.
115#[component]
116fn CopyMdxButton(content: String) -> Element {
117    #[allow(unused_mut)]
118    let mut copied = use_signal(|| false);
119
120    rsx! {
121        button {
122            class: "btn btn-ghost btn-sm gap-1.5 opacity-60 hover:opacity-100 transition-all duration-150 hover:bg-base-content/10 shrink-0",
123            title: "Copy MDX source",
124            onclick: move |_| {
125                #[cfg(target_arch = "wasm32")]
126                {
127                    use dioxus::prelude::*;
128                    let content = content.clone();
129                    spawn(async move {
130                        let js = format!(
131                            "navigator.clipboard.writeText({}).catch(console.error)",
132                            serde_json::to_string(&content).unwrap_or_default()
133                        );
134                        let _ = document::eval(&js);
135                        copied.set(true);
136                        gloo_timers::future::TimeoutFuture::new(2000).await;
137                        copied.set(false);
138                    });
139                }
140            },
141            if copied() {
142                Icon { class: "size-4 text-success", icon: LdCheck }
143                span { class: "text-xs", "Copied!" }
144            } else {
145                Icon { class: "size-4", icon: LdCopy }
146                span { class: "text-xs", "Copy page" }
147            }
148        }
149    }
150}