@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.
Files changed (48) hide show
  1. package/README.md +11 -0
  2. package/dist/assets/llms/accordion.md +184 -0
  3. package/dist/assets/llms/alert-dialog.md +306 -0
  4. package/dist/assets/llms/autocomplete.md +756 -0
  5. package/dist/assets/llms/avatar.md +166 -0
  6. package/dist/assets/llms/badge.md +478 -0
  7. package/dist/assets/llms/button.md +238 -0
  8. package/dist/assets/llms/card.md +264 -0
  9. package/dist/assets/llms/checkbox-group.md +158 -0
  10. package/dist/assets/llms/checkbox.md +83 -0
  11. package/dist/assets/llms/collapsible.md +165 -0
  12. package/dist/assets/llms/combobox.md +1255 -0
  13. package/dist/assets/llms/context-menu.md +371 -0
  14. package/dist/assets/llms/dialog.md +592 -0
  15. package/dist/assets/llms/drawer.md +437 -0
  16. package/dist/assets/llms/field.md +74 -0
  17. package/dist/assets/llms/form.md +1931 -0
  18. package/dist/assets/llms/input.md +47 -0
  19. package/dist/assets/llms/menu.md +484 -0
  20. package/dist/assets/llms/menubar.md +804 -0
  21. package/dist/assets/llms/meter.md +181 -0
  22. package/dist/assets/llms/navigation-menu.md +187 -0
  23. package/dist/assets/llms/number-field.md +243 -0
  24. package/dist/assets/llms/pagination.md +514 -0
  25. package/dist/assets/llms/popover.md +206 -0
  26. package/dist/assets/llms/preview-card.md +146 -0
  27. package/dist/assets/llms/progress.md +60 -0
  28. package/dist/assets/llms/radio-group.md +105 -0
  29. package/dist/assets/llms/scroll-area.md +132 -0
  30. package/dist/assets/llms/select.md +276 -0
  31. package/dist/assets/llms/separator.md +49 -0
  32. package/dist/assets/llms/skeleton.md +96 -0
  33. package/dist/assets/llms/slider.md +161 -0
  34. package/dist/assets/llms/switch.md +101 -0
  35. package/dist/assets/llms/table.md +1325 -0
  36. package/dist/assets/llms/tabs.md +327 -0
  37. package/dist/assets/llms/textarea.md +38 -0
  38. package/dist/assets/llms/toast.md +349 -0
  39. package/dist/assets/llms/toggle-group.md +261 -0
  40. package/dist/assets/llms/toggle.md +161 -0
  41. package/dist/assets/llms/toolbar.md +148 -0
  42. package/dist/assets/llms/tooltip.md +486 -0
  43. package/dist/assets/llms-full.txt +14515 -0
  44. package/dist/assets/llms.txt +65 -0
  45. package/dist/index.d.mts +1 -0
  46. package/dist/index.mjs +161 -0
  47. package/dist/index.mjs.map +1 -0
  48. 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 &ldquo;{value}&rdquo;
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 &ldquo;
524
+ <Autocomplete.Value />
525
+ &ldquo;
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)