@lglab/compose-ui-mcp 0.0.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/README.md +11 -0
- package/dist/assets/llms/accordion.md +184 -0
- package/dist/assets/llms/alert-dialog.md +306 -0
- package/dist/assets/llms/autocomplete.md +756 -0
- package/dist/assets/llms/avatar.md +166 -0
- package/dist/assets/llms/badge.md +478 -0
- package/dist/assets/llms/button.md +238 -0
- package/dist/assets/llms/card.md +264 -0
- package/dist/assets/llms/checkbox-group.md +158 -0
- package/dist/assets/llms/checkbox.md +83 -0
- package/dist/assets/llms/collapsible.md +165 -0
- package/dist/assets/llms/combobox.md +1255 -0
- package/dist/assets/llms/context-menu.md +371 -0
- package/dist/assets/llms/dialog.md +592 -0
- package/dist/assets/llms/drawer.md +437 -0
- package/dist/assets/llms/field.md +74 -0
- package/dist/assets/llms/form.md +1931 -0
- package/dist/assets/llms/input.md +47 -0
- package/dist/assets/llms/menu.md +484 -0
- package/dist/assets/llms/menubar.md +804 -0
- package/dist/assets/llms/meter.md +181 -0
- package/dist/assets/llms/navigation-menu.md +187 -0
- package/dist/assets/llms/number-field.md +243 -0
- package/dist/assets/llms/pagination.md +514 -0
- package/dist/assets/llms/popover.md +206 -0
- package/dist/assets/llms/preview-card.md +146 -0
- package/dist/assets/llms/progress.md +60 -0
- package/dist/assets/llms/radio-group.md +105 -0
- package/dist/assets/llms/scroll-area.md +132 -0
- package/dist/assets/llms/select.md +276 -0
- package/dist/assets/llms/separator.md +49 -0
- package/dist/assets/llms/skeleton.md +96 -0
- package/dist/assets/llms/slider.md +161 -0
- package/dist/assets/llms/switch.md +101 -0
- package/dist/assets/llms/table.md +1325 -0
- package/dist/assets/llms/tabs.md +327 -0
- package/dist/assets/llms/textarea.md +38 -0
- package/dist/assets/llms/toast.md +349 -0
- package/dist/assets/llms/toggle-group.md +261 -0
- package/dist/assets/llms/toggle.md +161 -0
- package/dist/assets/llms/toolbar.md +148 -0
- package/dist/assets/llms/tooltip.md +486 -0
- package/dist/assets/llms-full.txt +14515 -0
- package/dist/assets/llms.txt +65 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +161 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# Autocomplete
|
|
2
|
+
|
|
3
|
+
An input field with a filterable dropdown list of suggestions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @lglab/compose-ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { BaseAutocomplete as Autocomplete, AutocompleteRoot, AutocompleteInput, AutocompletePortal, AutocompletePositioner, AutocompletePopup, AutocompleteEmpty, AutocompleteList, AutocompleteItem, AutocompleteStatus, AutocompleteGroup, AutocompleteGroupLabel, AutocompleteCollection, AutocompleteSeparator } from '@lglab/compose-ui'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
### Default
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {
|
|
23
|
+
AutocompleteEmpty,
|
|
24
|
+
AutocompleteInput,
|
|
25
|
+
AutocompleteItem,
|
|
26
|
+
AutocompleteList,
|
|
27
|
+
AutocompletePopup,
|
|
28
|
+
AutocompletePortal,
|
|
29
|
+
AutocompletePositioner,
|
|
30
|
+
AutocompleteRoot,
|
|
31
|
+
} from '@lglab/compose-ui/autocomplete'
|
|
32
|
+
import { FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
|
|
33
|
+
|
|
34
|
+
interface Component {
|
|
35
|
+
id: string
|
|
36
|
+
value: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const components: Component[] = [
|
|
40
|
+
{ id: 'accordion', value: 'Accordion' },
|
|
41
|
+
{ id: 'alert-dialog', value: 'Alert Dialog' },
|
|
42
|
+
{ id: 'autocomplete', value: 'Autocomplete' },
|
|
43
|
+
{ id: 'avatar', value: 'Avatar' },
|
|
44
|
+
{ id: 'checkbox', value: 'Checkbox' },
|
|
45
|
+
{ id: 'checkbox-group', value: 'Checkbox Group' },
|
|
46
|
+
{ id: 'collapsible', value: 'Collapsible' },
|
|
47
|
+
{ id: 'combobox', value: 'Combobox' },
|
|
48
|
+
{ id: 'context-menu', value: 'Context Menu' },
|
|
49
|
+
{ id: 'dialog', value: 'Dialog' },
|
|
50
|
+
{ id: 'field', value: 'Field' },
|
|
51
|
+
{ id: 'fieldset', value: 'Fieldset' },
|
|
52
|
+
{ id: 'form', value: 'Form' },
|
|
53
|
+
{ id: 'input', value: 'Input' },
|
|
54
|
+
{ id: 'menu', value: 'Menu' },
|
|
55
|
+
{ id: 'menubar', value: 'Menubar' },
|
|
56
|
+
{ id: 'meter', value: 'Meter' },
|
|
57
|
+
{ id: 'navigation-menu', value: 'Navigation Menu' },
|
|
58
|
+
{ id: 'number-field', value: 'Number Field' },
|
|
59
|
+
{ id: 'popover', value: 'Popover' },
|
|
60
|
+
{ id: 'preview-card', value: 'Preview Card' },
|
|
61
|
+
{ id: 'progress', value: 'Progress' },
|
|
62
|
+
{ id: 'radio', value: 'Radio' },
|
|
63
|
+
{ id: 'scroll-area', value: 'Scroll Area' },
|
|
64
|
+
{ id: 'select', value: 'Select' },
|
|
65
|
+
{ id: 'separator', value: 'Separator' },
|
|
66
|
+
{ id: 'slider', value: 'Slider' },
|
|
67
|
+
{ id: 'switch', value: 'Switch' },
|
|
68
|
+
{ id: 'tabs', value: 'Tabs' },
|
|
69
|
+
{ id: 'toast', value: 'Toast' },
|
|
70
|
+
{ id: 'toggle', value: 'Toggle' },
|
|
71
|
+
{ id: 'toggle-group', value: 'Toggle Group' },
|
|
72
|
+
{ id: 'toolbar', value: 'Toolbar' },
|
|
73
|
+
{ id: 'tooltip', value: 'Tooltip' },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
export default function DefaultExample() {
|
|
77
|
+
return (
|
|
78
|
+
<FieldRoot>
|
|
79
|
+
<FieldLabel>Search components</FieldLabel>
|
|
80
|
+
<AutocompleteRoot items={components}>
|
|
81
|
+
<AutocompleteInput placeholder='e.g. Accordion' />
|
|
82
|
+
<AutocompletePortal>
|
|
83
|
+
<AutocompletePositioner>
|
|
84
|
+
<AutocompletePopup>
|
|
85
|
+
<AutocompleteEmpty>No components found.</AutocompleteEmpty>
|
|
86
|
+
<AutocompleteList>
|
|
87
|
+
{(component: Component) => (
|
|
88
|
+
<AutocompleteItem key={component.id} value={component}>
|
|
89
|
+
{component.value}
|
|
90
|
+
</AutocompleteItem>
|
|
91
|
+
)}
|
|
92
|
+
</AutocompleteList>
|
|
93
|
+
</AutocompletePopup>
|
|
94
|
+
</AutocompletePositioner>
|
|
95
|
+
</AutocompletePortal>
|
|
96
|
+
</AutocompleteRoot>
|
|
97
|
+
</FieldRoot>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Inline
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import {
|
|
106
|
+
AutocompleteInput,
|
|
107
|
+
AutocompleteItem,
|
|
108
|
+
AutocompleteList,
|
|
109
|
+
AutocompletePopup,
|
|
110
|
+
AutocompletePortal,
|
|
111
|
+
AutocompletePositioner,
|
|
112
|
+
AutocompleteRoot,
|
|
113
|
+
} from '@lglab/compose-ui/autocomplete'
|
|
114
|
+
import { FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
|
|
115
|
+
|
|
116
|
+
interface Component {
|
|
117
|
+
id: string
|
|
118
|
+
value: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const components: Component[] = [
|
|
122
|
+
{ id: 'accordion', value: 'Accordion' },
|
|
123
|
+
{ id: 'alert-dialog', value: 'Alert Dialog' },
|
|
124
|
+
{ id: 'autocomplete', value: 'Autocomplete' },
|
|
125
|
+
{ id: 'avatar', value: 'Avatar' },
|
|
126
|
+
{ id: 'checkbox', value: 'Checkbox' },
|
|
127
|
+
{ id: 'checkbox-group', value: 'Checkbox Group' },
|
|
128
|
+
{ id: 'collapsible', value: 'Collapsible' },
|
|
129
|
+
{ id: 'combobox', value: 'Combobox' },
|
|
130
|
+
{ id: 'context-menu', value: 'Context Menu' },
|
|
131
|
+
{ id: 'dialog', value: 'Dialog' },
|
|
132
|
+
{ id: 'field', value: 'Field' },
|
|
133
|
+
{ id: 'fieldset', value: 'Fieldset' },
|
|
134
|
+
{ id: 'form', value: 'Form' },
|
|
135
|
+
{ id: 'input', value: 'Input' },
|
|
136
|
+
{ id: 'menu', value: 'Menu' },
|
|
137
|
+
{ id: 'menubar', value: 'Menubar' },
|
|
138
|
+
{ id: 'meter', value: 'Meter' },
|
|
139
|
+
{ id: 'navigation-menu', value: 'Navigation Menu' },
|
|
140
|
+
{ id: 'number-field', value: 'Number Field' },
|
|
141
|
+
{ id: 'popover', value: 'Popover' },
|
|
142
|
+
{ id: 'preview-card', value: 'Preview Card' },
|
|
143
|
+
{ id: 'progress', value: 'Progress' },
|
|
144
|
+
{ id: 'radio', value: 'Radio' },
|
|
145
|
+
{ id: 'scroll-area', value: 'Scroll Area' },
|
|
146
|
+
{ id: 'select', value: 'Select' },
|
|
147
|
+
{ id: 'separator', value: 'Separator' },
|
|
148
|
+
{ id: 'slider', value: 'Slider' },
|
|
149
|
+
{ id: 'switch', value: 'Switch' },
|
|
150
|
+
{ id: 'tabs', value: 'Tabs' },
|
|
151
|
+
{ id: 'toast', value: 'Toast' },
|
|
152
|
+
{ id: 'toggle', value: 'Toggle' },
|
|
153
|
+
{ id: 'toggle-group', value: 'Toggle Group' },
|
|
154
|
+
{ id: 'toolbar', value: 'Toolbar' },
|
|
155
|
+
{ id: 'tooltip', value: 'Tooltip' },
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
export default function InlineExample() {
|
|
159
|
+
return (
|
|
160
|
+
<FieldRoot>
|
|
161
|
+
<FieldLabel>Search components</FieldLabel>
|
|
162
|
+
<AutocompleteRoot items={components} mode='both'>
|
|
163
|
+
<AutocompleteInput placeholder='e.g. Accordion' />
|
|
164
|
+
<AutocompletePortal>
|
|
165
|
+
<AutocompletePositioner className='data-empty:hidden'>
|
|
166
|
+
<AutocompletePopup>
|
|
167
|
+
<AutocompleteList>
|
|
168
|
+
{(component: Component) => (
|
|
169
|
+
<AutocompleteItem key={component.id} value={component}>
|
|
170
|
+
{component.value}
|
|
171
|
+
</AutocompleteItem>
|
|
172
|
+
)}
|
|
173
|
+
</AutocompleteList>
|
|
174
|
+
</AutocompletePopup>
|
|
175
|
+
</AutocompletePositioner>
|
|
176
|
+
</AutocompletePortal>
|
|
177
|
+
</AutocompleteRoot>
|
|
178
|
+
</FieldRoot>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Grouped
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import {
|
|
187
|
+
AutocompleteCollection,
|
|
188
|
+
AutocompleteEmpty,
|
|
189
|
+
AutocompleteGroup,
|
|
190
|
+
AutocompleteGroupLabel,
|
|
191
|
+
AutocompleteInput,
|
|
192
|
+
AutocompleteItem,
|
|
193
|
+
AutocompleteList,
|
|
194
|
+
AutocompletePopup,
|
|
195
|
+
AutocompletePortal,
|
|
196
|
+
AutocompletePositioner,
|
|
197
|
+
AutocompleteRoot,
|
|
198
|
+
AutocompleteSeparator,
|
|
199
|
+
} from '@lglab/compose-ui/autocomplete'
|
|
200
|
+
import { FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
|
|
201
|
+
import * as React from 'react'
|
|
202
|
+
|
|
203
|
+
interface Produce {
|
|
204
|
+
id: string
|
|
205
|
+
label: string
|
|
206
|
+
group: 'Fruits' | 'Vegetables' | 'Grains'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface ProduceGroup {
|
|
210
|
+
value: string
|
|
211
|
+
items: Produce[]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const produceData: Produce[] = [
|
|
215
|
+
{ id: 'fruit-apple', label: 'Apple', group: 'Fruits' },
|
|
216
|
+
{ id: 'fruit-banana', label: 'Banana', group: 'Fruits' },
|
|
217
|
+
{ id: 'fruit-mango', label: 'Mango', group: 'Fruits' },
|
|
218
|
+
{ id: 'fruit-kiwi', label: 'Kiwi', group: 'Fruits' },
|
|
219
|
+
{ id: 'veg-broccoli', label: 'Broccoli', group: 'Vegetables' },
|
|
220
|
+
{ id: 'veg-carrot', label: 'Carrot', group: 'Vegetables' },
|
|
221
|
+
{ id: 'veg-cauliflower', label: 'Cauliflower', group: 'Vegetables' },
|
|
222
|
+
{ id: 'veg-cucumber', label: 'Cucumber', group: 'Vegetables' },
|
|
223
|
+
{ id: 'veg-kale', label: 'Kale', group: 'Vegetables' },
|
|
224
|
+
{ id: 'veg-pepper', label: 'Bell pepper', group: 'Vegetables' },
|
|
225
|
+
{ id: 'veg-spinach', label: 'Spinach', group: 'Vegetables' },
|
|
226
|
+
{ id: 'veg-zucchini', label: 'Zucchini', group: 'Vegetables' },
|
|
227
|
+
{ id: 'grain-rice', label: 'Rice', group: 'Grains' },
|
|
228
|
+
{ id: 'grain-wheat', label: 'Wheat', group: 'Grains' },
|
|
229
|
+
{ id: 'grain-oats', label: 'Oats', group: 'Grains' },
|
|
230
|
+
{ id: 'grain-barley', label: 'Barley', group: 'Grains' },
|
|
231
|
+
{ id: 'grain-quinoa', label: 'Quinoa', group: 'Grains' },
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
function groupProduce(items: Produce[]): ProduceGroup[] {
|
|
235
|
+
const groups: Record<string, Produce[]> = {}
|
|
236
|
+
items.forEach((item) => {
|
|
237
|
+
;(groups[item.group] ??= []).push(item)
|
|
238
|
+
})
|
|
239
|
+
const order = ['Fruits', 'Vegetables', 'Grains']
|
|
240
|
+
return order.map((value) => ({ value, items: groups[value] ?? [] }))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const groupedProduce: ProduceGroup[] = groupProduce(produceData)
|
|
244
|
+
|
|
245
|
+
export default function GroupedExample() {
|
|
246
|
+
return (
|
|
247
|
+
<FieldRoot>
|
|
248
|
+
<FieldLabel>Search produce</FieldLabel>
|
|
249
|
+
<AutocompleteRoot items={groupedProduce}>
|
|
250
|
+
<AutocompleteInput placeholder='e.g. Mango' />
|
|
251
|
+
<AutocompletePortal>
|
|
252
|
+
<AutocompletePositioner>
|
|
253
|
+
<AutocompletePopup>
|
|
254
|
+
<AutocompleteEmpty>No produce found.</AutocompleteEmpty>
|
|
255
|
+
<AutocompleteList>
|
|
256
|
+
{(group: ProduceGroup, index: number) => (
|
|
257
|
+
<React.Fragment key={group.value}>
|
|
258
|
+
<AutocompleteGroup items={group.items}>
|
|
259
|
+
<AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
|
|
260
|
+
<AutocompleteCollection>
|
|
261
|
+
{(item: Produce) => (
|
|
262
|
+
<AutocompleteItem key={item.id} value={item}>
|
|
263
|
+
{item.label}
|
|
264
|
+
</AutocompleteItem>
|
|
265
|
+
)}
|
|
266
|
+
</AutocompleteCollection>
|
|
267
|
+
</AutocompleteGroup>
|
|
268
|
+
{index < groupedProduce.length - 1 && <AutocompleteSeparator />}
|
|
269
|
+
</React.Fragment>
|
|
270
|
+
)}
|
|
271
|
+
</AutocompleteList>
|
|
272
|
+
</AutocompletePopup>
|
|
273
|
+
</AutocompletePositioner>
|
|
274
|
+
</AutocompletePortal>
|
|
275
|
+
</AutocompleteRoot>
|
|
276
|
+
</FieldRoot>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Limit Results
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import {
|
|
285
|
+
Autocomplete,
|
|
286
|
+
AutocompleteEmpty,
|
|
287
|
+
AutocompleteInput,
|
|
288
|
+
AutocompleteItem,
|
|
289
|
+
AutocompleteList,
|
|
290
|
+
AutocompletePopup,
|
|
291
|
+
AutocompletePortal,
|
|
292
|
+
AutocompletePositioner,
|
|
293
|
+
AutocompleteRoot,
|
|
294
|
+
AutocompleteStatus,
|
|
295
|
+
} from '@lglab/compose-ui/autocomplete'
|
|
296
|
+
import { FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
|
|
297
|
+
import { useMemo, useState } from 'react'
|
|
298
|
+
|
|
299
|
+
interface Component {
|
|
300
|
+
id: string
|
|
301
|
+
value: string
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const limit = 8
|
|
305
|
+
|
|
306
|
+
const components: Component[] = [
|
|
307
|
+
{ id: 'accordion', value: 'Accordion' },
|
|
308
|
+
{ id: 'alert-dialog', value: 'Alert Dialog' },
|
|
309
|
+
{ id: 'autocomplete', value: 'Autocomplete' },
|
|
310
|
+
{ id: 'avatar', value: 'Avatar' },
|
|
311
|
+
{ id: 'checkbox', value: 'Checkbox' },
|
|
312
|
+
{ id: 'checkbox-group', value: 'Checkbox Group' },
|
|
313
|
+
{ id: 'collapsible', value: 'Collapsible' },
|
|
314
|
+
{ id: 'combobox', value: 'Combobox' },
|
|
315
|
+
{ id: 'context-menu', value: 'Context Menu' },
|
|
316
|
+
{ id: 'dialog', value: 'Dialog' },
|
|
317
|
+
{ id: 'field', value: 'Field' },
|
|
318
|
+
{ id: 'fieldset', value: 'Fieldset' },
|
|
319
|
+
{ id: 'form', value: 'Form' },
|
|
320
|
+
{ id: 'input', value: 'Input' },
|
|
321
|
+
{ id: 'menu', value: 'Menu' },
|
|
322
|
+
{ id: 'menubar', value: 'Menubar' },
|
|
323
|
+
{ id: 'meter', value: 'Meter' },
|
|
324
|
+
{ id: 'navigation-menu', value: 'Navigation Menu' },
|
|
325
|
+
{ id: 'number-field', value: 'Number Field' },
|
|
326
|
+
{ id: 'popover', value: 'Popover' },
|
|
327
|
+
{ id: 'preview-card', value: 'Preview Card' },
|
|
328
|
+
{ id: 'progress', value: 'Progress' },
|
|
329
|
+
{ id: 'radio', value: 'Radio' },
|
|
330
|
+
{ id: 'scroll-area', value: 'Scroll Area' },
|
|
331
|
+
{ id: 'select', value: 'Select' },
|
|
332
|
+
{ id: 'separator', value: 'Separator' },
|
|
333
|
+
{ id: 'slider', value: 'Slider' },
|
|
334
|
+
{ id: 'switch', value: 'Switch' },
|
|
335
|
+
{ id: 'tabs', value: 'Tabs' },
|
|
336
|
+
{ id: 'toast', value: 'Toast' },
|
|
337
|
+
{ id: 'toggle', value: 'Toggle' },
|
|
338
|
+
{ id: 'toggle-group', value: 'Toggle Group' },
|
|
339
|
+
{ id: 'toolbar', value: 'Toolbar' },
|
|
340
|
+
{ id: 'tooltip', value: 'Tooltip' },
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
export default function LimitResultsExample() {
|
|
344
|
+
const [value, setValue] = useState('')
|
|
345
|
+
|
|
346
|
+
const { contains } = Autocomplete.useFilter({ sensitivity: 'base' })
|
|
347
|
+
|
|
348
|
+
const totalMatches = useMemo(() => {
|
|
349
|
+
const trimmed = value.trim()
|
|
350
|
+
if (!trimmed) {
|
|
351
|
+
return components.length
|
|
352
|
+
}
|
|
353
|
+
return components.filter((c) => contains(c.value, trimmed)).length
|
|
354
|
+
}, [value, contains])
|
|
355
|
+
|
|
356
|
+
const moreCount = Math.max(0, totalMatches - limit)
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<FieldRoot>
|
|
360
|
+
<FieldLabel>Search components (limit 8)</FieldLabel>
|
|
361
|
+
<AutocompleteRoot
|
|
362
|
+
items={components}
|
|
363
|
+
value={value}
|
|
364
|
+
onValueChange={setValue}
|
|
365
|
+
limit={limit}
|
|
366
|
+
>
|
|
367
|
+
<AutocompleteInput placeholder='e.g. Dialog' />
|
|
368
|
+
<AutocompletePortal>
|
|
369
|
+
<AutocompletePositioner>
|
|
370
|
+
<AutocompletePopup>
|
|
371
|
+
<AutocompleteEmpty>
|
|
372
|
+
No results found for “{value}”
|
|
373
|
+
</AutocompleteEmpty>
|
|
374
|
+
<AutocompleteList>
|
|
375
|
+
{(component: Component) => (
|
|
376
|
+
<AutocompleteItem key={component.id} value={component}>
|
|
377
|
+
{component.value}
|
|
378
|
+
</AutocompleteItem>
|
|
379
|
+
)}
|
|
380
|
+
</AutocompleteList>
|
|
381
|
+
<AutocompleteStatus>
|
|
382
|
+
{moreCount > 0
|
|
383
|
+
? `Hiding ${moreCount} results (type a more specific query)`
|
|
384
|
+
: null}
|
|
385
|
+
</AutocompleteStatus>
|
|
386
|
+
</AutocompletePopup>
|
|
387
|
+
</AutocompletePositioner>
|
|
388
|
+
</AutocompletePortal>
|
|
389
|
+
</AutocompleteRoot>
|
|
390
|
+
</FieldRoot>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Fuzzy Matching
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
import {
|
|
399
|
+
Autocomplete,
|
|
400
|
+
AutocompleteEmpty,
|
|
401
|
+
AutocompleteInput,
|
|
402
|
+
AutocompleteItem,
|
|
403
|
+
AutocompleteList,
|
|
404
|
+
AutocompletePopup,
|
|
405
|
+
AutocompletePortal,
|
|
406
|
+
AutocompletePositioner,
|
|
407
|
+
AutocompleteRoot,
|
|
408
|
+
} from '@lglab/compose-ui/autocomplete'
|
|
409
|
+
import { FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
|
|
410
|
+
import { matchSorter } from 'match-sorter'
|
|
411
|
+
import type { ReactNode } from 'react'
|
|
412
|
+
|
|
413
|
+
interface Movie {
|
|
414
|
+
id: string
|
|
415
|
+
title: string
|
|
416
|
+
year: number
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function highlightText(text: string, query: string): ReactNode {
|
|
420
|
+
const trimmed = query.trim()
|
|
421
|
+
if (!trimmed) {
|
|
422
|
+
return text
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const limited = trimmed.slice(0, 100)
|
|
426
|
+
const escaped = limited.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
427
|
+
const regex = new RegExp(`(${escaped})`, 'gi')
|
|
428
|
+
|
|
429
|
+
return text.split(regex).map((part, idx) =>
|
|
430
|
+
regex.test(part) ? (
|
|
431
|
+
<mark key={idx} className='bg-transparent font-bold text-primary'>
|
|
432
|
+
{part}
|
|
433
|
+
</mark>
|
|
434
|
+
) : (
|
|
435
|
+
part
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function fuzzyFilter(item: Movie, query: string): boolean {
|
|
441
|
+
if (!query) {
|
|
442
|
+
return true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const results = matchSorter([item], query, {
|
|
446
|
+
keys: [
|
|
447
|
+
'title',
|
|
448
|
+
{ key: 'title', threshold: matchSorter.rankings.CONTAINS },
|
|
449
|
+
{ key: 'year', threshold: matchSorter.rankings.CONTAINS },
|
|
450
|
+
],
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return results.length > 0
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const movies: Movie[] = [
|
|
457
|
+
{ id: '1', title: 'The Shawshank Redemption', year: 1994 },
|
|
458
|
+
{ id: '2', title: 'The Godfather', year: 1972 },
|
|
459
|
+
{ id: '3', title: 'The Dark Knight', year: 2008 },
|
|
460
|
+
{ id: '4', title: 'The Godfather Part II', year: 1974 },
|
|
461
|
+
{ id: '5', title: '12 Angry Men', year: 1957 },
|
|
462
|
+
{ id: '6', title: 'The Lord of the Rings: The Return of the King', year: 2003 },
|
|
463
|
+
{ id: '7', title: "Schindler's List", year: 1993 },
|
|
464
|
+
{ id: '8', title: 'Pulp Fiction', year: 1994 },
|
|
465
|
+
{ id: '9', title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
|
|
466
|
+
{ id: '10', title: 'The Good, the Bad and the Ugly', year: 1966 },
|
|
467
|
+
{ id: '11', title: 'Forrest Gump', year: 1994 },
|
|
468
|
+
{ id: '12', title: 'The Lord of the Rings: The Two Towers', year: 2002 },
|
|
469
|
+
{ id: '13', title: 'Fight Club', year: 1999 },
|
|
470
|
+
{ id: '14', title: 'Inception', year: 2010 },
|
|
471
|
+
{ id: '15', title: 'Star Wars: Episode V - The Empire Strikes Back', year: 1980 },
|
|
472
|
+
{ id: '16', title: 'The Matrix', year: 1999 },
|
|
473
|
+
{ id: '17', title: 'Goodfellas', year: 1990 },
|
|
474
|
+
{ id: '18', title: 'Interstellar', year: 2014 },
|
|
475
|
+
{ id: '19', title: "One Flew Over the Cuckoo's Nest", year: 1975 },
|
|
476
|
+
{ id: '20', title: 'Se7en', year: 1995 },
|
|
477
|
+
{ id: '21', title: "It's a Wonderful Life", year: 1946 },
|
|
478
|
+
{ id: '22', title: 'The Silence of the Lambs', year: 1991 },
|
|
479
|
+
{ id: '23', title: 'Seven Samurai', year: 1954 },
|
|
480
|
+
{ id: '24', title: 'Saving Private Ryan', year: 1998 },
|
|
481
|
+
{ id: '25', title: 'City of God', year: 2002 },
|
|
482
|
+
{ id: '26', title: 'Life Is Beautiful', year: 1997 },
|
|
483
|
+
{ id: '27', title: 'The Green Mile', year: 1999 },
|
|
484
|
+
{ id: '28', title: 'Star Wars: Episode IV - A New Hope', year: 1977 },
|
|
485
|
+
{ id: '29', title: 'Terminator 2: Judgment Day', year: 1991 },
|
|
486
|
+
{ id: '30', title: 'Back to the Future', year: 1985 },
|
|
487
|
+
{ id: '31', title: 'Spirited Away', year: 2001 },
|
|
488
|
+
{ id: '32', title: 'The Pianist', year: 2002 },
|
|
489
|
+
{ id: '33', title: 'Psycho', year: 1960 },
|
|
490
|
+
{ id: '34', title: 'Parasite', year: 2019 },
|
|
491
|
+
{ id: '35', title: 'Gladiator', year: 2000 },
|
|
492
|
+
{ id: '36', title: 'Leon: The Professional', year: 1994 },
|
|
493
|
+
{ id: '37', title: 'American History X', year: 1998 },
|
|
494
|
+
{ id: '38', title: 'The Departed', year: 2006 },
|
|
495
|
+
{ id: '39', title: 'Whiplash', year: 2014 },
|
|
496
|
+
{ id: '40', title: 'The Prestige', year: 2006 },
|
|
497
|
+
{ id: '41', title: 'Grave of the Fireflies', year: 1988 },
|
|
498
|
+
{ id: '42', title: 'The Usual Suspects', year: 1995 },
|
|
499
|
+
{ id: '43', title: 'Casablanca', year: 1942 },
|
|
500
|
+
{ id: '44', title: 'Harakiri', year: 1962 },
|
|
501
|
+
{ id: '45', title: 'The Lion King', year: 1994 },
|
|
502
|
+
{ id: '46', title: 'The Intouchables', year: 2011 },
|
|
503
|
+
{ id: '47', title: 'Modern Times', year: 1936 },
|
|
504
|
+
{ id: '48', title: 'The Lives of Others', year: 2006 },
|
|
505
|
+
{ id: '49', title: 'Once Upon a Time in the West', year: 1968 },
|
|
506
|
+
{ id: '50', title: 'Rear Window', year: 1954 },
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
export default function FuzzyMatchingExample() {
|
|
510
|
+
return (
|
|
511
|
+
<FieldRoot className='w-80'>
|
|
512
|
+
<FieldLabel>Search movies</FieldLabel>
|
|
513
|
+
<AutocompleteRoot
|
|
514
|
+
items={movies}
|
|
515
|
+
filter={fuzzyFilter}
|
|
516
|
+
itemToStringValue={(item) => item.title}
|
|
517
|
+
>
|
|
518
|
+
<AutocompleteInput placeholder='e.g. Pulp Fiction or 1994' />
|
|
519
|
+
<AutocompletePortal>
|
|
520
|
+
<AutocompletePositioner>
|
|
521
|
+
<AutocompletePopup>
|
|
522
|
+
<AutocompleteEmpty>
|
|
523
|
+
No results found for “
|
|
524
|
+
<Autocomplete.Value />
|
|
525
|
+
“
|
|
526
|
+
</AutocompleteEmpty>
|
|
527
|
+
<AutocompleteList>
|
|
528
|
+
{(movie: Movie) => (
|
|
529
|
+
<AutocompleteItem key={movie.id} value={movie}>
|
|
530
|
+
<Autocomplete.Value>
|
|
531
|
+
{(value) => (
|
|
532
|
+
<div className='flex w-full flex-col gap-0.5'>
|
|
533
|
+
<div className='font-medium'>
|
|
534
|
+
{highlightText(movie.title, value)}
|
|
535
|
+
</div>
|
|
536
|
+
<div className='text-xs text-muted-foreground'>
|
|
537
|
+
{highlightText(movie.year.toString(), value)}
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
</Autocomplete.Value>
|
|
542
|
+
</AutocompleteItem>
|
|
543
|
+
)}
|
|
544
|
+
</AutocompleteList>
|
|
545
|
+
</AutocompletePopup>
|
|
546
|
+
</AutocompletePositioner>
|
|
547
|
+
</AutocompletePortal>
|
|
548
|
+
</AutocompleteRoot>
|
|
549
|
+
</FieldRoot>
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Async Search
|
|
555
|
+
|
|
556
|
+
```tsx
|
|
557
|
+
import {
|
|
558
|
+
Autocomplete,
|
|
559
|
+
AutocompleteInput,
|
|
560
|
+
AutocompleteItem,
|
|
561
|
+
AutocompleteList,
|
|
562
|
+
AutocompletePopup,
|
|
563
|
+
AutocompletePortal,
|
|
564
|
+
AutocompletePositioner,
|
|
565
|
+
AutocompleteRoot,
|
|
566
|
+
AutocompleteStatus,
|
|
567
|
+
} from '@lglab/compose-ui/autocomplete'
|
|
568
|
+
import { FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
|
|
569
|
+
import { Loader2 } from 'lucide-react'
|
|
570
|
+
import { useRef, useState, useTransition } from 'react'
|
|
571
|
+
|
|
572
|
+
interface Movie {
|
|
573
|
+
id: string
|
|
574
|
+
title: string
|
|
575
|
+
year: number
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export default function AsyncSearchExample() {
|
|
579
|
+
const [searchValue, setSearchValue] = useState('')
|
|
580
|
+
const [searchResults, setSearchResults] = useState<Movie[]>([])
|
|
581
|
+
const [error, setError] = useState<string | null>(null)
|
|
582
|
+
const [isPending, startTransition] = useTransition()
|
|
583
|
+
|
|
584
|
+
const { contains } = Autocomplete.useFilter()
|
|
585
|
+
|
|
586
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
587
|
+
|
|
588
|
+
function getStatus() {
|
|
589
|
+
if (isPending) {
|
|
590
|
+
return (
|
|
591
|
+
<>
|
|
592
|
+
<Loader2 className='size-3 animate-spin' />
|
|
593
|
+
Searching...
|
|
594
|
+
</>
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (error) {
|
|
599
|
+
return error
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (searchValue === '') {
|
|
603
|
+
return null
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (searchResults.length === 0) {
|
|
607
|
+
return `Movie or year "${searchValue}" does not exist`
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return `${searchResults.length} result${searchResults.length === 1 ? '' : 's'} found`
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const status = getStatus()
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<FieldRoot className='w-80'>
|
|
617
|
+
<FieldLabel>Search movies by name or year</FieldLabel>
|
|
618
|
+
<AutocompleteRoot
|
|
619
|
+
items={searchResults}
|
|
620
|
+
value={searchValue}
|
|
621
|
+
onValueChange={(nextSearchValue) => {
|
|
622
|
+
setSearchValue(nextSearchValue)
|
|
623
|
+
|
|
624
|
+
const controller = new AbortController()
|
|
625
|
+
abortControllerRef.current?.abort()
|
|
626
|
+
abortControllerRef.current = controller
|
|
627
|
+
|
|
628
|
+
if (nextSearchValue === '') {
|
|
629
|
+
setSearchResults([])
|
|
630
|
+
setError(null)
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
startTransition(async () => {
|
|
635
|
+
setError(null)
|
|
636
|
+
|
|
637
|
+
const result = await searchMovies(nextSearchValue, contains)
|
|
638
|
+
if (controller.signal.aborted) {
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
startTransition(() => {
|
|
643
|
+
setSearchResults(result.movies)
|
|
644
|
+
setError(result.error)
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
}}
|
|
648
|
+
itemToStringValue={(item) => item.title}
|
|
649
|
+
filter={null}
|
|
650
|
+
>
|
|
651
|
+
<AutocompleteInput placeholder='e.g. Pulp Fiction or 1994' />
|
|
652
|
+
<AutocompletePortal hidden={!status}>
|
|
653
|
+
<AutocompletePositioner align='start'>
|
|
654
|
+
<AutocompletePopup aria-busy={isPending || undefined}>
|
|
655
|
+
<AutocompleteStatus>{status}</AutocompleteStatus>
|
|
656
|
+
<AutocompleteList>
|
|
657
|
+
{(movie: Movie) => (
|
|
658
|
+
<AutocompleteItem key={movie.id} value={movie}>
|
|
659
|
+
<div className='flex w-full flex-col gap-0.5'>
|
|
660
|
+
<div className='font-medium'>{movie.title}</div>
|
|
661
|
+
<div className='text-xs text-muted-foreground'>{movie.year}</div>
|
|
662
|
+
</div>
|
|
663
|
+
</AutocompleteItem>
|
|
664
|
+
)}
|
|
665
|
+
</AutocompleteList>
|
|
666
|
+
</AutocompletePopup>
|
|
667
|
+
</AutocompletePositioner>
|
|
668
|
+
</AutocompletePortal>
|
|
669
|
+
</AutocompleteRoot>
|
|
670
|
+
</FieldRoot>
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function searchMovies(
|
|
675
|
+
query: string,
|
|
676
|
+
filter: (item: string, query: string) => boolean,
|
|
677
|
+
): Promise<{ movies: Movie[]; error: string | null }> {
|
|
678
|
+
await new Promise((resolve) => {
|
|
679
|
+
setTimeout(resolve, Math.random() * 500 + 100)
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
if (Math.random() < 0.01 || query === 'will_error') {
|
|
683
|
+
return {
|
|
684
|
+
movies: [],
|
|
685
|
+
error: 'Failed to fetch movies. Please try again.',
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const movies = top100Movies.filter(
|
|
690
|
+
(movie) => filter(movie.title, query) || filter(movie.year.toString(), query),
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
movies,
|
|
695
|
+
error: null,
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const top100Movies: Movie[] = [
|
|
700
|
+
{ id: '1', title: 'The Shawshank Redemption', year: 1994 },
|
|
701
|
+
{ id: '2', title: 'The Godfather', year: 1972 },
|
|
702
|
+
{ id: '3', title: 'The Dark Knight', year: 2008 },
|
|
703
|
+
{ id: '4', title: 'The Godfather Part II', year: 1974 },
|
|
704
|
+
{ id: '5', title: '12 Angry Men', year: 1957 },
|
|
705
|
+
{ id: '6', title: 'The Lord of the Rings: The Return of the King', year: 2003 },
|
|
706
|
+
{ id: '7', title: "Schindler's List", year: 1993 },
|
|
707
|
+
{ id: '8', title: 'Pulp Fiction', year: 1994 },
|
|
708
|
+
{ id: '9', title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
|
|
709
|
+
{ id: '10', title: 'The Good, the Bad and the Ugly', year: 1966 },
|
|
710
|
+
{ id: '11', title: 'Forrest Gump', year: 1994 },
|
|
711
|
+
{ id: '12', title: 'The Lord of the Rings: The Two Towers', year: 2002 },
|
|
712
|
+
{ id: '13', title: 'Fight Club', year: 1999 },
|
|
713
|
+
{ id: '14', title: 'Inception', year: 2010 },
|
|
714
|
+
{ id: '15', title: 'Star Wars: Episode V - The Empire Strikes Back', year: 1980 },
|
|
715
|
+
{ id: '16', title: 'The Matrix', year: 1999 },
|
|
716
|
+
{ id: '17', title: 'Goodfellas', year: 1990 },
|
|
717
|
+
{ id: '18', title: 'Interstellar', year: 2014 },
|
|
718
|
+
{ id: '19', title: "One Flew Over the Cuckoo's Nest", year: 1975 },
|
|
719
|
+
{ id: '20', title: 'Se7en', year: 1995 },
|
|
720
|
+
{ id: '21', title: "It's a Wonderful Life", year: 1946 },
|
|
721
|
+
{ id: '22', title: 'The Silence of the Lambs', year: 1991 },
|
|
722
|
+
{ id: '23', title: 'Seven Samurai', year: 1954 },
|
|
723
|
+
{ id: '24', title: 'Saving Private Ryan', year: 1998 },
|
|
724
|
+
{ id: '25', title: 'City of God', year: 2002 },
|
|
725
|
+
{ id: '26', title: 'Life Is Beautiful', year: 1997 },
|
|
726
|
+
{ id: '27', title: 'The Green Mile', year: 1999 },
|
|
727
|
+
{ id: '28', title: 'Star Wars: Episode IV - A New Hope', year: 1977 },
|
|
728
|
+
{ id: '29', title: 'Terminator 2: Judgment Day', year: 1991 },
|
|
729
|
+
{ id: '30', title: 'Back to the Future', year: 1985 },
|
|
730
|
+
{ id: '31', title: 'Spirited Away', year: 2001 },
|
|
731
|
+
{ id: '32', title: 'The Pianist', year: 2002 },
|
|
732
|
+
{ id: '33', title: 'Psycho', year: 1960 },
|
|
733
|
+
{ id: '34', title: 'Parasite', year: 2019 },
|
|
734
|
+
{ id: '35', title: 'Gladiator', year: 2000 },
|
|
735
|
+
{ id: '36', title: 'Leon: The Professional', year: 1994 },
|
|
736
|
+
{ id: '37', title: 'American History X', year: 1998 },
|
|
737
|
+
{ id: '38', title: 'The Departed', year: 2006 },
|
|
738
|
+
{ id: '39', title: 'Whiplash', year: 2014 },
|
|
739
|
+
{ id: '40', title: 'The Prestige', year: 2006 },
|
|
740
|
+
{ id: '41', title: 'Grave of the Fireflies', year: 1988 },
|
|
741
|
+
{ id: '42', title: 'The Usual Suspects', year: 1995 },
|
|
742
|
+
{ id: '43', title: 'Casablanca', year: 1942 },
|
|
743
|
+
{ id: '44', title: 'Harakiri', year: 1962 },
|
|
744
|
+
{ id: '45', title: 'The Lion King', year: 1994 },
|
|
745
|
+
{ id: '46', title: 'The Intouchables', year: 2011 },
|
|
746
|
+
{ id: '47', title: 'Modern Times', year: 1936 },
|
|
747
|
+
{ id: '48', title: 'The Lives of Others', year: 2006 },
|
|
748
|
+
{ id: '49', title: 'Once Upon a Time in the West', year: 1968 },
|
|
749
|
+
{ id: '50', title: 'Rear Window', year: 1954 },
|
|
750
|
+
]
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
## Resources
|
|
754
|
+
|
|
755
|
+
- [Base UI Autocomplete Documentation](https://base-ui.com/react/components/autocomplete)
|
|
756
|
+
- [API Reference](https://base-ui.com/react/components/autocomplete#api-reference)
|