@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.
Files changed (262) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +561 -0
  4. package/dist/accordion.js +24 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +7 -0
  7. package/dist/alert-dialog.d.ts +231 -0
  8. package/dist/alert-dialog.js +33 -0
  9. package/dist/async-options.d.ts +18 -0
  10. package/dist/async-options.js +40 -0
  11. package/dist/autocomplete.d.ts +384 -0
  12. package/dist/autocomplete.js +344 -0
  13. package/dist/avatar.d.ts +40 -0
  14. package/dist/avatar.js +28 -0
  15. package/dist/button.d.ts +52 -0
  16. package/dist/button.js +43 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +7 -0
  19. package/dist/calendar.d.ts +311 -0
  20. package/dist/calendar.js +349 -0
  21. package/dist/carousel.d.ts +236 -0
  22. package/dist/carousel.js +204 -0
  23. package/dist/checkbox.d.ts +211 -0
  24. package/dist/checkbox.js +151 -0
  25. package/dist/children.d.ts +41 -0
  26. package/dist/children.js +135 -0
  27. package/dist/collapsible.d.ts +54 -0
  28. package/dist/collapsible.js +18 -0
  29. package/dist/color-picker.d.ts +187 -0
  30. package/dist/color-picker.js +158 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +21 -0
  33. package/dist/combobox.d.ts +517 -0
  34. package/dist/combobox.js +531 -0
  35. package/dist/command.d.ts +322 -0
  36. package/dist/command.js +345 -0
  37. package/dist/controller-target.d.ts +49 -0
  38. package/dist/controller-target.js +112 -0
  39. package/dist/create-parts.d.ts +126 -0
  40. package/dist/create-parts.js +206 -0
  41. package/dist/date-input.d.ts +133 -0
  42. package/dist/date-input.js +31 -0
  43. package/dist/date-picker.d.ts +328 -0
  44. package/dist/date-picker.js +354 -0
  45. package/dist/date.d.ts +2 -0
  46. package/dist/date.js +59 -0
  47. package/dist/dialog-parts.d.ts +259 -0
  48. package/dist/dialog-parts.js +300 -0
  49. package/dist/dialog.d.ts +251 -0
  50. package/dist/dialog.js +25 -0
  51. package/dist/drawer.d.ts +304 -0
  52. package/dist/drawer.js +373 -0
  53. package/dist/editor-context.d.ts +4 -0
  54. package/dist/editor-context.js +5 -0
  55. package/dist/field-context.d.ts +26 -0
  56. package/dist/field-context.js +44 -0
  57. package/dist/field.d.ts +154 -0
  58. package/dist/field.js +240 -0
  59. package/dist/file-input.d.ts +212 -0
  60. package/dist/file-input.js +255 -0
  61. package/dist/form-inputs.d.ts +16 -0
  62. package/dist/form-inputs.js +25 -0
  63. package/dist/gesture.d.ts +1 -0
  64. package/dist/gesture.js +15 -0
  65. package/dist/history.d.ts +2 -0
  66. package/dist/history.js +11 -0
  67. package/dist/hover-card.d.ts +137 -0
  68. package/dist/hover-card.js +153 -0
  69. package/dist/image-cropper.d.ts +74 -0
  70. package/dist/image-cropper.js +36 -0
  71. package/dist/image.d.ts +2 -0
  72. package/dist/image.js +5 -0
  73. package/dist/index.d.ts +59 -0
  74. package/dist/index.js +60 -0
  75. package/dist/input.d.ts +120 -0
  76. package/dist/input.js +73 -0
  77. package/dist/item-icon.d.ts +34 -0
  78. package/dist/item-icon.js +63 -0
  79. package/dist/label.d.ts +16 -0
  80. package/dist/label.js +8 -0
  81. package/dist/lifecycle.d.ts +11 -0
  82. package/dist/lifecycle.js +91 -0
  83. package/dist/list-items.d.ts +38 -0
  84. package/dist/list-items.js +59 -0
  85. package/dist/list-text.d.ts +7 -0
  86. package/dist/list-text.js +15 -0
  87. package/dist/listbox.d.ts +273 -0
  88. package/dist/listbox.js +126 -0
  89. package/dist/menu.d.ts +678 -0
  90. package/dist/menu.js +607 -0
  91. package/dist/menubar-context.d.ts +5 -0
  92. package/dist/menubar-context.js +5 -0
  93. package/dist/menubar.d.ts +61 -0
  94. package/dist/menubar.js +48 -0
  95. package/dist/meter.d.ts +41 -0
  96. package/dist/meter.js +37 -0
  97. package/dist/navigation-menu.d.ts +228 -0
  98. package/dist/navigation-menu.js +195 -0
  99. package/dist/number-input.d.ts +176 -0
  100. package/dist/number-input.js +231 -0
  101. package/dist/number.d.ts +2 -0
  102. package/dist/number.js +39 -0
  103. package/dist/nuxt/entries.json +977 -0
  104. package/dist/nuxt/module.d.ts +18 -0
  105. package/dist/nuxt/module.js +31 -0
  106. package/dist/nuxt/registrations.d.ts +22 -0
  107. package/dist/nuxt/registrations.js +22 -0
  108. package/dist/observe.d.ts +2 -0
  109. package/dist/observe.js +7 -0
  110. package/dist/otp-input.d.ts +142 -0
  111. package/dist/otp-input.js +130 -0
  112. package/dist/overlays.d.ts +40 -0
  113. package/dist/overlays.js +118 -0
  114. package/dist/pagination.d.ts +201 -0
  115. package/dist/pagination.js +146 -0
  116. package/dist/password-input.d.ts +144 -0
  117. package/dist/password-input.js +134 -0
  118. package/dist/popover.d.ts +211 -0
  119. package/dist/popover.js +258 -0
  120. package/dist/popup.d.ts +60 -0
  121. package/dist/popup.js +89 -0
  122. package/dist/preferences.d.ts +44 -0
  123. package/dist/preferences.js +102 -0
  124. package/dist/primitive.d.ts +2 -0
  125. package/dist/primitive.js +11 -0
  126. package/dist/progress.d.ts +41 -0
  127. package/dist/progress.js +37 -0
  128. package/dist/radio.d.ts +164 -0
  129. package/dist/radio.js +145 -0
  130. package/dist/rating.d.ts +149 -0
  131. package/dist/rating.js +146 -0
  132. package/dist/renderless.d.ts +4 -0
  133. package/dist/renderless.js +28 -0
  134. package/dist/resizable.d.ts +54 -0
  135. package/dist/resizable.js +22 -0
  136. package/dist/scroll-area.d.ts +72 -0
  137. package/dist/scroll-area.js +77 -0
  138. package/dist/segmented-control.d.ts +151 -0
  139. package/dist/segmented-control.js +110 -0
  140. package/dist/segmented-input.d.ts +76 -0
  141. package/dist/segmented-input.js +115 -0
  142. package/dist/select.d.ts +458 -0
  143. package/dist/select.js +468 -0
  144. package/dist/separator.d.ts +14 -0
  145. package/dist/separator.js +8 -0
  146. package/dist/shortcuts.d.ts +32 -0
  147. package/dist/shortcuts.js +56 -0
  148. package/dist/slider.d.ts +65 -0
  149. package/dist/slider.js +194 -0
  150. package/dist/switch.d.ts +103 -0
  151. package/dist/switch.js +71 -0
  152. package/dist/tabs.d.ts +338 -0
  153. package/dist/tabs.js +27 -0
  154. package/dist/text-control.d.ts +89 -0
  155. package/dist/text-control.js +51 -0
  156. package/dist/text-match.d.ts +2 -0
  157. package/dist/text-match.js +7 -0
  158. package/dist/textarea.d.ts +106 -0
  159. package/dist/textarea.js +68 -0
  160. package/dist/time-input.d.ts +131 -0
  161. package/dist/time-input.js +32 -0
  162. package/dist/timeline.d.ts +81 -0
  163. package/dist/timeline.js +30 -0
  164. package/dist/toast-parts.d.ts +203 -0
  165. package/dist/toast-parts.js +270 -0
  166. package/dist/toast.d.ts +2 -0
  167. package/dist/toast.js +21 -0
  168. package/dist/toggle-props.d.ts +22 -0
  169. package/dist/toggle-props.js +14 -0
  170. package/dist/toggle.d.ts +199 -0
  171. package/dist/toggle.js +129 -0
  172. package/dist/token-input.d.ts +136 -0
  173. package/dist/token-input.js +214 -0
  174. package/dist/toolbar-context.d.ts +7 -0
  175. package/dist/toolbar-context.js +20 -0
  176. package/dist/toolbar.d.ts +120 -0
  177. package/dist/toolbar.js +77 -0
  178. package/dist/tooltip.d.ts +160 -0
  179. package/dist/tooltip.js +224 -0
  180. package/dist/tree.d.ts +300 -0
  181. package/dist/tree.js +209 -0
  182. package/dist/trigger-menu.d.ts +452 -0
  183. package/dist/trigger-menu.js +254 -0
  184. package/dist/use-primitive.d.ts +132 -0
  185. package/dist/use-primitive.js +462 -0
  186. package/dist/waterfall.d.ts +201 -0
  187. package/dist/waterfall.js +171 -0
  188. package/docs/README.md +89 -0
  189. package/docs/agents.md +1523 -0
  190. package/docs/components/accordion.md +1369 -0
  191. package/docs/components/alert-dialog.md +799 -0
  192. package/docs/components/autocomplete.md +1857 -0
  193. package/docs/components/avatar.md +272 -0
  194. package/docs/components/button.md +435 -0
  195. package/docs/components/calendar.md +1589 -0
  196. package/docs/components/carousel.md +1171 -0
  197. package/docs/components/checkbox.md +1008 -0
  198. package/docs/components/collapsible.md +536 -0
  199. package/docs/components/color-picker.md +706 -0
  200. package/docs/components/combobox.md +3504 -0
  201. package/docs/components/command.md +1481 -0
  202. package/docs/components/date-input.md +536 -0
  203. package/docs/components/date-picker.md +1244 -0
  204. package/docs/components/dialog.md +1397 -0
  205. package/docs/components/drawer.md +1166 -0
  206. package/docs/components/field.md +538 -0
  207. package/docs/components/file-input.md +611 -0
  208. package/docs/components/hover-card.md +505 -0
  209. package/docs/components/image-cropper.md +556 -0
  210. package/docs/components/input.md +306 -0
  211. package/docs/components/label.md +203 -0
  212. package/docs/components/listbox.md +1004 -0
  213. package/docs/components/menu.md +1229 -0
  214. package/docs/components/menubar.md +443 -0
  215. package/docs/components/meter.md +315 -0
  216. package/docs/components/navigation-menu.md +1523 -0
  217. package/docs/components/number-input.md +1029 -0
  218. package/docs/components/otp-input.md +512 -0
  219. package/docs/components/pagination.md +613 -0
  220. package/docs/components/password-input.md +649 -0
  221. package/docs/components/popover.md +1448 -0
  222. package/docs/components/preferences.md +208 -0
  223. package/docs/components/progress.md +423 -0
  224. package/docs/components/radio.md +785 -0
  225. package/docs/components/rating.md +416 -0
  226. package/docs/components/resizable.md +910 -0
  227. package/docs/components/scroll-area.md +915 -0
  228. package/docs/components/segmented-control.md +435 -0
  229. package/docs/components/select.md +2015 -0
  230. package/docs/components/separator.md +229 -0
  231. package/docs/components/slider.md +1084 -0
  232. package/docs/components/switch.md +565 -0
  233. package/docs/components/tabs.md +944 -0
  234. package/docs/components/textarea.md +299 -0
  235. package/docs/components/time-input.md +224 -0
  236. package/docs/components/timeline.md +654 -0
  237. package/docs/components/toast.md +1224 -0
  238. package/docs/components/toggle.md +500 -0
  239. package/docs/components/token-input.md +953 -0
  240. package/docs/components/toolbar.md +739 -0
  241. package/docs/components/tooltip.md +818 -0
  242. package/docs/components/tree.md +1306 -0
  243. package/docs/components/trigger-menu.md +1223 -0
  244. package/docs/components/waterfall.md +717 -0
  245. package/docs/controllers.md +71 -0
  246. package/docs/core.md +8027 -0
  247. package/docs/custom-parts.md +89 -0
  248. package/docs/first-component.md +83 -0
  249. package/docs/forms.md +122 -0
  250. package/docs/installation.md +72 -0
  251. package/docs/introduction.md +32 -0
  252. package/docs/lists-and-menus.md +122 -0
  253. package/docs/llms.txt +1502 -0
  254. package/docs/parts-and-composition.md +85 -0
  255. package/docs/popups.md +108 -0
  256. package/docs/rendering.md +56 -0
  257. package/docs/server-rendering.md +180 -0
  258. package/docs/state-and-events.md +100 -0
  259. package/docs/styling-and-animation.md +148 -0
  260. package/docs/utilities/async-options.md +348 -0
  261. package/docs/utilities/shortcuts.md +159 -0
  262. package/package.json +352 -3
