@datum-cloud/datum-ui 0.3.0-alpha.9d90881 → 0.3.0-alpha.bdce331

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 (142) hide show
  1. package/dist/autocomplete/index.mjs +4 -4
  2. package/dist/{autocomplete-e33EmvBu.mjs → autocomplete-B9bCkXtz.mjs} +3 -3
  3. package/dist/avatar-stack/index.mjs +2 -2
  4. package/dist/{avatar-stack-Ci0cnjxv.mjs → avatar-stack-Bh-tLz0X.mjs} +1 -1
  5. package/dist/{calendar-date-picker-BBAg78Lg.mjs → calendar-date-picker-mlbzp3xR.mjs} +2 -2
  6. package/dist/checkbox/index.mjs +1 -2
  7. package/dist/checkbox-LG1OKTpG.mjs +34 -0
  8. package/dist/collapsible/index.mjs +1 -1
  9. package/dist/command/index.mjs +2 -2
  10. package/dist/{command-DQlO6uTL.mjs → command-s0Yv3abE.mjs} +1 -1
  11. package/dist/components/features/data-table/adapters/nuqs-adapter.d.ts +13 -12
  12. package/dist/components/features/data-table/adapters/nuqs-adapter.d.ts.map +1 -1
  13. package/dist/components/features/data-table/components/active-filters.d.ts +5 -0
  14. package/dist/components/features/data-table/components/active-filters.d.ts.map +1 -0
  15. package/dist/components/features/data-table/components/bulk-actions.d.ts.map +1 -1
  16. package/dist/components/features/data-table/components/column-header.d.ts.map +1 -1
  17. package/dist/components/features/data-table/components/content.d.ts.map +1 -1
  18. package/dist/components/features/data-table/components/pagination.d.ts.map +1 -1
  19. package/dist/components/features/data-table/components/search.d.ts.map +1 -1
  20. package/dist/components/features/data-table/core/client-provider.d.ts +9 -2
  21. package/dist/components/features/data-table/core/client-provider.d.ts.map +1 -1
  22. package/dist/components/features/data-table/core/data-table-context.d.ts +22 -3
  23. package/dist/components/features/data-table/core/data-table-context.d.ts.map +1 -1
  24. package/dist/components/features/data-table/core/filter-engine.d.ts +16 -0
  25. package/dist/components/features/data-table/core/filter-engine.d.ts.map +1 -0
  26. package/dist/components/features/data-table/core/server-provider.d.ts +9 -2
  27. package/dist/components/features/data-table/core/server-provider.d.ts.map +1 -1
  28. package/dist/components/features/data-table/core/store.d.ts +3 -0
  29. package/dist/components/features/data-table/core/store.d.ts.map +1 -0
  30. package/dist/components/features/data-table/data-table.d.ts +1 -0
  31. package/dist/components/features/data-table/data-table.d.ts.map +1 -1
  32. package/dist/components/features/data-table/filters/checkbox-filter.d.ts.map +1 -1
  33. package/dist/components/features/data-table/filters/date-picker-filter.d.ts.map +1 -1
  34. package/dist/components/features/data-table/filters/select-filter.d.ts.map +1 -1
  35. package/dist/components/features/data-table/hooks/index.d.ts +1 -1
  36. package/dist/components/features/data-table/hooks/index.d.ts.map +1 -1
  37. package/dist/components/features/data-table/hooks/use-data-table-client.d.ts +17 -20
  38. package/dist/components/features/data-table/hooks/use-data-table-client.d.ts.map +1 -1
  39. package/dist/components/features/data-table/hooks/use-data-table-server.d.ts +27 -19
  40. package/dist/components/features/data-table/hooks/use-data-table-server.d.ts.map +1 -1
  41. package/dist/components/features/data-table/hooks/use-is-client.d.ts +8 -0
  42. package/dist/components/features/data-table/hooks/use-is-client.d.ts.map +1 -0
  43. package/dist/components/features/data-table/hooks/use-selectors.d.ts +50 -0
  44. package/dist/components/features/data-table/hooks/use-selectors.d.ts.map +1 -0
  45. package/dist/components/features/data-table/index.d.ts +3 -4
  46. package/dist/components/features/data-table/index.d.ts.map +1 -1
  47. package/dist/components/features/data-table/types.d.ts +68 -0
  48. package/dist/components/features/data-table/types.d.ts.map +1 -1
  49. package/dist/data-table/index.mjs +1039 -550
  50. package/dist/date-picker/index.mjs +5 -5
  51. package/dist/dialog/index.mjs +2 -2
  52. package/dist/{dialog-B2EZJW-q.mjs → dialog-bnMMf9GD.mjs} +2 -2
  53. package/dist/dropdown/index.mjs +1 -1
  54. package/dist/dropzone/index.mjs +1 -1
  55. package/dist/empty-content/index.mjs +1 -1
  56. package/dist/form/index.mjs +12 -12
  57. package/dist/grid/index.mjs +1 -1
  58. package/dist/hooks/index.mjs +2 -2
  59. package/dist/hover-card/index.mjs +1 -1
  60. package/dist/icons/index.mjs +1 -1
  61. package/dist/index.mjs +50 -52
  62. package/dist/input/index.mjs +2 -2
  63. package/dist/{input-D241oNEm.mjs → input-fzXBheCN.mjs} +1 -1
  64. package/dist/input-group/index.mjs +3 -3
  65. package/dist/{input-group-uobp64zr.mjs → input-group-CPaFSTEV.mjs} +2 -2
  66. package/dist/input-number/index.mjs +2 -2
  67. package/dist/{input-number-CEMgBk8-.mjs → input-number-D9ydFith.mjs} +1 -1
  68. package/dist/input-with-addons/index.mjs +1 -1
  69. package/dist/label/index.mjs +1 -2
  70. package/dist/{label-byipFGok.mjs → label-_ste_Re3.mjs} +12 -1
  71. package/dist/loader-overlay/index.mjs +1 -1
  72. package/dist/map/index.mjs +8 -8
  73. package/dist/{map-DupFPkJT.mjs → map-ClJD-qxm.mjs} +5 -5
  74. package/dist/more-actions/index.mjs +2 -2
  75. package/dist/{more-actions-D6OyqZQS.mjs → more-actions-DbC8dyed.mjs} +2 -2
  76. package/dist/page-title/index.mjs +1 -1
  77. package/dist/popover/index.mjs +1 -1
  78. package/dist/radio-group/index.mjs +1 -1
  79. package/dist/select/index.mjs +1 -1
  80. package/dist/{select-BznmyqBr.mjs → select-CwVIFWFO.mjs} +1 -1
  81. package/dist/sheet/index.mjs +2 -2
  82. package/dist/{sheet-Bmayi68h.mjs → sheet-mx5XjyEY.mjs} +2 -2
  83. package/dist/sidebar/index.mjs +6 -6
  84. package/dist/{sidebar-D2zE7rPy.mjs → sidebar-C4NqSr4r.mjs} +5 -5
  85. package/dist/skeleton/index.mjs +1 -1
  86. package/dist/spinner/index.mjs +1 -1
  87. package/dist/stepper/index.mjs +1 -1
  88. package/dist/switch/index.mjs +1 -1
  89. package/dist/table/index.mjs +1 -1
  90. package/dist/tabs/index.mjs +1 -1
  91. package/dist/tag-input/index.mjs +2 -2
  92. package/dist/{tag-input-BI8IRBDH.mjs → tag-input-BfHaKoMF.mjs} +1 -1
  93. package/dist/task-queue/index.mjs +4 -4
  94. package/dist/{task-queue-dropdown-D6k067_W.mjs → task-queue-dropdown-fo3TX58Q.mjs} +4 -4
  95. package/dist/textarea/index.mjs +2 -2
  96. package/dist/{textarea-BZ85VFsJ.mjs → textarea-X4OjkqLJ.mjs} +1 -1
  97. package/dist/theme/index.mjs +1 -1
  98. package/dist/{to-api-format-CXQ7knV4.mjs → to-api-format-zI26rEBI.mjs} +3 -3
  99. package/dist/toast/index.mjs +1 -1
  100. package/dist/tooltip/index.mjs +1 -1
  101. package/dist/typography/index.mjs +1 -1
  102. package/dist/{use-copy-to-clipboard-CC2hhyYI.mjs → use-copy-to-clipboard-C7xqNxBX.mjs} +1 -1
  103. package/dist/{use-stepper-CU75TdjZ.mjs → use-stepper-CB1injte.mjs} +14 -14
  104. package/dist/{use-toast-BLBGnOC3.mjs → use-toast-DN-fZBzJ.mjs} +1 -1
  105. package/dist/visually-hidden/index.mjs +1 -1
  106. package/package.json +3 -3
  107. package/dist/checkbox-DB5_3E_l.mjs +0 -22
  108. package/dist/checkbox-DMC1Mhaw.mjs +0 -17
  109. package/dist/components/features/data-table/hooks/use-data-table-context.d.ts +0 -2
  110. package/dist/components/features/data-table/hooks/use-data-table-context.d.ts.map +0 -1
  111. package/dist/components/features/data-table/hooks/use-inline-contents.d.ts +0 -7
  112. package/dist/components/features/data-table/hooks/use-inline-contents.d.ts.map +0 -1
  113. package/dist/label-ClzLBWRT.mjs +0 -16
  114. /package/dist/{close.icon-D2r5q3bj.mjs → close.icon-CMNMoXM_.mjs} +0 -0
  115. /package/dist/{col-Cg_2sTDA.mjs → col-RfO7d6AR.mjs} +0 -0
  116. /package/dist/{collapsible-Dw71o2um.mjs → collapsible-Bt9UYfv3.mjs} +0 -0
  117. /package/dist/{dialog-Bm4trnic.mjs → dialog-DXBaT9gA.mjs} +0 -0
  118. /package/dist/{dropdown-DLZXinlT.mjs → dropdown-Cs7Xr8w7.mjs} +0 -0
  119. /package/dist/{dropdown-menu-Xahj42Gr.mjs → dropdown-menu-DAFyO-qD.mjs} +0 -0
  120. /package/dist/{dropzone-CGyjGnER.mjs → dropzone-BT5fEDEF.mjs} +0 -0
  121. /package/dist/{empty-content-ByvwjHUs.mjs → empty-content-iDu3NUqG.mjs} +0 -0
  122. /package/dist/{hover-card-BNrHtWy6.mjs → hover-card-CUPfFUqE.mjs} +0 -0
  123. /package/dist/{input-C-ZmsHkk.mjs → input-DuyjEKEW.mjs} +0 -0
  124. /package/dist/{input-with-addons-DzuyGa6G.mjs → input-with-addons-CdgiUQce.mjs} +0 -0
  125. /package/dist/{loader-overlay-CbxcjyHV.mjs → loader-overlay-D83QeQNj.mjs} +0 -0
  126. /package/dist/{map-leaflet-imports-CgEyVRnp.mjs → map-leaflet-imports-CdzvEnzY.mjs} +0 -0
  127. /package/dist/{page-title-CrYQ091u.mjs → page-title-SGchAF6Y.mjs} +0 -0
  128. /package/dist/{popover-CYzXdp9q.mjs → popover-Ds9624qY.mjs} +0 -0
  129. /package/dist/{radio-group-WZCIDQCH.mjs → radio-group-B9Hm77LQ.mjs} +0 -0
  130. /package/dist/{sheet-b9V9soz8.mjs → sheet-Cemwh78x.mjs} +0 -0
  131. /package/dist/{skeleton-D3qW_KvG.mjs → skeleton-Cs6Q5GQc.mjs} +0 -0
  132. /package/dist/{spinner-CKTGKv5n.mjs → spinner-earfjpJs.mjs} +0 -0
  133. /package/dist/{stepper-B07hPGG7.mjs → stepper-BG9DIzN5.mjs} +0 -0
  134. /package/dist/{switch-CujyyOi6.mjs → switch-B2VVauH6.mjs} +0 -0
  135. /package/dist/{table-fZEvpdD-.mjs → table-Dc3HfbM4.mjs} +0 -0
  136. /package/dist/{tabs-B7cW59gB.mjs → tabs-Ccb4uqbe.mjs} +0 -0
  137. /package/dist/{textarea-BSkDKiej.mjs → textarea-QYRcDEpK.mjs} +0 -0
  138. /package/dist/{theme.provider-BG3cS9xe.mjs → theme.provider-Nun_O9-O.mjs} +0 -0
  139. /package/dist/{tooltip-CbCWKEzu.mjs → tooltip-DZFG1iMs.mjs} +0 -0
  140. /package/dist/{typography-DdrxIJMd.mjs → typography-T7WgvO77.mjs} +0 -0
  141. /package/dist/{use-debounce-Dc95PFRX.mjs → use-debounce-Ctljs3MB.mjs} +0 -0
  142. /package/dist/{visuallyhidden-CfBnXfvh.mjs → visuallyhidden-CgkVhApW.mjs} +0 -0
