@duro-app/ui 0.23.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/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/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/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 +7 -1
- 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 +11 -3
- 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/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2181 -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/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/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/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 +346 -0
- package/src/components/Table/Table.tsx +52 -23
- package/src/components/Table/styles.css.ts +16 -6
- package/src/components/Table/useDataTable.ts +113 -0
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +3 -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: {
|
|
@@ -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
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,7 +15,7 @@ export type TableSize = 'sm' | 'md'
|
|
|
12
15
|
interface TableContextValue {
|
|
13
16
|
variant: TableVariant
|
|
14
17
|
size: TableSize
|
|
15
|
-
/** Mutable ref: header Row writes inferred template,
|
|
18
|
+
/** Mutable ref: header Row writes inferred template, Root reads it */
|
|
16
19
|
inferredTemplateRef: MutableRefObject<string | null>
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -28,6 +31,10 @@ function useTable() {
|
|
|
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 {
|
|
@@ -36,12 +43,49 @@ interface RootProps {
|
|
|
36
43
|
size?: TableSize
|
|
37
44
|
}
|
|
38
45
|
|
|
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
|
+
|
|
39
71
|
function Root({children, variant = 'default', size = 'md'}: RootProps) {
|
|
40
72
|
const inferredTemplateRef = useRef<string | null>(null)
|
|
41
73
|
|
|
74
|
+
// Extract template from HeaderCell children synchronously
|
|
75
|
+
const template = extractTemplate(children)
|
|
76
|
+
if (template) {
|
|
77
|
+
inferredTemplateRef.current = template
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
return (
|
|
43
81
|
<TableContext.Provider value={{variant, size, inferredTemplateRef}}>
|
|
44
|
-
<html.div
|
|
82
|
+
<html.div
|
|
83
|
+
role="table"
|
|
84
|
+
style={[
|
|
85
|
+
styles.root,
|
|
86
|
+
template ? styles.gridColumns(template) : undefined,
|
|
87
|
+
]}
|
|
88
|
+
>
|
|
45
89
|
{children}
|
|
46
90
|
</html.div>
|
|
47
91
|
</TableContext.Provider>
|
|
@@ -68,7 +112,7 @@ function Body({children}: {children: ReactNode}) {
|
|
|
68
112
|
|
|
69
113
|
return (
|
|
70
114
|
<HeaderContext.Provider value={false}>
|
|
71
|
-
<html.div role="rowgroup">
|
|
115
|
+
<html.div role="rowgroup" style={styles.body}>
|
|
72
116
|
{childArray.map((child, index) => {
|
|
73
117
|
if (variant === 'striped') {
|
|
74
118
|
return (
|
|
@@ -89,35 +133,17 @@ const RowIndexContext = createContext<number>(-1)
|
|
|
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
|
|
96
140
|
const childArray = Children.toArray(children)
|
|
97
141
|
|
|
98
|
-
let template: string
|
|
99
|
-
|
|
100
|
-
if (isHeader) {
|
|
101
|
-
// Build template from HeaderCell width props
|
|
102
|
-
const widths: string[] = []
|
|
103
|
-
Children.forEach(children, (child) => {
|
|
104
|
-
if (isValidElement(child)) {
|
|
105
|
-
const props = child.props as {width?: string}
|
|
106
|
-
widths.push(props.width || '1fr')
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
template = widths.join(' ')
|
|
110
|
-
inferredTemplateRef.current = template
|
|
111
|
-
} else {
|
|
112
|
-
template = inferredTemplateRef.current ?? `repeat(${childArray.length}, 1fr)`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
142
|
return (
|
|
116
143
|
<html.div
|
|
117
144
|
role="row"
|
|
118
145
|
style={[
|
|
119
146
|
styles.row,
|
|
120
|
-
styles.gridColumns(template),
|
|
121
147
|
!isHeader && styles.bodyRow,
|
|
122
148
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
123
149
|
]}
|
|
@@ -143,7 +169,7 @@ function HeaderCell({
|
|
|
143
169
|
'aria-label': ariaLabel,
|
|
144
170
|
}: {
|
|
145
171
|
children?: ReactNode
|
|
146
|
-
/** Column width: CSS value like '40px', '2fr', '
|
|
172
|
+
/** Column width: CSS value like '40px', '2fr', 'max-content'. Defaults to '1fr'. */
|
|
147
173
|
width?: string
|
|
148
174
|
'aria-label'?: string
|
|
149
175
|
}) {
|
|
@@ -198,4 +224,7 @@ export const Table = {
|
|
|
198
224
|
Row,
|
|
199
225
|
HeaderCell,
|
|
200
226
|
Cell,
|
|
227
|
+
Pagination,
|
|
228
|
+
SortIndicator,
|
|
229
|
+
ColumnFilter,
|
|
201
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,7 +100,7 @@ export const styles = css.create({
|
|
|
90
100
|
borderRightWidth: 0,
|
|
91
101
|
},
|
|
92
102
|
|
|
93
|
-
// Dynamic
|
|
103
|
+
// Dynamic: grid columns applied on Root
|
|
94
104
|
gridColumns: (template: string) => ({
|
|
95
105
|
gridTemplateColumns: template,
|
|
96
106
|
}),
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
useReactTable,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getPaginationRowModel,
|
|
6
|
+
getSortedRowModel,
|
|
7
|
+
getFilteredRowModel,
|
|
8
|
+
type ColumnDef,
|
|
9
|
+
type SortingState,
|
|
10
|
+
type ColumnFiltersState,
|
|
11
|
+
type PaginationState,
|
|
12
|
+
type RowSelectionState,
|
|
13
|
+
type TableOptions,
|
|
14
|
+
type Table,
|
|
15
|
+
} from '@tanstack/react-table'
|
|
16
|
+
|
|
17
|
+
export interface UseDataTableOptions<TData> {
|
|
18
|
+
/** Initial pagination state */
|
|
19
|
+
pagination?: {pageSize?: number; pageIndex?: number}
|
|
20
|
+
/** Enable sorting on columns that don't explicitly disable it */
|
|
21
|
+
enableSorting?: boolean
|
|
22
|
+
/** Enable column filtering */
|
|
23
|
+
enableFiltering?: boolean
|
|
24
|
+
/** Enable row selection (checkbox) */
|
|
25
|
+
enableRowSelection?: boolean | ((row: any) => boolean)
|
|
26
|
+
/** Callback when row selection changes */
|
|
27
|
+
onRowSelectionChange?: (selection: RowSelectionState) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Thin wrapper around TanStack's useReactTable.
|
|
32
|
+
* Pre-configures row models for pagination, sorting, and filtering.
|
|
33
|
+
* Returns the full TanStack table instance.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const table = useDataTable(users, columns, {
|
|
38
|
+
* pagination: { pageSize: 20 },
|
|
39
|
+
* enableSorting: true,
|
|
40
|
+
* enableFiltering: true,
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function useDataTable<TData>(
|
|
45
|
+
data: TData[],
|
|
46
|
+
columns: ColumnDef<TData, any>[],
|
|
47
|
+
options: UseDataTableOptions<TData> = {},
|
|
48
|
+
): Table<TData> {
|
|
49
|
+
const {
|
|
50
|
+
pagination: paginationInit,
|
|
51
|
+
enableSorting = false,
|
|
52
|
+
enableFiltering = false,
|
|
53
|
+
enableRowSelection = false,
|
|
54
|
+
onRowSelectionChange: onSelectionChangeProp,
|
|
55
|
+
} = options
|
|
56
|
+
|
|
57
|
+
const [sorting, setSorting] = useState<SortingState>([])
|
|
58
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
59
|
+
const [pagination, setPagination] = useState<PaginationState>({
|
|
60
|
+
pageIndex: paginationInit?.pageIndex ?? 0,
|
|
61
|
+
pageSize: paginationInit?.pageSize ?? 10,
|
|
62
|
+
})
|
|
63
|
+
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
64
|
+
|
|
65
|
+
const handleRowSelectionChange = (updater: any) => {
|
|
66
|
+
setRowSelection(updater)
|
|
67
|
+
if (onSelectionChangeProp) {
|
|
68
|
+
const next = typeof updater === 'function' ? updater(rowSelection) : updater
|
|
69
|
+
onSelectionChangeProp(next)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const tableOptions: TableOptions<TData> = {
|
|
74
|
+
data,
|
|
75
|
+
columns,
|
|
76
|
+
getCoreRowModel: getCoreRowModel(),
|
|
77
|
+
|
|
78
|
+
// Sorting
|
|
79
|
+
...(enableSorting && {
|
|
80
|
+
getSortedRowModel: getSortedRowModel(),
|
|
81
|
+
onSortingChange: setSorting,
|
|
82
|
+
state: {sorting},
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
// Filtering
|
|
86
|
+
...(enableFiltering && {
|
|
87
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
88
|
+
onColumnFiltersChange: setColumnFilters,
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
// Pagination
|
|
92
|
+
...(paginationInit && {
|
|
93
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
94
|
+
onPaginationChange: setPagination,
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
// Row selection
|
|
98
|
+
...(enableRowSelection && {
|
|
99
|
+
enableRowSelection: typeof enableRowSelection === 'function' ? enableRowSelection : true,
|
|
100
|
+
onRowSelectionChange: handleRowSelectionChange,
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
// Merge all state
|
|
104
|
+
state: {
|
|
105
|
+
...(enableSorting && {sorting}),
|
|
106
|
+
...(enableFiltering && {columnFilters}),
|
|
107
|
+
...(paginationInit && {pagination}),
|
|
108
|
+
...(enableRowSelection && {rowSelection}),
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return useReactTable(tableOptions)
|
|
113
|
+
}
|
package/src/docs/helpers.tsx
CHANGED
|
@@ -64,7 +64,7 @@ export function TypeScaleTable({rows, label}: TypeScaleTableProps) {
|
|
|
64
64
|
return (
|
|
65
65
|
<html.div>
|
|
66
66
|
{label && <html.h3 style={styles.label}>{label}</html.h3>}
|
|
67
|
-
<Table.Root
|
|
67
|
+
<Table.Root variant="striped" size="sm">
|
|
68
68
|
<Table.Header>
|
|
69
69
|
<Table.Row>
|
|
70
70
|
<Table.HeaderCell>Step</Table.HeaderCell>
|
|
@@ -106,7 +106,7 @@ export function TokenTable({tokens, label}: TokenTableProps) {
|
|
|
106
106
|
return (
|
|
107
107
|
<html.div>
|
|
108
108
|
{label && <html.h3 style={styles.label}>{label}</html.h3>}
|
|
109
|
-
<Table.Root
|
|
109
|
+
<Table.Root variant="striped" size="sm">
|
|
110
110
|
<Table.Header>
|
|
111
111
|
<Table.Row>
|
|
112
112
|
<Table.HeaderCell>Token</Table.HeaderCell>
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export {Alert, type AlertVariant} from './components/Alert/Alert'
|
|
|
8
8
|
export {Icon, type IconName} from './components/Icon'
|
|
9
9
|
export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
|
|
10
10
|
export {Button, type ButtonVariant, type ButtonSize} from './components/Button/Button'
|
|
11
|
+
export {ButtonGroup} from './components/ButtonGroup/ButtonGroup'
|
|
11
12
|
export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
12
13
|
export {Card, type CardVariant, type CardSize} from './components/Card/Card'
|
|
13
14
|
export {Checkbox} from './components/Checkbox/Checkbox'
|
|
@@ -41,6 +42,8 @@ export {
|
|
|
41
42
|
type StatusIconVariant,
|
|
42
43
|
} from './components/StatusIcon/StatusIcon'
|
|
43
44
|
export {Table, type TableVariant, type TableSize} from './components/Table/Table'
|
|
45
|
+
// TanStack Table: consumers install @tanstack/react-table directly and use
|
|
46
|
+
// Table.Pagination, Table.SortIndicator, Table.ColumnFilter for styled integration
|
|
44
47
|
export {Tabs} from './components/Tabs/Tabs'
|
|
45
48
|
export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
46
49
|
export {ThemeProvider, type ThemeName} from './components/ThemeProvider/ThemeProvider'
|