@duro-app/ui 0.24.0 → 0.26.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.
Files changed (96) hide show
  1. package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -1
  2. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
  3. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -1
  4. package/dist/components/Combobox/Combobox.d.ts +44 -0
  5. package/dist/components/Combobox/Combobox.d.ts.map +1 -0
  6. package/dist/components/Combobox/Combobox.meta.d.ts +3 -0
  7. package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -0
  8. package/dist/components/Combobox/Combobox.stories.d.ts +9 -0
  9. package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -0
  10. package/dist/components/Combobox/ComboboxContext.d.ts +19 -0
  11. package/dist/components/Combobox/ComboboxContext.d.ts.map +1 -0
  12. package/dist/components/Combobox/styles.css.d.ts +112 -0
  13. package/dist/components/Combobox/styles.css.d.ts.map +1 -0
  14. package/dist/components/Combobox/useComboboxRoot.d.ts +15 -0
  15. package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -0
  16. package/dist/components/Field/Field.d.ts +4 -0
  17. package/dist/components/Field/Field.d.ts.map +1 -1
  18. package/dist/components/Field/Field.stories.d.ts +3 -0
  19. package/dist/components/Field/Field.stories.d.ts.map +1 -1
  20. package/dist/components/Field/FieldContext.d.ts +5 -0
  21. package/dist/components/Field/FieldContext.d.ts.map +1 -1
  22. package/dist/components/Field/styles.css.d.ts +26 -0
  23. package/dist/components/Field/styles.css.d.ts.map +1 -1
  24. package/dist/components/Form/Form.d.ts +7 -2
  25. package/dist/components/Form/Form.d.ts.map +1 -1
  26. package/dist/components/Form/Form.stories.d.ts +6 -0
  27. package/dist/components/Form/Form.stories.d.ts.map +1 -1
  28. package/dist/components/Form/FormContext.d.ts +9 -2
  29. package/dist/components/Form/FormContext.d.ts.map +1 -1
  30. package/dist/components/Input/Input.d.ts.map +1 -1
  31. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  32. package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -1
  33. package/dist/components/Table/SortIndicator.d.ts.map +1 -1
  34. package/dist/components/Table/Table.d.ts.map +1 -1
  35. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/components/Table/useDataTable.d.ts.map +1 -1
  37. package/dist/components/Tag/Tag.d.ts +18 -0
  38. package/dist/components/Tag/Tag.d.ts.map +1 -0
  39. package/dist/components/Tag/Tag.meta.d.ts +3 -0
  40. package/dist/components/Tag/Tag.meta.d.ts.map +1 -0
  41. package/dist/components/Tag/Tag.stories.d.ts +11 -0
  42. package/dist/components/Tag/Tag.stories.d.ts.map +1 -0
  43. package/dist/components/Tag/styles.css.d.ts +80 -0
  44. package/dist/components/Tag/styles.css.d.ts.map +1 -0
  45. package/dist/components/TagGroup/TagGroup.d.ts +30 -0
  46. package/dist/components/TagGroup/TagGroup.d.ts.map +1 -0
  47. package/dist/components/TagGroup/TagGroup.meta.d.ts +3 -0
  48. package/dist/components/TagGroup/TagGroup.meta.d.ts.map +1 -0
  49. package/dist/components/TagGroup/TagGroup.stories.d.ts +11 -0
  50. package/dist/components/TagGroup/TagGroup.stories.d.ts.map +1 -0
  51. package/dist/components/TagGroup/TagGroupContext.d.ts +17 -0
  52. package/dist/components/TagGroup/TagGroupContext.d.ts.map +1 -0
  53. package/dist/components/TagGroup/styles.css.d.ts +75 -0
  54. package/dist/components/TagGroup/styles.css.d.ts.map +1 -0
  55. package/dist/components/Textarea/Textarea.d.ts.map +1 -1
  56. package/dist/index.css +1 -1
  57. package/dist/index.d.ts +4 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +3256 -2318
  60. package/dist/index.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/components/ButtonGroup/ButtonGroup.meta.ts +10 -2
  63. package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +4 -1
  64. package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -9
  65. package/src/components/Combobox/Combobox.meta.ts +41 -0
  66. package/src/components/Combobox/Combobox.stories.tsx +157 -0
  67. package/src/components/Combobox/Combobox.tsx +235 -0
  68. package/src/components/Combobox/ComboboxContext.ts +26 -0
  69. package/src/components/Combobox/styles.css.ts +119 -0
  70. package/src/components/Combobox/useComboboxRoot.ts +235 -0
  71. package/src/components/Field/Field.stories.tsx +62 -0
  72. package/src/components/Field/Field.tsx +94 -9
  73. package/src/components/Field/FieldContext.ts +5 -0
  74. package/src/components/Field/styles.css.ts +26 -0
  75. package/src/components/Form/Form.stories.tsx +275 -0
  76. package/src/components/Form/Form.tsx +69 -7
  77. package/src/components/Form/FormContext.ts +10 -1
  78. package/src/components/Input/Input.tsx +2 -1
  79. package/src/components/RadioGroup/RadioGroup.meta.ts +4 -1
  80. package/src/components/RadioGroup/RadioGroup.tsx +1 -4
  81. package/src/components/Table/SortIndicator.tsx +1 -4
  82. package/src/components/Table/Table.stories.tsx +190 -84
  83. package/src/components/Table/Table.tsx +10 -5
  84. package/src/components/Table/useDataTable.ts +34 -48
  85. package/src/components/Tag/Tag.meta.ts +22 -0
  86. package/src/components/Tag/Tag.stories.tsx +92 -0
  87. package/src/components/Tag/Tag.tsx +139 -0
  88. package/src/components/Tag/styles.css.ts +99 -0
  89. package/src/components/TagGroup/TagGroup.meta.ts +27 -0
  90. package/src/components/TagGroup/TagGroup.stories.tsx +193 -0
  91. package/src/components/TagGroup/TagGroup.tsx +302 -0
  92. package/src/components/TagGroup/TagGroupContext.ts +22 -0
  93. package/src/components/TagGroup/styles.css.ts +97 -0
  94. package/src/components/Textarea/Textarea.tsx +2 -1
  95. package/src/index.ts +4 -0
  96. package/src/strict.css +1 -1
