# @macrulez/vue-command-palette — AI Reference Headless Command+K palette for Vue 3: a fuzzy-search command registry (`CommandStore`), a global keyboard-shortcut manager (`KeyboardManager`), a composable (`useCommandPalette`) exposing everything reactively, and one fully-featured, unstyled-by-default (but shipping a real `style.css`) `` component with grouping, nested sub-palettes/pages, async search, recent/pinned/frecency tracking, multi-select, a preview pane, and a secondary-actions menu. Zero runtime dependencies beyond Vue (peer). Optional Nuxt module. Version 0.2.8. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript/Vue source (not summarized from prose docs). For human-readable narrative docs, see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/vue-command-palette/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/vue-command-palette/guide/overview - GitHub: https://github.com/macrulezru/vue-command-palette - npm: https://www.npmjs.com/package/@macrulez/vue-command-palette Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map | Import path | Contains | |---|---| | `@macrulez/vue-command-palette` | Everything: plugin, composables, core factories, all 4 components, all types (section 2 onward). | | `@macrulez/vue-command-palette/nuxt` | The Nuxt module default export (section 10). | | `@macrulez/vue-command-palette/testing` | `createPaletteContext`, `PaletteProvider` — test-only context builders (section 11). | | `@macrulez/vue-command-palette/style.css` | All component CSS (custom properties + `.vcp-*` classes) — not auto-injected, import it yourself (or the Nuxt module does it for you, section 10). | `peerDependencies`: `vue: ^3.3.0` (required), `@nuxt/kit: >=3.0.0` (`peerDependenciesMeta`-optional — only needed if you use `/nuxt`). `compareByRelevance` is a real named export from `FuzzySearch.ts` but is **not** re-exported from the package root — only reachable via `fuzzySearch`'s own internal use of it, or a deep import. --- ## 2. Core types (`Command`, `CommandGroup`, `PaletteOptions`, …) ```ts interface Command { id: string label: string description?: string group?: string // group id this command belongs to (set on registration, not required to match a real group) keywords?: string[] aliases?: string[] icon?: Component | string shortcut?: string[] // display-only unless PaletteOptions.bindShortcuts is true — see 8.1 disabled?: boolean enabled?: () => boolean // re-checked on every render; disabled=true always wins over enabled() disabledReason?: string // shown as a `title` tooltip, ONLY when the item is actually disabled badge?: string | { text: string; color?: string } confirm?: string // confirmation prompt text — see execute() precedence, 9.1.7 perform: () => void | Promise subCommands?: Command[] // opens a nested sub-palette page?: CommandPage // opens a dedicated page with its own input/search actions?: CommandAction[] // secondary actions menu, opened with Tab info?: string // rendered via v-html in the preview pane — see gotcha 12 data?: T } interface CommandPage { placeholder?: string items?: Command[] // static items shown at empty query; also the fallback source for filterStatic (9.1.6) onSearch?: (query: string) => Command[] | Promise // debounced 200ms } interface CommandAction { id: string; label: string; icon?: Component | string; shortcut?: string[] perform: () => void | Promise } interface CommandGroup { id: string; label: string priority?: number // higher = earlier in getSortedGroups() — NOT the on-screen order in search mode, see gotcha 2 commands: Command[] onSearch?: (query: string) => Promise[]> // debounced 200ms, merged with sync results } interface PaletteMode { // a prefix-activated search scope, e.g. '>' for commands, '@' for people prefix: string; placeholder?: string; label?: string onSearch?: (query: string) => Command[] | Promise } interface SearchResult { command: Command score: number matches: Array<[start: number, end: number]> // ALWAYS from the label match, even if another field won the score — see 6's scoring notes groupId?: string parents?: Command[] // ancestor chain, for nested-result breadcrumbs matchedField?: 'label' | 'description' | 'keyword' | 'alias' matchedText?: string // the actual matched keyword/alias text, for a hint when the label itself didn't match } type SearchFn = (query: string, commands: Command[]) => SearchResult[] interface CommandUsage { count: number; lastUsed: number } // frecency stats interface PaletteOptions { name?: string // default 'default' — instance name, for multiple palettes (section 8.3) hotkey?: string[] // default ['$mod', 'k'] persistRecent?: boolean // default true — also gates pinned + frecency persistence, see below maxRecent?: number // default 5 maxRecentPerGroup?: number // default 0 (unlimited) — NOTE: stored on PaletteContext but not read by useCommandPalette.ts's own recent logic (see gotcha) localStorageKey?: string // default 'vcp:recent' — also the base for ':pinned' and ':frecency' suffixed keys, see 8.2 colorTheme?: 'light' | 'dark' | 'system' // default 'system' search?: SearchFn // replaces the built-in fuzzySearch entirely searchNested?: boolean // default true — flattens subCommands into the searchable set showDisabled?: boolean // default false frecency?: boolean // default false onSearch?: (query: string) => Command[] | Promise // global async source, merged into every query bindShortcuts?: boolean // default false — see 8.1 and gotcha 4/5 onOpen?: () => void onClose?: () => void onError?: (err: unknown, command: Command) => void // if unset, errors from perform() go to console.error onHighlight?: (command: Command | null) => void // fires when the keyboard-active command changes } ``` `PALETTE_INJECT_KEY`/`PALETTE_REGISTRY_KEY`/`PALETTE_LABELS_KEY`/ `PALETTE_SELECTION_KEY`/`PALETTE_QUERY_KEY`/`PALETTE_PINNED_KEY` are exported `Symbol`s — only `PALETTE_INJECT_KEY`/`PALETTE_REGISTRY_KEY` are actually used by the shipped code (provide/inject for the default instance and the named-instance registry); the other four are declared but not read/provided anywhere in `src/` — safe to ignore. --- ## 3. `createCommandStore(searchFn?, searchNested = true, scoreBonus?, showDisabled = false)` ```ts function createCommandStore( searchFn?: SearchFn, searchNested?: boolean, scoreBonus?: (command: Command) => number, showDisabled?: boolean, ): { state: { groups: Map; commands: Map } // reactive() registerCommands(commands: Command[], groupId?: string): () => void registerGroup(group: CommandGroup): () => void getAllCommands(): Command[] findCommand(id: string): Command | undefined search(query: string): SearchResult[] getSortedGroups(): CommandGroup[] } ``` - **`state.commands` holds only top-level commands — `subCommands` are never separately registered into it.** `findCommand(id)` does a DFS into `subCommands` (arbitrary depth) only when the direct top-level lookup misses — this is how "recent"/"pinned" can resolve a nested leaf command's id without it being a first-class registry entry. This same fact means anything driven by iterating `state.commands` directly (like `PaletteOptions.bindShortcuts`, section 8.1) never sees subCommands. - `registerCommands(commands, groupId?)`: always registers into `state.commands`. If `groupId` is given but no group with that id is currently registered, the group-attachment is **silently skipped** — the commands are still globally registered, just not listed under that group. It never auto-creates the group. - `registerGroup(group)`: wraps the group in its own `reactive()`, seeds every one of `group.commands` into `state.commands` too. - `search(query)`: returns `[]` immediately for a blank/whitespace query (no trim-then-search-empty-string path). With `searchNested` (default true), the entire command tree is **re-flattened and its ancestor chains rebuilt on every call** (not cached) — happens on every keystroke. A custom `searchFn` fully replaces `fuzzySearch`, but the store still assigns `groupId` (first-registered-group-wins) and `parents` afterward, and applies `scoreBonus` (frecency) by mutating `result.score += scoreBonus(command)`, only re-sorting via `compareByRelevance` if `scoreBonus` is actually set — so a custom `searchFn`'s own ordering survives untouched when frecency is off. - `getSortedGroups()`: descending by `priority ?? 0`; ties keep Map insertion (registration) order (stable sort). --- ## 4. `fuzzySearch(query, commands, includeDisabled = false)` — the scoring algorithm Not a Levenshtein/Fuse-style library — a hand-rolled 4-tier scorer (`scoreMatch`, internal) run per-field: ``` 1. Exact match (normalized) → score 100 2. Prefix match (text.startsWith(query)) → score 80 3. Substring match (text.includes(query)) → score 60 4. Ordered-subsequence ("fuzzy") match → score in (0, 40], floor 1 — every query char must appear in order; if not all found, EXCLUDED (internal score -1, never returned) rather than merely scored low. — score = max(1, 40 - penalty*20), penalty = totalCharacterGap / text.length ``` **Normalization**: `str.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase()` — `café` matches a query of `cafe`. A parallel index-mapping function translates match ranges computed in normalized space back to original string offsets (NFD decomposition can change string length). **Per-command scoring** (`fuzzySearch`'s loop): - `label` is **always** scored first; its result becomes `best` and is always what `SearchResult.matches` reports — **even when a different field ends up winning the actual `score`**. If a keyword/alias/ description produces a higher score, `matchedField`/`matchedText` update, but the returned `matches` array only ever reflects the label (empty `[]` if the label itself scored 0) — the UI shows a `matchedText` hint instead of a highlighted label in that case. - **`description` is capped to substring-or-better** (`score >= 60`) — it never falls through to the loose subsequence tier, specifically to avoid long descriptions producing fuzzy false-positives. A description containing "error" will NOT match a query of "err" via fuzzy-subsequence (blocked); it WOULD match via substring if "err" is literally contained. - `keywords`/`aliases` use the full 4-tier scorer including the fuzzy tier; each only replaces `best` if **strictly greater** — ties keep whichever field was checked first (label > description > keywords > aliases, in iteration order). - A command is included only if `best > 0` — note `0` itself (a real, valid — if meaningless — score) is excluded, same as the `-1` no-fuzzy-match sentinel. - `isCommandAvailable(cmd) = !disabled && (enabled ? enabled() : true)` — unavailable commands are skipped entirely unless `includeDisabled`. - **Sort**: with `includeDisabled`, available commands are hoisted above disabled ones first, then both groups sorted by `compareByRelevance`; otherwise just `compareByRelevance` directly. ```ts function compareByRelevance(a: SearchResult, b: SearchResult): number // A non-empty `matches` (i.e. the LABEL matched) always outranks a // match found only in description/keyword/alias, regardless of raw // score — then by score descending. Shared by frecency re-sort and by // CommandPalette.vue's own async-result merging (section 9). function getMatchRanges(query: string, text: string): Array<[number, number]> // Same scorer, any field — for highlighting a result that came from an // external/async source instead of fuzzySearch itself. [] if no match. function highlightMatches(label: string, matches: Array<[number, number]>): VNode // Builds alternating / VNodes. Assumes // `matches` ranges are sorted and non-overlapping. ``` Case-insensitive throughout (both text and query lowercased). --- ## 5. `createKeyboardManager()` — global hotkeys and key sequences ```ts function createKeyboardManager(): { registerShortcut(keys: string[], handler: () => void): () => void start(): void // idempotent; no-op if already listening or `document` is undefined (SSR-safe) stop(): void } ``` - **One global `document.addEventListener('keydown', ...)`** shared by every registered shortcut. `registerShortcut`'s unregister closure splices the exact entry **object** out by reference (`indexOf`) — two separately-registered shortcuts with textually-identical `keys` are independently removable. - **Two shortcut kinds**, auto-detected: a **sequence** shortcut is `keys.length >= 2` where every key is a non-modifier (`['g','h']`); anything else (including a single bare key like `['k']`) is a **hotkey** shortcut, matched via strict modifier equality — declared modifiers must ALL be pressed, and any modifier **not** declared must be **absent** (registering `['k']` will not fire while Ctrl is held, even though `'k'` alone was requested). - **`$mod`** = `event.metaKey || event.ctrlKey` (either satisfies it — OS-specific ⌘-vs-Ctrl *display* formatting is a separate concern, done in `CommandItem.vue`, not here). - **Dispatch per keydown**: (1) check every hotkey shortcut first — while focus is on an editable target (`input`/`textarea`/`select`/ `contenteditable`), only Ctrl/Meta-combo hotkeys still fire; a bare-key or Shift-only hotkey is suppressed. First match wins, `preventDefault()` + call + return. (2) If no hotkey matched and target isn't editable: push the lowercased key onto a **sequence buffer** (auto-cleared after 500ms of inactivity, timer reset on every keypress), then check each sequence shortcut's needed-keys against the buffer's **tail** — `['g','h']` still matches after typing `x g h` (leading noise is tolerated as long as the 500ms window never lapses). Modifier keys (Ctrl/Meta/Alt — **not Shift**) on the keydown event abort sequence matching for that keystroke entirely. - **No `stopPropagation()` anywhere** — only `preventDefault()` on an actual match. No special-casing of reserved browser/OS shortcuts. --- ## 6. `installPalette` / `VCommandPalettePlugin` / `createCommandPalette` ```ts const VCommandPalettePlugin: { install: typeof installPalette } function installPalette(app: App, options?: PaletteOptions): void function createCommandPalette(options?: PaletteOptions): { install(app: App): void } // A fresh plugin object per call — needed because app.use() dedupes by // object identity, which would otherwise silently skip a 2nd named instance. ``` ```vue app.use(VCommandPalettePlugin, { hotkey: ['$mod', 'k'] }) // default instance app.use(createCommandPalette({ name: 'sidebar', hotkey: ['$mod', 'j'] })) // an additional named instance ``` ### 6.1 Persistence — 3 independent `localStorage` keys `localStorageKey` (default `'vcp:recent'`) is the base for three actual keys, only discoverable by reading source: - `` — recent command ids (array) - `:pinned` — pinned command ids (array) - `:frecency` — `Record` usage stats All three reads are wrapped in their own try/catch and **silently ignore** malformed JSON or unavailable storage (no error surfaced to `onError`). **`persistRecent` alone gates all three** — there is no separate `persistPinned`/`persistFrecency` flag. `frecency: true` without `persistRecent: true` still tracks usage counts for the current session in memory, just never writes them to storage. ### 6.2 Frecency formula (not documented anywhere in types) ```ts bonus = stat.count * 2 + max(0, 1 - ageDays/30) * 15 ``` Linear recency decay to 0 over 30 days; unbounded growth from repeated use. This bonus is **added directly onto the 0–100 fuzzy score** — a single very-recent use contributes at most ≈17, not enough alone to beat a substring match (60), but repeated recent use easily can (5 recent uses ≈ +25, enough to invert fuzzy-tier (≤40) results and even threaten substring-tier ones). Applied inside `CommandStore.search()`, only when `frecency: true`. ### 6.3 Default/singleton instance precedence Every installed instance is registered by `name` into a shared `Map` (`PALETTE_REGISTRY_KEY`, get-or-create via `app.provide`). **Separately**, the *first* instance installed also claims the default/singleton slot (`PALETTE_INJECT_KEY`, what `useCommandPalette()` with no name resolves) — UNLESS an instance explicitly named `'default'` is installed, which **always** claims that slot regardless of install order. So: install order determines the default slot only among non-`'default'`-named instances; a `name: 'default'` instance always wins it whenever it appears. ### 6.4 The global hotkey and `bindShortcuts` are two separate open/close implementations The `hotkey` (default `['$mod','k']`) toggle registered in `installPalette` flips `isOpen`/resets `query`+`activeIndex`/calls `onOpen`/`onClose` **inline**, NOT by calling `useCommandPalette()`'s own `toggle()`/`open()`/`close()`. Functionally similar but a fully independent second implementation of the same semantics — e.g. it does **not** clear `history` the way `close()` does. `bindShortcuts: true` reactively (un)registers a global hotkey for every **top-level** command's `shortcut` field (`watch` on `store.state.commands`, `immediate: true`) — **never for subCommands' shortcuts**, since `state.commands` never contains them (section 3). When a bound shortcut fires: if a `` is mounted, it delegates to the component's own UI-aware `execute()` (via `ctx.executeRequest`); otherwise a **materially weaker fallback** (`runFallback`, local to `plugin.ts`) runs instead — it handles disabled/ `enabled()` and `subCommands` (just opens), but has **no `confirm` handling, no frecency/recent tracking, and mishandles a `page`-only command** (checks only `subCommands?.length`, so a command with `page` but no `subCommands` falls through and calls `perform()` directly instead of opening the page). --- ## 7. `useCommandPalette(name?)` ```ts function useCommandPalette(name?: string): { isOpen: Readonly> query: Ref results: ComputedRef // relevance-sorted; NOT the on-screen order — see 9.1's orderedResults note activeIndex: Ref history: Readonly>> loadingCommandId: Readonly> colorTheme: Ref<'light' | 'dark' | 'system'> open(paletteId?: string): void close(): void toggle(): void goBack(): void executeActive(): Promise executeCommand(cmd: Command): Promise getRecentCommands(): Command[] getPinnedCommands(): Command[] registerCommands(commands: Command[]): () => void registerGroup(group: CommandGroup): () => void addRecent(id: string): void isPinned(id: string): boolean pin/unpin/togglePin(id: string): void pinnedIds: Readonly> queryHistory: Readonly> } function resolvePaletteContext(name?: string): PaletteContext // throws if not installed / name not found function useRegisterCommands(commands: Command[], name?: string): void // registers + onUnmounted cleanup — MUST be called during setup() function useRegisterGroup(group: CommandGroup, name?: string): void // same ``` - **`open(paletteId?)`** has two distinct branches: if already open AND `paletteId` given → **pushes** onto `history` (drill into a nested sub-palette), resets query/index, does **not** call `onOpen`. If not already open → opens fresh, resets query/index, calls `onOpen`, and **ignores `paletteId` entirely** (a subtlety: `open('x')` while closed behaves exactly like `open()`). - **`close()`** clears `isOpen`/`query`/`activeIndex` AND the **entire** `history` array (not just the top level) — there is no way to close and later resume mid-drill-down. - **`goBack()`**: pops one `history` entry; if history was already empty, calls `close()` instead (so "back" at the top level closes). - **`addRecent(id)`**: de-dupes, unshifts, truncates to `maxRecent`. **The in-memory `recentIds` ref is always updated regardless of `persistRecent`** — only the `localStorage.setItem` call is gated. - **`executeCommand(cmd)`**: guards `disabled`/`!enabled()`. If `cmd.subCommands?.length || cmd.page` → calls `open(cmd.id)` and **returns before ever calling `perform()`** — `perform` is genuinely unreachable for a group/page command (by design; write `perform: () => {}` for such commands, matching the package's own demo convention). For a real leaf command: `addRecent` → `recordUsage` (frecency) → `recordQuery` (query history, capped at a hardcoded **25** entries) → `close()` — **then** `perform()` is awaited, meaning the palette is already closed while an async `perform()` is still in flight. Errors go to `onError?.(err, cmd)` if provided, else `console.error`. - **`executeActive()`**: reads from `ctx.currentResults` if non-empty, else falls back to the raw `results` computed. `currentResults` is only kept in sync by a **mounted** `` (via its own `orderedResults` watcher, section 9) — call this with no palette mounted and you get relevance-sorted `results` instead of what a user would actually see on screen (which reflects grouped/visual order, not raw relevance). --- ## 8. `` — the one stateful component ```ts interface Props { name?: string // which named instance to use (undefined = default) placeholder?: string // default 'Search commands…' maxResults?: number // default 10 emptyText?: string // default 'No commands found.' loadingText?: string // default 'Loading…' teleportTo?: string // default 'body' — passed straight to , unvalidated theme?: 'default' | 'compact' // default 'default' animationDuration?: number // default 150 — DEAD PROP, never read; CSS fade is a hardcoded 150ms labels?: Partial // merged over built-in defaults groupRecent?: boolean // default false — cluster recent-commands view by group modes?: PaletteMode[] // prefix-activated search scopes selectable?: boolean // default false — multi-select mode preview?: boolean // default false — preview pane for command.info previewHotkey?: string[] // default ['$mod', 'i'] } // Emits: 'submit-selection'[commands: Command[]] — only fires in selectable mode, $mod+Enter with a non-empty selection ``` **Slots**: `#trigger({open, toggle})`, `#header`, `#input({query, onInput})`, `#item` (per-row override; also `#item-icon`/`#item-shortcut` for partial overrides), `#group-header`, `#empty({query})`, `#footer`, `#actions({command, run, activeIndex, close})`, `#preview({command})`. ### 8.1 Result ordering — three DIFFERENT orders exist simultaneously - **`displayResults`**: relevance-sorted (sync `ctx.results` merged with async `asyncResults`, capped at `maxResults`). Sync+async dedup by id is **first-registered wins** — a slower-arriving async result with a strictly better score than an already-merged sync result of the same id is simply **dropped**, never used to replace it. - **`groupedResults`**: groups ordered by the **rendered rank of each group's best-matching item within `displayResults`** — explicitly NOT by `CommandGroup.priority`. Ungrouped results render after all groups. - **`orderedResults`** (= `groupedResults` flattened + `ungroupedResults`): the actual on-screen order — this is what `activeIndex` indexes into, and it's kept in sync into `ctx.currentResults` via a `watch(..., {immediate:true})`, which is exactly what makes `executeActive()` (section 7) match the visual selection only while this component is mounted. Use `displayResults`'s relevance order when reasoning about "what's the best match"; use `orderedResults`/`ctx.currentResults` when reasoning about "what's on screen right now." ### 8.2 Three independent async pipelines share ONE `isLoading` flag Group/global `onSearch` (`asyncResults`), page `onSearch` (`pageResults`), and mode `onSearch` (`modeResults`) are each debounced 200ms **independently**, and each sets the *same* shared `isLoading` ref true/false around its own fetch. Two of these racing (e.g. switching from a page view to a mode view while the page's async search is still in flight) can make the loading spinner clear prematurely while the other pipeline is still pending — there is no ref-counting. ### 8.3 `execute(command)` precedence (exact order) ``` 1. props.selectable → toggle selection, return (never executes) 2. command.confirm → open the palette if closed, show confirm dialog, return 3. subCommands/page → open() the sub-palette/page if palette was closed 4. → executeCommand(command) ``` **A command with both `confirm` and `subCommands`/`page` always hits the confirm branch — the sub-palette/page can never open for it**, since `confirm` is checked first and returns unconditionally. ### 8.4 No-query view vs search-mode view are separate code paths With an empty query: `noQueryFlat` = pinned commands → recent commands → every group's commands flattened (via `allGrouped`, sorted by `CommandGroup.priority` this time — the opposite of `groupedResults`' rank-based order used in search mode). `groupRecent: true` clusters `recentCommands` into sections by **contiguous** same-group runs (not a full re-group) — the same group id appearing twice non-contiguously in the recent list produces two separate sections with the same header. ### 8.5 Sub-command/page static filtering is a WEAKER algorithm than fuzzySearch `filterStatic(items, query)` (used for a page's static `items` with no `onSearch`, and for a sub-command list) is a **plain lowercase substring check** over `label`/`description`/`keywords` only — no `aliases`, no fuzzy-subsequence tier. A prefix `PaletteMode` with no `onSearch` does NOT use this — it falls through to real `ctx.store.search()` (full fuzzy) instead; `filterStatic` is specifically the page/subCommand path. ### 8.6 Keyboard behavior inside the open dialog - **Backspace at an empty query** navigates back one level (`goBack`-equivalent) rather than being a no-op. - **`Tab`** (no Shift) opens the secondary-actions menu if `activeCommand.actions?.length` exists; Shift+Tab always goes through the normal focus-trap instead. Inside the actions menu: Arrow Up/Down wrap around, Enter runs the action, Escape/Tab/Backspace all close it. - **Global `$mod+P`** toggles pin on the currently-active command in every view (search, no-query, nested) — `activeCommand` is computed uniformly across all three. - **Touch swipe-back**: `touchend` with horizontal delta `>70px` and vertical delta `<50px`, only when `history` is non-empty. No swipe-forward gesture exists. - **Confirm dialog and actions menu remove the `` from the DOM** entirely while open — focus is explicitly redirected to the dialog element (`tabindex="-1"`) so Escape/Enter still reach the right handler; otherwise key events would land on ``. ### 8.7 Virtualization `VirtualList` activates only when `displayResults.length > 50` (hardcoded `VIRTUAL_THRESHOLD`), fixed `ITEM_HEIGHT = 40`, `LIST_HEIGHT = 360` — not configurable via props, and **never** active for the no-query flat view, nested subCommand view, or page view regardless of item count. The virtualizer assumes uniform row height, but `flatItems` (fed to it) interleaves group-header/section-divider rows at the same 40px height as command rows — a real (cosmetic) mismatch for tall grouped result sets. ### 8.8 Misc component facts - `command.info` renders via **`v-html`** in the preview pane — a real HTML-injection surface if `info` ever derives from untrusted input (an `eslint-disable-next-line vue/no-v-html` acknowledges this in source). - Body scroll lock: `document.body.style.overflow = 'hidden'` on open, reset to `''` (not the prior value) on close — a non-default `body` overflow set by the host app before opening is lost, not restored. - `aria-expanded="true"` on the search `` is **hardcoded** — it doesn't reflect actual open/closed state. - `aria-live="polite"` results-count announcement only fires in query/search mode (`isOpen && query.trim()`) — silent for the no-query, nested, and page views. --- ## 9. ``, ``, `` **`CommandItem`**: `props: {command, active, matches, itemId, loadingCommandId?, parents?, matchedText?, alwaysShowPin?}`, `emits: execute, activate`. `role="option"`, `aria-selected`, `aria-disabled`; `title` (disabled-reason tooltip) is set **only** when actually disabled. Click is guarded by `!isDisabled && !isLoading`, but `mouseenter`/`activate` only checks `!isDisabled` (hovering a currently-executing async item still moves keyboard focus to it). **Description highlighting here is a second, independent, non- diacritic-aware substring-match implementation**, separate from `FuzzySearch.ts`'s real scorer — a description containing `café` will NOT visually highlight for a query of `cafe`, even though the actual match score (computed elsewhere) would have matched it. Platform detection for the `⌘`/`Ctrl` shortcut-key label (`isMac`/`formatKey`) is internal to this component, not exported — there's no public utility to reuse the same formatting elsewhere. Pin-icon and chevron columns are **always rendered** (reserved layout space), visibility purely CSS-driven. **`CommandGroup`**: pure presentational — `props: {group, items, activeIndex, globalOffset, loadingCommandId?}`, renders nothing if `items` is empty. **`VirtualList`** (`