aplance-ui 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/dist/api.d.ts +2 -1
  3. package/dist/api.js +103 -68
  4. package/dist/api.js.map +1 -1
  5. package/dist/frameworks/vite-react/vite.config.ts.template +1 -1
  6. package/dist/index.js +103 -68
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -6
  9. package/registry/components/primitives/modal/modal.module.css +9 -3
  10. package/registry/components/primitives/popover/popover-context.ts +2 -0
  11. package/registry/components/primitives/popover/popover.tsx +47 -2
  12. package/registry/components/ui/chart/chart.tsx +51 -30
  13. package/registry/components/ui/chart/types.ts +5 -1
  14. package/registry/components/ui/data-table/checkbox-column.tsx +12 -8
  15. package/registry/components/ui/data-table/data-table.module.css +41 -19
  16. package/registry/components/ui/data-table/data-table.tsx +1285 -1238
  17. package/registry/components/ui/data-table/filter-bar.tsx +56 -13
  18. package/registry/components/ui/data-table/filter-modal.tsx +16 -82
  19. package/registry/components/ui/data-table/filter-sheet.tsx +97 -0
  20. package/registry/components/ui/data-table/filters.tsx +21 -21
  21. package/registry/components/ui/data-table/pagination-bar.tsx +4 -6
  22. package/registry/components/ui/data-table/types.ts +149 -80
  23. package/registry/components/ui/data-table/useDataTable.tsx +35 -16
  24. package/registry/components/ui/data-table/utils.ts +691 -471
  25. package/registry/components/ui/input-fields/date-picker/calendar.tsx +7 -15
  26. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +1 -1
  27. package/registry/components/ui/input-fields/date-picker/types.ts +3 -3
  28. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +1 -1
  29. package/registry/registry.json +16 -11
@@ -4,55 +4,99 @@ import type {
4
4
  ColumnOrderState,
5
5
  ColumnPinningState,
6
6
  ColumnSizingState,
7
+ ColumnVisibilityState,
7
8
  ExpandedState,
8
9
  GroupingState,
9
10
  Header,
10
11
  OnChangeFn,
11
- RowData,
12
+ PaginationState,
12
13
  Row,
14
+ RowData,
13
15
  RowSelectionState,
14
16
  SortingState,
15
17
  Table,
16
- TableMeta as TanstackTableMeta,
17
- VisibilityState,
18
18
  } from "@tanstack/react-table";
19
19
  import type { ReactNode } from "react";
20
+ import type { dataTableFeatures } from "./utils";
21
+
22
+ // ─── Features & columns ───────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * The TanStack Table feature set every DataTable registers. Pass it where
26
+ * TanStack asks for a table's features, e.g.
27
+ * `createColumnHelper<DataTableFeatures, Invoice>()`.
28
+ */
29
+ export type DataTableFeatures = typeof dataTableFeatures;
20
30
 
