@duro-app/ui 0.23.0 → 0.25.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/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +44 -0
- package/dist/components/Combobox/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.meta.d.ts +3 -0
- package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.stories.d.ts +9 -0
- package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -0
- package/dist/components/Combobox/ComboboxContext.d.ts +19 -0
- package/dist/components/Combobox/ComboboxContext.d.ts.map +1 -0
- package/dist/components/Combobox/styles.css.d.ts +112 -0
- package/dist/components/Combobox/styles.css.d.ts.map +1 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts +15 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -0
- package/dist/components/Field/Field.d.ts +4 -0
- package/dist/components/Field/Field.d.ts.map +1 -1
- package/dist/components/Field/Field.stories.d.ts +3 -0
- package/dist/components/Field/Field.stories.d.ts.map +1 -1
- package/dist/components/Field/FieldContext.d.ts +5 -0
- package/dist/components/Field/FieldContext.d.ts.map +1 -1
- package/dist/components/Field/styles.css.d.ts +26 -0
- package/dist/components/Field/styles.css.d.ts.map +1 -1
- package/dist/components/Form/Form.d.ts +7 -2
- package/dist/components/Form/Form.d.ts.map +1 -1
- package/dist/components/Form/Form.stories.d.ts +6 -0
- package/dist/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/components/Form/FormContext.d.ts +9 -2
- package/dist/components/Form/FormContext.d.ts.map +1 -1
- package/dist/components/Input/Input.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.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.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 +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/components/Textarea/Textarea.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 +2863 -2026
- 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 +32 -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/CheckboxGroup/CheckboxGroup.meta.ts +4 -1
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -9
- package/src/components/Combobox/Combobox.meta.ts +35 -0
- package/src/components/Combobox/Combobox.stories.tsx +119 -0
- package/src/components/Combobox/Combobox.tsx +247 -0
- package/src/components/Combobox/ComboboxContext.ts +26 -0
- package/src/components/Combobox/styles.css.ts +119 -0
- package/src/components/Combobox/useComboboxRoot.ts +235 -0
- package/src/components/Field/Field.stories.tsx +60 -0
- package/src/components/Field/Field.tsx +97 -9
- package/src/components/Field/FieldContext.ts +5 -0
- package/src/components/Field/styles.css.ts +26 -0
- package/src/components/Form/Form.stories.tsx +275 -0
- package/src/components/Form/Form.tsx +73 -7
- package/src/components/Form/FormContext.ts +10 -1
- package/src/components/Input/Input.tsx +2 -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 +4 -1
- package/src/components/RadioGroup/RadioGroup.tsx +1 -4
- package/src/components/Table/ColumnFilter.tsx +46 -0
- package/src/components/Table/Pagination.tsx +82 -0
- package/src/components/Table/SortIndicator.tsx +53 -0
- package/src/components/Table/Table.stories.tsx +397 -0
- package/src/components/Table/Table.tsx +58 -24
- package/src/components/Table/styles.css.ts +16 -6
- package/src/components/Table/useDataTable.ts +99 -0
- package/src/components/Textarea/Textarea.tsx +2 -1
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +5 -0
- package/src/strict.css +5 -0
|
@@ -9,6 +9,11 @@ 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, type PaginationState} from '@tanstack/react-table'
|
|
13
|
+
import {useDataTable} from './useDataTable'
|
|
14
|
+
import {Combobox} from '../Combobox/Combobox'
|
|
15
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
16
|
+
|
|
12
17
|
const meta: Meta = {
|
|
13
18
|
title: 'Components/Table',
|
|
14
19
|
argTypes: {
|
|
@@ -426,3 +431,395 @@ export const AllVariants: Story = {
|
|
|
426
431
|
await expect(tables.length).toBe(4)
|
|
427
432
|
},
|
|
428
433
|
}
|
|
434
|
+
|
|
435
|
+
// --- Sorting: demonstrates click-to-sort with hover indicators ---
|
|
436
|
+
|
|
437
|
+
type Service = {name: string; role: string; port: number; uptime: string}
|
|
438
|
+
|
|
439
|
+
const sortingData: Service[] = [
|
|
440
|
+
{name: 'Traefik', role: 'Reverse Proxy', port: 443, uptime: '14d 3h'},
|
|
441
|
+
{name: 'Pi-hole', role: 'DNS Filter', port: 80, uptime: '14d 3h'},
|
|
442
|
+
{name: 'Grafana', role: 'Monitoring', port: 3000, uptime: '7d 12h'},
|
|
443
|
+
{name: 'Portainer', role: 'Container Mgmt', port: 9000, uptime: '2d 5h'},
|
|
444
|
+
{name: 'Nginx', role: 'Web Server', port: 8080, uptime: '30d 1h'},
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
const sortingCols = [
|
|
448
|
+
createColumnHelper<Service>().accessor('name', {header: 'Name', enableSorting: true}),
|
|
449
|
+
createColumnHelper<Service>().accessor('role', {header: 'Role', enableSorting: true}),
|
|
450
|
+
createColumnHelper<Service>().accessor('port', {header: 'Port', enableSorting: true}),
|
|
451
|
+
createColumnHelper<Service>().accessor('uptime', {header: 'Uptime', enableSorting: true}),
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
function SortingDemo() {
|
|
455
|
+
const [sorting, setSorting] = useState<SortingState>([])
|
|
456
|
+
|
|
457
|
+
const table = useReactTable({
|
|
458
|
+
data: sortingData,
|
|
459
|
+
columns: sortingCols,
|
|
460
|
+
state: {sorting},
|
|
461
|
+
onSortingChange: setSorting,
|
|
462
|
+
getCoreRowModel: getCoreRowModel(),
|
|
463
|
+
getSortedRowModel: getSortedRowModel(),
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<Table.Root>
|
|
468
|
+
<Table.Header>
|
|
469
|
+
{table.getHeaderGroups().map((hg) => (
|
|
470
|
+
<Table.Row key={hg.id}>
|
|
471
|
+
{hg.headers.map((header) => (
|
|
472
|
+
<Table.HeaderCell key={header.id}>
|
|
473
|
+
<html.span
|
|
474
|
+
style={fullFeaturedStyles.headerContent}
|
|
475
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
476
|
+
>
|
|
477
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
478
|
+
<Table.SortIndicator column={header.column} />
|
|
479
|
+
</html.span>
|
|
480
|
+
</Table.HeaderCell>
|
|
481
|
+
))}
|
|
482
|
+
</Table.Row>
|
|
483
|
+
))}
|
|
484
|
+
</Table.Header>
|
|
485
|
+
<Table.Body>
|
|
486
|
+
{table.getRowModel().rows.map((row) => (
|
|
487
|
+
<Table.Row key={row.id}>
|
|
488
|
+
{row.getVisibleCells().map((cell) => (
|
|
489
|
+
<Table.Cell key={cell.id}>
|
|
490
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
491
|
+
</Table.Cell>
|
|
492
|
+
))}
|
|
493
|
+
</Table.Row>
|
|
494
|
+
))}
|
|
495
|
+
</Table.Body>
|
|
496
|
+
</Table.Root>
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const Sorting: Story = {
|
|
501
|
+
render: () => <SortingDemo />,
|
|
502
|
+
play: async ({canvas}) => {
|
|
503
|
+
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
504
|
+
await expect(canvas.getAllByRole('columnheader').length).toBe(4)
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// --- Shared data helpers for interactive demos ---
|
|
509
|
+
|
|
510
|
+
interface User {
|
|
511
|
+
name: string
|
|
512
|
+
role: string
|
|
513
|
+
status: string
|
|
514
|
+
email: string
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const firstNames = [
|
|
518
|
+
'Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Hank',
|
|
519
|
+
'Iris', 'Jack', 'Karen', 'Leo', 'Mona', 'Nick', 'Olivia', 'Paul',
|
|
520
|
+
'Quinn', 'Rita', 'Sam', 'Tina', 'Uma', 'Vince', 'Wendy', 'Xander',
|
|
521
|
+
'Yara', 'Zach',
|
|
522
|
+
]
|
|
523
|
+
const lastNames = [
|
|
524
|
+
'Adams', 'Brown', 'Clark', 'Davis', 'Evans', 'Foster', 'Garcia',
|
|
525
|
+
'Hill', 'Ito', 'Jones', 'Kim', 'Lee', 'Moore', 'Nash', 'Owen',
|
|
526
|
+
'Patel', 'Quinn', 'Reed', 'Smith', 'Tran', 'Ueda', 'Voss', 'Wang',
|
|
527
|
+
'Xu', 'Yang', 'Zhang',
|
|
528
|
+
]
|
|
529
|
+
const roles = ['Admin', 'Editor', 'Viewer', 'Moderator', 'Analyst']
|
|
530
|
+
const statuses = ['Active', 'Inactive', 'Pending', 'Suspended']
|
|
531
|
+
|
|
532
|
+
function generateUsers(count: number): User[] {
|
|
533
|
+
return Array.from({length: count}, (_, i) => {
|
|
534
|
+
const first = firstNames[i % firstNames.length]
|
|
535
|
+
const last = lastNames[(i * 7) % lastNames.length]
|
|
536
|
+
return {
|
|
537
|
+
name: `${first} ${last}`,
|
|
538
|
+
role: roles[i % roles.length],
|
|
539
|
+
status: statuses[i % statuses.length],
|
|
540
|
+
email: `${first.toLowerCase()}.${last.toLowerCase()}@example.com`,
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// --- Pagination: demonstrates page navigation ---
|
|
546
|
+
|
|
547
|
+
const paginationData = generateUsers(35)
|
|
548
|
+
|
|
549
|
+
const paginationCols = [
|
|
550
|
+
createColumnHelper<User>().accessor('name', {header: 'Name'}),
|
|
551
|
+
createColumnHelper<User>().accessor('role', {header: 'Role'}),
|
|
552
|
+
createColumnHelper<User>().accessor('email', {header: 'Email'}),
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
function PaginationDemo() {
|
|
556
|
+
const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10})
|
|
557
|
+
|
|
558
|
+
const table = useReactTable({
|
|
559
|
+
data: paginationData,
|
|
560
|
+
columns: paginationCols,
|
|
561
|
+
state: {pagination},
|
|
562
|
+
onPaginationChange: setPagination,
|
|
563
|
+
getCoreRowModel: getCoreRowModel(),
|
|
564
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<html.div style={fullFeaturedStyles.wrapper}>
|
|
569
|
+
<Table.Root>
|
|
570
|
+
<Table.Header>
|
|
571
|
+
{table.getHeaderGroups().map((hg) => (
|
|
572
|
+
<Table.Row key={hg.id}>
|
|
573
|
+
{hg.headers.map((header) => (
|
|
574
|
+
<Table.HeaderCell key={header.id}>
|
|
575
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
576
|
+
</Table.HeaderCell>
|
|
577
|
+
))}
|
|
578
|
+
</Table.Row>
|
|
579
|
+
))}
|
|
580
|
+
</Table.Header>
|
|
581
|
+
<Table.Body>
|
|
582
|
+
{table.getRowModel().rows.map((row) => (
|
|
583
|
+
<Table.Row key={row.id}>
|
|
584
|
+
{row.getVisibleCells().map((cell) => (
|
|
585
|
+
<Table.Cell key={cell.id}>
|
|
586
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
587
|
+
</Table.Cell>
|
|
588
|
+
))}
|
|
589
|
+
</Table.Row>
|
|
590
|
+
))}
|
|
591
|
+
</Table.Body>
|
|
592
|
+
</Table.Root>
|
|
593
|
+
<Table.Pagination table={table} />
|
|
594
|
+
</html.div>
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export const WithPagination: Story = {
|
|
599
|
+
render: () => <PaginationDemo />,
|
|
600
|
+
play: async ({canvas}) => {
|
|
601
|
+
await expect(canvas.getByText('1 / 4')).toBeInTheDocument()
|
|
602
|
+
await expect(canvas.getByText('Next')).toBeInTheDocument()
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// --- Filtering: demonstrates column filter inputs ---
|
|
607
|
+
|
|
608
|
+
const filteringData = generateUsers(20)
|
|
609
|
+
|
|
610
|
+
const filteringCols = [
|
|
611
|
+
createColumnHelper<User>().accessor('name', {header: 'Name', enableColumnFilter: true}),
|
|
612
|
+
createColumnHelper<User>().accessor('role', {header: 'Role', enableColumnFilter: false}),
|
|
613
|
+
createColumnHelper<User>().accessor('status', {header: 'Status', enableColumnFilter: false}),
|
|
614
|
+
createColumnHelper<User>().accessor('email', {header: 'Email', enableColumnFilter: true}),
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
function FilteringDemo() {
|
|
618
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
619
|
+
|
|
620
|
+
const table = useReactTable({
|
|
621
|
+
data: filteringData,
|
|
622
|
+
columns: filteringCols,
|
|
623
|
+
state: {columnFilters},
|
|
624
|
+
onColumnFiltersChange: setColumnFilters,
|
|
625
|
+
getCoreRowModel: getCoreRowModel(),
|
|
626
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
return (
|
|
630
|
+
<Table.Root>
|
|
631
|
+
<Table.Header>
|
|
632
|
+
{table.getHeaderGroups().map((hg) => (
|
|
633
|
+
<Table.Row key={hg.id}>
|
|
634
|
+
{hg.headers.map((header) => (
|
|
635
|
+
<Table.HeaderCell key={header.id}>
|
|
636
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
637
|
+
{header.column.getCanFilter() && (
|
|
638
|
+
<Table.ColumnFilter
|
|
639
|
+
column={header.column}
|
|
640
|
+
placeholder={`Filter ${header.column.id}...`}
|
|
641
|
+
/>
|
|
642
|
+
)}
|
|
643
|
+
</Table.HeaderCell>
|
|
644
|
+
))}
|
|
645
|
+
</Table.Row>
|
|
646
|
+
))}
|
|
647
|
+
</Table.Header>
|
|
648
|
+
<Table.Body>
|
|
649
|
+
{table.getRowModel().rows.map((row) => (
|
|
650
|
+
<Table.Row key={row.id}>
|
|
651
|
+
{row.getVisibleCells().map((cell) => (
|
|
652
|
+
<Table.Cell key={cell.id}>
|
|
653
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
654
|
+
</Table.Cell>
|
|
655
|
+
))}
|
|
656
|
+
</Table.Row>
|
|
657
|
+
))}
|
|
658
|
+
</Table.Body>
|
|
659
|
+
</Table.Root>
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export const WithFiltering: Story = {
|
|
664
|
+
render: () => <FilteringDemo />,
|
|
665
|
+
play: async ({canvas}) => {
|
|
666
|
+
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
667
|
+
await expect(canvas.getByPlaceholderText('Filter name...')).toBeInTheDocument()
|
|
668
|
+
await expect(canvas.getByPlaceholderText('Filter email...')).toBeInTheDocument()
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// --- FullFeatured: useDataTable with pagination, sorting, and filtering ---
|
|
673
|
+
|
|
674
|
+
const fakeUsers = generateUsers(50)
|
|
675
|
+
|
|
676
|
+
const columnHelper = createColumnHelper<User>()
|
|
677
|
+
|
|
678
|
+
const fullFeaturedColumns = [
|
|
679
|
+
columnHelper.accessor('name', {
|
|
680
|
+
header: 'Name',
|
|
681
|
+
enableSorting: true,
|
|
682
|
+
enableColumnFilter: false,
|
|
683
|
+
}),
|
|
684
|
+
columnHelper.accessor('role', {
|
|
685
|
+
header: 'Role',
|
|
686
|
+
enableSorting: true,
|
|
687
|
+
enableColumnFilter: true,
|
|
688
|
+
}),
|
|
689
|
+
columnHelper.accessor('status', {
|
|
690
|
+
header: 'Status',
|
|
691
|
+
enableSorting: true,
|
|
692
|
+
enableColumnFilter: true,
|
|
693
|
+
}),
|
|
694
|
+
columnHelper.accessor('email', {
|
|
695
|
+
header: 'Email',
|
|
696
|
+
enableSorting: true,
|
|
697
|
+
enableColumnFilter: false,
|
|
698
|
+
}),
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
const fullFeaturedStyles = css.create({
|
|
702
|
+
wrapper: {display: 'flex', flexDirection: 'column', gap: 0},
|
|
703
|
+
filterBar: {
|
|
704
|
+
display: 'flex',
|
|
705
|
+
gap: spacing.md,
|
|
706
|
+
paddingBottom: spacing.md,
|
|
707
|
+
},
|
|
708
|
+
headerContent: {
|
|
709
|
+
display: 'flex',
|
|
710
|
+
flexDirection: 'row',
|
|
711
|
+
alignItems: 'center',
|
|
712
|
+
justifyContent: 'space-between',
|
|
713
|
+
cursor: 'pointer',
|
|
714
|
+
userSelect: 'none',
|
|
715
|
+
},
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
const uniqueRoles = [...new Set(fakeUsers.map((u) => u.role))].sort()
|
|
719
|
+
const uniqueStatuses = [...new Set(fakeUsers.map((u) => u.status))].sort()
|
|
720
|
+
|
|
721
|
+
function FullFeaturedDemo() {
|
|
722
|
+
const table = useDataTable(fakeUsers, fullFeaturedColumns, {
|
|
723
|
+
pagination: {pageSize: 10},
|
|
724
|
+
enableSorting: true,
|
|
725
|
+
enableFiltering: true,
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
return (
|
|
729
|
+
<html.div style={fullFeaturedStyles.wrapper}>
|
|
730
|
+
<html.div style={fullFeaturedStyles.filterBar}>
|
|
731
|
+
<Combobox.Root
|
|
732
|
+
value={table.getColumn('role')?.getFilterValue() as string ?? ''}
|
|
733
|
+
onValueChange={(v) => table.getColumn('role')?.setFilterValue(v || undefined)}
|
|
734
|
+
onInputChange={(v) => table.getColumn('role')?.setFilterValue(v || undefined)}
|
|
735
|
+
>
|
|
736
|
+
<Combobox.Input placeholder="Filter role...">
|
|
737
|
+
<Combobox.Trigger />
|
|
738
|
+
</Combobox.Input>
|
|
739
|
+
<Combobox.Popup>
|
|
740
|
+
<Combobox.Item value="">
|
|
741
|
+
<Combobox.ItemText>All Roles</Combobox.ItemText>
|
|
742
|
+
</Combobox.Item>
|
|
743
|
+
{uniqueRoles.map((role) => (
|
|
744
|
+
<Combobox.Item key={role} value={role}>
|
|
745
|
+
<Combobox.ItemText>{role}</Combobox.ItemText>
|
|
746
|
+
</Combobox.Item>
|
|
747
|
+
))}
|
|
748
|
+
<Combobox.Empty>No roles found</Combobox.Empty>
|
|
749
|
+
</Combobox.Popup>
|
|
750
|
+
</Combobox.Root>
|
|
751
|
+
<Combobox.Root
|
|
752
|
+
value={table.getColumn('status')?.getFilterValue() as string ?? ''}
|
|
753
|
+
onValueChange={(v) => table.getColumn('status')?.setFilterValue(v || undefined)}
|
|
754
|
+
onInputChange={(v) => table.getColumn('status')?.setFilterValue(v || undefined)}
|
|
755
|
+
>
|
|
756
|
+
<Combobox.Input placeholder="Filter status...">
|
|
757
|
+
<Combobox.Trigger />
|
|
758
|
+
</Combobox.Input>
|
|
759
|
+
<Combobox.Popup>
|
|
760
|
+
<Combobox.Item value="">
|
|
761
|
+
<Combobox.ItemText>All Statuses</Combobox.ItemText>
|
|
762
|
+
</Combobox.Item>
|
|
763
|
+
{uniqueStatuses.map((status) => (
|
|
764
|
+
<Combobox.Item key={status} value={status}>
|
|
765
|
+
<Combobox.ItemText>{status}</Combobox.ItemText>
|
|
766
|
+
</Combobox.Item>
|
|
767
|
+
))}
|
|
768
|
+
<Combobox.Empty>No statuses found</Combobox.Empty>
|
|
769
|
+
</Combobox.Popup>
|
|
770
|
+
</Combobox.Root>
|
|
771
|
+
</html.div>
|
|
772
|
+
<Table.Root>
|
|
773
|
+
<Table.Header>
|
|
774
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
775
|
+
<Table.Row key={headerGroup.id}>
|
|
776
|
+
{headerGroup.headers.map((header) => (
|
|
777
|
+
<Table.HeaderCell key={header.id}>
|
|
778
|
+
<html.div
|
|
779
|
+
style={fullFeaturedStyles.headerContent}
|
|
780
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
781
|
+
>
|
|
782
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
783
|
+
<Table.SortIndicator column={header.column} />
|
|
784
|
+
</html.div>
|
|
785
|
+
</Table.HeaderCell>
|
|
786
|
+
))}
|
|
787
|
+
</Table.Row>
|
|
788
|
+
))}
|
|
789
|
+
</Table.Header>
|
|
790
|
+
<Table.Body>
|
|
791
|
+
{table.getRowModel().rows.map((row) => (
|
|
792
|
+
<Table.Row key={row.id}>
|
|
793
|
+
{row.getVisibleCells().map((cell) => (
|
|
794
|
+
<Table.Cell key={cell.id}>
|
|
795
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
796
|
+
</Table.Cell>
|
|
797
|
+
))}
|
|
798
|
+
</Table.Row>
|
|
799
|
+
))}
|
|
800
|
+
</Table.Body>
|
|
801
|
+
</Table.Root>
|
|
802
|
+
<Table.Pagination table={table} />
|
|
803
|
+
</html.div>
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export const FullFeatured: Story = {
|
|
808
|
+
render: () => <FullFeaturedDemo />,
|
|
809
|
+
play: async ({canvas}) => {
|
|
810
|
+
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
811
|
+
|
|
812
|
+
// Should render 10 body rows (pageSize) + 1 header row
|
|
813
|
+
const rows = canvas.getAllByRole('row')
|
|
814
|
+
await expect(rows.length).toBe(11)
|
|
815
|
+
|
|
816
|
+
// All 4 column headers present
|
|
817
|
+
const columnHeaders = canvas.getAllByRole('columnheader')
|
|
818
|
+
await expect(columnHeaders.length).toBe(4)
|
|
819
|
+
|
|
820
|
+
// Pagination is visible (50 rows / 10 per page = 5 pages)
|
|
821
|
+
await expect(canvas.getByText('1 / 5')).toBeInTheDocument()
|
|
822
|
+
await expect(canvas.getByText('Next')).toBeInTheDocument()
|
|
823
|
+
await expect(canvas.getByText('Previous')).toBeInTheDocument()
|
|
824
|
+
},
|
|
825
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
type MutableRefObject,
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useRef,
|
|
7
|
+
Children,
|
|
8
|
+
isValidElement,
|
|
9
|
+
} from 'react'
|
|
2
10
|
import {html} from 'react-strict-dom'
|
|
3
11
|
import {styles} from './styles.css'
|
|
12
|
+
import {Pagination} from './Pagination'
|
|
13
|
+
import {SortIndicator} from './SortIndicator'
|
|
14
|
+
import {ColumnFilter} from './ColumnFilter'
|
|
4
15
|
|
|
5
16
|
// --- Types ---
|
|
6
17
|
|
|
@@ -12,7 +23,7 @@ export type TableSize = 'sm' | 'md'
|
|
|
12
23
|
interface TableContextValue {
|
|
13
24
|
variant: TableVariant
|
|
14
25
|
size: TableSize
|
|
15
|
-
/** Mutable ref: header Row writes inferred template,
|
|
26
|
+
/** Mutable ref: header Row writes inferred template, Root reads it */
|
|
16
27
|
inferredTemplateRef: MutableRefObject<string | null>
|
|
17
28
|
}
|
|
18
29
|
|
|
@@ -28,6 +39,10 @@ function useTable() {
|
|
|
28
39
|
|
|
29
40
|
const HeaderContext = createContext(false)
|
|
30
41
|
|
|
42
|
+
// --- Template context: set after first render pass ---
|
|
43
|
+
|
|
44
|
+
const TemplateContext = createContext<string | null>(null)
|
|
45
|
+
|
|
31
46
|
// --- Root ---
|
|
32
47
|
|
|
33
48
|
interface RootProps {
|
|
@@ -36,12 +51,46 @@ interface RootProps {
|
|
|
36
51
|
size?: TableSize
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Scans the React tree for HeaderCell elements to extract width props.
|
|
56
|
+
* Returns a grid-template-columns string.
|
|
57
|
+
*/
|
|
58
|
+
function extractTemplate(children: ReactNode): string {
|
|
59
|
+
const widths: string[] = []
|
|
60
|
+
|
|
61
|
+
function walk(node: ReactNode) {
|
|
62
|
+
Children.forEach(node, (child) => {
|
|
63
|
+
if (!isValidElement(child)) return
|
|
64
|
+
const props = child.props as Record<string, any>
|
|
65
|
+
// Check if this looks like a HeaderCell (has width prop or is inside Header)
|
|
66
|
+
const displayName = (child.type as any)?.name || (child.type as any)?.displayName || ''
|
|
67
|
+
if (displayName === 'HeaderCell' || child.type === HeaderCell) {
|
|
68
|
+
widths.push(props.width || '1fr')
|
|
69
|
+
} else if (props.children) {
|
|
70
|
+
walk(props.children)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
walk(children)
|
|
76
|
+
return widths.length > 0 ? widths.join(' ') : ''
|
|
77
|
+
}
|
|
78
|
+
|
|
39
79
|
function Root({children, variant = 'default', size = 'md'}: RootProps) {
|
|
40
80
|
const inferredTemplateRef = useRef<string | null>(null)
|
|
41
81
|
|
|
82
|
+
// Extract template from HeaderCell children synchronously
|
|
83
|
+
const template = extractTemplate(children)
|
|
84
|
+
if (template) {
|
|
85
|
+
inferredTemplateRef.current = template
|
|
86
|
+
}
|
|
87
|
+
|
|
42
88
|
return (
|
|
43
89
|
<TableContext.Provider value={{variant, size, inferredTemplateRef}}>
|
|
44
|
-
<html.div
|
|
90
|
+
<html.div
|
|
91
|
+
role="table"
|
|
92
|
+
style={[styles.root, template ? styles.gridColumns(template) : undefined]}
|
|
93
|
+
>
|
|
45
94
|
{children}
|
|
46
95
|
</html.div>
|
|
47
96
|
</TableContext.Provider>
|
|
@@ -68,7 +117,7 @@ function Body({children}: {children: ReactNode}) {
|
|
|
68
117
|
|
|
69
118
|
return (
|
|
70
119
|
<HeaderContext.Provider value={false}>
|
|
71
|
-
<html.div role="rowgroup">
|
|
120
|
+
<html.div role="rowgroup" style={styles.body}>
|
|
72
121
|
{childArray.map((child, index) => {
|
|
73
122
|
if (variant === 'striped') {
|
|
74
123
|
return (
|
|
@@ -89,35 +138,17 @@ const RowIndexContext = createContext<number>(-1)
|
|
|
89
138
|
// --- Row ---
|
|
90
139
|
|
|
91
140
|
function Row({children}: {children: ReactNode}) {
|
|
92
|
-
const {variant
|
|
141
|
+
const {variant} = useTable()
|
|
93
142
|
const isHeader = useContext(HeaderContext)
|
|
94
143
|
const rowIndex = useContext(RowIndexContext)
|
|
95
144
|
const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
|
|
96
145
|
const childArray = Children.toArray(children)
|
|
97
146
|
|
|
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
147
|
return (
|
|
116
148
|
<html.div
|
|
117
149
|
role="row"
|
|
118
150
|
style={[
|
|
119
151
|
styles.row,
|
|
120
|
-
styles.gridColumns(template),
|
|
121
152
|
!isHeader && styles.bodyRow,
|
|
122
153
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
123
154
|
]}
|
|
@@ -143,7 +174,7 @@ function HeaderCell({
|
|
|
143
174
|
'aria-label': ariaLabel,
|
|
144
175
|
}: {
|
|
145
176
|
children?: ReactNode
|
|
146
|
-
/** Column width: CSS value like '40px', '2fr', '
|
|
177
|
+
/** Column width: CSS value like '40px', '2fr', 'max-content'. Defaults to '1fr'. */
|
|
147
178
|
width?: string
|
|
148
179
|
'aria-label'?: string
|
|
149
180
|
}) {
|
|
@@ -198,4 +229,7 @@ export const Table = {
|
|
|
198
229
|
Row,
|
|
199
230
|
HeaderCell,
|
|
200
231
|
Cell,
|
|
232
|
+
Pagination,
|
|
233
|
+
SortIndicator,
|
|
234
|
+
ColumnFilter,
|
|
201
235
|
}
|
|
@@ -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
|
}),
|