@@ -0,0 +1,348 @@
1
+ # Async Options
2
+
3
+ Load search results, cancel stale requests, and fetch the next page.
4
+
5
+ `useAsyncOptions` loads items as someone types. It waits for typing to pause, cancels requests that are replaced, and keeps the previous results visible while loading.
6
+
7
+ ```vue title="async.vue"
8
+ <script setup lang="ts">
9
+ import { CheckIcon, ChevronDownIcon } from "@lucide/vue"
10
+ import {
11
+ Combobox,
12
+ ComboboxContent,
13
+ ComboboxEmpty,
14
+ ComboboxInput,
15
+ ComboboxItem,
16
+ ComboboxItemIndicator,
17
+ ComboboxLabel,
18
+ ComboboxList,
19
+ ComboboxStatus,
20
+ ComboboxTrigger,
21
+ } from "@ui-sdk/vue/combobox"
22
+ import { useAsyncOptions } from "@ui-sdk/vue/async-options"
23
+
24
+ const movieTitles = [
25
+ "The Shawshank Redemption",
26
+ "The Godfather",
27
+ "The Dark Knight",
28
+ "Pulp Fiction",
29
+ "Schindler's List",
30
+ "The Lord of the Rings",
31
+ "Fight Club",
32
+ "Forrest Gump",
33
+ "Inception",
34
+ "The Matrix",
35
+ "Goodfellas",
36
+ "Interstellar",
37
+ "Parasite",
38
+ "Spirited Away",
39
+ "Whiplash",
40
+ "The Prestige",
41
+ "The Departed",
42
+ "Gladiator",
43
+ "Alien",
44
+ "Casablanca",
45
+ "Amélie",
46
+ "Arrival",
47
+ ]
48
+
49
+ function searchMovies(query: string, { signal }: { signal: AbortSignal }) {
50
+ return new Promise<{ value: string; label: string }[]>((resolve, reject) => {
51
+ const timer = setTimeout(() => {
52
+ const needle = query.trim().toLowerCase()
53
+ resolve(
54
+ movieTitles
55
+ .filter((title) => title.toLowerCase().includes(needle))
56
+ .map((title) => ({
57
+ value: title.toLowerCase().replace(/\W+/g, "-"),
58
+ label: title,
59
+ }))
60
+ )
61
+ }, 500)
62
+ signal.addEventListener("abort", () => {
63
+ clearTimeout(timer)
64
+ reject(signal.reason)
65
+ })
66
+ })
67
+ }
68
+
69
+ const { state: movies, setQuery } = useAsyncOptions(searchMovies, {
70
+ debounce: 300,
71
+ query: "the",
72
+ immediate: true,
73
+ })
74
+ </script>
75
+
76
+ <template>
77
+ <Combobox
78
+ :items="movies.items"
79
+ default-query="the"
80
+ :filter="false"
81
+ :status="movies.status"
82
+ @query-change="(query: string) => setQuery(query)"
83
+ >
84
+ <div data-slot="stack">
85
+ <ComboboxLabel>Movie</ComboboxLabel>
86
+ <div data-slot="row">
87
+ <ComboboxInput placeholder="Search movies…" />
88
+ <ComboboxTrigger aria-label="Show movies">
89
+ <ChevronDownIcon aria-hidden="true" />
90
+ </ComboboxTrigger>
91
+ </div>
92
+ </div>
93
+ <ComboboxContent>
94
+ <ComboboxStatus v-slot="{ state }">
95
+ {{
96
+ state.status === "error"
97
+ ? "Couldn't load movies."
98
+ : state.loading
99
+ ? "Searching…"
100
+ : `${state.count} ${state.count === 1 ? "movie" : "movies"}`
101
+ }}
102
+ </ComboboxStatus>
103
+ <ComboboxEmpty v-slot="{ state }">
104
+ <template v-if="state.empty && !state.loading"
105
+ >No movies match “{{ state.query }}”.</template
106
+ >
107
+ </ComboboxEmpty>
108
+ <ComboboxList v-slot="{ state }">
109
+ <ComboboxItem
110
+ v-for="item in state.items as {
111
+ value: string
112
+ label: string
113
+ disabled?: boolean
114
+ }[]"
115
+ :key="item.value"
116
+ :value="item.value"
117
+ :label="item.label"
118
+ :disabled="item.disabled"
119
+ >
120
+ {{ item.label }}
121
+ <ComboboxItemIndicator>
122
+ <CheckIcon aria-hidden="true" />
123
+ </ComboboxItemIndicator>
124
+ </ComboboxItem>
125
+ </ComboboxList>
126
+ </ComboboxContent>
127
+ </Combobox>
128
+ </template>
129
+ ```
130
+
131
+ Change the movie search and wait for the results. Use the arrow keys to choose a movie, then press Enter.
132
+
133
+ ## Load Results
134
+
135
+ Pass a loader to the helper. This example expects `/api/movies` to return an array of movie titles:
136
+
137
+ ```vue
138
+ <script setup lang="ts">
139
+ import { useAsyncOptions } from "@ui-sdk/vue/async-options"
140
+ import { Combobox, ComboboxContent, ComboboxInput } from "@ui-sdk/vue/combobox"
141
+
142
+ const movies = useAsyncOptions<string>(async (query, { signal }) => {
143
+ const response = await fetch(`/api/movies?q=${encodeURIComponent(query)}`, {
144
+ signal,
145
+ })
146
+ if (!response.ok) throw new Error("Couldn't load movies")
147
+ return response.json()
148
+ })
149
+ const { state } = movies
150
+ </script>
151
+
152
+ <template>
153
+ <Combobox
154
+ :items="state.items"
155
+ :filter="false"
156
+ :status="state.status"
157
+ @query-change="movies.setQuery"
158
+ >
159
+ <ComboboxInput aria-label="Movie" placeholder="Search movies…" />
160
+ <ComboboxContent />
161
+ </Combobox>
162
+ </template>
163
+ ```
164
+
165
+ `state` is a ref, unwrapped in the template. In script, read `state.value.items` or `state.value.status`.
166
+
167
+ Pass `signal` to `fetch` so a newer search can cancel the request. The helper cancels on unmount. `filter` set to false leaves matching and ordering to the server.
168
+
169
+ Use `ComboboxStatus` and `ComboboxEmpty` to show loading, error, and empty messages, as in the demo. The same pattern works with [Select](../components/select.md#async-search), [Autocomplete](../components/autocomplete.md#async-suggestions), and [Command](../components/command.md#async-results).
170
+
171
+ ## Choose When to Load
172
+
173
+ The second argument sets the starting behavior:
174
+
175
+ | Option | What It Does | Default |
176
+ | ----------- | ------------------------------------------------------- | ------- |
177
+ | `debounce` | Milliseconds typing must pause before a request starts. | `200` |
178
+ | `query` | The starting search text. | `""` |
179
+ | `immediate` | Loads that text as soon as the component mounts. | `false` |
180
+
181
+ For initial choices, add `{ immediate: true, debounce: 300 }` after the loader. Until that first load starts, `status` is already `"loading"`, so the list never flashes an empty state. If you set `query`, use the same starting text for the component's `default-query`.
182
+
183
+ ## Load More
184
+
185
+ For paged results, the loader receives `page`, starting at `0`. Return `{ items, hasMore }`:
186
+
187
+ ```ts
188
+ import type { AsyncOptionsContext } from "@ui-sdk/vue/async-options"
189
+
190
+ async function loadMoviePages(
191
+ query: string,
192
+ { signal, page }: AsyncOptionsContext
193
+ ) {
194
+ const response = await fetch(
195
+ `/api/movies?q=${encodeURIComponent(query)}&page=${page}`,
196
+ { signal }
197
+ )
198
+ if (!response.ok) throw new Error("Couldn't load movies")
199
+ const data: { titles: string[]; next: number | null } = await response.json()
200
+ return { items: data.titles, hasMore: data.next !== null }
201
+ }
202
+
203
+ export { loadMoviePages }
204
+ ```
205
+
206
+ Pass `loadMoviePages` to `useAsyncOptions` in place of the inline loader above.
207
+
208
+ Connect `@load-more="movies.loadMore"` on Combobox. It asks for another page when scrolling or keyboard navigation reaches the last item. The helper appends the new items.
209
+
210
+ ```vue title="infinite.vue"
211
+ <script setup lang="ts">
212
+ import { CheckIcon, ChevronDownIcon } from "@lucide/vue"
213
+ import {
214
+ Combobox,
215
+ ComboboxContent,
216
+ ComboboxEmpty,
217
+ ComboboxInput,
218
+ ComboboxItem,
219
+ ComboboxItemIndicator,
220
+ ComboboxLabel,
221
+ ComboboxList,
222
+ ComboboxStatus,
223
+ ComboboxTrigger,
224
+ } from "@ui-sdk/vue/combobox"
225
+ import { useAsyncOptions } from "@ui-sdk/vue/async-options"
226
+
227
+ const movieTitles = [
228
+ "The Shawshank Redemption",
229
+ "The Godfather",
230
+ "The Dark Knight",
231
+ "Pulp Fiction",
232
+ "Schindler's List",
233
+ "The Lord of the Rings",
234
+ "Fight Club",
235
+ "Forrest Gump",
236
+ "Inception",
237
+ "The Matrix",
238
+ "Goodfellas",
239
+ "Interstellar",
240
+ "Parasite",
241
+ "Spirited Away",
242
+ "Whiplash",
243
+ "The Prestige",
244
+ "The Departed",
245
+ "Gladiator",
246
+ "Alien",
247
+ "Casablanca",
248
+ "Amélie",
249
+ "Arrival",
250
+ ]
251
+
252
+ function searchMoviePages(
253
+ query: string,
254
+ { signal, page }: { signal: AbortSignal; page: number }
255
+ ) {
256
+ return new Promise<{
257
+ items: { value: string; label: string }[]
258
+ hasMore: boolean
259
+ }>((resolve, reject) => {
260
+ const timer = setTimeout(() => {
261
+ const needle = query.trim().toLowerCase()
262
+ const found = movieTitles
263
+ .filter((title) => title.toLowerCase().includes(needle))
264
+ .map((title) => ({
265
+ value: title.toLowerCase().replace(/\W+/g, "-"),
266
+ label: title,
267
+ }))
268
+ resolve({
269
+ items: found.slice(page * 5, page * 5 + 5),
270
+ hasMore: found.length > page * 5 + 5,
271
+ })
272
+ }, 500)
273
+ signal.addEventListener("abort", () => {
274
+ clearTimeout(timer)
275
+ reject(signal.reason)
276
+ })
277
+ })
278
+ }
279
+
280
+ const {
281
+ state: movies,
282
+ setQuery,
283
+ loadMore,
284
+ } = useAsyncOptions(searchMoviePages, { immediate: true })
285
+ </script>
286
+
287
+ <template>
288
+ <Combobox
289
+ :items="movies.items"
290
+ :filter="false"
291
+ :status="movies.status"
292
+ @query-change="(query: string) => setQuery(query)"
293
+ @load-more="loadMore"
294
+ >
295
+ <div data-slot="stack">
296
+ <ComboboxLabel>Movie</ComboboxLabel>
297
+ <div data-slot="row">
298
+ <ComboboxInput placeholder="Search movies…" />
299
+ <ComboboxTrigger aria-label="Show movies">
300
+ <ChevronDownIcon aria-hidden="true" />
301
+ </ComboboxTrigger>
302
+ </div>
303
+ </div>
304
+ <ComboboxContent>
305
+ <ComboboxEmpty v-slot="{ state }">
306
+ <template v-if="state.empty && !state.loading"
307
+ >No movies found.</template
308
+ >
309
+ </ComboboxEmpty>
310
+ <ComboboxList v-slot="{ state }">
311
+ <ComboboxItem
312
+ v-for="item in state.items as {
313
+ value: string
314
+ label: string
315
+ disabled?: boolean
316
+ }[]"
317
+ :key="item.value"
318
+ :value="item.value"
319
+ :label="item.label"
320
+ :disabled="item.disabled"
321
+ >
322
+ {{ item.label }}
323
+ <ComboboxItemIndicator>
324
+ <CheckIcon aria-hidden="true" />
325
+ </ComboboxItemIndicator>
326
+ </ComboboxItem>
327
+ </ComboboxList>
328
+ <ComboboxStatus v-slot="{ state }">
329
+ {{
330
+ state.loading
331
+ ? "Loading more…"
332
+ : movies.hasMore
333
+ ? `${state.count} loaded`
334
+ : `All ${state.count}`
335
+ }}
336
+ </ComboboxStatus>
337
+ </ComboboxContent>
338
+ </Combobox>
339
+ </template>
340
+ ```
341
+
342
+ Open the movie list and scroll down, or use Arrow Down to reach the last result. The next page is added to the list.
343
+
344
+ ## Retry a Search
345
+
346
+ `status` is `"loading"` during a request, `"error"` after a failure, and `"idle"` otherwise. `error` holds the failure, and the previous items stay available.
347
+
348
+ Call `movies.reload()` from a retry button to load the current query again, starting at page `0`. `state.value.hasMore` tells you whether another page is available.
@@ -0,0 +1,159 @@
1
+ # Shortcuts
2
+
3
+ Register keyboard shortcuts and show the right keys for the user's platform.
4
+
5
+ `useShortcut` registers a shortcut while your component is mounted. For a component that already accepts `shortcut`, such as a dialog trigger, use that prop directly.
6
+
7
+ ```vue title="basic.vue"
8
+ <script setup lang="ts">
9
+ import { useShortcut, useShortcutHint } from "@ui-sdk/vue/shortcuts"
10
+ import { ref } from "vue"
11
+
12
+ const status = ref("Press N or the keys to save.")
13
+ const newNote = () => (status.value = "Note created")
14
+ const save = () => (status.value = "Saved")
15
+
16
+ useShortcut("n", newNote, { description: "New note" })
17
+ useShortcut("mod+s", save, { description: "Save" })
18
+ const { text: saveKeys, ariaKeyshortcuts: saveAria } = useShortcutHint("mod+s")
19
+ </script>
20
+
21
+ <template>
22
+ <div data-slot="stack">
23
+ <div data-slot="row">
24
+ <button type="button" aria-keyshortcuts="N" @click="newNote">
25
+ New note
26
+ <kbd aria-hidden="true" data-slot="kbd">N</kbd>
27
+ </button>
28
+ <button type="button" :aria-keyshortcuts="saveAria" @click="save">
29
+ Save
30
+ <kbd aria-hidden="true" data-slot="kbd">{{ saveKeys }}</kbd>
31
+ </button>
32
+ </div>
33
+ <output data-slot="output">{{ status }}</output>
34
+ </div>
35
+ </template>
36
+ ```
37
+
38
+ Press N to create a note. Press Command+S on Mac or Control+S elsewhere to save. The buttons run the same actions.
39
+
40
+ ## Register a Shortcut
41
+
42
+ Keys are joined with `+`. `mod` means Command on Apple platforms and Control elsewhere. This example also displays the keys using `useShortcutHint`:
43
+
44
+ ```vue
45
+ <script setup lang="ts">
46
+ import { useShortcut, useShortcutHint } from "@ui-sdk/vue/shortcuts"
47
+ import { ref } from "vue"
48
+
49
+ const count = ref(0)
50
+ const addNote = () => count.value++
51
+ useShortcut("mod+enter", addNote, { description: "New note" })
52
+ const { text, ariaKeyshortcuts } = useShortcutHint("mod+enter")
53
+ </script>
54
+
55
+ <template>
56
+ <button @click="addNote" :aria-keyshortcuts="ariaKeyshortcuts">
57
+ New note ({{ count }}) <kbd aria-hidden="true">{{ text }}</kbd>
58
+ </button>
59
+ </template>
60
+ ```
61
+
62
+ Call the helper in setup. It unregisters when the component goes away. Keys and options can be getters; return `null` for keys to leave the shortcut unregistered.
63
+
64
+ ## Show the Keys
65
+
66
+ `useShortcutHint` supplies display text and `aria-keyshortcuts`. It keeps the server and first client render consistent, then updates for the user's platform after mount.
67
+
68
+ Its fields are computed refs. The example destructures them so the template unwraps them automatically.
69
+
70
+ Outside rendering, use `formatShortcut` for display text or `ariaKeyshortcuts` for the ARIA value, from the same entry.
71
+
72
+ ## While Someone Is Typing
73
+
74
+ Shortcuts with Command, Control, or Option can run in text fields. Plain letters and other keys stay available for typing. Set `allowInInput: true` when a shortcut should work there too, such as Escape to clear a search.
75
+
76
+ ## Limit a Shortcut to an Area
77
+
78
+ Use a named `scope` for an editor or another area. Turn it on while that area has focus:
79
+
80
+ ```vue
81
+ <script setup lang="ts">
82
+ import { shortcuts, useShortcut } from "@ui-sdk/vue/shortcuts"
83
+ import { onScopeDispose, ref } from "vue"
84
+
85
+ const text = ref("")
86
+ useShortcut(
87
+ "escape",
88
+ () => {
89
+ text.value = ""
90
+ },
91
+ { scope: "note", allowInInput: true }
92
+ )
93
+ onScopeDispose(() => shortcuts.setScope("note", false))
94
+ </script>
95
+
96
+ <template>
97
+ <textarea
98
+ aria-label="Note"
99
+ v-model="text"
100
+ @focus="shortcuts.setScope('note', true)"
101
+ @blur="shortcuts.setScope('note', false)"
102
+ />
103
+ </template>
104
+ ```
105
+
106
+ Scope names are shared, so choose one for each area you want to enable together. A `when` function can add another condition, checked each time the keys are pressed.
107
+
108
+ ```vue title="scope.vue"
109
+ <script setup lang="ts">
110
+ import { shortcuts, useShortcut } from "@ui-sdk/vue/shortcuts"
111
+ import { ref } from "vue"
112
+
113
+ const text = ref("")
114
+ const status = ref("Focus the note to turn on its shortcuts.")
115
+
116
+ useShortcut(
117
+ "mod+enter",
118
+ () => (status.value = `Characters saved: ${text.value.trim().length}`),
119
+ {
120
+ scope: "note",
121
+ when: () => text.value.trim() !== "",
122
+ description: "Save the note",
123
+ }
124
+ )
125
+ useShortcut(
126
+ "escape",
127
+ () => {
128
+ text.value = ""
129
+ status.value = "Cleared"
130
+ },
131
+ { scope: "note", allowInInput: true, description: "Clear the note" }
132
+ )
133
+ </script>
134
+
135
+ <template>
136
+ <div data-slot="stack">
137
+ <textarea
138
+ v-model="text"
139
+ aria-label="Note"
140
+ placeholder="Write a note…"
141
+ @focus="shortcuts.setScope('note', true)"
142
+ @blur="shortcuts.setScope('note', false)"
143
+ />
144
+ <output data-slot="output">{{ status }}</output>
145
+ </div>
146
+ </template>
147
+ ```
148
+
149
+ Focus the note and type some text. Press Command+Enter on Mac or Control+Enter elsewhere to save, or Escape to clear it. Move focus outside the note to turn off its shortcuts.
150
+
151
+ ## Inside a Popup
152
+
153
+ Page shortcuts pause while a modal is open. Pass `layer`, an element inside the popup or a getter that returns one, to make a shortcut belong to that popup. It runs only while that popup is open.
154
+
155
+ When shortcuts share the same keys, the topmost open layer wins, then the most recently registered shortcut.
156
+
157
+ ## List Available Shortcuts
158
+
159
+ Call `shortcuts.list()` when a help screen opens. It returns the shortcuts that can run now, including their `keys`, formatted `hint`, and `description`. Add descriptions when registering shortcuts so the help screen can explain each action.