@@ -9,7 +9,21 @@ 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'
12
+ import {
13
+ flexRender,
14
+ createColumnHelper,
15
+ useReactTable,
16
+ getCoreRowModel,
17
+ getSortedRowModel,
18
+ getFilteredRowModel,
19
+ getPaginationRowModel,
20
+ type SortingState,
21
+ type ColumnFiltersState,
22
+ type PaginationState,
23
+ } from '@tanstack/react-table'
24
+ import {useDataTable} from './useDataTable'
25
+ import {Combobox} from '../Combobox/Combobox'
26
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
13
27
 
14
28
  const meta: Meta = {
15
29
  title: 'Components/Table',
@@ -429,30 +443,31 @@ export const AllVariants: Story = {
429
443
  },
430
444
  }
431
445
 
432
-
433
446
  // --- Sorting: demonstrates click-to-sort with hover indicators ---
434
447
 
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
- ]
448
+ type Service = {name: string; role: string; port: number; uptime: string}
443
449
 
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
+ const sortingData: Service[] = [
451
+ {name: 'Traefik', role: 'Reverse Proxy', port: 443, uptime: '14d 3h'},
452
+ {name: 'Pi-hole', role: 'DNS Filter', port: 80, uptime: '14d 3h'},
453
+ {name: 'Grafana', role: 'Monitoring', port: 3000, uptime: '7d 12h'},
454
+ {name: 'Portainer', role: 'Container Mgmt', port: 9000, uptime: '2d 5h'},
455
+ {name: 'Nginx', role: 'Web Server', port: 8080, uptime: '30d 1h'},
456
+ ]
450
457
 
