@macrulez/vue-command-palette 0.1.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 +1014 -0
- package/dist/components/CommandGroup.vue.d.ts +53 -0
- package/dist/components/CommandItem.vue.d.ts +50 -0
- package/dist/components/CommandPalette.vue.d.ts +97 -0
- package/dist/components/VirtualList.vue.d.ts +26 -0
- package/dist/core/CommandStore.d.ts +284 -0
- package/dist/core/FuzzySearch.d.ts +5 -0
- package/dist/core/FuzzySearch.test.d.ts +1 -0
- package/dist/core/KeyboardManager.d.ts +8 -0
- package/dist/core/KeyboardManager.test.d.ts +1 -0
- package/dist/core/useCommandPalette.d.ts +57 -0
- package/dist/index.d.ts +13 -0
- package/dist/nuxt.d.ts +2 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/style.css +1 -0
- package/dist/testing.d.ts +335 -0
- package/dist/types.d.ts +57 -0
- package/dist/vue-command-palette.js +1035 -0
- package/dist/vue-command-palette.umd.cjs +1 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
# @macrulez/vue-command-palette
|
|
2
|
+
|
|
3
|
+
Command+K palette for Vue 3. Fuzzy search with match highlighting, grouped commands, nested sub-palettes, global keyboard shortcuts, async search, confirmation dialogs, recent command history, and full headless customisation via slots — all with a single peer dependency (Vue 3).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Demo](#demo)
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick start](#quick-start)
|
|
13
|
+
- [CommandPalette](#commandpalette)
|
|
14
|
+
- [CommandItem](#commanditem)
|
|
15
|
+
- [CommandGroup](#commandgroup)
|
|
16
|
+
- [useCommandPalette](#usecommandpalette)
|
|
17
|
+
- [useRegisterCommands](#useregistercommands)
|
|
18
|
+
- [useRegisterGroup](#useregistergroup)
|
|
19
|
+
- [VCommandPalettePlugin](#vcommandpaletteplugin)
|
|
20
|
+
- [Command type](#command-type)
|
|
21
|
+
- [Fuzzy search](#fuzzy-search)
|
|
22
|
+
- [Keyboard shortcuts](#keyboard-shortcuts)
|
|
23
|
+
- [Nested palettes](#nested-palettes)
|
|
24
|
+
- [Confirmation step](#confirmation-step)
|
|
25
|
+
- [Async search](#async-search)
|
|
26
|
+
- [Recent commands](#recent-commands)
|
|
27
|
+
- [Theming](#theming)
|
|
28
|
+
- [Nuxt](#nuxt)
|
|
29
|
+
- [Testing utilities](#testing-utilities)
|
|
30
|
+
- [TypeScript types](#typescript-types)
|
|
31
|
+
- [Accessibility](#accessibility)
|
|
32
|
+
- [SSR compatibility](#ssr-compatibility)
|
|
33
|
+
- [Bundle size](#bundle-size)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Fuzzy search** — ranking: exact (100) › prefix (80) › substring (60) › fuzzy (1–40). Diacritic normalization so `café` matches `cafe`. Match highlighting via `<mark>` spans.
|
|
40
|
+
- **All-commands view** — palette shows all registered commands grouped on open; no empty screen.
|
|
41
|
+
- **Grouped commands** — groups with headers, priority ordering, and visual section dividers.
|
|
42
|
+
- **Recent commands** — `localStorage`-backed history of the last N executed commands shown at the top when the query is empty.
|
|
43
|
+
- **Nested palettes** — `subCommands` opens a child palette with breadcrumb trail; `Backspace` / `Esc` navigates back.
|
|
44
|
+
- **Async search** — `onSearch: (query) => Promise<Command[]>` per group, debounced 200 ms, merged with sync results.
|
|
45
|
+
- **Confirmation step** — `confirm: string` shows a yes/no dialog before the command executes.
|
|
46
|
+
- **Loading state** — spinner on the item while an async `perform()` is running.
|
|
47
|
+
- **`enabled()` guard** — dynamic command availability evaluated on every render cycle.
|
|
48
|
+
- **Aliases & keywords** — searched alongside the label; aliases score identically to label matches.
|
|
49
|
+
- **Global keyboard manager** — `$mod` (⌘ on macOS, Ctrl on Windows/Linux), modifier combinations, bare-key sequences (`g` → `h` within 500 ms).
|
|
50
|
+
- **Headless slots** — `#trigger`, `#header`, `#input`, `#item`, `#group-header`, `#empty`, `#footer` for complete UI control.
|
|
51
|
+
- **20+ CSS custom properties** — full theme customisation without touching source code. Automatic dark mode via `prefers-color-scheme`.
|
|
52
|
+
- **Custom scrollbar** — thin 4 px scrollbar styled to match the palette theme.
|
|
53
|
+
- **Virtual list** — own implementation for result sets > 50 items, no extra dependencies.
|
|
54
|
+
- **Nuxt module** — auto-installs the plugin via `nuxt.config.ts`.
|
|
55
|
+
- **Testing utilities** — `createPaletteContext` + `PaletteProvider` for isolated unit tests.
|
|
56
|
+
- **SSR-safe** — all browser API calls guarded with `typeof document !== 'undefined'`.
|
|
57
|
+
- **Zero runtime dependencies** — only Vue 3 as peer dep. ≤ 10 KB gzip.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Demo
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cd demo
|
|
65
|
+
npm install
|
|
66
|
+
npm run dev
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Opens at **http://localhost:5173**.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install @macrulez/vue-command-palette
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Peer dependency:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install vue@>=3.3
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
### 1. Install the plugin
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// main.ts
|
|
93
|
+
import { createApp } from 'vue'
|
|
94
|
+
import { VCommandPalettePlugin } from '@macrulez/vue-command-palette'
|
|
95
|
+
import '@macrulez/vue-command-palette/style.css'
|
|
96
|
+
import App from './App.vue'
|
|
97
|
+
|
|
98
|
+
const app = createApp(App)
|
|
99
|
+
|
|
100
|
+
app.use(VCommandPalettePlugin, {
|
|
101
|
+
hotkey: ['$mod', 'k'], // Cmd+K on macOS, Ctrl+K on Windows/Linux
|
|
102
|
+
colorTheme: 'system', // 'light' | 'dark' | 'system'
|
|
103
|
+
persistRecent: true,
|
|
104
|
+
maxRecent: 5,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
app.mount('#app')
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Place `CommandPalette` anywhere in your component tree
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<script setup lang="ts">
|
|
114
|
+
import { CommandPalette, useRegisterGroup } from '@macrulez/vue-command-palette'
|
|
115
|
+
|
|
116
|
+
useRegisterGroup({
|
|
117
|
+
id: 'navigation',
|
|
118
|
+
label: 'Navigation',
|
|
119
|
+
priority: 100,
|
|
120
|
+
commands: [
|
|
121
|
+
{
|
|
122
|
+
id: 'go-home',
|
|
123
|
+
label: 'Go to Home',
|
|
124
|
+
icon: '🏠',
|
|
125
|
+
perform: () => router.push('/'),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'go-settings',
|
|
129
|
+
label: 'Settings',
|
|
130
|
+
icon: '⚙️',
|
|
131
|
+
shortcut: ['$mod', ','],
|
|
132
|
+
perform: () => router.push('/settings'),
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
})
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<RouterView />
|
|
140
|
+
<CommandPalette placeholder="Search commands…" :max-results="12" />
|
|
141
|
+
</template>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Press **Cmd+K** / **Ctrl+K** to open.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## CommandPalette
|
|
149
|
+
|
|
150
|
+
The root component. Renders a modal overlay with a search input, result list, and optional slots. Teleports to `<body>` by default.
|
|
151
|
+
|
|
152
|
+
### Props
|
|
153
|
+
|
|
154
|
+
| Prop | Type | Default | Description |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| `placeholder` | `string` | `'Search commands…'` | Input placeholder text |
|
|
157
|
+
| `maxResults` | `number` | `10` | Maximum number of results shown |
|
|
158
|
+
| `emptyText` | `string` | `'No commands found.'` | Text shown when search returns nothing |
|
|
159
|
+
| `loadingText` | `string` | `'Loading…'` | Text shown while async groups are fetching |
|
|
160
|
+
| `teleportTo` | `string` | `'body'` | CSS selector for the `<Teleport>` target |
|
|
161
|
+
| `theme` | `'default' \| 'compact'` | `'default'` | Compact uses a narrower, shorter dialog |
|
|
162
|
+
| `animationDuration` | `number` | `150` | Fade transition duration in ms |
|
|
163
|
+
|
|
164
|
+
### Slots
|
|
165
|
+
|
|
166
|
+
| Slot | Scope | Description |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `#trigger` | `{ open, toggle }` | Custom element that opens the palette |
|
|
169
|
+
| `#header` | — | Content inserted above the search input |
|
|
170
|
+
| `#input` | `{ query, onInput }` | Replace the default `<input>` entirely |
|
|
171
|
+
| `#item` | `{ command, active, matches }` | Replace the entire result row |
|
|
172
|
+
| `#group-header` | `{ group }` | Replace the group label row |
|
|
173
|
+
| `#empty` | `{ query }` | Shown when the query returns no results |
|
|
174
|
+
| `#footer` | — | Content below the result list |
|
|
175
|
+
|
|
176
|
+
### Custom `#item` slot
|
|
177
|
+
|
|
178
|
+
```vue
|
|
179
|
+
<CommandPalette>
|
|
180
|
+
<template #item="{ command, active, matches }">
|
|
181
|
+
<div :class="['my-item', { 'my-item--active': active }]">
|
|
182
|
+
<span v-if="command.icon" class="my-item__icon">{{ command.icon }}</span>
|
|
183
|
+
|
|
184
|
+
<span class="my-item__body">
|
|
185
|
+
<component :is="highlightMatches(command.label, matches)" />
|
|
186
|
+
<span v-if="command.description" class="my-item__desc">
|
|
187
|
+
{{ command.description }}
|
|
188
|
+
</span>
|
|
189
|
+
</span>
|
|
190
|
+
|
|
191
|
+
<span v-if="command.shortcut?.length" class="my-item__shortcut">
|
|
192
|
+
<kbd v-for="k in command.shortcut" :key="k">{{ k }}</kbd>
|
|
193
|
+
</span>
|
|
194
|
+
|
|
195
|
+
<span v-if="command.subCommands?.length">›</span>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
</CommandPalette>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Custom `#footer` slot
|
|
202
|
+
|
|
203
|
+
```vue
|
|
204
|
+
<CommandPalette>
|
|
205
|
+
<template #footer>
|
|
206
|
+
<div class="my-footer">
|
|
207
|
+
<span><kbd>↑↓</kbd> navigate</span>
|
|
208
|
+
<span><kbd>↵</kbd> select</span>
|
|
209
|
+
<span><kbd>Esc</kbd> close</span>
|
|
210
|
+
</div>
|
|
211
|
+
</template>
|
|
212
|
+
</CommandPalette>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Programmatic trigger via `#trigger` slot
|
|
216
|
+
|
|
217
|
+
```vue
|
|
218
|
+
<CommandPalette>
|
|
219
|
+
<template #trigger="{ toggle }">
|
|
220
|
+
<button @click="toggle">Open palette</button>
|
|
221
|
+
</template>
|
|
222
|
+
</CommandPalette>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## CommandItem
|
|
228
|
+
|
|
229
|
+
Renders a single command row with icon, label (with match highlighting), description, shortcut badge, loading spinner, and disabled state. Used internally; also available for custom layouts.
|
|
230
|
+
|
|
231
|
+
### Props
|
|
232
|
+
|
|
233
|
+
| Prop | Type | Description |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `command` | `Command` | The command to render |
|
|
236
|
+
| `active` | `boolean` | Whether this row is keyboard-selected |
|
|
237
|
+
| `matches` | `Array<[number, number]>` | Highlight ranges from the fuzzy search engine |
|
|
238
|
+
| `itemId` | `string` | `id` attribute for ARIA `aria-activedescendant` |
|
|
239
|
+
| `loadingCommandId` | `string \| null` | ID of the currently executing command; shows spinner |
|
|
240
|
+
|
|
241
|
+
### Emits
|
|
242
|
+
|
|
243
|
+
| Event | Description |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `execute` | User clicked or pressed Enter on this item |
|
|
246
|
+
| `activate` | User hovered over this item |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## CommandGroup
|
|
251
|
+
|
|
252
|
+
Renders a group header followed by its `CommandItem` rows. Passes all `#item`, `#item-icon`, and `#item-shortcut` slots through to each child.
|
|
253
|
+
|
|
254
|
+
### Props
|
|
255
|
+
|
|
256
|
+
| Prop | Type | Description |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `group` | `CommandGroup` | Group metadata (`id`, `label`, `priority`) |
|
|
259
|
+
| `items` | `SearchResult[]` | Filtered results for this group |
|
|
260
|
+
| `activeIndex` | `number` | Global active index for highlight tracking |
|
|
261
|
+
| `globalOffset` | `number` | Index offset within the flat result list |
|
|
262
|
+
| `loadingCommandId` | `string \| null` | Forwarded to each `CommandItem` |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## useCommandPalette
|
|
267
|
+
|
|
268
|
+
Composable that exposes the global palette state and all control functions. Must be called inside a component tree where `VCommandPalettePlugin` is installed.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { useCommandPalette } from '@macrulez/vue-command-palette'
|
|
272
|
+
|
|
273
|
+
const {
|
|
274
|
+
isOpen, // Readonly<Ref<boolean>>
|
|
275
|
+
query, // Ref<string>
|
|
276
|
+
results, // ComputedRef<SearchResult[]>
|
|
277
|
+
activeIndex, // Ref<number>
|
|
278
|
+
history, // Readonly<Ref<HistoryEntry[]>>
|
|
279
|
+
loadingCommandId, // Readonly<Ref<string | null>>
|
|
280
|
+
|
|
281
|
+
open, // (paletteId?: string) => void
|
|
282
|
+
close, // () => void
|
|
283
|
+
toggle, // () => void
|
|
284
|
+
goBack, // () => void — pop history, or close if empty
|
|
285
|
+
executeCommand, // (cmd: Command) => Promise<void>
|
|
286
|
+
executeActive, // () => Promise<void> — run the currently selected result
|
|
287
|
+
getRecentCommands, // () => Command[]
|
|
288
|
+
registerCommands, // (commands: Command[]) => () => void
|
|
289
|
+
registerGroup, // (group: CommandGroup) => () => void
|
|
290
|
+
addRecent, // (id: string) => void
|
|
291
|
+
} = useCommandPalette()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Programmatic control
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const { open, close, toggle } = useCommandPalette()
|
|
298
|
+
|
|
299
|
+
open() // open the palette
|
|
300
|
+
close() // close and reset state
|
|
301
|
+
toggle() // toggle open/close
|
|
302
|
+
|
|
303
|
+
// Push a sub-palette (breadcrumb navigation)
|
|
304
|
+
open('parent-command-id')
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## useRegisterCommands
|
|
310
|
+
|
|
311
|
+
Registers commands when the component mounts and automatically unregisters them when it unmounts. Commands registered this way have no group header.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { useRegisterCommands } from '@macrulez/vue-command-palette'
|
|
315
|
+
|
|
316
|
+
// In any component setup()
|
|
317
|
+
useRegisterCommands([
|
|
318
|
+
{
|
|
319
|
+
id: 'format-doc',
|
|
320
|
+
label: 'Format Document',
|
|
321
|
+
icon: '✨',
|
|
322
|
+
perform: () => formatDocument(),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'toggle-sidebar',
|
|
326
|
+
label: 'Toggle Sidebar',
|
|
327
|
+
shortcut: ['$mod', 'b'],
|
|
328
|
+
perform: () => sidebar.toggle(),
|
|
329
|
+
},
|
|
330
|
+
])
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## useRegisterGroup
|
|
336
|
+
|
|
337
|
+
Registers a full command group with a label and priority on mount, unregisters on unmount.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { useRegisterGroup } from '@macrulez/vue-command-palette'
|
|
341
|
+
|
|
342
|
+
useRegisterGroup({
|
|
343
|
+
id: 'editor',
|
|
344
|
+
label: 'Editor',
|
|
345
|
+
priority: 80,
|
|
346
|
+
commands: [
|
|
347
|
+
{
|
|
348
|
+
id: 'editor-format',
|
|
349
|
+
label: 'Format Document',
|
|
350
|
+
description: 'Run Prettier on the current file',
|
|
351
|
+
icon: '✨',
|
|
352
|
+
perform: () => format(),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: 'editor-lint',
|
|
356
|
+
label: 'Lint File',
|
|
357
|
+
description: 'Run ESLint and show errors',
|
|
358
|
+
icon: '🔍',
|
|
359
|
+
enabled: () => isFileOpen.value,
|
|
360
|
+
perform: () => lint(),
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## VCommandPalettePlugin
|
|
369
|
+
|
|
370
|
+
The Vue plugin that sets up the global command store, keyboard listener, and reactive state.
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
app.use(VCommandPalettePlugin, options)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Options (`PaletteOptions`)
|
|
377
|
+
|
|
378
|
+
| Option | Type | Default | Description |
|
|
379
|
+
|---|---|---|---|
|
|
380
|
+
| `hotkey` | `string[]` | `['$mod', 'k']` | Key combination to toggle the palette |
|
|
381
|
+
| `colorTheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Initial color theme of the palette |
|
|
382
|
+
| `persistRecent` | `boolean` | `true` | Persist recent commands to `localStorage` |
|
|
383
|
+
| `maxRecent` | `number` | `5` | Maximum total recent commands stored |
|
|
384
|
+
| `maxRecentPerGroup` | `number` | `0` | Max recent per group (`0` = unlimited) |
|
|
385
|
+
| `localStorageKey` | `string` | `'vcp:recent'` | Key used in `localStorage` |
|
|
386
|
+
| `onOpen` | `() => void` | — | Called every time the palette opens |
|
|
387
|
+
| `onClose` | `() => void` | — | Called every time the palette closes |
|
|
388
|
+
| `onError` | `(err: unknown, command: Command) => void` | — | Called when `perform()` throws |
|
|
389
|
+
|
|
390
|
+
### Example with all options
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
app.use(VCommandPalettePlugin, {
|
|
394
|
+
hotkey: ['$mod', 'k'],
|
|
395
|
+
colorTheme: 'system', // 'light' | 'dark' | 'system'
|
|
396
|
+
persistRecent: true,
|
|
397
|
+
maxRecent: 8,
|
|
398
|
+
maxRecentPerGroup: 2,
|
|
399
|
+
localStorageKey: 'myapp:palette:recent',
|
|
400
|
+
onOpen: () => analytics.track('palette_opened'),
|
|
401
|
+
onClose: () => analytics.track('palette_closed'),
|
|
402
|
+
onError: (err, cmd) => {
|
|
403
|
+
console.error(`Command "${cmd.label}" failed:`, err)
|
|
404
|
+
toast.error(`Failed to run "${cmd.label}"`)
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### `$mod` key
|
|
410
|
+
|
|
411
|
+
`$mod` resolves to `Meta` (⌘) on macOS and `Ctrl` on Windows / Linux — use it for portable shortcuts:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
hotkey: ['$mod', 'k'] // Cmd+K on Mac, Ctrl+K on Windows
|
|
415
|
+
shortcut: ['$mod', 'shift', 'p'] // Cmd+Shift+P on Mac, Ctrl+Shift+P on Windows
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Command type
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
interface Command {
|
|
424
|
+
id: string // unique identifier
|
|
425
|
+
label: string // display text, searched by fuzzy engine
|
|
426
|
+
description?: string // subtitle shown below the label
|
|
427
|
+
icon?: Component | string // Vue component or emoji / string
|
|
428
|
+
keywords?: string[] // extra search terms
|
|
429
|
+
aliases?: string[] // alternate labels (same score as label match)
|
|
430
|
+
shortcut?: string[] // display-only hint: ['$mod', 'k']
|
|
431
|
+
disabled?: boolean // permanently unavailable
|
|
432
|
+
enabled?: () => boolean // dynamically disable — evaluated on each render
|
|
433
|
+
confirm?: string // prompt shown before execute
|
|
434
|
+
perform: () => void | Promise<void> // action; may be async
|
|
435
|
+
subCommands?: Command[] // opens a nested palette when selected
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### `icon` field
|
|
440
|
+
|
|
441
|
+
Accepts an emoji string, a plain text string, or any Vue component:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import MyIcon from './MyIcon.vue'
|
|
445
|
+
|
|
446
|
+
{ icon: '🏠' } // emoji string
|
|
447
|
+
{ icon: '⌘' } // symbol string
|
|
448
|
+
{ icon: MyIcon } // Vue component — rendered as <MyIcon />
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Fuzzy search
|
|
454
|
+
|
|
455
|
+
The built-in `fuzzySearch` function is exported for standalone use:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
import { fuzzySearch, highlightMatches } from '@macrulez/vue-command-palette'
|
|
459
|
+
|
|
460
|
+
const results = fuzzySearch('git cm', commands)
|
|
461
|
+
// sorted by score: exact → prefix → substring → fuzzy
|
|
462
|
+
|
|
463
|
+
// Render highlighted label in a custom slot
|
|
464
|
+
const vnode = highlightMatches(command.label, result.matches)
|
|
465
|
+
// returns a VNode: <span>git <mark class="vcp-match">c</mark>o<mark class="vcp-match">m</mark>mit</span>
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Scoring table
|
|
469
|
+
|
|
470
|
+
| Match type | Score |
|
|
471
|
+
|---|---|
|
|
472
|
+
| Exact match | 100 |
|
|
473
|
+
| Prefix match | 80 |
|
|
474
|
+
| Substring (contains) | 60 |
|
|
475
|
+
| Fuzzy (all chars in order) | 1 – 40 (penalised by character gaps) |
|
|
476
|
+
| No match | −1 (excluded from results) |
|
|
477
|
+
|
|
478
|
+
The engine checks `label`, all `keywords[]`, and all `aliases[]`. The highest score across all fields wins. Commands where `disabled: true` or `enabled()` returns `false` are excluded before scoring.
|
|
479
|
+
|
|
480
|
+
### Diacritic normalization
|
|
481
|
+
|
|
482
|
+
Strings are normalized with `NFD` Unicode decomposition before comparison, so accents are ignored:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
fuzzySearch('cafe', [{ id: '1', label: 'Café', perform: () => {} }]) // → match
|
|
486
|
+
fuzzySearch('strase',[{ id: '2', label: 'Straße', perform: () => {} }]) // → match
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Keyboard shortcuts
|
|
492
|
+
|
|
493
|
+
### Modifier + key
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
// In a command definition (display hint only — use perform() for the action)
|
|
497
|
+
{
|
|
498
|
+
id: 'save',
|
|
499
|
+
label: 'Save File',
|
|
500
|
+
shortcut: ['$mod', 's'],
|
|
501
|
+
perform: () => save(),
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
To bind a real global shortcut, use `createKeyboardManager` directly:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
import { createKeyboardManager } from '@macrulez/vue-command-palette'
|
|
509
|
+
|
|
510
|
+
const km = createKeyboardManager()
|
|
511
|
+
km.start()
|
|
512
|
+
|
|
513
|
+
const unregister = km.registerShortcut(['$mod', 'shift', 'p'], () => {
|
|
514
|
+
openCommandPalette()
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// Later, to clean up:
|
|
518
|
+
unregister()
|
|
519
|
+
km.stop()
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Bare-key sequences
|
|
523
|
+
|
|
524
|
+
Two consecutive keys without any modifier, within a 500 ms window:
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
km.registerShortcut(['g', 'h'], () => router.push('/home'))
|
|
528
|
+
km.registerShortcut(['g', 'p'], () => router.push('/projects'))
|
|
529
|
+
km.registerShortcut(['g', 's'], () => router.push('/settings'))
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Key reference
|
|
533
|
+
|
|
534
|
+
| String | Resolved to |
|
|
535
|
+
|---|---|
|
|
536
|
+
| `'$mod'` | `Meta` on macOS, `Ctrl` on Windows/Linux |
|
|
537
|
+
| `'shift'` | Shift |
|
|
538
|
+
| `'alt'` | Alt / Option |
|
|
539
|
+
| `'ctrl'` | Ctrl (explicit, not cross-platform) |
|
|
540
|
+
| `'meta'` | Meta / Cmd (explicit) |
|
|
541
|
+
| Any other string | Compared with `event.key.toLowerCase()` |
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Nested palettes
|
|
546
|
+
|
|
547
|
+
Add `subCommands` to any command to open a child palette when it is selected. The parent state is pushed to a breadcrumb history stack.
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
{
|
|
551
|
+
id: 'change-theme',
|
|
552
|
+
label: 'Change Theme',
|
|
553
|
+
icon: '🎨',
|
|
554
|
+
perform: () => {}, // not called when subCommands is present
|
|
555
|
+
subCommands: [
|
|
556
|
+
{
|
|
557
|
+
id: 'theme-light',
|
|
558
|
+
label: 'Light',
|
|
559
|
+
icon: '☀️',
|
|
560
|
+
enabled: () => theme.value !== 'light',
|
|
561
|
+
perform: () => { theme.value = 'light' },
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 'theme-dark',
|
|
565
|
+
label: 'Dark',
|
|
566
|
+
icon: '🌙',
|
|
567
|
+
enabled: () => theme.value !== 'dark',
|
|
568
|
+
perform: () => { theme.value = 'dark' },
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
id: 'theme-system',
|
|
572
|
+
label: 'System',
|
|
573
|
+
icon: '💻',
|
|
574
|
+
enabled: () => theme.value !== 'system',
|
|
575
|
+
perform: () => { theme.value = 'system' },
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Sub-palettes can be nested to any depth.
|
|
582
|
+
|
|
583
|
+
**Navigation keys inside a sub-palette:**
|
|
584
|
+
|
|
585
|
+
| Key | Action |
|
|
586
|
+
|---|---|
|
|
587
|
+
| `Backspace` (empty input) | Go back to parent palette |
|
|
588
|
+
| `Esc` | Go back if history exists, otherwise close |
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Confirmation step
|
|
593
|
+
|
|
594
|
+
Set `confirm` to a non-empty string to require user confirmation before the command runs.
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
{
|
|
598
|
+
id: 'delete-project',
|
|
599
|
+
label: 'Delete Project',
|
|
600
|
+
icon: '🗑️',
|
|
601
|
+
keywords: ['remove', 'erase'],
|
|
602
|
+
confirm: 'Delete this project permanently? This action cannot be undone.',
|
|
603
|
+
perform: async () => {
|
|
604
|
+
await api.deleteProject(projectId)
|
|
605
|
+
router.push('/')
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The palette replaces the result list with the confirmation message and two buttons:
|
|
611
|
+
|
|
612
|
+
- **Yes, proceed** — executes the command and closes
|
|
613
|
+
- **Cancel** — dismisses and returns to the result list
|
|
614
|
+
|
|
615
|
+
`Enter` confirms, `Esc` cancels.
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Async search
|
|
620
|
+
|
|
621
|
+
Each group can provide an `onSearch` callback that returns dynamic commands for a given query. Useful for searching external APIs, databases, or documentation.
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
useRegisterGroup({
|
|
625
|
+
id: 'docs-search',
|
|
626
|
+
label: 'Documentation',
|
|
627
|
+
commands: [], // static commands (can be empty for search-only groups)
|
|
628
|
+
onSearch: async (query: string) => {
|
|
629
|
+
const results = await searchDocs(query)
|
|
630
|
+
return results.slice(0, 5).map(doc => ({
|
|
631
|
+
id: `doc-${doc.slug}`,
|
|
632
|
+
label: doc.title,
|
|
633
|
+
description: doc.excerpt,
|
|
634
|
+
icon: '📄',
|
|
635
|
+
perform: () => window.open(doc.url, '_blank'),
|
|
636
|
+
}))
|
|
637
|
+
},
|
|
638
|
+
})
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
- Debounced by **200 ms** to avoid excessive requests
|
|
642
|
+
- A loading indicator appears while the request is in flight
|
|
643
|
+
- Async results are merged with sync results and re-sorted by score
|
|
644
|
+
- Empty query clears async results immediately (no debounce)
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Recent commands
|
|
649
|
+
|
|
650
|
+
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.
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
app.use(VCommandPalettePlugin, {
|
|
654
|
+
persistRecent: true,
|
|
655
|
+
maxRecent: 8, // keep at most 8 commands total
|
|
656
|
+
maxRecentPerGroup: 2, // at most 2 per group (0 = unlimited)
|
|
657
|
+
localStorageKey: 'myapp:recent',
|
|
658
|
+
})
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Recent commands are resolved at runtime — if a command is unregistered (e.g. its component unmounted), it is silently excluded from the recent list.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Theming
|
|
666
|
+
|
|
667
|
+
The palette is fully styled via CSS custom properties. Import the default stylesheet, then override variables in your own CSS.
|
|
668
|
+
|
|
669
|
+
```css
|
|
670
|
+
/* Override globally */
|
|
671
|
+
:root {
|
|
672
|
+
--vcp-dialog-width: 640px;
|
|
673
|
+
--vcp-item-height: 44px;
|
|
674
|
+
--vcp-item-active-bg: #ede9fe;
|
|
675
|
+
--vcp-match-color: #7c3aed;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* Or scope to a parent element */
|
|
679
|
+
.my-app [data-vcp] {
|
|
680
|
+
--vcp-dialog-bg: #fafafa;
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### All CSS custom properties
|
|
685
|
+
|
|
686
|
+
```css
|
|
687
|
+
:root {
|
|
688
|
+
/* Overlay & dialog */
|
|
689
|
+
--vcp-z-index: 9999;
|
|
690
|
+
--vcp-overlay-bg: rgba(0, 0, 0, 0.5);
|
|
691
|
+
--vcp-dialog-bg: #ffffff;
|
|
692
|
+
--vcp-dialog-color: #111111;
|
|
693
|
+
--vcp-dialog-radius: 8px;
|
|
694
|
+
--vcp-dialog-shadow: 0 16px 70px rgba(0, 0, 0, 0.2);
|
|
695
|
+
--vcp-dialog-width: 560px;
|
|
696
|
+
--vcp-dialog-max-height: 60vh;
|
|
697
|
+
--vcp-dialog-padding-top: 15vh;
|
|
698
|
+
|
|
699
|
+
/* Borders */
|
|
700
|
+
--vcp-border-color: #eeeeee;
|
|
701
|
+
|
|
702
|
+
/* Search input */
|
|
703
|
+
--vcp-input-font-size: 16px;
|
|
704
|
+
|
|
705
|
+
/* Result items */
|
|
706
|
+
--vcp-item-height: 40px;
|
|
707
|
+
--vcp-item-active-bg: #f0f0f0;
|
|
708
|
+
--vcp-item-font-size: 14px;
|
|
709
|
+
--vcp-item-radius: 4px;
|
|
710
|
+
|
|
711
|
+
/* Group headers */
|
|
712
|
+
--vcp-group-header-color: #999999;
|
|
713
|
+
--vcp-group-header-font-size: 11px;
|
|
714
|
+
|
|
715
|
+
/* Keyboard badge */
|
|
716
|
+
--vcp-kbd-bg: #eeeeee;
|
|
717
|
+
--vcp-kbd-border: #dddddd;
|
|
718
|
+
|
|
719
|
+
/* Match highlight */
|
|
720
|
+
--vcp-match-color: inherit;
|
|
721
|
+
|
|
722
|
+
/* Breadcrumb (nested palettes) */
|
|
723
|
+
--vcp-breadcrumb-color: #888888;
|
|
724
|
+
|
|
725
|
+
/* Empty / loading states */
|
|
726
|
+
--vcp-state-color: #999999;
|
|
727
|
+
|
|
728
|
+
/* Scrollbar */
|
|
729
|
+
--vcp-scrollbar-thumb: #d0d0d0;
|
|
730
|
+
--vcp-scrollbar-thumb-hover: #b0b0b0;
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Dark mode
|
|
735
|
+
|
|
736
|
+
The stylesheet includes automatic dark mode via `@media (prefers-color-scheme: dark)`. To override manually with a theme class:
|
|
737
|
+
|
|
738
|
+
```css
|
|
739
|
+
[data-theme="dark"] {
|
|
740
|
+
--vcp-dialog-bg: #1a1a1a;
|
|
741
|
+
--vcp-dialog-color: #eeeeee;
|
|
742
|
+
--vcp-border-color: #333333;
|
|
743
|
+
--vcp-item-active-bg: #2a2a2a;
|
|
744
|
+
--vcp-kbd-bg: #2a2a2a;
|
|
745
|
+
--vcp-kbd-border: #444444;
|
|
746
|
+
--vcp-group-header-color: #666666;
|
|
747
|
+
--vcp-scrollbar-thumb: #444444;
|
|
748
|
+
--vcp-scrollbar-thumb-hover: #666666;
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Built-in theme switcher
|
|
753
|
+
|
|
754
|
+
The palette includes a built-in light / system / dark switcher rendered directly inside the search bar. The initial theme is set via the `colorTheme` plugin option and can be changed at runtime via `useCommandPalette()`:
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
app.use(VCommandPalettePlugin, {
|
|
758
|
+
colorTheme: 'dark', // 'light' | 'dark' | 'system' (default: 'system')
|
|
759
|
+
})
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
// Change theme programmatically from any component
|
|
764
|
+
import { useCommandPalette } from '@macrulez/vue-command-palette'
|
|
765
|
+
|
|
766
|
+
const { colorTheme } = useCommandPalette()
|
|
767
|
+
colorTheme.value = 'dark'
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
`'system'` follows `prefers-color-scheme`. Selecting `'light'` or `'dark'` applies `.vcp-theme-light` / `.vcp-theme-dark` on the overlay, which override the media query.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## Nuxt
|
|
775
|
+
|
|
776
|
+
Add to `nuxt.config.ts`:
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
export default defineNuxtConfig({
|
|
780
|
+
modules: ['@macrulez/vue-command-palette/nuxt'],
|
|
781
|
+
})
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Options are read from `runtimeConfig.public.vCommandPalette`. Configure in `nuxt.config.ts`:
|
|
785
|
+
|
|
786
|
+
```ts
|
|
787
|
+
export default defineNuxtConfig({
|
|
788
|
+
modules: ['@macrulez/vue-command-palette/nuxt'],
|
|
789
|
+
runtimeConfig: {
|
|
790
|
+
public: {
|
|
791
|
+
vCommandPalette: {
|
|
792
|
+
hotkey: ['$mod', 'k'],
|
|
793
|
+
persistRecent: true,
|
|
794
|
+
maxRecent: 5,
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
})
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
The Nuxt module installs the plugin automatically. `useCommandPalette`, `useRegisterGroup`, and `useRegisterCommands` are available in all components without explicit imports (if using `@nuxt/eslint` with auto-imports enabled).
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## Testing utilities
|
|
806
|
+
|
|
807
|
+
```ts
|
|
808
|
+
import { createPaletteContext, PaletteProvider } from '@macrulez/vue-command-palette/testing'
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### `createPaletteContext`
|
|
812
|
+
|
|
813
|
+
Creates a fully isolated palette context — no real DOM, no plugin, no `localStorage` side-effects:
|
|
814
|
+
|
|
815
|
+
```ts
|
|
816
|
+
import { createPaletteContext } from '@macrulez/vue-command-palette/testing'
|
|
817
|
+
import { mount } from '@vue/test-utils'
|
|
818
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
819
|
+
import MyComponent from './MyComponent.vue'
|
|
820
|
+
|
|
821
|
+
describe('MyComponent', () => {
|
|
822
|
+
it('executes the command', async () => {
|
|
823
|
+
const performFn = vi.fn()
|
|
824
|
+
|
|
825
|
+
const { provide, isOpen, query, store } = createPaletteContext({
|
|
826
|
+
commands: [
|
|
827
|
+
{ id: 'test-cmd', label: 'Test Command', perform: performFn },
|
|
828
|
+
],
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
const wrapper = mount(MyComponent, {
|
|
832
|
+
global: { provide },
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
// Interact
|
|
836
|
+
query.value = 'test'
|
|
837
|
+
await wrapper.find('[data-testid="item"]').trigger('click')
|
|
838
|
+
|
|
839
|
+
expect(performFn).toHaveBeenCalledOnce()
|
|
840
|
+
})
|
|
841
|
+
})
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### `PaletteProvider`
|
|
845
|
+
|
|
846
|
+
A wrapper component that provides context to its slot children — useful for component tree tests:
|
|
847
|
+
|
|
848
|
+
```ts
|
|
849
|
+
import { PaletteProvider } from '@macrulez/vue-command-palette/testing'
|
|
850
|
+
import { mount } from '@vue/test-utils'
|
|
851
|
+
|
|
852
|
+
const wrapper = mount(PaletteProvider, {
|
|
853
|
+
props: {
|
|
854
|
+
commands: [{ id: 'cmd', label: 'My Command', perform: vi.fn() }],
|
|
855
|
+
groups: [],
|
|
856
|
+
},
|
|
857
|
+
slots: {
|
|
858
|
+
default: MyConsumerComponent,
|
|
859
|
+
},
|
|
860
|
+
})
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### `createPaletteContext` options
|
|
864
|
+
|
|
865
|
+
| Option | Type | Default | Description |
|
|
866
|
+
|---|---|---|---|
|
|
867
|
+
| `commands` | `Command[]` | `[]` | Commands to pre-register (no group) |
|
|
868
|
+
| `groups` | `CommandGroup[]` | `[]` | Groups to pre-register |
|
|
869
|
+
| `persistRecent` | `boolean` | `false` | Enable `localStorage` persistence |
|
|
870
|
+
| `maxRecent` | `number` | `5` | Recent command limit |
|
|
871
|
+
| `maxRecentPerGroup` | `number` | `0` | Per-group recent limit |
|
|
872
|
+
| `localStorageKey` | `string` | `'vcp:recent:test'` | Key used if `persistRecent` is `true` |
|
|
873
|
+
| `onOpen` | `() => void` | — | Mock callback for open events |
|
|
874
|
+
| `onClose` | `() => void` | — | Mock callback for close events |
|
|
875
|
+
| `onError` | `(err, cmd) => void` | — | Mock error handler |
|
|
876
|
+
|
|
877
|
+
### Return value
|
|
878
|
+
|
|
879
|
+
```ts
|
|
880
|
+
const {
|
|
881
|
+
ctx, // full PaletteContext — pass to inject-based code
|
|
882
|
+
store, // CommandStore — register/search commands directly
|
|
883
|
+
isOpen, // Ref<boolean>
|
|
884
|
+
query, // Ref<string>
|
|
885
|
+
activeIndex, // Ref<number>
|
|
886
|
+
provide, // Record for Vue Test Utils `global: { provide }`
|
|
887
|
+
} = createPaletteContext(options)
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## TypeScript types
|
|
893
|
+
|
|
894
|
+
All public types are exported from the package root:
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
import type {
|
|
898
|
+
Command,
|
|
899
|
+
CommandGroupType, // group definition — NOT the CommandGroup component
|
|
900
|
+
CommandSection,
|
|
901
|
+
SearchResult, // { command, score, matches, groupId? }
|
|
902
|
+
PaletteOptions,
|
|
903
|
+
PaletteContext,
|
|
904
|
+
PaletteState,
|
|
905
|
+
CommandStore,
|
|
906
|
+
KeyboardManager,
|
|
907
|
+
} from '@macrulez/vue-command-palette'
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
> **Note**: The named export `CommandGroup` is the **Vue component**. The group-definition interface is exported as `CommandGroupType` to avoid the collision.
|
|
911
|
+
|
|
912
|
+
### `SearchResult`
|
|
913
|
+
|
|
914
|
+
```ts
|
|
915
|
+
interface SearchResult {
|
|
916
|
+
command: Command
|
|
917
|
+
score: number
|
|
918
|
+
matches: Array<[start: number, end: number]>
|
|
919
|
+
groupId?: string
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### `PaletteContext`
|
|
924
|
+
|
|
925
|
+
The full injectable context, accessible in custom composables via `inject(PALETTE_INJECT_KEY)`:
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
interface PaletteContext {
|
|
929
|
+
store: CommandStore
|
|
930
|
+
keyboard: KeyboardManager
|
|
931
|
+
isOpen: Ref<boolean>
|
|
932
|
+
query: Ref<string>
|
|
933
|
+
activeIndex: Ref<number>
|
|
934
|
+
history: Ref<HistoryEntry[]>
|
|
935
|
+
recentIds: Ref<string[]>
|
|
936
|
+
loadingCommandId: Ref<string | null>
|
|
937
|
+
results: ComputedRef<SearchResult[]>
|
|
938
|
+
persistRecent: boolean
|
|
939
|
+
maxRecent: number
|
|
940
|
+
maxRecentPerGroup: number
|
|
941
|
+
localStorageKey: string
|
|
942
|
+
onOpen?: () => void
|
|
943
|
+
onClose?: () => void
|
|
944
|
+
onError?: (err: unknown, command: Command) => void
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## Accessibility
|
|
951
|
+
|
|
952
|
+
| Feature | Implementation |
|
|
953
|
+
|---|---|
|
|
954
|
+
| `role="dialog"` + `aria-modal="true"` | Applied to the palette dialog element |
|
|
955
|
+
| `role="combobox"` | Applied to the search `<input>` |
|
|
956
|
+
| `aria-expanded="true"` | Set on the input while the palette is open |
|
|
957
|
+
| `aria-controls` | Input points to the `role="listbox"` result list |
|
|
958
|
+
| `aria-activedescendant` | Updated as the keyboard-active item changes |
|
|
959
|
+
| `role="listbox"` | Applied to the result list container |
|
|
960
|
+
| `role="option"` | Applied to each `CommandItem` |
|
|
961
|
+
| `aria-selected` | Set to `true` on the currently active item |
|
|
962
|
+
| `aria-disabled` | Set when `disabled: true` or `enabled()` returns `false` |
|
|
963
|
+
| `aria-live="polite"` | Breadcrumb — screen readers announce sub-palette navigation |
|
|
964
|
+
| Focus trap | `Tab` is intercepted to keep focus inside the dialog |
|
|
965
|
+
| Scroll lock | `document.body.style.overflow` is set to `hidden` while open |
|
|
966
|
+
| Reduced motion | `@media (prefers-reduced-motion: reduce)` disables the fade transition |
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
## SSR compatibility
|
|
971
|
+
|
|
972
|
+
All browser-only APIs are guarded before use:
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
// KeyboardManager — skips addEventListener on the server
|
|
976
|
+
if (typeof document === 'undefined') return
|
|
977
|
+
|
|
978
|
+
// Recent commands — skips localStorage on the server
|
|
979
|
+
if (typeof localStorage === 'undefined') return
|
|
980
|
+
|
|
981
|
+
// CommandItem — platform detection for ⌘ vs Ctrl label
|
|
982
|
+
typeof navigator !== 'undefined' && navigator.platform.includes('Mac')
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
`VirtualList` (used for result sets > 50 items) renders an empty placeholder on the server and hydrates on the client. All slot content and command registration are fully SSR-safe.
|
|
986
|
+
|
|
987
|
+
---
|
|
988
|
+
|
|
989
|
+
## Bundle size
|
|
990
|
+
|
|
991
|
+
| Entry point | Peer deps | Notes |
|
|
992
|
+
|---|---|---|
|
|
993
|
+
| `@macrulez/vue-command-palette` | `vue ^3.3` | Components, composables, fuzzy engine, keyboard manager |
|
|
994
|
+
| `@macrulez/vue-command-palette/style.css` | — | Default styles; ~3 KB |
|
|
995
|
+
| `@macrulez/vue-command-palette/testing` | `vue ^3.3` | `createPaletteContext` + `PaletteProvider`; dev/test only |
|
|
996
|
+
| `@macrulez/vue-command-palette/nuxt` | `nuxt ^3`, `vue ^3.3` | Nuxt auto-plugin |
|
|
997
|
+
|
|
998
|
+
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.
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
## License
|
|
1003
|
+
|
|
1004
|
+
MIT
|
|
1005
|
+
|
|
1006
|
+
---
|
|
1007
|
+
|
|
1008
|
+
## Author
|
|
1009
|
+
|
|
1010
|
+
Danil Lisin Vladimirovich aka Macrulez
|
|
1011
|
+
|
|
1012
|
+
GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru/en](https://macrulez.ru/en)
|
|
1013
|
+
|
|
1014
|
+
Bugs and questions — [issues](https://github.com/macrulezru/@macrulez/vue-command-palette/issues)
|