# Command Palette

## Usage

`CommandPalette` is a searchable command list. It owns the dialog, the search field, grouping, the empty state, and keyboard movement. The commands themselves come from a `SearchSource`. An empty query calls `bootstrap`; a typed query calls `search`.

`isInline` draws the palette in the page instead of a modal. Production palettes leave it off. Opening from a shortcut is the page's job: `useHotkeys` already handles `mod+k`.

```vue
<script setup lang="ts">
import { createStaticSource, useHotkeys } from '@astryx-vue/core'
import { CommandPalette } from '@astryx-vue/core/CommandPalette'
import { ref } from 'vue'

const isOpen = ref(false)
const source = createStaticSource([
  { id: 'home', label: 'Home' },
  { id: 'settings', label: 'Settings' },
])

function openPalette() {
  isOpen.value = true
}

useHotkeys([{ keys: 'mod+k', onPress: openPalette }])
</script>

<template>
  <CommandPalette
    :is-open="isOpen"
    :search-source="source"
    @open-change="isOpen = $event"
  />
</template>
```

`v-model:value` turns the palette into a picker: the matching row stays selected, and choosing a row writes that id. `auxiliaryData.group` splits the list into groups in first-seen order. The `item` slot replaces the row label; `footer` replaces the hint row.

Arrow keys move the highlight and do not wrap. Home and End stay in the input and leave the highlight where it is. Page Up jumps to the first enabled row, and Page Down jumps to the last. Enter selects the highlighted row. Escape closes the palette and drops an in-flight search. Moving the pointer highlights a row without scrolling the list. The highlight stays empty until the pointer or the keyboard moves it.

### Anatomy

- **Dialog**: Modal surface that contains the command palette.
- **Input**: Search region containing the query field and its supporting visuals.
- **Search glyph**: Search symbol rendered by Icon in the default Input.
- **Query field**: Native text field used to enter a search query.
- **Loading spinner**: Spinner shown in the default Input while a search is pending.
- **List**: Scrollable listbox containing the current results or Empty state.
- **Item**: Selectable command result rendered inside the List.
- **Group**: Optional collection of Items that share a heading.
- **Group heading**: Visible heading rendered for a Group.
- **Empty**: Message shown when the current result set is empty.
- **Footer**: Footer region for default keyboard guidance or caller-provided content.
- **Keyboard shortcut**: Painted key badges rendered by Kbd in the default Footer guidance.

## Examples

### Async search

Search waits briefly, then filters filenames. An empty query asks for a filename.

```vue
<script setup lang="ts">
import type { SearchSource } from '@astryx-vue/core'
import { CommandPalette, CommandPaletteInput } from '@astryx-vue/core/CommandPalette'

const files = [
  { id: 'readme', label: 'README.md' },
  { id: 'package', label: 'package.json' },
  { id: 'tsconfig', label: 'tsconfig.json' },
  { id: 'index', label: 'src/index.ts' },
  { id: 'app', label: 'src/App.tsx' },
]

const source: SearchSource = {
  async search(query: string) {
    await new Promise(resolve => setTimeout(resolve, 400))
    const normalized = query.toLowerCase()
    return files.filter(file => file.label.toLowerCase().includes(normalized))
  },
  bootstrap() {
    return []
  },
}
</script>

<template>
  <CommandPalette
    empty-bootstrap-text="Type a filename to search"
    empty-search-text="No files found"
    is-inline
    is-open
    :search-source="source"
  >
    <template #input>
      <CommandPaletteInput placeholder="Search files..." />
    </template>
  </CommandPalette>
</template>
```

### Grouped

Home and Settings sit under Navigation. New File and Save sit under Actions.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { CommandPalette } from '@astryx-vue/core/CommandPalette'

const source = createStaticSource([
  { id: 'home', label: 'Home', auxiliaryData: { group: 'Navigation' } },
  { id: 'settings', label: 'Settings', auxiliaryData: { group: 'Navigation' } },
  { id: 'new-file', label: 'New File', auxiliaryData: { group: 'Actions' } },
  { id: 'save', label: 'Save', auxiliaryData: { group: 'Actions' } },
])
</script>

<template>
  <CommandPalette is-inline is-open :search-source="source" />
</template>
```

### Custom footer

The hint row is replaced with a short note.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { CommandPalette, CommandPaletteFooter } from '@astryx-vue/core/CommandPalette'
import { Text } from '@astryx-vue/core/Text'

const source = createStaticSource([
  { id: 'home', label: 'Home' },
  { id: 'settings', label: 'Settings' },
])
</script>

<template>
  <CommandPalette is-inline is-open :search-source="source">
    <template #footer>
      <CommandPaletteFooter>
        <Text type="supporting">
          Pro tip: use ⌘K to open anywhere
        </Text>
      </CommandPaletteFooter>
    </template>
  </CommandPalette>
</template>
```

### Picker mode