@@ -1,22 +1,25 @@
1
- import { t as cn } from "../utils-Bfgoe-Gm.mjs";
2
- import { t as Button } from "../button-AzpnV-WB.mjs";
3
- import { t as Checkbox } from "../checkbox-DB5_3E_l.mjs";
4
- import "../dialog-Bm4trnic.mjs";
5
- import { a as CommandInput, i as CommandGroup, o as CommandItem, r as CommandEmpty, s as CommandList, t as Command } from "../command-DQlO6uTL.mjs";
6
- import { t as Input } from "../input-C-ZmsHkk.mjs";
7
- import { t as Label } from "../label-ClzLBWRT.mjs";
8
- import { i as DropdownMenuItem, l as DropdownMenuTrigger, r as DropdownMenuContent, t as DropdownMenu } from "../dropdown-menu-Xahj42Gr.mjs";
9
- import { i as PopoverTrigger, r as PopoverContent, t as Popover } from "../popover-CYzXdp9q.mjs";
10
- import { d as Select, f as SelectContent, h as SelectValue, m as SelectTrigger, p as SelectItem } from "../select-BznmyqBr.mjs";
11
- import { t as Skeleton } from "../skeleton-D3qW_KvG.mjs";
12
- import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader, t as Table } from "../table-fZEvpdD-.mjs";
13
- import { t as CalendarDatePicker } from "../calendar-date-picker-BBAg78Lg.mjs";
14
- import { cva } from "class-variance-authority";
1
+ import { t as cn } from "../cn-DWCc1QRE.mjs";
2
+ import { t as Badge } from "../badge-bFgeYceE.mjs";
3
+ import "../utils-Bfgoe-Gm.mjs";
4
+ import { t as Button } from "../button-C1wRfGtT.mjs";
5
+ import "../button-AzpnV-WB.mjs";
6
+ import { t as Checkbox } from "../checkbox-LG1OKTpG.mjs";
7
+ import "../dialog-DXBaT9gA.mjs";
8
+ import { a as CommandInput, i as CommandGroup, o as CommandItem, r as CommandEmpty, s as CommandList, t as Command } from "../command-s0Yv3abE.mjs";
9
+ import "../input-DuyjEKEW.mjs";
10
+ import { t as Input } from "../input-fzXBheCN.mjs";
11
+ import { t as Label } from "../label-_ste_Re3.mjs";
12
+ import { i as DropdownMenuItem, l as DropdownMenuTrigger, r as DropdownMenuContent, t as DropdownMenu } from "../dropdown-menu-DAFyO-qD.mjs";
13
+ import { i as PopoverTrigger, r as PopoverContent, t as Popover } from "../popover-Ds9624qY.mjs";
14
+ import { i as SelectItem, l as SelectTrigger, n as SelectContent, t as Select, u as SelectValue } from "../select-CwVIFWFO.mjs";
15
+ import { t as Skeleton } from "../skeleton-Cs6Q5GQc.mjs";
16
+ import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader, t as Table } from "../table-Dc3HfbM4.mjs";
17
+ import { t as CalendarDatePicker } from "../calendar-date-picker-mlbzp3xR.mjs";
15
18
  import { ArrowDown, ArrowUp, ArrowUpDown, Check, ChevronDown, ChevronLeft, ChevronRight, MoreHorizontal, X } from "lucide-react";
