@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,1325 @@
1
+ # Table
2
+
3
+ A responsive table component with support for variants, alignment, and a useTable hook for declarative column configuration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @lglab/compose-ui
9
+ ```
10
+
11
+ ## Import
12
+
13
+ ```tsx
14
+ import { TableRoot } from '@lglab/compose-ui'
15
+ ```
16
+
17
+ ## Examples
18
+
19
+ ### Basic
20
+
21
+ ```tsx
22
+ import { Badge, BadgeVariant } from '@lglab/compose-ui/badge'
23
+ import {
24
+ TableBody,
25
+ TableCaption,
26
+ TableCell,
27
+ TableHead,
28
+ TableHeader,
29
+ TableRoot,
30
+ TableRow,
31
+ } from '@lglab/compose-ui/table'
32
+
33
+ const invoices = [
34
+ { id: 'INV001', status: 'Paid', method: 'Credit Card', amount: '$250.00' },
35
+ { id: 'INV002', status: 'Pending', method: 'PayPal', amount: '$150.00' },
36
+ { id: 'INV003', status: 'Unpaid', method: 'Bank Transfer', amount: '$350.00' },
37
+ { id: 'INV004', status: 'Paid', method: 'Credit Card', amount: '$450.00' },
38
+ { id: 'INV005', status: 'Paid', method: 'PayPal', amount: '$550.00' },
39
+ ]
40
+
41
+ const statusVariants: Record<string, BadgeVariant> = {
42
+ Paid: 'success',
43
+ Pending: 'warning',
44
+ Unpaid: 'destructive',
45
+ }
46
+
47
+ export default function BasicExample() {
48
+ return (
49
+ <TableRoot>
50
+ <TableCaption>A list of your recent invoices.</TableCaption>
51
+ <TableHeader>
52
+ <TableRow>
53
+ <TableHead>Invoice</TableHead>
54
+ <TableHead>Status</TableHead>
55
+ <TableHead>Method</TableHead>
56
+ <TableHead>Amount</TableHead>
57
+ </TableRow>
58
+ </TableHeader>
59
+ <TableBody>
60
+ {invoices.map((invoice) => (
61
+ <TableRow key={invoice.id}>
62
+ <TableCell className='font-medium'>{invoice.id}</TableCell>
63
+ <TableCell>
64
+ <Badge
65
+ variant={statusVariants[invoice.status]}
66
+ appearance='outline'
67
+ size='sm'
68
+ >
69
+ {invoice.status}
70
+ </Badge>
71
+ </TableCell>
72
+ <TableCell>{invoice.method}</TableCell>
73
+ <TableCell>{invoice.amount}</TableCell>
74
+ </TableRow>
75
+ ))}
76
+ </TableBody>
77
+ </TableRoot>
78
+ )
79
+ }
80
+ ```
81
+
82
+ ### Variants
83
+
84
+ ```tsx
85
+ import {
86
+ TableBody,
87
+ TableCell,
88
+ TableHead,
89
+ TableHeader,
90
+ TableRoot,
91
+ TableRow,
92
+ } from '@lglab/compose-ui/table'
93
+
94
+ const data = [
95
+ {
96
+ name: 'Alice Johnson',
97
+ email: 'alice@example.com',
98
+ role: 'Admin',
99
+ department: 'Engineering',
100
+ },
101
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'User', department: 'Marketing' },
102
+ {
103
+ name: 'Charlie Brown',
104
+ email: 'charlie@example.com',
105
+ role: 'User',
106
+ department: 'Sales',
107
+ },
108
+ {
109
+ name: 'Diana Prince',
110
+ email: 'diana@example.com',
111
+ role: 'Editor',
112
+ department: 'Design',
113
+ },
114
+ ]
115
+
116
+ export default function VariantsExample() {
117
+ return (
118
+ <div className='flex flex-col w-full gap-8'>
119
+ <div>
120
+ <h4 className='mb-2 text-sm font-medium'>Default</h4>
121
+ <TableRoot className='w-full'>
122
+ <TableHeader>
123
+ <TableRow>
124
+ <TableHead>Name</TableHead>
125
+ <TableHead>Email</TableHead>
126
+ <TableHead>Role</TableHead>
127
+ <TableHead>Department</TableHead>
128
+ </TableRow>
129
+ </TableHeader>
130
+ <TableBody>
131
+ {data.map((row) => (
132
+ <TableRow key={row.email}>
133
+ <TableCell>{row.name}</TableCell>
134
+ <TableCell>{row.email}</TableCell>
135
+ <TableCell>{row.role}</TableCell>
136
+ <TableCell>{row.department}</TableCell>
137
+ </TableRow>
138
+ ))}
139
+ </TableBody>
140
+ </TableRoot>
141
+ </div>
142
+
143
+ <div>
144
+ <h4 className='mb-2 text-sm font-medium'>Striped</h4>
145
+ <TableRoot variant='striped'>
146
+ <TableHeader>
147
+ <TableRow>
148
+ <TableHead>Name</TableHead>
149
+ <TableHead>Email</TableHead>
150
+ <TableHead>Role</TableHead>
151
+ <TableHead>Department</TableHead>
152
+ </TableRow>
153
+ </TableHeader>
154
+ <TableBody>
155
+ {data.map((row) => (
156
+ <TableRow key={row.email}>
157
+ <TableCell>{row.name}</TableCell>
158
+ <TableCell>{row.email}</TableCell>
159
+ <TableCell>{row.role}</TableCell>
160
+ <TableCell>{row.department}</TableCell>
161
+ </TableRow>
162
+ ))}
163
+ </TableBody>
164
+ </TableRoot>
165
+ </div>
166
+
167
+ <div>
168
+ <h4 className='mb-2 text-sm font-medium'>Bordered</h4>
169
+ <div className='rounded-md border border-border'>
170
+ <TableRoot variant='bordered'>
171
+ <TableHeader>
172
+ <TableRow>
173
+ <TableHead>Name</TableHead>
174
+ <TableHead>Email</TableHead>
175
+ <TableHead>Role</TableHead>
176
+ <TableHead>Department</TableHead>
177
+ </TableRow>
178
+ </TableHeader>
179
+ <TableBody>
180
+ {data.map((row) => (
181
+ <TableRow key={row.email}>
182
+ <TableCell>{row.name}</TableCell>
183
+ <TableCell>{row.email}</TableCell>
184
+ <TableCell>{row.role}</TableCell>
185
+ <TableCell>{row.department}</TableCell>
186
+ </TableRow>
187
+ ))}
188
+ </TableBody>
189
+ </TableRoot>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ )
194
+ }
195
+ ```
196
+
197
+ ### Compact
198
+
199
+ ```tsx
200
+ import {
201
+ TableBody,
202
+ TableCell,
203
+ TableHead,
204
+ TableHeader,
205
+ TableRoot,
206
+ TableRow,
207
+ } from '@lglab/compose-ui/table'
208
+
209
+ const data = [
210
+ {
211
+ name: 'Alice Johnson',
212
+ email: 'alice@example.com',
213
+ role: 'Admin',
214
+ department: 'Engineering',
215
+ },
216
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'User', department: 'Marketing' },
217
+ {
218
+ name: 'Charlie Brown',
219
+ email: 'charlie@example.com',
220
+ role: 'User',
221
+ department: 'Sales',
222
+ },
223
+ {
224
+ name: 'Diana Prince',
225
+ email: 'diana@example.com',
226
+ role: 'Editor',
227
+ department: 'Design',
228
+ },
229
+ ]
230
+
231
+ export default function SizesExample() {
232
+ return (
233
+ <TableRoot size='compact'>
234
+ <TableHeader>
235
+ <TableRow>
236
+ <TableHead>Name</TableHead>
237
+ <TableHead>Email</TableHead>
238
+ <TableHead>Role</TableHead>
239
+ <TableHead>Department</TableHead>
240
+ </TableRow>
241
+ </TableHeader>
242
+ <TableBody>
243
+ {data.map((row) => (
244
+ <TableRow key={row.email}>
245
+ <TableCell>{row.name}</TableCell>
246
+ <TableCell>{row.email}</TableCell>
247
+ <TableCell>{row.role}</TableCell>
248
+ <TableCell>{row.department}</TableCell>
249
+ </TableRow>
250
+ ))}
251
+ </TableBody>
252
+ </TableRoot>
253
+ )
254
+ }
255
+ ```
256
+
257
+ ### Pagination
258
+
259
+ ```tsx
260
+ import {
261
+ PaginationButton,
262
+ PaginationContent,
263
+ PaginationEllipsis,
264
+ PaginationItem,
265
+ PaginationNext,
266
+ PaginationPrevious,
267
+ PaginationRoot,
268
+ usePagination,
269
+ } from '@lglab/compose-ui/pagination'
270
+ import {
271
+ SelectIcon,
272
+ SelectItem,
273
+ SelectItemIndicator,
274
+ SelectItemText,
275
+ SelectList,
276
+ SelectPopup,
277
+ SelectPortal,
278
+ SelectPositioner,
279
+ SelectRoot,
280
+ SelectTrigger,
281
+ SelectValue,
282
+ } from '@lglab/compose-ui/select'
283
+ import {
284
+ TableBody,
285
+ TableCell,
286
+ TableHead,
287
+ TableHeader,
288
+ TableRoot,
289
+ TableRow,
290
+ useTable,
291
+ } from '@lglab/compose-ui/table'
292
+ import { Check, ChevronLeft, ChevronRight, ChevronsUpDown, Ellipsis } from 'lucide-react'
293
+
294
+ interface Product {
295
+ id: number
296
+ name: string
297
+ price: number
298
+ category: string
299
+ }
300
+
301
+ const products: Product[] = Array.from({ length: 47 }, (_, i) => ({
302
+ id: i + 1,
303
+ name: `Product ${i + 1}`,
304
+ price: Math.round((Math.random() * 200 + 10) * 100) / 100,
305
+ category: ['Electronics', 'Clothing', 'Books', 'Home'][i % 4],
306
+ }))
307
+
308
+ export default function WithPaginationExample() {
309
+ const {
310
+ columns,
311
+ rows,
312
+ totalItems,
313
+ currentPage,
314
+ totalPages,
315
+ pageSize,
316
+ pageSizeOptions,
317
+ onPageChange,
318
+ onPageSizeChange,
319
+ } = useTable({
320
+ data: products,
321
+ columns: [
322
+ { key: 'id', header: 'ID', width: 60 },
323
+ { key: 'name', header: 'Product Name' },
324
+ {
325
+ key: 'price',
326
+ header: 'Price',
327
+ format: (value) => `$${(value as number).toFixed(2)}`,
328
+ },
329
+ { key: 'category', header: 'Category' },
330
+ ],
331
+ pagination: { pageSize: 10 },
332
+ })
333
+
334
+ const pagination = usePagination({
335
+ currentPage,
336
+ totalPages,
337
+ onPageChange,
338
+ pageSize,
339
+ pageSizeOptions,
340
+ onPageSizeChange,
341
+ })
342
+
343
+ const pageSizeItems = pageSizeOptions.map((size) => ({
344
+ label: `${size} per page`,
345
+ value: size,
346
+ }))
347
+
348
+ return (
349
+ <div className='flex flex-col w-full gap-4'>
350
+ <TableRoot>
351
+ <TableHeader>
352
+ <TableRow>
353
+ {columns.map((col) => (
354
+ <TableHead key={col.key} {...col.head} />
355
+ ))}
356
+ </TableRow>
357
+ </TableHeader>
358
+ <TableBody>
359
+ {rows.map((row) => (
360
+ <TableRow key={row.id}>
361
+ {columns.map((col) => (
362
+ <TableCell key={col.key} {...col.cell}>
363
+ {col.renderCell(row)}
364
+ </TableCell>
365
+ ))}
366
+ </TableRow>
367
+ ))}
368
+ </TableBody>
369
+ </TableRoot>
370
+
371
+ <div className='flex flex-wrap gap-4 items-center justify-between'>
372
+ <span className='text-sm text-muted-foreground'>
373
+ Showing {(currentPage - 1) * pageSize + 1}-
374
+ {Math.min(currentPage * pageSize, totalItems)} of {totalItems} items
375
+ </span>
376
+
377
+ <div className='flex flex-wrap gap-2 items-center'>
378
+ <PaginationRoot>
379
+ <PaginationContent>
380
+ <PaginationItem>
381
+ <PaginationPrevious
382
+ onClick={pagination.goToPrevious}
383
+ disabled={!pagination.canGoPrevious}
384
+ >
385
+ <ChevronLeft className='size-4' />
386
+ </PaginationPrevious>
387
+ </PaginationItem>
388
+
389
+ {pagination.pages.map((page, i) => (
390
+ <PaginationItem key={i}>
391
+ {page === 'ellipsis' ? (
392
+ <PaginationEllipsis>
393
+ <Ellipsis className='size-4' />
394
+ </PaginationEllipsis>
395
+ ) : (
396
+ <PaginationButton
397
+ isActive={page === currentPage}
398
+ onClick={() => pagination.goToPage(page)}
399
+ >
400
+ {page}
401
+ </PaginationButton>
402
+ )}
403
+ </PaginationItem>
404
+ ))}
405
+
406
+ <PaginationItem>
407
+ <PaginationNext
408
+ onClick={pagination.goToNext}
409
+ disabled={!pagination.canGoNext}
410
+ >
411
+ <ChevronRight className='size-4' />
412
+ </PaginationNext>
413
+ </PaginationItem>
414
+ </PaginationContent>
415
+ </PaginationRoot>
416
+
417
+ <SelectRoot
418
+ value={pageSize}
419
+ onValueChange={(value) => value && pagination.setPageSize(value)}
420
+ items={pageSizeItems}
421
+ >
422
+ <SelectTrigger aria-label='Select page size' className='min-w-32 min-h-8'>
423
+ <SelectValue placeholder='Page size' />
424
+ <SelectIcon>
425
+ <ChevronsUpDown className='size-4' />
426
+ </SelectIcon>
427
+ </SelectTrigger>
428
+ <SelectPortal>
429
+ <SelectPositioner>
430
+ <SelectPopup>
431
+ <SelectList>
432
+ {pageSizeItems.map(({ label, value }) => (
433
+ <SelectItem key={value} value={value}>
434
+ <SelectItemText>{label}</SelectItemText>
435
+ <SelectItemIndicator>
436
+ <Check className='size-3.5' />
437
+ </SelectItemIndicator>
438
+ </SelectItem>
439
+ ))}
440
+ </SelectList>
441
+ </SelectPopup>
442
+ </SelectPositioner>
443
+ </SelectPortal>
444
+ </SelectRoot>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ )
449
+ }
450
+ ```
451
+
452
+ ### Sorting
453
+
454
+ ```tsx
455
+ import {
456
+ TableBody,
457
+ TableCell,
458
+ TableHead,
459
+ TableHeader,
460
+ TableRoot,
461
+ TableRow,
462
+ useTable,
463
+ } from '@lglab/compose-ui/table'
464
+
465
+ interface Product {
466
+ id: number
467
+ name: string
468
+ price: number
469
+ inStock: boolean
470
+ createdAt: Date
471
+ }
472
+
473
+ const products: Product[] = [
474
+ {
475
+ id: 1,
476
+ name: 'Laptop',
477
+ price: 999.99,
478
+ inStock: true,
479
+ createdAt: new Date('2024-01-15'),
480
+ },
481
+ {
482
+ id: 2,
483
+ name: 'Mouse',
484
+ price: 29.99,
485
+ inStock: true,
486
+ createdAt: new Date('2024-02-20'),
487
+ },
488
+ {
489
+ id: 3,
490
+ name: 'Keyboard',
491
+ price: 79.99,
492
+ inStock: false,
493
+ createdAt: new Date('2024-01-10'),
494
+ },
495
+ {
496
+ id: 4,
497
+ name: 'Monitor',
498
+ price: 349.99,
499
+ inStock: true,
500
+ createdAt: new Date('2024-03-05'),
501
+ },
502
+ {
503
+ id: 5,
504
+ name: 'Headphones',
505
+ price: 149.99,
506
+ inStock: false,
507
+ createdAt: new Date('2024-02-28'),
508
+ },
509
+ {
510
+ id: 6,
511
+ name: 'Webcam',
512
+ price: 89.99,
513
+ inStock: true,
514
+ createdAt: new Date('2024-01-22'),
515
+ },
516
+ ]
517
+
518
+ export default function WithSortingExample() {
519
+ const { columns, rows } = useTable({
520
+ data: products,
521
+ columns: [
522
+ { key: 'id', header: 'ID', width: 60 },
523
+ { key: 'name', header: 'Product Name', sortable: true },
524
+ {
525
+ key: 'price',
526
+ header: 'Price',
527
+ format: (value) => `$${(value as number).toFixed(2)}`,
528
+ sortable: true,
529
+ },
530
+ {
531
+ key: 'inStock',
532
+ header: 'In Stock',
533
+ format: (value) => (value ? 'Yes' : 'No'),
534
+ sortable: true,
535
+ },
536
+ {
537
+ key: 'createdAt',
538
+ header: 'Created',
539
+ format: (value) => (value as Date).toLocaleDateString(),
540
+ sortable: true,
541
+ },
542
+ ],
543
+ sort: { key: 'name', direction: 'asc' },
544
+ })
545
+
546
+ return (
547
+ <TableRoot>
548
+ <TableHeader>
549
+ <TableRow>
550
+ {columns.map((col) => (
551
+ <TableHead key={col.key} {...col.head} />
552
+ ))}
553
+ </TableRow>
554
+ </TableHeader>
555
+ <TableBody>
556
+ {rows.map((row) => (
557
+ <TableRow key={row.id}>
558
+ {columns.map((col) => (
559
+ <TableCell key={col.key} {...col.cell}>
560
+ {col.renderCell(row)}
561
+ </TableCell>
562
+ ))}
563
+ </TableRow>
564
+ ))}
565
+ </TableBody>
566
+ </TableRoot>
567
+ )
568
+ }
569
+ ```
570
+
571
+ ### Search
572
+
573
+ ```tsx
574
+ import { FieldControl, FieldLabel, FieldRoot } from '@lglab/compose-ui/field'
575
+ import { Input } from '@lglab/compose-ui/input'
576
+ import {
577
+ TableBody,
578
+ TableCell,
579
+ TableHead,
580
+ TableHeader,
581
+ TableRoot,
582
+ TableRow,
583
+ useTable,
584
+ } from '@lglab/compose-ui/table'
585
+
586
+ interface User {
587
+ id: number
588
+ name: string
589
+ email: string
590
+ role: string
591
+ }
592
+
593
+ const users: User[] = [
594
+ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },
595
+ { id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'User' },
596
+ { id: 3, name: 'Charlie Brown', email: 'charlie@example.com', role: 'User' },
597
+ { id: 4, name: 'Diana Ross', email: 'diana@example.com', role: 'Moderator' },
598
+ { id: 5, name: 'Edward Norton', email: 'edward@example.com', role: 'User' },
599
+ { id: 6, name: 'Fiona Apple', email: 'fiona@example.com', role: 'Admin' },
600
+ ]
601
+
602
+ export default function WithSearchExample() {
603
+ const { columns, rows, totalItems, searchTerm, onSearchChange } = useTable({
604
+ data: users,
605
+ columns: [
606
+ { key: 'id', header: 'ID', width: 60 },
607
+ { key: 'name', header: 'Name' },
608
+ { key: 'email', header: 'Email' },
609
+ { key: 'role', header: 'Role' },
610
+ ],
611
+ search: { keys: ['name', 'email'] },
612
+ })
613
+
614
+ return (
615
+ <div className='flex flex-col w-full gap-2'>
616
+ <FieldRoot className='w-[250px]'>
617
+ <FieldLabel>Search users by name or email</FieldLabel>
618
+ <FieldControl
619
+ render={
620
+ <Input
621
+ placeholder='Search'
622
+ value={searchTerm}
623
+ onChange={(e) => onSearchChange(e.target.value)}
624
+ />
625
+ }
626
+ />
627
+ </FieldRoot>
628
+ <TableRoot>
629
+ <TableHeader>
630
+ <TableRow>
631
+ {columns.map((col) => (
632
+ <TableHead key={col.key} {...col.head} />
633
+ ))}
634
+ </TableRow>
635
+ </TableHeader>
636
+ <TableBody>
637
+ {rows.length === 0 ? (
638
+ <TableRow className='hover:bg-transparent'>
639
+ <TableCell
640
+ colSpan={columns.length}
641
+ className='h-24 text-center text-muted-foreground'
642
+ >
643
+ No results found.
644
+ </TableCell>
645
+ </TableRow>
646
+ ) : (
647
+ rows.map((row) => (
648
+ <TableRow key={row.id}>
649
+ {columns.map((col) => (
650
+ <TableCell key={col.key} {...col.cell}>
651
+ {col.renderCell(row)}
652
+ </TableCell>
653
+ ))}
654
+ </TableRow>
655
+ ))
656
+ )}
657
+ </TableBody>
658
+ </TableRoot>
659
+ <span className='text-sm text-muted-foreground'>{totalItems} results</span>
660
+ </div>
661
+ )
662
+ }
663
+ ```
664
+
665
+ ### Filters
666
+
667
+ ```tsx
668
+ import { Badge } from '@lglab/compose-ui/badge'
669
+ import { Button } from '@lglab/compose-ui/button'
670
+ import { CheckboxIndicator, CheckboxRoot } from '@lglab/compose-ui/checkbox'
671
+ import { CheckboxGroupRoot } from '@lglab/compose-ui/checkbox-group'
672
+ import { Input } from '@lglab/compose-ui/input'
673
+ import {
674
+ PopoverPopup,
675
+ PopoverPortal,
676
+ PopoverPositioner,
677
+ PopoverRoot,
678
+ PopoverTrigger,
679
+ } from '@lglab/compose-ui/popover'
680
+ import { RadioIndicator, RadioRoot } from '@lglab/compose-ui/radio'
681
+ import { RadioGroupRoot } from '@lglab/compose-ui/radio-group'
682
+ import {
683
+ SliderControl,
684
+ SliderIndicator,
685
+ SliderRoot,
686
+ SliderThumb,
687
+ SliderTrack,
688
+ SliderValue,
689
+ } from '@lglab/compose-ui/slider'
690
+ import {
691
+ TableBody,
692
+ TableCell,
693
+ TableHead,
694
+ TableHeader,
695
+ TableRoot,
696
+ TableRow,
697
+ containsFilter,
698
+ equalsFilter,
699
+ includesFilter,
700
+ rangeFilter,
701
+ useTable,
702
+ } from '@lglab/compose-ui/table'
703
+ import { Check, ChevronDown, X } from 'lucide-react'
704
+
705
+ type Status = 'pending' | 'paid' | 'overdue'
706
+
707
+ interface Invoice {
708
+ id: string
709
+ customer: string
710
+ email: string
711
+ amount: number
712
+ status: Status
713
+ }
714
+
715
+ const invoices: Invoice[] = [
716
+ {
717
+ id: 'INV-001',
718
+ customer: 'Acme Corp',
719
+ email: 'billing@acme.com',
720
+ amount: 1250,
721
+ status: 'paid',
722
+ },
723
+ {
724
+ id: 'INV-002',
725
+ customer: 'Globex Inc',
726
+ email: 'ap@globex.com',
727
+ amount: 430,
728
+ status: 'pending',
729
+ },
730
+ {
731
+ id: 'INV-003',
732
+ customer: 'Stark Industries',
733
+ email: 'tony@stark.com',
734
+ amount: 890,
735
+ status: 'overdue',
736
+ },
737
+ {
738
+ id: 'INV-004',
739
+ customer: 'Wayne Enterprises',
740
+ email: 'bruce@wayne.com',
741
+ amount: 2100,
742
+ status: 'paid',
743
+ },
744
+ {
745
+ id: 'INV-005',
746
+ customer: 'Umbrella Corp',
747
+ email: 'finance@umbrella.com',
748
+ amount: 560,
749
+ status: 'pending',
750
+ },
751
+ {
752
+ id: 'INV-006',
753
+ customer: 'Cyberdyne Systems',
754
+ email: 'accounts@cyberdyne.com',
755
+ amount: 1800,
756
+ status: 'paid',
757
+ },
758
+ {
759
+ id: 'INV-007',
760
+ customer: 'Oscorp',
761
+ email: 'norman@oscorp.com',
762
+ amount: 340,
763
+ status: 'overdue',
764
+ },
765
+ {
766
+ id: 'INV-008',
767
+ customer: 'LexCorp',
768
+ email: 'lex@lexcorp.com',
769
+ amount: 1500,
770
+ status: 'pending',
771
+ },
772
+ ]
773
+
774
+ const statuses: { value: Status; label: string }[] = [
775
+ { value: 'pending', label: 'Pending' },
776
+ { value: 'paid', label: 'Paid' },
777
+ { value: 'overdue', label: 'Overdue' },
778
+ ]
779
+
780
+ const statusVariants: Record<Status, 'warning' | 'success' | 'destructive'> = {
781
+ pending: 'warning',
782
+ paid: 'success',
783
+ overdue: 'destructive',
784
+ }
785
+
786
+ export default function WithFiltersExample() {
787
+ const table = useTable({
788
+ data: invoices,
789
+ columns: [
790
+ { key: 'id', header: 'Invoice', width: 100 },
791
+ { key: 'customer', header: 'Customer' },
792
+ { key: 'email', header: 'Email' },
793
+ {
794
+ key: 'amount',
795
+ header: 'Amount',
796
+ cell: (value) => `$${value.toLocaleString()}`,
797
+ },
798
+ {
799
+ key: 'status',
800
+ header: 'Status',
801
+ width: 100,
802
+ cell: (value) => (
803
+ <Badge
804
+ variant={statusVariants[value as Status]}
805
+ appearance='outline'
806
+ size='sm'
807
+ shape='pill'
808
+ >
809
+ {value}
810
+ </Badge>
811
+ ),
812
+ },
813
+ ],
814
+ filters: {
815
+ status: {
816
+ predicate: includesFilter('status'),
817
+ defaultValue: [],
818
+ },
819
+ amount: {
820
+ predicate: rangeFilter('amount'),
821
+ defaultValue: [0, 2500],
822
+ },
823
+ customer: {
824
+ predicate: containsFilter('customer'),
825
+ defaultValue: '',
826
+ },
827
+ email: {
828
+ predicate: equalsFilter('email'),
829
+ defaultValue: undefined,
830
+ },
831
+ },
832
+ })
833
+
834
+ const selectedStatuses = (table.filterValues.status as Status[]) ?? []
835
+ const amountRange = (table.filterValues.amount as [number, number]) ?? [0, 2500]
836
+ const customerSearch = (table.filterValues.customer as string) ?? ''
837
+ const selectedEmail = (table.filterValues.email as string) ?? ''
838
+
839
+ return (
840
+ <div className='flex flex-col w-full gap-4'>
841
+ <div className='flex flex-wrap items-center gap-2'>
842
+ {/* Status Filter */}
843
+ <PopoverRoot>
844
+ <PopoverTrigger
845
+ render={(props) => (
846
+ <Button {...props} variant='outline' size='sm'>
847
+ Status
848
+ {selectedStatuses.length > 0 && (
849
+ <span className='flex items-center justify-center size-5 rounded-full aspect-square bg-primary text-xs text-primary-foreground'>
850
+ {selectedStatuses.length}
851
+ </span>
852
+ )}
853
+ <ChevronDown className='ml-1 size-3.5' />
854
+ </Button>
855
+ )}
856
+ />
857
+ <PopoverPortal>
858
+ <PopoverPositioner align='start'>
859
+ <PopoverPopup className='min-w-[140px] p-2'>
860
+ <CheckboxGroupRoot
861
+ value={selectedStatuses}
862
+ onValueChange={(value) => table.setFilterValue('status', value)}
863
+ >
864
+ {statuses.map((status) => (
865
+ <label
866
+ key={status.value}
867
+ className='flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-muted'
868
+ >
869
+ <CheckboxRoot value={status.value}>
870
+ <CheckboxIndicator>
871
+ <Check className='size-3.5' />
872
+ </CheckboxIndicator>
873
+ </CheckboxRoot>
874
+ {status.label}
875
+ </label>
876
+ ))}
877
+ </CheckboxGroupRoot>
878
+ </PopoverPopup>
879
+ </PopoverPositioner>
880
+ </PopoverPortal>
881
+ </PopoverRoot>
882
+
883
+ {/* Amount Filter */}
884
+ <PopoverRoot>
885
+ <PopoverTrigger
886
+ render={(props) => (
887
+ <Button {...props} variant='outline' size='sm'>
888
+ Amount: ${amountRange[0]} - ${amountRange[1]}
889
+ <ChevronDown className='ml-1 size-3.5' />
890
+ </Button>
891
+ )}
892
+ />
893
+ <PopoverPortal>
894
+ <PopoverPositioner align='start'>
895
+ <PopoverPopup className='w-72 p-4'>
896
+ <SliderRoot
897
+ value={amountRange}
898
+ min={0}
899
+ max={2500}
900
+ step={50}
901
+ onValueChange={(value) => table.setFilterValue('amount', value)}
902
+ format={{
903
+ style: 'currency',
904
+ currency: 'USD',
905
+ maximumFractionDigits: 0,
906
+ }}
907
+ >
908
+ <div className='mb-2 flex items-center justify-between text-sm'>
909
+ <span className='font-medium'>Amount</span>
910
+ <SliderValue className='tabular-nums' />
911
+ </div>
912
+ <SliderControl>
913
+ <SliderTrack>
914
+ <SliderIndicator />
915
+ <SliderThumb aria-label='Minimum amount' />
916
+ <SliderThumb aria-label='Maximum amount' />
917
+ </SliderTrack>
918
+ </SliderControl>
919
+ </SliderRoot>
920
+ </PopoverPopup>
921
+ </PopoverPositioner>
922
+ </PopoverPortal>
923
+ </PopoverRoot>
924
+
925
+ {/* Customer Filter (containsFilter) */}
926
+ <PopoverRoot>
927
+ <PopoverTrigger
928
+ render={(props) => (
929
+ <Button {...props} variant='outline' size='sm'>
930
+ Customer
931
+ {customerSearch && (
932
+ <span className='flex items-center justify-center size-5 rounded-full aspect-square bg-primary text-xs text-primary-foreground'>
933
+ 1
934
+ </span>
935
+ )}
936
+ <ChevronDown className='ml-1 size-3.5' />
937
+ </Button>
938
+ )}
939
+ />
940
+ <PopoverPortal>
941
+ <PopoverPositioner align='start'>
942
+ <PopoverPopup className='w-64 p-3'>
943
+ <Input
944
+ placeholder='Search customer...'
945
+ value={customerSearch}
946
+ onChange={(e) =>
947
+ table.setFilterValue('customer', e.target.value || undefined)
948
+ }
949
+ />
950
+ </PopoverPopup>
951
+ </PopoverPositioner>
952
+ </PopoverPortal>
953
+ </PopoverRoot>
954
+
955
+ {/* Email Filter (equalsFilter) */}
956
+ <PopoverRoot>
957
+ <PopoverTrigger
958
+ render={(props) => (
959
+ <Button {...props} variant='outline' size='sm'>
960
+ Email
961
+ {selectedEmail && (
962
+ <span className='flex items-center justify-center size-5 rounded-full aspect-square bg-primary text-xs text-primary-foreground'>
963
+ 1
964
+ </span>
965
+ )}
966
+ <ChevronDown className='ml-1 size-3.5' />
967
+ </Button>
968
+ )}
969
+ />
970
+ <PopoverPortal>
971
+ <PopoverPositioner align='start'>
972
+ <PopoverPopup className='min-w-[200px] p-2'>
973
+ <RadioGroupRoot
974
+ value={selectedEmail}
975
+ onValueChange={(value) =>
976
+ table.setFilterValue('email', value || undefined)
977
+ }
978
+ >
979
+ {invoices.map((invoice) => (
980
+ <label
981
+ key={invoice.email}
982
+ className='flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-muted'
983
+ >
984
+ <RadioRoot value={invoice.email}>
985
+ <RadioIndicator />
986
+ </RadioRoot>
987
+ {invoice.email}
988
+ </label>
989
+ ))}
990
+ </RadioGroupRoot>
991
+ {selectedEmail && (
992
+ <Button
993
+ variant='outline'
994
+ size='sm'
995
+ className='w-full mt-2'
996
+ onClick={() => table.setFilterValue('email', undefined)}
997
+ >
998
+ Clear selection
999
+ </Button>
1000
+ )}
1001
+ </PopoverPopup>
1002
+ </PopoverPositioner>
1003
+ </PopoverPortal>
1004
+ </PopoverRoot>
1005
+
1006
+ {/* Clear Filters */}
1007
+ {table.activeFilterCount > 0 && (
1008
+ <Button variant='ghost' size='sm' onClick={table.clearFilters}>
1009
+ <X className='size-3.5' />
1010
+ Clear filters ({table.activeFilterCount})
1011
+ </Button>
1012
+ )}
1013
+ </div>
1014
+
1015
+ <TableRoot>
1016
+ <TableHeader>
1017
+ <TableRow>
1018
+ {table.columns.map((col) => (
1019
+ <TableHead key={col.key} {...col.head} />
1020
+ ))}
1021
+ </TableRow>
1022
+ </TableHeader>
1023
+ <TableBody>
1024
+ {table.rows.length === 0 ? (
1025
+ <TableRow className='hover:bg-transparent'>
1026
+ <TableCell
1027
+ colSpan={table.columns.length}
1028
+ className='h-24 text-center text-muted-foreground'
1029
+ >
1030
+ No results found.
1031
+ </TableCell>
1032
+ </TableRow>
1033
+ ) : (
1034
+ table.rows.map((row) => (
1035
+ <TableRow key={row.id}>
1036
+ {table.columns.map((col) => (
1037
+ <TableCell key={col.key} {...col.cell}>
1038
+ {col.renderCell(row)}
1039
+ </TableCell>
1040
+ ))}
1041
+ </TableRow>
1042
+ ))
1043
+ )}
1044
+ </TableBody>
1045
+ </TableRoot>
1046
+
1047
+ <span className='text-sm text-muted-foreground'>
1048
+ {table.totalItems} {table.totalItems === 1 ? 'result' : 'results'}
1049
+ </span>
1050
+ </div>
1051
+ )
1052
+ }
1053
+ ```
1054
+
1055
+ ### Row Selection
1056
+
1057
+ ```tsx
1058
+ import {
1059
+ AlertDialogBackdrop,
1060
+ AlertDialogClose,
1061
+ AlertDialogDescription,
1062
+ AlertDialogPopup,
1063
+ AlertDialogPortal,
1064
+ AlertDialogRoot,
1065
+ AlertDialogTitle,
1066
+ AlertDialogTrigger,
1067
+ } from '@lglab/compose-ui/alert-dialog'
1068
+ import { Button } from '@lglab/compose-ui/button'
1069
+ import { CheckboxIndicator, CheckboxRoot } from '@lglab/compose-ui/checkbox'
1070
+ import {
1071
+ PaginationButton,
1072
+ PaginationContent,
1073
+ PaginationEllipsis,
1074
+ PaginationItem,
1075
+ PaginationNext,
1076
+ PaginationPrevious,
1077
+ PaginationRoot,
1078
+ usePagination,
1079
+ } from '@lglab/compose-ui/pagination'
1080
+ import { Separator } from '@lglab/compose-ui/separator'
1081
+ import {
1082
+ TableBody,
1083
+ TableCell,
1084
+ TableHead,
1085
+ TableHeader,
1086
+ TableRoot,
1087
+ TableRow,
1088
+ } from '@lglab/compose-ui/table'
1089
+ import { useTable } from '@lglab/compose-ui/table'
1090
+ import { Check, ChevronLeft, ChevronRight, Ellipsis, Minus, Trash2 } from 'lucide-react'
1091
+
1092
+ interface User {
1093
+ id: number
1094
+ name: string
1095
+ email: string
1096
+ role: string
1097
+ }
1098
+
1099
+ const users: User[] = [
1100
+ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },
1101
+ { id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'Editor' },
1102
+ { id: 3, name: 'Carol Williams', email: 'carol@example.com', role: 'Viewer' },
1103
+ { id: 4, name: 'David Brown', email: 'david@example.com', role: 'Editor' },
1104
+ { id: 5, name: 'Eva Martinez', email: 'eva@example.com', role: 'Admin' },
1105
+ { id: 6, name: 'Frank Garcia', email: 'frank@example.com', role: 'Viewer' },
1106
+ { id: 7, name: 'Grace Lee', email: 'grace@example.com', role: 'Editor' },
1107
+ { id: 8, name: 'Henry Wilson', email: 'henry@example.com', role: 'Viewer' },
1108
+ { id: 9, name: 'Ivy Chen', email: 'ivy@example.com', role: 'Admin' },
1109
+ { id: 10, name: 'Jack Taylor', email: 'jack@example.com', role: 'Editor' },
1110
+ { id: 11, name: 'Karen Davis', email: 'karen@example.com', role: 'Viewer' },
1111
+ { id: 12, name: 'Leo Anderson', email: 'leo@example.com', role: 'Editor' },
1112
+ ]
1113
+
1114
+ export default function WithSelectionExample() {
1115
+ const table = useTable({
1116
+ data: users,
1117
+ columns: [
1118
+ { key: 'id', header: 'ID', width: 60 },
1119
+ { key: 'name', header: 'Name' },
1120
+ { key: 'email', header: 'Email' },
1121
+ { key: 'role', header: 'Role', width: 100 },
1122
+ ],
1123
+ pagination: { pageSize: 5 },
1124
+ selection: {
1125
+ rowKey: (row) => row.id,
1126
+ },
1127
+ })
1128
+
1129
+ const pagination = usePagination({
1130
+ currentPage: table.currentPage,
1131
+ totalPages: table.totalPages,
1132
+ onPageChange: table.onPageChange,
1133
+ })
1134
+
1135
+ return (
1136
+ <div className='flex flex-col w-full gap-4'>
1137
+ <div className='flex flex-col sm:flex-row sm:items-center gap-2 py-3 px-5 bg-muted rounded-md'>
1138
+ <span className='text-sm font-medium'>
1139
+ {table.selection?.selectedCount} row
1140
+ {table.selection?.selectedCount !== 1 ? 's' : ''} selected
1141
+ </span>
1142
+ <Separator className='hidden sm:block h-4 mx-2' orientation='vertical' />
1143
+
1144
+ <Button
1145
+ variant='outline'
1146
+ size='sm'
1147
+ disabled={table.selection?.selectedCount === 0}
1148
+ onClick={table.selection?.clearSelection}
1149
+ >
1150
+ Clear selection
1151
+ </Button>
1152
+ <AlertDialogRoot>
1153
+ <AlertDialogTrigger
1154
+ disabled={table.selection?.selectedCount === 0}
1155
+ size='sm'
1156
+ variant='destructive'
1157
+ >
1158
+ <Trash2 className='size-3.5 mr-1' />
1159
+ Delete selected
1160
+ </AlertDialogTrigger>
1161
+ <AlertDialogPortal>
1162
+ <AlertDialogBackdrop />
1163
+ <AlertDialogPopup>
1164
+ <AlertDialogTitle>Delete selected rows</AlertDialogTitle>
1165
+ <AlertDialogDescription className='text-sm'>
1166
+ Selected IDs: {Array.from(table.selection?.selectedKeys ?? []).join(', ')}
1167
+ </AlertDialogDescription>
1168
+ <div className='mt-6 flex justify-end gap-2'>
1169
+ <AlertDialogClose>Cancel</AlertDialogClose>
1170
+ <AlertDialogClose variant='destructive'>Delete</AlertDialogClose>
1171
+ </div>
1172
+ </AlertDialogPopup>
1173
+ </AlertDialogPortal>
1174
+ </AlertDialogRoot>
1175
+ </div>
1176
+
1177
+ <TableRoot>
1178
+ <TableHeader>
1179
+ <TableRow>
1180
+ <TableHead className='w-10'>
1181
+ <CheckboxRoot
1182
+ checked={table.selection?.isAllOnPageSelected}
1183
+ indeterminate={table.selection?.isIndeterminate}
1184
+ onCheckedChange={() => table.selection?.toggleAllOnPage()}
1185
+ >
1186
+ <CheckboxIndicator
1187
+ render={(props, state) => (
1188
+ <span {...props}>
1189
+ {state.indeterminate ? (
1190
+ <Minus className='size-3.5' />
1191
+ ) : (
1192
+ <Check className='size-3.5' />
1193
+ )}
1194
+ </span>
1195
+ )}
1196
+ />
1197
+ </CheckboxRoot>
1198
+ </TableHead>
1199
+ {table.columns.map((col) => (
1200
+ <TableHead key={col.key} {...col.head} />
1201
+ ))}
1202
+ </TableRow>
1203
+ </TableHeader>
1204
+ <TableBody>
1205
+ {table.rows.map((row) => (
1206
+ <TableRow
1207
+ key={row.id}
1208
+ data-selected={table.selection?.isRowSelected(row) || undefined}
1209
+ className='data-selected:bg-muted/50'
1210
+ >
1211
+ <TableCell className='w-10'>
1212
+ <CheckboxRoot
1213
+ checked={table.selection?.isRowSelected(row)}
1214
+ onCheckedChange={() => table.selection?.toggleRowSelection(row)}
1215
+ >
1216
+ <CheckboxIndicator>
1217
+ <Check className='size-3.5' />
1218
+ </CheckboxIndicator>
1219
+ </CheckboxRoot>
1220
+ </TableCell>
1221
+ {table.columns.map((col) => (
1222
+ <TableCell key={col.key} {...col.cell}>
1223
+ {col.renderCell(row)}
1224
+ </TableCell>
1225
+ ))}
1226
+ </TableRow>
1227
+ ))}
1228
+ </TableBody>
1229
+ </TableRoot>
1230
+
1231
+ <div className='flex flex-wrap gap-4 items-center justify-between'>
1232
+ <span className='text-sm text-muted-foreground'>
1233
+ Showing {(table.currentPage - 1) * table.pageSize + 1}-
1234
+ {Math.min(table.currentPage * table.pageSize, table.totalItems)} of{' '}
1235
+ {table.totalItems} users
1236
+ </span>
1237
+
1238
+ <PaginationRoot>
1239
+ <PaginationContent>
1240
+ <PaginationItem>
1241
+ <PaginationPrevious
1242
+ onClick={pagination.goToPrevious}
1243
+ disabled={!pagination.canGoPrevious}
1244
+ >
1245
+ <ChevronLeft className='size-4' />
1246
+ </PaginationPrevious>
1247
+ </PaginationItem>
1248
+
1249
+ {pagination.pages.map((page, i) => (
1250
+ <PaginationItem key={i}>
1251
+ {page === 'ellipsis' ? (
1252
+ <PaginationEllipsis>
1253
+ <Ellipsis className='size-4' />
1254
+ </PaginationEllipsis>
1255
+ ) : (
1256
+ <PaginationButton
1257
+ isActive={page === table.currentPage}
1258
+ onClick={() => pagination.goToPage(page)}
1259
+ >
1260
+ {page}
1261
+ </PaginationButton>
1262
+ )}
1263
+ </PaginationItem>
1264
+ ))}
1265
+
1266
+ <PaginationItem>
1267
+ <PaginationNext
1268
+ onClick={pagination.goToNext}
1269
+ disabled={!pagination.canGoNext}
1270
+ >
1271
+ <ChevronRight className='size-4' />
1272
+ </PaginationNext>
1273
+ </PaginationItem>
1274
+ </PaginationContent>
1275
+ </PaginationRoot>
1276
+ </div>
1277
+ </div>
1278
+ )
1279
+ }
1280
+ ```
1281
+
1282
+ ### Loading State
1283
+
1284
+ ```tsx
1285
+ import { Skeleton } from '@lglab/compose-ui/skeleton'
1286
+ import {
1287
+ TableBody,
1288
+ TableCell,
1289
+ TableHead,
1290
+ TableHeader,
1291
+ TableRoot,
1292
+ TableRow,
1293
+ } from '@lglab/compose-ui/table'
1294
+
1295
+ export default function WithLoadingExample() {
1296
+ return (
1297
+ <TableRoot size='compact'>
1298
+ <TableHeader>
1299
+ <TableRow>
1300
+ <TableHead>Invoice</TableHead>
1301
+ <TableHead>Status</TableHead>
1302
+ <TableHead>Method</TableHead>
1303
+ <TableHead>Amount</TableHead>
1304
+ </TableRow>
1305
+ </TableHeader>
1306
+ <TableBody>
1307
+ {Array.from({ length: 5 }).map((_, i) => (
1308
+ <TableRow key={i} className='hover:bg-transparent'>
1309
+ {Array.from({ length: 4 }).map((_, i) => (
1310
+ <TableCell key={i}>
1311
+ <Skeleton className='h-5 w-full' animation='shimmer' />
1312
+ </TableCell>
1313
+ ))}
1314
+ </TableRow>
1315
+ ))}
1316
+ </TableBody>
1317
+ </TableRoot>
1318
+ )
1319
+ }
1320
+ ```
1321
+
1322
+ ## Resources
1323
+
1324
+ - [Base UI Table Documentation](https://base-ui.com/react/components/table)
1325
+ - [API Reference](https://base-ui.com/react/components/table#api-reference)