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

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 (36) hide show
  1. package/dist/components/features/data-table/adapters/nuqs-adapter.d.ts +13 -12
  2. package/dist/components/features/data-table/adapters/nuqs-adapter.d.ts.map +1 -1
  3. package/dist/components/features/data-table/components/bulk-actions.d.ts.map +1 -1
  4. package/dist/components/features/data-table/components/content.d.ts.map +1 -1
  5. package/dist/components/features/data-table/components/pagination.d.ts.map +1 -1
  6. package/dist/components/features/data-table/core/client-provider.d.ts +10 -2
  7. package/dist/components/features/data-table/core/client-provider.d.ts.map +1 -1
  8. package/dist/components/features/data-table/core/data-table-context.d.ts +8 -3
  9. package/dist/components/features/data-table/core/data-table-context.d.ts.map +1 -1
  10. package/dist/components/features/data-table/core/filter-engine.d.ts +16 -0
  11. package/dist/components/features/data-table/core/filter-engine.d.ts.map +1 -0
  12. package/dist/components/features/data-table/core/server-provider.d.ts +10 -2
  13. package/dist/components/features/data-table/core/server-provider.d.ts.map +1 -1
  14. package/dist/components/features/data-table/core/store.d.ts +3 -0
  15. package/dist/components/features/data-table/core/store.d.ts.map +1 -0
  16. package/dist/components/features/data-table/filters/checkbox-filter.d.ts.map +1 -1
  17. package/dist/components/features/data-table/filters/date-picker-filter.d.ts.map +1 -1
  18. package/dist/components/features/data-table/filters/select-filter.d.ts.map +1 -1
  19. package/dist/components/features/data-table/hooks/index.d.ts +1 -1
  20. package/dist/components/features/data-table/hooks/index.d.ts.map +1 -1
  21. package/dist/components/features/data-table/hooks/use-data-table-client.d.ts +17 -20
  22. package/dist/components/features/data-table/hooks/use-data-table-client.d.ts.map +1 -1
  23. package/dist/components/features/data-table/hooks/use-data-table-context.d.ts +1 -1
  24. package/dist/components/features/data-table/hooks/use-data-table-context.d.ts.map +1 -1
  25. package/dist/components/features/data-table/hooks/use-data-table-server.d.ts +27 -19
  26. package/dist/components/features/data-table/hooks/use-data-table-server.d.ts.map +1 -1
  27. package/dist/components/features/data-table/hooks/use-selectors.d.ts +81 -0
  28. package/dist/components/features/data-table/hooks/use-selectors.d.ts.map +1 -0
  29. package/dist/components/features/data-table/index.d.ts +3 -2
  30. package/dist/components/features/data-table/index.d.ts.map +1 -1
  31. package/dist/components/features/data-table/types.d.ts +47 -0
  32. package/dist/components/features/data-table/types.d.ts.map +1 -1
  33. package/dist/data-table/index.mjs +795 -480
  34. package/package.json +3 -3
  35. package/dist/components/features/data-table/hooks/use-inline-contents.d.ts +0 -7
  36. package/dist/components/features/data-table/hooks/use-inline-contents.d.ts.map +0 -1
@@ -13,10 +13,10 @@ import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as Tab
13
13
  import { t as CalendarDatePicker } from "../calendar-date-picker-BBAg78Lg.mjs";
14
14
  import { cva } from "class-variance-authority";
15
15
  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";
