# `<command-ui>` — Searchable command palette

`<command-ui>` is a searchable command palette with keyboard navigation, recent-items memory, and grouped sections. It accepts **native `<option>` and `<optgroup>` children** as its content API — no custom element wrappers around items.

```html
<command-ui open placeholder="Type a command…">
  <optgroup label="Navigation">
    <option value="home" data-icon="house" data-shortcut="⌘H">Go Home</option>
    <option value="settings" data-icon="gear" data-shortcut="⌘,">Settings</option>
  </optgroup>
  <optgroup label="Edit">
    <option value="undo" data-icon="arrow-counter-clockwise" data-shortcut="⌘Z">Undo</option>
    <option value="redo" data-icon="arrow-clockwise" data-shortcut="⌘⇧Z">Redo</option>
  </optgroup>
  <option value="logout" data-icon="sign-out" data-shortcut="⌘⇧Q">Log out</option>
</command-ui>
```

## Why native `<option>`

The native option semantics carry through to screen readers automatically, mirror the form-control patterns consumers already know, and inherit standard `disabled` handling. The alternative — a `<command-item-ui>` custom element — would duplicate the surface without adding capability.

If you're coming from a primitive that uses `*-item-ui` children (e.g. `<tree-item-ui>` inside `<tree-ui>`, `<menu-item-ui>` inside `<menu-ui>`), the `<option>`-based API here is the deliberate exception. Other primitives ship custom-element children because their item state (open / selected / expanded) is rich; commands are stateless declarative entries.

## Attributes

| On `<command-ui>` | Type | Default | Description |
|---|---|---|---|
| `open` | boolean | `false` | Whether the palette is visible (reflected — set via property or attribute). |
| `placeholder` | string | `Type a command…` | Search input placeholder. |

| On each `<option>` | Description |
|---|---|
| `value` | Identifier emitted in the `select` event detail. |
| `data-icon` | Phosphor icon name (rendered as `<icon-ui>` to the left of the label). |
| `data-shortcut` | Keyboard shortcut hint string (rendered to the right, e.g. `⌘K`, `⇧E`). Display-only — wiring the actual shortcut is the consumer's responsibility (see "Wiring a global Cmd+K trigger" below). |
| `data-keywords` | Extra space-separated keywords folded into the search index alongside the label. |
| `disabled` | Boolean — render as inactive, skipped during keyboard navigation. |

| On each `<optgroup>` | Description |
|---|---|
| `label` | Section heading. Propagates to `event.detail.category` on selection (§-TBD / FB-50 #3, v0.5.19+). |

## Events

```ts
type CommandSelectDetail = {
  value: string;     // option's [value]
  label: string;     // option's text content
  category: string;  // parent <optgroup label="…"> label, or '' if the option has no group
};
```

```js
palette.addEventListener('select', (e) => {
  const { value, label, category } = e.detail;
  switch (category) {
    case 'Navigation': router.navigate(value); break;
    case 'Edit':       editor.invoke(value);   break;
    default:           runCommand(value);
  }
});

palette.addEventListener('dismiss', () => {
  // User pressed Escape — close it
  palette.open = false;
});
```

The palette doesn't auto-close on selection — the consumer decides whether to stay open (multi-action flow) or dismiss (typical single-action flow):

```js
palette.addEventListener('select', () => { palette.open = false; });
```

## Slots

| Slot | What goes inside |
|---|---|
| `empty` | Shown when the filter has no matches. Use for a "No commands match" affordance or a "Create new" call-to-action. |
| `footer` | Persistent footer band — usually a `<kbd>` hint bar like `↑↓ to navigate · ↵ to select · esc to close`. |

```html
<command-ui open placeholder="Type a command…">
  <option value="…">…</option>

  <div slot="empty">
    <text-ui variant="caption" muted>No commands match — try a different search.</text-ui>
  </div>

  <div slot="footer">
    <kbd>↑</kbd> <kbd>↓</kbd> navigate · <kbd>↵</kbd> select · <kbd>esc</kbd> close
  </div>
</command-ui>
```

## Wiring a global Cmd+K trigger

Common pattern — global keyboard listener flips the `[open]` property:

```js
const palette = document.querySelector('command-ui');

window.addEventListener('keydown', (e) => {
  // Cmd+K on Mac, Ctrl+K elsewhere
  if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
    e.preventDefault();
    palette.open = true;
  }
});

palette.addEventListener('dismiss', () => { palette.open = false; });
palette.addEventListener('select',   () => { palette.open = false; });
```

Once `open` flips to true, the input auto-focuses and Escape / outside-click fire `dismiss`. The palette handles arrow-key navigation, Enter to select, and recent-items recall on its own.

## Dynamic command lists

If your commands change reactively (signal-bound, fetched, etc.), use `palette.setItems([…])` to swap the entire item set in one call. Each item is `{ value, label, icon?, shortcut?, keywords?, disabled?, category? }`:

```js
palette.setItems([
  // ungrouped items
  { value: 'new', label: 'New file', icon: 'plus', shortcut: '⌘N' },

  // a group — pass items inline, the palette renders the group header
  { label: 'Recent', items: [
    { value: 'open:foo.ts', label: 'foo.ts' },
    { value: 'open:bar.ts', label: 'bar.ts' },
  ]},
]);
```

Or stay declarative and update the `<option>` / `<optgroup>` children directly — the palette re-parses on every `open` flip and when `setItems()` is called.

## Recent items

The palette automatically tracks the last 10 selected values and shows them as a "Recent" section when the search input is empty. Items appear in MRU order; selections promote items to the top. Recent items are filtered against the current item set, so a stale recent (item removed via `setItems()`) is silently dropped.

This is a built-in behavior — no consumer wiring needed. To disable, override with an explicit empty group at the top, or skip and use the section-based grouping pattern instead.

## Required icons

`<command-ui>` auto-stamps the `magnifying-glass` icon in the search field. Per `command.yaml` `requiredIcons:`, this is aggregated into the global icon-load manifest by `installIconLoadersForRegistered()`. Any additional `data-icon="<name>"` on options must be in the Phosphor icon set (or registered via `IconRegistry.register(name, svgString)`).

## Accessibility

- The search input has `role="combobox"`, `aria-expanded`, `aria-controls`, and `aria-activedescendant` wiring per WAI-ARIA combobox pattern.
- Each rendered option has `role="option"` and reflects `aria-disabled` from the source `<option>`.
- The active item (keyboard-highlighted) gets `data-active` for CSS targeting + `aria-activedescendant` reference from the input.
- Escape fires `dismiss` (not `close`) — the consumer decides whether to actually close.

## Related primitives

- `<menu-ui>` — for static actionable lists tied to a trigger (e.g. context menu). Not searchable.
- `<select-ui>` — single-value form input with options. Not searchable.
- `<tree-ui>` — hierarchical navigation with expand/collapse. Carries selection state.

`<command-ui>` is the right choice when: (a) the user is typing to filter from a list, (b) the action set is broad (10+ items), (c) keyboard-first navigation is the primary affordance, OR (d) you need a Cmd+K-style global launcher.
