Kosmesis
Components

Command

A cmdk-style, always-filtered command list, with a CommandDialog composition over Morphos's Dialog.

Installation

npx kosmesis add command
pnpm dlx kosmesis add command
yarn dlx kosmesis add command
bunx kosmesis add command

Install the following dependencies:

npm install @morphos/overlays @morphos/core
pnpm add @morphos/overlays @morphos/core
yarn add @morphos/overlays @morphos/core
bun add @morphos/overlays @morphos/core

Copy and paste the following code into your project.

command.tsx
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/css
pnpm add @morphos/overlays @morphos/core @praxisjs/css
yarn add @morphos/overlays @morphos/core @praxisjs/css
bun add @morphos/overlays @morphos/core @praxisjs/css

Copy and paste the following code into your project.

command.tsx
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({...})).

PropTypeDefault
itemsCommandItemDef[] ({ value, label, group?, disabled?, onSelect? })[]
onSelect(item: CommandItemDef) => void

Command

PropTypeDefault
stateCommandState— required
placeholderstring"Type a command or search..."
emptyTextstring"No results found."
classstring

CommandDialog

PropTypeDefault
dialogDialog— required, a Morphos Dialog instance
stateCommandState— required
placeholderstring
classstring

On this page