16
- import { createContext, use, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
17
- import { jsx, jsxs } from "react/jsx-runtime";
18
- import { parseAsInteger, parseAsJson, parseAsString, useQueryStates } from "nuqs";
19
- import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
19
+ import { createContext, memo, use, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
20
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
21
+ import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
22
+ import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
20
23
 
21
24
  //#region src/components/features/data-table/constants.ts
22
25
  const DEFAULT_PAGE_SIZE = 20;
@@ -32,15 +35,32 @@ const DEFAULT_LOADING_ROWS = 5;
32
35
  //#endregion
33
36
  //#region src/components/features/data-table/adapters/nuqs-adapter.ts
34
37
  /**
35
- * Default parsers for core DataTable state.
36
- * URL params: ?sort=[...]&q=hello&page=0&size=20
38
+ * Serialize SortingState to URL-friendly string.
39
+ * Format: "name" (asc), "-name" (desc), comma-separated for multi-sort.
40
+ * Example: "-department,name" → [{id:"department",desc:true},{id:"name",desc:false}]
37
41
  */
38
- function sortingValidator(value) {
39
- if (!Array.isArray(value)) return null;
40
- return value.every((item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.desc === "boolean") ? value : null;
42
+ function serializeSorting(sorting) {
43
+ if (sorting.length === 0) return "";
44
+ return sorting.map((s) => s.desc ? `-${s.id}` : s.id).join(",");
45
+ }
46
+ /**
47
+ * Parse URL sort string back to SortingState.
48
+ */
49
+ function parseSorting(value) {
50
+ if (!value) return [];
51
+ return value.split(",").filter(Boolean).map((part) => {
52
+ if (part.startsWith("-")) return {
53
+ id: part.slice(1),
54
+ desc: true
55
+ };
56
+ return {
57
+ id: part,
58
+ desc: false
59
+ };
60
+ });
41
61
  }
42
62
  const coreSearchParams = {
43
- sort: parseAsJson(sortingValidator).withDefault([]),
63
+ sort: parseAsString.withDefault(""),
44
64
  q: parseAsString.withDefault(""),
45
65
  page: parseAsInteger.withDefault(0),
46
66
  size: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE)
@@ -48,22 +68,21 @@ const coreSearchParams = {
48
68
  /**
49
69
  * Hook that creates a StateAdapter backed by nuqs URL query state.
50
70
  *
51
- * Manages sorting, search, pageIndex, and pageSize automatically.
52
- * Consumer only needs to declare custom filter parsers.
71
+ * URL format:
72
+ * - `?sort=name` (asc) or `?sort=-name` (desc), comma-separated for multi-sort
73
+ * - `?q=search` for search text
74
+ * - `?page=0&size=20` for pagination
75
+ * - Custom filter keys as declared in options
53
76
  *
54
77
  * Requires `nuqs` to be installed in the consumer app.
55
78
  *
56
79
  * @example
57
80
  * ```tsx
58
- * // Zero config — all defaults synced to URL
59
- * const stateAdapter = useNuqsAdapter()
60
- * const tableState = useDataTableClient({ data, columns, stateAdapter })
61
- *
62
- * // With custom filters synced to URL
63
- * import { parseAsString } from 'nuqs'
64
- *
65
81
  * const stateAdapter = useNuqsAdapter({
66
- * filters: { status: parseAsString.withDefault('') },
82
+ * filters: {
83
+ * status: parseAsString.withDefault(''),
84
+ * department: parseAsArrayOf(parseAsString).withDefault([]),
85
+ * },
67
86
  * })
68
87
  * const tableState = useDataTableClient({ data, columns, stateAdapter })
69
88
  * ```
@@ -75,7 +94,7 @@ function useNuqsAdapter(options = {}) {
75
94
  const [filterState, setFilterState] = useQueryStates(hasFilters ? filterParsers : { _dt: parseAsString.withDefault("") });
76
95
  return useMemo(() => ({
77
96
  read: () => ({
78
- sorting: coreState.sort,
97
+ sorting: parseSorting(coreState.sort),
79
98
  search: coreState.q,
80
99
  pageIndex: coreState.page,
81
100
  pageSize: coreState.size,
@@ -83,19 +102,24 @@ function useNuqsAdapter(options = {}) {
83
102
  }),
84
103
  write: (state) => {
85
104
  setCoreState({
86
- sort: state.sorting,
105
+ sort: serializeSorting(state.sorting),
87
106
  q: state.search,
88
107
  page: state.pageIndex ?? 0,
89
108
  size: state.pageSize ?? DEFAULT_PAGE_SIZE
90
109
  });
91
- if (hasFilters) setFilterState(state.filters);
110
+ if (hasFilters && filterParsers) {
111
+ const update = {};
112
+ for (const key of Object.keys(filterParsers)) update[key] = state.filters?.[key] ?? null;
113
+ setFilterState(update);
114
+ }
92
115
  }
93
116
  }), [
94
117
  coreState,
95
118
  filterState,
96
119
  hasFilters,
97
120
  setCoreState,
98
- setFilterState
121
+ setFilterState,
122
+ filterParsers
99
123
  ]);
100
124
  }
101
125
 
@@ -131,22 +155,587 @@ function withSelectionColumn(columns, options = {}) {
131
155
  return [createSelectionColumn(options), ...columns];
132
156
  }
133
157
 
158
+ //#endregion
159
+ //#region src/components/features/data-table/core/filter-engine.ts
160
+ const FILTER_STRATEGIES = {
161
+ "checkbox": (cellValue, filterValue) => {
162
+ if (filterValue == null) return true;
163
+ if (Array.isArray(filterValue) && filterValue.length === 0) return true;
164
+ if (!Array.isArray(filterValue)) return cellValue === filterValue;
165
+ if (Array.isArray(cellValue)) return cellValue.some((v) => filterValue.includes(v));
166
+ return filterValue.includes(cellValue);
167
+ },
168
+ "select": (cellValue, filterValue) => {
169
+ if (filterValue == null || filterValue === "") return true;
170
+ return cellValue === filterValue;
171
+ },
172
+ "date-gte": (cellValue, filterValue) => {
173
+ if (!filterValue) return true;
174
+ const cell = new Date(cellValue);
175
+ const filter = new Date(filterValue);
176
+ if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
177
+ return cell >= filter;
178
+ },
179
+ "date-lte": (cellValue, filterValue) => {
180
+ if (!filterValue) return true;
181
+ const cell = new Date(cellValue);
182
+ const filter = new Date(filterValue);
183
+ if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
184
+ return cell <= filter;
185
+ }
186
+ };
187
+ function resolveStrategy(strategy) {
188
+ if (!strategy) return void 0;
189
+ if (typeof strategy === "function") return strategy;
190
+ return FILTER_STRATEGIES[strategy];
191
+ }
192
+ function applyFilters(data, filters, search, registeredFilters, customFilterFns, searchConfig) {
193
+ const hasFilters = Object.keys(filters).length > 0;
194
+ const hasSearch = search.length > 0;
195
+ if (!hasFilters && !hasSearch) return data;
196
+ return data.filter((row) => {
197
+ if (hasFilters) for (const [column, value] of Object.entries(filters)) {
198
+ const fn = customFilterFns[column] ?? resolveStrategy(registeredFilters.get(column));
199
+ if (!fn) {
200
+ console.warn(`[DataTable] No filter strategy registered for column "${column}". Filter ignored.`);
201
+ continue;
202
+ }
203
+ const cellValue = row[column];
204
+ if (!fn(cellValue, value)) return false;
205
+ }
206
+ if (hasSearch) {
207
+ const query = search.toLowerCase();
208
+ if (searchConfig.searchFn) return searchConfig.searchFn(row, search);
209
+ if (searchConfig.searchableColumns && searchConfig.searchableColumns.length > 0) return searchConfig.searchableColumns.some((col) => {
210
+ const cellValue = row[col];
211
+ return cellValue != null && String(cellValue).toLowerCase().includes(query);
212
+ });
213
+ return Object.values(row).some((val) => {
214
+ if (val == null) return false;
215
+ return String(val).toLowerCase().includes(query);
216
+ });
217
+ }
218
+ return true;
219
+ });
220
+ }
221
+
222
+ //#endregion
223
+ //#region src/components/features/data-table/core/store.ts
224
+ function createDataTableStore(options) {
225
+ let registeredFilters = /* @__PURE__ */ new Map();
226
+ const listeners = /* @__PURE__ */ new Set();
227
+ function computeFilteredData(s) {
228
+ if (s.mode === "server") return s.data;
229
+ return applyFilters(s.data, s.filters, s.search, registeredFilters, options.filterFns ?? {}, {
230
+ searchFn: options.searchFn,
231
+ searchableColumns: options.searchableColumns
232
+ });
233
+ }
234
+ let state = {
235
+ data: options.data,
236
+ filteredData: options.data,
237
+ sorting: options.defaultSort ?? [],
238
+ filters: options.defaultFilters ?? {},
239
+ search: "",
240
+ rowSelection: {},
241
+ pageIndex: 0,
242
+ pageSize: options.pageSize ?? DEFAULT_PAGE_SIZE,
243
+ mode: options.mode,
244
+ isLoading: false,
245
+ error: null,
246
+ inlineContents: [],
247
+ _version: 0
248
+ };
249
+ if (options.defaultFilters && Object.keys(options.defaultFilters).length > 0) state = {
250
+ ...state,
251
+ filteredData: computeFilteredData(state)
252
+ };
253
+ function notify() {
254
+ for (const listener of listeners) listener();
255
+ }
256
+ function setState(next) {
257
+ state = {
258
+ ...next,
259
+ _version: state._version + 1
260
+ };
261
+ notify();
262
+ }
263
+ return {
264
+ getSnapshot: () => state,
265
+ subscribe: (listener) => {
266
+ listeners.add(listener);
267
+ return () => listeners.delete(listener);
268
+ },
269
+ setData: (data) => {
270
+ const next = {
271
+ ...state,
272
+ data,
273
+ pageIndex: 0,
274
+ rowSelection: {}
275
+ };
276
+ setState({
277
+ ...next,
278
+ filteredData: computeFilteredData(next)
279
+ });
280
+ },
281
+ setServerData: (data) => {
282
+ setState({
283
+ ...state,
284
+ data,
285
+ filteredData: data
286
+ });
287
+ },
288
+ setTable: (_t) => {},
289
+ setSorting: (sorting) => {
290
+ setState({
291
+ ...state,
292
+ sorting,
293
+ rowSelection: {}
294
+ });
295
+ },
296
+ setFilter: (key, value) => {
297
+ const next = {
298
+ ...state,
299
+ filters: {
300
+ ...state.filters,
301
+ [key]: value
302
+ },
303
+ rowSelection: {},
304
+ pageIndex: 0
305
+ };
306
+ setState({
307
+ ...next,
308
+ filteredData: computeFilteredData(next)
309
+ });
310
+ },
311
+ clearFilter: (key) => {
312
+ const filters = Object.fromEntries(Object.entries(state.filters).filter(([k]) => k !== key));
313
+ const next = {
314
+ ...state,
315
+ filters,
316
+ rowSelection: {},
317
+ pageIndex: 0
318
+ };
319
+ setState({
320
+ ...next,
321
+ filteredData: computeFilteredData(next)
322
+ });
323
+ },
324
+ clearAllFilters: () => {
325
+ const next = {
326
+ ...state,
327
+ filters: {},
328
+ rowSelection: {},
329
+ pageIndex: 0
330
+ };
331
+ setState({
332
+ ...next,
333
+ filteredData: computeFilteredData(next)
334
+ });
335
+ },
336
+ setSearch: (search) => {
337
+ const next = {
338
+ ...state,
339
+ search,
340
+ rowSelection: {},
341
+ pageIndex: 0
342
+ };
343
+ setState({
344
+ ...next,
345
+ filteredData: computeFilteredData(next)
346
+ });
347
+ },
348
+ clearSearch: () => {
349
+ const next = {
350
+ ...state,
351
+ search: "",
352
+ rowSelection: {},
353
+ pageIndex: 0
354
+ };
355
+ setState({
356
+ ...next,
357
+ filteredData: computeFilteredData(next)
358
+ });
359
+ },
360
+ setRowSelection: (rowSelection) => {
361
+ setState({
362
+ ...state,
363
+ rowSelection
364
+ });
365
+ },
366
+ setPageIndex: (pageIndex) => {
367
+ if (!Number.isFinite(pageIndex) || pageIndex < 0) return;
368
+ setState({
369
+ ...state,
370
+ pageIndex: Math.floor(pageIndex),
371
+ rowSelection: {}
372
+ });
373
+ },
374
+ setPageSize: (pageSize) => {
375
+ if (!Number.isFinite(pageSize) || pageSize < 1) return;
376
+ setState({
377
+ ...state,
378
+ pageSize: Math.floor(pageSize),
379
+ pageIndex: 0,
380
+ rowSelection: {}
381
+ });
382
+ },
383
+ setPagination: (pageIndex, pageSize) => {
384
+ const safeIndex = Number.isFinite(pageIndex) ? Math.max(0, Math.floor(pageIndex)) : state.pageIndex;
385
+ const safeSize = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : state.pageSize;
386
+ setState({
387
+ ...state,
388
+ pageIndex: safeIndex,
389
+ pageSize: safeSize,
390
+ rowSelection: {}
391
+ });
392
+ },
393
+ setLoading: (isLoading) => {
394
+ setState({
395
+ ...state,
396
+ isLoading
397
+ });
398
+ },
399
+ setError: (error) => {
400
+ setState({
401
+ ...state,
402
+ error
403
+ });
404
+ },
405
+ registerFilter: (column, strategy) => {
406
+ const next = new Map(registeredFilters);
407
+ next.set(column, strategy);
408
+ registeredFilters = next;
409
+ const filteredData = computeFilteredData(state);
410
+ setState({
411
+ ...state,
412
+ filteredData
413
+ });
414
+ },
415
+ unregisterFilter: (column) => {
416
+ const next = new Map(registeredFilters);
417
+ next.delete(column);
418
+ registeredFilters = next;
419
+ if (column in state.filters) {
420
+ const filteredData = computeFilteredData(state);
421
+ setState({
422
+ ...state,
423
+ filteredData
424
+ });
425
+ }
426
+ },
427
+ registerInlineContent: (entry) => {
428
+ const existing = state.inlineContents.findIndex((e) => e.id === entry.id);
429
+ const inlineContents = existing >= 0 ? state.inlineContents.map((e, i) => i === existing ? entry : e) : [...state.inlineContents, entry];
430
+ setState({
431
+ ...state,
432
+ inlineContents
433
+ });
434
+ },
435
+ unregisterInlineContent: (id) => {
436
+ const inlineContents = state.inlineContents.filter((e) => e.id !== id);
437
+ setState({
438
+ ...state,
439
+ inlineContents
440
+ });
441
+ }
442
+ };
443
+ }
444
+
134
445
  //#endregion
135
446
  //#region src/components/features/data-table/core/data-table-context.tsx
447
+ const DataTableStoreContext = createContext(null);
448
+ const TableInstanceContext = createContext(null);
449
+ /**
450
+ * Monotonic counter that increments on every store mutation.
451
+ * Table-dependent hooks consume this context to force re-renders
452
+ * through React's {children} composition boundary, since the
453
+ * mutable table singleton (stable ref) cannot trigger context updates.
454
+ */
455
+ const DataTableRenderKeyContext = createContext(0);
456
+ /**
457
+ * Forces a re-render when the store changes. Used by table-dependent hooks.
458
+ * Uses useContext (not use()) because use() does not reliably register
459
+ * context subscriptions in SSR/hydration scenarios (React Router SSR),
460
+ * preventing re-renders when the context value changes.
461
+ */
462
+ function useRenderKey() {
463
+ return useContext(DataTableRenderKeyContext);
464
+ }
136
465
  const DataTableContext = createContext(null);
137
- function useDataTableContext() {
138
- const context = use(DataTableContext);
139
- if (!context) throw new Error("useDataTableContext must be used within a DataTable provider");
140
- return context;
466
+ function useDataTableStore() {
467
+ const store = use(DataTableStoreContext);
468
+ if (!store) throw new Error("useDataTableStore must be used within a <DataTable.Client> or <DataTable.Server> provider");
469
+ return store;
470
+ }
471
+ function useTableInstance() {
472
+ const table = use(TableInstanceContext);
473
+ if (!table) throw new Error("useTableInstance must be used within a <DataTable.Client> or <DataTable.Server> provider");
474
+ return table;
475
+ }
476
+
477
+ //#endregion
478
+ //#region src/components/features/data-table/hooks/use-selectors.ts
479
+ function shallowEqual(a, b) {
480
+ const keysA = Object.keys(a);
481
+ const keysB = Object.keys(b);
482
+ if (keysA.length !== keysB.length) return false;
483
+ for (const key of keysA) {
484
+ const va = a[key];
485
+ const vb = b[key];
486
+ if (va === vb) continue;
487
+ if (Array.isArray(va) && Array.isArray(vb)) {
488
+ if (va.length !== vb.length) return false;
489
+ for (let i = 0; i < va.length; i++) if (va[i] !== vb[i]) return false;
490
+ continue;
491
+ }
492
+ return false;
493
+ }
494
+ return true;
495
+ }
496
+ function useSliceSelector(selector) {
497
+ const store = useDataTableStore();
498
+ const cachedRef = useRef(null);
499
+ const getSnapshot = useCallback(() => {
500
+ const next = selector(store.getSnapshot());
501
+ if (cachedRef.current && shallowEqual(cachedRef.current, next)) return cachedRef.current;
502
+ cachedRef.current = next;
503
+ return next;
504
+ }, [store, selector]);
505
+ return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
506
+ }
507
+ function useDataTableFilters() {
508
+ const store = useDataTableStore();
509
+ return useSliceSelector(useCallback((state) => ({
510
+ filters: state.filters,
511
+ setFilter: store.setFilter,
512
+ clearFilter: store.clearFilter,
513
+ clearAllFilters: store.clearAllFilters,
514
+ registerFilter: store.registerFilter,
515
+ unregisterFilter: store.unregisterFilter
516
+ }), [store]));
517
+ }
518
+ function useDataTableSearch() {
519
+ const store = useDataTableStore();
520
+ return useSliceSelector(useCallback((state) => ({
521
+ search: state.search,
522
+ setSearch: store.setSearch,
523
+ clearSearch: store.clearSearch
524
+ }), [store]));
525
+ }
526
+ function useDataTableSorting() {
527
+ const store = useDataTableStore();
528
+ return useSliceSelector(useCallback((state) => ({
529
+ sorting: state.sorting,
530
+ setSorting: store.setSorting
531
+ }), [store]));
532
+ }
533
+ function useDataTableSelection() {
534
+ useRenderKey();
535
+ const store = useDataTableStore();
536
+ const table = useTableInstance();
537
+ return {
538
+ rowSelection: store.getSnapshot().rowSelection,
539
+ setRowSelection: store.setRowSelection,
540
+ selectedRows: table.getFilteredSelectedRowModel().rows.map((r) => r.original)
541
+ };
542
+ }
543
+ function useDataTablePagination() {
544
+ useRenderKey();
545
+ const store = useDataTableStore();
546
+ const table = useTableInstance();
547
+ const state = store.getSnapshot();
548
+ const nextPage = useCallback(() => table.nextPage(), [table]);
549
+ const prevPage = useCallback(() => table.previousPage(), [table]);
550
+ return {
551
+ canNextPage: table.getCanNextPage(),
552
+ canPrevPage: table.getCanPreviousPage(),
553
+ nextPage,
554
+ prevPage,
555
+ pageIndex: state.pageIndex,
556
+ pageCount: table.getPageCount(),
557
+ setPageIndex: store.setPageIndex,
558
+ pageSize: state.pageSize,
559
+ setPageSize: store.setPageSize,
560
+ totalRows: state.filteredData.length
561
+ };
562
+ }
563
+ function useDataTableRows() {
564
+ useRenderKey();
565
+ const table = useTableInstance();
566
+ return {
567
+ rows: table.getRowModel().rows,
568
+ headerGroups: table.getHeaderGroups(),
569
+ totalColumns: table.getAllColumns().length
570
+ };
571
+ }
572
+ function useDataTableLoading() {
573
+ return useSliceSelector(useCallback((state) => ({
574
+ isLoading: state.isLoading,
575
+ error: state.error
576
+ }), []));
577
+ }
578
+ function useDataTableInlineContents() {
579
+ const store = useDataTableStore();
580
+ return useSliceSelector(useCallback((state) => ({
581
+ inlineContents: state.inlineContents,
582
+ registerInlineContent: store.registerInlineContent,
583
+ unregisterInlineContent: store.unregisterInlineContent
584
+ }), [store]));
141
585
  }
142
586
 
587
+ //#endregion
588
+ //#region src/components/features/data-table/components/active-filters.tsx
589
+ function formatValue(column, value, formatter) {
590
+ if (formatter) {
591
+ const result = formatter(column, value);
592
+ if (result !== void 0) return result;
593
+ }
594
+ return String(value);
595
+ }
596
+ function FilterGroup({ label, children, className }) {
597
+ return /* @__PURE__ */ jsxs("div", {
598
+ className: cn("flex items-center gap-2 rounded-md border px-2 py-1", className),
599
+ "data-slot": "dt-filter-group",
600
+ "data-testid": "dt-filter-group",
601
+ children: [/* @__PURE__ */ jsx("span", {
602
+ className: "text-muted-foreground border-r pr-2 text-xs",
603
+ children: label
604
+ }), children]
605
+ });
606
+ }
607
+ const EMPTY_LABELS = {};
608
+ function ActiveFiltersInner({ label = "Selected Filters", filterLabels = EMPTY_LABELS, formatFilterValue: formatter, clearAll = "icon", clearAllLabel = "Clear all", className, groupClassName, badgeClassName }) {
609
+ const { filters, setFilter, clearFilter, clearAllFilters } = useDataTableFilters();
610
+ const { search, clearSearch } = useDataTableSearch();
611
+ const activeFilterEntries = Object.entries(filters).filter(([, value]) => value != null && value !== "" && !(Array.isArray(value) && value.length === 0));
612
+ const hasSearch = search.length > 0;
613
+ const hasFilters = activeFilterEntries.length > 0;
614
+ if (!hasSearch && !hasFilters) return null;
615
+ const totalGroups = activeFilterEntries.length + (hasSearch ? 1 : 0);
616
+ const removeArrayItem = (column, items, item) => {
617
+ const remaining = items.filter((v) => v !== item);
618
+ if (remaining.length > 0) setFilter(column, remaining);
619
+ else clearFilter(column);
620
+ };
621
+ const handleClearAll = () => {
622
+ clearAllFilters();
623
+ if (hasSearch) clearSearch();
624
+ };
625
+ const badgeCn = cn("flex items-center gap-1.5 px-2 py-0.5 text-xs", badgeClassName);
626
+ return /* @__PURE__ */ jsxs("div", {
627
+ className: cn("flex flex-wrap items-center gap-2", className),
628
+ "data-slot": "dt-active-filters",
629
+ "data-testid": "dt-active-filters",
630
+ children: [
631
+ label !== null && /* @__PURE__ */ jsx("span", {
632
+ className: "text-sm text-muted-foreground",
633
+ "data-slot": "dt-active-filters-label",
634
+ children: label
635
+ }),
636
+ hasSearch && /* @__PURE__ */ jsx(FilterGroup, {
637
+ label: "Search",
638
+ className: groupClassName,
639
+ children: /* @__PURE__ */ jsxs(Badge, {
640
+ type: "muted",
641
+ theme: "solid",
642
+ className: badgeCn,
643
+ children: [/* @__PURE__ */ jsx("span", { children: search }), /* @__PURE__ */ jsx(Button, {
644
+ theme: "borderless",
645
+ size: "small",
646
+ "aria-label": "Clear search",
647
+ className: "h-auto p-0 text-muted-foreground hover:text-foreground",
648
+ onClick: clearSearch,
649
+ children: /* @__PURE__ */ jsx(X, {
650
+ className: "size-2.5",
651
+ "aria-hidden": "true"
652
+ })
653
+ })]
654
+ })
655
+ }),
656
+ activeFilterEntries.map(([column, value]) => {
657
+ const groupLabel = filterLabels[column] ?? column;
658
+ if (Array.isArray(value)) return /* @__PURE__ */ jsx(FilterGroup, {
659
+ label: groupLabel,
660
+ className: groupClassName,
661
+ children: value.map((item) => /* @__PURE__ */ jsxs(Badge, {
662
+ type: "muted",
663
+ theme: "solid",
664
+ className: badgeCn,
665
+ children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, item, formatter) }), /* @__PURE__ */ jsx(Button, {
666
+ theme: "borderless",
667
+ size: "small",
668
+ "aria-label": `Remove ${formatValue(column, item, formatter)} from ${groupLabel}`,
669
+ className: "h-auto p-0 text-muted-foreground hover:text-foreground",
670
+ onClick: () => removeArrayItem(column, value, item),
671
+ children: /* @__PURE__ */ jsx(X, {
672
+ className: "size-2.5",
673
+ "aria-hidden": "true"
674
+ })
675
+ })]
676
+ }, item))
677
+ }, column);
678
+ return /* @__PURE__ */ jsx(FilterGroup, {
679
+ label: groupLabel,
680
+ className: groupClassName,
681
+ children: /* @__PURE__ */ jsxs(Badge, {
682
+ type: "muted",
683
+ theme: "solid",
684
+ className: badgeCn,
685
+ children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, value, formatter) }), /* @__PURE__ */ jsx(Button, {
686
+ theme: "borderless",
687
+ size: "small",
688
+ "aria-label": `Clear ${groupLabel} filter`,
689
+ className: "h-auto p-0 text-muted-foreground hover:text-foreground",
690
+ onClick: () => clearFilter(column),
691
+ children: /* @__PURE__ */ jsx(X, {
692
+ className: "size-2.5",
693
+ "aria-hidden": "true"
694
+ })
695
+ })]
696
+ })
697
+ }, column);
698
+ }),
699
+ totalGroups > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
700
+ clearAll === "icon" && /* @__PURE__ */ jsx(Button, {
701
+ theme: "borderless",
702
+ size: "small",
703
+ "aria-label": clearAllLabel,
704
+ title: clearAllLabel,
705
+ className: "h-auto p-1 text-muted-foreground hover:text-foreground",
706
+ "data-slot": "dt-clear-all-filters",
707
+ "data-testid": "dt-clear-all-filters",
708
+ onClick: handleClearAll,
709
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
710
+ }),
711
+ clearAll === "button" && /* @__PURE__ */ jsxs(Button, {
712
+ theme: "outline",
713
+ size: "small",
714
+ className: "h-auto px-2 py-1 text-xs",
715
+ "data-slot": "dt-clear-all-filters",
716
+ "data-testid": "dt-clear-all-filters",
717
+ onClick: handleClearAll,
718
+ children: [/* @__PURE__ */ jsx(X, { className: "size-3 mr-1" }), clearAllLabel]
719
+ }),
720
+ clearAll === "text" && /* @__PURE__ */ jsx(Button, {
721
+ theme: "borderless",
722
+ size: "small",
723
+ className: "h-auto px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground",
724
+ "data-slot": "dt-clear-all-filters",
725
+ "data-testid": "dt-clear-all-filters",
726
+ onClick: handleClearAll,
727
+ children: clearAllLabel
728
+ })
729
+ ] })
730
+ ]
731
+ });
732
+ }
733
+ const DataTableActiveFilters = memo(ActiveFiltersInner);
734
+
143
735
  //#endregion
