@macrulez/vue-command-palette 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danil Lisin Vladimirovich (macrulez)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -33,7 +33,21 @@ Command+K palette for Vue 3. Fuzzy search with match highlighting, grouped comma
33
33
  - [Confirmation step](#confirmation-step)
34
34
  - [Async search](#async-search)
35
35
  - [Recent commands](#recent-commands)
36
+ - [Preview pane](#preview-pane)
37
+ - [Mobile / touch](#mobile--touch)
38
+ - [Pinned commands](#pinned-commands)
39
+ - [Secondary actions](#secondary-actions)
40
+ - [Multi-select](#multi-select)
41
+ - [Query history](#query-history)
42
+ - [Frecency](#frecency)
43
+ - [Modes / scopes](#modes--scopes)
44
+ - [Typed command data](#typed-command-data)
45
+ - [Custom search strategy](#custom-search-strategy)
46
+ - [Binding shortcuts](#binding-shortcuts)
47
+ - [Command pages](#command-pages)
48
+ - [Multiple instances](#multiple-instances)
36
49
  - [Theming](#theming)
50
+ - [Localization](#localization)
37
51
  - [Nuxt](#nuxt)
38
52
  - [Testing utilities](#testing-utilities)
39
53
  - [TypeScript types](#typescript-types)
@@ -45,7 +59,12 @@ Command+K palette for Vue 3. Fuzzy search with match highlighting, grouped comma
45
59
 
46
60
  ## Features
47
61
 
48
- - **Fuzzy search** — ranking: exact (100) › prefix (80) › substring (60) › fuzzy (1–40). Diacritic normalization so `café` matches `cafe`. Match highlighting via `<mark>` spans.
62
+ - **Fuzzy search** — ranking: exact (100) › prefix (80) › substring (60) › fuzzy (1–40). Searches label, `description`, `keywords` and `aliases`; label highlighting is preserved even when another field wins the score. Diacritic normalization so `café` matches `cafe`. Match highlighting via `<mark>` spans.
63
+ - **Pluggable search** — swap the built-in scorer for your own (e.g. Fuse.js) via the `search` option.
64
+ - **Bindable shortcuts** — `bindShortcuts: true` turns each command's `shortcut` into a real global hotkey.
65
+ - **Searchable nested commands** — sub-commands surface directly in search with breadcrumb context (`Change Theme › Light`); a `›` chevron marks items that open a sub-palette/page.
66
+ - **Command pages** — a command can open a `page` with its own placeholder and async search (filters, remote pickers, multi-step flows).
67
+ - **Multiple instances** — run several independent, named palettes on one app via `createCommandPalette()`.
49
68
  - **All-commands view** — palette shows all registered commands grouped on open; no empty screen.
50
69
  - **Grouped commands** — groups with headers, priority ordering, and visual section dividers.
51
70
  - **Recent commands** — `localStorage`-backed history of the last N executed commands shown at the top when the query is empty.
@@ -63,7 +82,7 @@ Command+K palette for Vue 3. Fuzzy search with match highlighting, grouped comma
63
82
  - **Nuxt module** — auto-installs the plugin via `nuxt.config.ts`.
64
83
  - **Testing utilities** — `createPaletteContext` + `PaletteProvider` for isolated unit tests.
65
84
  - **SSR-safe** — all browser API calls guarded with `typeof document !== 'undefined'`.
66
- - **Zero runtime dependencies** — only Vue 3 as peer dep. 10 KB gzip.
85
+ - **Zero runtime dependencies** — only Vue 3 as peer dep. ~11 KB gzip.
67
86
 
68
87
  ---
69
88
 
@@ -162,6 +181,7 @@ The root component. Renders a modal overlay with a search input, result list, an
162
181
 
163
182
  | Prop | Type | Default | Description |
164
183
  |---|---|---|---|
184
+ | `name` | `string` | — | Target a specific named palette instance (default singleton when omitted) |
165
185
  | `placeholder` | `string` | `'Search commands…'` | Input placeholder text |
166
186
  | `maxResults` | `number` | `10` | Maximum number of results shown |
167
187
  | `emptyText` | `string` | `'No commands found.'` | Text shown when search returns nothing |
@@ -169,6 +189,18 @@ The root component. Renders a modal overlay with a search input, result list, an
169
189
  | `teleportTo` | `string` | `'body'` | CSS selector for the `<Teleport>` target |
170
190
  | `theme` | `'default' \| 'compact'` | `'default'` | Compact uses a narrower, shorter dialog |
171
191
  | `animationDuration` | `number` | `150` | Fade transition duration in ms |
192
+ | `labels` | `Partial<PaletteLabels>` | — | Override built-in UI strings (i18n) — see [Localization](#localization) |
193
+ | `groupRecent` | `boolean` | `false` | Cluster recent commands by their `group` with sub-headers |
194
+ | `modes` | `PaletteMode[]` | — | Prefix-activated search scopes (see [Modes / scopes](#modes--scopes)) |
195
+ | `selectable` | `boolean` | `false` | Multi-select mode (see [Multi-select](#multi-select)) |
196
+ | `preview` | `boolean` | `false` | Show a preview pane for the active command (see [Preview pane](#preview-pane)) |
197
+ | `previewHotkey` | `string[]` | `['$mod', 'i']` | Key combo to toggle the preview pane (`[]` to disable) |
198
+
199
+ ### Emits
200
+
201
+ | Event | Payload | Description |
202
+ |---|---|---|
203
+ | `submit-selection` | `Command[]` | Emitted on `$mod+Enter` in [multi-select](#multi-select) mode |
172
204
 
173
205
  ### Slots
174
206
 
@@ -177,9 +209,11 @@ The root component. Renders a modal overlay with a search input, result list, an
177
209
  | `#trigger` | `{ open, toggle }` | Custom element that opens the palette |
178
210
  | `#header` | — | Content inserted above the search input |
179
211
  | `#input` | `{ query, onInput }` | Replace the default `<input>` entirely |
180
- | `#item` | `{ command, active, matches }` | Replace the entire result row |
212
+ | `#item` | `{ command, active, matches, parents, matchedText }` | Replace the entire result row (`parents` = breadcrumb context; `matchedText` = matching keyword/alias) |
181
213
  | `#group-header` | `{ group }` | Replace the group label row |
182
214
  | `#empty` | `{ query }` | Shown when the query returns no results |
215
+ | `#actions` | `{ command, run, activeIndex, close }` | Replace the secondary-actions menu |
216
+ | `#preview` | `{ command }` | Preview pane content for the active command (requires `preview` prop) |
183
217
  | `#footer` | — | Content below the result list |
184
218
 
185
219
  ### Custom `#item` slot
@@ -274,7 +308,7 @@ Renders a group header followed by its `CommandItem` rows. Passes all `#item`, `
274
308
 
275
309
  ## useCommandPalette
276
310
 
277
- Composable that exposes the global palette state and all control functions. Must be called inside a component tree where `VCommandPalettePlugin` is installed.
311
+ Composable that exposes the global palette state and all control functions. Must be called inside a component tree where `VCommandPalettePlugin` is installed. Pass an instance name — `useCommandPalette('sidebar')` — to target a [named instance](#multiple-instances).
278
312
 
279
313
  ```ts
280
314
  import { useCommandPalette } from '@macrulez/vue-command-palette'
@@ -294,9 +328,13 @@ const {
294
328
  executeCommand, // (cmd: Command) => Promise<void>
295
329
  executeActive, // () => Promise<void> — run the currently selected result
296
330
  getRecentCommands, // () => Command[]
331
+ getPinnedCommands, // () => Command[]
297
332
  registerCommands, // (commands: Command[]) => () => void
298
333
  registerGroup, // (group: CommandGroup) => () => void
299
334
  addRecent, // (id: string) => void
335
+ pin, unpin, togglePin, isPinned, // pinned commands API
336
+ pinnedIds, // Readonly<Ref<string[]>>
337
+ queryHistory, // Readonly<Ref<string[]>>
300
338
  } = useCommandPalette()
301
339
  ```
302
340
 
@@ -386,8 +424,15 @@ app.use(VCommandPalettePlugin, options)
386
424
 
387
425
  | Option | Type | Default | Description |
388
426
  |---|---|---|---|
427
+ | `name` | `string` | `'default'` | Instance name (see [Multiple instances](#multiple-instances)) |
389
428
  | `hotkey` | `string[]` | `['$mod', 'k']` | Key combination to toggle the palette |
390
429
  | `colorTheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Initial color theme of the palette |
430
+ | `search` | `SearchFn` | built-in fuzzy | Custom search strategy (see [Custom search](#custom-search-strategy)) |
431
+ | `searchNested` | `boolean` | `true` | Surface nested `subCommands` in search results with breadcrumb context |
432
+ | `showDisabled` | `boolean` | `false` | Show disabled commands (greyed, non-executable, demoted) instead of hiding them |
433
+ | `frecency` | `boolean` | `false` | Boost frequently & recently used commands in the ranking (see [Frecency](#frecency)) |
434
+ | `onSearch` | `(query) => Command[] \| Promise<Command[]>` | — | Plugin-level async data source merged into every query (see [Async search](#async-search)) |
435
+ | `bindShortcuts` | `boolean` | `false` | Auto-register each command's `shortcut` as a global hotkey |
391
436
  | `persistRecent` | `boolean` | `true` | Persist recent commands to `localStorage` |
392
437
  | `maxRecent` | `number` | `5` | Maximum total recent commands stored |
393
438
  | `maxRecentPerGroup` | `number` | `0` | Max recent per group (`0` = unlimited) |
@@ -395,6 +440,7 @@ app.use(VCommandPalettePlugin, options)
395
440
  | `onOpen` | `() => void` | — | Called every time the palette opens |
396
441
  | `onClose` | `() => void` | — | Called every time the palette closes |
397
442
  | `onError` | `(err: unknown, command: Command) => void` | — | Called when `perform()` throws |
443
+ | `onHighlight` | `(command: Command \| null) => void` | — | Called when the keyboard-active command changes (previews/analytics) |
398
444
 
399
445
  ### Example with all options
400
446
 
@@ -429,7 +475,7 @@ shortcut: ['$mod', 'shift', 'p'] // Cmd+Shift+P on Mac, Ctrl+Shift+P on Windows
429
475
  ## Command type
430
476
 
431
477
  ```ts
432
- interface Command {
478
+ interface Command<T = unknown> {
433
479
  id: string // unique identifier
434
480
  label: string // display text, searched by fuzzy engine
435
481
  description?: string // subtitle shown below the label
@@ -439,9 +485,29 @@ interface Command {
439
485
  shortcut?: string[] // display-only hint: ['$mod', 'k']
440
486
  disabled?: boolean // permanently unavailable
441
487
  enabled?: () => boolean // dynamically disable — evaluated on each render
488
+ disabledReason?: string // tooltip shown when the command is disabled
489
+ badge?: string | { text: string; color?: string } // small label (e.g. "New", "Pro")
442
490
  confirm?: string // prompt shown before execute
443
491
  perform: () => void | Promise<void> // action; may be async
444
492
  subCommands?: Command[] // opens a nested palette when selected
493
+ page?: CommandPage // opens a page with its own input/async search
494
+ actions?: CommandAction[] // secondary actions, opened with Tab
495
+ info?: string // text/HTML shown in the preview pane (v-html)
496
+ data?: T // type-safe payload (see Typed command data)
497
+ }
498
+
499
+ interface CommandAction {
500
+ id: string
501
+ label: string
502
+ icon?: Component | string
503
+ shortcut?: string[] // display-only hint
504
+ perform: () => void | Promise<void>
505
+ }
506
+
507
+ interface CommandPage {
508
+ placeholder?: string // input placeholder on the page
509
+ items?: Command[] // static items (empty query)
510
+ onSearch?: (query: string) => Command[] | Promise<Command[]> // query-driven results (debounced)
445
511
  }
446
512
  ```
447
513
 
@@ -472,6 +538,11 @@ const results = fuzzySearch('git cm', commands)
472
538
  // Render highlighted label in a custom slot
473
539
  const vnode = highlightMatches(command.label, result.matches)
474
540
  // returns a VNode: <span>git <mark class="vcp-match">c</mark>o<mark class="vcp-match">m</mark>mit</span>
541
+
542
+ // Highlight ranges for results that came from an external source
543
+ // (async groups, pages, modes — these are already highlighted internally):
544
+ import { getMatchRanges } from '@macrulez/vue-command-palette'
545
+ const ranges = getMatchRanges('al', 'Alan Turing') // → [[0, 1]]
475
546
  ```
476
547
 
477
548
  ### Scoring table
@@ -491,10 +562,12 @@ The engine checks `label`, all `keywords[]`, and all `aliases[]`. The highest sc
491
562
  Strings are normalized with `NFD` Unicode decomposition before comparison, so accents are ignored:
492
563
 
493
564
  ```ts
494
- fuzzySearch('cafe', [{ id: '1', label: 'Café', perform: () => {} }]) // → match
495
- fuzzySearch('strase',[{ id: '2', label: 'Straße', perform: () => {} }]) // → match
565
+ fuzzySearch('cafe', [{ id: '1', label: 'Café', perform: () => {} }]) // → match
566
+ fuzzySearch('muller', [{ id: '2', label: 'Müller', perform: () => {} }]) // → match
496
567
  ```
497
568
 
569
+ > Only diacritics are stripped (`é` → `e`, `ü` → `u`). Letters that have no canonical decomposition — such as `ß` — are left as-is, so `ß` does **not** match `ss`.
570
+
498
571
  ---
499
572
 
500
573
  ## Keyboard shortcuts
@@ -589,6 +662,10 @@ Add `subCommands` to any command to open a child palette when it is selected. Th
589
662
 
590
663
  Sub-palettes can be nested to any depth.
591
664
 
665
+ ### Nested commands are searchable
666
+
667
+ By default (`searchNested: true`), typing a query also matches commands **inside** `subCommands`, so searching `light` surfaces the actual *Light* command — not just its *Change Theme* parent. Such results are shown with a breadcrumb context (`Change Theme › Light`) via `SearchResult.parents`, and selecting one runs it directly. Commands that open a sub-palette or page show a `›` chevron affordance. Set `searchNested: false` to restrict search to top-level commands only.
668
+
592
669
  **Navigation keys inside a sub-palette:**
593
670
 
594
671
  | Key | Action |
@@ -648,15 +725,19 @@ useRegisterGroup({
648
725
  ```
649
726
 
650
727
  - Debounced by **200 ms** to avoid excessive requests
651
- - A loading indicator appears while the request is in flight
728
+ - An unobtrusive spinner appears in the input corner while the request is in flight — already-shown results stay visible (no blanking). The centered loading text only appears when there is nothing to show yet.
652
729
  - Async results are merged with sync results and re-sorted by score
653
730
  - Empty query clears async results immediately (no debounce)
731
+ - A **plugin-level** `onSearch` option provides one global async source (not tied to a group), merged the same way:
732
+ ```ts
733
+ app.use(VCommandPalettePlugin, { onSearch: (q) => api.search(q) })
734
+ ```
654
735
 
655
736
  ---
656
737
 
657
738
  ## Recent commands
658
739
 
659
- When `persistRecent: true` (the default), each executed command's ID is written to `localStorage`. On open with an empty query, recent commands appear above all other commands.
740
+ Recent commands are always tracked in memory for the current session and shown above all other commands when the palette opens with an empty query. When `persistRecent: true` (the default), the list is additionally written to `localStorage` so it survives reloads; setting `persistRecent: false` keeps recent working for the session without touching `localStorage`.
660
741
 
661
742
  ```ts
662
743
  app.use(VCommandPalettePlugin, {
@@ -702,6 +783,8 @@ The palette is fully styled via CSS custom properties. Import the default styles
702
783
  --vcp-dialog-radius: 8px;
703
784
  --vcp-dialog-shadow: 0 16px 70px rgba(0, 0, 0, 0.2);
704
785
  --vcp-dialog-width: 560px;
786
+ --vcp-dialog-preview-width: 860px;
787
+ --vcp-preview-width: 300px;
705
788
  --vcp-dialog-max-height: 60vh;
706
789
  --vcp-dialog-padding-top: 15vh;
707
790
 
@@ -780,6 +863,291 @@ colorTheme.value = 'dark'
780
863
 
781
864
  ---
782
865
 
866
+ ## Frecency
867
+
868
+ With `frecency: true`, the palette tracks how often and how recently each command is executed and adds a bonus to its search score, so your most-used commands float to the top. Stats are kept in memory and (when `persistRecent` is on) persisted to `localStorage` under `<localStorageKey>:frecency`.
869
+
870
+ ```ts
871
+ app.use(VCommandPalettePlugin, { frecency: true })
872
+ ```
873
+
874
+ The bonus combines frequency (run count) with recency (decays over ~30 days), and never hides a strong exact/prefix match — it only reorders comparable results.
875
+
876
+ ---
877
+
878
+ ## Modes / scopes
879
+
880
+ Define prefix-activated scopes (like VS Code's `>` commands or `@` symbols). When the query starts with a mode's `prefix`, the prefix is stripped, the placeholder switches, a chip appears, and results come from the mode's `onSearch` (or, if omitted, the regular fuzzy search over the stripped query).
881
+
882
+ ```vue
883
+ <CommandPalette
884
+ :modes="[
885
+ { prefix: '>', label: 'Run', placeholder: 'Run a command…', onSearch: searchCommands },
886
+ { prefix: '@', label: 'People', placeholder: 'Find a person…', onSearch: searchPeople },
887
+ ]"
888
+ />
889
+ ```
890
+
891
+ ```ts
892
+ type PaletteMode = {
893
+ prefix: string
894
+ placeholder?: string
895
+ label?: string
896
+ onSearch?: (query: string) => Command[] | Promise<Command[]>
897
+ }
898
+ ```
899
+
900
+ `Backspace` over the prefix exits the mode. Results are debounced 200 ms.
901
+
902
+ ---
903
+
904
+ ## Preview pane
905
+
906
+ Set `preview` to show a right-hand panel for the active command — great for details, docs, or thumbnails. It updates as the selection changes (it pairs naturally with the `onHighlight` option for async previews). On narrow screens the pane is hidden automatically.
907
+
908
+ Two sources fill the pane, in order:
909
+
910
+ 1. The `#preview` slot — `{ command }` scope, full control over the markup.
911
+ 2. The active command's **`info`** field (plain text or HTML) — rendered after the slot. Handy when you don't need a custom slot.
912
+
913
+ ```vue
914
+ <CommandPalette preview>
915
+ <template #preview="{ command }">
916
+ <div v-if="command">
917
+ <h3>{{ command.label }}</h3>
918
+ <p>{{ command.description }}</p>
919
+ </div>
920
+ </template>
921
+ </CommandPalette>
922
+ ```
923
+
924
+ ```ts
925
+ useRegisterCommands([
926
+ {
927
+ id: 'analytics',
928
+ label: 'Open Analytics',
929
+ info: '<p>Traffic, conversions and revenue charts.</p>', // text or HTML
930
+ perform: () => {},
931
+ },
932
+ ])
933
+ ```
934
+
935
+ > **Security:** `info` is rendered with `v-html`. Only pass trusted/sanitised markup.
936
+
937
+ ### Toggling the pane
938
+
939
+ - A **sidebar icon** appears next to the theme switcher to expand/collapse the pane.
940
+ - The `previewHotkey` prop sets a keyboard toggle (default `['$mod', 'i']` → ⌘/Ctrl + I; pass `[]` to disable).
941
+ - Opening/closing animates the pane's **width** in sync with the dialog width, so the list stays a constant width and there's no jump; respects `prefers-reduced-motion`.
942
+
943
+ The dialog only widens to `--vcp-dialog-preview-width` (default `860px`) while the pane is **expanded** — when it's collapsed (or `preview` is off) the dialog returns to the normal `--vcp-dialog-width` (`560px`). The pane is `--vcp-preview-width` wide (default `300px`); keep `dialog-preview-width − dialog-width = preview-width` for a perfectly steady list during the animation.
944
+
945
+ ---
946
+
947
+ ## Mobile / touch
948
+
949
+ The palette is responsive out of the box: on viewports ≤ 640px the dialog goes full-width with larger (48px) touch targets, the preview pane is hidden, and hover styles are disabled on touch devices. Inside a nested palette/page, **swipe right** to go back.
950
+
951
+ ---
952
+
953
+ ## Pinned commands
954
+
955
+ Users can pin any command to a **Pinned** section shown above *Recent* in the empty-query view. Toggle a pin with `$mod+P` on the active item, by **clicking the pin icon** on the row (it appears as a ghost on hover / keyboard-active rows, and stays lit on pinned commands — clicking it never runs the command), or via the composable API. Pins persist to `localStorage` (`<localStorageKey>:pinned`) when `persistRecent` is on.
956
+
957
+ ```ts
958
+ const { pin, unpin, togglePin, isPinned, getPinnedCommands, pinnedIds } = useCommandPalette()
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Secondary actions
964
+
965
+ A command can expose secondary `actions` (open, copy, delete, …). Press `Tab` on the active item to open the actions menu; arrows navigate, `Enter` runs, and `Esc` / `Tab` / `Backspace` (or the **‹ Back** button in the header) returns to the list. Customise the menu with the `#actions` slot (`{ command, run, activeIndex, close }`).
966
+
967
+ ```ts
968
+ useRegisterCommands([
969
+ {
970
+ id: 'export',
971
+ label: 'Export data',
972
+ perform: () => download(),
973
+ actions: [
974
+ { id: 'copy', label: 'Copy as JSON', perform: () => copyJson() },
975
+ { id: 'mail', label: 'Email export', shortcut: ['$mod', 'm'], perform: () => email() },
976
+ ],
977
+ },
978
+ ])
979
+ ```
980
+
981
+ Items with actions show a `⋯` affordance.
982
+
983
+ ---
984
+
985
+ ## Multi-select
986
+
987
+ With `selectable`, the palette becomes a multi-picker: `Enter` (or click) toggles the active item, `$mod+Enter` submits. Selected rows show a checkbox.
988
+
989
+ ```vue
990
+ <CommandPalette selectable @submit-selection="onPicked" />
991
+ ```
992
+
993
+ ```ts
994
+ function onPicked(commands: Command[]) {
995
+ // do something with the chosen commands
996
+ }
997
+ ```
998
+
999
+ ---
1000
+
1001
+ ## Query history
1002
+
1003
+ Recently submitted queries are remembered for the session. With an empty or any input, press `Alt+ArrowUp` / `Alt+ArrowDown` to cycle through previous queries (most recent first). Exposed read-only as `useCommandPalette().queryHistory`.
1004
+
1005
+ ---
1006
+
1007
+ ## Custom search strategy
1008
+
1009
+ Replace the built-in fuzzy engine with any scorer — for example [Fuse.js](https://fusejs.io). The function receives the query and all available commands and returns ranked `SearchResult[]` (highest score first). The store still assigns `groupId` to each result afterwards.
1010
+
1011
+ ```ts
1012
+ import Fuse from 'fuse.js'
1013
+ import type { SearchFn } from '@macrulez/vue-command-palette'
1014
+
1015
+ const fuseSearch: SearchFn = (query, commands) => {
1016
+ const fuse = new Fuse(commands, { keys: ['label', 'description', 'keywords'], includeScore: true })
1017
+ return fuse.search(query).map(r => ({
1018
+ command: r.item,
1019
+ score: 1 - (r.score ?? 0),
1020
+ matches: [],
1021
+ }))
1022
+ }
1023
+
1024
+ app.use(VCommandPalettePlugin, { search: fuseSearch })
1025
+ ```
1026
+
1027
+ ---
1028
+
1029
+ ## Binding shortcuts
1030
+
1031
+ By default `shortcut` is a display-only hint. Set `bindShortcuts: true` and each command's `shortcut` becomes a real global hotkey that runs the command through the same flow as clicking it (confirm dialogs and pages included). Shortcuts are registered and cleaned up automatically as commands are added and removed.
1032
+
1033
+ ```ts
1034
+ app.use(VCommandPalettePlugin, { bindShortcuts: true })
1035
+
1036
+ useRegisterCommands([
1037
+ { id: 'save', label: 'Save', shortcut: ['$mod', 's'], perform: () => save() },
1038
+ { id: 'find', label: 'Find', shortcut: ['$mod', 'f'], perform: () => openFind() },
1039
+ ])
1040
+ ```
1041
+
1042
+ ---
1043
+
1044
+ ## Command pages
1045
+
1046
+ A command can open a **page** instead of (or in addition to) running. A page is like a nested palette, but with its own placeholder and an async `onSearch` handler driven by the input — ideal for remote pickers and filters.
1047
+
1048
+ ```ts
1049
+ useRegisterCommands([
1050
+ {
1051
+ id: 'assign-user',
1052
+ label: 'Assign to user…',
1053
+ icon: '👤',
1054
+ perform: () => {}, // not called — the page opens instead
1055
+ page: {
1056
+ placeholder: 'Search users…',
1057
+ // items: [...] // optional static items shown on empty query
1058
+ onSearch: async (query) => {
1059
+ const users = await api.searchUsers(query)
1060
+ return users.map(u => ({
1061
+ id: `user-${u.id}`,
1062
+ label: u.name,
1063
+ description: u.email,
1064
+ perform: () => assign(u.id),
1065
+ }))
1066
+ },
1067
+ },
1068
+ },
1069
+ ])
1070
+ ```
1071
+
1072
+ `Backspace` (empty input) / `Esc` navigate back, exactly like sub-palettes. Results are debounced 200 ms; if `onSearch` is omitted, the page filters its static `items` by the query.
1073
+
1074
+ ---
1075
+
1076
+ ## Multiple instances
1077
+
1078
+ Run several independent palettes on one app — e.g. a global command bar plus a sidebar search — each with its own hotkey, commands and state. Use `createCommandPalette()` for every instance beyond the default (it returns a fresh plugin object so Vue's `app.use` de-duplication doesn't skip it).
1079
+
1080
+ ```ts
1081
+ import { VCommandPalettePlugin, createCommandPalette } from '@macrulez/vue-command-palette'
1082
+
1083
+ app.use(VCommandPalettePlugin) // default instance
1084
+ app.use(createCommandPalette({ name: 'sidebar', hotkey: ['$mod', 'j'] }))
1085
+ ```
1086
+
1087
+ ```vue
1088
+ <template>
1089
+ <!-- default -->
1090
+ <CommandPalette />
1091
+ <!-- sidebar -->
1092
+ <CommandPalette name="sidebar" placeholder="Search the sidebar…" />
1093
+ </template>
1094
+ ```
1095
+
1096
+ Target a specific instance from composables via the `name` argument:
1097
+
1098
+ ```ts
1099
+ const sidebar = useCommandPalette('sidebar')
1100
+ useRegisterCommands([/* … */], 'sidebar')
1101
+ useRegisterGroup({ /* … */ }, 'sidebar')
1102
+ ```
1103
+
1104
+ ---
1105
+
1106
+ ## Localization
1107
+
1108
+ Built-in UI strings (the *Recent* header, confirm dialog buttons, theme-switcher titles, ARIA labels) can be overridden via the `labels` prop. Only the keys you pass are overridden; the rest fall back to the English defaults.
1109
+
1110
+ ```vue
1111
+ <CommandPalette
1112
+ placeholder="Поиск команд…"
1113
+ empty-text="Ничего не найдено."
1114
+ loading-text="Загрузка…"
1115
+ :labels="{
1116
+ recent: 'Недавние',
1117
+ confirmYes: 'Да, продолжить',
1118
+ confirmCancel: 'Отмена',
1119
+ themeLight: 'Светлая тема',
1120
+ themeDark: 'Тёмная тема',
1121
+ themeSystem: 'Системная тема',
1122
+ dialogLabel: 'Палитра команд',
1123
+ loading: 'Загрузка',
1124
+ }"
1125
+ />
1126
+ ```
1127
+
1128
+ ### `PaletteLabels`
1129
+
1130
+ | Key | Default | Where it appears |
1131
+ |---|---|---|
1132
+ | `recent` | `'Recent'` | Header above recent commands (empty query) |
1133
+ | `pinned` | `'Pinned'` | Header above pinned commands (empty query) |
1134
+ | `pin` / `unpin` | `'Pin'` / `'Unpin'` | `title` of the per-row pin icon |
1135
+ | `actions` | `'Actions'` | Header of the secondary-actions menu |
1136
+ | `back` | `'Back'` | "Back" affordance (actions menu) |
1137
+ | `togglePreview` | `'Toggle preview panel'` | `title`/`aria-label` of the preview toggle button |
1138
+ | `confirmYes` | `'Yes, proceed'` | Confirm dialog — proceed button |
1139
+ | `confirmCancel` | `'Cancel'` | Confirm dialog — cancel button |
1140
+ | `themeLight` | `'Light theme'` | Theme switcher button title |
1141
+ | `themeDark` | `'Dark theme'` | Theme switcher button title |
1142
+ | `themeSystem` | `'System theme'` | Theme switcher button title |
1143
+ | `dialogLabel` | `'Command palette'` | `aria-label` of the dialog |
1144
+ | `loading` | `'Loading'` | `aria-label` of the per-item spinner |
1145
+ | `resultsCount` | `(n) => '… results available'` | `aria-live` announcement of the result count (a function) |
1146
+
1147
+ > Note: `placeholder`, `emptyText` and `loadingText` remain separate props on `CommandPalette`.
1148
+
1149
+ ---
1150
+
783
1151
  ## Nuxt
784
1152
 
785
1153
  Add to `nuxt.config.ts`:
@@ -898,6 +1266,36 @@ const {
898
1266
 
899
1267
  ---
900
1268
 
1269
+ ## Typed command data
1270
+
1271
+ Attach an arbitrary, type-safe payload to commands via the generic `Command<T>` and its `data` field. `useRegisterCommands<T>` / `useRegisterGroup<T>`, `fuzzySearch<T>`, `SearchResult<T>` and `SearchFn<T>` all carry the type through, so you get full inference (and errors on mismatches). It defaults to `unknown`, so existing untyped usage is unaffected.
1272
+
1273
+ ```ts
1274
+ interface UserData { id: number; email: string }
1275
+
1276
+ useRegisterCommands<UserData>([
1277
+ {
1278
+ id: 'user-ada',
1279
+ label: 'Ada Lovelace',
1280
+ data: { id: 1, email: 'ada@example.com' }, // checked against UserData
1281
+ perform: () => {},
1282
+ },
1283
+ ])
1284
+
1285
+ // Standalone search keeps the type:
1286
+ const results = fuzzySearch<UserData>('ada', commands)
1287
+ results[0].command.data?.email // string | undefined
1288
+ ```
1289
+
1290
+ ```ts
1291
+ // @ts-expect-error — data must match UserData
1292
+ const bad: Command<UserData> = { id: 'x', label: 'X', data: { wrong: true }, perform: () => {} }
1293
+ ```
1294
+
1295
+ > Inside the `#item` / `#preview` slots the `command` is typed as `Command` (`data: unknown`) since the palette stores commands of mixed types — narrow with a cast or a type guard when you need the payload there.
1296
+
1297
+ ---
1298
+
901
1299
  ## TypeScript types
902
1300
 
903
1301
  All public types are exported from the package root:
@@ -906,9 +1304,14 @@ All public types are exported from the package root:
906
1304
  import type {
907
1305
  Command,
908
1306
  CommandGroupType, // group definition — NOT the CommandGroup component
909
- CommandSection,
910
- SearchResult, // { command, score, matches, groupId? }
1307
+ CommandAction, // secondary action on a command
1308
+ CommandPage, // page opened by a command (placeholder + async onSearch)
1309
+ SearchResult, // { command, score, matches, groupId?, parents?, matchedField? }
1310
+ SearchFn, // custom search strategy signature
1311
+ PaletteMode, // prefix-activated scope
1312
+ CommandUsage, // frecency stat { count, lastUsed }
911
1313
  PaletteOptions,
1314
+ PaletteLabels, // customisable UI strings (i18n)
912
1315
  PaletteContext,
913
1316
  PaletteState,
914
1317
  CommandStore,
@@ -926,6 +1329,9 @@ interface SearchResult {
926
1329
  score: number
927
1330
  matches: Array<[start: number, end: number]>
928
1331
  groupId?: string
1332
+ parents?: Command[] // ancestor chain when the result is a nested sub-command
1333
+ matchedField?: 'label' | 'description' | 'keyword' | 'alias' // which field won the score
1334
+ matchedText?: string // matching keyword/alias text
929
1335
  }
930
1336
  ```
931
1337
 
@@ -969,7 +1375,7 @@ interface PaletteContext {
969
1375
  | `role="option"` | Applied to each `CommandItem` |
970
1376
  | `aria-selected` | Set to `true` on the currently active item |
971
1377
  | `aria-disabled` | Set when `disabled: true` or `enabled()` returns `false` |
972
- | `aria-live="polite"` | Breadcrumb — screen readers announce sub-palette navigation |
1378
+ | `aria-live="polite"` | Breadcrumb — screen readers announce sub-palette navigation; a visually-hidden region also announces the result count (`labels.resultsCount`) |
973
1379
  | Focus trap | `Tab` is intercepted to keep focus inside the dialog |
974
1380
  | Scroll lock | `document.body.style.overflow` is set to `hidden` while open |
975
1381
  | Reduced motion | `@media (prefers-reduced-motion: reduce)` disables the fade transition |
@@ -1004,7 +1410,7 @@ typeof navigator !== 'undefined' && navigator.platform.includes('Mac')
1004
1410
  | `@macrulez/vue-command-palette/testing` | `vue ^3.3` | `createPaletteContext` + `PaletteProvider`; dev/test only |
1005
1411
  | `@macrulez/vue-command-palette/nuxt` | `nuxt ^3`, `vue ^3.3` | Nuxt auto-plugin |
1006
1412
 
1007
- Ships as tree-shakeable ESM (`dist/@macrulez/vue-command-palette.js`) + CJS (`dist/@macrulez/vue-command-palette.cjs`). Core bundle without styles is 10 KB gzip.
1413
+ Ships as tree-shakeable ESM (`dist/@macrulez/vue-command-palette.js`) + CJS (`dist/@macrulez/vue-command-palette.cjs`). Core bundle without styles is ~11 KB gzip; the stylesheet is ~2 KB gzip.
1008
1414
 
1009
1415
  ---
1010
1416
 
@@ -1020,7 +1426,7 @@ Danil Lisin Vladimirovich aka Macrulez
1020
1426
 
1021
1427
  GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru/en](https://macrulez.ru/en)
1022
1428
 
1023
- Bugs and questions — [issues](https://github.com/macrulezru/@macrulez/vue-command-palette/issues)
1429
+ Bugs and questions — [issues](https://github.com/macrulezru/vue-command-palette/issues)
1024
1430
 
1025
1431
  ---
1026
1432