458
+ const sortingCols = [
459
+ createColumnHelper<Service>().accessor('name', {header: 'Name', enableSorting: true}),
460
+ createColumnHelper<Service>().accessor('role', {header: 'Role', enableSorting: true}),
461
+ createColumnHelper<Service>().accessor('port', {header: 'Port', enableSorting: true}),
462
+ createColumnHelper<Service>().accessor('uptime', {header: 'Uptime', enableSorting: true}),
463
+ ]
464
+
465
+ function SortingDemo() {
451
466
  const [sorting, setSorting] = useState<SortingState>([])
452
467
 
453
468
  const table = useReactTable({
454
- data,
455
- columns: cols,
469
+ data: sortingData,
470
+ columns: sortingCols,
456
471
  state: {sorting},
457
472
  onSortingChange: setSorting,
458
473
  getCoreRowModel: getCoreRowModel(),
@@ -501,22 +516,103 @@ export const Sorting: Story = {
501
516
  },
502
517
  }
503
518
 
519
+ // --- Shared data helpers for interactive demos ---
520
+
521
+ interface User {
522
+ name: string
523
+ role: string
524
+ status: string
525
+ email: string
526
+ }
527
+
528
+ const firstNames = [
529
+ 'Alice',
530
+ 'Bob',
531
+ 'Carol',
532
+ 'Dave',
533
+ 'Eve',
534
+ 'Frank',
535
+ 'Grace',
536
+ 'Hank',
537
+ 'Iris',
538
+ 'Jack',
539
+ 'Karen',
540
+ 'Leo',
541
+ 'Mona',
542
+ 'Nick',
543
+ 'Olivia',
544
+ 'Paul',
545
+ 'Quinn',
546
+ 'Rita',
547
+ 'Sam',
548
+ 'Tina',
549
+ 'Uma',
550
+ 'Vince',
551
+ 'Wendy',
552
+ 'Xander',
553
+ 'Yara',
554
+ 'Zach',
555
+ ]
556
+ const lastNames = [
557
+ 'Adams',
558
+ 'Brown',
559
+ 'Clark',
560
+ 'Davis',
561
+ 'Evans',
562
+ 'Foster',
563
+ 'Garcia',
564
+ 'Hill',
565
+ 'Ito',
566
+ 'Jones',
567
+ 'Kim',
568
+ 'Lee',
569
+ 'Moore',
570
+ 'Nash',
571
+ 'Owen',
572
+ 'Patel',
573
+ 'Quinn',
574
+ 'Reed',
575
+ 'Smith',
576
+ 'Tran',
577
+ 'Ueda',
578
+ 'Voss',
579
+ 'Wang',
580
+ 'Xu',
581
+ 'Yang',
582
+ 'Zhang',
583
+ ]
584
+ const roles = ['Admin', 'Editor', 'Viewer', 'Moderator', 'Analyst']
585
+ const statuses = ['Active', 'Inactive', 'Pending', 'Suspended']
586
+
587
+ function generateUsers(count: number): User[] {
588
+ return Array.from({length: count}, (_, i) => {
589
+ const first = firstNames[i % firstNames.length]
590
+ const last = lastNames[(i * 7) % lastNames.length]
591
+ return {
592
+ name: `${first} ${last}`,
593
+ role: roles[i % roles.length],
594
+ status: statuses[i % statuses.length],
595
+ email: `${first.toLowerCase()}.${last.toLowerCase()}@example.com`,
596
+ }
597
+ })
598
+ }
599
+
504
600
  // --- Pagination: demonstrates page navigation ---
505
601
 
506
- function PaginationDemo() {
507
- const data = generateUsers(35)
602
+ const paginationData = generateUsers(35)
508
603
 
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
- ]
604
+ const paginationCols = [
605
+ createColumnHelper<User>().accessor('name', {header: 'Name'}),
606
+ createColumnHelper<User>().accessor('role', {header: 'Role'}),
607
+ createColumnHelper<User>().accessor('email', {header: 'Email'}),
608
+ ]
514
609
 
610
+ function PaginationDemo() {
515
611
  const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10})
516
612
 
517
613
  const table = useReactTable({
518
- data,
519
- columns: cols,
614
+ data: paginationData,
615
+ columns: paginationCols,
520
616
  state: {pagination},
521
617
  onPaginationChange: setPagination,
522
618
  getCoreRowModel: getCoreRowModel(),
@@ -564,21 +660,21 @@ export const WithPagination: Story = {
564
660
 
565
661
  // --- Filtering: demonstrates column filter inputs ---
566
662
 
567
- function FilteringDemo() {
568
- const data = generateUsers(20)
663
+ const filteringData = generateUsers(20)
569
664
 
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
- ]
665
+ const filteringCols = [
666
+ createColumnHelper<User>().accessor('name', {header: 'Name', enableColumnFilter: true}),
667
+ createColumnHelper<User>().accessor('role', {header: 'Role', enableColumnFilter: false}),
668
+ createColumnHelper<User>().accessor('status', {header: 'Status', enableColumnFilter: false}),
669
+ createColumnHelper<User>().accessor('email', {header: 'Email', enableColumnFilter: true}),
670
+ ]
576
671
 
672
+ function FilteringDemo() {
577
673
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
578
674
 
579
675
  const table = useReactTable({
580
- data,
581
- columns: cols,
676
+ data: filteringData,
677
+ columns: filteringCols,
582
678
  state: {columnFilters},
583
679
  onColumnFiltersChange: setColumnFilters,
584
680
  getCoreRowModel: getCoreRowModel(),
@@ -628,44 +724,8 @@ export const WithFiltering: Story = {
628
724
  },
629
725
  }
630
726
 
631
-
632
727
  // --- FullFeatured: useDataTable with pagination, sorting, and filtering ---
633
728
 
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
729
  const fakeUsers = generateUsers(50)
670
730
 
671
731
  const columnHelper = createColumnHelper<User>()
@@ -674,35 +734,45 @@ const fullFeaturedColumns = [
674
734
  columnHelper.accessor('name', {
675
735
  header: 'Name',
676
736
  enableSorting: true,
677
- enableColumnFilter: true,
737
+ enableColumnFilter: false,
678
738
  }),
679
739
  columnHelper.accessor('role', {
680
740
  header: 'Role',
681
741
  enableSorting: true,
682
- enableColumnFilter: false,
742
+ enableColumnFilter: true,
683
743
  }),
684
744
  columnHelper.accessor('status', {
685
745
  header: 'Status',
686
746
  enableSorting: true,
687
- enableColumnFilter: false,
747
+ enableColumnFilter: true,
688
748
  }),
689
749
  columnHelper.accessor('email', {
690
750
  header: 'Email',
691
751
  enableSorting: true,
692
- enableColumnFilter: true,
752
+ enableColumnFilter: false,
693
753
  }),
694
754
  ]
695
755
 
696
756
  const fullFeaturedStyles = css.create({
697
757
  wrapper: {display: 'flex', flexDirection: 'column', gap: 0},
758
+ filterBar: {
759
+ display: 'flex',
760
+ gap: spacing.md,
761
+ paddingBottom: spacing.md,
762
+ },
698
763
  headerContent: {
699
764
  display: 'flex',
700
- flexDirection: 'column',
765
+ flexDirection: 'row',
766
+ alignItems: 'center',
767
+ justifyContent: 'space-between',
701
768
  cursor: 'pointer',
702
769
  userSelect: 'none',
703
770
  },
704
771
  })
705
772
 
773
+ const uniqueRoles = [...new Set(fakeUsers.map((u) => u.role))].sort()
774
+ const uniqueStatuses = [...new Set(fakeUsers.map((u) => u.status))].sort()
775
+
706
776
  function FullFeaturedDemo() {
707
777
  const table = useDataTable(fakeUsers, fullFeaturedColumns, {
708
778
  pagination: {pageSize: 10},
@@ -712,6 +782,48 @@ function FullFeaturedDemo() {
712
782
 
713
783
  return (
714
784
  <html.div style={fullFeaturedStyles.wrapper}>
785
+ <html.div style={fullFeaturedStyles.filterBar}>
786
+ <Combobox.Root
787
+ value={(table.getColumn('role')?.getFilterValue() as string) ?? ''}
788
+ onValueChange={(v) => table.getColumn('role')?.setFilterValue(v || undefined)}
789
+ onInputChange={(v) => table.getColumn('role')?.setFilterValue(v || undefined)}
790
+ >
791
+ <Combobox.Input placeholder="Filter role...">
792
+ <Combobox.Trigger />
793
+ </Combobox.Input>
794
+ <Combobox.Popup>
795
+ <Combobox.Item value="">
796
+ <Combobox.ItemText>All Roles</Combobox.ItemText>
797
+ </Combobox.Item>
798
+ {uniqueRoles.map((role) => (
799
+ <Combobox.Item key={role} value={role}>
800
+ <Combobox.ItemText>{role}</Combobox.ItemText>
801
+ </Combobox.Item>
802
+ ))}
803
+ <Combobox.Empty>No roles found</Combobox.Empty>
804
+ </Combobox.Popup>
805
+ </Combobox.Root>
806
+ <Combobox.Root
807
+ value={(table.getColumn('status')?.getFilterValue() as string) ?? ''}
808
+ onValueChange={(v) => table.getColumn('status')?.setFilterValue(v || undefined)}
809
+ onInputChange={(v) => table.getColumn('status')?.setFilterValue(v || undefined)}
810
+ >
811
+ <Combobox.Input placeholder="Filter status...">
812
+ <Combobox.Trigger />
813
+ </Combobox.Input>
814
+ <Combobox.Popup>
815
+ <Combobox.Item value="">
816
+ <Combobox.ItemText>All Statuses</Combobox.ItemText>
817
+ </Combobox.Item>
818
+ {uniqueStatuses.map((status) => (
819
+ <Combobox.Item key={status} value={status}>
820
+ <Combobox.ItemText>{status}</Combobox.ItemText>
821
+ </Combobox.Item>
822
+ ))}
823
+ <Combobox.Empty>No statuses found</Combobox.Empty>
824
+ </Combobox.Popup>
825
+ </Combobox.Root>
826
+ </html.div>
715
827
  <Table.Root>
716
828
  <Table.Header>
717
829
  {table.getHeaderGroups().map((headerGroup) => (
@@ -725,12 +837,6 @@ function FullFeaturedDemo() {
725
837
  {flexRender(header.column.columnDef.header, header.getContext())}
726
838
  <Table.SortIndicator column={header.column} />
727
839
  </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
840
  </Table.HeaderCell>
735
841
  ))}
736
842
  </Table.Row>
@@ -1,4 +1,12 @@
1
- import {type ReactNode, type MutableRefObject, createContext, useContext, useRef, Children, isValidElement} from 'react'
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'
4
12
  import {Pagination} from './Pagination'
@@ -81,10 +89,7 @@ function Root({children, variant = 'default', size = 'md'}: RootProps) {
81
89
  <TableContext.Provider value={{variant, size, inferredTemplateRef}}>
82
90
  <html.div
83
91
  role="table"
84
- style={[
85
- styles.root,
86
- template ? styles.gridColumns(template) : undefined,
87
- ]}
92
+ style={[styles.root, template ? styles.gridColumns(template) : undefined]}
88
93
  >
89
94
  {children}
90
95
  </html.div>
@@ -1,4 +1,4 @@
1
- import {useState} from 'react'
1
+ import {useCallback, useState} from 'react'
2
2
  import {
3
3
  useReactTable,
4
4
  getCoreRowModel,
@@ -10,7 +10,6 @@ import {
10
10
  type ColumnFiltersState,
11
11
  type PaginationState,
12
12
  type RowSelectionState,
13
- type TableOptions,
14
13
  type Table,
15
14
  } from '@tanstack/react-table'
16
15
 
@@ -47,67 +46,54 @@ export function useDataTable<TData>(
47
46
  options: UseDataTableOptions<TData> = {},
48
47
  ): Table<TData> {
49
48
  const {
50
- pagination: paginationInit,
51
49
  enableSorting = false,
52
50
  enableFiltering = false,
53
51
  enableRowSelection = false,
54
52
  onRowSelectionChange: onSelectionChangeProp,
55
53
  } = options
56
54
 
55
+ const enablePagination = options.pagination != null
56
+
57
57
  const [sorting, setSorting] = useState<SortingState>([])
58
58
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
59
59
  const [pagination, setPagination] = useState<PaginationState>({
60
- pageIndex: paginationInit?.pageIndex ?? 0,
61
- pageSize: paginationInit?.pageSize ?? 10,
60
+ pageIndex: options.pagination?.pageIndex ?? 0,
61
+ pageSize: options.pagination?.pageSize ?? 10,
62
62
  })
63
63
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
64
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
- }
65
+ const handleRowSelectionChange = useCallback(
66
+ (updater: any) => {
67
+ setRowSelection((prev: RowSelectionState) => {
68
+ const next = typeof updater === 'function' ? updater(prev) : updater
69
+ onSelectionChangeProp?.(next)
70
+ return next
71
+ })
72
+ },
73
+ [onSelectionChangeProp],
74
+ )
72
75
 
73
- const tableOptions: TableOptions<TData> = {
76
+ // Follow TanStack's recommended pattern: always pass all row models
77
+ // unconditionally and let the library handle unused ones. Conditional
78
+ // spreads create unstable option shapes that can trigger infinite loops.
79
+ return useReactTable({
74
80
  data,
75
81
  columns,
82
+ state: {sorting, columnFilters, pagination, rowSelection},
76
83
  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,
84
+ getSortedRowModel: getSortedRowModel(),
85
+ getFilteredRowModel: getFilteredRowModel(),
86
+ getPaginationRowModel: getPaginationRowModel(),
87
+ onSortingChange: setSorting,
88
+ onColumnFiltersChange: setColumnFilters,
89
+ onPaginationChange: setPagination,
90
+ enableSorting,
91
+ enableRowSelection:
92
+ typeof enableRowSelection === 'function' ? enableRowSelection : !!enableRowSelection,
93
+ onRowSelectionChange: handleRowSelectionChange,
94
+ // When pagination is disabled, set a very large page size to show all rows
95
+ ...(!enablePagination && {
96
+ autoResetPageIndex: false,
95
97
  }),
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)
98
+ })
113
99
  }
@@ -0,0 +1,22 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction.',
6
+ whenToUse: [
7
+ 'Removable items in a list (emails, categories)',
8
+ 'Inside TagGroup for editable tag collections',
9
+ 'Interactive labels that can be dismissed',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Static status indicators — use Badge',
13
+ 'Toggleable selection — use Toggle or ToggleGroup',
14
+ ],
15
+ relatedTo: [
16
+ {component: 'Badge', relationship: 'Static display variant'},
17
+ {component: 'TagGroup', relationship: 'Wrap in TagGroup.Root for collection management'},
18
+ ],
19
+ example: `<Tag removable onRemove={() => console.log('removed')}>
20
+ user@example.com
21
+ </Tag>`,
22
+ }
@@ -0,0 +1,92 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, fn} from 'storybook/test'
3
+ import {css, html} from 'react-strict-dom'
4
+ import {Tag} from './Tag'
5
+
6
+ const meta: Meta<typeof Tag> = {
7
+ title: 'Components/Tag',
8
+ component: Tag,
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['default', 'success', 'warning', 'error', 'info'],
13
+ },
14
+ size: {
15
+ control: 'select',
16
+ options: ['sm', 'md'],
17
+ },
18
+ removable: {control: 'boolean'},
19
+ disabled: {control: 'boolean'},
20
+ },
21
+ }
22
+
23
+ export default meta
24
+ type Story = StoryObj<typeof Tag>
25
+
26
+ export const Default: Story = {
27
+ args: {children: 'Tag'},
28
+ play: async ({canvas}) => {
29
+ await expect(canvas.getByText('Tag')).toBeInTheDocument()
30
+ },
31
+ }
32
+
33
+ export const Removable: Story = {
34
+ args: {children: 'user@example.com', removable: true, onRemove: fn()},
35
+ play: async ({canvas}) => {
36
+ await expect(canvas.getByText('user@example.com')).toBeInTheDocument()
37
+ await expect(canvas.getByRole('button', {name: /remove/i})).toBeInTheDocument()
38
+ },
39
+ }
40
+
41
+ export const Disabled: Story = {
42
+ args: {children: 'Disabled', removable: true, disabled: true},
43
+ }
44
+
45
+ export const Small: Story = {
46
+ args: {children: 'Small', size: 'sm', removable: true, onRemove: fn()},
47
+ }
48
+
49
+ const layoutStyles = css.create({
50
+ row: {display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap'},
51
+ stack: {display: 'flex', flexDirection: 'column', gap: 16},
52
+ })
53
+
54
+ export const AllVariants: Story = {
55
+ render: () => (
56
+ <html.div style={layoutStyles.stack}>
57
+ <html.div style={layoutStyles.row}>
58
+ <Tag>Default</Tag>
59
+ <Tag variant="success">Success</Tag>
60
+ <Tag variant="warning">Warning</Tag>
61
+ <Tag variant="error">Error</Tag>
62
+ <Tag variant="info">Info</Tag>
63
+ </html.div>
64
+ <html.div style={layoutStyles.row}>
65
+ <Tag removable onRemove={() => {}}>
66
+ Default
67
+ </Tag>
68
+ <Tag variant="success" removable onRemove={() => {}}>
69
+ Success
70
+ </Tag>
71
+ <Tag variant="warning" removable onRemove={() => {}}>
72
+ Warning
73
+ </Tag>
74
+ <Tag variant="error" removable onRemove={() => {}}>
75
+ Error
76
+ </Tag>
77
+ <Tag variant="info" removable onRemove={() => {}}>
78
+ Info
79
+ </Tag>
80
+ </html.div>
81
+ <html.div style={layoutStyles.row}>
82
+ <Tag size="sm">Small</Tag>
83
+ <Tag size="sm" removable onRemove={() => {}}>
84
+ Removable Small
85
+ </Tag>
86
+ </html.div>
87
+ </html.div>
88
+ ),
89
+ play: async ({canvas}) => {
90
+ await expect(canvas.getAllByText('Default').length).toBe(2)
91
+ },
92
+ }