vue-command-kit 0.1.2 → 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 CHANGED
@@ -2,33 +2,26 @@
2
2
  <img src="https://img.shields.io/npm/v/vue-command-kit?color=blue&label=version" alt="npm">
3
3
  <img src="https://img.shields.io/badge/vue-3.4%2B-brightgreen" alt="vue">
4
4
  <img src="https://img.shields.io/badge/license-MIT-blue" alt="license">
5
- <img src="https://img.shields.io/badge/bundle-3.4kB_gzip-green" alt="size">
5
+ <img src="https://img.shields.io/badge/bundle-5.0kB_gzip-green" alt="size">
6
+ <img src="https://img.shields.io/badge/tests-117_passed-brightgreen" alt="tests">
6
7
  </p>
7
8
 
8
9
  <h1 align="center">⌘K — vue-cmdk</h1>
9
10
  <p align="center">
10
- <b>A fast, composable, unstyled command palette for Vue 3.</b><br>
11
- Press <kbd>⌘K</kbd> and take control.
11
+ A fast, composable, unstyled command palette for Vue 3.
12
12
  </p>
13
13
 
14
- <p align="center">
15
- <img src="./demo.gif" alt="vue-cmdk demo" width="720">
16
- </p>
17
-
18
- ## ✨ Features
14
+ ## Features
19
15
 
20
- - **🧩 Compound component API** — `<Command.Dialog>`, `<Command.Input>`, `<Command.Item>`, etc.
21
- - **💄 Unstyled** — Bring your own CSS, zero opinions, full design control
22
- - **🔍 Built-in search** — Fast case-insensitive filtering with keyword matching
23
- - **⌨️ Keyboard-first** — Arrow keys, Enter, Escape all built-in, no config needed
24
- - **🔊 Global shortcut** — Register shortcuts on items (e.g. `⌘S`) and they work globally
25
- - **📦 Tiny** — 3.4 kB gzipped, **zero runtime dependencies** (peer: `vue` only)
26
- - **🎯 TypeScript** — Full type inference and declaration files
27
- - **🔄 Dynamic items** — Pass items as a reactive array, swap anytime
28
- - **🛠 Custom filter** — Provide your own filter function
29
- - **♿ Accessible** — ARIA attributes, focus trap, `aria-live` region
16
+ - **Compound component API** — Declarative composition with `<Command.Dialog>`, `<Command.Input>`, `<Command.Item>`, and more
17
+ - **Built-in search** — Case-insensitive filtering with keyword matching and result highlighting
18
+ - **Force render** — `forceMount` prop keeps items in the DOM when filtered out (for animations)
19
+ - **Keyboard-first** — Arrow key navigation, Enter to select, Escape to close, global shortcuts
20
+ - **Unstyled**Zero CSS opinions; bring your own styles
21
+ - **TypeScript**Full type inference, exported types, and declaration files
22
+ - **Zero runtime dependencies** — Peer dependency only on `vue ^3.4.0`
30
23
 
31
- ## 🚀 Install
24
+ ## Installation
32
25
 
33
26
  ```bash
34
27
  npm install vue-command-kit
@@ -36,7 +29,7 @@ npm install vue-command-kit
36
29
 
37
30
  ## Quick Start
38
31
 
39
- ### Simple items prop
32
+ ### With items prop
40
33
 
41
34
  ```vue
42
35
  <script setup lang="ts">
