@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,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)
|