maud-ui 0.2.1

64 headless, accessible UI components for Rust web apps — shadcn Base UI API parity. Plus block templates, a live theme customiser, and shell hooks for 15 third-party widgets (Monaco, xyflow, Excalidraw, Three.js, AG Grid, Leaflet, FullCalendar, SortableJS, and more). Built on maud + htmx, styled like shadcn/ui.
Documentation
//! Command component — Cmd+K command palette with search, grouped items, and keyboard navigation.
use maud::{html, Markup};

/// A single command item in the palette
#[derive(Clone, Debug)]
pub struct CommandItem {
    /// Display label for the command
    pub label: String,
    /// Optional keyboard shortcut (e.g., "⌘N")
    pub shortcut: Option<String>,
    /// Optional group name for categorization
    pub group: Option<String>,
    /// Whether the item is disabled
    pub disabled: bool,
}

/// Command palette rendering properties
#[derive(Clone, Debug)]
pub struct Props {
    /// Unique identifier for the command palette dialog
    pub id: String,
    /// List of command items
    pub items: Vec<CommandItem>,
    /// Placeholder text for the search input
    pub placeholder: String,
}

impl Default for Props {
    fn default() -> Self {
        Self {
            id: "command".to_string(),
            items: vec![],
            placeholder: "Type a command or search\u{2026}".to_string(),
        }
    }
}

/// Render a trigger button that opens the command palette
pub fn trigger(target_id: &str, label: &str) -> Markup {
    html! {
        button type="button"
            class="mui-btn mui-btn--default mui-btn--md"
            data-mui="command-trigger"
            data-target=(target_id)
        {
            (label)
        }
    }
}

/// Render a keyboard shortcut slot for a command item.
///
/// Matches the `<kbd class="mui-kbd">` markup that `CommandItem::shortcut`
/// emits inside [`render`], so callers can drop a shortcut inline when
/// composing their own item markup.
pub fn shortcut(children: Markup) -> Markup {
    html! {
        kbd class="mui-kbd mui-command__shortcut" { (children) }
    }
}

/// Render a visual separator between command groups.
///
/// Use between groups of items when composing a custom command list.
pub fn separator() -> Markup {
    html! {
        div class="mui-command__separator" role="separator" {}
    }
}

/// Render the "no results" empty-state row for a command list.
///
/// Use when a search yields no matches and the caller is composing the
/// list markup by hand rather than relying on [`render`]'s built-in
/// empty slot.
pub fn empty(text: &str) -> Markup {
    html! {
        div class="mui-command__empty" { (text) }
    }
}

/// Render the command palette
pub fn render(props: Props) -> Markup {
    // Collect unique groups in insertion order
    let mut groups: Vec<String> = Vec::new();
    for item in &props.items {
        let group_key = item.group.clone().unwrap_or_default();
        let mut found = false;
        for g in &groups {
            if *g == group_key {
                found = true;
                break;
            }
        }
        if !found {
            groups.push(group_key);
        }
    }

    html! {
        dialog class="mui-command"
            id=(props.id)
            data-mui="command"
            aria-label="Command palette"
            aria-modal="true"
        {
            div class="mui-command__search-wrap" {
                span class="mui-command__search-icon" aria-hidden="true" { "\u{2315}" }
                input type="text" class="mui-command__search"
                    placeholder=(props.placeholder)
                    autocomplete="off"
                    aria-label="Search commands";
            }
            div class="mui-command__list" role="listbox" {
                @for group_name in &groups {
                    div class="mui-command__group" {
                        @if !group_name.is_empty() {
                            div class="mui-command__group-label" { (group_name) }
                        }
                        @for item in &props.items {
                            @let item_group = item.group.clone().unwrap_or_default();
                            @if item_group == *group_name {
                                @if item.disabled {
                                    div class="mui-command__item mui-command__item--disabled"
                                        role="option"
                                        tabindex="-1"
                                        aria-disabled="true"
                                        data-label=(item.label)
                                    {
                                        span class="mui-command__item-label" { (item.label) }
                                        @if let Some(shortcut) = &item.shortcut {
                                            kbd class="mui-kbd" { (shortcut) }
                                        }
                                    }
                                } @else {
                                    div class="mui-command__item"
                                        role="option"
                                        tabindex="-1"
                                        data-label=(item.label)
                                    {
                                        span class="mui-command__item-label" { (item.label) }
                                        @if let Some(shortcut) = &item.shortcut {
                                            kbd class="mui-kbd" { (shortcut) }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            div class="mui-command__empty" hidden { "No results found." }
        }
    }
}

/// Showcase the command palette
pub fn showcase() -> Markup {
    let items = vec![
        CommandItem {
            label: "Calendar".to_string(),
            shortcut: None,
            group: Some("Suggestions".to_string()),
            disabled: false,
        },
        CommandItem {
            label: "Search".to_string(),
            shortcut: None,
            group: Some("Suggestions".to_string()),
            disabled: false,
        },
        CommandItem {
            label: "Settings".to_string(),
            shortcut: None,
            group: Some("Suggestions".to_string()),
            disabled: false,
        },
        CommandItem {
            label: "New File".to_string(),
            shortcut: Some("\u{2318}N".to_string()),
            group: Some("Actions".to_string()),
            disabled: false,
        },
        CommandItem {
            label: "Save".to_string(),
            shortcut: Some("\u{2318}S".to_string()),
            group: Some("Actions".to_string()),
            disabled: false,
        },
        CommandItem {
            label: "Export".to_string(),
            shortcut: None,
            group: Some("Actions".to_string()),
            disabled: false,
        },
    ];

    html! {
        div.mui-showcase__grid {
            div {
                p.mui-showcase__caption { "Command palette trigger" }
                div.mui-showcase__row {
                    (trigger("demo-command", "Open command palette"))
                    span.mui-text-muted style="font-size: 0.875rem;" {
                        "Press "
                        kbd.mui-kbd { "\u{2318}K" }
                    }
                }
            }
            div {
                (render(Props {
                    id: "demo-command".to_string(),
                    items,
                    placeholder: "Type a command or search\u{2026}".to_string(),
                }))
            }

            // Composable slots demo: shortcut() + separator() + empty()
            div {
                p.mui-showcase__caption { "Composable slots: shortcut / separator / empty" }
                div class="mui-command" style="position: static; display: block; max-width: 24rem; width: 100%;" {
                    div class="mui-command__list" role="listbox" {
                        div class="mui-command__group" {
                            div class="mui-command__group-label" { "File" }
                            div class="mui-command__item" role="option" tabindex="-1" {
                                span class="mui-command__item-label" { "New File" }
                                (shortcut(html! { "\u{2318}N" }))
                            }
                            div class="mui-command__item" role="option" tabindex="-1" {
                                span class="mui-command__item-label" { "Save" }
                                (shortcut(html! { "\u{2318}S" }))
                            }
                        }
                        (separator())
                        div class="mui-command__group" {
                            div class="mui-command__group-label" { "Edit" }
                            div class="mui-command__item" role="option" tabindex="-1" {
                                span class="mui-command__item-label" { "Undo" }
                                (shortcut(html! { "\u{2318}Z" }))
                            }
                        }
                    }
                }
            }

            div {
                p.mui-showcase__caption { "Empty state" }
                div class="mui-command" style="position: static; display: block; max-width: 24rem; width: 100%;" {
                    div class="mui-command__list" role="listbox" {
                        (empty("No results found."))
                    }
                }
            }
        }
    }
}