@ui-sdk/vue 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +10 -2
- package/dist/accordion.d.ts +561 -0
- package/dist/accordion.js +24 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +7 -0
- package/dist/alert-dialog.d.ts +231 -0
- package/dist/alert-dialog.js +33 -0
- package/dist/async-options.d.ts +18 -0
- package/dist/async-options.js +40 -0
- package/dist/autocomplete.d.ts +384 -0
- package/dist/autocomplete.js +344 -0
- package/dist/avatar.d.ts +40 -0
- package/dist/avatar.js +28 -0
- package/dist/button.d.ts +52 -0
- package/dist/button.js +43 -0
- package/dist/calendar-context.d.ts +10 -0
- package/dist/calendar-context.js +7 -0
- package/dist/calendar.d.ts +311 -0
- package/dist/calendar.js +349 -0
- package/dist/carousel.d.ts +236 -0
- package/dist/carousel.js +204 -0
- package/dist/checkbox.d.ts +211 -0
- package/dist/checkbox.js +151 -0
- package/dist/children.d.ts +41 -0
- package/dist/children.js +135 -0
- package/dist/collapsible.d.ts +54 -0
- package/dist/collapsible.js +18 -0
- package/dist/color-picker.d.ts +187 -0
- package/dist/color-picker.js +158 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +21 -0
- package/dist/combobox.d.ts +517 -0
- package/dist/combobox.js +531 -0
- package/dist/command.d.ts +322 -0
- package/dist/command.js +345 -0
- package/dist/controller-target.d.ts +49 -0
- package/dist/controller-target.js +112 -0
- package/dist/create-parts.d.ts +126 -0
- package/dist/create-parts.js +206 -0
- package/dist/date-input.d.ts +133 -0
- package/dist/date-input.js +31 -0
- package/dist/date-picker.d.ts +328 -0
- package/dist/date-picker.js +354 -0
- package/dist/date.d.ts +2 -0
- package/dist/date.js +59 -0
- package/dist/dialog-parts.d.ts +259 -0
- package/dist/dialog-parts.js +300 -0
- package/dist/dialog.d.ts +251 -0
- package/dist/dialog.js +25 -0
- package/dist/drawer.d.ts +304 -0
- package/dist/drawer.js +373 -0
- package/dist/editor-context.d.ts +4 -0
- package/dist/editor-context.js +5 -0
- package/dist/field-context.d.ts +26 -0
- package/dist/field-context.js +44 -0
- package/dist/field.d.ts +154 -0
- package/dist/field.js +240 -0
- package/dist/file-input.d.ts +212 -0
- package/dist/file-input.js +255 -0
- package/dist/form-inputs.d.ts +16 -0
- package/dist/form-inputs.js +25 -0
- package/dist/gesture.d.ts +1 -0
- package/dist/gesture.js +15 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +11 -0
- package/dist/hover-card.d.ts +137 -0
- package/dist/hover-card.js +153 -0
- package/dist/image-cropper.d.ts +74 -0
- package/dist/image-cropper.js +36 -0
- package/dist/image.d.ts +2 -0
- package/dist/image.js +5 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +60 -0
- package/dist/input.d.ts +120 -0
- package/dist/input.js +73 -0
- package/dist/item-icon.d.ts +34 -0
- package/dist/item-icon.js +63 -0
- package/dist/label.d.ts +16 -0
- package/dist/label.js +8 -0
- package/dist/lifecycle.d.ts +11 -0
- package/dist/lifecycle.js +91 -0
- package/dist/list-items.d.ts +38 -0
- package/dist/list-items.js +59 -0
- package/dist/list-text.d.ts +7 -0
- package/dist/list-text.js +15 -0
- package/dist/listbox.d.ts +273 -0
- package/dist/listbox.js +126 -0
- package/dist/menu.d.ts +678 -0
- package/dist/menu.js +607 -0
- package/dist/menubar-context.d.ts +5 -0
- package/dist/menubar-context.js +5 -0
- package/dist/menubar.d.ts +61 -0
- package/dist/menubar.js +48 -0
- package/dist/meter.d.ts +41 -0
- package/dist/meter.js +37 -0
- package/dist/navigation-menu.d.ts +228 -0
- package/dist/navigation-menu.js +195 -0
- package/dist/number-input.d.ts +176 -0
- package/dist/number-input.js +231 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +39 -0
- package/dist/nuxt/entries.json +977 -0
- package/dist/nuxt/module.d.ts +18 -0
- package/dist/nuxt/module.js +31 -0
- package/dist/nuxt/registrations.d.ts +22 -0
- package/dist/nuxt/registrations.js +22 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +7 -0
- package/dist/otp-input.d.ts +142 -0
- package/dist/otp-input.js +130 -0
- package/dist/overlays.d.ts +40 -0
- package/dist/overlays.js +118 -0
- package/dist/pagination.d.ts +201 -0
- package/dist/pagination.js +146 -0
- package/dist/password-input.d.ts +144 -0
- package/dist/password-input.js +134 -0
- package/dist/popover.d.ts +211 -0
- package/dist/popover.js +258 -0
- package/dist/popup.d.ts +60 -0
- package/dist/popup.js +89 -0
- package/dist/preferences.d.ts +44 -0
- package/dist/preferences.js +102 -0
- package/dist/primitive.d.ts +2 -0
- package/dist/primitive.js +11 -0
- package/dist/progress.d.ts +41 -0
- package/dist/progress.js +37 -0
- package/dist/radio.d.ts +164 -0
- package/dist/radio.js +145 -0
- package/dist/rating.d.ts +149 -0
- package/dist/rating.js +146 -0
- package/dist/renderless.d.ts +4 -0
- package/dist/renderless.js +28 -0
- package/dist/resizable.d.ts +54 -0
- package/dist/resizable.js +22 -0
- package/dist/scroll-area.d.ts +72 -0
- package/dist/scroll-area.js +77 -0
- package/dist/segmented-control.d.ts +151 -0
- package/dist/segmented-control.js +110 -0
- package/dist/segmented-input.d.ts +76 -0
- package/dist/segmented-input.js +115 -0
- package/dist/select.d.ts +458 -0
- package/dist/select.js +468 -0
- package/dist/separator.d.ts +14 -0
- package/dist/separator.js +8 -0
- package/dist/shortcuts.d.ts +32 -0
- package/dist/shortcuts.js +56 -0
- package/dist/slider.d.ts +65 -0
- package/dist/slider.js +194 -0
- package/dist/switch.d.ts +103 -0
- package/dist/switch.js +71 -0
- package/dist/tabs.d.ts +338 -0
- package/dist/tabs.js +27 -0
- package/dist/text-control.d.ts +89 -0
- package/dist/text-control.js +51 -0
- package/dist/text-match.d.ts +2 -0
- package/dist/text-match.js +7 -0
- package/dist/textarea.d.ts +106 -0
- package/dist/textarea.js +68 -0
- package/dist/time-input.d.ts +131 -0
- package/dist/time-input.js +32 -0
- package/dist/timeline.d.ts +81 -0
- package/dist/timeline.js +30 -0
- package/dist/toast-parts.d.ts +203 -0
- package/dist/toast-parts.js +270 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.js +21 -0
- package/dist/toggle-props.d.ts +22 -0
- package/dist/toggle-props.js +14 -0
- package/dist/toggle.d.ts +199 -0
- package/dist/toggle.js +129 -0
- package/dist/token-input.d.ts +136 -0
- package/dist/token-input.js +214 -0
- package/dist/toolbar-context.d.ts +7 -0
- package/dist/toolbar-context.js +20 -0
- package/dist/toolbar.d.ts +120 -0
- package/dist/toolbar.js +77 -0
- package/dist/tooltip.d.ts +160 -0
- package/dist/tooltip.js +224 -0
- package/dist/tree.d.ts +300 -0
- package/dist/tree.js +209 -0
- package/dist/trigger-menu.d.ts +452 -0
- package/dist/trigger-menu.js +254 -0
- package/dist/use-primitive.d.ts +132 -0
- package/dist/use-primitive.js +462 -0
- package/dist/waterfall.d.ts +201 -0
- package/dist/waterfall.js +171 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1369 -0
- package/docs/components/alert-dialog.md +799 -0
- package/docs/components/autocomplete.md +1857 -0
- package/docs/components/avatar.md +272 -0
- package/docs/components/button.md +435 -0
- package/docs/components/calendar.md +1589 -0
- package/docs/components/carousel.md +1171 -0
- package/docs/components/checkbox.md +1008 -0
- package/docs/components/collapsible.md +536 -0
- package/docs/components/color-picker.md +706 -0
- package/docs/components/combobox.md +3504 -0
- package/docs/components/command.md +1481 -0
- package/docs/components/date-input.md +536 -0
- package/docs/components/date-picker.md +1244 -0
- package/docs/components/dialog.md +1397 -0
- package/docs/components/drawer.md +1166 -0
- package/docs/components/field.md +538 -0
- package/docs/components/file-input.md +611 -0
- package/docs/components/hover-card.md +505 -0
- package/docs/components/image-cropper.md +556 -0
- package/docs/components/input.md +306 -0
- package/docs/components/label.md +203 -0
- package/docs/components/listbox.md +1004 -0
- package/docs/components/menu.md +1229 -0
- package/docs/components/menubar.md +443 -0
- package/docs/components/meter.md +315 -0
- package/docs/components/navigation-menu.md +1523 -0
- package/docs/components/number-input.md +1029 -0
- package/docs/components/otp-input.md +512 -0
- package/docs/components/pagination.md +613 -0
- package/docs/components/password-input.md +649 -0
- package/docs/components/popover.md +1448 -0
- package/docs/components/preferences.md +208 -0
- package/docs/components/progress.md +423 -0
- package/docs/components/radio.md +785 -0
- package/docs/components/rating.md +416 -0
- package/docs/components/resizable.md +910 -0
- package/docs/components/scroll-area.md +915 -0
- package/docs/components/segmented-control.md +435 -0
- package/docs/components/select.md +2015 -0
- package/docs/components/separator.md +229 -0
- package/docs/components/slider.md +1084 -0
- package/docs/components/switch.md +565 -0
- package/docs/components/tabs.md +944 -0
- package/docs/components/textarea.md +299 -0
- package/docs/components/time-input.md +224 -0
- package/docs/components/timeline.md +654 -0
- package/docs/components/toast.md +1224 -0
- package/docs/components/toggle.md +500 -0
- package/docs/components/token-input.md +953 -0
- package/docs/components/toolbar.md +739 -0
- package/docs/components/tooltip.md +818 -0
- package/docs/components/tree.md +1306 -0
- package/docs/components/trigger-menu.md +1223 -0
- package/docs/components/waterfall.md +717 -0
- package/docs/controllers.md +71 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +89 -0
- package/docs/first-component.md +83 -0
- package/docs/forms.md +122 -0
- package/docs/installation.md +72 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +122 -0
- package/docs/llms.txt +1502 -0
- package/docs/parts-and-composition.md +85 -0
- package/docs/popups.md +108 -0
- package/docs/rendering.md +56 -0
- package/docs/server-rendering.md +180 -0
- package/docs/state-and-events.md +100 -0
- package/docs/styling-and-animation.md +148 -0
- package/docs/utilities/async-options.md +348 -0
- package/docs/utilities/shortcuts.md +159 -0
- package/package.json +352 -3
|
@@ -0,0 +1,1481 @@
|
|
|
1
|
+
# Command
|
|
2
|
+
|
|
3
|
+
Search commands, run actions, and open pages of more choices.
|
|
4
|
+
|
|
5
|
+
Command puts a search field over a list of actions. The list stays open while someone searches. You can use it on its own, or put it in a dialog to build a command palette.
|
|
6
|
+
|
|
7
|
+
```vue title="demo.vue"
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import {
|
|
10
|
+
CalculatorIcon,
|
|
11
|
+
CalendarIcon,
|
|
12
|
+
CreditCardIcon,
|
|
13
|
+
SearchIcon,
|
|
14
|
+
SettingsIcon,
|
|
15
|
+
SmileIcon,
|
|
16
|
+
UserIcon,
|
|
17
|
+
} from "@lucide/vue"
|
|
18
|
+
import {
|
|
19
|
+
Command,
|
|
20
|
+
CommandEmpty,
|
|
21
|
+
CommandGroup,
|
|
22
|
+
CommandGroupLabel,
|
|
23
|
+
CommandInput,
|
|
24
|
+
CommandItem,
|
|
25
|
+
CommandItemIcon,
|
|
26
|
+
CommandList,
|
|
27
|
+
CommandSeparator,
|
|
28
|
+
CommandShortcut,
|
|
29
|
+
} from "@ui-sdk/vue/command"
|
|
30
|
+
|
|
31
|
+
const suggestions = [
|
|
32
|
+
{ value: "calendar", label: "Calendar", icon: CalendarIcon },
|
|
33
|
+
{ value: "emoji", label: "Search Emoji", icon: SmileIcon },
|
|
34
|
+
{
|
|
35
|
+
value: "calculator",
|
|
36
|
+
label: "Calculator",
|
|
37
|
+
icon: CalculatorIcon,
|
|
38
|
+
disabled: true,
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const settings = [
|
|
43
|
+
{ value: "profile", label: "Profile", icon: UserIcon, shortcut: "mod+p" },
|
|
44
|
+
{
|
|
45
|
+
value: "billing",
|
|
46
|
+
label: "Billing",
|
|
47
|
+
icon: CreditCardIcon,
|
|
48
|
+
shortcut: "mod+b",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "settings",
|
|
52
|
+
label: "Settings",
|
|
53
|
+
icon: SettingsIcon,
|
|
54
|
+
shortcut: "mod+s",
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<Command>
|
|
61
|
+
<div data-slot="row">
|
|
62
|
+
<SearchIcon aria-hidden="true" />
|
|
63
|
+
<CommandInput
|
|
64
|
+
aria-label="Search commands"
|
|
65
|
+
placeholder="Type a command or search…"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
69
|
+
<CommandList>
|
|
70
|
+
<CommandGroup>
|
|
71
|
+
<CommandGroupLabel>Suggestions</CommandGroupLabel>
|
|
72
|
+
<CommandItem
|
|
73
|
+
v-for="item in suggestions"
|
|
74
|
+
:key="item.value"
|
|
75
|
+
:value="item.value"
|
|
76
|
+
:disabled="item.disabled"
|
|
77
|
+
>
|
|
78
|
+
<CommandItemIcon>
|
|
79
|
+
<component :is="item.icon" />
|
|
80
|
+
</CommandItemIcon>
|
|
81
|
+
<span>{{ item.label }}</span>
|
|
82
|
+
</CommandItem>
|
|
83
|
+
</CommandGroup>
|
|
84
|
+
<CommandSeparator />
|
|
85
|
+
<CommandGroup>
|
|
86
|
+
<CommandGroupLabel>Settings</CommandGroupLabel>
|
|
87
|
+
<CommandItem
|
|
88
|
+
v-for="item in settings"
|
|
89
|
+
:key="item.value"
|
|
90
|
+
:value="item.value"
|
|
91
|
+
:shortcut="item.shortcut"
|
|
92
|
+
>
|
|
93
|
+
<CommandItemIcon>
|
|
94
|
+
<component :is="item.icon" />
|
|
95
|
+
</CommandItemIcon>
|
|
96
|
+
<span>{{ item.label }}</span>
|
|
97
|
+
<CommandShortcut />
|
|
98
|
+
</CommandItem>
|
|
99
|
+
</CommandGroup>
|
|
100
|
+
</CommandList>
|
|
101
|
+
</Command>
|
|
102
|
+
</template>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Type “bil” to find Billing. Arrow keys move the highlight; Escape clears the search.
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install @ui-sdk/vue @ui-sdk/core
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Composition
|
|
114
|
+
|
|
115
|
+
Use the following composition to build a `Command`:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Command
|
|
119
|
+
├── CommandInput
|
|
120
|
+
├── CommandEmpty
|
|
121
|
+
└── CommandList
|
|
122
|
+
├── CommandItem
|
|
123
|
+
└── CommandItem
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Give the input an accessible name and each command a unique value. Keep empty and status messages outside the list, which holds commands and groups. `@action` reports the value of the command that ran; use it to perform the action.
|
|
127
|
+
|
|
128
|
+
Use `CommandGroup` and `CommandGroupLabel` to name related commands, with a `CommandSeparator` between groups. Groups and separators disappear when filtering leaves them empty.
|
|
129
|
+
|
|
130
|
+
## Usage
|
|
131
|
+
|
|
132
|
+
Import the parts and put them together:
|
|
133
|
+
|
|
134
|
+
```vue
|
|
135
|
+
<script setup lang="ts">
|
|
136
|
+
import {
|
|
137
|
+
Command,
|
|
138
|
+
CommandEmpty,
|
|
139
|
+
CommandInput,
|
|
140
|
+
CommandItem,
|
|
141
|
+
CommandList,
|
|
142
|
+
} from "@ui-sdk/vue/command"
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<template>
|
|
146
|
+
<Command>
|
|
147
|
+
<CommandInput aria-label="Search commands" />
|
|
148
|
+
<CommandEmpty>No commands found.</CommandEmpty>
|
|
149
|
+
<CommandList>
|
|
150
|
+
<CommandItem value="new-file">New file</CommandItem>
|
|
151
|
+
<CommandItem value="new-folder">New folder</CommandItem>
|
|
152
|
+
</CommandList>
|
|
153
|
+
</Command>
|
|
154
|
+
</template>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Build Your Own
|
|
158
|
+
|
|
159
|
+
`@ui-sdk/vue/command` comes with a ready-made Command. If you'd like to build your own, with your own names, start from the `command` primitive in `@ui-sdk/core`:
|
|
160
|
+
|
|
161
|
+
```ts title="palette.ts"
|
|
162
|
+
import { command } from "@ui-sdk/core/command"
|
|
163
|
+
import { createParts } from "@ui-sdk/vue/primitive"
|
|
164
|
+
|
|
165
|
+
const {
|
|
166
|
+
Root: Palette,
|
|
167
|
+
Input: PaletteSearch,
|
|
168
|
+
Empty: PaletteEmpty,
|
|
169
|
+
List: PaletteList,
|
|
170
|
+
Item: PaletteAction,
|
|
171
|
+
} = createParts(command, {
|
|
172
|
+
input: "input",
|
|
173
|
+
empty: "div",
|
|
174
|
+
list: "div",
|
|
175
|
+
item: "div",
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
export { Palette, PaletteAction, PaletteEmpty, PaletteList, PaletteSearch }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```vue
|
|
182
|
+
<template>
|
|
183
|
+
<Palette>
|
|
184
|
+
<PaletteSearch aria-label="Search actions" />
|
|
185
|
+
<PaletteEmpty>No actions found.</PaletteEmpty>
|
|
186
|
+
<PaletteList>
|
|
187
|
+
<PaletteAction value="new-file">New file</PaletteAction>
|
|
188
|
+
<PaletteAction value="new-folder">New folder</PaletteAction>
|
|
189
|
+
</PaletteList>
|
|
190
|
+
</Palette>
|
|
191
|
+
</template>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Each part keeps the behavior, state and data attributes of its ready-made counterpart: typing filters the commands, arrow keys move the highlight and Enter runs it. See [Rendering](../rendering.md) and [Custom Parts](../custom-parts.md) for more.
|
|
195
|
+
|
|
196
|
+
## Dialog
|
|
197
|
+
|
|
198
|
+
Put Command inside a [Dialog](./dialog.md) for a command palette. Give the dialog a title, put `shortcut="mod+k"` on its trigger, and close it when a command runs. The dialog handles dismissal and returns focus to the trigger.
|
|
199
|
+
|
|
200
|
+
```vue title="dialog.vue"
|
|
201
|
+
<script setup lang="ts">
|
|
202
|
+
import {
|
|
203
|
+
CalculatorIcon,
|
|
204
|
+
CalendarIcon,
|
|
205
|
+
CreditCardIcon,
|
|
206
|
+
SearchIcon,
|
|
207
|
+
SettingsIcon,
|
|
208
|
+
SmileIcon,
|
|
209
|
+
UserIcon,
|
|
210
|
+
} from "@lucide/vue"
|
|
211
|
+
import {
|
|
212
|
+
Command,
|
|
213
|
+
CommandEmpty,
|
|
214
|
+
CommandGroup,
|
|
215
|
+
CommandGroupLabel,
|
|
216
|
+
CommandInput,
|
|
217
|
+
CommandItem,
|
|
218
|
+
CommandItemIcon,
|
|
219
|
+
CommandList,
|
|
220
|
+
} from "@ui-sdk/vue/command"
|
|
221
|
+
import {
|
|
222
|
+
Dialog,
|
|
223
|
+
DialogBackdrop,
|
|
224
|
+
DialogContent,
|
|
225
|
+
DialogShortcut,
|
|
226
|
+
DialogTitle,
|
|
227
|
+
DialogTrigger,
|
|
228
|
+
} from "@ui-sdk/vue/dialog"
|
|
229
|
+
import { ref } from "vue"
|
|
230
|
+
import { useLog } from "./log"
|
|
231
|
+
|
|
232
|
+
const commands = [
|
|
233
|
+
{ value: "calendar", label: "Calendar", icon: CalendarIcon },
|
|
234
|
+
{ value: "emoji", label: "Search Emoji", icon: SmileIcon },
|
|
235
|
+
{
|
|
236
|
+
value: "calculator",
|
|
237
|
+
label: "Calculator",
|
|
238
|
+
icon: CalculatorIcon,
|
|
239
|
+
disabled: true,
|
|
240
|
+
},
|
|
241
|
+
{ value: "profile", label: "Profile", icon: UserIcon },
|
|
242
|
+
{ value: "billing", label: "Billing", icon: CreditCardIcon },
|
|
243
|
+
{ value: "settings", label: "Settings", icon: SettingsIcon },
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
const open = ref(false)
|
|
247
|
+
const log = useLog()
|
|
248
|
+
|
|
249
|
+
function onAction(value: string) {
|
|
250
|
+
log(`Ran ${value}`)
|
|
251
|
+
open.value = false
|
|
252
|
+
}
|
|
253
|
+
</script>
|
|
254
|
+
|
|
255
|
+
<template>
|
|
256
|
+
<div data-slot="stack">
|
|
257
|
+
<Dialog v-model:open="open">
|
|
258
|
+
<DialogTrigger shortcut="mod+k">
|
|
259
|
+
Search…
|
|
260
|
+
<DialogShortcut />
|
|
261
|
+
</DialogTrigger>
|
|
262
|
+
<DialogBackdrop />
|
|
263
|
+
<DialogContent>
|
|
264
|
+
<DialogTitle>Command palette</DialogTitle>
|
|
265
|
+
<Command @action="onAction">
|
|
266
|
+
<div data-slot="row">
|
|
267
|
+
<SearchIcon aria-hidden="true" />
|
|
268
|
+
<CommandInput aria-label="Search commands" placeholder="Type a command or search…" />
|
|
269
|
+
</div>
|
|
270
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
271
|
+
<CommandList>
|
|
272
|
+
<CommandGroup>
|
|
273
|
+
<CommandGroupLabel>Suggestions</CommandGroupLabel>
|
|
274
|
+
<CommandItem
|
|
275
|
+
v-for="item in commands"
|
|
276
|
+
:key="item.value"
|
|
277
|
+
:value="item.value"
|
|
278
|
+
:disabled="item.disabled"
|
|
279
|
+
>
|
|
280
|
+
<CommandItemIcon>
|
|
281
|
+
<component :is="item.icon" />
|
|
282
|
+
</CommandItemIcon>
|
|
283
|
+
{{ item.label }}
|
|
284
|
+
</CommandItem>
|
|
285
|
+
</CommandGroup>
|
|
286
|
+
</CommandList>
|
|
287
|
+
</Command>
|
|
288
|
+
</DialogContent>
|
|
289
|
+
</Dialog>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Press Search or use Cmd+K / Ctrl+K. Choose a command to run it and close the palette; the console logs what ran.
|
|
295
|
+
|
|
296
|
+
## Shortcuts
|
|
297
|
+
|
|
298
|
+
An item’s `shortcut` can run it while the command list is mounted, even when the search or current page hides it. `CommandShortcut` displays the keys for the current platform, and the item gets `aria-keyshortcuts`. Shortcuts inside a dialog run while it is open.
|
|
299
|
+
|
|
300
|
+
```vue title="shortcuts.vue"
|
|
301
|
+
<script setup lang="ts">
|
|
302
|
+
import { CreditCardIcon, SettingsIcon, UserIcon } from "@lucide/vue"
|
|
303
|
+
import {
|
|
304
|
+
Command,
|
|
305
|
+
CommandGroup,
|
|
306
|
+
CommandGroupLabel,
|
|
307
|
+
CommandInput,
|
|
308
|
+
CommandItem,
|
|
309
|
+
CommandItemIcon,
|
|
310
|
+
CommandList,
|
|
311
|
+
CommandShortcut,
|
|
312
|
+
} from "@ui-sdk/vue/command"
|
|
313
|
+
import { useLog } from "./log"
|
|
314
|
+
|
|
315
|
+
const settings = [
|
|
316
|
+
{ value: "profile", label: "Profile", icon: UserIcon, shortcut: "mod+p" },
|
|
317
|
+
{
|
|
318
|
+
value: "billing",
|
|
319
|
+
label: "Billing",
|
|
320
|
+
icon: CreditCardIcon,
|
|
321
|
+
shortcut: "mod+b",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
value: "settings",
|
|
325
|
+
label: "Settings",
|
|
326
|
+
icon: SettingsIcon,
|
|
327
|
+
shortcut: "mod+s",
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
const log = useLog()
|
|
332
|
+
</script>
|
|
333
|
+
|
|
334
|
+
<template>
|
|
335
|
+
<div data-slot="stack">
|
|
336
|
+
<Command @action="(value: string) => log(`Ran ${value}`)">
|
|
337
|
+
<CommandInput aria-label="Search settings" placeholder="Search settings…" />
|
|
338
|
+
<CommandList>
|
|
339
|
+
<CommandGroup>
|
|
340
|
+
<CommandGroupLabel>Settings</CommandGroupLabel>
|
|
341
|
+
<CommandItem
|
|
342
|
+
v-for="item in settings"
|
|
343
|
+
:key="item.value"
|
|
344
|
+
:value="item.value"
|
|
345
|
+
:shortcut="item.shortcut"
|
|
346
|
+
>
|
|
347
|
+
<CommandItemIcon>
|
|
348
|
+
<component :is="item.icon" />
|
|
349
|
+
</CommandItemIcon>
|
|
350
|
+
<span>{{ item.label }}</span>
|
|
351
|
+
<CommandShortcut v-slot="{ state }">{{
|
|
352
|
+
state.text
|
|
353
|
+
}}</CommandShortcut>
|
|
354
|
+
</CommandItem>
|
|
355
|
+
</CommandGroup>
|
|
356
|
+
</CommandList>
|
|
357
|
+
</Command>
|
|
358
|
+
</div>
|
|
359
|
+
</template>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Press Cmd+P / Ctrl+P to run Profile, even when the search hides it. The console logs each command that runs.
|
|
363
|
+
|
|
364
|
+
## Highlight Matches
|
|
365
|
+
|
|
366
|
+
`CommandItemText` marks the matched text. The default fuzzy filter can match letters that are apart, so one command may have several marked segments.
|
|
367
|
+
|
|
368
|
+
```vue title="highlight.vue"
|
|
369
|
+
<script setup lang="ts">
|
|
370
|
+
import {
|
|
371
|
+
CalculatorIcon,
|
|
372
|
+
CalendarIcon,
|
|
373
|
+
CreditCardIcon,
|
|
374
|
+
SearchIcon,
|
|
375
|
+
SettingsIcon,
|
|
376
|
+
SmileIcon,
|
|
377
|
+
UserIcon,
|
|
378
|
+
} from "@lucide/vue"
|
|
379
|
+
import {
|
|
380
|
+
Command,
|
|
381
|
+
CommandEmpty,
|
|
382
|
+
CommandInput,
|
|
383
|
+
CommandItem,
|
|
384
|
+
CommandItemIcon,
|
|
385
|
+
CommandItemText,
|
|
386
|
+
CommandList,
|
|
387
|
+
} from "@ui-sdk/vue/command"
|
|
388
|
+
|
|
389
|
+
const commands = [
|
|
390
|
+
{ value: "calendar", label: "Calendar", icon: CalendarIcon },
|
|
391
|
+
{ value: "emoji", label: "Search Emoji", icon: SmileIcon },
|
|
392
|
+
{
|
|
393
|
+
value: "calculator",
|
|
394
|
+
label: "Calculator",
|
|
395
|
+
icon: CalculatorIcon,
|
|
396
|
+
disabled: true,
|
|
397
|
+
},
|
|
398
|
+
{ value: "profile", label: "Profile", icon: UserIcon },
|
|
399
|
+
{ value: "billing", label: "Billing", icon: CreditCardIcon },
|
|
400
|
+
{ value: "settings", label: "Settings", icon: SettingsIcon },
|
|
401
|
+
]
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<template>
|
|
405
|
+
<Command>
|
|
406
|
+
<div data-slot="row">
|
|
407
|
+
<SearchIcon aria-hidden="true" />
|
|
408
|
+
<CommandInput aria-label="Search commands" placeholder="Try “prf” or “emo”…" />
|
|
409
|
+
</div>
|
|
410
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
411
|
+
<CommandList>
|
|
412
|
+
<CommandItem v-for="item in commands" :key="item.value" :value="item.value" :disabled="item.disabled">
|
|
413
|
+
<CommandItemIcon>
|
|
414
|
+
<component :is="item.icon" />
|
|
415
|
+
</CommandItemIcon>
|
|
416
|
+
<CommandItemText>{{ item.label }}</CommandItemText>
|
|
417
|
+
</CommandItem>
|
|
418
|
+
</CommandList>
|
|
419
|
+
</Command>
|
|
420
|
+
</template>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Type “prf” to find Profile. Matching letters are marked even when they are not next to each other.
|
|
424
|
+
|
|
425
|
+
## Filter and Keywords
|
|
426
|
+
|
|
427
|
+
`"fuzzy"` is the default: `nf` matches New file. `"contains"` and `"startsWith"` match the text itself. A custom function returns a boolean or a score; zero hides a command and higher scores rank first.
|
|
428
|
+
|
|
429
|
+
Data commands are sorted by score. Commands written directly in markup keep their order, even though the best match can be highlighted.
|
|
430
|
+
|
|
431
|
+
`keywords` adds search terms without changing the command's label. Use it for synonyms or names someone might type instead.
|
|
432
|
+
|
|
433
|
+
```vue title="filter.vue"
|
|
434
|
+
<script setup lang="ts">
|
|
435
|
+
import { CheckIcon, ChevronDownIcon } from "@lucide/vue"
|
|
436
|
+
import {
|
|
437
|
+
Command,
|
|
438
|
+
CommandEmpty,
|
|
439
|
+
CommandInput,
|
|
440
|
+
CommandItem,
|
|
441
|
+
CommandList,
|
|
442
|
+
} from "@ui-sdk/vue/command"
|
|
443
|
+
import {
|
|
444
|
+
Select,
|
|
445
|
+
SelectContent,
|
|
446
|
+
SelectItem,
|
|
447
|
+
SelectItemIndicator,
|
|
448
|
+
SelectLabel,
|
|
449
|
+
SelectTrigger,
|
|
450
|
+
SelectValue,
|
|
451
|
+
} from "@ui-sdk/vue/select"
|
|
452
|
+
import { ref } from "vue"
|
|
453
|
+
|
|
454
|
+
const commands = [
|
|
455
|
+
{ label: "Home" },
|
|
456
|
+
{ label: "Inbox", keywords: ["mail", "messages"] },
|
|
457
|
+
{ label: "Documents" },
|
|
458
|
+
{ label: "Downloads" },
|
|
459
|
+
{ label: "New file" },
|
|
460
|
+
{ label: "New folder" },
|
|
461
|
+
{ label: "Copy" },
|
|
462
|
+
{ label: "Paste" },
|
|
463
|
+
{ label: "Delete", keywords: ["remove", "trash"] },
|
|
464
|
+
{ label: "Zoom in" },
|
|
465
|
+
{ label: "Zoom out" },
|
|
466
|
+
{ label: "Search emoji", keywords: ["smiley", "face"] },
|
|
467
|
+
{ label: "Keyboard shortcuts", keywords: ["hotkeys"] },
|
|
468
|
+
{ label: "Settings", keywords: ["preferences"] },
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
const modes = [
|
|
472
|
+
{ value: "fuzzy", label: "Fuzzy" },
|
|
473
|
+
{ value: "contains", label: "Contains" },
|
|
474
|
+
{ value: "startsWith", label: "Starts with" },
|
|
475
|
+
{ value: "words", label: "Words" },
|
|
476
|
+
]
|
|
477
|
+
type Mode = "fuzzy" | "contains" | "startsWith" | "words"
|
|
478
|
+
|
|
479
|
+
function words(item: { label: string }, query: string) {
|
|
480
|
+
return query
|
|
481
|
+
.toLowerCase()
|
|
482
|
+
.split(/\s+/)
|
|
483
|
+
.every((word) =>
|
|
484
|
+
item.label
|
|
485
|
+
.toLowerCase()
|
|
486
|
+
.split(" ")
|
|
487
|
+
.some((part) => part.startsWith(word))
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const mode = ref<Mode>("fuzzy")
|
|
492
|
+
</script>
|
|
493
|
+
|
|
494
|
+
<template>
|
|
495
|
+
<div data-slot="stack">
|
|
496
|
+
<div data-slot="controls">
|
|
497
|
+
<Select
|
|
498
|
+
:items="modes"
|
|
499
|
+
:value="mode"
|
|
500
|
+
@value-change="(next) => next && (mode = next as Mode)"
|
|
501
|
+
>
|
|
502
|
+
<SelectLabel>Filter</SelectLabel>
|
|
503
|
+
<SelectTrigger>
|
|
504
|
+
<SelectValue />
|
|
505
|
+
<ChevronDownIcon />
|
|
506
|
+
</SelectTrigger>
|
|
507
|
+
<SelectContent>
|
|
508
|
+
<SelectItem v-for="option in modes" :key="option.value" :value="option.value">
|
|
509
|
+
{{ option.label }}
|
|
510
|
+
<SelectItemIndicator>
|
|
511
|
+
<CheckIcon />
|
|
512
|
+
</SelectItemIndicator>
|
|
513
|
+
</SelectItem>
|
|
514
|
+
</SelectContent>
|
|
515
|
+
</Select>
|
|
516
|
+
</div>
|
|
517
|
+
<Command :filter="mode === 'words' ? words : mode">
|
|
518
|
+
<CommandInput aria-label="Search commands" placeholder="Try “nf”, “zo in” or “smiley”" />
|
|
519
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
520
|
+
<CommandList>
|
|
521
|
+
<CommandItem
|
|
522
|
+
v-for="command in commands"
|
|
523
|
+
:key="command.label"
|
|
524
|
+
:value="command.label"
|
|
525
|
+
:keywords="command.keywords"
|
|
526
|
+
>
|
|
527
|
+
{{ command.label }}
|
|
528
|
+
</CommandItem>
|
|
529
|
+
</CommandList>
|
|
530
|
+
</Command>
|
|
531
|
+
</div>
|
|
532
|
+
</template>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Type “nf” with Fuzzy to find New file. Choose Words and type “zo in” for Zoom in, or type “smiley” to find Search emoji through a keyword.
|
|
536
|
+
|
|
537
|
+
## Pages
|
|
538
|
+
|
|
539
|
+
Set an item’s `page` to the value of a `CommandPage` to open it. An unnamed page holds the first set of commands. The `pages` array records the stack of pages that have been opened.
|
|
540
|
+
|
|
541
|
+
Backspace in an empty search goes back and restores the previous page’s query. Escape clears the query first, then goes back. A Go back command can call `pop()` on a controller.
|
|
542
|
+
|
|
543
|
+
```vue title="pages.vue"
|
|
544
|
+
<script setup lang="ts">
|
|
545
|
+
import { ArrowLeftIcon, MoonIcon, UsersIcon } from "@lucide/vue"
|
|
546
|
+
import {
|
|
547
|
+
Command,
|
|
548
|
+
CommandEmpty,
|
|
549
|
+
CommandGroup,
|
|
550
|
+
CommandGroupLabel,
|
|
551
|
+
CommandInput,
|
|
552
|
+
CommandItem,
|
|
553
|
+
CommandItemIcon,
|
|
554
|
+
CommandList,
|
|
555
|
+
CommandPage,
|
|
556
|
+
createCommandController,
|
|
557
|
+
} from "@ui-sdk/vue/command"
|
|
558
|
+
import { ref } from "vue"
|
|
559
|
+
|
|
560
|
+
const themes = [
|
|
561
|
+
{ value: "light", label: "Light" },
|
|
562
|
+
{ value: "dark", label: "Dark" },
|
|
563
|
+
{ value: "system", label: "System" },
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
const teams = [
|
|
567
|
+
{ value: "design", label: "Design" },
|
|
568
|
+
{ value: "engineering", label: "Engineering" },
|
|
569
|
+
{ value: "marketing", label: "Marketing" },
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
const command = createCommandController()
|
|
573
|
+
const pages = ref<string[]>([])
|
|
574
|
+
|
|
575
|
+
function onAction(value: string) {
|
|
576
|
+
if (value === "back") {
|
|
577
|
+
command.pop()
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
</script>
|
|
581
|
+
|
|
582
|
+
<template>
|
|
583
|
+
<Command v-model:pages="pages" :controller="command" @action="onAction">
|
|
584
|
+
<div data-slot="row">
|
|
585
|
+
<span v-for="page in pages" :key="page" data-slot="badge">{{
|
|
586
|
+
page
|
|
587
|
+
}}</span>
|
|
588
|
+
</div>
|
|
589
|
+
<CommandInput
|
|
590
|
+
aria-label="Search commands"
|
|
591
|
+
:placeholder="
|
|
592
|
+
pages.length ? 'Search… (Backspace to go back)' : 'What do you need?'
|
|
593
|
+
"
|
|
594
|
+
/>
|
|
595
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
596
|
+
<CommandList>
|
|
597
|
+
<CommandPage>
|
|
598
|
+
<CommandGroup>
|
|
599
|
+
<CommandGroupLabel>Workspace</CommandGroupLabel>
|
|
600
|
+
<CommandItem value="theme" page="theme">
|
|
601
|
+
<CommandItemIcon>
|
|
602
|
+
<MoonIcon />
|
|
603
|
+
</CommandItemIcon>
|
|
604
|
+
Change theme…
|
|
605
|
+
</CommandItem>
|
|
606
|
+
<CommandItem value="teams" page="teams">
|
|
607
|
+
<CommandItemIcon>
|
|
608
|
+
<UsersIcon />
|
|
609
|
+
</CommandItemIcon>
|
|
610
|
+
Switch team…
|
|
611
|
+
</CommandItem>
|
|
612
|
+
</CommandGroup>
|
|
613
|
+
</CommandPage>
|
|
614
|
+
<CommandPage value="theme">
|
|
615
|
+
<CommandItem v-for="theme in themes" :key="theme.value" :value="theme.value">
|
|
616
|
+
{{ theme.label }}
|
|
617
|
+
</CommandItem>
|
|
618
|
+
</CommandPage>
|
|
619
|
+
<CommandPage value="teams">
|
|
620
|
+
<CommandItem v-for="team in teams" :key="team.value" :value="team.value">
|
|
621
|
+
{{ team.label }}
|
|
622
|
+
</CommandItem>
|
|
623
|
+
</CommandPage>
|
|
624
|
+
<CommandItem v-if="pages.length > 0" value="back">
|
|
625
|
+
<CommandItemIcon mirror>
|
|
626
|
+
<ArrowLeftIcon />
|
|
627
|
+
</CommandItemIcon>
|
|
628
|
+
Go back
|
|
629
|
+
</CommandItem>
|
|
630
|
+
</CommandList>
|
|
631
|
+
</Command>
|
|
632
|
+
</template>
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Choose Change theme or Switch team. Use Go back, or press Backspace in an empty search field, to return.
|
|
636
|
+
|
|
637
|
+
## Items as Data
|
|
638
|
+
|
|
639
|
+
Pass `items` to provide commands, groups, separators, and pages as `CommandData`. A data command with its own `items` opens a page. Its shortcut still works when another page or filter hides it.
|
|
640
|
+
|
|
641
|
+
The data can render bare parts for you. For styled commands, render from the list’s `state.items`, which follows the current page and ranked results. The example handles groups and separators as well as commands. [Lists and Menus](../lists-and-menus.md) covers the shared data format.
|
|
642
|
+
|
|
643
|
+
Pass `empty-items` alongside `items` to show suggested or recent commands while the first page's search is empty. Once someone types, the full collection is searched.
|
|
644
|
+
|
|
645
|
+
```vue
|
|
646
|
+
<Command
|
|
647
|
+
:items="commands"
|
|
648
|
+
:empty-items="[{ type: 'group', label: 'Suggested', items: recent }]"
|
|
649
|
+
/>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
```vue title="data.vue"
|
|
653
|
+
<script setup lang="ts">
|
|
654
|
+
import {
|
|
655
|
+
Command,
|
|
656
|
+
CommandEmpty,
|
|
657
|
+
CommandGroup,
|
|
658
|
+
CommandGroupLabel,
|
|
659
|
+
CommandInput,
|
|
660
|
+
CommandItem,
|
|
661
|
+
CommandList,
|
|
662
|
+
CommandSeparator,
|
|
663
|
+
CommandShortcut,
|
|
664
|
+
type CommandData,
|
|
665
|
+
type CommandDataItem,
|
|
666
|
+
} from "@ui-sdk/vue/command"
|
|
667
|
+
import { h, type FunctionalComponent } from "vue"
|
|
668
|
+
import { useLog } from "./log"
|
|
669
|
+
|
|
670
|
+
const commands: CommandData = [
|
|
671
|
+
{
|
|
672
|
+
type: "group",
|
|
673
|
+
label: "Navigation",
|
|
674
|
+
items: [
|
|
675
|
+
{ value: "home", label: "Go home", keywords: ["start"] },
|
|
676
|
+
{ value: "inbox", label: "Open inbox", shortcut: "mod+i" },
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
{ type: "separator" },
|
|
680
|
+
{
|
|
681
|
+
type: "group",
|
|
682
|
+
label: "Preferences",
|
|
683
|
+
items: [
|
|
684
|
+
{
|
|
685
|
+
value: "theme",
|
|
686
|
+
label: "Change theme…",
|
|
687
|
+
items: [
|
|
688
|
+
{ value: "light", label: "Light" },
|
|
689
|
+
{ value: "dark", label: "Dark" },
|
|
690
|
+
{ value: "system", label: "System" },
|
|
691
|
+
],
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
value: "language",
|
|
695
|
+
label: "Change language…",
|
|
696
|
+
items: ["English", "Français", "Deutsch", "日本語"],
|
|
697
|
+
},
|
|
698
|
+
{ value: "sign-out", label: "Sign out", shortcut: "mod+shift+q" },
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
]
|
|
702
|
+
|
|
703
|
+
const Commands: FunctionalComponent<{ items: CommandData }> = ({ items }) =>
|
|
704
|
+
items.map((entry, index) => {
|
|
705
|
+
if (typeof entry !== "string" && "type" in entry) {
|
|
706
|
+
if (entry.type === "separator") {
|
|
707
|
+
return h(CommandSeparator, { key: `separator-${index}` })
|
|
708
|
+
}
|
|
709
|
+
return h(CommandGroup, { key: entry.label }, () => [
|
|
710
|
+
h(CommandGroupLabel, () => entry.label),
|
|
711
|
+
h(Commands, { items: entry.items }),
|
|
712
|
+
])
|
|
713
|
+
}
|
|
714
|
+
const command: CommandDataItem =
|
|
715
|
+
typeof entry === "string" ? { value: entry, label: entry } : entry
|
|
716
|
+
return h(
|
|
717
|
+
CommandItem,
|
|
718
|
+
{
|
|
719
|
+
key: command.value,
|
|
720
|
+
value: command.value,
|
|
721
|
+
label: command.label,
|
|
722
|
+
disabled: command.disabled,
|
|
723
|
+
shortcut: command.shortcut,
|
|
724
|
+
},
|
|
725
|
+
() => [command.label, command.shortcut && h(CommandShortcut)]
|
|
726
|
+
)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
const log = useLog()
|
|
730
|
+
</script>
|
|
731
|
+
|
|
732
|
+
<template>
|
|
733
|
+
<div data-slot="stack">
|
|
734
|
+
<Command :items="commands" @action="(value: string) => log(`Ran ${value}`)">
|
|
735
|
+
<CommandInput aria-label="Search commands" placeholder="Type a command…" />
|
|
736
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
737
|
+
<CommandList v-slot="{ state }">
|
|
738
|
+
<Commands :items="state.items" />
|
|
739
|
+
</CommandList>
|
|
740
|
+
</Command>
|
|
741
|
+
</div>
|
|
742
|
+
</template>
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Choose Change theme to open its commands. Press Cmd+I / Ctrl+I to run Open inbox from any page; the console logs what ran.
|
|
746
|
+
|
|
747
|
+
## Async Results
|
|
748
|
+
|
|
749
|
+
Use [async options](../utilities/async-options.md) to wait for typing to pause and discard stale results. Send query changes to `setQuery()`, render the returned items, and pass `status`. Set `filter` to `false` to keep the server’s order.
|
|
750
|
+
|
|
751
|
+
`CommandStatus` describes loading and failures, and the list gets `aria-busy` while loading. `@load-more` can request another page when scrolling or highlighting reaches the last command. Without a status part, `announcement` controls the result-count message after typing pauses.
|
|
752
|
+
|
|
753
|
+
```vue title="async.vue"
|
|
754
|
+
<script setup lang="ts">
|
|
755
|
+
import {
|
|
756
|
+
Command,
|
|
757
|
+
CommandEmpty,
|
|
758
|
+
CommandInput,
|
|
759
|
+
CommandItem,
|
|
760
|
+
CommandList,
|
|
761
|
+
CommandStatus,
|
|
762
|
+
} from "@ui-sdk/vue/command"
|
|
763
|
+
import { useAsyncOptions } from "@ui-sdk/vue/async-options"
|
|
764
|
+
|
|
765
|
+
function searchIssues(query: string, { signal }: { signal: AbortSignal }) {
|
|
766
|
+
const issues = [
|
|
767
|
+
"Fix login redirect",
|
|
768
|
+
"Dark mode flicker",
|
|
769
|
+
"Add billing page",
|
|
770
|
+
"Search is slow",
|
|
771
|
+
"Update dependencies",
|
|
772
|
+
"Crash on upload",
|
|
773
|
+
"Export to CSV",
|
|
774
|
+
"Keyboard shortcuts help",
|
|
775
|
+
]
|
|
776
|
+
return new Promise<{ value: string; label: string }[]>((resolve, reject) => {
|
|
777
|
+
const timer = setTimeout(() => {
|
|
778
|
+
const needle = query.trim().toLowerCase()
|
|
779
|
+
resolve(
|
|
780
|
+
issues
|
|
781
|
+
.filter((issue) => issue.toLowerCase().includes(needle))
|
|
782
|
+
.map((issue) => ({
|
|
783
|
+
value: issue.toLowerCase().replaceAll(" ", "-"),
|
|
784
|
+
label: issue,
|
|
785
|
+
}))
|
|
786
|
+
)
|
|
787
|
+
}, 400)
|
|
788
|
+
signal.addEventListener("abort", () => {
|
|
789
|
+
clearTimeout(timer)
|
|
790
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
791
|
+
})
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const {
|
|
796
|
+
state: issues,
|
|
797
|
+
setQuery,
|
|
798
|
+
loadMore,
|
|
799
|
+
} = useAsyncOptions(searchIssues, { debounce: 250, immediate: true })
|
|
800
|
+
</script>
|
|
801
|
+
|
|
802
|
+
<template>
|
|
803
|
+
<Command
|
|
804
|
+
:filter="false"
|
|
805
|
+
:status="issues.status"
|
|
806
|
+
@query-change="(query: string) => setQuery(query)"
|
|
807
|
+
@load-more="loadMore"
|
|
808
|
+
>
|
|
809
|
+
<CommandInput aria-label="Search issues" placeholder="Search issues…" />
|
|
810
|
+
<CommandStatus v-slot="{ state }">
|
|
811
|
+
{{
|
|
812
|
+
state.loading
|
|
813
|
+
? "Searching…"
|
|
814
|
+
: `${state.count} ${state.count === 1 ? "issue" : "issues"}`
|
|
815
|
+
}}
|
|
816
|
+
</CommandStatus>
|
|
817
|
+
<CommandEmpty v-slot="{ state }">
|
|
818
|
+
<template v-if="state.empty && !state.loading"
|
|
819
|
+
>No issues match “{{ state.query }}”.</template
|
|
820
|
+
>
|
|
821
|
+
</CommandEmpty>
|
|
822
|
+
<CommandList>
|
|
823
|
+
<CommandItem v-for="issue in issues.items" :key="issue.value" :value="issue.value" :label="issue.label">
|
|
824
|
+
{{ issue.label }}
|
|
825
|
+
</CommandItem>
|
|
826
|
+
</CommandList>
|
|
827
|
+
</Command>
|
|
828
|
+
</template>
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
Type “login” and wait for matching issues. Replace the text to start a new search; only the latest results appear.
|
|
832
|
+
|
|
833
|
+
## Pending Actions
|
|
834
|
+
|
|
835
|
+
Return a promise from `@action` or an item’s `@select`. The command gets `data-pending`, `aria-busy` and `aria-disabled` until it settles; further presses do not start another run. A rejection becomes the command’s `error` state, which clears when it runs again.
|
|
836
|
+
|
|
837
|
+
An item’s `@select` runs before `@action`. Call `preventDefault()` on its event to stop the action.
|
|
838
|
+
|
|
839
|
+
```vue title="pending.vue"
|
|
840
|
+
<script setup lang="ts">
|
|
841
|
+
import { LoaderCircleIcon } from "@lucide/vue"
|
|
842
|
+
import {
|
|
843
|
+
Command,
|
|
844
|
+
CommandInput,
|
|
845
|
+
CommandItem,
|
|
846
|
+
CommandList,
|
|
847
|
+
} from "@ui-sdk/vue/command"
|
|
848
|
+
import { useLog } from "./log"
|
|
849
|
+
|
|
850
|
+
const wait = (ms: number) =>
|
|
851
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
852
|
+
|
|
853
|
+
const log = useLog()
|
|
854
|
+
|
|
855
|
+
async function deploy() {
|
|
856
|
+
await wait(1200)
|
|
857
|
+
log("Deployed to production")
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function sync() {
|
|
861
|
+
await wait(600)
|
|
862
|
+
log("Couldn't sync: offline")
|
|
863
|
+
throw new Error("Offline")
|
|
864
|
+
}
|
|
865
|
+
</script>
|
|
866
|
+
|
|
867
|
+
<template>
|
|
868
|
+
<div data-slot="stack">
|
|
869
|
+
<Command>
|
|
870
|
+
<CommandInput aria-label="Search actions" placeholder="Search actions…" />
|
|
871
|
+
<CommandList>
|
|
872
|
+
<CommandItem v-slot="{ state }" value="deploy" @select="deploy">
|
|
873
|
+
Deploy to production
|
|
874
|
+
<LoaderCircleIcon data-slot="spinner" aria-hidden="true" v-if="state.pending" />
|
|
875
|
+
</CommandItem>
|
|
876
|
+
<CommandItem v-slot="{ state }" value="sync" @select="sync">
|
|
877
|
+
Sync with server
|
|
878
|
+
<LoaderCircleIcon data-slot="spinner" aria-hidden="true" v-if="state.pending" />
|
|
879
|
+
<span v-else-if="state.error !== undefined" data-slot="hint"
|
|
880
|
+
>Failed, try again</span
|
|
881
|
+
>
|
|
882
|
+
</CommandItem>
|
|
883
|
+
</CommandList>
|
|
884
|
+
</Command>
|
|
885
|
+
</div>
|
|
886
|
+
</template>
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Run Deploy to production and try it again while it's pending. Sync with server fails and can be retried; the console logs each outcome.
|
|
890
|
+
|
|
891
|
+
## Virtualized
|
|
892
|
+
|
|
893
|
+
Pass the full `items` collection so filtering and keyboard navigation include commands that are not mounted. Render only visible rows, giving each its index in the current results. `@value-change` includes that index so you can scroll a newly highlighted row into view.
|
|
894
|
+
|
|
895
|
+
```vue title="virtualized.vue"
|
|
896
|
+
<script setup lang="ts">
|
|
897
|
+
import {
|
|
898
|
+
Command,
|
|
899
|
+
CommandInput,
|
|
900
|
+
CommandItem,
|
|
901
|
+
CommandList,
|
|
902
|
+
} from "@ui-sdk/vue/command"
|
|
903
|
+
import { ref } from "vue"
|
|
904
|
+
|
|
905
|
+
const commands = Array.from({ length: 10_000 }, (_, index) => ({
|
|
906
|
+
value: `command-${index + 1}`,
|
|
907
|
+
label: `Command ${index + 1}`,
|
|
908
|
+
}))
|
|
909
|
+
|
|
910
|
+
const row = 32
|
|
911
|
+
const height = 256
|
|
912
|
+
|
|
913
|
+
const scroller = ref<HTMLDivElement | null>(null)
|
|
914
|
+
const scrollTop = ref(0)
|
|
915
|
+
|
|
916
|
+
function onValueChange(_value: string | null, { index }: { index: number }) {
|
|
917
|
+
const el = scroller.value
|
|
918
|
+
if (!el || index < 0) {
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
const top = index * row
|
|
922
|
+
if (top < el.scrollTop) {
|
|
923
|
+
el.scrollTop = top
|
|
924
|
+
} else if (top + row > el.scrollTop + el.clientHeight) {
|
|
925
|
+
el.scrollTop = top + row - el.clientHeight
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function visible(items: typeof commands) {
|
|
930
|
+
const start = Math.max(0, Math.floor(scrollTop.value / row) - 4)
|
|
931
|
+
const end = Math.min(
|
|
932
|
+
items.length,
|
|
933
|
+
Math.ceil((scrollTop.value + height) / row) + 4
|
|
934
|
+
)
|
|
935
|
+
return items
|
|
936
|
+
.slice(start, end)
|
|
937
|
+
.map((item, offset) => ({ item, index: start + offset }))
|
|
938
|
+
}
|
|
939
|
+
</script>
|
|
940
|
+
|
|
941
|
+
<template>
|
|
942
|
+
<Command :items="commands" @value-change="onValueChange">
|
|
943
|
+
<CommandInput aria-label="Search 10,000 commands" placeholder="Search 10,000 commands…" />
|
|
944
|
+
<div ref="scroller" @scroll="scrollTop = ($event.currentTarget as HTMLDivElement).scrollTop">
|
|
945
|
+
<CommandList v-slot="{ state }">
|
|
946
|
+
<div :style="{ height: `${state.items.length * row}px` }">
|
|
947
|
+
<CommandItem
|
|
948
|
+
v-for="{ item, index } in visible(state.items as typeof commands)"
|
|
949
|
+
:key="item.value"
|
|
950
|
+
:value="item.value"
|
|
951
|
+
:label="item.label"
|
|
952
|
+
:index="index"
|
|
953
|
+
:style="{ top: `${index * row}px` }"
|
|
954
|
+
>
|
|
955
|
+
{{ item.label }}
|
|
956
|
+
</CommandItem>
|
|
957
|
+
</div>
|
|
958
|
+
</CommandList>
|
|
959
|
+
</div>
|
|
960
|
+
</Command>
|
|
961
|
+
</template>
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
Search 10,000 commands or move through them with the arrow keys. Only the rows near the viewport render.
|
|
965
|
+
|
|
966
|
+
## Controlled
|
|
967
|
+
|
|
968
|
+
The highlighted command, search text, and page stack are separate:
|
|
969
|
+
|
|
970
|
+
| Value | Shape | Change Notification |
|
|
971
|
+
| ------- | ----------------------------------- | ------------------- |
|
|
972
|
+
| `value` | A command’s string value or `null`. | `@value-change` |
|
|
973
|
+
| `query` | The search text. | `@query-change` |
|
|
974
|
+
| `pages` | An array of page values. | `@pages-change` |
|
|
975
|
+
|
|
976
|
+
Bind them with `v-model:value`, `v-model:query` and `v-model:pages`, or use `default-value`, `default-query`, and `default-pages` for starting state. Change events receive details; call `details.cancel()` to reject a change. `value` is the highlighted command, not a chosen one, and it changes as the highlight moves. `@action` reports activation, so moving the highlight alone does not run a command. [State and Events](../state-and-events.md) covers the shared binding pattern.
|
|
977
|
+
|
|
978
|
+
```vue title="controlled.vue"
|
|
979
|
+
<script setup lang="ts">
|
|
980
|
+
import {
|
|
981
|
+
Command,
|
|
982
|
+
CommandInput,
|
|
983
|
+
CommandItem,
|
|
984
|
+
CommandList,
|
|
985
|
+
CommandPage,
|
|
986
|
+
} from "@ui-sdk/vue/command"
|
|
987
|
+
import { ref } from "vue"
|
|
988
|
+
import { useLog } from "./log"
|
|
989
|
+
|
|
990
|
+
const themes = [
|
|
991
|
+
{ value: "light", label: "Light" },
|
|
992
|
+
{ value: "dark", label: "Dark" },
|
|
993
|
+
{ value: "system", label: "System" },
|
|
994
|
+
]
|
|
995
|
+
|
|
996
|
+
const value = ref<string | null>("dark")
|
|
997
|
+
const query = ref("")
|
|
998
|
+
const pages = ref(["theme"])
|
|
999
|
+
const log = useLog()
|
|
1000
|
+
</script>
|
|
1001
|
+
|
|
1002
|
+
<template>
|
|
1003
|
+
<Command
|
|
1004
|
+
v-model:value="value"
|
|
1005
|
+
v-model:query="query"
|
|
1006
|
+
v-model:pages="pages"
|
|
1007
|
+
@value-change="
|
|
1008
|
+
(next: string | null, details: { reason: string }) =>
|
|
1009
|
+
log(`Highlighted ${next ?? 'nothing'} (${details.reason})`)
|
|
1010
|
+
"
|
|
1011
|
+
@query-change="
|
|
1012
|
+
(next: string, details: { reason: string }) =>
|
|
1013
|
+
log(`Query “${next}” (${details.reason})`)
|
|
1014
|
+
"
|
|
1015
|
+
@pages-change="
|
|
1016
|
+
(next: string[], details: { reason: string }) =>
|
|
1017
|
+
log(`Pages [${next.join(', ')}] (${details.reason})`)
|
|
1018
|
+
"
|
|
1019
|
+
@action="(next: string) => log(`Ran ${next}`)"
|
|
1020
|
+
>
|
|
1021
|
+
<CommandInput aria-label="Search themes" placeholder="Search themes…" />
|
|
1022
|
+
<CommandList>
|
|
1023
|
+
<CommandPage>
|
|
1024
|
+
<CommandItem value="theme" page="theme">Change theme…</CommandItem>
|
|
1025
|
+
</CommandPage>
|
|
1026
|
+
<CommandPage value="theme">
|
|
1027
|
+
<CommandItem v-for="theme in themes" :key="theme.value" :value="theme.value">
|
|
1028
|
+
{{ theme.label }}
|
|
1029
|
+
</CommandItem>
|
|
1030
|
+
</CommandPage>
|
|
1031
|
+
</CommandList>
|
|
1032
|
+
</Command>
|
|
1033
|
+
</template>
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Open Change theme, search and move the highlight. The console logs each highlight, query and page change with its reason.
|
|
1037
|
+
|
|
1038
|
+
## Controller
|
|
1039
|
+
|
|
1040
|
+
Create a `createCommandController()` for one instance and pass it as `controller`. It can read and set the highlight, query, and page stack. Use `push()` and `pop()` to navigate pages, `run()` to run the highlighted command or a named one, and `focus()` to focus the search. See [Controllers](../controllers.md) for setup and lifecycle.
|
|
1041
|
+
|
|
1042
|
+
```vue title="controller.vue"
|
|
1043
|
+
<script setup lang="ts">
|
|
1044
|
+
import {
|
|
1045
|
+
Command,
|
|
1046
|
+
CommandInput,
|
|
1047
|
+
CommandItem,
|
|
1048
|
+
CommandList,
|
|
1049
|
+
CommandPage,
|
|
1050
|
+
createCommandController,
|
|
1051
|
+
} from "@ui-sdk/vue/command"
|
|
1052
|
+
import { useLog } from "./log"
|
|
1053
|
+
|
|
1054
|
+
const themes = [
|
|
1055
|
+
{ value: "light", label: "Light" },
|
|
1056
|
+
{ value: "dark", label: "Dark" },
|
|
1057
|
+
{ value: "system", label: "System" },
|
|
1058
|
+
]
|
|
1059
|
+
|
|
1060
|
+
const palette = createCommandController()
|
|
1061
|
+
const log = useLog()
|
|
1062
|
+
</script>
|
|
1063
|
+
|
|
1064
|
+
<template>
|
|
1065
|
+
<div data-slot="stack">
|
|
1066
|
+
<div data-slot="controls">
|
|
1067
|
+
<button type="button" @click="palette.push('theme')">Open Themes</button>
|
|
1068
|
+
<button type="button" @click="palette.pop()">Back</button>
|
|
1069
|
+
<button type="button" @click="palette.setValue('dark')">
|
|
1070
|
+
Highlight Dark
|
|
1071
|
+
</button>
|
|
1072
|
+
<button type="button" @click="palette.setQuery('li')">Search “li”</button>
|
|
1073
|
+
<button type="button" @click="palette.run()">Run Highlighted</button>
|
|
1074
|
+
<button type="button" @click="palette.focus()">Focus</button>
|
|
1075
|
+
</div>
|
|
1076
|
+
<Command
|
|
1077
|
+
:controller="palette"
|
|
1078
|
+
@action="(value: string) => log(`Ran ${value}`)"
|
|
1079
|
+
>
|
|
1080
|
+
<CommandInput aria-label="Search commands" />
|
|
1081
|
+
<CommandList>
|
|
1082
|
+
<CommandPage>
|
|
1083
|
+
<CommandItem value="theme" page="theme">Change theme…</CommandItem>
|
|
1084
|
+
</CommandPage>
|
|
1085
|
+
<CommandPage value="theme">
|
|
1086
|
+
<CommandItem v-for="theme in themes" :key="theme.value" :value="theme.value">
|
|
1087
|
+
{{ theme.label }}
|
|
1088
|
+
</CommandItem>
|
|
1089
|
+
</CommandPage>
|
|
1090
|
+
</CommandList>
|
|
1091
|
+
</Command>
|
|
1092
|
+
</div>
|
|
1093
|
+
</template>
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
Use the buttons to open pages, search, move the highlight and run it from outside. The console logs each command that runs.
|
|
1097
|
+
|
|
1098
|
+
## Custom Rendering
|
|
1099
|
+
|
|
1100
|
+
Render a command as an anchor when it should navigate. Enter follows the highlighted link; modified clicks keep their usual browser behavior.
|
|
1101
|
+
|
|
1102
|
+
```vue title="custom-rendering.vue"
|
|
1103
|
+
<script setup lang="ts">
|
|
1104
|
+
import { CornerDownLeftIcon } from "@lucide/vue"
|
|
1105
|
+
import {
|
|
1106
|
+
Command,
|
|
1107
|
+
CommandInput,
|
|
1108
|
+
CommandItem,
|
|
1109
|
+
CommandItemText,
|
|
1110
|
+
CommandList,
|
|
1111
|
+
} from "@ui-sdk/vue/command"
|
|
1112
|
+
|
|
1113
|
+
const pages = ["Composition", "Installation", "Styling"]
|
|
1114
|
+
</script>
|
|
1115
|
+
|
|
1116
|
+
<template>
|
|
1117
|
+
<Command>
|
|
1118
|
+
<CommandInput aria-label="Search docs" placeholder="Search docs…" />
|
|
1119
|
+
<CommandList>
|
|
1120
|
+
<CommandItem v-for="page in pages" :key="page" :value="page">
|
|
1121
|
+
<template #render="{ props, children }">
|
|
1122
|
+
<a v-bind="props" :href="`#${page.toLowerCase()}`"
|
|
1123
|
+
><component :is="children"
|
|
1124
|
+
/></a>
|
|
1125
|
+
</template>
|
|
1126
|
+
<template #default="{ state }">
|
|
1127
|
+
<CommandItemText v-slot="{ state: text }">
|
|
1128
|
+
<template v-for="(segment, index) in text.segments" :key="index">
|
|
1129
|
+
<strong v-if="segment.match">{{ segment.text }}</strong>
|
|
1130
|
+
<template v-else>{{ segment.text }}</template>
|
|
1131
|
+
</template>
|
|
1132
|
+
</CommandItemText>
|
|
1133
|
+
<CornerDownLeftIcon v-if="state.highlighted" aria-hidden="true" />
|
|
1134
|
+
</template>
|
|
1135
|
+
</CommandItem>
|
|
1136
|
+
</CommandList>
|
|
1137
|
+
</Command>
|
|
1138
|
+
</template>
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
Search for Installation and press Enter. The command follows the link to that section on this page.
|
|
1142
|
+
|
|
1143
|
+
## Styling
|
|
1144
|
+
|
|
1145
|
+
Each part has a `data-slot`, such as `command-item`, and shows its state through the attributes listed under [Data Attributes](#data-attributes).
|
|
1146
|
+
|
|
1147
|
+
Use `data-highlighted` for the active command and `data-pending` for an action in progress. Give the list a height limit and `overflow-y: auto` for long lists; keyboard navigation scrolls the highlighted command into view. When mixing commands with and without icons, `data-has-icons` on the list and `data-has-icon` on each item let you reserve matching space with logical padding.
|
|
1148
|
+
|
|
1149
|
+
## Data Attributes
|
|
1150
|
+
|
|
1151
|
+
### Command
|
|
1152
|
+
`[data-slot="command"]`
|
|
1153
|
+
|
|
1154
|
+
- `data-loading`: Present while results load, with `status="loading"`.
|
|
1155
|
+
|
|
1156
|
+
### CommandInput
|
|
1157
|
+
### CommandList
|
|
1158
|
+
`[data-slot="command-list"]`
|
|
1159
|
+
|
|
1160
|
+
- `data-has-icons`: Present while any command inside holds an icon part, so items without one can line up.
|
|
1161
|
+
|
|
1162
|
+
### CommandItem
|
|
1163
|
+
`[data-slot="command-item"]`
|
|
1164
|
+
|
|
1165
|
+
- `data-disabled`: Present when the command can't run. Arrow keys skip it.
|
|
1166
|
+
- `data-highlighted`: Present on the highlighted command, which Enter runs. Focus stays in the field.
|
|
1167
|
+
- `data-pending`: Present while the Promise the command's `onSelect` or `onAction` returned is running.
|
|
1168
|
+
- `data-has-icon`: Present on a command while it holds an icon part.
|
|
1169
|
+
|
|
1170
|
+
### CommandItemText
|
|
1171
|
+
`[data-slot="command-item-text"]`
|
|
1172
|
+
|
|
1173
|
+
- `data-matched`: Present on a command's text while the search matched part of it, which is marked.
|
|
1174
|
+
|
|
1175
|
+
### CommandItemIcon
|
|
1176
|
+
`[data-slot="command-item-icon"]`
|
|
1177
|
+
|
|
1178
|
+
- `data-mirror`: Present on an icon that flips in right-to-left text, with `mirror`.
|
|
1179
|
+
|
|
1180
|
+
### CommandEmpty
|
|
1181
|
+
`[data-slot="command-empty"]`
|
|
1182
|
+
|
|
1183
|
+
- `data-loading`: Present while results load, with `status="loading"`.
|
|
1184
|
+
- `data-empty`: Present while no command matches.
|
|
1185
|
+
|
|
1186
|
+
### CommandStatus
|
|
1187
|
+
`[data-slot="command-status"]`
|
|
1188
|
+
|
|
1189
|
+
- `data-loading`: Present while results load, with `status="loading"`.
|
|
1190
|
+
|
|
1191
|
+
## API Reference
|
|
1192
|
+
|
|
1193
|
+
Props are the same as below, and callbacks are events, except `onAction` and `onSelect`, which are function props.
|
|
1194
|
+
|
|
1195
|
+
### Command
|
|
1196
|
+
|
|
1197
|
+
- `value?` (`string | null`): The highlighted command's value. Pass it to control the highlight. `null` highlights nothing.
|
|
1198
|
+
- `defaultValue?` (`string | null`): The command highlighted at first, when uncontrolled. Without it, the first command is, with `autoHighlight`.
|
|
1199
|
+
- `@value-change` (`((value: string | null, details: CommandValueChangeDetails) => void)`): Runs when the highlight moves: arrow keys, the pointer, typing, a page change or a controller. `details.index` says where the command is, for virtualized lists.
|
|
1200
|
+
- `query?` (`string`): The search text, which filters the commands. Pass it to control it.
|
|
1201
|
+
- `defaultQuery?` (`string`): The search text at first, when uncontrolled.
|
|
1202
|
+
- `@query-change` (`((query: string, details: ChangeDetails<CommandQueryChangeReason>) => void)`): Runs when the search text wants to change: typing, opening a page, going back, or a controller.
|
|
1203
|
+
- `pages?` (`string[]`): The stack of open pages, from the first page opened to the current one. Pass it to control it.
|
|
1204
|
+
- `defaultPages?` (`string[]`): The pages open at first, when uncontrolled.
|
|
1205
|
+
- `@pages-change` (`((pages: string[], details: ChangeDetails<CommandPagesChangeReason>) => void)`): Runs when a page opens or Backspace or Escape goes back.
|
|
1206
|
+
- `@action` (`((value: string, details: CommandActionDetails) => unknown)`): Runs when a command runs, with its value: Enter, a click, or its shortcut. Return a Promise to mark the command pending until it settles: it gets `data-pending`, `aria-busy` and `aria-disabled`, and running it again waits. A rejection puts the reason in its `error` state until the next run.
|
|
1207
|
+
- `pendingAnnouncement?` (`string`): What screen readers hear once a command has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
|
|
1208
|
+
- `items?` (`CommandData`): The commands as data. Renders them for you, ranked as the user types, with pages for commands that have `items`.
|
|
1209
|
+
- `emptyItems?` (`CommandData`): Commands shown instead of `items` on the first page while the search is empty, like recent or suggested ones. Groups work, like `{ type: "group", label: "Suggestions", items }`. Needs `items`.
|
|
1210
|
+
- `filter?` (`SearchFilter<string>`): How typing narrows the list: `"fuzzy"` keeps commands with the text's letters in order, best matches first; `"contains"` and `"startsWith"` match the text itself. A function returns a score (0 hides) or a boolean. `false` keeps every command, for results a server already filtered. Composed commands keep their order, and data commands are ranked by score.
|
|
1211
|
+
- `autoHighlight?` (`boolean`): Whether the first command is highlighted while nothing else is, and the best match as the user types, so Enter runs it.
|
|
1212
|
+
- `loop?` (`boolean`): Whether arrow keys wrap from the last command to the first.
|
|
1213
|
+
- `status?` (`AsyncOptionsStatus`): Whether results are loading or failed, for the empty and status parts. Sets `aria-busy` on the list while loading.
|
|
1214
|
+
- `@load-more` (`(() => void)`): Runs when the highlight or scrolling reaches the last command, so the next page can load, like `createAsyncOptions`'s `loadMore`. Not again for the same commands, unless the last load failed.
|
|
1215
|
+
- `announcement?` (`((change: ListAnnouncement) => string | null | undefined)`): What screen readers hear after typing pauses, when no `CommandStatus` is rendered. Return null for nothing.
|
|
1216
|
+
- `controller?` (`object`): Reads and drives the list from anywhere. Create one with `createCommandController()`.
|
|
1217
|
+
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
|
|
1218
|
+
|
|
1219
|
+
### CommandInput
|
|
1220
|
+
|
|
1221
|
+
The search field. Renders an `input` with the combobox role.
|
|
1222
|
+
|
|
1223
|
+
### CommandStatus
|
|
1224
|
+
|
|
1225
|
+
Describes loading and failures. Renders a `div` with the status role.
|
|
1226
|
+
|
|
1227
|
+
### CommandEmpty
|
|
1228
|
+
|
|
1229
|
+
Shows while no command matches. Renders a `div`.
|
|
1230
|
+
|
|
1231
|
+
### CommandList
|
|
1232
|
+
|
|
1233
|
+
Holds the commands. Renders a `div` with the listbox role, always shown.
|
|
1234
|
+
|
|
1235
|
+
### CommandPage
|
|
1236
|
+
|
|
1237
|
+
- `value?` (`string`): The page's name, which a command's `page` opens. Leave it out for the first page.
|
|
1238
|
+
|
|
1239
|
+
### CommandGroup
|
|
1240
|
+
|
|
1241
|
+
Groups commands under a label. Renders a `div` with the group role.
|
|
1242
|
+
|
|
1243
|
+
### CommandGroupLabel
|
|
1244
|
+
|
|
1245
|
+
Names its group. Renders a `div`.
|
|
1246
|
+
|
|
1247
|
+
### CommandItem
|
|
1248
|
+
|
|
1249
|
+
- `value?` (`string`): Passed to `@action` and `@value-change`, and identifies the command. Without it, one is generated.
|
|
1250
|
+
- `label?` (`string`): The text the search matches, when the content isn't plain text. Not the accessible name: use `aria-label` for that.
|
|
1251
|
+
- `keywords?` (`string[]`): More words the search matches.
|
|
1252
|
+
- `disabled?` (`boolean`): Keeps the command from running. Arrow keys and the pointer skip it. Sets `data-disabled` and `aria-disabled`.
|
|
1253
|
+
- `shortcut?` (`string`): A keyboard shortcut that runs the command while it's in the page, like `"mod+p"`. Sets `aria-keyshortcuts`, and a `CommandShortcut` inside shows it.
|
|
1254
|
+
- `page?` (`string`): The page choosing this command opens, by the `value` of a `CommandPage`.
|
|
1255
|
+
- `index?` (`number`): The command's position among all visible commands, for virtualized lists that render only some. Sets `aria-posinset` and `aria-setsize`.
|
|
1256
|
+
- `@select` (`((event: Event) => unknown)`): Runs when the command runs, before `@action`, with the click or key press that ran it, or a new `select` Event for a shortcut or a controller's `run()`. Call `event.preventDefault()` to skip `@action` and the page. Return a Promise to mark the command pending until it settles, like `@action`.
|
|
1257
|
+
|
|
1258
|
+
### CommandItemIcon
|
|
1259
|
+
|
|
1260
|
+
- `mirror?` (`boolean`): Flips the icon horizontally in right-to-left text, for icons that point a way, like arrows and chevrons. Icons that picture a thing, like a clock or a check, stay as they are.
|
|
1261
|
+
|
|
1262
|
+
### CommandItemText
|
|
1263
|
+
|
|
1264
|
+
Shows its command's label, and marks what matched the search. Renders a `span`.
|
|
1265
|
+
|
|
1266
|
+
### CommandShortcut
|
|
1267
|
+
|
|
1268
|
+
- `shortcut?` (`string`): The keys to show, like `"mod+p"`. Inside a command, its `shortcut`.
|
|
1269
|
+
|
|
1270
|
+
### CommandSeparator
|
|
1271
|
+
|
|
1272
|
+
Separates commands or groups. Renders a `div` with the separator role.
|
|
1273
|
+
|
|
1274
|
+
### Items as Data
|
|
1275
|
+
|
|
1276
|
+
Each command in `items`. Groups are `{ type: "group", label, items }` and separators `{ type: "separator" }`.
|
|
1277
|
+
|
|
1278
|
+
- `value` (`string`): Identifies the item, and is what choosing it reports. A string, or any value with `itemToValue`.
|
|
1279
|
+
- `label` (`string`): The text shown and matched by typeahead and filtering. A select's trigger shows it once chosen.
|
|
1280
|
+
- `keywords?` (`string[]`): More words filtering matches, like `["team"]` for a plan called Pro.
|
|
1281
|
+
- `disabled?` (`boolean`): Keeps the item from being chosen. The keyboard skips it, like in a native select.
|
|
1282
|
+
- `shortcut?` (`string`): A keyboard shortcut that runs the command, like `"mod+p"`, while the list is in the page, whatever page it's on. Shows `⌘P` on Apple platforms and `Ctrl+P` elsewhere.
|
|
1283
|
+
- `items?` (`CommandData`): Commands on a page of their own. Choosing this command opens the page, and Backspace in the empty field goes back.
|
|
1284
|
+
|
|
1285
|
+
### State
|
|
1286
|
+
|
|
1287
|
+
What render functions, state-based class names and slots receive, per part.
|
|
1288
|
+
|
|
1289
|
+
`Command`:
|
|
1290
|
+
|
|
1291
|
+
- `page` (`string | null`): The current page, or `null` on the first page.
|
|
1292
|
+
- `loading` (`boolean`): Whether results are loading.
|
|
1293
|
+
|
|
1294
|
+
`CommandInput`:
|
|
1295
|
+
|
|
1296
|
+
- `page` (`string | null`): The current page, or `null` on the first page.
|
|
1297
|
+
|
|
1298
|
+
`CommandList`:
|
|
1299
|
+
|
|
1300
|
+
- `status` (`AsyncOptionsStatus`): Whether results are loading or failed.
|
|
1301
|
+
- `items` (`CommandData`): The visible data commands, ranked, for rendering a virtualized list yourself.
|
|
1302
|
+
|
|
1303
|
+
`CommandItem`:
|
|
1304
|
+
|
|
1305
|
+
- `highlighted` (`boolean`): Whether the command is highlighted, by the keyboard, the pointer or typing.
|
|
1306
|
+
- `disabled` (`boolean`): Whether the command can't run.
|
|
1307
|
+
- `pending` (`boolean`): Whether its action's Promise is running.
|
|
1308
|
+
- `error` (`unknown`): Why its last action failed, until it runs again.
|
|
1309
|
+
|
|
1310
|
+
`CommandEmpty`:
|
|
1311
|
+
|
|
1312
|
+
- `loading` (`boolean`): Whether results are loading.
|
|
1313
|
+
- `empty` (`boolean`): Whether no item matches.
|
|
1314
|
+
- `query` (`string`): The text the list is filtered by.
|
|
1315
|
+
|
|
1316
|
+
`CommandStatus`:
|
|
1317
|
+
|
|
1318
|
+
- `status` (`"idle" | "loading" | "error"`): Whether results are loading or failed.
|
|
1319
|
+
- `loading` (`boolean`): Whether results are loading.
|
|
1320
|
+
- `count` (`number`): How many items are visible.
|
|
1321
|
+
|
|
1322
|
+
`CommandShortcut`:
|
|
1323
|
+
|
|
1324
|
+
- `text` (`string`): The shortcut formatted for this platform: `⌘P` on Apple platforms and `Ctrl+P` elsewhere. Empty without one.
|
|
1325
|
+
|
|
1326
|
+
`CommandItemText`, which takes no props of its own:
|
|
1327
|
+
|
|
1328
|
+
- `segments` (`MatchSegment[]`): The text in pieces, each marked when it matched. Joined, they're the whole text.
|
|
1329
|
+
- `matched` (`boolean`): Whether any piece matched.
|
|
1330
|
+
|
|
1331
|
+
`CommandItemIcon`:
|
|
1332
|
+
|
|
1333
|
+
- `mirror` (`boolean`): Whether it flips in right-to-left text.
|
|
1334
|
+
|
|
1335
|
+
### Reasons
|
|
1336
|
+
|
|
1337
|
+
Why the highlighted command changed:
|
|
1338
|
+
|
|
1339
|
+
- `list-navigation` (`KeyboardEvent`): Arrow, Home or End keys moved the selection, typing picked an item, or Ctrl or ⌘ with A selected every item of a listbox.
|
|
1340
|
+
- `item-hover` (`PointerEvent`): The pointer moved onto an item, which highlighted it, like in a command list.
|
|
1341
|
+
- `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
|
|
1342
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
1343
|
+
- `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
|
|
1344
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
1345
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
1346
|
+
- `item-removed` (`none`): The selected item left the page, and another was picked.
|
|
1347
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1348
|
+
|
|
1349
|
+
Why the search text changed:
|
|
1350
|
+
|
|
1351
|
+
- `input-change` (`Event`): Typing, pasting or dropping changed a text field's value; in a combobox it can also open or close it, or Enter keeps the typed text as the value.
|
|
1352
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
1353
|
+
- `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
|
|
1354
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
1355
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
1356
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1357
|
+
|
|
1358
|
+
And why the stack of pages changed:
|
|
1359
|
+
|
|
1360
|
+
- `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
|
|
1361
|
+
- `shortcut` (`KeyboardEvent`): A keyboard shortcut, like a trigger's `shortcut`.
|
|
1362
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
1363
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
1364
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
1365
|
+
|
|
1366
|
+
### Controller
|
|
1367
|
+
|
|
1368
|
+
Create one with `createCommandController()` and pass it as `controller`.
|
|
1369
|
+
|
|
1370
|
+
- `getValue()` (`() => string | null`): The highlighted command's value, or `null`.
|
|
1371
|
+
- `setValue(value, options?)` (`(value: string | null, options?: CommandChangeOptions<CommandValueChangeReason> | undefined) => boolean`): Highlights a command, and scrolls it into view. Returns false when the change was canceled or nothing changed.
|
|
1372
|
+
- `getQuery()` (`() => string`): The search text.
|
|
1373
|
+
- `setQuery(query, options?)` (`(query: string, options?: CommandChangeOptions<CommandQueryChangeReason> | undefined) => boolean`): Changes the search text, which filters the commands. Returns false when the change was canceled or nothing changed.
|
|
1374
|
+
- `getPages()` (`() => string[]`): The stack of open pages, from the first page opened to the current one. Empty on the first page.
|
|
1375
|
+
- `setPages(pages, options?)` (`(pages: string[], options?: CommandChangeOptions<CommandPagesChangeReason> | undefined) => boolean`): Replaces the stack of pages. Returns false when the change was canceled or nothing changed.
|
|
1376
|
+
- `push(page, options?)` (`(page: string, options?: CommandChangeOptions<CommandPagesChangeReason> | undefined) => boolean`): Opens a page on top of the current one, by the `value` of a `CommandPage` or a data command. Returns false when the change was canceled or nothing changed.
|
|
1377
|
+
- `pop(options?)` (`(options?: CommandChangeOptions<CommandPagesChangeReason> | undefined) => boolean`): Goes back to the previous page, and puts back the search text typed there. Returns false when the change was canceled or nothing changed.
|
|
1378
|
+
- `run(value?)` (`(value?: string | undefined) => void`): Runs a command, as Enter would: the highlighted one, or the one with this value.
|
|
1379
|
+
- `focus()` (`() => void`): Moves focus to the search field.
|
|
1380
|
+
|
|
1381
|
+
### Events
|
|
1382
|
+
|
|
1383
|
+
Events get the new state first, then the details. `v-model` only updates when the change event wasn't canceled.
|
|
1384
|
+
|
|
1385
|
+
| Binding | On | Receives |
|
|
1386
|
+
| --------------- | ------------- | ----------------------------------------------------------------------------------------------------- |
|
|
1387
|
+
| `v-model:value` | `Command` | The highlighted command |
|
|
1388
|
+
| `v-model:query` | `Command` | The search text |
|
|
1389
|
+
| `v-model:pages` | `Command` | The stack of pages |
|
|
1390
|
+
| `@value-change` | `Command` | `(value, details)`: the highlighted command, the [reason](#reasons) and its `index` |
|
|
1391
|
+
| `@query-change` | `Command` | `(query, details)`: the new text, and the [reason](#reasons) |
|
|
1392
|
+
| `@pages-change` | `Command` | `(pages, details)`: the new stack, and the [reason](#reasons) |
|
|
1393
|
+
| `@action` | `Command` | `(value, { event })`, a function prop. Return a Promise to mark the command pending |
|
|
1394
|
+
| `@load-more` | `Command` | When the last command is reached |
|
|
1395
|
+
| `@select` | `CommandItem` | `(event)`, a function prop, before `@action`. `preventDefault()` skips it. A Promise marks it pending |
|
|
1396
|
+
|
|
1397
|
+
## Accessibility
|
|
1398
|
+
|
|
1399
|
+
The command list follows the [WAI-ARIA combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) with a listbox that's always shown. Focus stays in the input, which points to the highlighted command with `aria-activedescendant`. Every line below is an automated test.
|
|
1400
|
+
|
|
1401
|
+
### Keyboard
|
|
1402
|
+
|
|
1403
|
+
| Key | Does |
|
|
1404
|
+
| ------------------ | --------------------------------------------------------------------- |
|
|
1405
|
+
| Typing | Filters the commands and highlights the best match |
|
|
1406
|
+
| ArrowDown, ArrowUp | Moves the highlight. Wraps unless `:loop="false"` |
|
|
1407
|
+
| Enter | Runs the highlighted command, or follows it when it renders as a link |
|
|
1408
|
+
| Escape | Clears the search, then goes back a page |
|
|
1409
|
+
| Backspace | In an empty search, goes back a page and restores its query |
|
|
1410
|
+
| An item's shortcut | Runs it while the list is mounted, even when hidden by the search |
|
|
1411
|
+
|
|
1412
|
+
The first available command is highlighted by default. Set `:auto-highlight="false"` to wait for an arrow key or pointer movement.
|
|
1413
|
+
|
|
1414
|
+
### Tested Behavior
|
|
1415
|
+
|
|
1416
|
+
**Field**
|
|
1417
|
+
|
|
1418
|
+
- Is a combobox that controls an always-expanded listbox.
|
|
1419
|
+
- The first command is highlighted from the start.
|
|
1420
|
+
|
|
1421
|
+
**Filtering**
|
|
1422
|
+
|
|
1423
|
+
- Typing keeps the commands that match, ignoring case and accents.
|
|
1424
|
+
- Groups without a match hide, and the rest keep their labels.
|
|
1425
|
+
- The first match is highlighted as the text changes, so Enter runs it.
|
|
1426
|
+
- Deleting text brings back the commands the shorter text matches.
|
|
1427
|
+
- Commands without a value that mount while the field has text show when they match.
|
|
1428
|
+
- Going back a page reports the new highlight as back, not as a removal.
|
|
1429
|
+
- Nothing matching shows the empty message.
|
|
1430
|
+
|
|
1431
|
+
**Keyboard**
|
|
1432
|
+
|
|
1433
|
+
- Arrow keys move the highlight while focus stays in the field.
|
|
1434
|
+
- Home and End stay in the field for the caret.
|
|
1435
|
+
- The highlighted option has the id aria-activedescendant names.
|
|
1436
|
+
- Enter runs the highlighted command.
|
|
1437
|
+
|
|
1438
|
+
**Pointer**
|
|
1439
|
+
|
|
1440
|
+
- Clicking a command runs it.
|
|
1441
|
+
- A disabled command doesn't run.
|
|
1442
|
+
|
|
1443
|
+
**AutoHighlight**
|
|
1444
|
+
|
|
1445
|
+
- Without autoHighlight, nothing is highlighted until an arrow key.
|
|
1446
|
+
|
|
1447
|
+
Runs in React, Vue and web components.
|
|
1448
|
+
|
|
1449
|
+
Long lists stay fast and announce their size:
|
|
1450
|
+
|
|
1451
|
+
**Virtualized**
|
|
1452
|
+
|
|
1453
|
+
- Renders only the rows in view, each with its place in the whole list.
|
|
1454
|
+
- Arrow keys reach commands that weren't rendered, and the active descendant follows.
|
|
1455
|
+
- Typing ranks every command, rendered or not, best match first.
|
|
1456
|
+
|
|
1457
|
+
Runs in React.
|
|
1458
|
+
|
|
1459
|
+
Also tested:
|
|
1460
|
+
|
|
1461
|
+
**Enhancements**
|
|
1462
|
+
|
|
1463
|
+
- The highlighted command is the selected option, as the APG's list autocomplete marks it.
|
|
1464
|
+
- Keywords match as well as labels.
|
|
1465
|
+
- Fuzzy matching finds the letters in order, across words.
|
|
1466
|
+
- Arrow keys skip disabled commands.
|
|
1467
|
+
- A command's shortcut runs it and sets aria-keyshortcuts.
|
|
1468
|
+
- A shortcut runs its command while the search hides it.
|
|
1469
|
+
- A command is pending while its action's Promise runs, and runs only once.
|
|
1470
|
+
- A command opens its page, and Backspace in the empty field goes back with the text.
|
|
1471
|
+
- Escape clears the text, then goes back a page.
|
|
1472
|
+
- Results that still hold the highlighted command keep the highlight.
|
|
1473
|
+
- The list is busy while results load.
|
|
1474
|
+
|
|
1475
|
+
Runs in React, Vue and web components.
|
|
1476
|
+
|
|
1477
|
+
Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
|
|
1478
|
+
|
|
1479
|
+
- Announcements while searching with a keyboard.
|
|
1480
|
+
|
|
1481
|
+
Runs in React, Vue and web components.
|