@ui-sdk/react 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 +331 -0
- package/dist/accordion.js +25 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +8 -0
- package/dist/alert-dialog.d.ts +56 -0
- package/dist/alert-dialog.js +45 -0
- package/dist/async-options.d.ts +22 -0
- package/dist/async-options.js +56 -0
- package/dist/autocomplete.d.ts +102 -0
- package/dist/autocomplete.js +195 -0
- package/dist/avatar.d.ts +301 -0
- package/dist/avatar.js +17 -0
- package/dist/button.d.ts +21 -0
- package/dist/button.js +45 -0
- package/dist/calendar-context.d.ts +10 -0
- package/dist/calendar-context.js +9 -0
- package/dist/calendar.d.ts +80 -0
- package/dist/calendar.js +257 -0
- package/dist/carousel.d.ts +52 -0
- package/dist/carousel.js +151 -0
- package/dist/checkbox.d.ts +24 -0
- package/dist/checkbox.js +98 -0
- package/dist/children.d.ts +38 -0
- package/dist/children.js +63 -0
- package/dist/collapsible.d.ts +302 -0
- package/dist/collapsible.js +19 -0
- package/dist/color-picker.d.ts +72 -0
- package/dist/color-picker.js +133 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +22 -0
- package/dist/combobox.d.ts +152 -0
- package/dist/combobox.js +338 -0
- package/dist/command.d.ts +88 -0
- package/dist/command.js +233 -0
- package/dist/controller-target.d.ts +3 -0
- package/dist/controller-target.js +59 -0
- package/dist/date-input.d.ts +19 -0
- package/dist/date-input.js +58 -0
- package/dist/date-picker.d.ts +47 -0
- package/dist/date-picker.js +310 -0
- package/dist/date.d.ts +2 -0
- package/dist/date.js +60 -0
- package/dist/dialog-parts.d.ts +23 -0
- package/dist/dialog-parts.js +142 -0
- package/dist/dialog.d.ts +57 -0
- package/dist/dialog.js +32 -0
- package/dist/drawer.d.ts +70 -0
- package/dist/drawer.js +158 -0
- package/dist/editor-context.d.ts +4 -0
- package/dist/editor-context.js +9 -0
- package/dist/field-context.d.ts +47 -0
- package/dist/field-context.js +87 -0
- package/dist/field.d.ts +49 -0
- package/dist/field.js +169 -0
- package/dist/file-input.d.ts +46 -0
- package/dist/file-input.js +170 -0
- package/dist/form-inputs.d.ts +5 -0
- package/dist/form-inputs.js +22 -0
- package/dist/gesture.d.ts +1 -0
- package/dist/gesture.js +16 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +12 -0
- package/dist/hover-card.d.ts +33 -0
- package/dist/hover-card.js +84 -0
- package/dist/image-cropper.d.ts +333 -0
- package/dist/image-cropper.js +37 -0
- package/dist/image.d.ts +2 -0
- package/dist/image.js +6 -0
- package/dist/input.d.ts +22 -0
- package/dist/input.js +51 -0
- package/dist/label.d.ts +294 -0
- package/dist/label.js +9 -0
- package/dist/list-items.d.ts +15 -0
- package/dist/list-items.js +39 -0
- package/dist/list-text.d.ts +4 -0
- package/dist/list-text.js +15 -0
- package/dist/listbox.d.ts +56 -0
- package/dist/listbox.js +122 -0
- package/dist/menu.d.ts +116 -0
- package/dist/menu.js +426 -0
- package/dist/menubar-context.d.ts +4 -0
- package/dist/menubar-context.js +7 -0
- package/dist/menubar.d.ts +11 -0
- package/dist/menubar.js +32 -0
- package/dist/meter.d.ts +312 -0
- package/dist/meter.js +30 -0
- package/dist/navigation-menu.d.ts +61 -0
- package/dist/navigation-menu.js +169 -0
- package/dist/number-input.d.ts +44 -0
- package/dist/number-input.js +185 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +40 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +8 -0
- package/dist/otp-input.d.ts +29 -0
- package/dist/otp-input.js +94 -0
- package/dist/overlays.d.ts +26 -0
- package/dist/overlays.js +82 -0
- package/dist/pagination.d.ts +336 -0
- package/dist/pagination.js +71 -0
- package/dist/password-input.d.ts +23 -0
- package/dist/password-input.js +65 -0
- package/dist/popover.d.ts +54 -0
- package/dist/popover.js +151 -0
- package/dist/popup.d.ts +41 -0
- package/dist/popup.js +180 -0
- package/dist/preferences.d.ts +35 -0
- package/dist/preferences.js +93 -0
- package/dist/primitive.d.ts +2 -0
- package/dist/primitive.js +14 -0
- package/dist/progress.d.ts +312 -0
- package/dist/progress.js +30 -0
- package/dist/radio.d.ts +22 -0
- package/dist/radio.js +86 -0
- package/dist/rating.d.ts +20 -0
- package/dist/rating.js +94 -0
- package/dist/render-element.d.ts +35 -0
- package/dist/render-element.js +103 -0
- package/dist/resizable.d.ts +302 -0
- package/dist/resizable.js +23 -0
- package/dist/scroll-area.d.ts +324 -0
- package/dist/scroll-area.js +49 -0
- package/dist/segmented-control.d.ts +24 -0
- package/dist/segmented-control.js +78 -0
- package/dist/segmented-input.d.ts +15 -0
- package/dist/segmented-input.js +48 -0
- package/dist/select.d.ts +125 -0
- package/dist/select.js +330 -0
- package/dist/separator.d.ts +290 -0
- package/dist/separator.js +9 -0
- package/dist/shortcuts.d.ts +32 -0
- package/dist/shortcuts.js +61 -0
- package/dist/slider.d.ts +38 -0
- package/dist/slider.js +163 -0
- package/dist/switch.d.ts +15 -0
- package/dist/switch.js +39 -0
- package/dist/tabs.d.ts +331 -0
- package/dist/tabs.js +28 -0
- package/dist/text-control.d.ts +22 -0
- package/dist/text-control.js +24 -0
- package/dist/text-match.d.ts +2 -0
- package/dist/text-match.js +8 -0
- package/dist/textarea.d.ts +10 -0
- package/dist/textarea.js +48 -0
- package/dist/time-input.d.ts +19 -0
- package/dist/time-input.js +63 -0
- package/dist/timeline.d.ts +327 -0
- package/dist/timeline.js +31 -0
- package/dist/toast-parts.d.ts +63 -0
- package/dist/toast-parts.js +144 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.js +22 -0
- package/dist/toggle.d.ts +23 -0
- package/dist/toggle.js +76 -0
- package/dist/token-input.d.ts +35 -0
- package/dist/token-input.js +100 -0
- package/dist/toolbar-context.d.ts +6 -0
- package/dist/toolbar-context.js +16 -0
- package/dist/toolbar.d.ts +38 -0
- package/dist/toolbar.js +63 -0
- package/dist/tooltip.d.ts +39 -0
- package/dist/tooltip.js +108 -0
- package/dist/tree.d.ts +51 -0
- package/dist/tree.js +150 -0
- package/dist/trigger-menu.d.ts +83 -0
- package/dist/trigger-menu.js +167 -0
- package/dist/use-primitive.d.ts +178 -0
- package/dist/use-primitive.js +674 -0
- package/dist/waterfall.d.ts +80 -0
- package/dist/waterfall.js +108 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1391 -0
- package/docs/components/alert-dialog.md +850 -0
- package/docs/components/autocomplete.md +1963 -0
- package/docs/components/avatar.md +252 -0
- package/docs/components/button.md +444 -0
- package/docs/components/calendar.md +1687 -0
- package/docs/components/carousel.md +1244 -0
- package/docs/components/checkbox.md +1069 -0
- package/docs/components/collapsible.md +567 -0
- package/docs/components/color-picker.md +736 -0
- package/docs/components/combobox.md +3647 -0
- package/docs/components/command.md +1573 -0
- package/docs/components/date-input.md +608 -0
- package/docs/components/date-picker.md +1266 -0
- package/docs/components/dialog.md +1469 -0
- package/docs/components/drawer.md +1197 -0
- package/docs/components/field.md +549 -0
- package/docs/components/file-input.md +609 -0
- package/docs/components/hover-card.md +514 -0
- package/docs/components/image-cropper.md +579 -0
- package/docs/components/input.md +301 -0
- package/docs/components/label.md +217 -0
- package/docs/components/listbox.md +1130 -0
- package/docs/components/menu.md +1286 -0
- package/docs/components/menubar.md +455 -0
- package/docs/components/meter.md +304 -0
- package/docs/components/navigation-menu.md +1581 -0
- package/docs/components/number-input.md +1086 -0
- package/docs/components/otp-input.md +565 -0
- package/docs/components/pagination.md +648 -0
- package/docs/components/password-input.md +737 -0
- package/docs/components/popover.md +1552 -0
- package/docs/components/preferences.md +220 -0
- package/docs/components/progress.md +443 -0
- package/docs/components/radio.md +846 -0
- package/docs/components/rating.md +458 -0
- package/docs/components/resizable.md +982 -0
- package/docs/components/scroll-area.md +980 -0
- package/docs/components/segmented-control.md +465 -0
- package/docs/components/select.md +2133 -0
- package/docs/components/separator.md +243 -0
- package/docs/components/slider.md +1169 -0
- package/docs/components/switch.md +613 -0
- package/docs/components/tabs.md +1014 -0
- package/docs/components/textarea.md +318 -0
- package/docs/components/time-input.md +231 -0
- package/docs/components/timeline.md +677 -0
- package/docs/components/toast.md +1395 -0
- package/docs/components/toggle.md +541 -0
- package/docs/components/token-input.md +1007 -0
- package/docs/components/toolbar.md +787 -0
- package/docs/components/tooltip.md +884 -0
- package/docs/components/tree.md +1346 -0
- package/docs/components/trigger-menu.md +1257 -0
- package/docs/components/waterfall.md +754 -0
- package/docs/controllers.md +89 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +120 -0
- package/docs/first-component.md +83 -0
- package/docs/forms.md +148 -0
- package/docs/hooks/async-options.md +366 -0
- package/docs/hooks/shortcuts.md +221 -0
- package/docs/installation.md +45 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +170 -0
- package/docs/llms.txt +1844 -0
- package/docs/parts-and-composition.md +93 -0
- package/docs/popups.md +117 -0
- package/docs/rendering.md +121 -0
- package/docs/server-rendering.md +253 -0
- package/docs/state-and-events.md +105 -0
- package/docs/styling-and-animation.md +176 -0
- package/package.json +333 -3
|
@@ -0,0 +1,366 @@
|
|
|
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
|
+
```tsx title="async.tsx"
|
|
8
|
+
"use client"
|
|
9
|
+
|
|
10
|
+
import { useAsyncOptions } from "@ui-sdk/react/async-options"
|
|
11
|
+
import {
|
|
12
|
+
Combobox,
|
|
13
|
+
ComboboxContent,
|
|
14
|
+
ComboboxEmpty,
|
|
15
|
+
ComboboxInput,
|
|
16
|
+
ComboboxItem,
|
|
17
|
+
ComboboxItemIndicator,
|
|
18
|
+
ComboboxLabel,
|
|
19
|
+
ComboboxList,
|
|
20
|
+
ComboboxStatus,
|
|
21
|
+
ComboboxTrigger,
|
|
22
|
+
} from "@ui-sdk/react/combobox"
|
|
23
|
+
import { CheckIcon, ChevronDownIcon } from "lucide-react"
|
|
24
|
+
|
|
25
|
+
const movieTitles = [
|
|
26
|
+
"The Shawshank Redemption",
|
|
27
|
+
"The Godfather",
|
|
28
|
+
"The Dark Knight",
|
|
29
|
+
"Pulp Fiction",
|
|
30
|
+
"Schindler's List",
|
|
31
|
+
"The Lord of the Rings",
|
|
32
|
+
"Fight Club",
|
|
33
|
+
"Forrest Gump",
|
|
34
|
+
"Inception",
|
|
35
|
+
"The Matrix",
|
|
36
|
+
"Goodfellas",
|
|
37
|
+
"Interstellar",
|
|
38
|
+
"Parasite",
|
|
39
|
+
"Spirited Away",
|
|
40
|
+
"Whiplash",
|
|
41
|
+
"The Prestige",
|
|
42
|
+
"The Departed",
|
|
43
|
+
"Gladiator",
|
|
44
|
+
"Alien",
|
|
45
|
+
"Casablanca",
|
|
46
|
+
"Amélie",
|
|
47
|
+
"Arrival",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
function searchMovies(query: string, { signal }: { signal: AbortSignal }) {
|
|
51
|
+
return new Promise<{ value: string; label: string }[]>((resolve, reject) => {
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
const needle = query.trim().toLowerCase()
|
|
54
|
+
resolve(
|
|
55
|
+
movieTitles
|
|
56
|
+
.filter((title) => title.toLowerCase().includes(needle))
|
|
57
|
+
.map((title) => ({
|
|
58
|
+
value: title.toLowerCase().replace(/\W+/g, "-"),
|
|
59
|
+
label: title,
|
|
60
|
+
}))
|
|
61
|
+
)
|
|
62
|
+
}, 500)
|
|
63
|
+
signal.addEventListener("abort", () => {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
reject(signal.reason)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Example() {
|
|
71
|
+
const movies = useAsyncOptions(searchMovies, {
|
|
72
|
+
debounce: 300,
|
|
73
|
+
query: "the",
|
|
74
|
+
immediate: true,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Combobox
|
|
79
|
+
items={movies.items}
|
|
80
|
+
defaultQuery="the"
|
|
81
|
+
filter={false}
|
|
82
|
+
status={movies.status}
|
|
83
|
+
onQueryChange={(query) => movies.setQuery(query)}
|
|
84
|
+
>
|
|
85
|
+
<div data-slot="stack">
|
|
86
|
+
<ComboboxLabel>Movie</ComboboxLabel>
|
|
87
|
+
<div data-slot="row">
|
|
88
|
+
<ComboboxInput placeholder="Search movies…" />
|
|
89
|
+
<ComboboxTrigger aria-label="Show movies">
|
|
90
|
+
<ChevronDownIcon aria-hidden="true" />
|
|
91
|
+
</ComboboxTrigger>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<ComboboxContent>
|
|
95
|
+
<ComboboxStatus>
|
|
96
|
+
{(state) =>
|
|
97
|
+
state.status === "error"
|
|
98
|
+
? "Couldn't load movies."
|
|
99
|
+
: state.loading
|
|
100
|
+
? "Searching…"
|
|
101
|
+
: `${state.count} ${state.count === 1 ? "movie" : "movies"}`
|
|
102
|
+
}
|
|
103
|
+
</ComboboxStatus>
|
|
104
|
+
<ComboboxEmpty>
|
|
105
|
+
{(state) =>
|
|
106
|
+
state.empty && !state.loading
|
|
107
|
+
? `No movies match “${state.query}”.`
|
|
108
|
+
: null
|
|
109
|
+
}
|
|
110
|
+
</ComboboxEmpty>
|
|
111
|
+
<ComboboxList>
|
|
112
|
+
{(state) =>
|
|
113
|
+
(
|
|
114
|
+
state.items as {
|
|
115
|
+
value: string
|
|
116
|
+
label: string
|
|
117
|
+
disabled?: boolean
|
|
118
|
+
}[]
|
|
119
|
+
).map((item) => (
|
|
120
|
+
<ComboboxItem
|
|
121
|
+
key={item.value}
|
|
122
|
+
value={item.value}
|
|
123
|
+
label={item.label}
|
|
124
|
+
disabled={item.disabled}
|
|
125
|
+
>
|
|
126
|
+
{item.label}
|
|
127
|
+
<ComboboxItemIndicator>
|
|
128
|
+
<CheckIcon aria-hidden="true" />
|
|
129
|
+
</ComboboxItemIndicator>
|
|
130
|
+
</ComboboxItem>
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
</ComboboxList>
|
|
134
|
+
</ComboboxContent>
|
|
135
|
+
</Combobox>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { Example }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Change the movie search and wait for the results. Use the arrow keys to choose a movie, then press Enter.
|
|
143
|
+
|
|
144
|
+
## Load Results
|
|
145
|
+
|
|
146
|
+
Pass a loader to the hook. This example expects `/api/movies` to return an array of movie titles:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
"use client"
|
|
150
|
+
|
|
151
|
+
import { useAsyncOptions } from "@ui-sdk/react/async-options"
|
|
152
|
+
import {
|
|
153
|
+
Combobox,
|
|
154
|
+
ComboboxContent,
|
|
155
|
+
ComboboxInput,
|
|
156
|
+
} from "@ui-sdk/react/combobox"
|
|
157
|
+
|
|
158
|
+
function MovieSearch() {
|
|
159
|
+
const movies = useAsyncOptions<string>(async (query, { signal }) => {
|
|
160
|
+
const response = await fetch(`/api/movies?q=${encodeURIComponent(query)}`, {
|
|
161
|
+
signal,
|
|
162
|
+
})
|
|
163
|
+
if (!response.ok) throw new Error("Couldn't load movies")
|
|
164
|
+
return response.json()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Combobox
|
|
169
|
+
items={movies.items}
|
|
170
|
+
filter={false}
|
|
171
|
+
status={movies.status}
|
|
172
|
+
onQueryChange={movies.setQuery}
|
|
173
|
+
>
|
|
174
|
+
<ComboboxInput aria-label="Movie" placeholder="Search movies…" />
|
|
175
|
+
<ComboboxContent />
|
|
176
|
+
</Combobox>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { MovieSearch }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Pass `signal` to `fetch` so a newer search can cancel the request. The hook also cancels on unmount.
|
|
184
|
+
|
|
185
|
+
`filter={false}` leaves matching and result order to the server. `status` tells the component when it's busy. Use `ComboboxStatus` and `ComboboxEmpty` to show loading, error, and empty messages, as in the demo.
|
|
186
|
+
|
|
187
|
+
The same pattern works with [Select](../components/select.md#async-search), [Autocomplete](../components/autocomplete.md#async-suggestions), and [Command](../components/command.md#async-results).
|
|
188
|
+
|
|
189
|
+
## Choose When to Load
|
|
190
|
+
|
|
191
|
+
The hook's second argument sets its starting behavior:
|
|
192
|
+
|
|
193
|
+
| Option | What It Does | Default |
|
|
194
|
+
| ----------- | ------------------------------------------------ | ------- |
|
|
195
|
+
| `debounce` | How long typing must pause, in milliseconds. | `200` |
|
|
196
|
+
| `query` | The starting search text. | `""` |
|
|
197
|
+
| `immediate` | Loads that text as soon as the component mounts. | `false` |
|
|
198
|
+
|
|
199
|
+
For example, add `{ immediate: true, debounce: 300 }` after the loader to fetch initial choices and wait a little longer between searches. 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 `defaultQuery`.
|
|
200
|
+
|
|
201
|
+
## Load More
|
|
202
|
+
|
|
203
|
+
For paged results, the loader also receives `page`, starting at `0`. Return `{ items, hasMore }`:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
"use client"
|
|
207
|
+
|
|
208
|
+
import { useAsyncOptions } from "@ui-sdk/react/async-options"
|
|
209
|
+
|
|
210
|
+
function useMoviePages() {
|
|
211
|
+
return useAsyncOptions<string>(async (query, { signal, page }) => {
|
|
212
|
+
const response = await fetch(
|
|
213
|
+
`/api/movies?q=${encodeURIComponent(query)}&page=${page}`,
|
|
214
|
+
{ signal }
|
|
215
|
+
)
|
|
216
|
+
if (!response.ok) throw new Error("Couldn't load movies")
|
|
217
|
+
const data: { titles: string[]; next: number | null } =
|
|
218
|
+
await response.json()
|
|
219
|
+
return { items: data.titles, hasMore: data.next !== null }
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export { useMoviePages }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Call `useMoviePages()` inside your component, then pass `onLoadMore={movies.loadMore}` to Combobox alongside the props above. It asks for another page when scrolling or keyboard navigation reaches the last item. The hook appends the new items.
|
|
227
|
+
|
|
228
|
+
```tsx title="infinite.tsx"
|
|
229
|
+
"use client"
|
|
230
|
+
|
|
231
|
+
import { useAsyncOptions } from "@ui-sdk/react/async-options"
|
|
232
|
+
import {
|
|
233
|
+
Combobox,
|
|
234
|
+
ComboboxContent,
|
|
235
|
+
ComboboxEmpty,
|
|
236
|
+
ComboboxInput,
|
|
237
|
+
ComboboxItem,
|
|
238
|
+
ComboboxItemIndicator,
|
|
239
|
+
ComboboxLabel,
|
|
240
|
+
ComboboxList,
|
|
241
|
+
ComboboxStatus,
|
|
242
|
+
ComboboxTrigger,
|
|
243
|
+
} from "@ui-sdk/react/combobox"
|
|
244
|
+
import { CheckIcon, ChevronDownIcon } from "lucide-react"
|
|
245
|
+
|
|
246
|
+
const movieTitles = [
|
|
247
|
+
"The Shawshank Redemption",
|
|
248
|
+
"The Godfather",
|
|
249
|
+
"The Dark Knight",
|
|
250
|
+
"Pulp Fiction",
|
|
251
|
+
"Schindler's List",
|
|
252
|
+
"The Lord of the Rings",
|
|
253
|
+
"Fight Club",
|
|
254
|
+
"Forrest Gump",
|
|
255
|
+
"Inception",
|
|
256
|
+
"The Matrix",
|
|
257
|
+
"Goodfellas",
|
|
258
|
+
"Interstellar",
|
|
259
|
+
"Parasite",
|
|
260
|
+
"Spirited Away",
|
|
261
|
+
"Whiplash",
|
|
262
|
+
"The Prestige",
|
|
263
|
+
"The Departed",
|
|
264
|
+
"Gladiator",
|
|
265
|
+
"Alien",
|
|
266
|
+
"Casablanca",
|
|
267
|
+
"Amélie",
|
|
268
|
+
"Arrival",
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
function searchMoviePages(
|
|
272
|
+
query: string,
|
|
273
|
+
{ signal, page }: { signal: AbortSignal; page: number }
|
|
274
|
+
) {
|
|
275
|
+
return new Promise<{
|
|
276
|
+
items: { value: string; label: string }[]
|
|
277
|
+
hasMore: boolean
|
|
278
|
+
}>((resolve, reject) => {
|
|
279
|
+
const timer = setTimeout(() => {
|
|
280
|
+
const needle = query.trim().toLowerCase()
|
|
281
|
+
const found = movieTitles
|
|
282
|
+
.filter((title) => title.toLowerCase().includes(needle))
|
|
283
|
+
.map((title) => ({
|
|
284
|
+
value: title.toLowerCase().replace(/\W+/g, "-"),
|
|
285
|
+
label: title,
|
|
286
|
+
}))
|
|
287
|
+
resolve({
|
|
288
|
+
items: found.slice(page * 5, page * 5 + 5),
|
|
289
|
+
hasMore: found.length > page * 5 + 5,
|
|
290
|
+
})
|
|
291
|
+
}, 500)
|
|
292
|
+
signal.addEventListener("abort", () => {
|
|
293
|
+
clearTimeout(timer)
|
|
294
|
+
reject(signal.reason)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function Example() {
|
|
300
|
+
const movies = useAsyncOptions(searchMoviePages, { immediate: true })
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<Combobox
|
|
304
|
+
items={movies.items}
|
|
305
|
+
filter={false}
|
|
306
|
+
status={movies.status}
|
|
307
|
+
onQueryChange={(query) => movies.setQuery(query)}
|
|
308
|
+
onLoadMore={movies.loadMore}
|
|
309
|
+
>
|
|
310
|
+
<div data-slot="stack">
|
|
311
|
+
<ComboboxLabel>Movie</ComboboxLabel>
|
|
312
|
+
<div data-slot="row">
|
|
313
|
+
<ComboboxInput placeholder="Search movies…" />
|
|
314
|
+
<ComboboxTrigger aria-label="Show movies">
|
|
315
|
+
<ChevronDownIcon aria-hidden="true" />
|
|
316
|
+
</ComboboxTrigger>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<ComboboxContent>
|
|
320
|
+
<ComboboxEmpty>
|
|
321
|
+
{(state) =>
|
|
322
|
+
state.empty && !state.loading ? "No movies found." : null
|
|
323
|
+
}
|
|
324
|
+
</ComboboxEmpty>
|
|
325
|
+
<ComboboxList>
|
|
326
|
+
{(state) =>
|
|
327
|
+
(
|
|
328
|
+
state.items as {
|
|
329
|
+
value: string
|
|
330
|
+
label: string
|
|
331
|
+
disabled?: boolean
|
|
332
|
+
}[]
|
|
333
|
+
).map((item) => (
|
|
334
|
+
<ComboboxItem key={item.value} value={item.value} label={item.label} disabled={item.disabled}>
|
|
335
|
+
{item.label}
|
|
336
|
+
<ComboboxItemIndicator>
|
|
337
|
+
<CheckIcon aria-hidden="true" />
|
|
338
|
+
</ComboboxItemIndicator>
|
|
339
|
+
</ComboboxItem>
|
|
340
|
+
))
|
|
341
|
+
}
|
|
342
|
+
</ComboboxList>
|
|
343
|
+
<ComboboxStatus>
|
|
344
|
+
{(state) =>
|
|
345
|
+
state.loading
|
|
346
|
+
? "Loading more…"
|
|
347
|
+
: movies.hasMore
|
|
348
|
+
? `${state.count} loaded`
|
|
349
|
+
: `All ${state.count}`
|
|
350
|
+
}
|
|
351
|
+
</ComboboxStatus>
|
|
352
|
+
</ComboboxContent>
|
|
353
|
+
</Combobox>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export { Example }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Open the movie list and scroll down, or use Arrow Down to reach the last result. The next page is added to the list.
|
|
361
|
+
|
|
362
|
+
## Retry a Search
|
|
363
|
+
|
|
364
|
+
`status` is `"loading"` while a request is pending, `"error"` after a failure, and `"idle"` otherwise. `error` holds the failure, and the previous items stay available.
|
|
365
|
+
|
|
366
|
+
Call `movies.reload()` from a retry button to load the current query again, starting at page `0`. `movies.hasMore` tells you whether another page is available.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Shortcuts
|
|
2
|
+
|
|
3
|
+
Register keyboard shortcuts and show the right keys for the user's platform.
|
|
4
|
+
|
|
5
|
+
`useShortcut` registers a keyboard shortcut while your component is mounted. Use it for actions outside a particular control. For a component that already accepts `shortcut`, such as a dialog trigger, use that prop directly.
|
|
6
|
+
|
|
7
|
+
```tsx title="basic.tsx"
|
|
8
|
+
import { useShortcut, useShortcutHint } from "@ui-sdk/react/shortcuts"
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
|
|
11
|
+
export function Example() {
|
|
12
|
+
const [status, setStatus] = React.useState("Press N or the keys to save.")
|
|
13
|
+
const newNote = () => setStatus("Note created")
|
|
14
|
+
const save = () => setStatus("Saved")
|
|
15
|
+
|
|
16
|
+
useShortcut("n", newNote, { description: "New note" })
|
|
17
|
+
useShortcut("mod+s", save, { description: "Save" })
|
|
18
|
+
const saveHint = useShortcutHint("mod+s")
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div data-slot="stack">
|
|
22
|
+
<div data-slot="row">
|
|
23
|
+
<button type="button" onClick={newNote} aria-keyshortcuts="N">
|
|
24
|
+
New note
|
|
25
|
+
<kbd aria-hidden="true" data-slot="kbd">
|
|
26
|
+
N
|
|
27
|
+
</kbd>
|
|
28
|
+
</button>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={save}
|
|
32
|
+
aria-keyshortcuts={saveHint.ariaKeyshortcuts}
|
|
33
|
+
>
|
|
34
|
+
Save
|
|
35
|
+
<kbd aria-hidden="true" data-slot="kbd">
|
|
36
|
+
{saveHint.text}
|
|
37
|
+
</kbd>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<output data-slot="output">{status}</output>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Press N to create a note. Press Command+S on Mac or Control+S elsewhere to save. The buttons run the same actions.
|
|
47
|
+
|
|
48
|
+
## Register a Shortcut
|
|
49
|
+
|
|
50
|
+
Keys are joined with `+`. `mod` means Command on Apple platforms and Control elsewhere:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
"use client"
|
|
54
|
+
|
|
55
|
+
import { useShortcut } from "@ui-sdk/react/shortcuts"
|
|
56
|
+
import { useState } from "react"
|
|
57
|
+
|
|
58
|
+
function NewNote() {
|
|
59
|
+
const [count, setCount] = useState(0)
|
|
60
|
+
const addNote = () => setCount((count) => count + 1)
|
|
61
|
+
useShortcut("mod+enter", addNote, { description: "New note" })
|
|
62
|
+
|
|
63
|
+
return <button onClick={addNote}>New note ({count})</button>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { NewNote }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The hook uses your latest handler and unregisters on unmount. You don't need `useCallback`. Pass `null` instead of keys to leave a shortcut unregistered.
|
|
70
|
+
|
|
71
|
+
## Show the Keys
|
|
72
|
+
|
|
73
|
+
`useShortcutHint` formats keys for the current platform and supplies `aria-keyshortcuts` for the control:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
"use client"
|
|
77
|
+
|
|
78
|
+
import { useShortcut, useShortcutHint } from "@ui-sdk/react/shortcuts"
|
|
79
|
+
|
|
80
|
+
function SaveButton({ onSave }: { onSave: () => void }) {
|
|
81
|
+
useShortcut("mod+s", onSave, { description: "Save" })
|
|
82
|
+
const hint = useShortcutHint("mod+s")
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<button onClick={onSave} aria-keyshortcuts={hint.ariaKeyshortcuts}>
|
|
86
|
+
Save <kbd aria-hidden="true">{hint.text}</kbd>
|
|
87
|
+
</button>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { SaveButton }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This shows `⌘S` on Apple platforms and `Ctrl+S` elsewhere. The hook keeps the server and first client render consistent, then updates after mount.
|
|
95
|
+
|
|
96
|
+
Outside rendering, use `formatShortcut` for display text or `ariaKeyshortcuts` for the ARIA value. Both are exported from the same entry. Compare the original keys, not the formatted hint.
|
|
97
|
+
|
|
98
|
+
## While Someone Is Typing
|
|
99
|
+
|
|
100
|
+
Shortcuts with Command, Control, or Option can run in a text field. Plain letters and other keys stay available for typing.
|
|
101
|
+
|
|
102
|
+
Set `allowInInput: true` when the shortcut should work there too, such as Escape to clear a search. `when` checks a condition each time the keys are pressed; it can keep a Save shortcut inactive until there's text.
|
|
103
|
+
|
|
104
|
+
```tsx title="scope.tsx"
|
|
105
|
+
import { shortcuts, useShortcut } from "@ui-sdk/react/shortcuts"
|
|
106
|
+
import * as React from "react"
|
|
107
|
+
|
|
108
|
+
export function Example() {
|
|
109
|
+
const [text, setText] = React.useState("")
|
|
110
|
+
const [status, setStatus] = React.useState(
|
|
111
|
+
"Focus the note to turn on its shortcuts."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
useShortcut(
|
|
115
|
+
"mod+enter",
|
|
116
|
+
() => setStatus(`Characters saved: ${text.trim().length}`),
|
|
117
|
+
{
|
|
118
|
+
scope: "note",
|
|
119
|
+
when: () => text.trim() !== "",
|
|
120
|
+
description: "Save the note",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
useShortcut(
|
|
124
|
+
"escape",
|
|
125
|
+
() => {
|
|
126
|
+
setText("")
|
|
127
|
+
setStatus("Cleared")
|
|
128
|
+
},
|
|
129
|
+
{ scope: "note", allowInInput: true, description: "Clear the note" }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div data-slot="stack">
|
|
134
|
+
<textarea
|
|
135
|
+
aria-label="Note"
|
|
136
|
+
placeholder="Write a note…"
|
|
137
|
+
value={text}
|
|
138
|
+
onChange={(event) => setText(event.target.value)}
|
|
139
|
+
onFocus={() => shortcuts.setScope("note", true)}
|
|
140
|
+
onBlur={() => shortcuts.setScope("note", false)}
|
|
141
|
+
/>
|
|
142
|
+
<output data-slot="output">{status}</output>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Focus the note and type some text. Press Command+Enter on Mac or Control+Enter elsewhere to save it, or Escape to clear it. Move focus outside the note to turn off its shortcuts.
|
|
149
|
+
|
|
150
|
+
## Limit a Shortcut to an Area
|
|
151
|
+
|
|
152
|
+
Use a named `scope` for an editor or another part of the app. Turn it on while that area has focus:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
"use client"
|
|
156
|
+
|
|
157
|
+
import { shortcuts, useShortcut } from "@ui-sdk/react/shortcuts"
|
|
158
|
+
import { useEffect, useState } from "react"
|
|
159
|
+
|
|
160
|
+
function Note() {
|
|
161
|
+
const [text, setText] = useState("")
|
|
162
|
+
useEffect(() => () => shortcuts.setScope("note", false), [])
|
|
163
|
+
useShortcut("escape", () => setText(""), {
|
|
164
|
+
scope: "note",
|
|
165
|
+
allowInInput: true,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<textarea
|
|
170
|
+
aria-label="Note"
|
|
171
|
+
value={text}
|
|
172
|
+
onChange={(event) => setText(event.target.value)}
|
|
173
|
+
onFocus={() => shortcuts.setScope("note", true)}
|
|
174
|
+
onBlur={() => shortcuts.setScope("note", false)}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { Note }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A `when: () => text.length > 0` option can further limit when it runs. Scope names are shared, so choose one for each area you want to enable together.
|
|
183
|
+
|
|
184
|
+
## Inside a Popup
|
|
185
|
+
|
|
186
|
+
Page shortcuts pause while a modal is open. Pass `layer` to make a shortcut belong to a popup instead:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
"use client"
|
|
190
|
+
|
|
191
|
+
import { DialogContent } from "@ui-sdk/react/dialog"
|
|
192
|
+
import { useShortcut } from "@ui-sdk/react/shortcuts"
|
|
193
|
+
import { useRef } from "react"
|
|
194
|
+
|
|
195
|
+
function EditorContent({ onSave }: { onSave: () => void }) {
|
|
196
|
+
const content = useRef<HTMLDivElement>(null)
|
|
197
|
+
useShortcut("mod+enter", onSave, { layer: content, description: "Save" })
|
|
198
|
+
|
|
199
|
+
return <DialogContent ref={content}>Edit your note here.</DialogContent>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { EditorContent }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Render this part inside a Dialog. Its shortcut runs only while that dialog is open. `layer` also accepts an element or a function that returns one.
|
|
206
|
+
|
|
207
|
+
When shortcuts share the same keys, the topmost open layer wins, then the most recently registered shortcut. Development builds warn about duplicate keys in the same place.
|
|
208
|
+
|
|
209
|
+
## List Available Shortcuts
|
|
210
|
+
|
|
211
|
+
Call `shortcuts.list()` when a help screen opens. It returns the shortcuts that can run now, with their `keys`, formatted `hint`, and `description`:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { shortcuts } from "@ui-sdk/react/shortcuts"
|
|
215
|
+
|
|
216
|
+
const labels = shortcuts
|
|
217
|
+
.list()
|
|
218
|
+
.map(({ hint, description }) => `${hint} ${description ?? ""}`)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Add descriptions when registering shortcuts so the help screen can explain each action.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
Install UI SDK in your React app.
|
|
4
|
+
|
|
5
|
+
Start with an app using React 19 and React DOM 19 or newer.
|
|
6
|
+
|
|
7
|
+
## Install the Packages
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ui-sdk/react @ui-sdk/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@ui-sdk/react` provides the React components. `@ui-sdk/core` provides their behavior and is a peer dependency. Install both so the components and any helpers you use share one copy of the core.
|
|
14
|
+
|
|
15
|
+
If you use TypeScript, you'll also need `@types/react` 19.2.10 or newer.
|
|
16
|
+
|
|
17
|
+
## Import a Component
|
|
18
|
+
|
|
19
|
+
Import each component from its own entry:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { Button } from "@ui-sdk/react/button"
|
|
23
|
+
import { Toggle } from "@ui-sdk/react/toggle"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A Dialog has separate parts for its trigger, content, and title. Import those parts from the same entry:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {
|
|
30
|
+
Dialog,
|
|
31
|
+
DialogContent,
|
|
32
|
+
DialogTitle,
|
|
33
|
+
DialogTrigger,
|
|
34
|
+
} from "@ui-sdk/react/dialog"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
There is no root import from `@ui-sdk/react`. Each [component page](./README.md) lists the parts you can use.
|
|
38
|
+
|
|
39
|
+
## Add Your Styles
|
|
40
|
+
|
|
41
|
+
Components are unstyled, so you style them with your app's CSS or with Tailwind CSS. [Styling and Animation](./styling-and-animation.md) covers both.
|
|
42
|
+
|
|
43
|
+
You can render your first component without setting up a provider or importing a UI SDK stylesheet. If you use Next.js, [Server Rendering](./server-rendering.md) explains when your own components need `"use client"`.
|
|
44
|
+
|
|
45
|
+
Continue with [First Component](./first-component.md) to add a working toggle to your app.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
Use unstyled React components with keyboard, focus, and screen reader support built in.
|
|
4
|
+
|
|
5
|
+
These docs cover ui-sdk in React. Using another framework? See Vue, Svelte, Solid or Web Components. To build your own components from behaviors, see [Core](./core.md).
|
|
6
|
+
|
|
7
|
+
`@ui-sdk/react` gives you components such as Select, Dialog, and Tabs, built on ui-sdk's behaviors. They handle interactions, focus, and ARIA attributes. You choose their markup and styles.
|
|
8
|
+
|
|
9
|
+
Import the parts you need, put them together in your app, and style them with your own CSS.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Copy this prompt into your agent:
|
|
14
|
+
|
|
15
|
+
```text prompt
|
|
16
|
+
Set up ui-sdk in this React project. It needs React 19 or newer.
|
|
17
|
+
|
|
18
|
+
1. Install @ui-sdk/react and @ui-sdk/core with the project's package manager.
|
|
19
|
+
2. Add a note to AGENTS.md, creating it if needed: before using ui-sdk, follow the ui-sdk skill if it's installed, and read node_modules/@ui-sdk/react/docs/llms.txt. To build a new component, also search node_modules/@ui-sdk/react/docs/core.md by heading, and read rendering.md next to it.
|
|
20
|
+
3. If you support skills, run: npx skills add vercel-labs/ui --skill ui-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It installs ui-sdk and points your `AGENTS.md` at the docs that ship with the packages.
|
|
24
|
+
|
|
25
|
+
## Set It Up Yourself
|
|
26
|
+
|
|
27
|
+
- [Installation](./installation.md): Install the packages in your React app.
|
|
28
|
+
- [First Component](./first-component.md): Add and style your first component.
|
|
29
|
+
|
|
30
|
+
## Explore the Components
|
|
31
|
+
|
|
32
|
+
The [Using Components guides](./parts-and-composition.md) cover patterns the components share, such as composing parts, handling state, and styling.
|