Light, Dark, and System. The current theme keeps a check mark.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { CommandPalette } from '@astryx-vue/core/CommandPalette'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const theme = ref('light')
const source = createStaticSource([
  { id: 'light', label: 'Light' },
  { id: 'dark', label: 'Dark' },
  { id: 'system', label: 'System' },
])
</script>

<template>
  <CommandPalette v-model:value="theme" is-inline is-open :search-source="source">
    <template #item="{ item, isSelected }">
      <Text type="body" style="flex: 1">
        {{ item.label }}
      </Text>
      <span v-if="isSelected" class="css-icon css-icon--sm i-carbon-checkmark" aria-hidden="true" />
    </template>
  </CommandPalette>
</template>
```

### Rich items

Rows show the command and, when one is set, its shortcut.

```vue
<script setup lang="ts">
import type { SearchableItem } from '@astryx-vue/core'
import { createStaticSource } from '@astryx-vue/core'
import { CommandPalette } from '@astryx-vue/core/CommandPalette'
import { Kbd } from '@astryx-vue/core/Kbd'
import { Text } from '@astryx-vue/core/Text'

interface RichCommand extends SearchableItem<{ group?: string, shortcut?: string }> {}

const commands: RichCommand[] = [
  { id: 'settings', label: 'Open Settings', auxiliaryData: { group: 'Navigation', shortcut: 'mod+,' } },
  { id: 'profile', label: 'View Profile', auxiliaryData: { group: 'Navigation' } },
  { id: 'new-file', label: 'Create New File', auxiliaryData: { group: 'Actions', shortcut: 'mod+n' } },
  { id: 'search', label: 'Search Files', auxiliaryData: { group: 'Actions', shortcut: 'mod+p' } },
]

const source = createStaticSource(commands)

function shortcut(item: { auxiliaryData?: unknown }): string | undefined {
  const data = item.auxiliaryData
  if (data != null && typeof data === 'object' && 'shortcut' in data && typeof data.shortcut === 'string')
    return data.shortcut
  return undefined
}
</script>

<template>
  <CommandPalette is-inline is-open :search-source="source">
    <template #item="{ item }">
      <Text type="body" style="flex: 1">
        {{ item.label }}
      </Text>
      <Kbd v-if="shortcut(item)" :keys="shortcut(item)!" />
    </template>
  </CommandPalette>
</template>
```

## Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `isOpen` | `boolean` | - | Whether the palette is open. |
| `searchSource` | `SearchSource` | - | Where commands come from. An empty query calls `bootstrap`. |
| `isInline` | `boolean` | `false` | Render in the page flow instead of a modal dialog. |
| `label` | `string` | Command palette | Accessible name of the dialog. |
| `width` | `number \| string` | `640` | Width of the dialog. Numbers are pixels. |
| `maxHeight` | `number \| string` | `480` | Maximum height of the dialog. Numbers are pixels. |
| `emptySearchText` | `string` | No results | Shown when a query returns nothing. |
| `emptyBootstrapText` | `string` | Type to search | Shown before the source has anything to list. |
| `value` | `string` | - | Selected id in picker mode. Bind with `v-model:value`. |

## Events

| Name | Payload | Description |
| --- | --- | --- |
| `openChange` | `boolean` | The next open state. Escape and a selection both close the palette. |
| `update:value` | `string` | The id of the row that was selected. |
| `valueChange` | `string` | The same selection under the original callback name, for code ported from React. |

## Slots

| Name | Props | Description |
| --- | --- | --- |
| `input` | - | Replaces `CommandPaletteInput`. |
| `item` | `{ item, isSelected }` | Row content. `isSelected` is the picker match, not the keyboard highlight. |
| `footer` | - | Replaces the default hint row. Put a `CommandPaletteFooter` here when the replacement should keep the footer chrome. |

## CommandPaletteInput

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | Search… | Placeholder and the fallback accessible name. |
| `label` | `string` | - | Accessible name of the combobox. Falls back to `placeholder`. |
| `hasAutoFocus` | `boolean` | `true` | Focus the field when the palette opens. Inline palettes skip this. |
| `value` | `string` | - | Search value, bound with `v-model:value`. Read from the surrounding `CommandPalette` when omitted. |

| Name | Description |
| --- | --- |
| `valueChange` | Next search value. Fired only while `value` is bound; otherwise the input writes to the palette. |

The <code>endContent</code> slot (also available as <code>end</code>) is trailing content after the spinner.

## CommandPaletteList

`label` names the listbox. The default is Commands.

## CommandPaletteGroup

`heading` is the visible group label and the group's accessible name.

## CommandPaletteItem

`value` is the command id. `isDisabled` blocks selection. `isHighlighted` and `isSelected` override the state the row reads from the palette, which is what standalone previews need. `select` fires with the value when the row is activated by pointer. The default slot is the row content.

## CommandPaletteEmpty

The default slot is the empty message.

## CommandPaletteFooter

The default slot replaces the Navigate, Select, and Close hints. The footer chrome stays.
