command_palette.rs
1 use dioxus::prelude::*; 2 use dioxus_primitives::dialog::{DialogContent, DialogRoot, DialogTitle}; 3 use lucide_dioxus::{Braces, HardDrive, Radar, RefreshCw, Search, Trash2, Upload, Wifi, Zap}; 4 use ui::components::button::{Button, ButtonSize, ButtonVariant}; 5 use ui::components::input::Input; 6 7 fn scroll_to_element(id: &str) { 8 #[cfg(target_arch = "wasm32")] 9 if let Some(el) = web_sys::window() 10 .and_then(|w| w.document()) 11 .and_then(|d| d.get_element_by_id(id)) 12 { 13 use wasm_bindgen::JsCast; 14 if let Ok(html_el) = el.dyn_into::<web_sys::HtmlElement>() { 15 html_el.scroll_into_view(); 16 } 17 } 18 } 19 20 #[component] 21 pub fn CommandPalette( 22 on_open_api: EventHandler<()>, 23 on_sample: EventHandler<()>, 24 on_scan_networks: EventHandler<()>, 25 on_refresh_files: EventHandler<()>, 26 on_upload: EventHandler<()>, 27 ) -> Element { 28 let mut open = use_signal(|| false); 29 let mut filter_text = use_signal(String::new); 30 31 use_effect(move || { 32 if *crate::SHOW_COMMAND_PALETTE.read() { 33 open.set(true); 34 *crate::SHOW_COMMAND_PALETTE.write() = false; 35 } 36 }); 37 38 let mut close = move || { 39 open.set(false); 40 filter_text.set(String::new()); 41 }; 42 43 let query = filter_text.read().to_ascii_lowercase(); 44 let matches = move |keywords: &str| -> bool { 45 query.is_empty() || keywords.to_ascii_lowercase().contains(&query) 46 }; 47 48 rsx! { 49 DialogRoot { 50 open: open(), 51 on_open_change: move |v: bool| { 52 if !v { close(); } else { open.set(true); } 53 }, 54 class: "fixed inset-0 z-50 flex items-start justify-center pt-[20vh] bg-black/60 backdrop-blur-sm", 55 56 DialogContent { 57 class: "w-full max-w-lg mx-4 rounded-lg border border-border bg-card shadow-2xl overflow-hidden", 58 59 div { class: "flex items-center gap-3 border-b border-border px-4 py-3", 60 Search { class: "w-5 h-5 text-muted-foreground shrink-0" } 61 div { class: "flex-1", 62 Input { 63 class: Some("w-full border-0 bg-transparent text-foreground placeholder:text-muted-foreground text-sm focus:ring-0 focus:ring-offset-0".to_string()), 64 autofocus: true, 65 input_type: "text".to_string(), 66 aria_label: Some("Search commands".to_string()), 67 placeholder: "Type a command or search...".to_string(), 68 value: filter_text.read().clone(), 69 on_input: Some(Callback::new(move |e: FormEvent| filter_text.set(e.value()))), 70 } 71 } 72 Button { 73 variant: ButtonVariant::Ghost, 74 size: ButtonSize::Small, 75 is_icon_button: true, 76 aria_label: "Close".to_string(), 77 on_click: move |_| close(), 78 lucide_dioxus::X { class: "w-4 h-4" } 79 } 80 } 81 82 DialogTitle { class: "sr-only", "Command Palette" } 83 84 div { class: "max-h-[400px] overflow-y-auto p-2", 85 86 if matches("sample voltage current temperature sensor") || matches("api cloudevents json") || matches("upload file sd") || matches("scan networks") || matches("refresh filesystem") || matches("clear cache") { 87 h3 { class: "px-2 py-1.5 text-xs text-muted-foreground uppercase tracking-wider", "Actions" } 88 } 89 90 if matches("sample voltage current temperature sensor") { 91 CmdItem { 92 icon: rsx! { lucide_dioxus::FlaskConical { class: "w-4 h-4" } }, 93 label: "Sample Sensors", 94 shortcut: "Ctrl+Enter", 95 on_click: move |_| { on_sample.call(()); close(); }, 96 } 97 } 98 if matches("api cloudevents json response") { 99 CmdItem { 100 icon: rsx! { Braces { class: "w-4 h-4" } }, 101 label: "Open API", 102 shortcut: "Ctrl+/", 103 on_click: move |_| { on_open_api.call(()); close(); }, 104 } 105 } 106 if matches("upload file sd card") { 107 CmdItem { 108 icon: rsx! { Upload { class: "w-4 h-4" } }, 109 label: "Upload File to SD", 110 on_click: move |_| { on_upload.call(()); close(); }, 111 } 112 } 113 if matches("scan networks wifi") { 114 CmdItem { 115 icon: rsx! { Radar { class: "w-4 h-4" } }, 116 label: "Scan Networks", 117 on_click: move |_| { on_scan_networks.call(()); close(); }, 118 } 119 } 120 if matches("refresh filesystem files sd littlefs") { 121 CmdItem { 122 icon: rsx! { RefreshCw { class: "w-4 h-4" } }, 123 label: "Refresh Filesystems", 124 on_click: move |_| { on_refresh_files.call(()); close(); }, 125 } 126 } 127 if matches("clear cache reset storage reload") { 128 CmdItem { 129 icon: rsx! { Trash2 { class: "w-4 h-4" } }, 130 label: "Clear Cache & Reload", 131 on_click: move |_| { 132 #[cfg(target_arch = "wasm32")] 133 if let Some(storage) = web_sys::window() 134 .and_then(|w| w.local_storage().ok().flatten()) 135 { storage.clear().ok(); } 136 document::eval("location.reload()"); 137 close(); 138 }, 139 } 140 } 141 142 if matches("measurements sensor terminal network filesystem flash") { 143 hr { class: "my-1 border-border" } 144 h3 { class: "px-2 py-1.5 text-xs text-muted-foreground uppercase tracking-wider", "Navigate" } 145 } 146 147 if matches("measurements sensor cloudevents voltage temperature co2") { 148 CmdItem { 149 icon: rsx! { Zap { class: "w-4 h-4" } }, 150 label: "Measurements", 151 on_click: move |_| { scroll_to_element("cloudevents-section"); close(); }, 152 } 153 } 154 if matches("terminal shell console") { 155 CmdItem { 156 icon: rsx! { lucide_dioxus::Terminal { class: "w-4 h-4" } }, 157 label: "Terminal", 158 on_click: move |_| { scroll_to_element("terminal-container"); close(); }, 159 } 160 } 161 if matches("network wifi ssid connect") { 162 CmdItem { 163 icon: rsx! { Wifi { class: "w-4 h-4" } }, 164 label: "Network", 165 on_click: move |_| { scroll_to_element("network-section"); close(); }, 166 } 167 } 168 if matches("filesystem sd card files littlefs") { 169 CmdItem { 170 icon: rsx! { HardDrive { class: "w-4 h-4" } }, 171 label: "Filesystem", 172 on_click: move |_| { scroll_to_element("filesystem-section"); close(); }, 173 } 174 } 175 if matches("flash firmware serial esptool") { 176 CmdItem { 177 icon: rsx! { lucide_dioxus::Cpu { class: "w-4 h-4" } }, 178 label: "Firmware Flash", 179 on_click: move |_| { scroll_to_element("flash-panel"); close(); }, 180 } 181 } 182 } 183 } 184 } 185 } 186 } 187 188 #[component] 189 fn CmdItem( 190 icon: Element, 191 label: &'static str, 192 shortcut: Option<&'static str>, 193 on_click: EventHandler<()>, 194 ) -> Element { 195 rsx! { 196 Button { 197 variant: ButtonVariant::Ghost, 198 class: "flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-foreground hover:bg-muted/50 justify-start font-normal".to_string(), 199 on_click: move |_| on_click.call(()), 200 span { class: "text-primary", {icon} } 201 span { "{label}" } 202 if let Some(kbd) = shortcut { 203 span { class: "ml-auto text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded", "{kbd}" } 204 } 205 } 206 } 207 }