144
736
  //#region src/components/features/data-table/components/bulk-actions.tsx
145
737
  function DataTableBulkActions({ children, className }) {
146
- const { table, rowSelection } = useDataTableContext();
147
- const selectedRows = useMemo(() => {
148
- return table.getFilteredSelectedRowModel().rows.map((row) => row.original);
149
- }, [table, rowSelection]);
738
+ const { selectedRows } = useDataTableSelection();
150
739
  if (selectedRows.length === 0) return null;
151
740
  return /* @__PURE__ */ jsx("div", {
152
741
  "data-slot": "dt-bulk-actions",
@@ -170,7 +759,8 @@ function DataTableColumnHeader({ column, title, className }) {
170
759
  children: /* @__PURE__ */ jsxs("button", {
171
760
  type: "button",
172
761
  className: "flex items-center gap-1 hover:text-foreground -ml-3 h-8 px-3 cursor-pointer",
173
- onClick: () => column.toggleSorting(sorted === "asc"),
762
+ onClick: column.getToggleSortingHandler(),
763
+ "aria-label": `Sort by ${title}${sorted === "asc" ? ", sorted ascending" : sorted === "desc" ? ", sorted descending" : ""}`,
174
764
  children: [/* @__PURE__ */ jsx("span", { children: title }), sorted === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { className: "size-4" }) : sorted === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "size-4" })]
175
765
  })
176
766
  });
