@macrulez/vue-command-palette 0.1.1 → 0.2.0
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/README.md +432 -14
- package/dist/components/CommandGroup.vue.d.ts +8 -6
- package/dist/components/CommandItem.vue.d.ts +11 -3
- package/dist/components/CommandPalette.test.d.ts +1 -0
- package/dist/components/CommandPalette.vue.d.ts +336 -7
- package/dist/components/VirtualList.vue.d.ts +2 -0
- package/dist/core/CommandStore.d.ts +278 -7
- package/dist/core/CommandStore.test.d.ts +1 -0
- package/dist/core/FuzzySearch.d.ts +13 -1
- package/dist/core/generics.test.d.ts +1 -0
- package/dist/core/useCommandPalette.d.ts +40 -7
- package/dist/core/useCommandPalette.test.d.ts +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/plugin.d.ts +22 -1
- package/dist/plugin.test.d.ts +1 -0
- package/dist/testing.d.ts +281 -10
- package/dist/types.d.ts +122 -10
- package/dist/vue-command-palette.js +1457 -713
- package/dist/vue-command-palette.umd.cjs +1 -1
- package/package.json +5 -1
- package/dist/style.css +0 -1
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.
|
|
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',
|
|
495
|
-
fuzzySearch('
|
|
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
|
-
-
|
|
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),
|
|
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
|
-
|
|
910
|
-
|
|
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
|
|
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,4 +1426,16 @@ 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
|
|
1429
|
+
Bugs and questions — [issues](https://github.com/macrulezru/vue-command-palette/issues)
|
|
1430
|
+
|
|
1431
|
+
---
|
|
1432
|
+
|
|
1433
|
+
## 💖 Support the project
|
|
1434
|
+
|
|
1435
|
+
Open source takes time and effort. If my work saves you time or brings value, consider supporting further development.
|
|
1436
|
+
|
|
1437
|
+
<a href="https://donate.cryptocloud.plus/M6O34NIN" target="_blank">
|
|
1438
|
+
<img src="https://img.shields.io/badge/Donate-CryptoCloud-8A2BE2?style=for-the-badge&logo=cryptocurrency&logoColor=white" alt="Donate via CryptoCloud">
|
|
1439
|
+
</a>
|
|
1440
|
+
|
|
1441
|
+
Thank you for being part of this journey. ❤️
|
|
@@ -2,18 +2,20 @@ import { Command, CommandGroup, SearchResult } from '../types';
|
|
|
2
2
|
|
|
3
3
|
declare function __VLS_template(): {
|
|
4
4
|
"group-header"?(_: {
|
|
5
|
-
group: CommandGroup
|
|
5
|
+
group: CommandGroup<unknown>;
|
|
6
6
|
}): any;
|
|
7
7
|
"item-icon"?(_: {
|
|
8
|
-
command: Command
|
|
8
|
+
command: Command<unknown>;
|
|
9
9
|
}): any;
|
|
10
10
|
"item-shortcut"?(_: {
|
|
11
|
-
command: Command
|
|
11
|
+
command: Command<unknown>;
|
|
12
12
|
}): any;
|
|
13
13
|
item?(_: {
|
|
14
|
-
command: Command
|
|
14
|
+
command: Command<unknown>;
|
|
15
15
|
active: boolean;
|
|
16
16
|
matches: [number, number][];
|
|
17
|
+
parents: Command<unknown>[] | undefined;
|
|
18
|
+
matchedText: string | undefined;
|
|
17
19
|
}): any;
|
|
18
20
|
};
|
|
19
21
|
declare const __VLS_component: import('vue').DefineComponent<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
|
|
@@ -23,7 +25,7 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
|
|
|
23
25
|
globalOffset: number;
|
|
24
26
|
loadingCommandId?: string | null;
|
|
25
27
|
}>>, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
26
|
-
execute: (command: Command) => void;
|
|
28
|
+
execute: (command: Command<unknown>) => void;
|
|
27
29
|
activate: (index: number) => void;
|
|
28
30
|
}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
|
|
29
31
|
group: CommandGroup;
|
|
@@ -32,7 +34,7 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
|
|
|
32
34
|
globalOffset: number;
|
|
33
35
|
loadingCommandId?: string | null;
|
|
34
36
|
}>>> & Readonly<{
|
|
35
|
-
onExecute?: ((command: Command) => any) | undefined;
|
|
37
|
+
onExecute?: ((command: Command<unknown>) => any) | undefined;
|
|
36
38
|
onActivate?: ((index: number) => any) | undefined;
|
|
37
39
|
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
38
40
|
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
|