use leptos::prelude::*;
use leptos::portal::Portal;
use crate::icons::{Icon, icondata};
use crate::components::shared::MENU_ITEM_CLASS;
#[derive(Clone, Copy)]
struct CommandFilter(RwSignal<String>);
#[component]
pub fn Command(
#[prop(into)] open: RwSignal<bool>,
children: ChildrenFn,
) -> impl IntoView {
let filter = RwSignal::new(String::new());
provide_context(open);
provide_context(CommandFilter(filter));
let children = StoredValue::new(children);
view! {
<Portal>
<Show when=move || open.get()>
<CommandOverlay open=open filter=filter>
{children.with_value(|c| c())}
</CommandOverlay>
</Show>
</Portal>
}
}
#[component]
fn CommandOverlay(
open: RwSignal<bool>,
filter: RwSignal<String>,
children: Children,
) -> impl IntoView {
window_event_listener(leptos::ev::keydown, move |e| {
if e.key() == "Escape" {
open.set(false);
filter.set(String::new());
}
});
let close = move |_| {
open.set(false);
filter.set(String::new());
};
view! {
<>
<div class="fixed inset-0 z-40 bg-black/60 animate-fade-in" on:click=close />
<div class="fixed left-1/2 top-[20%] -translate-x-1/2 z-50 w-full max-w-lg rounded-lg border border-border bg-card shadow-elev-lg animate-scale-in overflow-hidden">
<div class="flex items-center gap-2 border-b border-border px-3 py-2">
<Icon icon=Signal::derive(|| icondata::LuSearch) width="16" height="16" attr:class="text-muted-foreground shrink-0" />
<input
type="text"
placeholder="Type a command or search…"
autofocus=true
class="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
prop:value=move || filter.get()
on:input=move |e| filter.set(event_target_value(&e))
/>
</div>
<div class="max-h-80 overflow-y-auto p-1">
{children()}
</div>
</div>
</>
}
}
#[component]
pub fn CommandGroup(
#[prop(into)] heading: String,
children: ChildrenFn,
) -> impl IntoView {
let children = StoredValue::new(children);
view! {
<div class="mb-1">
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{heading}
</div>
{children.with_value(|c| c())}
</div>
}
}
#[component]
pub fn CommandItem(
#[prop(into)] keywords: String,
#[prop(optional)] on_select: Option<Callback<()>>,
children: ChildrenFn,
) -> impl IntoView {
let open = use_context::<RwSignal<bool>>()
.expect("CommandItem must be inside Command");
let CommandFilter(filter) = use_context::<CommandFilter>()
.expect("CommandItem must be inside Command");
let kw = StoredValue::new(keywords.to_lowercase());
let children = StoredValue::new(children);
let visible = move || filter.get().is_empty() || kw.with_value(|k| k.contains(&filter.get().to_lowercase()));
let handle_click = move |_| {
if let Some(cb) = on_select {
cb.run(());
}
open.set(false);
filter.set(String::new());
};
view! {
<Show when=visible>
<div
class=MENU_ITEM_CLASS
on:click=handle_click
>
{children.with_value(|c| c())}
</div>
</Show>
}
}
#[component]
pub fn CommandEmpty(children: ChildrenFn) -> impl IntoView {
let CommandFilter(filter) = use_context::<CommandFilter>()
.expect("CommandEmpty must be inside Command");
let children = StoredValue::new(children);
view! {
<Show when=move || !filter.get().is_empty()>
<div class="px-2 py-1.5 text-sm text-muted-foreground">
{children.with_value(|c| c())}
</div>
</Show>
}
}