BasecoatUI components for htmy.

Command

Example:



Code example:

from htmy import ComponentSequence, ComponentType, html

from htmui.basecoat.command import command, command_dialog, command_item, search_icon
from htmui.basecoat.menu import menu_item_group
from htmui.basecoat.separator import separator


def example() -> ComponentType:
    return html.div(
        html.button(
            "Open command dialog",
            html.kbd(
                "⌘+K",
                class_="kbd",
            ),
            class_="btn",
            data_variant="outline",
            type="button",
            onclick="document.getElementById('command-dialog-example').showModal()",
        ),
        command_dialog(*command_menu(), id="command-dialog-example", input_icon=search_icon),
        html.hr(),
        command(*command_menu(), id="command-example", input_icon=search_icon),
        class_="flex flex-col gap-4",
    )


def command_menu() -> ComponentSequence:
    return (
        menu_item_group(
            command_item(html.span("Commit"), filter="Commit"),
            command_item(html.span("Pull"), filter="Pull"),
            command_item(html.span("Push"), filter="Push"),
            id="command-group-1",
            label="Git",
        ),
        separator,
        menu_item_group(
            command_item(html.span("Review"), filter="Review"),
            command_item(html.span("Approve"), filter="Approve", disabled=True),
            command_item(html.span("Comment"), filter="Comment"),
            id="command-group-2",
            label="Actions",
        ),
    )

Component implementation:

For more details, see the BasecoatUI documentation.

from typing import Any

from htmy import ComponentType, PropertyValue, SafeStr, html, join_classes

from .menu import menu_item

__version__ = "0.2.0"
__framework__ = "BasecoatUI"
__framework_version__ = "1"
__framework_url__ = "https://basecoatui.com/components/command/"

js = SafeStr(
    '<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1/dist/js/command.min.js" defer></script>'
)


search_icon = SafeStr(
    '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" '
    'stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" '
    'd="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>'
)
"""`magnifying-glass` icon from https://heroicons.com/."""


def command(
    *items: ComponentType,
    id: str,
    input_icon: ComponentType = None,
    input_placeholder: str = "Type a command or search...",
    aria_label: str | None = "Command menu",
    no_results_message: str = "No results found.",
    class_: str | None = None,
    **kwargs: PropertyValue,
) -> ComponentType:
    """Basecoat v1 command menu.

    Items should be created with `menu_item(..., filter=..., keywords=...)` from
    `htmui.basecoat.menu`. The `filter` text is required for the command JavaScript
    to match the item while typing.
    """
    menu_id = f"{id}-menu"
    root_props: dict[str, Any] = {"id": id}
    if aria_label is not None:
        root_props["aria_label"] = aria_label

    return html.div(
        html.header(
            input_icon,
            html.input_(
                type="text",
                id=f"{id}-input",
                role="combobox",
                autocomplete="off",
                autocorrect="off",
                placeholder=input_placeholder,
                spellcheck="false",
                aria_autocomplete="list",
                aria_expanded="true",
                aria_controls=menu_id,
            ),
        ),
        html.div(
            *items,
            id=menu_id,
            role="menu",
            aria_orientation="vertical",
            data_empty=no_results_message,
        ),
        class_=join_classes("command", class_),
        **root_props,
        **kwargs,
    )


def command_item(
    *children: ComponentType,
    filter: str,
    keywords: str | None = None,
    shortcut: str | None = None,
    indicator: ComponentType | None = None,
    disabled: bool = False,
    force: bool = False,
    keep_open: bool = False,
    class_: str | None = None,
    **kwargs: PropertyValue,
) -> ComponentType:
    """Basecoat v1 command item (`filter` is required for the command JS to match it)."""
    return menu_item(
        *children,
        filter=filter,
        keywords=keywords,
        shortcut=shortcut,
        shortcut_kind="command",
        indicator=indicator,
        disabled=disabled,
        force=force,
        keep_open=keep_open,
        class_=class_,
        **kwargs,  # type: ignore[arg-type]
    )


def command_dialog(
    *items: ComponentType,
    id: str,
    input_icon: ComponentType = None,
    input_placeholder: str = "Type a command or search...",
    aria_label: str | None = "Command menu",
    no_results_message: str = "No results found.",
    onclick: str | None = "if (event.target === this) this.close()",
    **kwargs: PropertyValue,
) -> ComponentType:
    """Basecoat v1 command palette wrapped in `<dialog class="command-dialog">`."""
    return html.dialog(
        command(
            *items,
            id=f"{id}-command",
            input_icon=input_icon,
            input_placeholder=input_placeholder,
            aria_label=None,
            no_results_message=no_results_message,
        ),
        id=id,
        class_="command-dialog",
        aria_label=aria_label,
        onclick=onclick,
        **kwargs,
    )