@@ -197,10 +787,10 @@ function renderInlineContentRow(entry, colSpan, rows) {
197
787
  }, entry.id);
198
788
  }
199
789
  function DataTableContent({ emptyMessage, className, tableClassName, headerClassName, headerRowClassName, headerCellClassName, bodyClassName, rowClassName, cellClassName }) {
200
- const { table, inlineContents } = useDataTableContext();
201
- const rows = table.getRowModel().rows;
790
+ const { rows, headerGroups, totalColumns } = useDataTableRows();
791
+ const { inlineContents } = useDataTableInlineContents();
202
792
  const openInlineContents = useMemo(() => inlineContents.filter((e) => e.open), [inlineContents]);
203
- const colSpan = table.getAllColumns().length;
793
+ const colSpan = totalColumns;
204
794
  return /* @__PURE__ */ jsx("div", {
205
795
  className: cn("datum-ui-data-table", className),
206
796
  "data-slot": "dt",
@@ -211,7 +801,7 @@ function DataTableContent({ emptyMessage, className, tableClassName, headerClass
211
801
  children: [/* @__PURE__ */ jsx(TableHeader, {
212
802
  className: cn(headerClassName),
213
803
  "data-slot": "dt-header",
214
- children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
804
+ children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
215
805
  className: cn(headerRowClassName),
216
806
  "data-slot": "dt-header-row",
217
807
  children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(TableHead, {
@@ -228,6 +818,7 @@ function DataTableContent({ emptyMessage, className, tableClassName, headerClass
228
818
  if (rowEntry) return renderInlineContentRow(rowEntry, colSpan, rows);
229
819
  return /* @__PURE__ */ jsx(TableRow, {
230
820
  className: cn(resolveClassName(rowClassName, row)),
821
+ style: { transitionProperty: "none" },
231
822
  "data-slot": "dt-row",
232
823
  "data-state": row.getIsSelected() ? "selected" : void 0,
233
824
  children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
@@ -254,7 +845,7 @@ function DataTableContent({ emptyMessage, className, tableClassName, headerClass
254
845
  //#region src/components/features/data-table/components/inline-content.tsx
255
846
  function DataTableInlineContent({ position, rowId, open, onClose, className, children }) {
256
847
  const id = useId();
257
- const { registerInlineContent, unregisterInlineContent } = useDataTableContext();
848
+ const { registerInlineContent, unregisterInlineContent } = useDataTableInlineContents();
258
849
  const initialRender = useRef(true);
259
850
  useEffect(() => {
260
851
  registerInlineContent({
@@ -325,57 +916,121 @@ function DataTableLoading({ rows = DEFAULT_LOADING_ROWS, columns = 4, className
325
916
 
326
917
  //#endregion
327
918
  //#region src/components/features/data-table/components/pagination.tsx
919
+ /**
920
+ * Generates page numbers with ellipsis for large page counts.
921
+ * Shows up to 7 items: first, last, current +/- 1 neighbor, and ellipsis gaps.
922
+ */
923
+ function getPageNumbers(currentPage, totalPages) {
924
+ if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
925
+ const pages = [1];
926
+ const current = currentPage + 1;
927
+ if (current <= 4) {
928
+ for (let i = 2; i <= 5; i++) pages.push(i);
929
+ pages.push("...");
930
+ pages.push(totalPages);
931
+ } else if (current >= totalPages - 3) {
932
+ pages.push("...");
933
+ for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
934
+ } else {
935
+ pages.push("...");
936
+ for (let i = current - 1; i <= current + 1; i++) pages.push(i);
937
+ pages.push("...");
938
+ pages.push(totalPages);
939
+ }
940
+ return pages;
941
+ }
328
942
  function DataTablePagination({ pageSizes = DEFAULT_PAGE_SIZES, className }) {
329
- const { pagination, mode } = useDataTableContext();
943
+ const { canNextPage, canPrevPage, nextPage, prevPage, pageIndex, pageCount, setPageIndex, pageSize, setPageSize, totalRows } = useDataTablePagination();
944
+ const isClientMode = pageCount > 0;
945
+ const startRow = pageIndex * pageSize + 1;
946
+ const endRow = Math.min((pageIndex + 1) * pageSize, totalRows);
947
+ const pageNumbers = useMemo(() => getPageNumbers(pageIndex, pageCount), [pageIndex, pageCount]);
330
948
  return /* @__PURE__ */ jsxs("div", {
331
- className: cn("flex items-center justify-between px-2 py-4", className),
949
+ className: cn("flex flex-col-reverse items-center justify-between gap-4 px-2 py-4 sm:flex-row", className),
332
950
  "data-slot": "dt-pagination",
333
951
  children: [/* @__PURE__ */ jsxs("div", {
334
- className: "flex items-center gap-2",
335
- children: [/* @__PURE__ */ jsx("span", {
336
- className: "text-sm text-muted-foreground",
337
- children: "Rows per page"
338
- }), /* @__PURE__ */ jsxs(Select, {
339
- value: String(pagination.pageSize),
340
- onValueChange: (value) => pagination.setPageSize(Number(value)),
341
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
342
- className: "h-8 w-[70px]",
343
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: String(pagination.pageSize) })
344
- }), /* @__PURE__ */ jsx(SelectContent, {
345
- side: "top",
346
- children: pageSizes.map((size) => /* @__PURE__ */ jsx(SelectItem, {
347
- value: String(size),
348
- children: size
349
- }, size))
952
+ className: "flex items-center gap-4",
953
+ children: [isClientMode && totalRows > 0 && /* @__PURE__ */ jsxs("span", {
954
+ className: "text-sm text-muted-foreground whitespace-nowrap",
955
+ children: [
956
+ "Showing",
957
+ " ",
958
+ startRow,
959
+ " ",
960
+ "to",
961
+ " ",
962
+ endRow,
963
+ " ",
964
+ "of",
965
+ " ",
966
+ totalRows,
967
+ " ",
968
+ "rows"
969
+ ]
970
+ }), /* @__PURE__ */ jsxs("div", {
971
+ className: "flex items-center gap-2",
972
+ children: [/* @__PURE__ */ jsx("span", {
973
+ className: "text-sm text-muted-foreground whitespace-nowrap",
974
+ children: "Rows per page"
975
+ }), /* @__PURE__ */ jsxs(Select, {
976
+ value: String(pageSize),
977
+ onValueChange: (value) => setPageSize(Number(value)),
978
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
979
+ className: "h-8 w-[70px]",
980
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: String(pageSize) })
981
+ }), /* @__PURE__ */ jsx(SelectContent, {
982
+ side: "top",
983
+ children: pageSizes.map((size) => /* @__PURE__ */ jsx(SelectItem, {
984
+ value: String(size),
985
+ children: size
986
+ }, size))
987
+ })]
350
988
  })]
351
989
  })]
352
- }), /* @__PURE__ */ jsxs("div", {
353
- className: "flex items-center gap-2",
990
+ }), /* @__PURE__ */ jsxs("nav", {
991
+ "aria-label": "Table pagination",
992
+ className: "flex items-center gap-1",
354
993
  children: [
355
- mode === "client" && pagination.pageIndex !== void 0 && pagination.pageCount !== void 0 && /* @__PURE__ */ jsxs("span", {
356
- className: "text-sm text-muted-foreground",
994
+ /* @__PURE__ */ jsx(Button, {
995
+ theme: "outline",
996
+ size: "icon",
997
+ className: "size-8",
998
+ onClick: prevPage,
999
+ disabled: !canPrevPage,
1000
+ "aria-label": "Previous page",
1001
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
1002
+ }),
1003
+ isClientMode && pageCount > 1 ? pageNumbers.map((page, index) => {
1004
+ if (page === "...") return /* @__PURE__ */ jsx("span", {
1005
+ className: "px-2 text-sm text-muted-foreground",
1006
+ children: "..."
1007
+ }, `ellipsis-${index}`);
1008
+ const isActive = page === pageIndex + 1;
1009
+ return /* @__PURE__ */ jsx(Button, {
1010
+ theme: isActive ? "solid" : "outline",
1011
+ size: "small",
1012
+ className: cn("h-8 min-w-8 px-2", isActive && "pointer-events-none font-semibold"),
1013
+ onClick: () => setPageIndex(page - 1),
1014
+ "aria-disabled": isActive || void 0,
1015
+ "aria-label": `Page ${page}`,
1016
+ "aria-current": isActive ? "page" : void 0,
1017
+ children: page
1018
+ }, page);
1019
+ }) : !isClientMode && /* @__PURE__ */ jsxs("span", {
1020
+ className: "px-2 text-sm text-muted-foreground",
357
1021
  children: [
358
1022
  "Page",
359
1023
  " ",
360
- pagination.pageIndex + 1,
361
- " ",
362
- "of",
363
- " ",
364
- pagination.pageCount
1024
+ pageIndex + 1
365
1025
  ]
366
1026
  }),
367
1027
  /* @__PURE__ */ jsx(Button, {
368
- variant: "outline",
369
- size: "sm",
370
- onClick: pagination.prevPage,
371
- disabled: !pagination.canPrevPage,
372
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
373
- }),
374
- /* @__PURE__ */ jsx(Button, {
375
- variant: "outline",
376
- size: "sm",
377
- onClick: pagination.nextPage,
378
- disabled: !pagination.canNextPage,
1028
+ theme: "outline",
1029
+ size: "icon",
1030
+ className: "size-8",
1031
+ onClick: nextPage,
1032
+ disabled: !canNextPage,
1033
+ "aria-label": "Next page",
379
1034
  children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
380
1035
  })
381
1036
  ]
@@ -395,8 +1050,8 @@ function DataTableRowActions({ row, actions, isLoading = false, className }) {
395
1050
  return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
396
1051
  asChild: true,
397
1052
  children: /* @__PURE__ */ jsxs(Button, {
398
- variant: "ghost",
399
- size: "sm",
1053
+ theme: "borderless",
1054
+ size: "small",
400
1055
  className,
401
1056
  disabled: isLoading,
402
1057
  "data-slot": "dt-row-actions",
@@ -421,7 +1076,7 @@ function DataTableRowActions({ row, actions, isLoading = false, className }) {
421
1076
  //#endregion
422
1077
  //#region src/components/features/data-table/components/search.tsx
423
1078
  function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOUNCE_MS, className }) {
424
- const { search, setSearch } = useDataTableContext();
1079
+ const { search, setSearch } = useDataTableSearch();
425
1080
  const [inputValue, setInputValue] = useState(search);
426
1081
  useEffect(() => {
427
1082
  setInputValue(search);
@@ -442,262 +1097,331 @@ function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOU
442
1097
  value: inputValue,
443
1098
  onChange: (e) => setInputValue(e.target.value),
444
1099
  className,
1100
+ "aria-label": placeholder,
445
1101
  "data-slot": "dt-search"
446
1102
  });
447
1103
  }
448
1104
 
449
1105
  //#endregion
450
- //#region src/components/features/data-table/hooks/use-inline-contents.ts
451
- function useInlineContents() {
452
- const [inlineContents, setInlineContents] = useState([]);
453
- return {
454
- inlineContents,
455
- registerInlineContent: useCallback((entry) => {
456
- setInlineContents((prev) => {
457
- const index = prev.findIndex((e) => e.id === entry.id);
458
- if (index >= 0) {
459
- const next = [...prev];
460
- next[index] = entry;
461
- return next;
462
- }
463
- return [...prev, entry];
464
- });
465
- }, []),
466
- unregisterInlineContent: useCallback((id) => {
467
- setInlineContents((prev) => prev.filter((e) => e.id !== id));
468
- }, [])
469
- };
470
- }
471
-
472
- //#endregion
473
- //#region src/components/features/data-table/core/client-provider.tsx
474
- function ClientProvider({ data, columns, sorting, setSorting, filters, setFilter, clearFilter, clearAllFilters, search, setSearch, clearSearch, searchableColumns, searchFn, rowSelection, setRowSelection, pagination, getRowId, enableRowSelection = false, className, children }) {
475
- const columnFilters = useMemo(() => Object.entries(filters).map(([id, value]) => ({
476
- id,
477
- value
478
- })), [filters]);
479
- const arrayIncludesFilter = useMemo(() => (row, columnId, filterValue) => {
480
- if (filterValue == null) return true;
481
- if (Array.isArray(filterValue) && filterValue.length === 0) return true;
482
- const cellValue = row.getValue(columnId);
483
- if (Array.isArray(filterValue)) {
484
- if (Array.isArray(cellValue)) return cellValue.some((v) => filterValue.includes(v));
485
- return filterValue.includes(cellValue);
1106
+ //#region src/components/features/data-table/hooks/use-data-table-client.ts
1107
+ function useDataTableClient(options) {
1108
+ const { data, columns, pageSize, getRowId, enableRowSelection = false, defaultSort, defaultFilters, searchableColumns, searchFn, filterFns, stateAdapter } = options;
1109
+ const store = useMemo(() => createDataTableStore({
1110
+ data,
1111
+ mode: "client",
1112
+ defaultSort,
1113
+ defaultFilters,
1114
+ pageSize,
1115
+ searchableColumns,
1116
+ searchFn,
1117
+ filterFns
1118
+ }), []);
1119
+ const isInitialRender = useRef(true);
1120
+ useEffect(() => {
1121
+ if (isInitialRender.current) {
1122
+ isInitialRender.current = false;
1123
+ return;
486
1124
  }
487
- if (typeof filterValue === "string" && typeof cellValue === "string") return cellValue.toLowerCase().includes(filterValue.toLowerCase());
488
- return cellValue === filterValue;
489
- }, []);
490
- const globalFilterFn = useMemo(() => {
491
- if (searchFn) return (row, _columnId, filterValue) => searchFn(row.original, String(filterValue));
492
- if (searchableColumns && searchableColumns.length > 0) return (row, _columnId, filterValue) => {
493
- const query = String(filterValue).toLowerCase();
494
- return searchableColumns.some((col) => {
495
- const cellValue = row.getValue(col);
496
- return cellValue != null && String(cellValue).toLowerCase().includes(query);
497
- });
498
- };
499
- }, [searchFn, searchableColumns]);
1125
+ store.setData(data);
1126
+ }, [data, store]);
1127
+ const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1128
+ const { filteredData, sorting, rowSelection, pageIndex, pageSize: storePageSize, filters, search } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
500
1129
  const table = useReactTable({
501
- data,
502
- columns,
1130
+ data: filteredData,
1131
+ columns: resolvedColumns,
503
1132
  state: {
504
1133
  sorting,
505
1134
  rowSelection,
506
- globalFilter: search,
507
- columnFilters,
508
1135
  pagination: {
509
- pageIndex: pagination.pageIndex,
510
- pageSize: pagination.pageSize
1136
+ pageIndex,
1137
+ pageSize: storePageSize
511
1138
  }
512
1139
  },
513
- filterFns: { arrayIncludes: arrayIncludesFilter },
514
- defaultColumn: { filterFn: arrayIncludesFilter },
515
- ...globalFilterFn ? { globalFilterFn } : {},
516
1140
  onSortingChange: (updater) => {
517
- setSorting(typeof updater === "function" ? updater(sorting) : updater);
1141
+ const next = typeof updater === "function" ? updater(sorting) : updater;
1142
+ store.setSorting(next);
518
1143
  },
519
1144
  onRowSelectionChange: (updater) => {
520
- setRowSelection(typeof updater === "function" ? updater(rowSelection) : updater);
1145
+ const next = typeof updater === "function" ? updater(rowSelection) : updater;
1146
+ store.setRowSelection(next);
521
1147
  },
522
1148
  onPaginationChange: (updater) => {
523
1149
  const next = typeof updater === "function" ? updater({
524
- pageIndex: pagination.pageIndex,
525
- pageSize: pagination.pageSize
1150
+ pageIndex,
1151
+ pageSize: storePageSize
526
1152
  }) : updater;
527
- pagination.setPageIndex(next.pageIndex);
528
- pagination.setPageSize(next.pageSize);
1153
+ store.setPagination(next.pageIndex, next.pageSize);
529
1154
  },
530
1155
  getCoreRowModel: getCoreRowModel(),
531
1156
  getSortedRowModel: getSortedRowModel(),
532
- getFilteredRowModel: getFilteredRowModel(),
533
1157
  getPaginationRowModel: getPaginationRowModel(),
534
1158
  getRowId,
535
1159
  enableRowSelection: !!enableRowSelection
536
1160
  });
537
- const { inlineContents, registerInlineContent, unregisterInlineContent } = useInlineContents();
538
- const paginationState = useMemo(() => ({
539
- canNextPage: table.getCanNextPage(),
540
- canPrevPage: table.getCanPreviousPage(),
541
- nextPage: () => table.nextPage(),
542
- prevPage: () => table.previousPage(),
543
- pageIndex: pagination.pageIndex,
544
- pageCount: table.getPageCount(),
545
- setPageIndex: pagination.setPageIndex,
546
- pageSize: pagination.pageSize,
547
- setPageSize: pagination.setPageSize
548
- }), [table, pagination]);
549
- return /* @__PURE__ */ jsx(DataTableContext, {
550
- value: useMemo(() => ({
551
- table,
552
- mode: "client",
553
- sorting,
554
- filters,
555
- search,
556
- rowSelection,
557
- isLoading: false,
558
- setSorting,
559
- setFilter,
560
- clearFilter,
561
- clearAllFilters,
562
- setSearch,
563
- clearSearch,
564
- setRowSelection,
565
- pagination: paginationState,
566
- inlineContents,
567
- registerInlineContent,
568
- unregisterInlineContent
569
- }), [
570
- table,
1161
+ const hydratedRef = useRef(false);
1162
+ useEffect(() => {
1163
+ if (stateAdapter && !hydratedRef.current) {
1164
+ hydratedRef.current = true;
1165
+ const persisted = stateAdapter.read();
1166
+ if (persisted.sorting && persisted.sorting.length > 0) store.setSorting(persisted.sorting);
1167
+ if (persisted.filters) {
1168
+ for (const [key, value] of Object.entries(persisted.filters)) if (value != null) store.setFilter(key, value);
1169
+ }
1170
+ if (persisted.search) store.setSearch(persisted.search);
1171
+ if (persisted.pageIndex != null && persisted.pageIndex > 0) store.setPageIndex(persisted.pageIndex);
1172
+ if (persisted.pageSize != null) store.setPageSize(persisted.pageSize);
1173
+ }
1174
+ }, []);
1175
+ const isFirstWrite = useRef(true);
1176
+ useEffect(() => {
1177
+ if (!stateAdapter) return;
1178
+ if (isFirstWrite.current) {
1179
+ isFirstWrite.current = false;
1180
+ return;
1181
+ }
1182
+ stateAdapter.write({
571
1183
  sorting,
572
1184
  filters,
573
1185
  search,
574
- rowSelection,
575
- setSorting,
576
- setFilter,
577
- clearFilter,
578
- clearAllFilters,
579
- setSearch,
580
- clearSearch,
581
- setRowSelection,
582
- paginationState,
583
- inlineContents,
584
- registerInlineContent,
585
- unregisterInlineContent
586
- ]),
587
- children: /* @__PURE__ */ jsx("div", {
588
- className,
589
- children
1186
+ pageIndex,
1187
+ pageSize: storePageSize
1188
+ });
1189
+ }, [
1190
+ sorting,
1191
+ filters,
1192
+ search,
1193
+ pageIndex,
1194
+ storePageSize,
1195
+ stateAdapter
1196
+ ]);
1197
+ return {
1198
+ store,
1199
+ table
1200
+ };
1201
+ }
1202
+
1203
+ //#endregion
1204
+ //#region src/components/features/data-table/hooks/use-is-client.ts
1205
+ /**
1206
+ * Returns `false` during SSR and `true` after hydration.
1207
+ * Used to gate components that depend on client-only APIs
1208
+ * (e.g. TanStack Table's internal useSyncExternalStore call
1209
+ * which omits the getServerSnapshot argument).
1210
+ */
1211
+ function useIsClient() {
1212
+ const [isClient, setIsClient] = useState(false);
1213
+ useEffect(() => setIsClient(true), []);
1214
+ return isClient;
1215
+ }
1216
+
1217
+ //#endregion
1218
+ //#region src/components/features/data-table/core/client-provider.tsx
1219
+ /**
1220
+ * Inner component that calls useDataTableClient.
1221
+ * Only rendered on the client (gated by ClientProvider).
1222
+ */
1223
+ function ClientProviderInner({ className, children, ssrFallback: _ssrFallback, ...options }) {
1224
+ const { store, table } = useDataTableClient(options);
1225
+ return /* @__PURE__ */ jsx(DataTableStoreContext, {
1226
+ value: store,
1227
+ children: /* @__PURE__ */ jsx(TableInstanceContext, {
1228
+ value: table,
1229
+ children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1230
+ value: store.getSnapshot()._version,
1231
+ children: /* @__PURE__ */ jsx("div", {
1232
+ className,
1233
+ children
1234
+ })
1235
+ })
590
1236
  })
591
1237
  });
592
1238
  }
1239
+ function ClientProvider(props) {
1240
+ if (!useIsClient()) return /* @__PURE__ */ jsx(Fragment$1, { children: props.ssrFallback ?? null });
1241
+ return /* @__PURE__ */ jsx(ClientProviderInner, { ...props });
1242
+ }
593
1243
 
594
1244
  //#endregion
595
- //#region src/components/features/data-table/core/server-provider.tsx
596
- function ServerProvider({ data, columns, isLoading, sorting, setSorting, filters, setFilter, clearFilter, clearAllFilters, search, setSearch, clearSearch, rowSelection, setRowSelection, pagination, getRowId, enableRowSelection = false, className, children }) {
1245
+ //#region src/components/features/data-table/hooks/use-data-table-server.ts
1246
+ function useDataTableServer(options) {
1247
+ const { columns, fetchFn, transform, limit = 20, getRowId, enableRowSelection = false, defaultSort, defaultFilters, stateAdapter } = options;
1248
+ const fetchRef = useRef(fetchFn);
1249
+ const transformRef = useRef(transform);
1250
+ useEffect(() => {
1251
+ fetchRef.current = fetchFn;
1252
+ }, [fetchFn]);
1253
+ useEffect(() => {
1254
+ transformRef.current = transform;
1255
+ }, [transform]);
1256
+ const cursorMapRef = useRef(/* @__PURE__ */ new Map());
1257
+ const hasNextPageRef = useRef(false);
1258
+ const store = useMemo(() => createDataTableStore({
1259
+ data: [],
1260
+ mode: "server",
1261
+ defaultSort,
1262
+ defaultFilters,
1263
+ pageSize: limit
1264
+ }), []);
1265
+ const { sorting, filters, search, rowSelection, pageSize, pageIndex } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1266
+ const prevQueryRef = useRef({
1267
+ sorting,
1268
+ filters,
1269
+ search,
1270
+ pageSize
1271
+ });
1272
+ useEffect(() => {
1273
+ const prev = prevQueryRef.current;
1274
+ if (prev.sorting !== sorting || prev.filters !== filters || prev.search !== search || prev.pageSize !== pageSize) {
1275
+ cursorMapRef.current = /* @__PURE__ */ new Map();
1276
+ hasNextPageRef.current = false;
1277
+ if (pageIndex !== 0) {
1278
+ prevQueryRef.current = {
1279
+ sorting,
1280
+ filters,
1281
+ search,
1282
+ pageSize
1283
+ };
1284
+ store.setPageIndex(0);
1285
+ return;
1286
+ }
1287
+ }
1288
+ prevQueryRef.current = {
1289
+ sorting,
1290
+ filters,
1291
+ search,
1292
+ pageSize
1293
+ };
1294
+ let cancelled = false;
1295
+ store.setLoading(true);
1296
+ const cursor = cursorMapRef.current.get(pageIndex);
1297
+ fetchRef.current({
1298
+ sorting,
1299
+ filters,
1300
+ search,
1301
+ cursor,
1302
+ limit: pageSize
1303
+ }).then((response) => {
1304
+ if (cancelled) return;
1305
+ const result = transformRef.current(response);
1306
+ store.setServerData(result.data);
1307
+ store.setError(null);
1308
+ if (result.nextCursor) cursorMapRef.current.set(pageIndex + 1, result.nextCursor);
1309
+ hasNextPageRef.current = result.hasNextPage;
1310
+ }).catch((error) => {
1311
+ if (cancelled) return;
1312
+ store.setServerData([]);
1313
+ store.setError(error instanceof Error ? error : new Error(String(error)));
1314
+ hasNextPageRef.current = false;
1315
+ }).finally(() => {
1316
+ if (!cancelled) store.setLoading(false);
1317
+ });
1318
+ return () => {
1319
+ cancelled = true;
1320
+ };
1321
+ }, [
1322
+ sorting,
1323
+ filters,
1324
+ search,
1325
+ pageSize,
1326
+ pageIndex,
1327
+ store
1328
+ ]);
1329
+ const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
597
1330
  const table = useReactTable({
598
- data,
599
- columns,
1331
+ data: store.getSnapshot().data,
1332
+ columns: resolvedColumns,
600
1333
  state: {
601
1334
  sorting,
602
- rowSelection
1335
+ rowSelection,
1336
+ pagination: {
1337
+ pageIndex,
1338
+ pageSize
1339
+ }
603
1340
  },
1341
+ manualPagination: true,
1342
+ manualSorting: true,
1343
+ manualFiltering: true,
1344
+ pageCount: hasNextPageRef.current ? pageIndex + 2 : pageIndex + 1,
1345
+ getCoreRowModel: getCoreRowModel(),
1346
+ getRowId,
1347
+ enableRowSelection: !!enableRowSelection,
604
1348
  onSortingChange: (updater) => {
605
- setSorting(typeof updater === "function" ? updater(sorting) : updater);
1349
+ const next = typeof updater === "function" ? updater(sorting) : updater;
1350
+ store.setSorting(next);
606
1351
  },
607
1352
  onRowSelectionChange: (updater) => {
608
- setRowSelection(typeof updater === "function" ? updater(rowSelection) : updater);
1353
+ const next = typeof updater === "function" ? updater(rowSelection) : updater;
1354
+ store.setRowSelection(next);
609
1355
  },
610
- getCoreRowModel: getCoreRowModel(),
611
- getRowId,
612
- enableRowSelection: !!enableRowSelection,
613
- manualPagination: true,
614
- manualSorting: true,
615
- manualFiltering: true
1356
+ onPaginationChange: (updater) => {
1357
+ const next = typeof updater === "function" ? updater({
1358
+ pageIndex,
1359
+ pageSize
1360
+ }) : updater;
1361
+ store.setPagination(next.pageIndex, next.pageSize);
1362
+ }
616
1363
  });
617
- const { inlineContents, registerInlineContent, unregisterInlineContent } = useInlineContents();
618
- const paginationState = useMemo(() => ({
619
- canNextPage: pagination.hasNextPage,
620
- canPrevPage: pagination.hasPrevPage,
621
- nextPage: pagination.nextPage,
622
- prevPage: pagination.prevPage,
623
- pageSize: pagination.limit,
624
- setPageSize: pagination.setLimit
625
- }), [pagination]);
626
- return /* @__PURE__ */ jsx(DataTableContext, {
627
- value: useMemo(() => ({
628
- table,
629
- mode: "server",
630
- sorting,
631
- filters,
632
- search,
633
- rowSelection,
634
- isLoading,
635
- setSorting,
636
- setFilter,
637
- clearFilter,
638
- clearAllFilters,
639
- setSearch,
640
- clearSearch,
641
- setRowSelection,
642
- pagination: paginationState,
643
- inlineContents,
644
- registerInlineContent,
645
- unregisterInlineContent
646
- }), [
647
- table,
1364
+ useEffect(() => {
1365
+ if (stateAdapter) stateAdapter.write({
648
1366
  sorting,
649
1367
  filters,
650
1368
  search,
651
- rowSelection,
652
- isLoading,
653
- setSorting,
654
- setFilter,
655
- clearFilter,
656
- clearAllFilters,
657
- setSearch,
658
- clearSearch,
659
- setRowSelection,
660
- paginationState,
661
- inlineContents,
662
- registerInlineContent,
663
- unregisterInlineContent
664
- ]),
665
- children: /* @__PURE__ */ jsx("div", {
666
- className,
667
- children
668
- })
669
- });
1369
+ pageSize
1370
+ });
1371
+ }, [
1372
+ sorting,
1373
+ filters,
1374
+ search,
1375
+ pageSize,
1376
+ stateAdapter
1377
+ ]);
1378
+ return {
1379
+ store,
1380
+ table
1381
+ };
670
1382
  }
671
1383
 
672
1384
  //#endregion
673
- //#region ../shadcn/ui/badge.tsx
1385
+ //#region src/components/features/data-table/core/server-provider.tsx
674
1386
  /**
675
- * Vanilla shadcn/ui Badge Component
676
- * Pure shadcn badge without Datum customizations
677
- * For Datum-specific variants (sunglow, butter), import from @/modules/datum-ui
1387
+ * Inner component that calls useDataTableServer.
1388
+ * Only rendered on the client (gated by ServerProvider).
678
1389
  */
679
- const badgeVariants = cva("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", {
680
- variants: { variant: {
681
- default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
682
- secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
683
- destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
684
- outline: "text-foreground"
685
- } },
686
- defaultVariants: { variant: "default" }
687
- });
688
- function Badge({ className, variant, ...props }) {
689
- return /* @__PURE__ */ jsx("div", {
690
- className: cn(badgeVariants({ variant }), className),
691
- ...props
1390
+ function ServerProviderInner({ className, children, ssrFallback: _ssrFallback, ...options }) {
1391
+ const { store, table } = useDataTableServer(options);
1392
+ return /* @__PURE__ */ jsx(DataTableStoreContext, {
1393
+ value: store,
1394
+ children: /* @__PURE__ */ jsx(TableInstanceContext, {
1395
+ value: table,
1396
+ children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
1397
+ value: store.getSnapshot()._version,
1398
+ children: /* @__PURE__ */ jsx("div", {
1399
+ className,
1400
+ children
1401
+ })
1402
+ })
1403
+ })
692
1404
  });
693
1405
  }
1406
+ function ServerProvider(props) {
1407
+ if (!useIsClient()) return /* @__PURE__ */ jsx(Fragment$1, { children: props.ssrFallback ?? null });
1408
+ return /* @__PURE__ */ jsx(ServerProviderInner, { ...props });
1409
+ }
694
1410
 
695
1411
  //#endregion
696
1412
  //#region src/components/features/data-table/filters/checkbox-filter.tsx
697
1413
  const MAX_VISIBLE_BADGES = 2;
698
1414
  function CheckboxFilter({ column, label, options, className, checkboxPopoverClassName }) {
699
- const { filters, setFilter, clearFilter } = useDataTableContext();
1415
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
700
1416
  const [open, setOpen] = useState(false);
1417
+ useEffect(() => {
1418
+ registerFilter(column, "checkbox");
1419
+ return () => unregisterFilter(column);
1420
+ }, [
1421
+ column,
1422
+ registerFilter,
1423
+ unregisterFilter
1424
+ ]);
701
1425
  const selectedValues = filters[column] ?? [];
702
1426
  const updateValues = (newValues) => {
703
1427
  if (newValues.length > 0) setFilter(column, newValues);
@@ -717,7 +1441,7 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
717
1441
  children: [/* @__PURE__ */ jsx(PopoverTrigger, {
718
1442
  asChild: true,
719
1443
  children: /* @__PURE__ */ jsxs(Button, {
720
- variant: "outline",
1444
+ theme: "outline",
721
1445
  className: cn("justify-between gap-1", className),
722
1446
  "data-slot": "dt-filter",
723
1447
  "data-testid": "dt-filter-trigger",
@@ -726,7 +1450,8 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
726
1450
  children: [visibleBadges.map((val) => {
727
1451
  const opt = options.find((o) => o.value === val);
728
1452
  return /* @__PURE__ */ jsxs(Badge, {
729
- variant: "secondary",
1453
+ type: "secondary",
1454
+ theme: "light",
730
1455
  className: "text-xs px-1.5 py-0",
731
1456
  children: [opt?.label ?? val, /* @__PURE__ */ jsx("span", {
732
1457
  role: "button",
@@ -766,8 +1491,8 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
766
1491
  className: "text-sm font-medium",
767
1492
  children: label
768
1493
  }), selectedValues.length > 0 && /* @__PURE__ */ jsx(Button, {
769
- variant: "ghost",
770
- size: "sm",
1494
+ theme: "borderless",
1495
+ size: "small",
771
1496
  className: "h-auto p-1 text-xs",
772
1497
  onClick: () => clearFilter(column),
773
1498
  children: "Clear"
@@ -797,8 +1522,16 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
797
1522
  //#endregion
798
1523
  //#region src/components/features/data-table/filters/date-picker-filter.tsx
799
1524
  function DatePickerFilter({ column, label, className, datePickerPopoverClassName, disableFuture, disablePast, minDate, maxDate }) {
800
- const { filters, setFilter, clearFilter } = useDataTableContext();
1525
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
801
1526
  const rawValue = filters[column];
1527
+ useEffect(() => {
1528
+ registerFilter(column, "date-gte");
1529
+ return () => unregisterFilter(column);
1530
+ }, [
1531
+ column,
1532
+ registerFilter,
1533
+ unregisterFilter
1534
+ ]);
802
1535
  const dateRange = useMemo(() => {
803
1536
  const date = rawValue ? new Date(rawValue) : void 0;
804
1537
  return {
@@ -831,9 +1564,17 @@ function DatePickerFilter({ column, label, className, datePickerPopoverClassName
831
1564
  //#endregion
832
1565
  //#region src/components/features/data-table/filters/select-filter.tsx
833
1566
  function SelectFilter({ column, label, options, placeholder, searchable = true, className, selectPopoverClassName }) {
834
- const { filters, setFilter, clearFilter } = useDataTableContext();
1567
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
835
1568
  const [open, setOpen] = useState(false);
836
1569
  const value = filters[column];
1570
+ useEffect(() => {
1571
+ registerFilter(column, "select");
1572
+ return () => unregisterFilter(column);
1573
+ }, [
1574
+ column,
1575
+ registerFilter,
1576
+ unregisterFilter
1577
+ ]);
837
1578
  const selectedOption = options.find((o) => o.value === value);
838
1579
  return /* @__PURE__ */ jsxs(Popover, {
839
1580
  open,
@@ -841,7 +1582,7 @@ function SelectFilter({ column, label, options, placeholder, searchable = true,
841
1582
  children: [/* @__PURE__ */ jsx(PopoverTrigger, {
842
1583
  asChild: true,
843
1584
  children: /* @__PURE__ */ jsxs(Button, {
844
- variant: "outline",
1585
+ theme: "outline",
845
1586
  role: "combobox",
846
1587
  "aria-expanded": open,
847
1588
  className: cn("justify-between", className),
@@ -892,6 +1633,7 @@ function SelectFilter({ column, label, options, placeholder, searchable = true,
892
1633
  const DataTable = {
893
1634
  Client: ClientProvider,
894
1635
  Server: ServerProvider,
1636
+ ActiveFilters: DataTableActiveFilters,
895
1637
  Content: DataTableContent,
896
1638
  InlineContent: DataTableInlineContent,
897
1639
  ColumnHeader: DataTableColumnHeader,
@@ -906,257 +1648,4 @@ const DataTable = {
906
1648
  };
907
1649
 
908
1650
  //#endregion
909
- //#region src/components/features/data-table/hooks/use-data-table-client.ts
910
- function useDataTableClient(options) {
911
- const { data, columns, pageSize: initialPageSize = DEFAULT_PAGE_SIZE, getRowId, enableRowSelection = false, defaultSort = [], defaultFilters = {}, searchableColumns, searchFn, stateAdapter } = options;
912
- const adapterState = useMemo(() => stateAdapter?.read() ?? {}, []);
913
- const [sorting, setSorting] = useState(adapterState.sorting ?? defaultSort);
914
- const [filters, setFilters] = useState(adapterState.filters ?? defaultFilters);
915
- const [search, setSearch] = useState(adapterState.search ?? "");
916
- const [rowSelection, setRowSelection] = useState({});
917
- const [pageIndex, setPageIndex] = useState(adapterState.pageIndex ?? 0);
918
- const [pageSize, setPageSize] = useState(adapterState.pageSize ?? initialPageSize);
919
- const setFilter = useCallback((key, value) => {
920
- setFilters((prev) => ({
921
- ...prev,
922
- [key]: value
923
- }));
924
- }, []);
925
- const clearFilter = useCallback((key) => {
926
- setFilters((prev) => {
927
- const { [key]: _, ...rest } = prev;
928
- return rest;
929
- });
930
- }, []);
931
- const clearAllFilters = useCallback(() => {
932
- setFilters({});
933
- }, []);
934
- const clearSearch = useCallback(() => {
935
- setSearch("");
936
- }, []);
937
- useEffect(() => {
938
- setRowSelection({});
939
- }, [
940
- sorting,
941
- filters,
942
- search,
943
- pageIndex,
944
- pageSize
945
- ]);
946
- useEffect(() => {
947
- stateAdapter?.write({
948
- sorting,
949
- filters,
950
- search,
951
- pageIndex,
952
- pageSize
953
- });
954
- }, [
955
- stateAdapter,
956
- sorting,
957
- filters,
958
- search,
959
- pageIndex,
960
- pageSize
961
- ]);
962
- const pagination = useMemo(() => ({
963
- pageIndex,
964
- pageSize,
965
- setPageIndex,
966
- setPageSize
967
- }), [pageIndex, pageSize]);
968
- const selectionEnabled = !!enableRowSelection;
969
- const selectionOptions = typeof enableRowSelection === "object" ? enableRowSelection : void 0;
970
- return {
971
- data,
972
- columns: useMemo(() => selectionEnabled ? withSelectionColumn(columns, selectionOptions) : columns, [
973
- selectionEnabled,
974
- columns,
975
- selectionOptions
976
- ]),
977
- sorting,
978
- setSorting,
979
- filters,
980
- setFilter,
981
- clearFilter,
982
- clearAllFilters,
983
- search,
984
- setSearch,
985
- clearSearch,
986
- rowSelection,
987
- setRowSelection,
988
- pagination,
989
- getRowId,
990
- enableRowSelection: selectionEnabled,
991
- searchableColumns,
992
- searchFn
993
- };
994
- }
995
-
996
- //#endregion
997
- //#region src/components/features/data-table/hooks/use-data-table-server.ts
998
- function useDataTableServer(options) {
999
- const { columns, fetchFn, transform, limit: initialLimit = DEFAULT_PAGE_SIZE, getRowId, enableRowSelection = false, defaultSort = [], defaultFilters = {}, stateAdapter } = options;
1000
- const adapterState = useMemo(() => stateAdapter?.read() ?? {}, []);
1001
- const [data, setData] = useState([]);
1002
- const [isLoading, setIsLoading] = useState(true);
1003
- const [sorting, setSorting] = useState(adapterState.sorting ?? defaultSort);
1004
- const [filters, setFilters] = useState(adapterState.filters ?? defaultFilters);
1005
- const [search, setSearch] = useState(adapterState.search ?? "");
1006
- const [rowSelection, setRowSelection] = useState({});
1007
- const [limit, setLimit] = useState(adapterState.limit ?? initialLimit);
1008
- const [cursor, setCursor] = useState(void 0);
1009
- const [nextCursor, setNextCursor] = useState(void 0);
1010
- const [hasNextPage, setHasNextPage] = useState(false);
1011
- const [cursorHistory, setCursorHistory] = useState([]);
1012
- const fetchFnRef = useRef(fetchFn);
1013
- fetchFnRef.current = fetchFn;
1014
- const transformRef = useRef(transform);
1015
- transformRef.current = transform;
1016
- const setFilter = useCallback((key, value) => {
1017
- setFilters((prev) => ({
1018
- ...prev,
1019
- [key]: value
1020
- }));
1021
- }, []);
1022
- const clearFilter = useCallback((key) => {
1023
- setFilters((prev) => {
1024
- const { [key]: _, ...rest } = prev;
1025
- return rest;
1026
- });
1027
- }, []);
1028
- const clearAllFilters = useCallback(() => {
1029
- setFilters({});
1030
- }, []);
1031
- const clearSearch = useCallback(() => {
1032
- setSearch("");
1033
- }, []);
1034
- const resetPagination = useCallback(() => {
1035
- setCursor(void 0);
1036
- setNextCursor(void 0);
1037
- setCursorHistory([]);
1038
- setHasNextPage(false);
1039
- setRowSelection({});
1040
- }, []);
1041
- useEffect(() => {
1042
- resetPagination();
1043
- }, [
1044
- sorting,
1045
- filters,
1046
- search,
1047
- limit,
1048
- resetPagination
1049
- ]);
1050
- useEffect(() => {
1051
- let cancelled = false;
1052
- setIsLoading(true);
1053
- fetchFnRef.current({
1054
- cursor,
1055
- limit,
1056
- sorting,
1057
- filters,
1058
- search
1059
- }).then((response) => {
1060
- if (cancelled) return;
1061
- const result = transformRef.current(response);
1062
- setData(result.data);
1063
- setNextCursor(result.cursor);
1064
- setHasNextPage(result.hasNextPage);
1065
- }).catch(() => {
1066
- if (cancelled) return;
1067
- setData([]);
1068
- setNextCursor(void 0);
1069
- setHasNextPage(false);
1070
- }).finally(() => {
1071
- if (!cancelled) setIsLoading(false);
1072
- });
1073
- return () => {
1074
- cancelled = true;
1075
- };
1076
- }, [
1077
- cursor,
1078
- limit,
1079
- sorting,
1080
- filters,
1081
- search
1082
- ]);
1083
- useEffect(() => {
1084
- stateAdapter?.write({
1085
- sorting,
1086
- filters,
1087
- search,
1088
- limit
1089
- });
1090
- }, [
1091
- stateAdapter,
1092
- sorting,
1093
- filters,
1094
- search,
1095
- limit
1096
- ]);
1097
- const hasPrevPage = cursorHistory.length > 0;
1098
- const nextPage = useCallback(() => {
1099
- if (nextCursor && hasNextPage) {
1100
- setCursorHistory((prev) => [...prev, cursor ?? ""]);
1101
- setCursor(nextCursor);
1102
- setRowSelection({});
1103
- }
1104
- }, [
1105
- nextCursor,
1106
- hasNextPage,
1107
- cursor
1108
- ]);
1109
- const prevPage = useCallback(() => {
1110
- setCursorHistory((prev) => {
1111
- const newHistory = [...prev];
1112
- const previousCursor = newHistory.pop();
1113
- setCursor(previousCursor === "" ? void 0 : previousCursor);
1114
- return newHistory;
1115
- });
1116
- setRowSelection({});
1117
- }, []);
1118
- const pagination = useMemo(() => ({
1119
- cursor,
1120
- limit,
1121
- hasNextPage,
1122
- hasPrevPage,
1123
- nextPage,
1124
- prevPage,
1125
- setLimit
1126
- }), [
1127
- cursor,
1128
- limit,
1129
- hasNextPage,
1130
- hasPrevPage,
1131
- nextPage,
1132
- prevPage
1133
- ]);
1134
- const selectionEnabled = !!enableRowSelection;
1135
- const selectionOptions = typeof enableRowSelection === "object" ? enableRowSelection : void 0;
1136
- return {
1137
- data,
1138
- columns: useMemo(() => selectionEnabled ? withSelectionColumn(columns, selectionOptions) : columns, [
1139
- selectionEnabled,
1140
- columns,
1141
- selectionOptions
1142
- ]),
1143
- isLoading,
1144
- sorting,
1145
- setSorting,
1146
- filters,
1147
- setFilter,
1148
- clearFilter,
1149
- clearAllFilters,
1150
- search,
1151
- setSearch,
1152
- clearSearch,
1153
- rowSelection,
1154
- setRowSelection,
1155
- pagination,
1156
- getRowId,
1157
- enableRowSelection: selectionEnabled
1158
- };
1159
- }
1160
-
1161
- //#endregion
1162
- export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, createSelectionColumn, useDataTableClient, useDataTableContext, useDataTableServer, useNuqsAdapter };
1651
+ export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, createDataTableStore, createSelectionColumn, useDataTableFilters, useDataTableInlineContents, useDataTableLoading, useDataTablePagination, useDataTableRows, useDataTableSearch, useDataTableSelection, useDataTableSorting, useNuqsAdapter };