Command palette
A Spotlight-style launcher opened with ⌘K (or Ctrl-K). The global hotkey is a
.window modifier on the root; inside the dialog, arrow keys and Enter run off
the filtered command list.
Markup
<div
data-component="palette"
@keydown.meta.k.window.prevent="toggle"
@keydown.ctrl.k.window.prevent="toggle"
>
<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>
</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";
},
async openIt() {
this.state.open = true;
this.state.query = "";
this.state.active = 0;
await this.tick(); // the panel is rendered (no longer display: none)
this.refs.input.focus();
},
toggle() {
this.state.open ? this.close() : this.openIt();
},
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;
}
},
});
@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.
⌘K and Ctrl+K are two modifiers on the root: .window listens on window,
the .meta/.ctrl and .k guards match the shortcut, and .prevent stops the
browser’s own handling only when it matched. openIt waits for the render with
await this.tick() before focusing the input; right after open = true the
panel is still display: none and focus() would do nothing (Micra 2.9+).