Components
Command
A cmdk-style, always-filtered command list, with a CommandDialog composition over Morphos's Dialog.
Installation
npx kosmesis add commandpnpm dlx kosmesis add commandyarn dlx kosmesis add commandbunx kosmesis add commandInstall the following dependencies:
npm install @morphos/overlays @morphos/corepnpm add @morphos/overlays @morphos/coreyarn add @morphos/overlays @morphos/corebun add @morphos/overlays @morphos/coreCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Emit, Prop, State } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Keys } from "@morphos/core";import type { Dialog } from "@morphos/overlays";import { DialogContent } from "./dialog";import { cn } from "@/lib/utils";export interface CommandItemDef { value: string; label: string; group?: string; disabled?: boolean; onSelect?: () => void;}export interface CommandStateProps { items: CommandItemDef[]; onSelect?: (item: CommandItemDef) => void;}/** * A composition, not a new Morphos primitive: Morphos has no cmdk-style "type to jump anywhere" * component, so `CommandState` implements the always-visible filtered list + roving keyboard * navigation directly (closer to Morphos's own `Combobox` internals than to a wrap of it, since * Command's list is always rendered rather than toggled open/closed). `CommandDialog` composes * this with Morphos's `Dialog` for the "⌘K" palette variant. */@Component()export class CommandState extends StatefulComponent { @Prop() items: CommandItemDef[] = []; @Prop() onSelect?: CommandStateProps["onSelect"]; @State() _query = ""; @State() _activeIndex = 0; get query(): string { return this._query; } get filtered(): CommandItemDef[] { const q = this._query.trim().toLowerCase(); if (!q) return this.items; return this.items.filter((item) => item.label.toLowerCase().includes(q)); } get groups(): Array<{ group: string | undefined; items: CommandItemDef[] }> { const filtered = this.filtered; const groups: Array<{ group: string | undefined; items: CommandItemDef[] }> = []; for (const item of filtered) { const last = groups.at(-1); if (last && last.group === item.group) { last.items.push(item); } else { groups.push({ group: item.group, items: [item] }); } } return groups; } get activeIndex(): number { return this._activeIndex; } setQuery(value: string): void { this._query = value; this._activeIndex = 0; } isActive(item: CommandItemDef): boolean { return this.filtered[this._activeIndex] === item; } /** Mouse-driven equivalent of `moveActive` — makes hover and keyboard navigation share the same active item instead of only the keyboard state ever setting `data-active`. */ setActive(item: CommandItemDef): void { if (item.disabled) return; const index = this.filtered.indexOf(item); if (index !== -1) this._activeIndex = index; } moveActive(delta: number): void { const enabled = this.filtered.filter((i) => !i.disabled); if (enabled.length === 0) return; const currentEnabledIndex = enabled.indexOf(this.filtered[this._activeIndex]); const nextEnabledIndex = (currentEnabledIndex + delta + enabled.length) % enabled.length; this._activeIndex = this.filtered.indexOf(enabled[nextEnabledIndex]); } @Emit("onSelect") selectActive(): CommandItemDef | undefined { const item = this.filtered.at(this._activeIndex); if (!item || item.disabled) return undefined; item.onSelect?.(); return item; } /** Pure state container — never mounted via JSX, only instantiated directly. */ render() { return null; }}export interface CommandProps { state: CommandState; placeholder?: string; emptyText?: string; class?: string;}@Component()export class Command extends StatelessComponent<CommandProps> { render() { const { state, placeholder = "Type a command or search...", emptyText = "No results found.", class: cls } = this.props; return ( <div class={cn("flex w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", cls)} onKeyDown={(event: KeyboardEvent) => { if (event.key === Keys.ArrowDown) { event.preventDefault(); state.moveActive(1); } else if (event.key === Keys.ArrowUp) { event.preventDefault(); state.moveActive(-1); } else if (event.key === Keys.Enter) { event.preventDefault(); state.selectActive(); } }} > <div class="flex items-center gap-2 border-b px-3"> <span aria-hidden class="text-muted-foreground">⌕</span> <input autoFocus role="combobox" aria-expanded={"true" as const} placeholder={placeholder} value={() => state.query} class="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50" onInput={(event: Event) => { state.setQuery((event.target as HTMLInputElement).value); }} /> </div> <div role="listbox" class="max-h-80 scroll-py-1 overflow-x-hidden overflow-y-auto p-1"> {() => state.filtered.length === 0 ? ( <p class="py-6 text-center text-sm text-muted-foreground">{emptyText}</p> ) : ( state.groups.map((group) => ( <div key={group.group ?? "_"} class="overflow-hidden p-1 text-foreground"> {group.group && ( <div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">{group.group}</div> )} {group.items.map((item) => ( <div key={item.value} role="option" aria-selected={state.isActive(item) ? ("true" as const) : ("false" as const)} aria-disabled={item.disabled ? ("true" as const) : undefined} data-active={state.isActive(item) ? "" : undefined} data-disabled={item.disabled ? "" : undefined} class={cn( "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none", "data-active:bg-accent data-active:text-accent-foreground", "data-disabled:pointer-events-none data-disabled:opacity-50", )} onMouseEnter={() => { state.setActive(item); }} onClick={() => { if (!item.disabled) { item.onSelect?.(); state.onSelect?.(item); } }} > {item.label} </div> ))} </div> )) ) } </div> </div> ); }}export interface CommandDialogProps { dialog: Dialog; state: CommandState; placeholder?: string; class?: string; children?: Children;}@Component()export class CommandDialog extends StatelessComponent<CommandDialogProps> { render() { const { dialog, state, placeholder, class: cls } = this.props; return ( <DialogContent dialog={dialog} class={cn("overflow-hidden p-0 shadow-lg", cls)} aria-label="Command menu" showCloseButton={false} > <Command state={state} placeholder={placeholder} /> </DialogContent> ); }}Install the following dependencies:
npm install @morphos/overlays @morphos/core @praxisjs/csspnpm add @morphos/overlays @morphos/core @praxisjs/cssyarn add @morphos/overlays @morphos/core @praxisjs/cssbun add @morphos/overlays @morphos/core @praxisjs/cssCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Emit, Prop, State } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Keys } from "@morphos/core";import type { Dialog } from "@morphos/overlays";import { DialogContent } from "./dialog";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);export interface CommandItemDef { value: string; label: string; group?: string; disabled?: boolean; onSelect?: () => void;}export interface CommandStateProps { items: CommandItemDef[]; onSelect?: (item: CommandItemDef) => void;}/** * A composition, not a new Morphos primitive: Morphos has no cmdk-style "type to jump anywhere" * component, so `CommandState` implements the always-visible filtered list + roving keyboard * navigation directly (closer to Morphos's own `Combobox` internals than to a wrap of it, since * Command's list is always rendered rather than toggled open/closed). `CommandDialog` composes * this with Morphos's `Dialog` for the "⌘K" palette variant. */@Component()export class CommandState extends StatefulComponent { @Prop() items: CommandItemDef[] = []; @Prop() onSelect?: CommandStateProps["onSelect"]; @State() _query = ""; @State() _activeIndex = 0; get query(): string { return this._query; } get filtered(): CommandItemDef[] { const q = this._query.trim().toLowerCase(); if (!q) return this.items; return this.items.filter((item) => item.label.toLowerCase().includes(q)); } get groups(): Array<{ group: string | undefined; items: CommandItemDef[] }> { const filtered = this.filtered; const groups: Array<{ group: string | undefined; items: CommandItemDef[] }> = []; for (const item of filtered) { const last = groups.at(-1); if (last && last.group === item.group) { last.items.push(item); } else { groups.push({ group: item.group, items: [item] }); } } return groups; } get activeIndex(): number { return this._activeIndex; } setQuery(value: string): void { this._query = value; this._activeIndex = 0; } isActive(item: CommandItemDef): boolean { return this.filtered[this._activeIndex] === item; } /** Mouse-driven equivalent of `moveActive` — makes hover and keyboard navigation share the same active item instead of only the keyboard state ever setting `data-active`. */ setActive(item: CommandItemDef): void { if (item.disabled) return; const index = this.filtered.indexOf(item); if (index !== -1) this._activeIndex = index; } moveActive(delta: number): void { const enabled = this.filtered.filter((i) => !i.disabled); if (enabled.length === 0) return; const currentEnabledIndex = enabled.indexOf(this.filtered[this._activeIndex]); const nextEnabledIndex = (currentEnabledIndex + delta + enabled.length) % enabled.length; this._activeIndex = this.filtered.indexOf(enabled[nextEnabledIndex]); } @Emit("onSelect") selectActive(): CommandItemDef | undefined { const item = this.filtered.at(this._activeIndex); if (!item || item.disabled) return undefined; item.onSelect?.(); return item; } /** Pure state container — never mounted via JSX, only instantiated directly. */ render() { return null; }}class CommandStyles extends Stylesheet { $root = this.css({ display: "flex", width: "100%", flexDirection: "column", overflow: "hidden", borderRadius: `calc(${t.radius} - 2px)`, backgroundColor: t.popover, color: t.popoverForeground }); $searchRow = this.css({ display: "flex", alignItems: "center", gap: "0.5rem", borderBottom: `1px solid ${t.border}`, padding: "0 0.75rem" }); $searchIcon = this.css({ color: t.mutedForeground }); $input = this.css({ display: "flex", height: "2.75rem", width: "100%", borderRadius: `calc(${t.radius} - 2px)`, backgroundColor: "transparent", padding: "0.75rem 0", fontSize: "0.875rem", outline: "none", }) .placeholder({ color: t.mutedForeground }) .disabled({ cursor: "not-allowed", opacity: 0.5 }); $list = this.css({ maxHeight: "20rem", overflowX: "hidden", overflowY: "auto", padding: "0.25rem" }); $empty = this.css({ padding: "1.5rem 0", textAlign: "center", fontSize: "0.875rem", color: t.mutedForeground }); $group = this.css({ overflow: "hidden", padding: "0.25rem", color: t.foreground }); $groupLabel = this.css({ padding: "0.375rem 0.5rem", fontSize: "0.75rem", fontWeight: 500, color: t.mutedForeground }); $item = this.css({ position: "relative", display: "flex", cursor: "default", alignItems: "center", gap: "0.5rem", borderRadius: "0.125rem", padding: "0.375rem 0.5rem", fontSize: "0.875rem", outline: "none", userSelect: "none", }) .on("&[data-active]", { backgroundColor: t.accent, color: t.accentForeground }) .on("&[data-disabled]", { pointerEvents: "none", opacity: 0.5 });}export interface CommandProps { state: CommandState; placeholder?: string; emptyText?: string; class?: string;}@Component()export class Command extends StatelessComponent<CommandProps> { @Styled(CommandStyles) $s!: CommandStyles; render() { const { state, placeholder = "Type a command or search...", emptyText = "No results found.", class: cls } = this.props; return ( <div class={cx(this.$s.$root, cls)} onKeyDown={(event: KeyboardEvent) => { if (event.key === Keys.ArrowDown) { event.preventDefault(); state.moveActive(1); } else if (event.key === Keys.ArrowUp) { event.preventDefault(); state.moveActive(-1); } else if (event.key === Keys.Enter) { event.preventDefault(); state.selectActive(); } }} > <div class={this.$s.$searchRow}> <span aria-hidden class={this.$s.$searchIcon}>⌕</span> <input autoFocus role="combobox" aria-expanded={"true" as const} placeholder={placeholder} value={() => state.query} class={this.$s.$input} onInput={(event: Event) => { state.setQuery((event.target as HTMLInputElement).value); }} /> </div> <div role="listbox" class={this.$s.$list}> {() => state.filtered.length === 0 ? ( <p class={this.$s.$empty}>{emptyText}</p> ) : ( state.groups.map((group) => ( <div key={group.group ?? "_"} class={this.$s.$group}> {group.group && <div class={this.$s.$groupLabel}>{group.group}</div>} {group.items.map((item) => ( <div key={item.value} role="option" aria-selected={state.isActive(item) ? ("true" as const) : ("false" as const)} aria-disabled={item.disabled ? ("true" as const) : undefined} data-active={state.isActive(item) ? "" : undefined} data-disabled={item.disabled ? "" : undefined} class={this.$s.$item} onMouseEnter={() => { state.setActive(item); }} onClick={() => { if (!item.disabled) { item.onSelect?.(); state.onSelect?.(item); } }} > {item.label} </div> ))} </div> )) ) } </div> </div> ); }}class CommandDialogStyles extends Stylesheet { $content = this.css({ overflow: "hidden", padding: "0", boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" });}export interface CommandDialogProps { dialog: Dialog; state: CommandState; placeholder?: string; class?: string; children?: Children;}@Component()export class CommandDialog extends StatelessComponent<CommandDialogProps> { @Styled(CommandDialogStyles) $s!: CommandDialogStyles; render() { const { dialog, state, placeholder, class: cls } = this.props; return ( <DialogContent dialog={dialog} class={cx(this.$s.$content, cls)} aria-label="Command menu" showCloseButton={false} > <Command state={state} placeholder={placeholder} /> </DialogContent> ); }}Examples
Usage
import { Command, CommandState } from "@/components/ui/command";
@Component()
class QuickSearch extends StatefulComponent {
@State() commandState = new CommandState({
items: [
{ value: "calendar", label: "Calendar", group: "Suggestions" },
{ value: "emoji", label: "Search Emoji", group: "Suggestions" },
{ value: "profile", label: "Profile", group: "Settings" },
],
onSelect: (item) => console.log(item.value),
});
render() {
return <Command state={this.commandState} class="w-96 border shadow-md" />;
}
}As a ⌘K palette
import { Dialog } from "@morphos/overlays";
import { CommandDialog, CommandState } from "@/components/ui/command";
@Component()
class Palette extends StatefulComponent {
@State() dialog = new Dialog();
@State() commandState = new CommandState({ items: [/* ... */] });
onMount() {
document.addEventListener("keydown", this._handleShortcut);
}
onUnmount() {
document.removeEventListener("keydown", this._handleShortcut);
}
private readonly _handleShortcut = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
this.dialog.toggle();
}
};
render() {
return <CommandDialog dialog={this.dialog} state={this.commandState} />;
}
}Morphos has no cmdk-style primitive to wrap, so CommandState implements filtering and roving
keyboard navigation directly — closer to Morphos's own Combobox internals than a wrap of it,
since Command's list is always rendered rather than toggled open/closed.
Props
CommandState
Instantiate directly (@State() commandState = new CommandState({...})).
| Prop | Type | Default |
|---|---|---|
items | CommandItemDef[] ({ value, label, group?, disabled?, onSelect? }) | [] |
onSelect | (item: CommandItemDef) => void | — |
Command
| Prop | Type | Default |
|---|---|---|
state | CommandState | — required |
placeholder | string | "Type a command or search..." |
emptyText | string | "No results found." |
class | string | — |
CommandDialog
| Prop | Type | Default |
|---|---|---|
dialog | Dialog | — required, a Morphos Dialog instance |
state | CommandState | — required |
placeholder | string | — |
class | string | — |