/ web / src / components / command_palette.rs
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  }