dioxus_docs_kit/components/
search_modal.rs1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::{LdSearch, LdX};
4use dioxus_mdx::HttpMethod;
5
6use crate::DocsContext;
7use crate::registry::DocsRegistry;
8
9#[component]
11pub fn SearchModal() -> Element {
12 let mut search_open = use_context::<Signal<bool>>();
13 let mut query = use_signal(String::new);
14 let ctx = use_context::<DocsContext>();
15 let registry = use_context::<&'static DocsRegistry>();
16
17 let results = use_memo(move || registry.search_docs(&query()));
18
19 let on_keydown = move |e: KeyboardEvent| {
20 if e.key() == Key::Enter {
21 let results = results.read();
22 if let Some(entry) = results.first() {
23 (ctx.navigate)(entry.path.clone());
24 search_open.set(false);
25 query.set(String::new());
26 }
27 } else if e.key() == Key::Escape {
28 search_open.set(false);
29 query.set(String::new());
30 }
31 };
32
33 if !search_open() {
34 return rsx! {};
35 }
36
37 rsx! {
38 div {
40 class: "fixed inset-0 z-[100] bg-black/50 flex items-start justify-center pt-[15vh]",
41 onclick: move |_| {
42 search_open.set(false);
43 query.set(String::new());
44 },
45
46 div {
48 class: "bg-base-200 rounded-xl w-full max-w-lg mx-4 border border-base-300 shadow-2xl overflow-hidden",
49 onclick: move |e| e.stop_propagation(),
50
51 div { class: "flex items-center gap-3 px-4 py-3 border-b border-base-300",
53 Icon { class: "size-5 text-base-content/50 shrink-0", icon: LdSearch }
54 input {
55 class: "flex-1 bg-transparent outline-none text-base placeholder:text-base-content/40",
56 placeholder: "Search documentation...",
57 autofocus: true,
58 value: "{query}",
59 oninput: move |e| query.set(e.value()),
60 onkeydown: on_keydown,
61 }
62 button {
63 class: "btn btn-ghost btn-xs btn-square",
64 onclick: move |_| {
65 search_open.set(false);
66 query.set(String::new());
67 },
68 Icon { class: "size-4", icon: LdX }
69 }
70 }
71
72 div { class: "max-h-80 overflow-y-auto",
74 if query().trim().is_empty() {
75 div { class: "px-4 py-8 text-center text-base-content/50 text-sm",
76 "Type to search..."
77 }
78 } else if results.read().is_empty() {
79 div { class: "px-4 py-8 text-center text-base-content/50 text-sm",
80 "No results for \"{query}\""
81 }
82 } else {
83 for entry in results.read().iter() {
84 {
85 let path = entry.path.clone();
86 let title = entry.title.clone();
87 let breadcrumb = entry.breadcrumb.clone();
88 let api_method = entry.api_method;
89 rsx! {
90 SearchResultItem {
91 path,
92 title,
93 breadcrumb,
94 api_method,
95 search_open,
96 query,
97 }
98 }
99 }
100 }
101 }
102 }
103
104 div { class: "px-4 py-2 border-t border-base-300 text-xs text-base-content/40 flex justify-between",
106 span { "Esc to close" }
107 span { "Enter to navigate" }
108 }
109 }
110 }
111 }
112}
113
114#[component]
115fn SearchResultItem(
116 path: String,
117 title: String,
118 breadcrumb: String,
119 api_method: Option<HttpMethod>,
120 mut search_open: Signal<bool>,
121 mut query: Signal<String>,
122) -> Element {
123 let ctx = use_context::<DocsContext>();
124 let path_for_click = path.clone();
125
126 rsx! {
127 button {
128 class: "w-full text-left px-4 py-3 hover:bg-base-300/50 transition-colors flex items-center gap-3 border-b border-base-300/50 last:border-b-0",
129 onclick: move |_| {
130 (ctx.navigate)(path_for_click.clone());
131 search_open.set(false);
132 query.set(String::new());
133 },
134 div { class: "flex-1 min-w-0",
135 div { class: "flex items-center gap-2",
136 if let Some(method) = api_method {
137 {
138 let (label, color) = match method {
139 HttpMethod::Get => ("GET", "badge-success"),
140 HttpMethod::Post => ("POST", "badge-primary"),
141 HttpMethod::Put => ("PUT", "badge-warning"),
142 HttpMethod::Delete => ("DEL", "badge-error"),
143 HttpMethod::Patch => ("PATCH", "badge-info"),
144 _ => ("???", "badge-ghost"),
145 };
146 rsx! {
147 span { class: "badge badge-xs font-mono {color}", "{label}" }
148 }
149 }
150 }
151 span { class: "font-medium text-sm truncate", "{title}" }
152 }
153 span { class: "text-xs text-base-content/50 truncate block mt-0.5",
154 "{breadcrumb}"
155 }
156 }
157 }
158 }
159}