@@ -68,7 +61,7 @@ function onSelect(item: CommandItemData) {
68
61
  </template>
69
62
  ```
70
63
 
71
- ### Advanced custom slot content
64
+ ### With custom slot content
72
65
 
73
66
  ```vue
74
67
  <Command.Dialog :visible="visible" @update:visible="visible = $event">
@@ -85,87 +78,7 @@ function onSelect(item: CommandItemData) {
85
78
  </Command.Dialog>
86
79
  ```
87
80
 
88
- ### With custom filter
89
-
90
- ```vue
91
- <script setup lang="ts">
92
- import { Command } from 'vue-command-kit'
93
-
94
- function myFilter(items: CommandItemData[], query: string) {
95
- return items.filter((item) => item.label?.includes(query))
96
- }
97
- </script>
98
-
99
- <template>
100
- <Command.Dialog :filter="myFilter" ... />
101
- </template>
102
- ```
103
-
104
- ### With v-model:searchQuery
105
-
106
- ```vue
107
- <script setup lang="ts">
108
- import { ref } from 'vue'
109
- import { Command } from 'vue-command-kit'
110
- import type { CommandItemData } from 'vue-command-kit'
111
-
112
- const visible = ref(false)
113
- const query = ref('')
114
-
115
- const items: CommandItemData[] = [
116
- { value: 'home', label: 'Home', keywords: ['dashboard'] },
117
- { value: 'settings', label: 'Settings' },
118
- ]
119
- </script>
120
-
121
- <template>
122
- <Command.Dialog
123
- :visible="visible"
124
- :items="items"
125
- v-model:searchQuery="query"
126
- @update:visible="visible = $event"
127
- />
128
- </template>
129
- ```
130
-
131
- ### Async data + loading
132
-
133
- ```vue
134
- <script setup lang="ts">
135
- import { ref, watch } from 'vue'
136
- import { Command } from 'vue-command-kit'
137
- import type { CommandItemData } from 'vue-command-kit'
138
-
139
- const visible = ref(false)
140
- const loading = ref(false)
141
- const items = ref<CommandItemData[]>([])
142
-
143
- watch(visible, async (v) => {
144
- if (v) {
145
- loading.value = true
146
- const data = await fetch('/api/commands').then((r) => r.json())
147
- items.value = data.map((d: any) => ({
148
- value: d.id,
149
- label: d.name,
150
- group: d.category,
151
- }))
152
- loading.value = false
153
- }
154
- })
155
- </script>
156
-
157
- <template>
158
- <Command.Dialog
159
- :visible="visible"
160
- :items="items"
161
- :loading="loading"
162
- @update:visible="visible = $event"
163
- @select="console.log('selected', $event)"
164
- />
165
- </template>
166
- ```
167
-
168
- ## 📖 API
81
+ ## API
169
82
 
170
83
  ### Components
171
84
 
@@ -177,26 +90,27 @@ watch(visible, async (v) => {
177
90
  | `<Command.List>` | Scrollable list rendering grouped items |
178
91
  | `<Command.Group>` | Group of items with heading |
179
92
  | `<Command.Item>` | Single selectable command item |
93
+ | `<Command.Separator>` | Visual separator with optional `alwaysRender` |
180
94
  | `<Command.Empty>` | Shown when no results match |
181
- | `<Command.Separator>` | Visual separator |
182
95
  | `<Command.Loading>` | Loading indicator |
183
96
 
184
97
  ### `<Command.Dialog>` Props
185
98
 
186
- | Prop | Type | Default | Description |
187
- | --------------- | ------------------- | ------------------------------- | ---------------------------------------------- |
188
- | `visible` | `boolean` | `false` | Controlled open state |
189
- | `items` | `CommandItemData[]` | `[]` | Items to display |
190
- | `searchQuery` | `string` | `''` | Search query (`v-model:searchQuery`) |
191
- | `value` | `string` | — | Selected item value (`v-model:value`) |
192
- | `placeholder` | `string` | `'Type a command or search...'` | Input placeholder |
193
- | `filter` | `FilterFn` | — | Custom filter function |
194
- | `loading` | `boolean` | `false` | Show loading state |
195
- | `autoFocus` | `boolean` | `true` | Auto-focus input on open |
196
- | `closeOnSelect` | `boolean` | `true` | Close dialog after selection |
197
- | `shouldFilter` | `boolean` | `true` | When `false`, skip built-in filtering |
198
- | `loop` | `boolean` | `true` | When `false`, keyboard nav stops at boundaries |
199
- | `label` | `string` | `'Command menu'` | `aria-label` for the dialog |
99
+ | Prop | Type | Default | Description |
100
+ | --------------- | ----------------------- | ------------------------------- | ---------------------------------------------- |
101
+ | `visible` | `boolean` | `false` | Controlled open state |
102
+ | `items` | `CommandItemData[]` | `[]` | Items to display |
103
+ | `searchQuery` | `string` | | Search query (`v-model:searchQuery`) |
104
+ | `value` | `string` | — | Selected item value (`v-model:value`) |
105
+ | `placeholder` | `string` | `'Type a command or search...'` | Input placeholder |
106
+ | `filter` | `FilterFn` | — | Custom filter function |
107
+ | `loading` | `boolean` | `false` | Show loading state |
108
+ | `autoFocus` | `boolean` | `true` | Auto-focus input on open |
109
+ | `closeOnSelect` | `boolean` | `true` | Close dialog after selection |
110
+ | `shouldFilter` | `boolean` | `true` | When `false`, skip built-in filtering |
111
+ | `loop` | `boolean` | `true` | When `false`, keyboard nav stops at boundaries |
112
+ | `label` | `string` | `'Command menu'` | `aria-label` for the dialog |
113
+ | `container` | `string \| HTMLElement` | `'body'` | Teleport target for the dialog |
200
114
 
201
115
  ### `<Command.Dialog>` Events
202
116
 
@@ -211,155 +125,61 @@ watch(visible, async (v) => {
211
125
 
212
126
  ```ts
213
127
  interface CommandItemData {
214
- /** Unique value for this item */
215
128
  value: string
216
- /** Display label (falls back to value) */
217
129
  label?: string
218
- /** Optional keywords for search matching */
219
130
  keywords?: string[]
220
- /** Optional shortcut display (e.g. "⌘S") */
221
131
  shortcut?: string
222
- /** Group this item belongs to */
223
132
  group?: string
224
- /** Disabled state */
225
133
  disabled?: boolean
226
- /** Custom render icon or prefix */
134
+ forceMount?: boolean
227
135
  icon?: Component | VNode | (() => VNode)
228
- /** Callback when item is selected */
229
136
  onSelect?: (item: CommandItemData) => void
230
137
  }
231
138
  ```
232
139
 
233
- ### `useCommandMenu()` Composable
140
+ ### `useCommandMenu()`
234
141
 
235
- Use the composable for programmatic control outside of `Command.Dialog` / `Command.Menu`.
142
+ The composable provides programmatic control outside of `Command.Dialog` / `Command.Menu`.
236
143
 
237
144
  ```ts
238
145
  import { useCommandMenu } from 'vue-command-kit'
239
- import type { UseCommandMenuReturn, FilterFn } from 'vue-command-kit'
240
146
 
241
- const menu: UseCommandMenuReturn = useCommandMenu(customFilter?)
147
+ const menu = useCommandMenu()
242
148
  menu.items.value = [...]
243
149
  menu.open()
244
150
  menu.close()
245
151
  menu.toggle()
246
152
  ```
247
153
 
248
- Returns:
249
-
250
- | Return | Type | Description |
251
- | ----------------- | ------------------------- | ------------------------------------------------ |
252
- | `visible` | `Ref<boolean>` | Open state |
253
- | `searchQuery` | `Ref<string>` | Current search query |
254
- | `activeIndex` | `Ref<number>` | Currently highlighted item index |
255
- | `items` | `Ref<CommandItemData[]>` | Raw item list |
256
- | `filteredItems` | `ComputedRef<...>` | Items after filtering |
257
- | `groupedItems` | `ComputedRef<...>` | Filtered items grouped by `group` field |
258
- | `open()` | `() => void` | Open the menu |
259
- | `close()` | `() => void` | Close and reset search |
260
- | `toggle()` | `() => void` | Toggle open state |
261
- | `selectNext()` | `() => void` | Move active index down |
262
- | `selectPrev()` | `() => void` | Move active index up |
263
- | `selectCurrent()` | `() => void` | Select currently active item |
264
- | `defaultFilter()` | `(items, query) => items` | Default filter implementation (case-insensitive) |
265
-
266
- ## 📦 Bundle Size
267
-
268
- | Format | Size |
269
- | ------- | ---------- |
270
- | ESM | 11.8 kB |
271
- | UMD | 11.8 kB |
272
- | Gzipped | **3.4 kB** |
273
-
274
- Zero runtime dependencies. Peer dependency only on `vue ^3.4.0`.
275
-
276
- ## 🤝 Acknowledgements
277
-
278
- vue-cmdk is inspired by [`cmdk`](https://github.com/pacocoursey/cmdk) by [@pacocoursey](https://github.com/pacocoursey) — a fast, unstyled command menu for React.
279
-
280
- ### Keyboard
281
-
282
- | Key | Action |
283
- | -------- | -------------------------------------- |
284
- | `↑` `↓` | Navigate items (wraps around) |
285
- | `Enter` | Select current item |
286
- | `Escape` | Close dialog |
287
- | `Tab` | Moves focus within dialog (focus trap) |
288
-
289
- ## 🤝 Contributing
290
-
291
- ### Prerequisites
292
-
293
- - **Node.js** >= 22.13
294
- - **pnpm** >= 11.x
295
-
296
- ### Setup
297
-
298
- ```bash
299
- # Clone the repo
300
- git clone https://github.com/yvng-jie/vue-cmdk.git
301
- cd vue-cmdk
302
-
303
- # Install dependencies
304
- pnpm install
305
-
306
- # Start demo dev server
307
- pnpm dev
308
- ```
309
-
310
- ### Scripts
311
-
312
- | Command | Description |
313
- | ----------------- | ----------------------------------------- |
314
- | `pnpm dev` | Start demo dev server at `localhost:5173` |
315
- | `pnpm build` | Build the library + type declarations |
316
- | `pnpm typecheck` | Run TypeScript type checking |
317
- | `pnpm preview` | Preview production build |
318
- | `pnpm build:demo` | Build demo site to `dist-demo/` |
319
-
320
- ### Project Structure
321
-
322
- ```
323
- src/
324
- ├── useCommandMenu.ts # core composable (state, filter, shortcuts)
325
- ├── useCommandRoot.ts # shared composable (provide/inject wiring)
326
- ├── types.ts # TypeScript type definitions
327
- ├── injectionKeys.ts # provide/inject keys
328
- ├── utils/
329
- │ └── injectStrict.ts # type-safe inject helper
330
- ├── CommandMenu.vue # inline command menu
331
- ├── CommandDialog.vue # modal dialog command palette
332
- ├── CommandInput.vue # search input
333
- ├── CommandList.vue # scrollable filtered list
334
- ├── CommandGroup.vue # group with heading
335
- ├── CommandItem.vue # single selectable item
336
- ├── CommandEmpty.vue # empty state
337
- ├── CommandSeparator.vue # visual separator
338
- ├── CommandLoading.vue # loading indicator
339
- ├── index.ts # barrel exports
340
- └── env.d.ts # type shims
341
- demo/
342
- ├── App.vue # demo application
343
- ├── main.ts # demo entry
344
- └── style.css # demo styles
345
- ```
346
-
347
- ### Pull Request Process
348
-
349
- 1. Fork the repo and create a feature branch from `main`
350
- 2. Make your changes and run `pnpm typecheck && pnpm build`
351
- 3. Test your changes with `pnpm dev` (demo app)
352
- 4. Update `CHANGELOG.md` with a description of your changes
353
- 5. Submit a PR with a clear description of what and why
354
-
355
- PRs and issues are welcome!
356
-
357
- ## 📄 License
358
-
359
- MIT © [yvng-jie](https://github.com/yvng-jie)
154
+ | Return | Type | Description |
155
+ | ----------------- | ------------------------ | --------------------------------------- |
156
+ | `visible` | `Ref<boolean>` | Open state |
157
+ | `searchQuery` | `Ref<string>` | Current search query |
158
+ | `activeIndex` | `Ref<number>` | Currently highlighted item index |
159
+ | `items` | `Ref<CommandItemData[]>` | Raw item list |
160
+ | `filteredItems` | `ComputedRef<...>` | Items after filtering |
161
+ | `groupedItems` | `ComputedRef<...>` | Filtered items grouped by `group` field |
162
+ | `open()` | `() => void` | Open the menu |
163
+ | `close()` | `() => void` | Close and reset search |
164
+ | `toggle()` | `() => void` | Toggle open state |
165
+ | `selectNext()` | `() => void` | Move active index down |
166
+ | `selectPrev()` | `() => void` | Move active index up |
167
+ | `selectCurrent()` | `() => void` | Select currently active item |
168
+
169
+ ## Bundle Size
170
+
171
+ | Format | Size |
172
+ | ------- | ------- |
173
+ | ESM | 17.0 kB |
174
+ | UMD | 13.5 kB |
175
+ | Gzipped | 5.0 kB |
176
+
177
+ ## License
178
+
179
+ MIT &copy; [yvng-jie](https://github.com/yvng-jie)
360
180
 
361
181
  ---
362
182
 
363
183
  <p align="center">
364
- <sub>Made with ❤️ for the Vue community. Thanks to <a href="https://github.com/xiaoluoboding">@xiaoluoboding</a> for the original <a href="https://github.com/xiaoluoboding/vue-command-palette">vue-command-palette</a>.</sub>
184
+ <a href="./CONTRIBUTING.md">Contributing Guide</a>
365
185
  </p>
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface CommandItemData {
15
15
  disabled?: boolean;
16
16
  /** Custom render icon or prefix */
17
17
  icon?: Component | VNode | (() => VNode);
18
+ /** Whether to force this item to always appear in filtered results */
19
+ forceMount?: boolean;
18
20
  /** Callback when item is selected */
19
21
  onSelect?: (item: CommandItemData) => void;
20
22
  }
@@ -54,6 +56,8 @@ export interface CommandRootEmits {
54
56
  export interface CommandDialogProps extends CommandRootProps {
55
57
  /** Command items to display */
56
58
  items?: CommandItemData[];
59
+ /** Teleport target for the dialog (defaults to 'body') */
60
+ container?: string | HTMLElement;
57
61
  }
58
62
  /** Group definition */
59
63
  export interface CommandGroupData {
@@ -0,0 +1,9 @@
1
+ export interface HighlightSegment {
2
+ text: string;
3
+ highlighted: boolean;
4
+ }
5
+ /**
6
+ * Split text into segments, marking parts that match the query.
7
+ * Matching is case-insensitive.
8
+ */
9
+ export declare function highlightText(text: string, query: string): HighlightSegment[];
@@ -0,0 +1,12 @@
1
+ /** Parsed shortcut descriptor */
2
+ export interface ShortcutDescriptor {
3
+ key: string;
4
+ metaKey?: boolean;
5
+ ctrlKey?: boolean;
6
+ altKey?: boolean;
7
+ shiftKey?: boolean;
8
+ }
9
+ /** Parse a shortcut string like "⌘S" or "⌘⇧F" into a key+modifiers descriptor */
10
+ export declare function parseShortcut(shortcut: string): ShortcutDescriptor | null;
11
+ /** Check if a KeyboardEvent matches a parsed shortcut descriptor */
12
+ export declare function eventMatchesShortcut(e: KeyboardEvent, desc: ShortcutDescriptor | null): boolean;