Command palette

A Spotlight-style launcher opened with ⌘K (or Ctrl-K). The global hotkey is a document listener registered in onCreate; inside the dialog, arrow keys and Enter run off the filtered command list.

Markup

<button @click="openIt">Search commands <kbd>⌘K</kbd></button>

<div data-show="open" @click.self="close">
  <div>
    <input
      data-ref="input"
      data-model="query"
      @input="reset"
      @keydown.down.prevent="move(1)"
      @keydown.up.prevent="move(-1)"
      @keydown.enter.prevent="run"
      @keydown.escape="close"
    />
    <ul role="listbox">
      <template data-each="filtered()" data-key="id">
        <li role="option" @click="pick" data-bind="class:rowClass($index), data-i:$index">
          <span data-text="item.label"></span>
          <span data-text="item.group"></span>
        </li>
      </template>
    </ul>
  </div>
</div>

Component

Micra.define("palette", {
  state: {
    open: false,
    query: "",
    active: 0,
    last: "",
    commands: [
      { id: 1, label: "Go to Dashboard", group: "Navigate" },
      { id: 2, label: "Create new user", group: "Action" },
      { id: 3, label: "Open settings", group: "Navigate" },
      // ...
    ],
  },
  filtered() {
    const q = this.state.query.trim().toLowerCase();
    return this.state.commands.filter((c) => c.label.toLowerCase().includes(q));
  },
  rowClass(i) {
    return i === this.state.active ? "bg-indigo-600 text-white" : "hover:bg-zinc-100";
  },
  openIt() {
    this.state.open = true;
    this.state.query = "";
    this.state.active = 0;
    this.refs.input.focus();
  },
  close() {
    this.state.open = false;
  },
  reset() {
    this.state.active = 0;
  },
  move(d) {
    const n = this.filtered().length;
    if (!n) return;
    this.state.active = (this.state.active + d + n) % n;
  },
  run() {
    const c = this.filtered()[this.state.active];
    if (c) {
      this.state.last = c.label;
      this.state.open = false;
    }
  },
  pick(e) {
    const c = this.filtered()[Number(e.currentTarget.dataset.i)];
    if (c) {
      this.state.last = c.label;
      this.state.open = false;
    }
  },
  onCreate() {
    this._hotkey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        this.state.open ? this.close() : this.openIt();
      }
    };
    document.addEventListener("keydown", this._hotkey);
  },
  onDestroy() {
    document.removeEventListener("keydown", this._hotkey);
  },
});

@click.self closes the dialog only when the backdrop itself is clicked, not when the click bubbles up from inside the panel — so selecting a command and dismissing the overlay stay distinct.