21
- // ─── Column meta augmentation ─────────────────────────────────────────────────
22
- // Attach presentation hints directly to the column definition so consumers
23
- // stay in TanStack's idiomatic API surface (`columns: [{ ..., meta: {...} }]`)
24
- // instead of inventing parallel prop maps on the table.
25
- declare module "@tanstack/react-table" {
26
- /* eslint-disable @typescript-eslint/no-unused-vars -- generics must mirror tanstack's signature exactly */
27
- interface ColumnMeta<TData extends RowData, TValue> {
28
- /** Horizontal alignment of cell + header content. Default: `'left'`. */
29
- align?: "left" | "center" | "right";
30
- /**
31
- * Whether to truncate overflowing text with an ellipsis. Default: `true`.
32
- * Set to `false` for cells that host interactive controls (inputs,
33
- * selects, buttons) so focus rings and dropdowns are not clipped.
34
- */
35
- truncate?: boolean;
36
- /** Extra class merged onto the `<th>` element. */
37
- headerClassName?: string;
38
- /** Extra class merged onto the `<td>` element. */
39
- cellClassName?: string;
40
- }
41
- /* eslint-enable @typescript-eslint/no-unused-vars */
31
+ /**
32
+ * Presentation hints a column carries in `meta`, so consumers stay in
33
+ * TanStack's idiomatic API surface (`columns: [{ ..., meta: {...} }]`)
34
+ * instead of inventing parallel prop maps on the table.
35
+ */
36
+ export interface DataTableColumnMeta {
37
+ /** Horizontal alignment of cell + header content. Default: `'left'`. */
38
+ align?: "left" | "center" | "right";
39
+ /**
40
+ * Whether to truncate overflowing text with an ellipsis. Default: `true`.
41
+ * Set to `false` for cells that host interactive controls (inputs,
42
+ * selects, buttons) so focus rings and dropdowns are not clipped.
43
+ */
44
+ truncate?: boolean;
45
+ /** Extra class merged onto the `<th>` element. */
46
+ headerClassName?: string;
47
+ /** Extra class merged onto the `<td>` element. */
48
+ cellClassName?: string;
42
49
  }
43
50
 
51
+ /**
52
+ * One DataTable column. TanStack's `ColumnDef` is invariant on its value type
53
+ * parameter: `ColumnDef<F, Email, string>` is NOT assignable to
54
+ * `ColumnDef<F, Email, unknown>`, because `footer`/`header` templates place the
55
+ * value param in a contravariant position. `any` is the correct default - it
56
+ * is bivariant (simultaneously supertype and subtype of every type), which is
57
+ * the only param that lets columns of any concrete value type share one
58
+ * array. This matches how TanStack Table itself types its own internal APIs.
59
+ *
60
+ * In consumers: do NOT write `useMemo<DataTableColumnDef<X, unknown>[]>(...)`.
61
+ * Just write `useMemo(() => [...], [deps])` - let TypeScript infer the
62
+ * column types. The inferred union widens correctly at the prop boundary.
63
+ */
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ export type DataTableColumnDef<TData extends RowData, TValue = any> = ColumnDef<
66
+ DataTableFeatures,
67
+ TData,
68
+ TValue
69
+ >;
70
+
44
71
  // ─── Filter fields ────────────────────────────────────────────────────────────
45
72
  // Two headless primitives - nothing else. Every concrete filter (search,
46
73
  // select, date picker, custom slider, etc.) plugs in through the same
47
74
  // `value`/`onChange` render-prop contract, so the data-table has zero
48
75
  // compile-time dependency on any specific input component.
49
76
 
77
+ /**
78
+ * Where a filter sits. `auto`: in the row while it fits, behind the Filters
79
+ * button on a compact screen. `inline`: in the row at every width, as a search
80
+ * people reach for first. `modal`: behind the Filters button at every width,
81
+ * for the filters a row has no room for.
82
+ */
83
+ export type FilterPlacement = "auto" | "inline" | "modal";
84
+
50
85
  /** Any input-like field with a string value. Render anything that fulfils the API. */
51
86
  export interface FilterInputField {
52
87
  type: "field";
53
88
  key: string;
54
89
  /** Grid column width template. Default: `'1fr'` (shares remaining space). */
55
90
  width?: string;
91
+ /** Where it sits. Default: `'auto'`. */
92
+ placement?: FilterPlacement;
93
+ /**
94
+ * What it holds until someone sets it: the control shows it, it does not
95
+ * count as in use, and Clear all returns it here. With `useDataTable`, give
96
+ * the same value in `defaultFilterValues` so the table's state, its URL or
97
+ * storage, and `resetFilters` start from it too. Default: `''`.
98
+ */
99
+ defaultValue?: string;
56
100
  render: (api: {
57
101
  value: string;
58
102
  onChange: (value: string) => void;
@@ -64,6 +108,8 @@ export interface FilterAction {
64
108
  type: "action";
65
109
  /** Grid column width template. Default: `'auto'` (hugs content). */
66
110
  width?: string;
111
+ /** Where it sits. Default: `'auto'`. */
112
+ placement?: FilterPlacement;
67
113
  render: () => ReactNode;
68
114
  }
69
115
 
@@ -75,9 +121,18 @@ export interface FilterBarProps {
75
121
  onChange: (key: string, value: string) => void;
76
122
  }
77
123
 
124
+ export interface FilterSheetProps extends FilterBarProps {
125
+ /**
126
+ * The button shows only its icon, for a row whose own fields, as a search,
127
+ * already say what it is. Default: `false`.
128
+ */
129
+ iconOnly?: boolean;
130
+ }
131
+
78
132
  // ─── Table meta ───────────────────────────────────────────────────────────────
79
133
 
80
- export interface DataTableMeta<TData> extends TanstackTableMeta<TData> {
134
+ /** What a DataTable puts in `table.options.meta`, for cells to read. */
135
+ export interface DataTableMeta {
81
136
  editingRowId: string | null;
82
137
  updateData: (rowId: string, columnId: string, value: unknown) => void;
83
138
  }
@@ -94,7 +149,7 @@ export interface PersistedState {
94
149
  pagination?: PaginationState;
95
150
  sorting?: SortingState;
96
151
  columnOrder?: ColumnOrderState;
97
- columnVisibility?: VisibilityState;
152
+ columnVisibility?: ColumnVisibilityState;
98
153
  columnPinning?: ColumnPinningState;
99
154
  columnSizing?: ColumnSizingState;
100
155
  grouping?: GroupingState;
@@ -143,13 +198,8 @@ export type PersistState =
143
198
 
144
199
  // ─── Pagination ───────────────────────────────────────────────────────────────
145
200
 
146
- export interface PaginationState {
147
- pageIndex: number;
148
- pageSize: number;
149
- }
150
-
151
- export interface PaginationBarProps<TData> {
152
- table: Table<TData>;
201
+ export interface PaginationBarProps<TData extends RowData> {
202
+ table: Table<DataTableFeatures, TData>;
153
203
  pagination: PaginationState;
154
204
  onPaginationChange: (next: PaginationState) => void;
155
205
  pageSizeOptions: number[];
@@ -163,37 +213,24 @@ export interface PaginationBarProps<TData> {
163
213
  extra: ReactNode;
164
214
  }
165
215
 
166
- // ─── Re-exports for column-control state ─────────────────────────────────────
216
+ // ─── Re-exports for table state ───────────────────────────────────────────────
167
217
  // Re-exposed from tanstack so consumers do not need to import two modules.
168
218
  export type {
169
219
  ColumnOrderState,
170
220
  ColumnPinningState,
171
221
  ColumnSizingState,
222
+ ColumnVisibilityState,
172
223
  ExpandedState,
173
224
  GroupingState,
174
- VisibilityState,
225
+ PaginationState,
175
226
  };
176
227
 
177
228
  // ─── Main DataTable props ─────────────────────────────────────────────────────
178
229
 
179
230
  export interface DataTableProps<TData extends { id: string }> {
180
231
  data: TData[];
181
- /**
182
- * TanStack Table's ColumnDef is invariant on its value type parameter.
183
- * `ColumnDef<Email, string>` is NOT assignable to `ColumnDef<Email, unknown>`
184
- * because `footer`/`header` templates place the value param in a contravariant
185
- * position. `any` is the correct solution - it is bivariant (simultaneously
186
- * supertype and subtype of every type), which is the only param that lets
187
- * columns of any concrete value type pass here without errors.
188
- * This matches how TanStack Table itself types its own internal APIs.
189
- *
190
- * In consumers: do NOT write `useMemo<ColumnDef<X, unknown>[]>(...)`.
191
- * Just write `useMemo(() => [...], [deps])` - let TypeScript infer the
192
- * column types. The inferred union widens correctly into `ColumnDef<X, any>`
193
- * at the prop boundary with zero errors.
194
- */
195
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
- columns: ColumnDef<TData, any>[];
232
+ /** Column definitions - see `DataTableColumnDef` for how to type them. */
233
+ columns: DataTableColumnDef<TData>[];
197
234
 
198
235
  isLoading?: boolean;
199
236
  emptyMessage?: string;
@@ -215,7 +252,20 @@ export interface DataTableProps<TData extends { id: string }> {
215
252
  showPagination?: boolean;
216
253
  pagination?: PaginationState;
217
254
  onPaginationChange?: (next: PaginationState) => void;
255
+ /**
256
+ * When true, `data` is one page (from a server, or sliced by you) and the
257
+ * table shows it as is. Set it to false to pass every row: the table then
258
+ * sorts and groups all of them before it pages them. `useDataTable` sets it
259
+ * from `clientSidePagination`. Default: `true`.
260
+ */
261
+ manualPagination?: boolean;
262
+ /**
263
+ * Page count when `manualPagination` is on. Without it, pages are counted
264
+ * from `totalCount`, and with neither the total is unknown: "next" stays
265
+ * open and the page controls are hidden.
266
+ */
218
267
  totalPages?: number;
268
+ /** Total row count, shown in the pagination bar. */
219
269
  totalCount?: number;
220
270
  pageSizeOptions?: number[];
221
271
  showPageSizeSelect?: boolean;
@@ -229,7 +279,7 @@ export interface DataTableProps<TData extends { id: string }> {
229
279
  * all). Returning false disables the per-row checkbox via the standard
230
280
  * `row.getCanSelect()` flow.
231
281
  */
232
- getRowCanSelect?: (row: Row<TData>) => boolean;
282
+ getRowCanSelect?: (row: Row<DataTableFeatures, TData>) => boolean;
233
283
  rowSelection?: RowSelectionState;
234
284
  onRowSelectionChange?: OnChangeFn<RowSelectionState>;
235
285
 
@@ -263,17 +313,23 @@ export interface DataTableProps<TData extends { id: string }> {
263
313
  onColumnSizingChange?: OnChangeFn<ColumnSizingState>;
264
314
 
265
315
  /**
266
- * Sticky column pinning. When enabled, pinned columns stay visible
267
- * during horizontal scroll. Per-column opt-out: `enablePinning: false`.
316
+ * Sticky column pinning to the row's `start` or `end`. When enabled, pinned
317
+ * columns stay visible during horizontal scroll. Per-column opt-out:
318
+ * `enablePinning: false`.
268
319
  */
269
320
  enableColumnPinning?: boolean;
270
321
  columnPinning?: ColumnPinningState;
271
322
  onColumnPinningChange?: OnChangeFn<ColumnPinningState>;
272
- defaultColumnPinning?: ColumnPinningState;
323
+ /**
324
+ * Columns pinned from the start, e.g. `{ end: ['actions'] }`. They also
325
+ * anchor the order: they stay on the outer edges, with pins made later
326
+ * placed inside them.
327
+ */
328
+ defaultColumnPinning?: Partial<ColumnPinningState>;
273
329
 
274
330
  /** Show/hide columns. Pass with a UI to let users toggle visibility. */
275
- columnVisibility?: VisibilityState;
276
- onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
331
+ columnVisibility?: ColumnVisibilityState;
332
+ onColumnVisibilityChange?: OnChangeFn<ColumnVisibilityState>;
277
333
 
278
334
  /**
279
335
  * Row grouping by column id. Renders a collapsible group row per
@@ -328,8 +384,8 @@ export interface HeaderActionButtonProps {
328
384
  children: ReactNode;
329
385
  }
330
386
 
331
- export interface HeaderCellProps<TData> {
332
- header: Header<TData, unknown>;
387
+ export interface HeaderCellProps<TData extends RowData> {
388
+ header: Header<DataTableFeatures, TData, unknown>;
333
389
  extraWidth: number;
334
390
  enableColumnOrdering: boolean;
335
391
  enableColumnResizing: boolean;
@@ -344,7 +400,7 @@ export interface HeaderCellProps<TData> {
344
400
  }
345
401
 
346
402
  export interface BodyRowProps<TData extends { id: string }> {
347
- row: Row<TData>;
403
+ row: Row<DataTableFeatures, TData>;
348
404
  editingRowId: string | null;
349
405
  onRow?: (row: TData) => void;
350
406
  enableGrouping: boolean;
@@ -358,17 +414,25 @@ export interface BodyRowProps<TData extends { id: string }> {
358
414
  onDropRow?: (draggedId: string, targetId: string) => void;
359
415
  }
360
416
 
361
- export interface BodyCellProps<TData> {
362
- cell: Cell<TData, unknown>;
417
+ export interface FooterCellProps<TData extends RowData> {
418
+ header: Header<DataTableFeatures, TData, unknown>;
363
419
  extraWidth: number;
364
- groupSourceCell?: Cell<TData, unknown>;
420
+ }
421
+
422
+ export interface BodyCellProps<TData extends RowData> {
423
+ cell: Cell<DataTableFeatures, TData, unknown>;
424
+ extraWidth: number;
425
+ groupSourceCell?: Cell<DataTableFeatures, TData, unknown>;
365
426
  isGroupAnchorCell: boolean;
366
427
  }
367
428
 
368
429
  // ─── useDataTable hook ────────────────────────────────────────────────────────
369
430
 
370
431
  export interface UseDataTableOptions<TData extends { id: string }> {
371
- /** Full dataset. The hook slices it per page automatically (client-side mode). */
432
+ /**
433
+ * Full dataset. Client-side, the table sorts and groups all of it, then
434
+ * shows one page at a time.
435
+ */
372
436
  data: TData[];
373
437
 
374
438
  /** Initial page size. Default: 10. */
@@ -424,9 +488,9 @@ export interface UseDataTableOptions<TData extends { id: string }> {
424
488
  onDataChange?: (data: TData[]) => void;
425
489
 
426
490
  /**
427
- * If true (default), the hook slices `data` for the current page.
428
- * Set to false when using server-side pagination - you control `data` externally
429
- * and just want the hook to manage state.
491
+ * If true (default), the table pages `data` itself, after sorting and
492
+ * grouping every row. Set to false when using server-side pagination - you
493
+ * control `data` (one page) externally and just want the hook to manage state.
430
494
  */
431
495
  clientSidePagination?: boolean;
432
496
 
@@ -447,9 +511,13 @@ export interface UseDataTableOptions<TData extends { id: string }> {
447
511
  /** Initial drag-drop column order. */
448
512
  defaultColumnOrder?: ColumnOrderState;
449
513
  /** Initial column visibility map. */
450
- defaultColumnVisibility?: VisibilityState;
451
- /** Initial column pinning state. This also acts as the "anchors" for normalization. */
452
- defaultColumnPinning?: ColumnPinningState;
514
+ defaultColumnVisibility?: ColumnVisibilityState;
515
+ /**
516
+ * Initial column pinning, e.g. `{ end: ['actions'] }`. These columns also
517
+ * anchor the order: they stay on the outer edges, with pins made later
518
+ * placed inside them.
519
+ */
520
+ defaultColumnPinning?: Partial<ColumnPinningState>;
453
521
  /** Initial column sizing state. */
454
522
  defaultColumnSizing?: ColumnSizingState;
455
523
  /** Initial grouping state (column ids to group by). */
@@ -472,14 +540,12 @@ export interface UseDataTableState<TData extends { id: string }> {
472
540
  rowSelection: RowSelectionState;
473
541
  editingRowId: string | null;
474
542
  columnOrder: ColumnOrderState;
475
- columnVisibility: VisibilityState;
543
+ columnVisibility: ColumnVisibilityState;
476
544
  columnPinning: ColumnPinningState;
477
545
  columnSizing: ColumnSizingState;
478
546
  grouping: GroupingState;
479
547
  expanded: ExpandedState;
480
- /** Rows in the current page (already sliced when clientSidePagination=true) */
481
- pageData: TData[];
482
- /** All rows after filtering (before paging) */
548
+ /** All rows after filtering (before sorting and paging) */
483
549
  filteredData: TData[];
484
550
  /** IDs of selected rows */
485
551
  selectedIds: string[];
@@ -492,7 +558,10 @@ export interface UseDataTableActions {
492
558
  setFilter: (key: string, value: string) => void;
493
559
  /** Set multiple filter values at once (resets page to 0 automatically) */
494
560
  setFilters: (values: Record<string, string>) => void;
495
- /** Reset all filters to their default values */
561
+ /**
562
+ * Reset all filters to `defaultFilterValues`. Every value that changes is
563
+ * reported through `onFilterChange`, like a single `setFilter`.
564
+ */
496
565
  resetFilters: () => void;
497
566
  /** Go to a specific page index (0-based) */
498
567
  goToPage: (index: number) => void;
@@ -511,7 +580,7 @@ export interface UseDataTableActions {
511
580
  /** Toggle visibility of a single column. */
512
581
  toggleColumnVisibility: (columnId: string) => void;
513
582
  /** Replace the visibility map. */
514
- setColumnVisibility: (next: VisibilityState) => void;
583
+ setColumnVisibility: (next: ColumnVisibilityState) => void;
515
584
  /** Replace the column order. */
516
585
  setColumnOrder: (next: ColumnOrderState) => void;
517
586
  /** Replace the column pinning state. */
@@ -556,8 +625,8 @@ export interface PersistArgs {
556
625
  setSorting: (next: SortingState) => void;
557
626
  columnOrder: ColumnOrderState;
558
627
  setColumnOrder: (next: ColumnOrderState) => void;
559
- columnVisibility: VisibilityState;
560
- setColumnVisibility: (next: VisibilityState) => void;
628
+ columnVisibility: ColumnVisibilityState;
629
+ setColumnVisibility: (next: ColumnVisibilityState) => void;
561
630
  columnPinning: ColumnPinningState;
562
631
  setColumnPinning: (next: ColumnPinningState) => void;
563
632
  columnSizing: ColumnSizingState;
@@ -580,7 +649,7 @@ export interface SnapshotInput {
580
649
  defaultPageSize: number;
581
650
  sorting: SortingState;
582
651
  columnOrder: ColumnOrderState;
583
- columnVisibility: VisibilityState;
652
+ columnVisibility: ColumnVisibilityState;
584
653
  columnPinning: ColumnPinningState;
585
654
  columnSizing: ColumnSizingState;
586
655
  grouping: GroupingState;
@@ -601,7 +670,7 @@ export interface ApplyPersistedTarget {
601
670
  setPagination: (next: PaginationState) => void;
602
671
  setSorting: (next: SortingState) => void;
603
672
  setColumnOrder: (next: ColumnOrderState) => void;
604
- setColumnVisibility: (next: VisibilityState) => void;
673
+ setColumnVisibility: (next: ColumnVisibilityState) => void;
605
674
  setColumnPinning: (next: ColumnPinningState) => void;
606
675
  setColumnSizing: (next: ColumnSizingState) => void;
607
676
  setGrouping: (next: GroupingState) => void;
@@ -3,16 +3,16 @@ import type {
3
3
  ColumnOrderState,
4
4
  ColumnPinningState,
5
5
  ColumnSizingState,
6
+ ColumnVisibilityState,
6
7
  ExpandedState,
7
8
  GroupingState,
8
9
  OnChangeFn,
10
+ PaginationState,
9
11
  RowSelectionState,
10
12
  SortingState,
11
- VisibilityState,
12
13
  } from "@tanstack/react-table";
13
14
  import type {
14
15
  DataTableProps,
15
- PaginationState,
16
16
  PersistArgs,
17
17
  PersistedState,
18
18
  UseDataTableOptions,
@@ -69,9 +69,12 @@ export function useDataTable<TData extends { id: string }>({
69
69
  defaultColumnOrder ?? [],
70
70
  );
71
71
  const [columnVisibility, setColumnVisibilityState] =
72
- useState<VisibilityState>(defaultColumnVisibility ?? {});
72
+ useState<ColumnVisibilityState>(defaultColumnVisibility ?? {});
73
73
  const [columnPinning, setColumnPinningState] = useState<ColumnPinningState>(
74
- defaultColumnPinning ?? {},
74
+ () => ({
75
+ start: defaultColumnPinning?.start ?? [],
76
+ end: defaultColumnPinning?.end ?? [],
77
+ }),
75
78
  );
76
79
  const [columnSizing, setColumnSizingState] = useState<ColumnSizingState>(
77
80
  defaultColumnSizing ?? {},
@@ -93,6 +96,10 @@ export function useDataTable<TData extends { id: string }>({
93
96
  // updater. Firing consumer callbacks inside an updater causes double-fires
94
97
  // under React strict/concurrent rendering (updaters may run twice) which
95
98
  // would trigger duplicate API calls for server-side sort/filter consumers.
99
+ // Each handler also assigns its ref as soon as it computes `next`, so several
100
+ // calls in one event (Clear all resets each filter in turn) build on each
101
+ // other instead of on the last render. The effect below covers state set
102
+ // any other way (hydration from the URL or storage).
96
103
  const sortingRef = useRef(sorting);
97
104
  const rowSelectionRef = useRef(rowSelection);
98
105
  const filterValuesRef = useRef(filterValues);
@@ -140,6 +147,7 @@ export function useDataTable<TData extends { id: string }>({
140
147
  (updater) => {
141
148
  const next =
142
149
  typeof updater === "function" ? updater(sortingRef.current) : updater;
150
+ sortingRef.current = next;
143
151
  setSorting(next);
144
152
  externalOnSortingChange?.(next);
145
153
  },
@@ -149,6 +157,7 @@ export function useDataTable<TData extends { id: string }>({
149
157
  // ── Pagination handler ──────────────────────────────────────────────────────
150
158
  const handlePaginationChange = useCallback(
151
159
  (next: PaginationState) => {
160
+ paginationRef.current = next;
152
161
  setPagination(next);
153
162
  externalOnPaginationChange?.(next);
154
163
  },
@@ -162,6 +171,7 @@ export function useDataTable<TData extends { id: string }>({
162
171
  typeof updater === "function"
163
172
  ? updater(rowSelectionRef.current)
164
173
  : updater;
174
+ rowSelectionRef.current = next;
165
175
  setRowSelection(next);
166
176
  externalOnRowSelectionChange?.(next);
167
177
  },
@@ -176,6 +186,7 @@ export function useDataTable<TData extends { id: string }>({
176
186
  const setFilter = useCallback(
177
187
  (key: string, value: string) => {
178
188
  const next = { ...filterValuesRef.current, [key]: value };
189
+ filterValuesRef.current = next;
179
190
  setFilterValues(next);
180
191
  onFilterChange?.(key, value, next);
181
192
 
@@ -190,6 +201,7 @@ export function useDataTable<TData extends { id: string }>({
190
201
  const setFilters = useCallback(
191
202
  (values: Record<string, string>) => {
192
203
  const next = { ...filterValuesRef.current, ...values };
204
+ filterValuesRef.current = next;
193
205
  setFilterValues(next);
194
206
  for (const [k, v] of Object.entries(values)) {
195
207
  onFilterChange?.(k, v, next);
@@ -204,13 +216,22 @@ export function useDataTable<TData extends { id: string }>({
204
216
  );
205
217
 
206
218
  const resetFilters = useCallback(() => {
207
- setFilterValues(defaultFilterValues);
208
-
219
+ const prev = filterValuesRef.current;
220
+ const next = defaultFilterValues;
221
+ filterValuesRef.current = next;
222
+ setFilterValues(next);
223
+ // Same contract as setFilter: every value that changes is reported, so a
224
+ // server-side consumer refetches after a reset too.
225
+ for (const key of new Set([...Object.keys(prev), ...Object.keys(next)])) {
226
+ const value = next[key] ?? "";
227
+ if ((prev[key] ?? "") !== value) onFilterChange?.(key, value, next);
228
+ }
229
+
209
230
  const p = paginationRef.current;
210
231
  if (p.pageIndex !== 0) {
211
232
  handlePaginationChange({ ...p, pageIndex: 0 });
212
233
  }
213
- }, [defaultFilterValues, handlePaginationChange]);
234
+ }, [defaultFilterValues, onFilterChange, handlePaginationChange]);
214
235
 
215
236
  // ── Inline editing actions ──────────────────────────────────────────────────
216
237
  const startEdit = useCallback((rowId: string) => {
@@ -281,6 +302,7 @@ export function useDataTable<TData extends { id: string }>({
281
302
  );
282
303
 
283
304
  const clearSelection = useCallback(() => {
305
+ rowSelectionRef.current = {};
284
306
  setRowSelection({});
285
307
  }, []);
286
308
 
@@ -342,12 +364,6 @@ export function useDataTable<TData extends { id: string }>({
342
364
  return filterFn(localData, filterValues);
343
365
  }, [localData, filterFn, filterValues]);
344
366
 
345
- const pageData = useMemo<TData[]>(() => {
346
- if (!clientSidePagination) return filteredData;
347
- const { pageIndex, pageSize } = pagination;
348
- return filteredData.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
349
- }, [filteredData, pagination, clientSidePagination]);
350
-
351
367
  const totalCount = externalTotalCount ?? filteredData.length;
352
368
  const totalPages =
353
369
  externalTotalPages ?? Math.ceil(filteredData.length / pagination.pageSize);
@@ -364,7 +380,9 @@ export function useDataTable<TData extends { id: string }>({
364
380
  "columns" | "filters" | "className"
365
381
  > = useMemo(
366
382
  () => ({
367
- data: pageData,
383
+ // Client-side, the table gets every filtered row and sorts, groups and
384
+ // pages them itself; server-side, `data` is already the page.
385
+ data: filteredData,
368
386
  sorting,
369
387
  onSortingChange: handleSortingChange,
370
388
  manualSorting,
@@ -373,6 +391,7 @@ export function useDataTable<TData extends { id: string }>({
373
391
  showPagination: true,
374
392
  pagination,
375
393
  onPaginationChange: handlePaginationChange,
394
+ manualPagination: !clientSidePagination,
376
395
  totalPages,
377
396
  totalCount,
378
397
  rowSelection,
@@ -395,7 +414,7 @@ export function useDataTable<TData extends { id: string }>({
395
414
  onRowOrderChange: reorderRow,
396
415
  }),
397
416
  [
398
- pageData,
417
+ filteredData,
399
418
  sorting,
400
419
  handleSortingChange,
401
420
  manualSorting,
@@ -403,6 +422,7 @@ export function useDataTable<TData extends { id: string }>({
403
422
  setFilter,
404
423
  pagination,
405
424
  handlePaginationChange,
425
+ clientSidePagination,
406
426
  totalPages,
407
427
  totalCount,
408
428
  rowSelection,
@@ -479,7 +499,6 @@ export function useDataTable<TData extends { id: string }>({
479
499
  columnSizing,
480
500
  grouping,
481
501
  expanded,
482
- pageData,
483
502
  filteredData,
484
503
  selectedIds,
485
504
  selectedRows,