@duro-app/ui 0.22.0 → 0.24.0
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/dist/components/Alert/Alert.meta.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +14 -0
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts +3 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +11 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -0
- package/dist/components/ButtonGroup/styles.css.d.ts +45 -0
- package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -0
- package/dist/components/Callout/Callout.meta.d.ts.map +1 -1
- package/dist/components/Card/Card.meta.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +22 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts +3 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts +9 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/styles.css.d.ts +12 -0
- package/dist/components/CheckboxGroup/styles.css.d.ts.map +1 -0
- package/dist/components/EmptyState/EmptyState.meta.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.meta.d.ts.map +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.meta.d.ts.map +1 -1
- package/dist/components/List/List.stories.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +22 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts +3 -0
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts +9 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -0
- package/dist/components/RadioGroup/styles.css.d.ts +60 -0
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -0
- package/dist/components/StatusIcon/StatusIcon.meta.d.ts.map +1 -1
- package/dist/components/Table/ColumnFilter.d.ts +8 -0
- package/dist/components/Table/ColumnFilter.d.ts.map +1 -0
- package/dist/components/Table/Pagination.d.ts +7 -0
- package/dist/components/Table/Pagination.d.ts.map +1 -0
- package/dist/components/Table/SortIndicator.d.ts +16 -0
- package/dist/components/Table/SortIndicator.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +10 -3
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +4 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +12 -4
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Table/useDataTable.d.ts +32 -0
- package/dist/components/Table/useDataTable.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2399 -1927
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/ActionBar/ActionBar.stories.tsx +3 -3
- package/src/components/Alert/Alert.meta.ts +4 -1
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +24 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +103 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +59 -0
- package/src/components/ButtonGroup/styles.css.ts +28 -0
- package/src/components/Callout/Callout.meta.ts +4 -1
- package/src/components/Card/Card.meta.ts +4 -1
- package/src/components/Checkbox/styles.css.ts +1 -1
- package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +25 -0
- package/src/components/CheckboxGroup/CheckboxGroup.stories.tsx +88 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +105 -0
- package/src/components/CheckboxGroup/styles.css.ts +13 -0
- package/src/components/EmptyState/EmptyState.meta.ts +1 -4
- package/src/components/Icon/Icon.meta.ts +5 -5
- package/src/components/InputGroup/InputGroup.meta.ts +4 -1
- package/src/components/List/List.meta.ts +8 -2
- package/src/components/List/List.stories.tsx +35 -15
- package/src/components/List/List.tsx +1 -5
- package/src/components/RadioGroup/RadioGroup.meta.ts +25 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +83 -0
- package/src/components/RadioGroup/RadioGroup.tsx +113 -0
- package/src/components/RadioGroup/styles.css.ts +66 -0
- package/src/components/StatusIcon/StatusIcon.meta.ts +1 -4
- package/src/components/Table/ColumnFilter.tsx +46 -0
- package/src/components/Table/Pagination.tsx +82 -0
- package/src/components/Table/SortIndicator.tsx +56 -0
- package/src/components/Table/Table.stories.tsx +355 -9
- package/src/components/Table/Table.tsx +62 -17
- package/src/components/Table/styles.css.ts +18 -8
- package/src/components/Table/useDataTable.ts +113 -0
- package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -3
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +5 -0
- package/src/strict.css +5 -0
|
@@ -9,6 +9,8 @@ import {Button} from '../Button/Button'
|
|
|
9
9
|
import {ActionBar} from '../ActionBar/ActionBar'
|
|
10
10
|
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
11
11
|
|
|
12
|
+
import {flexRender, createColumnHelper, useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, type SortingState, type ColumnFiltersState} from '@tanstack/react-table'
|
|
13
|
+
|
|
12
14
|
const meta: Meta = {
|
|
13
15
|
title: 'Components/Table',
|
|
14
16
|
argTypes: {
|
|
@@ -55,7 +57,7 @@ function statusVariant(status: string) {
|
|
|
55
57
|
|
|
56
58
|
export const Default: Story = {
|
|
57
59
|
render: () => (
|
|
58
|
-
<Table.Root
|
|
60
|
+
<Table.Root>
|
|
59
61
|
<Table.Header>
|
|
60
62
|
<Table.Row>
|
|
61
63
|
<Table.HeaderCell>Service</Table.HeaderCell>
|
|
@@ -108,7 +110,7 @@ export const Default: Story = {
|
|
|
108
110
|
|
|
109
111
|
export const Striped: Story = {
|
|
110
112
|
render: () => (
|
|
111
|
-
<Table.Root variant="striped"
|
|
113
|
+
<Table.Root variant="striped">
|
|
112
114
|
<Table.Header>
|
|
113
115
|
<Table.Row>
|
|
114
116
|
<Table.HeaderCell>Service</Table.HeaderCell>
|
|
@@ -141,7 +143,7 @@ export const Striped: Story = {
|
|
|
141
143
|
|
|
142
144
|
export const Bordered: Story = {
|
|
143
145
|
render: () => (
|
|
144
|
-
<Table.Root variant="bordered"
|
|
146
|
+
<Table.Root variant="bordered">
|
|
145
147
|
<Table.Header>
|
|
146
148
|
<Table.Row>
|
|
147
149
|
<Table.HeaderCell>Service</Table.HeaderCell>
|
|
@@ -175,7 +177,7 @@ export const Bordered: Story = {
|
|
|
175
177
|
|
|
176
178
|
export const Compact: Story = {
|
|
177
179
|
render: () => (
|
|
178
|
-
<Table.Root size="sm"
|
|
180
|
+
<Table.Root size="sm">
|
|
179
181
|
<Table.Header>
|
|
180
182
|
<Table.Row>
|
|
181
183
|
<Table.HeaderCell>Service</Table.HeaderCell>
|
|
@@ -248,7 +250,7 @@ function CheckboxTableDemo() {
|
|
|
248
250
|
|
|
249
251
|
return (
|
|
250
252
|
<>
|
|
251
|
-
<Table.Root
|
|
253
|
+
<Table.Root>
|
|
252
254
|
<Table.Header>
|
|
253
255
|
<Table.Row>
|
|
254
256
|
<Table.HeaderCell aria-label="Select all">
|
|
@@ -316,7 +318,7 @@ export const AllVariants: Story = {
|
|
|
316
318
|
<html.div style={storyStyles.stack}>
|
|
317
319
|
<html.div>
|
|
318
320
|
<html.span style={storyStyles.label}>Default</html.span>
|
|
319
|
-
<Table.Root
|
|
321
|
+
<Table.Root>
|
|
320
322
|
<Table.Header>
|
|
321
323
|
<Table.Row>
|
|
322
324
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
@@ -341,7 +343,7 @@ export const AllVariants: Story = {
|
|
|
341
343
|
|
|
342
344
|
<html.div>
|
|
343
345
|
<html.span style={storyStyles.label}>Striped</html.span>
|
|
344
|
-
<Table.Root variant="striped"
|
|
346
|
+
<Table.Root variant="striped">
|
|
345
347
|
<Table.Header>
|
|
346
348
|
<Table.Row>
|
|
347
349
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
@@ -371,7 +373,7 @@ export const AllVariants: Story = {
|
|
|
371
373
|
|
|
372
374
|
<html.div>
|
|
373
375
|
<html.span style={storyStyles.label}>Bordered</html.span>
|
|
374
|
-
<Table.Root variant="bordered"
|
|
376
|
+
<Table.Root variant="bordered">
|
|
375
377
|
<Table.Header>
|
|
376
378
|
<Table.Row>
|
|
377
379
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
@@ -396,7 +398,7 @@ export const AllVariants: Story = {
|
|
|
396
398
|
|
|
397
399
|
<html.div>
|
|
398
400
|
<html.span style={storyStyles.label}>Compact (sm)</html.span>
|
|
399
|
-
<Table.Root size="sm"
|
|
401
|
+
<Table.Root size="sm">
|
|
400
402
|
<Table.Header>
|
|
401
403
|
<Table.Row>
|
|
402
404
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
@@ -426,3 +428,347 @@ export const AllVariants: Story = {
|
|
|
426
428
|
await expect(tables.length).toBe(4)
|
|
427
429
|
},
|
|
428
430
|
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
// --- Sorting: demonstrates click-to-sort with hover indicators ---
|
|
434
|
+
|
|
435
|
+
function SortingDemo() {
|
|
436
|
+
const data = [
|
|
437
|
+
{name: 'Traefik', role: 'Reverse Proxy', port: 443, uptime: '14d 3h'},
|
|
438
|
+
{name: 'Pi-hole', role: 'DNS Filter', port: 80, uptime: '14d 3h'},
|
|
439
|
+
{name: 'Grafana', role: 'Monitoring', port: 3000, uptime: '7d 12h'},
|
|
440
|
+
{name: 'Portainer', role: 'Container Mgmt', port: 9000, uptime: '2d 5h'},
|
|
441
|
+
{name: 'Nginx', role: 'Web Server', port: 8080, uptime: '30d 1h'},
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
const cols = [
|
|
445
|
+
createColumnHelper<typeof data[0]>().accessor('name', {header: 'Name', enableSorting: true}),
|
|
446
|
+
createColumnHelper<typeof data[0]>().accessor('role', {header: 'Role', enableSorting: true}),
|
|
447
|
+
createColumnHelper<typeof data[0]>().accessor('port', {header: 'Port', enableSorting: true}),
|
|
448
|
+
createColumnHelper<typeof data[0]>().accessor('uptime', {header: 'Uptime', enableSorting: true}),
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
const [sorting, setSorting] = useState<SortingState>([])
|
|
452
|
+
|
|
453
|
+
const table = useReactTable({
|
|
454
|
+
data,
|
|
455
|
+
columns: cols,
|
|
456
|
+
state: {sorting},
|
|
457
|
+
onSortingChange: setSorting,
|
|
458
|
+
getCoreRowModel: getCoreRowModel(),
|
|
459
|
+
getSortedRowModel: getSortedRowModel(),
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<Table.Root>
|
|
464
|
+
<Table.Header>
|
|
465
|
+
{table.getHeaderGroups().map((hg) => (
|
|
466
|
+
<Table.Row key={hg.id}>
|
|
467
|
+
{hg.headers.map((header) => (
|
|
468
|
+
<Table.HeaderCell key={header.id}>
|
|
469
|
+
<html.span
|
|
470
|
+
style={fullFeaturedStyles.headerContent}
|
|
471
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
472
|
+
>
|
|
473
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
474
|
+
<Table.SortIndicator column={header.column} />
|
|
475
|
+
</html.span>
|
|
476
|
+
</Table.HeaderCell>
|
|
477
|
+
))}
|
|
478
|
+
</Table.Row>
|
|
479
|
+
))}
|
|
480
|
+
</Table.Header>
|
|
481
|
+
<Table.Body>
|
|
482
|
+
{table.getRowModel().rows.map((row) => (
|
|
483
|
+
<Table.Row key={row.id}>
|
|
484
|
+
{row.getVisibleCells().map((cell) => (
|
|
485
|
+
<Table.Cell key={cell.id}>
|
|
486
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
487
|
+
</Table.Cell>
|
|
488
|
+
))}
|
|
489
|
+
</Table.Row>
|
|
490
|
+
))}
|
|
491
|
+
</Table.Body>
|
|
492
|
+
</Table.Root>
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export const Sorting: Story = {
|
|
497
|
+
render: () => <SortingDemo />,
|
|
498
|
+
play: async ({canvas}) => {
|
|
499
|
+
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
500
|
+
await expect(canvas.getAllByRole('columnheader').length).toBe(4)
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// --- Pagination: demonstrates page navigation ---
|
|
505
|
+
|
|
506
|
+
function PaginationDemo() {
|
|
507
|
+
const data = generateUsers(35)
|
|
508
|
+
|
|
509
|
+
const cols = [
|
|
510
|
+
createColumnHelper<User>().accessor('name', {header: 'Name'}),
|
|
511
|
+
createColumnHelper<User>().accessor('role', {header: 'Role'}),
|
|
512
|
+
createColumnHelper<User>().accessor('email', {header: 'Email'}),
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10})
|
|
516
|
+
|
|
517
|
+
const table = useReactTable({
|
|
518
|
+
data,
|
|
519
|
+
columns: cols,
|
|
520
|
+
state: {pagination},
|
|
521
|
+
onPaginationChange: setPagination,
|
|
522
|
+
getCoreRowModel: getCoreRowModel(),
|
|
523
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<html.div style={fullFeaturedStyles.wrapper}>
|
|
528
|
+
<Table.Root>
|
|
529
|
+
<Table.Header>
|
|
530
|
+
{table.getHeaderGroups().map((hg) => (
|
|
531
|
+
<Table.Row key={hg.id}>
|
|
532
|
+
{hg.headers.map((header) => (
|
|
533
|
+
<Table.HeaderCell key={header.id}>
|
|
534
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
535
|
+
</Table.HeaderCell>
|
|
536
|
+
))}
|
|
537
|
+
</Table.Row>
|
|
538
|
+
))}
|
|
539
|
+
</Table.Header>
|
|
540
|
+
<Table.Body>
|
|
541
|
+
{table.getRowModel().rows.map((row) => (
|
|
542
|
+
<Table.Row key={row.id}>
|
|
543
|
+
{row.getVisibleCells().map((cell) => (
|
|
544
|
+
<Table.Cell key={cell.id}>
|
|
545
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
546
|
+
</Table.Cell>
|
|
547
|
+
))}
|
|
548
|
+
</Table.Row>
|
|
549
|
+
))}
|
|
550
|
+
</Table.Body>
|
|
551
|
+
</Table.Root>
|
|
552
|
+
<Table.Pagination table={table} />
|
|
553
|
+
</html.div>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export const WithPagination: Story = {
|
|
558
|
+
render: () => <PaginationDemo />,
|
|
559
|
+
play: async ({canvas}) => {
|
|
560
|
+
await expect(canvas.getByText('1 / 4')).toBeInTheDocument()
|
|
561
|
+
await expect(canvas.getByText('Next')).toBeInTheDocument()
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// --- Filtering: demonstrates column filter inputs ---
|
|
566
|
+
|
|
567
|
+
function FilteringDemo() {
|
|
568
|
+
const data = generateUsers(20)
|
|
569
|
+
|
|
570
|
+
const cols = [
|
|
571
|
+
createColumnHelper<User>().accessor('name', {header: 'Name', enableColumnFilter: true}),
|
|
572
|
+
createColumnHelper<User>().accessor('role', {header: 'Role', enableColumnFilter: false}),
|
|
573
|
+
createColumnHelper<User>().accessor('status', {header: 'Status', enableColumnFilter: false}),
|
|
574
|
+
createColumnHelper<User>().accessor('email', {header: 'Email', enableColumnFilter: true}),
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
578
|
+
|
|
579
|
+
const table = useReactTable({
|
|
580
|
+
data,
|
|
581
|
+
columns: cols,
|
|
582
|
+
state: {columnFilters},
|
|
583
|
+
onColumnFiltersChange: setColumnFilters,
|
|
584
|
+
getCoreRowModel: getCoreRowModel(),
|
|
585
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<Table.Root>
|
|
590
|
+
<Table.Header>
|
|
591
|
+
{table.getHeaderGroups().map((hg) => (
|
|
592
|
+
<Table.Row key={hg.id}>
|
|
593
|
+
{hg.headers.map((header) => (
|
|
594
|
+
<Table.HeaderCell key={header.id}>
|
|
595
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
596
|
+
{header.column.getCanFilter() && (
|
|
597
|
+
<Table.ColumnFilter
|
|
598
|
+
column={header.column}
|
|
599
|
+
placeholder={`Filter ${header.column.id}...`}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
602
|
+
</Table.HeaderCell>
|
|
603
|
+
))}
|
|
604
|
+
</Table.Row>
|
|
605
|
+
))}
|
|
606
|
+
</Table.Header>
|
|
607
|
+
<Table.Body>
|
|
608
|
+
{table.getRowModel().rows.map((row) => (
|
|
609
|
+
<Table.Row key={row.id}>
|
|
610
|
+
{row.getVisibleCells().map((cell) => (
|
|
611
|
+
<Table.Cell key={cell.id}>
|
|
612
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
613
|
+
</Table.Cell>
|
|
614
|
+
))}
|
|
615
|
+
</Table.Row>
|
|
616
|
+
))}
|
|
617
|
+
</Table.Body>
|
|
618
|
+
</Table.Root>
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export const WithFiltering: Story = {
|
|
623
|
+
render: () => <FilteringDemo />,
|
|
624
|
+
play: async ({canvas}) => {
|
|
625
|
+
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
626
|
+
await expect(canvas.getByPlaceholderText('Filter name...')).toBeInTheDocument()
|
|
627
|
+
await expect(canvas.getByPlaceholderText('Filter email...')).toBeInTheDocument()
|
|
628
|
+
},
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
// --- FullFeatured: useDataTable with pagination, sorting, and filtering ---
|
|
633
|
+
|
|
634
|
+
interface User {
|
|
635
|
+
name: string
|
|
636
|
+
role: string
|
|
637
|
+
status: string
|
|
638
|
+
email: string
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const firstNames = [
|
|
642
|
+
'Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Hank',
|
|
643
|
+
'Iris', 'Jack', 'Karen', 'Leo', 'Mona', 'Nick', 'Olivia', 'Paul',
|
|
644
|
+
'Quinn', 'Rita', 'Sam', 'Tina', 'Uma', 'Vince', 'Wendy', 'Xander',
|
|
645
|
+
'Yara', 'Zach',
|
|
646
|
+
]
|
|
647
|
+
const lastNames = [
|
|
648
|
+
'Adams', 'Brown', 'Clark', 'Davis', 'Evans', 'Foster', 'Garcia',
|
|
649
|
+
'Hill', 'Ito', 'Jones', 'Kim', 'Lee', 'Moore', 'Nash', 'Owen',
|
|
650
|
+
'Patel', 'Quinn', 'Reed', 'Smith', 'Tran', 'Ueda', 'Voss', 'Wang',
|
|
651
|
+
'Xu', 'Yang', 'Zhang',
|
|
652
|
+
]
|
|
653
|
+
const roles = ['Admin', 'Editor', 'Viewer', 'Moderator', 'Analyst']
|
|
654
|
+
const statuses = ['Active', 'Inactive', 'Pending', 'Suspended']
|
|
655
|
+
|
|
656
|
+
function generateUsers(count: number): User[] {
|
|
657
|
+
return Array.from({length: count}, (_, i) => {
|
|
658
|
+
const first = firstNames[i % firstNames.length]
|
|
659
|
+
const last = lastNames[(i * 7) % lastNames.length]
|
|
660
|
+
return {
|
|
661
|
+
name: `${first} ${last}`,
|
|
662
|
+
role: roles[i % roles.length],
|
|
663
|
+
status: statuses[i % statuses.length],
|
|
664
|
+
email: `${first.toLowerCase()}.${last.toLowerCase()}@example.com`,
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const fakeUsers = generateUsers(50)
|
|
670
|
+
|
|
671
|
+
const columnHelper = createColumnHelper<User>()
|
|
672
|
+
|
|
673
|
+
const fullFeaturedColumns = [
|
|
674
|
+
columnHelper.accessor('name', {
|
|
675
|
+
header: 'Name',
|
|
676
|
+
enableSorting: true,
|
|
677
|
+
enableColumnFilter: true,
|
|
678
|
+
}),
|
|
679
|
+
columnHelper.accessor('role', {
|
|
680
|
+
header: 'Role',
|
|
681
|
+
enableSorting: true,
|
|
682
|
+
enableColumnFilter: false,
|
|
683
|
+
}),
|
|
684
|
+
columnHelper.accessor('status', {
|
|
685
|
+
header: 'Status',
|
|
686
|
+
enableSorting: true,
|
|
687
|
+
enableColumnFilter: false,
|
|
688
|
+
}),
|
|
689
|
+
columnHelper.accessor('email', {
|
|
690
|
+
header: 'Email',
|
|
691
|
+
enableSorting: true,
|
|
692
|
+
enableColumnFilter: true,
|
|
693
|
+
}),
|
|
694
|
+
]
|
|
695
|
+
|
|
696
|
+
const fullFeaturedStyles = css.create({
|
|
697
|
+
wrapper: {display: 'flex', flexDirection: 'column', gap: 0},
|
|
698
|
+
headerContent: {
|
|
699
|
+
display: 'flex',
|
|
700
|
+
flexDirection: 'column',
|
|
701
|
+
cursor: 'pointer',
|
|
702
|
+
userSelect: 'none',
|
|
703
|
+
},
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
function FullFeaturedDemo() {
|
|
707
|
+
const table = useDataTable(fakeUsers, fullFeaturedColumns, {
|
|
708
|
+
pagination: {pageSize: 10},
|
|
709
|
+
enableSorting: true,
|
|
710
|
+
enableFiltering: true,
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
return (
|
|
714
|
+
<html.div style={fullFeaturedStyles.wrapper}>
|
|
715
|
+
<Table.Root>
|
|
716
|
+
<Table.Header>
|
|
717
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
718
|
+
<Table.Row key={headerGroup.id}>
|
|
719
|
+
{headerGroup.headers.map((header) => (
|
|
720
|
+
<Table.HeaderCell key={header.id}>
|
|
721
|
+
<html.div
|
|
722
|
+
style={fullFeaturedStyles.headerContent}
|
|
723
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
724
|
+
>
|
|
725
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
726
|
+
<Table.SortIndicator column={header.column} />
|
|
727
|
+
</html.div>
|
|
728
|
+
{(header.column.id === 'name' || header.column.id === 'email') && (
|
|
729
|
+
<Table.ColumnFilter
|
|
730
|
+
column={header.column}
|
|
731
|
+
placeholder={`Filter ${header.column.id}...`}
|
|
732
|
+
/>
|
|
733
|
+
)}
|
|
734
|
+
</Table.HeaderCell>
|
|
735
|
+
))}
|
|
736
|
+
</Table.Row>
|
|
737
|
+
))}
|
|
738
|
+
</Table.Header>
|
|
739
|
+
<Table.Body>
|
|
740
|
+
{table.getRowModel().rows.map((row) => (
|
|
741
|
+
<Table.Row key={row.id}>
|
|
742
|
+
{row.getVisibleCells().map((cell) => (
|
|
743
|
+
<Table.Cell key={cell.id}>
|
|
744
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
745
|
+
</Table.Cell>
|
|
746
|
+
))}
|
|
747
|
+
</Table.Row>
|
|
748
|
+
))}
|
|
749
|
+
</Table.Body>
|
|
750
|
+
</Table.Root>
|
|
751
|
+
<Table.Pagination table={table} />
|
|
752
|
+
</html.div>
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export const FullFeatured: Story = {
|
|
757
|
+
render: () => <FullFeaturedDemo />,
|
|
758
|
+
play: async ({canvas}) => {
|
|
759
|
+
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
760
|
+
|
|
761
|
+
// Should render 10 body rows (pageSize) + 1 header row
|
|
762
|
+
const rows = canvas.getAllByRole('row')
|
|
763
|
+
await expect(rows.length).toBe(11)
|
|
764
|
+
|
|
765
|
+
// All 4 column headers present
|
|
766
|
+
const columnHeaders = canvas.getAllByRole('columnheader')
|
|
767
|
+
await expect(columnHeaders.length).toBe(4)
|
|
768
|
+
|
|
769
|
+
// Pagination is visible (50 rows / 10 per page = 5 pages)
|
|
770
|
+
await expect(canvas.getByText('1 / 5')).toBeInTheDocument()
|
|
771
|
+
await expect(canvas.getByText('Next')).toBeInTheDocument()
|
|
772
|
+
await expect(canvas.getByText('Previous')).toBeInTheDocument()
|
|
773
|
+
},
|
|
774
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {type ReactNode, createContext, useContext, Children} from 'react'
|
|
1
|
+
import {type ReactNode, type MutableRefObject, createContext, useContext, useRef, Children, isValidElement} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
|
+
import {Pagination} from './Pagination'
|
|
5
|
+
import {SortIndicator} from './SortIndicator'
|
|
6
|
+
import {ColumnFilter} from './ColumnFilter'
|
|
4
7
|
|
|
5
8
|
// --- Types ---
|
|
6
9
|
|
|
@@ -12,8 +15,8 @@ export type TableSize = 'sm' | 'md'
|
|
|
12
15
|
interface TableContextValue {
|
|
13
16
|
variant: TableVariant
|
|
14
17
|
size: TableSize
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
/** Mutable ref: header Row writes inferred template, Root reads it */
|
|
19
|
+
inferredTemplateRef: MutableRefObject<string | null>
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
const TableContext = createContext<TableContextValue | null>(null)
|
|
@@ -24,23 +27,65 @@ function useTable() {
|
|
|
24
27
|
return ctx
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
// --- HeaderContext
|
|
30
|
+
// --- HeaderContext ---
|
|
28
31
|
|
|
29
32
|
const HeaderContext = createContext(false)
|
|
30
33
|
|
|
34
|
+
// --- Template context: set after first render pass ---
|
|
35
|
+
|
|
36
|
+
const TemplateContext = createContext<string | null>(null)
|
|
37
|
+
|
|
31
38
|
// --- Root ---
|
|
32
39
|
|
|
33
40
|
interface RootProps {
|
|
34
41
|
children: ReactNode
|
|
35
42
|
variant?: TableVariant
|
|
36
43
|
size?: TableSize
|
|
37
|
-
columns: number
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Scans the React tree for HeaderCell elements to extract width props.
|
|
48
|
+
* Returns a grid-template-columns string.
|
|
49
|
+
*/
|
|
50
|
+
function extractTemplate(children: ReactNode): string {
|
|
51
|
+
const widths: string[] = []
|
|
52
|
+
|
|
53
|
+
function walk(node: ReactNode) {
|
|
54
|
+
Children.forEach(node, (child) => {
|
|
55
|
+
if (!isValidElement(child)) return
|
|
56
|
+
const props = child.props as Record<string, any>
|
|
57
|
+
// Check if this looks like a HeaderCell (has width prop or is inside Header)
|
|
58
|
+
const displayName = (child.type as any)?.name || (child.type as any)?.displayName || ''
|
|
59
|
+
if (displayName === 'HeaderCell' || child.type === HeaderCell) {
|
|
60
|
+
widths.push(props.width || '1fr')
|
|
61
|
+
} else if (props.children) {
|
|
62
|
+
walk(props.children)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
walk(children)
|
|
68
|
+
return widths.length > 0 ? widths.join(' ') : ''
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Root({children, variant = 'default', size = 'md'}: RootProps) {
|
|
72
|
+
const inferredTemplateRef = useRef<string | null>(null)
|
|
73
|
+
|
|
74
|
+
// Extract template from HeaderCell children synchronously
|
|
75
|
+
const template = extractTemplate(children)
|
|
76
|
+
if (template) {
|
|
77
|
+
inferredTemplateRef.current = template
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
return (
|
|
42
|
-
<TableContext.Provider value={{variant, size,
|
|
43
|
-
<html.div
|
|
81
|
+
<TableContext.Provider value={{variant, size, inferredTemplateRef}}>
|
|
82
|
+
<html.div
|
|
83
|
+
role="table"
|
|
84
|
+
style={[
|
|
85
|
+
styles.root,
|
|
86
|
+
template ? styles.gridColumns(template) : undefined,
|
|
87
|
+
]}
|
|
88
|
+
>
|
|
44
89
|
{children}
|
|
45
90
|
</html.div>
|
|
46
91
|
</TableContext.Provider>
|
|
@@ -67,7 +112,7 @@ function Body({children}: {children: ReactNode}) {
|
|
|
67
112
|
|
|
68
113
|
return (
|
|
69
114
|
<HeaderContext.Provider value={false}>
|
|
70
|
-
<html.div role="rowgroup">
|
|
115
|
+
<html.div role="rowgroup" style={styles.body}>
|
|
71
116
|
{childArray.map((child, index) => {
|
|
72
117
|
if (variant === 'striped') {
|
|
73
118
|
return (
|
|
@@ -83,13 +128,12 @@ function Body({children}: {children: ReactNode}) {
|
|
|
83
128
|
)
|
|
84
129
|
}
|
|
85
130
|
|
|
86
|
-
// Row index context for striped variant
|
|
87
131
|
const RowIndexContext = createContext<number>(-1)
|
|
88
132
|
|
|
89
133
|
// --- Row ---
|
|
90
134
|
|
|
91
135
|
function Row({children}: {children: ReactNode}) {
|
|
92
|
-
const {variant
|
|
136
|
+
const {variant} = useTable()
|
|
93
137
|
const isHeader = useContext(HeaderContext)
|
|
94
138
|
const rowIndex = useContext(RowIndexContext)
|
|
95
139
|
const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
|
|
@@ -100,7 +144,6 @@ function Row({children}: {children: ReactNode}) {
|
|
|
100
144
|
role="row"
|
|
101
145
|
style={[
|
|
102
146
|
styles.row,
|
|
103
|
-
styles.gridColumns(columns),
|
|
104
147
|
!isHeader && styles.bodyRow,
|
|
105
148
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
106
149
|
]}
|
|
@@ -116,19 +159,18 @@ function Row({children}: {children: ReactNode}) {
|
|
|
116
159
|
)
|
|
117
160
|
}
|
|
118
161
|
|
|
119
|
-
|
|
120
|
-
const CellIndexContext = createContext<{index: number; total: number}>({
|
|
121
|
-
index: 0,
|
|
122
|
-
total: 0,
|
|
123
|
-
})
|
|
162
|
+
const CellIndexContext = createContext<{index: number; total: number}>({index: 0, total: 0})
|
|
124
163
|
|
|
125
164
|
// --- HeaderCell ---
|
|
126
165
|
|
|
127
166
|
function HeaderCell({
|
|
128
167
|
children,
|
|
168
|
+
width: _width,
|
|
129
169
|
'aria-label': ariaLabel,
|
|
130
170
|
}: {
|
|
131
171
|
children?: ReactNode
|
|
172
|
+
/** Column width: CSS value like '40px', '2fr', 'max-content'. Defaults to '1fr'. */
|
|
173
|
+
width?: string
|
|
132
174
|
'aria-label'?: string
|
|
133
175
|
}) {
|
|
134
176
|
const {size, variant} = useTable()
|
|
@@ -182,4 +224,7 @@ export const Table = {
|
|
|
182
224
|
Row,
|
|
183
225
|
HeaderCell,
|
|
184
226
|
Cell,
|
|
227
|
+
Pagination,
|
|
228
|
+
SortIndicator,
|
|
229
|
+
ColumnFilter,
|
|
185
230
|
}
|
|
@@ -4,8 +4,9 @@ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
|
|
6
6
|
export const styles = css.create({
|
|
7
|
-
// Root
|
|
7
|
+
// Root — the single grid container for the entire table
|
|
8
8
|
root: {
|
|
9
|
+
display: 'grid',
|
|
9
10
|
borderWidth: 1,
|
|
10
11
|
borderStyle: 'solid',
|
|
11
12
|
borderColor: colors.border,
|
|
@@ -18,19 +19,28 @@ export const styles = css.create({
|
|
|
18
19
|
|
|
19
20
|
// Header group
|
|
20
21
|
header: {
|
|
22
|
+
display: 'grid',
|
|
23
|
+
gridColumn: '1 / -1',
|
|
24
|
+
gridTemplateColumns: 'subgrid',
|
|
21
25
|
backgroundColor: colors.bgCard,
|
|
22
26
|
},
|
|
23
27
|
|
|
24
|
-
//
|
|
28
|
+
// Body group
|
|
29
|
+
body: {
|
|
30
|
+
display: 'grid',
|
|
31
|
+
gridColumn: '1 / -1',
|
|
32
|
+
gridTemplateColumns: 'subgrid',
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Row — spans all columns, uses subgrid to share parent tracks
|
|
25
36
|
row: {
|
|
26
37
|
display: 'grid',
|
|
38
|
+
gridColumn: '1 / -1',
|
|
39
|
+
gridTemplateColumns: 'subgrid',
|
|
27
40
|
borderBottomWidth: 1,
|
|
28
41
|
borderBottomStyle: 'solid',
|
|
29
42
|
borderBottomColor: colors.border,
|
|
30
43
|
},
|
|
31
|
-
rowLastChild: {
|
|
32
|
-
borderBottomWidth: 0,
|
|
33
|
-
},
|
|
34
44
|
bodyRow: {
|
|
35
45
|
backgroundColor: {
|
|
36
46
|
default: 'transparent',
|
|
@@ -90,8 +100,8 @@ export const styles = css.create({
|
|
|
90
100
|
borderRightWidth: 0,
|
|
91
101
|
},
|
|
92
102
|
|
|
93
|
-
// Dynamic
|
|
94
|
-
gridColumns: (
|
|
95
|
-
gridTemplateColumns:
|
|
103
|
+
// Dynamic: grid columns applied on Root
|
|
104
|
+
gridColumns: (template: string) => ({
|
|
105
|
+
gridTemplateColumns: template,
|
|
96
106
|
}),
|
|
97
107
|
})
|