# command-palette

Cmd/Ctrl+K command launcher. Headless: it wires the combobox/listbox ARIA
pattern over existing markup, filters items as the user types, hides empty
groups, manages active-descendant keyboard navigation and emits typed events —
it applies no visual styles (only ARIA + `data-state` / `data-active`).

## Markup

```html
<div data-c42-command-palette>
  <div data-c42-command-overlay></div>
  <div data-c42-command-dialog>
    <input data-c42-command-input placeholder="Type a command…" aria-label="Command" />
    <div data-c42-command-list>
      <div data-c42-command-group data-label="Files">
        <button data-c42-command-item data-value="new" data-keywords="create add">New File</button>
        <button data-c42-command-item data-value="open">Open File…</button>
      </div>
      <div data-c42-command-group data-label="Edit">
        <button data-c42-command-item data-value="copy">Copy Selection</button>
      </div>
      <div data-c42-command-empty>No matching commands</div>
    </div>
  </div>
</div>
```

### Markup parts

| Selector | Role |
|----------|------|
| `[data-c42-command-palette]` | Root (hidden until open; carries `data-state`) |
| `[data-c42-command-overlay]` | Optional backdrop; click closes when enabled |
| `[data-c42-command-dialog]` | The panel (focus trap target) |
| `[data-c42-command-input]` | Search field → becomes `role="combobox"` |
| `[data-c42-command-list]` | Results container → becomes `role="listbox"` |
| `[data-c42-command-group]` | Optional grouping; `data-label` renders a heading via CSS; auto-hidden when it has no visible items |
| `[data-c42-command-item]` | Selectable command → `role="option"`; `data-value` is the select payload (falls back to text); `data-keywords` adds to the searchable text |
| `[data-c42-command-empty]` | Shown only when nothing matches |

## Options

```ts
import { CommandPalette } from '@42/core/command-palette';

new CommandPalette(root, {
  defaultOpen: false,          // open on init (default false)
  hotkey: 'k',                 // Cmd/Ctrl+<key>; null disables (default 'k')
  closeOnOverlayClick: true,   // default true
  closeOnEscape: true,         // default true
  clearOnClose: true,          // reset the query on close (default true)
  filter: (query, text) => text.includes(query), // custom matcher
});
```

## Interaction

- **Open/close**: ⌘/Ctrl+K toggles (when `hotkey` set). Opening focuses the
  input and traps focus inside the dialog; closing restores the previously
  focused element.
- **Filter**: typing filters items by their text + `data-keywords` (lowercased).
  Groups with no remaining items are hidden; the empty element shows when the
  visible count is 0.
- **Navigate**: `ArrowUp`/`ArrowDown` move the highlight across *visible* items
  (wrapping), `Home`/`End` jump to first/last. The active item is tracked via
  `aria-activedescendant` on the input and `data-active="true"` on the item.
- **Select**: `Enter` or click activates the highlighted item, emits
  `commandpalette:select`, then closes. Items with `aria-disabled` are skipped.

## Methods

| Method | Description |
|--------|-------------|
| `openPalette()` | Open and focus the input |
| `close()` | Close and restore focus |
| `toggle()` | Toggle open state |
| `isOpen` | Boolean getter |
| `activeItem` | Currently highlighted item element, or `null` |
| `destroy()` | Remove all listeners (incl. the global hotkey) |

## Events

| Event | Detail |
|-------|--------|
| `commandpalette:open` | — |
| `commandpalette:close` | — |
| `commandpalette:select` | `{ value: string, item: HTMLElement }` |
| `commandpalette:filter` | `{ query: string, count: number }` |

## Notes

- The controller never renders items — populate the list yourself (server-side
  or client-side) and re-create the instance if the item set changes.
- The built-in `filter` is a case-insensitive substring match; pass a custom
  `filter` for fuzzy matching or scoring.