16
+ import { createContext, use, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
17
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";
18
+ import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
19
+ import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
20
20
 
21
21
  //#region src/components/features/data-table/constants.ts
22
22
  const DEFAULT_PAGE_SIZE = 20;
@@ -32,15 +32,32 @@ const DEFAULT_LOADING_ROWS = 5;
32
32
  //#endregion
33
33
  //#region src/components/features/data-table/adapters/nuqs-adapter.ts
34
34
  /**
35
- * Default parsers for core DataTable state.
36
- * URL params: ?sort=[...]&q=hello&page=0&size=20
35
+ * Serialize SortingState to URL-friendly string.
36
+ * Format: "name" (asc), "-name" (desc), comma-separated for multi-sort.
37
+ * Example: "-department,name" → [{id:"department",desc:true},{id:"name",desc:false}]
37
38
  */
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;
39
+ function serializeSorting(sorting) {
40
+ if (sorting.length === 0) return "";
41
+ return sorting.map((s) => s.desc ? `-${s.id}` : s.id).join(",");
42
+ }
43
+ /**
44
+ * Parse URL sort string back to SortingState.
45
+ */
46
+ function parseSorting(value) {
47
+ if (!value) return [];
48
+ return value.split(",").filter(Boolean).map((part) => {
49
+ if (part.startsWith("-")) return {
50
+ id: part.slice(1),
51
+ desc: true
52
+ };
53
+ return {
54
+ id: part,
55
+ desc: false
56
+ };
57
+ });
41
58
  }
42
59
  const coreSearchParams = {
43
- sort: parseAsJson(sortingValidator).withDefault([]),
60
+ sort: parseAsString.withDefault(""),
44
61
  q: parseAsString.withDefault(""),
45
62
  page: parseAsInteger.withDefault(0),
46
63
  size: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE)
@@ -48,22 +65,21 @@ const coreSearchParams = {
48
65
  /**
49
66
  * Hook that creates a StateAdapter backed by nuqs URL query state.
50
67
  *
51
- * Manages sorting, search, pageIndex, and pageSize automatically.
52
- * Consumer only needs to declare custom filter parsers.
68
+ * URL format:
69
+ * - `?sort=name` (asc) or `?sort=-name` (desc), comma-separated for multi-sort
70
+ * - `?q=search` for search text
71
+ * - `?page=0&size=20` for pagination
72
+ * - Custom filter keys as declared in options
53
73
  *
54
74
  * Requires `nuqs` to be installed in the consumer app.
55
75
  *
56
76
  * @example
57
77
  * ```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
78
  * const stateAdapter = useNuqsAdapter({
66
- * filters: { status: parseAsString.withDefault('') },
79
+ * filters: {
80
+ * status: parseAsString.withDefault(''),
81
+ * department: parseAsArrayOf(parseAsString).withDefault([]),
82
+ * },
67
83
  * })
68
84
  * const tableState = useDataTableClient({ data, columns, stateAdapter })
69
85
  * ```
@@ -75,7 +91,7 @@ function useNuqsAdapter(options = {}) {
75
91
  const [filterState, setFilterState] = useQueryStates(hasFilters ? filterParsers : { _dt: parseAsString.withDefault("") });
76
92
  return useMemo(() => ({
77
93
  read: () => ({
78
- sorting: coreState.sort,
94
+ sorting: parseSorting(coreState.sort),
79
95
  search: coreState.q,
80
96
  pageIndex: coreState.page,
81
97
  pageSize: coreState.size,
@@ -83,19 +99,24 @@ function useNuqsAdapter(options = {}) {
83
99
  }),
84
100
  write: (state) => {
85
101
  setCoreState({
86
- sort: state.sorting,
102
+ sort: serializeSorting(state.sorting),
87
103
  q: state.search,
88
104
  page: state.pageIndex ?? 0,
89
105
  size: state.pageSize ?? DEFAULT_PAGE_SIZE
90
106
  });
91
- if (hasFilters) setFilterState(state.filters);
107
+ if (hasFilters && filterParsers) {
108
+ const update = {};
109
+ for (const key of Object.keys(filterParsers)) update[key] = state.filters?.[key] ?? null;
110
+ setFilterState(update);
111
+ }
92
112
  }
93
113
  }), [
94
114
  coreState,
95
115
  filterState,
96
116
  hasFilters,
97
117
  setCoreState,
98
- setFilterState
118
+ setFilterState,
119
+ filterParsers
99
120
  ]);
100
121
  }
101
122
 
@@ -131,22 +152,439 @@ function withSelectionColumn(columns, options = {}) {
131
152
  return [createSelectionColumn(options), ...columns];
132
153
  }
133
154
 
155
+ //#endregion
156
+ //#region src/components/features/data-table/core/filter-engine.ts
157
+ const FILTER_STRATEGIES = {
158
+ "checkbox": (cellValue, filterValue) => {
159
+ if (filterValue == null) return true;
160
+ if (Array.isArray(filterValue) && filterValue.length === 0) return true;
161
+ if (!Array.isArray(filterValue)) return cellValue === filterValue;
162
+ if (Array.isArray(cellValue)) return cellValue.some((v) => filterValue.includes(v));
163
+ return filterValue.includes(cellValue);
164
+ },
165
+ "select": (cellValue, filterValue) => {
166
+ if (filterValue == null || filterValue === "") return true;
167
+ return cellValue === filterValue;
168
+ },
169
+ "date-gte": (cellValue, filterValue) => {
170
+ if (!filterValue) return true;
171
+ const cell = new Date(cellValue);
172
+ const filter = new Date(filterValue);
173
+ if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
174
+ return cell >= filter;
175
+ },
176
+ "date-lte": (cellValue, filterValue) => {
177
+ if (!filterValue) return true;
178
+ const cell = new Date(cellValue);
179
+ const filter = new Date(filterValue);
180
+ if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
181
+ return cell <= filter;
182
+ }
183
+ };
184
+ function resolveStrategy(strategy) {
185
+ if (!strategy) return void 0;
186
+ if (typeof strategy === "function") return strategy;
187
+ return FILTER_STRATEGIES[strategy];
188
+ }
189
+ function applyFilters(data, filters, search, registeredFilters, customFilterFns, searchConfig) {
190
+ const hasFilters = Object.keys(filters).length > 0;
191
+ const hasSearch = search.length > 0;
192
+ if (!hasFilters && !hasSearch) return data;
193
+ return data.filter((row) => {
194
+ if (hasFilters) for (const [column, value] of Object.entries(filters)) {
195
+ const fn = customFilterFns[column] ?? resolveStrategy(registeredFilters.get(column));
196
+ if (!fn) {
197
+ console.warn(`[DataTable] No filter strategy registered for column "${column}". Filter ignored.`);
198
+ continue;
199
+ }
200
+ const cellValue = row[column];
201
+ if (!fn(cellValue, value)) return false;
202
+ }
203
+ if (hasSearch) {
204
+ const query = search.toLowerCase();
205
+ if (searchConfig.searchFn) return searchConfig.searchFn(row, search);
206
+ if (searchConfig.searchableColumns && searchConfig.searchableColumns.length > 0) return searchConfig.searchableColumns.some((col) => {
207
+ const cellValue = row[col];
208
+ return cellValue != null && String(cellValue).toLowerCase().includes(query);
209
+ });
210
+ return Object.values(row).some((val) => {
211
+ if (val == null) return false;
212
+ return String(val).toLowerCase().includes(query);
213
+ });
214
+ }
215
+ return true;
216
+ });
217
+ }
218
+
219
+ //#endregion
220
+ //#region src/components/features/data-table/core/store.ts
221
+ function createDataTableStore(options) {
222
+ const registeredFilters = /* @__PURE__ */ new Map();
223
+ const listeners = /* @__PURE__ */ new Set();
224
+ function computeFilteredData(s) {
225
+ if (s.mode === "server") return s.data;
226
+ return applyFilters(s.data, s.filters, s.search, registeredFilters, options.filterFns ?? {}, {
227
+ searchFn: options.searchFn,
228
+ searchableColumns: options.searchableColumns
229
+ });
230
+ }
231
+ let state = {
232
+ data: options.data,
233
+ filteredData: options.data,
234
+ sorting: options.defaultSort ?? [],
235
+ filters: options.defaultFilters ?? {},
236
+ search: "",
237
+ rowSelection: {},
238
+ pageIndex: 0,
239
+ pageSize: options.pageSize ?? DEFAULT_PAGE_SIZE,
240
+ mode: options.mode,
241
+ isLoading: false,
242
+ error: null,
243
+ inlineContents: []
244
+ };
245
+ if (options.defaultFilters && Object.keys(options.defaultFilters).length > 0) state = {
246
+ ...state,
247
+ filteredData: computeFilteredData(state)
248
+ };
249
+ function notify() {
250
+ for (const listener of listeners) listener();
251
+ }
252
+ function setState(next) {
253
+ state = next;
254
+ notify();
255
+ }
256
+ return {
257
+ getSnapshot: () => state,
258
+ subscribe: (listener) => {
259
+ listeners.add(listener);
260
+ return () => listeners.delete(listener);
261
+ },
262
+ setData: (data) => {
263
+ const next = {
264
+ ...state,
265
+ data,
266
+ pageIndex: 0,
267
+ rowSelection: {}
268
+ };
269
+ setState({
270
+ ...next,
271
+ filteredData: computeFilteredData(next)
272
+ });
273
+ },
274
+ setServerData: (data) => {
275
+ setState({
276
+ ...state,
277
+ data,
278
+ filteredData: data
279
+ });
280
+ },
281
+ setTable: (_t) => {},
282
+ setSorting: (sorting) => {
283
+ setState({
284
+ ...state,
285
+ sorting,
286
+ rowSelection: {}
287
+ });
288
+ },
289
+ setFilter: (key, value) => {
290
+ const next = {
291
+ ...state,
292
+ filters: {
293
+ ...state.filters,
294
+ [key]: value
295
+ },
296
+ rowSelection: {},
297
+ pageIndex: 0
298
+ };
299
+ setState({
300
+ ...next,
301
+ filteredData: computeFilteredData(next)
302
+ });
303
+ },
304
+ clearFilter: (key) => {
305
+ const filters = Object.fromEntries(Object.entries(state.filters).filter(([k]) => k !== key));
306
+ const next = {
307
+ ...state,
308
+ filters,
309
+ rowSelection: {},
310
+ pageIndex: 0
311
+ };
312
+ setState({
313
+ ...next,
314
+ filteredData: computeFilteredData(next)
315
+ });
316
+ },
317
+ clearAllFilters: () => {
318
+ const next = {
319
+ ...state,
320
+ filters: {},
321
+ rowSelection: {},
322
+ pageIndex: 0
323
+ };
324
+ setState({
325
+ ...next,
326
+ filteredData: computeFilteredData(next)
327
+ });
328
+ },
329
+ setSearch: (search) => {
330
+ const next = {
331
+ ...state,
332
+ search,
333
+ rowSelection: {},
334
+ pageIndex: 0
335
+ };
336
+ setState({
337
+ ...next,
338
+ filteredData: computeFilteredData(next)
339
+ });
340
+ },
341
+ clearSearch: () => {
342
+ const next = {
343
+ ...state,
344
+ search: "",
345
+ rowSelection: {},
346
+ pageIndex: 0
347
+ };
348
+ setState({
349
+ ...next,
350
+ filteredData: computeFilteredData(next)
351
+ });
352
+ },
353
+ setRowSelection: (rowSelection) => {
354
+ setState({
355
+ ...state,
356
+ rowSelection
357
+ });
358
+ },
359
+ setPageIndex: (pageIndex) => {
360
+ setState({
361
+ ...state,
362
+ pageIndex,
363
+ rowSelection: {}
364
+ });
365
+ },
366
+ setPageSize: (pageSize) => {
367
+ setState({
368
+ ...state,
369
+ pageSize,
370
+ pageIndex: 0,
371
+ rowSelection: {}
372
+ });
373
+ },
374
+ setLoading: (isLoading) => {
375
+ setState({
376
+ ...state,
377
+ isLoading
378
+ });
379
+ },
380
+ setError: (error) => {
381
+ setState({
382
+ ...state,
383
+ error
384
+ });
385
+ },
386
+ registerFilter: (column, strategy) => {
387
+ registeredFilters.set(column, strategy);
388
+ const filteredData = computeFilteredData(state);
389
+ setState({
390
+ ...state,
391
+ filteredData
392
+ });
393
+ },
394
+ unregisterFilter: (column) => {
395
+ registeredFilters.delete(column);
396
+ if (column in state.filters) {
397
+ const filteredData = computeFilteredData(state);
398
+ setState({
399
+ ...state,
400
+ filteredData
401
+ });
402
+ }
403
+ },
404
+ registerInlineContent: (entry) => {
405
+ const existing = state.inlineContents.findIndex((e) => e.id === entry.id);
406
+ const inlineContents = existing >= 0 ? state.inlineContents.map((e, i) => i === existing ? entry : e) : [...state.inlineContents, entry];
407
+ setState({
408
+ ...state,
409
+ inlineContents
410
+ });
411
+ },
412
+ unregisterInlineContent: (id) => {
413
+ const inlineContents = state.inlineContents.filter((e) => e.id !== id);
414
+ setState({
415
+ ...state,
416
+ inlineContents
417
+ });
418
+ }
419
+ };
420
+ }
421
+
134
422
  //#endregion
135
423
  //#region src/components/features/data-table/core/data-table-context.tsx
424
+ const DataTableStoreContext = createContext(null);
425
+ const TableInstanceContext = createContext(null);
136
426
  const DataTableContext = createContext(null);
427
+ function useDataTableStore() {
428
+ const store = use(DataTableStoreContext);
429
+ if (!store) throw new Error("useDataTableStore must be used within a <DataTable.Client> or <DataTable.Server> provider");
430
+ return store;
431
+ }
432
+ function useTableInstance() {
433
+ const table = use(TableInstanceContext);
434
+ if (!table) throw new Error("useTableInstance must be used within a <DataTable.Client> or <DataTable.Server> provider");
435
+ return table;
436
+ }
437
+
438
+ //#endregion
439
+ //#region src/components/features/data-table/hooks/use-selectors.ts
440
+ function shallowEqual(a, b) {
441
+ const keysA = Object.keys(a);
442
+ const keysB = Object.keys(b);
443
+ if (keysA.length !== keysB.length) return false;
444
+ for (const key of keysA) {
445
+ const va = a[key];
446
+ const vb = b[key];
447
+ if (va === vb) continue;
448
+ if (Array.isArray(va) && Array.isArray(vb)) {
449
+ if (va.length !== vb.length) return false;
450
+ for (let i = 0; i < va.length; i++) if (va[i] !== vb[i]) return false;
451
+ continue;
452
+ }
453
+ return false;
454
+ }
455
+ return true;
456
+ }
457
+ function useSliceSelector(selector) {
458
+ const store = useDataTableStore();
459
+ const cachedRef = useRef(null);
460
+ const getSnapshot = useCallback(() => {
461
+ const next = selector(store.getSnapshot());
462
+ if (cachedRef.current && shallowEqual(cachedRef.current, next)) return cachedRef.current;
463
+ cachedRef.current = next;
464
+ return next;
465
+ }, [store, selector]);
466
+ return useSyncExternalStore(store.subscribe, getSnapshot);
467
+ }
468
+ function useDataTableFilters() {
469
+ const store = useDataTableStore();
470
+ return useSliceSelector(useCallback((state) => ({
471
+ filters: state.filters,
472
+ setFilter: store.setFilter,
473
+ clearFilter: store.clearFilter,
474
+ clearAllFilters: store.clearAllFilters,
475
+ registerFilter: store.registerFilter,
476
+ unregisterFilter: store.unregisterFilter
477
+ }), [store]));
478
+ }
479
+ function useDataTableSearch() {
480
+ const store = useDataTableStore();
481
+ return useSliceSelector(useCallback((state) => ({
482
+ search: state.search,
483
+ setSearch: store.setSearch,
484
+ clearSearch: store.clearSearch
485
+ }), [store]));
486
+ }
487
+ function useDataTableSorting() {
488
+ const store = useDataTableStore();
489
+ return useSliceSelector(useCallback((state) => ({
490
+ sorting: state.sorting,
491
+ setSorting: store.setSorting
492
+ }), [store]));
493
+ }
494
+ function useDataTableSelection() {
495
+ const store = useDataTableStore();
496
+ const table = useTableInstance();
497
+ return useSliceSelector(useCallback((state) => ({
498
+ rowSelection: state.rowSelection,
499
+ setRowSelection: store.setRowSelection,
500
+ selectedRows: table.getFilteredSelectedRowModel().rows.map((r) => r.original)
501
+ }), [store, table]));
502
+ }
503
+ function useDataTablePagination() {
504
+ const store = useDataTableStore();
505
+ const table = useTableInstance();
506
+ const nextPage = useCallback(() => table.nextPage(), [table]);
507
+ const prevPage = useCallback(() => table.previousPage(), [table]);
508
+ return useSliceSelector(useCallback((state) => ({
509
+ canNextPage: table.getCanNextPage(),
510
+ canPrevPage: table.getCanPreviousPage(),
511
+ nextPage,
512
+ prevPage,
513
+ pageIndex: state.pageIndex,
514
+ pageCount: table.getPageCount(),
515
+ setPageIndex: store.setPageIndex,
516
+ pageSize: state.pageSize,
517
+ setPageSize: store.setPageSize,
518
+ totalRows: state.filteredData.length
519
+ }), [
520
+ store,
521
+ table,
522
+ nextPage,
523
+ prevPage
524
+ ]));
525
+ }
526
+ function useDataTableRows() {
527
+ const table = useTableInstance();
528
+ return useSliceSelector(useCallback((_state) => ({
529
+ rows: table.getRowModel().rows,
530
+ headerGroups: table.getHeaderGroups(),
531
+ totalColumns: table.getAllColumns().length
532
+ }), [table]));
533
+ }
534
+ function useDataTableLoading() {
535
+ return useSliceSelector(useCallback((state) => ({
536
+ isLoading: state.isLoading,
537
+ error: state.error
538
+ }), []));
539
+ }
540
+ function useDataTableInlineContents() {
541
+ const store = useDataTableStore();
542
+ return useSliceSelector(useCallback((state) => ({
543
+ inlineContents: state.inlineContents,
544
+ registerInlineContent: store.registerInlineContent,
545
+ unregisterInlineContent: store.unregisterInlineContent
546
+ }), [store]));
547
+ }
137
548
  function useDataTableContext() {
138
- const context = use(DataTableContext);
139
- if (!context) throw new Error("useDataTableContext must be used within a DataTable provider");
140
- return context;
549
+ const store = useDataTableStore();
550
+ const table = useTableInstance();
551
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
552
+ return {
553
+ table,
554
+ mode: state.mode,
555
+ sorting: state.sorting,
556
+ filters: state.filters,
557
+ search: state.search,
558
+ rowSelection: state.rowSelection,
559
+ isLoading: state.isLoading,
560
+ setSorting: store.setSorting,
561
+ setFilter: store.setFilter,
562
+ clearFilter: store.clearFilter,
563
+ clearAllFilters: store.clearAllFilters,
564
+ setSearch: store.setSearch,
565
+ clearSearch: store.clearSearch,
566
+ setRowSelection: store.setRowSelection,
567
+ pagination: {
568
+ canNextPage: table.getCanNextPage(),
569
+ canPrevPage: table.getCanPreviousPage(),
570
+ nextPage: () => table.nextPage(),
571
+ prevPage: () => table.previousPage(),
572
+ pageIndex: state.pageIndex,
573
+ pageCount: table.getPageCount(),
574
+ setPageIndex: store.setPageIndex,
575
+ pageSize: state.pageSize,
576
+ setPageSize: store.setPageSize
577
+ },
578
+ inlineContents: state.inlineContents,
579
+ registerInlineContent: store.registerInlineContent,
580
+ unregisterInlineContent: store.unregisterInlineContent
581
+ };
141
582
  }
142
583
 
143
584
  //#endregion
144
585
  //#region src/components/features/data-table/components/bulk-actions.tsx
145
586
  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]);
587
+ const { selectedRows } = useDataTableSelection();
150
588
  if (selectedRows.length === 0) return null;
151
589
  return /* @__PURE__ */ jsx("div", {
152
590
  "data-slot": "dt-bulk-actions",
@@ -170,7 +608,7 @@ function DataTableColumnHeader({ column, title, className }) {
170
608
  children: /* @__PURE__ */ jsxs("button", {
171
609
  type: "button",
172
610
  className: "flex items-center gap-1 hover:text-foreground -ml-3 h-8 px-3 cursor-pointer",
173
- onClick: () => column.toggleSorting(sorted === "asc"),
611
+ onClick: column.getToggleSortingHandler(),
174
612
  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
613
  })
176
614
  });
@@ -197,10 +635,10 @@ function renderInlineContentRow(entry, colSpan, rows) {
197
635
  }, entry.id);
198
636
  }
199
637
  function DataTableContent({ emptyMessage, className, tableClassName, headerClassName, headerRowClassName, headerCellClassName, bodyClassName, rowClassName, cellClassName }) {
200
- const { table, inlineContents } = useDataTableContext();
201
- const rows = table.getRowModel().rows;
638
+ const { rows, headerGroups, totalColumns } = useDataTableRows();
639
+ const { inlineContents } = useDataTableInlineContents();
202
640
  const openInlineContents = useMemo(() => inlineContents.filter((e) => e.open), [inlineContents]);
203
- const colSpan = table.getAllColumns().length;
641
+ const colSpan = totalColumns;
204
642
  return /* @__PURE__ */ jsx("div", {
205
643
  className: cn("datum-ui-data-table", className),
206
644
  "data-slot": "dt",
@@ -211,7 +649,7 @@ function DataTableContent({ emptyMessage, className, tableClassName, headerClass
211
649
  children: [/* @__PURE__ */ jsx(TableHeader, {
212
650
  className: cn(headerClassName),
213
651
  "data-slot": "dt-header",
214
- children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
652
+ children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
215
653
  className: cn(headerRowClassName),
216
654
  "data-slot": "dt-header-row",
217
655
  children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(TableHead, {
@@ -228,6 +666,7 @@ function DataTableContent({ emptyMessage, className, tableClassName, headerClass
228
666
  if (rowEntry) return renderInlineContentRow(rowEntry, colSpan, rows);
229
667
  return /* @__PURE__ */ jsx(TableRow, {
230
668
  className: cn(resolveClassName(rowClassName, row)),
669
+ style: { transitionProperty: "none" },
231
670
  "data-slot": "dt-row",
232
671
  "data-state": row.getIsSelected() ? "selected" : void 0,
233
672
  children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
@@ -254,7 +693,7 @@ function DataTableContent({ emptyMessage, className, tableClassName, headerClass
254
693
  //#region src/components/features/data-table/components/inline-content.tsx
255
694
  function DataTableInlineContent({ position, rowId, open, onClose, className, children }) {
256
695
  const id = useId();
257
- const { registerInlineContent, unregisterInlineContent } = useDataTableContext();
696
+ const { registerInlineContent, unregisterInlineContent } = useDataTableInlineContents();
258
697
  const initialRender = useRef(true);
259
698
  useEffect(() => {
260
699
  registerInlineContent({
@@ -325,57 +764,116 @@ function DataTableLoading({ rows = DEFAULT_LOADING_ROWS, columns = 4, className
325
764
 
326
765
  //#endregion
327
766
  //#region src/components/features/data-table/components/pagination.tsx
767
+ /**
768
+ * Generates page numbers with ellipsis for large page counts.
769
+ * Shows up to 7 items: first, last, current +/- 1 neighbor, and ellipsis gaps.
770
+ */
771
+ function getPageNumbers(currentPage, totalPages) {
772
+ if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
773
+ const pages = [1];
774
+ const current = currentPage + 1;
775
+ if (current <= 4) {
776
+ for (let i = 2; i <= 5; i++) pages.push(i);
777
+ pages.push("...");
778
+ pages.push(totalPages);
779
+ } else if (current >= totalPages - 3) {
780
+ pages.push("...");
781
+ for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
782
+ } else {
783
+ pages.push("...");
784
+ for (let i = current - 1; i <= current + 1; i++) pages.push(i);
785
+ pages.push("...");
786
+ pages.push(totalPages);
787
+ }
788
+ return pages;
789
+ }
328
790
  function DataTablePagination({ pageSizes = DEFAULT_PAGE_SIZES, className }) {
329
- const { pagination, mode } = useDataTableContext();
791
+ const { canNextPage, canPrevPage, nextPage, prevPage, pageIndex, pageCount, setPageIndex, pageSize, setPageSize, totalRows } = useDataTablePagination();
792
+ const isClientMode = pageCount > 0;
793
+ const startRow = pageIndex * pageSize + 1;
794
+ const endRow = Math.min((pageIndex + 1) * pageSize, totalRows);
795
+ const pageNumbers = useMemo(() => getPageNumbers(pageIndex, pageCount), [pageIndex, pageCount]);
330
796
  return /* @__PURE__ */ jsxs("div", {
331
- className: cn("flex items-center justify-between px-2 py-4", className),
797
+ className: cn("flex flex-col-reverse items-center justify-between gap-4 px-2 py-4 sm:flex-row", className),
332
798
  "data-slot": "dt-pagination",
333
799
  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))
800
+ className: "flex items-center gap-4",
801
+ children: [isClientMode && totalRows > 0 && /* @__PURE__ */ jsxs("span", {
802
+ className: "text-sm text-muted-foreground whitespace-nowrap",
803
+ children: [
804
+ "Showing",
805
+ " ",
806
+ startRow,
807
+ " ",
808
+ "to",
809
+ " ",
810
+ endRow,
811
+ " ",
812
+ "of",
813
+ " ",
814
+ totalRows,
815
+ " ",
816
+ "rows"
817
+ ]
818
+ }), /* @__PURE__ */ jsxs("div", {
819
+ className: "flex items-center gap-2",
820
+ children: [/* @__PURE__ */ jsx("span", {
821
+ className: "text-sm text-muted-foreground whitespace-nowrap",
822
+ children: "Rows per page"
823
+ }), /* @__PURE__ */ jsxs(Select, {
824
+ value: String(pageSize),
825
+ onValueChange: (value) => setPageSize(Number(value)),
826
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
827
+ className: "h-8 w-[70px]",
828
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: String(pageSize) })
829
+ }), /* @__PURE__ */ jsx(SelectContent, {
830
+ side: "top",
831
+ children: pageSizes.map((size) => /* @__PURE__ */ jsx(SelectItem, {
832
+ value: String(size),
833
+ children: size
834
+ }, size))
835
+ })]
350
836
  })]
351
837
  })]
352
838
  }), /* @__PURE__ */ jsxs("div", {
353
- className: "flex items-center gap-2",
839
+ className: "flex items-center gap-1",
354
840
  children: [
355
- mode === "client" && pagination.pageIndex !== void 0 && pagination.pageCount !== void 0 && /* @__PURE__ */ jsxs("span", {
356
- className: "text-sm text-muted-foreground",
841
+ /* @__PURE__ */ jsx(Button, {
842
+ variant: "outline",
843
+ size: "icon",
844
+ className: "size-8",
845
+ onClick: prevPage,
846
+ disabled: !canPrevPage,
847
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
848
+ }),
849
+ isClientMode && pageCount > 1 ? pageNumbers.map((page, index) => {
850
+ if (page === "...") return /* @__PURE__ */ jsx("span", {
851
+ className: "px-2 text-sm text-muted-foreground",
852
+ children: "..."
853
+ }, `ellipsis-${index}`);
854
+ const isActive = page === pageIndex + 1;
855
+ return /* @__PURE__ */ jsx(Button, {
856
+ variant: isActive ? "default" : "outline",
857
+ size: "sm",
858
+ className: cn("h-8 min-w-8 px-2", isActive && "font-semibold"),
859
+ onClick: () => setPageIndex(page - 1),
860
+ disabled: isActive,
861
+ children: page
862
+ }, page);
863
+ }) : !isClientMode && /* @__PURE__ */ jsxs("span", {
864
+ className: "px-2 text-sm text-muted-foreground",
357
865
  children: [
358
866
  "Page",
359
867
  " ",
360
- pagination.pageIndex + 1,
361
- " ",
362
- "of",
363
- " ",
364
- pagination.pageCount
868
+ pageIndex + 1
365
869
  ]
366
870
  }),
367
871
  /* @__PURE__ */ jsx(Button, {
368
872
  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,
873
+ size: "icon",
874
+ className: "size-8",
875
+ onClick: nextPage,
876
+ disabled: !canNextPage,
379
877
  children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
380
878
  })
381
879
  ]
@@ -421,7 +919,7 @@ function DataTableRowActions({ row, actions, isLoading = false, className }) {
421
919
  //#endregion
422
920
  //#region src/components/features/data-table/components/search.tsx
423
921
  function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOUNCE_MS, className }) {
424
- const { search, setSearch } = useDataTableContext();
922
+ const { search, setSearch } = useDataTableSearch();
425
923
  const [inputValue, setInputValue] = useState(search);
426
924
  useEffect(() => {
427
925
  setInputValue(search);
@@ -446,225 +944,32 @@ function DataTableSearch({ placeholder = "Search...", debounceMs = DEFAULT_DEBOU
446
944
  });
447
945
  }
448
946
 
449
- //#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
947
  //#endregion
473
948
  //#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);
486
- }
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]);
500
- const table = useReactTable({
501
- data,
502
- columns,
503
- state: {
504
- sorting,
505
- rowSelection,
506
- globalFilter: search,
507
- columnFilters,
508
- pagination: {
509
- pageIndex: pagination.pageIndex,
510
- pageSize: pagination.pageSize
511
- }
512
- },
513
- filterFns: { arrayIncludes: arrayIncludesFilter },
514
- defaultColumn: { filterFn: arrayIncludesFilter },
515
- ...globalFilterFn ? { globalFilterFn } : {},
516
- onSortingChange: (updater) => {
517
- setSorting(typeof updater === "function" ? updater(sorting) : updater);
518
- },
519
- onRowSelectionChange: (updater) => {
520
- setRowSelection(typeof updater === "function" ? updater(rowSelection) : updater);
521
- },
522
- onPaginationChange: (updater) => {
523
- const next = typeof updater === "function" ? updater({
524
- pageIndex: pagination.pageIndex,
525
- pageSize: pagination.pageSize
526
- }) : updater;
527
- pagination.setPageIndex(next.pageIndex);
528
- pagination.setPageSize(next.pageSize);
529
- },
530
- getCoreRowModel: getCoreRowModel(),
531
- getSortedRowModel: getSortedRowModel(),
532
- getFilteredRowModel: getFilteredRowModel(),
533
- getPaginationRowModel: getPaginationRowModel(),
534
- getRowId,
535
- enableRowSelection: !!enableRowSelection
536
- });
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,
571
- sorting,
572
- filters,
573
- 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
949
+ function ClientProvider({ store, table, className, children }) {
950
+ return /* @__PURE__ */ jsx(DataTableStoreContext, {
951
+ value: store,
952
+ children: /* @__PURE__ */ jsx(TableInstanceContext, {
953
+ value: table,
954
+ children: /* @__PURE__ */ jsx("div", {
955
+ className,
956
+ children
957
+ })
590
958
  })
591
959
  });
592
960
  }
593
961
 
594
962
  //#endregion
595
963
  //#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 }) {
597
- const table = useReactTable({
598
- data,
599
- columns,
600
- state: {
601
- sorting,
602
- rowSelection
603
- },
604
- onSortingChange: (updater) => {
605
- setSorting(typeof updater === "function" ? updater(sorting) : updater);
606
- },
607
- onRowSelectionChange: (updater) => {
608
- setRowSelection(typeof updater === "function" ? updater(rowSelection) : updater);
609
- },
610
- getCoreRowModel: getCoreRowModel(),
611
- getRowId,
612
- enableRowSelection: !!enableRowSelection,
613
- manualPagination: true,
614
- manualSorting: true,
615
- manualFiltering: true
616
- });
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,
648
- sorting,
649
- filters,
650
- 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
964
+ function ServerProvider({ store, table, className, children }) {
965
+ return /* @__PURE__ */ jsx(DataTableStoreContext, {
966
+ value: store,
967
+ children: /* @__PURE__ */ jsx(TableInstanceContext, {
968
+ value: table,
969
+ children: /* @__PURE__ */ jsx("div", {
970
+ className,
971
+ children
972
+ })
668
973
  })
669
974
  });
670
975
  }
@@ -696,8 +1001,16 @@ function Badge({ className, variant, ...props }) {
696
1001
  //#region src/components/features/data-table/filters/checkbox-filter.tsx
697
1002
  const MAX_VISIBLE_BADGES = 2;
698
1003
  function CheckboxFilter({ column, label, options, className, checkboxPopoverClassName }) {
699
- const { filters, setFilter, clearFilter } = useDataTableContext();
1004
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
700
1005
  const [open, setOpen] = useState(false);
1006
+ useEffect(() => {
1007
+ registerFilter(column, "checkbox");
1008
+ return () => unregisterFilter(column);
1009
+ }, [
1010
+ column,
1011
+ registerFilter,
1012
+ unregisterFilter
1013
+ ]);
701
1014
  const selectedValues = filters[column] ?? [];
702
1015
  const updateValues = (newValues) => {
703
1016
  if (newValues.length > 0) setFilter(column, newValues);
@@ -797,8 +1110,16 @@ function CheckboxFilter({ column, label, options, className, checkboxPopoverClas
797
1110
  //#endregion
798
1111
  //#region src/components/features/data-table/filters/date-picker-filter.tsx
799
1112
  function DatePickerFilter({ column, label, className, datePickerPopoverClassName, disableFuture, disablePast, minDate, maxDate }) {
800
- const { filters, setFilter, clearFilter } = useDataTableContext();
1113
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
801
1114
  const rawValue = filters[column];
1115
+ useEffect(() => {
1116
+ registerFilter(column, "date-gte");
1117
+ return () => unregisterFilter(column);
1118
+ }, [
1119
+ column,
1120
+ registerFilter,
1121
+ unregisterFilter
1122
+ ]);
802
1123
  const dateRange = useMemo(() => {
803
1124
  const date = rawValue ? new Date(rawValue) : void 0;
804
1125
  return {
@@ -831,9 +1152,17 @@ function DatePickerFilter({ column, label, className, datePickerPopoverClassName
831
1152
  //#endregion
832
1153
  //#region src/components/features/data-table/filters/select-filter.tsx
833
1154
  function SelectFilter({ column, label, options, placeholder, searchable = true, className, selectPopoverClassName }) {
834
- const { filters, setFilter, clearFilter } = useDataTableContext();
1155
+ const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
835
1156
  const [open, setOpen] = useState(false);
836
1157
  const value = filters[column];
1158
+ useEffect(() => {
1159
+ registerFilter(column, "select");
1160
+ return () => unregisterFilter(column);
1161
+ }, [
1162
+ column,
1163
+ registerFilter,
1164
+ unregisterFilter
1165
+ ]);
837
1166
  const selectedOption = options.find((o) => o.value === value);
838
1167
  return /* @__PURE__ */ jsxs(Popover, {
839
1168
  open,
@@ -908,255 +1237,241 @@ const DataTable = {
908
1237
  //#endregion
909
1238
  //#region src/components/features/data-table/hooks/use-data-table-client.ts
910
1239
  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
- }, []);
1240
+ const { data, columns, pageSize, getRowId, enableRowSelection = false, defaultSort, defaultFilters, searchableColumns, searchFn, filterFns, stateAdapter } = options;
1241
+ const store = useMemo(() => createDataTableStore({
1242
+ data,
1243
+ mode: "client",
1244
+ defaultSort,
1245
+ defaultFilters,
1246
+ pageSize,
1247
+ searchableColumns,
1248
+ searchFn,
1249
+ filterFns
1250
+ }), []);
1251
+ const isInitialRender = useRef(true);
937
1252
  useEffect(() => {
938
- setRowSelection({});
939
- }, [
940
- sorting,
941
- filters,
942
- search,
943
- pageIndex,
944
- pageSize
945
- ]);
1253
+ if (isInitialRender.current) {
1254
+ isInitialRender.current = false;
1255
+ return;
1256
+ }
1257
+ store.setData(data);
1258
+ }, [data, store]);
1259
+ const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1260
+ const { filteredData, sorting, rowSelection, pageIndex, pageSize: storePageSize, filters, search } = useSyncExternalStore(store.subscribe, store.getSnapshot);
1261
+ const table = useReactTable({
1262
+ data: filteredData,
1263
+ columns: resolvedColumns,
1264
+ state: {
1265
+ sorting,
1266
+ rowSelection,
1267
+ pagination: {
1268
+ pageIndex,
1269
+ pageSize: storePageSize
1270
+ }
1271
+ },
1272
+ onSortingChange: (updater) => {
1273
+ const next = typeof updater === "function" ? updater(sorting) : updater;
1274
+ store.setSorting(next);
1275
+ },
1276
+ onRowSelectionChange: (updater) => {
1277
+ const next = typeof updater === "function" ? updater(rowSelection) : updater;
1278
+ store.setRowSelection(next);
1279
+ },
1280
+ onPaginationChange: (updater) => {
1281
+ const next = typeof updater === "function" ? updater({
1282
+ pageIndex,
1283
+ pageSize: storePageSize
1284
+ }) : updater;
1285
+ store.setPageIndex(next.pageIndex);
1286
+ store.setPageSize(next.pageSize);
1287
+ },
1288
+ getCoreRowModel: getCoreRowModel(),
1289
+ getSortedRowModel: getSortedRowModel(),
1290
+ getPaginationRowModel: getPaginationRowModel(),
1291
+ getRowId,
1292
+ enableRowSelection: !!enableRowSelection
1293
+ });
1294
+ const hydratedRef = useRef(false);
1295
+ useEffect(() => {
1296
+ if (stateAdapter && !hydratedRef.current) {
1297
+ hydratedRef.current = true;
1298
+ const persisted = stateAdapter.read();
1299
+ if (persisted.sorting && persisted.sorting.length > 0) store.setSorting(persisted.sorting);
1300
+ if (persisted.filters) {
1301
+ for (const [key, value] of Object.entries(persisted.filters)) if (value != null) store.setFilter(key, value);
1302
+ }
1303
+ if (persisted.search) store.setSearch(persisted.search);
1304
+ if (persisted.pageIndex != null && persisted.pageIndex > 0) store.setPageIndex(persisted.pageIndex);
1305
+ if (persisted.pageSize != null) store.setPageSize(persisted.pageSize);
1306
+ }
1307
+ }, []);
1308
+ const isFirstWrite = useRef(true);
946
1309
  useEffect(() => {
947
- stateAdapter?.write({
1310
+ if (!stateAdapter) return;
1311
+ if (isFirstWrite.current) {
1312
+ isFirstWrite.current = false;
1313
+ return;
1314
+ }
1315
+ stateAdapter.write({
948
1316
  sorting,
949
1317
  filters,
950
1318
  search,
951
1319
  pageIndex,
952
- pageSize
1320
+ pageSize: storePageSize
953
1321
  });
954
1322
  }, [
955
- stateAdapter,
956
1323
  sorting,
957
1324
  filters,
958
1325
  search,
959
1326
  pageIndex,
960
- pageSize
1327
+ storePageSize,
1328
+ stateAdapter
961
1329
  ]);
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
1330
  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
1331
+ store,
1332
+ table
993
1333
  };
994
1334
  }
995
1335
 
996
1336
  //#endregion
997
1337
  //#region src/components/features/data-table/hooks/use-data-table-server.ts
998
1338
  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;
1339
+ const { columns, fetchFn, transform, limit = 20, getRowId, enableRowSelection = false, defaultSort, defaultFilters, stateAdapter } = options;
1340
+ const fetchRef = useRef(fetchFn);
1014
1341
  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
1342
  useEffect(() => {
1042
- resetPagination();
1043
- }, [
1044
- sorting,
1045
- filters,
1046
- search,
1047
- limit,
1048
- resetPagination
1049
- ]);
1343
+ fetchRef.current = fetchFn;
1344
+ }, [fetchFn]);
1345
+ useEffect(() => {
1346
+ transformRef.current = transform;
1347
+ }, [transform]);
1348
+ const cursorMapRef = useRef(/* @__PURE__ */ new Map());
1349
+ const [hasNextPage, setHasNextPage] = useState(false);
1350
+ const store = useMemo(() => createDataTableStore({
1351
+ data: [],
1352
+ mode: "server",
1353
+ defaultSort,
1354
+ defaultFilters,
1355
+ pageSize: limit
1356
+ }), []);
1357
+ const { sorting, filters, search, rowSelection, pageSize, pageIndex } = useSyncExternalStore(store.subscribe, store.getSnapshot);
1050
1358
  useEffect(() => {
1051
1359
  let cancelled = false;
1052
- setIsLoading(true);
1053
- fetchFnRef.current({
1054
- cursor,
1055
- limit,
1360
+ store.setLoading(true);
1361
+ const cursor = cursorMapRef.current.get(pageIndex);
1362
+ fetchRef.current({
1056
1363
  sorting,
1057
1364
  filters,
1058
- search
1365
+ search,
1366
+ cursor,
1367
+ limit: pageSize
1059
1368
  }).then((response) => {
1060
1369
  if (cancelled) return;
1061
1370
  const result = transformRef.current(response);
1062
- setData(result.data);
1063
- setNextCursor(result.cursor);
1371
+ store.setServerData(result.data);
1372
+ store.setError(null);
1373
+ if (result.nextCursor) cursorMapRef.current.set(pageIndex + 1, result.nextCursor);
1064
1374
  setHasNextPage(result.hasNextPage);
1065
- }).catch(() => {
1375
+ }).catch((error) => {
1066
1376
  if (cancelled) return;
1067
- setData([]);
1068
- setNextCursor(void 0);
1377
+ store.setServerData([]);
1378
+ store.setError(error instanceof Error ? error : new Error(String(error)));
1069
1379
  setHasNextPage(false);
1070
1380
  }).finally(() => {
1071
- if (!cancelled) setIsLoading(false);
1381
+ if (!cancelled) store.setLoading(false);
1072
1382
  });
1073
1383
  return () => {
1074
1384
  cancelled = true;
1075
1385
  };
1076
1386
  }, [
1077
- cursor,
1078
- limit,
1079
1387
  sorting,
1080
1388
  filters,
1081
- search
1389
+ search,
1390
+ pageSize,
1391
+ pageIndex,
1392
+ store
1082
1393
  ]);
1394
+ const prevQueryRef = useRef({
1395
+ sorting,
1396
+ filters,
1397
+ search,
1398
+ pageSize
1399
+ });
1083
1400
  useEffect(() => {
1084
- stateAdapter?.write({
1401
+ const prev = prevQueryRef.current;
1402
+ if (prev.sorting !== sorting || prev.filters !== filters || prev.search !== search || prev.pageSize !== pageSize) {
1403
+ cursorMapRef.current = /* @__PURE__ */ new Map();
1404
+ store.setPageIndex(0);
1405
+ setHasNextPage(false);
1406
+ }
1407
+ prevQueryRef.current = {
1085
1408
  sorting,
1086
1409
  filters,
1087
1410
  search,
1088
- limit
1089
- });
1411
+ pageSize
1412
+ };
1090
1413
  }, [
1091
- stateAdapter,
1092
1414
  sorting,
1093
1415
  filters,
1094
1416
  search,
1095
- limit
1417
+ pageSize,
1418
+ store
1096
1419
  ]);
1097
- const hasPrevPage = cursorHistory.length > 0;
1098
- const nextPage = useCallback(() => {
1099
- if (nextCursor && hasNextPage) {
1100
- setCursorHistory((prev) => [...prev, cursor ?? ""]);
1101
- setCursor(nextCursor);
1102
- setRowSelection({});
1420
+ const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
1421
+ const table = useReactTable({
1422
+ data: store.getSnapshot().data,
1423
+ columns: resolvedColumns,
1424
+ state: {
1425
+ sorting,
1426
+ rowSelection,
1427
+ pagination: {
1428
+ pageIndex,
1429
+ pageSize
1430
+ }
1431
+ },
1432
+ manualPagination: true,
1433
+ manualSorting: true,
1434
+ manualFiltering: true,
1435
+ pageCount: hasNextPage ? pageIndex + 2 : pageIndex + 1,
1436
+ getCoreRowModel: getCoreRowModel(),
1437
+ getRowId,
1438
+ enableRowSelection: !!enableRowSelection,
1439
+ onSortingChange: (updater) => {
1440
+ const next = typeof updater === "function" ? updater(sorting) : updater;
1441
+ store.setSorting(next);
1442
+ },
1443
+ onRowSelectionChange: (updater) => {
1444
+ const next = typeof updater === "function" ? updater(rowSelection) : updater;
1445
+ store.setRowSelection(next);
1446
+ },
1447
+ onPaginationChange: (updater) => {
1448
+ const next = typeof updater === "function" ? updater({
1449
+ pageIndex,
1450
+ pageSize
1451
+ }) : updater;
1452
+ if (next.pageIndex !== pageIndex) store.setPageIndex(next.pageIndex);
1453
+ if (next.pageSize !== pageSize) store.setPageSize(next.pageSize);
1103
1454
  }
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;
1455
+ });
1456
+ useEffect(() => {
1457
+ if (stateAdapter) stateAdapter.write({
1458
+ sorting,
1459
+ filters,
1460
+ search,
1461
+ pageSize
1115
1462
  });
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,
1463
+ }, [
1144
1464
  sorting,
1145
- setSorting,
1146
1465
  filters,
1147
- setFilter,
1148
- clearFilter,
1149
- clearAllFilters,
1150
1466
  search,
1151
- setSearch,
1152
- clearSearch,
1153
- rowSelection,
1154
- setRowSelection,
1155
- pagination,
1156
- getRowId,
1157
- enableRowSelection: selectionEnabled
1467
+ pageSize,
1468
+ stateAdapter
1469
+ ]);
1470
+ return {
1471
+ store,
1472
+ table
1158
1473
  };
1159
1474
  }
1160
1475
 
1161
1476
  //#endregion
1162
- export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, createSelectionColumn, useDataTableClient, useDataTableContext, useDataTableServer, useNuqsAdapter };
1477
+ export { DEFAULT_DEBOUNCE_MS, DEFAULT_LOADING_ROWS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, DataTable, createDataTableStore, createSelectionColumn, useDataTableClient, useDataTableContext, useDataTableFilters, useDataTableInlineContents, useDataTableLoading, useDataTablePagination, useDataTableRows, useDataTableSearch, useDataTableSelection, useDataTableServer, useDataTableSorting, useNuqsAdapter };