@bmsuisse/datagrid 0.23.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.
@@ -0,0 +1,1027 @@
1
+ import { ReactNode, ReactElement, ComponentType } from 'react';
2
+ import { ColumnSizingState, RowData } from '@tanstack/react-table';
3
+ export { ColumnSizingState } from '@tanstack/react-table';
4
+
5
+ /**
6
+ * Shared filter/sort contract for @bmsuisse/datagrid.
7
+ *
8
+ * This shape is intentionally mirrored byte-for-byte (field names and operator
9
+ * set) by the Python package `bmsdna.datagrid` (see `bmsdna/datagrid/filters.py`)
10
+ * so that a `GridState` produced by the grid in "server" mode can be serialized
11
+ * as JSON and handed directly to a FastAPI endpoint that turns it into SQL or a
12
+ * Meilisearch filter string.
13
+ */
14
+ /** The full set of operators supported by any FilterDescriptor. */
15
+ type FilterOperator = "eq" | "neq" | "lt" | "lte" | "gt" | "gte" | "in" | "notIn" | "contains" | "doesNotContain" | "startsWith" | "endsWith" | "isEmpty" | "isNotEmpty" | "isNull" | "isNotNull" | "between";
16
+ /**
17
+ * A single leaf filter condition.
18
+ *
19
+ * `field` is either a plain field name, or a pre-qualified path (e.g.
20
+ * `["c", "customer_name"]` for `c.customer_name` in a join) — the grid never
21
+ * infers joins, callers pre-qualify ambiguous fields themselves.
22
+ *
23
+ * `value` is an array for `in`/`notIn`/`between`, and is omitted entirely for
24
+ * unary operators like `isNull`/`isNotNull`/`isEmpty`/`isNotEmpty`.
25
+ */
26
+ interface FilterDescriptor {
27
+ field: string | string[];
28
+ operator: FilterOperator;
29
+ value?: unknown;
30
+ ignoreCase?: boolean;
31
+ }
32
+ /** Combines two or more filters (leaf or nested composite) with AND/OR logic. */
33
+ interface CompositeFilterDescriptor {
34
+ logic: "and" | "or";
35
+ filters: (FilterDescriptor | CompositeFilterDescriptor)[];
36
+ }
37
+ interface SortDescriptor {
38
+ field: string;
39
+ dir: "asc" | "desc";
40
+ }
41
+ /** The full sort/filter/pagination state a <DataGrid> tracks and can emit. */
42
+ interface GridState {
43
+ filter: CompositeFilterDescriptor | null;
44
+ sort: SortDescriptor[];
45
+ page: number;
46
+ pageSize: number;
47
+ }
48
+ /** True for a leaf `FilterDescriptor`, false for a `CompositeFilterDescriptor`. */
49
+ declare function isFilterDescriptor(node: FilterDescriptor | CompositeFilterDescriptor): node is FilterDescriptor;
50
+ /** True for a `CompositeFilterDescriptor`, false for a leaf `FilterDescriptor`. */
51
+ declare function isCompositeFilterDescriptor(node: FilterDescriptor | CompositeFilterDescriptor): node is CompositeFilterDescriptor;
52
+ /** Operators that never carry a `value` (unary presence/absence checks). */
53
+ declare const UNARY_OPERATORS: ReadonlySet<FilterOperator>;
54
+ /** Renders a `field` (string or pre-qualified path) as a single dotted key, for use as a stable id/key. */
55
+ declare function fieldKey(field: string | string[]): string;
56
+
57
+ /**
58
+ * Evaluates a filter tree (leaf or composite, or `null`/`undefined` for "no
59
+ * filter") against a single row. Used by `<DataGrid>` in `"client"` mode;
60
+ * `"server"` mode never calls this — it trusts the caller already applied
61
+ * the equivalent filter server-side.
62
+ */
63
+ declare function evaluateFilter<TRow>(row: TRow, filter: CompositeFilterDescriptor | FilterDescriptor | null | undefined): boolean;
64
+
65
+ /**
66
+ * Contract every built-in filter widget implements. `TColumn` is narrowed to
67
+ * the specific column shape the widget knows how to render (e.g. `EnumColumn`
68
+ * for `EnumFilter`) — the grid picks the right widget by `column.type` before
69
+ * handing it a column, so the widget itself never has to branch on `type`.
70
+ *
71
+ * `value` is the current FilterDescriptor for this column, or undefined when
72
+ * no filter is applied. `onChange(undefined)` clears the filter entirely.
73
+ */
74
+ interface FilterWidgetProps<TColumn> {
75
+ column: TColumn;
76
+ value: FilterDescriptor | undefined;
77
+ onChange: (next: FilterDescriptor | undefined) => void;
78
+ }
79
+
80
+ /** Horizontal alignment used for a column's cell content and header. */
81
+ type ColumnAlign = "left" | "center" | "right";
82
+ /** Fields shared by every column type, independent of `type`. */
83
+ interface BaseColumn<TRow> {
84
+ id: string;
85
+ accessorKey?: keyof TRow & string;
86
+ accessorFn?: (row: TRow) => unknown;
87
+ header: string;
88
+ width?: number;
89
+ /** Opt-in: defaults to false — must be explicitly set to true to show a sort toggle in the header. */
90
+ sortable?: boolean;
91
+ /**
92
+ * Opt-in: defaults to false, meaning a first click sorts ascending. Set true for a
93
+ * column whose "interesting" direction is descending — revenue, risk scores, backlog —
94
+ * so a first click surfaces the highest/most-urgent values first instead of the
95
+ * lowest/safest ones. Cycles desc -> asc -> none instead of the default asc -> desc -> none.
96
+ */
97
+ sortDescFirst?: boolean;
98
+ /** Opt-in: defaults to false — must be explicitly set to true to show a filter icon/widget in the header. */
99
+ filterable?: boolean;
100
+ pinned?: "left" | "right";
101
+ /** Overrides the default type-based renderer. */
102
+ cell?: (value: unknown, row: TRow) => ReactNode;
103
+ /** Free-text group label for <ColumnSelector>; columns sharing a group are shown together. Ungrouped when omitted. */
104
+ group?: string;
105
+ /**
106
+ * Free-text label for a spanning header cell in `<DataGrid>`'s own thead —
107
+ * unlike `group` above (which only affects `<ColumnSelector>`), this
108
+ * changes the grid's rendered markup. Contiguous *visible* columns sharing
109
+ * the same `headerGroup` string get merged under one `colSpan`'d label
110
+ * cell in an extra row above the normal per-column header row; a column
111
+ * with no `headerGroup` instead spans both header rows itself (via
112
+ * `rowSpan`), so it reads as a single ungrouped column rather than leaving
113
+ * a blank cell above it. Two columns with the same `headerGroup` that
114
+ * AREN'T adjacent in the visible column order (e.g. a differently-grouped
115
+ * or ungrouped column sits between them) form two separate spanning cells
116
+ * with the same label, rather than one — grouping is purely positional,
117
+ * matching how a caller would read the columns left-to-right.
118
+ */
119
+ headerGroup?: string;
120
+ /**
121
+ * Overrides the default type-based filter widget (see
122
+ * `renderDefaultFilterWidget`) for this column. Receives the same
123
+ * `(value, onChange)` pair the default widget would, plus the grid's full
124
+ * current `GridState.filter` (every column's combined filter) as a third
125
+ * argument — needed for a widget like `NumberHistogramFilter`, whose
126
+ * `allValues` prop must be computed via `facetedNumberValues(data, column,
127
+ * filter)` to respect every OTHER active filter while excluding this
128
+ * column's own (see `facetedNumberValues`'s own doc for why). Set this to
129
+ * close over whatever extra data (e.g. the full unfiltered `data`) the
130
+ * custom widget needs.
131
+ */
132
+ renderFilter?: (value: FilterDescriptor | undefined, onChange: (next: FilterDescriptor | undefined) => void, filter: CompositeFilterDescriptor | null) => ReactNode;
133
+ /**
134
+ * Where this column's filter control renders. Defaults to `"popover"`: a
135
+ * filter-icon button in the header that opens the widget in a popover,
136
+ * same as every column gets automatically today. `"row"` renders the
137
+ * widget's content directly, inline, in an additional filter row under
138
+ * the header instead — needed for a wide control (e.g.
139
+ * `NumberHistogramFilter`'s slider/histogram) that doesn't fit well
140
+ * collapsed into the header's compact trigger button. The filter row
141
+ * itself is entirely opt-in: it only renders at all if at least one
142
+ * visible, filterable column sets `filterDisplay: "row"`.
143
+ */
144
+ filterDisplay?: "popover" | "row";
145
+ /**
146
+ * Overrides the plain-text `header` rendering *inside* the header cell's
147
+ * sort toggle `<button>` (the sort caret still renders alongside it, and
148
+ * still inside that same button). Use this for a header that needs a
149
+ * tooltip, an info icon, a loading spinner tied to a separate
150
+ * batch-hydration fetch, or any other rich content — `header` itself
151
+ * stays a plain string (used for the filter icon's `aria-label` and by
152
+ * `<ColumnSelector>`, neither of which can work off a ReactNode).
153
+ *
154
+ * Whatever this returns is nested inside a real `<button>` — don't return
155
+ * another focusable/interactive element (a nested `<button>`, a link, a
156
+ * tooltip trigger that itself renders as a `<button>`) or the browser will
157
+ * split the outer button apart, breaking click-to-sort for that column.
158
+ * If the return value is non-textual (an icon with no visible label), the
159
+ * button's accessible name is whatever text content survives inside it —
160
+ * add your own `aria-label` inside the returned content if there isn't
161
+ * any, since `<DataGrid>` has no way to tell your content lacks one.
162
+ */
163
+ renderHeader?: (column: BaseColumn<TRow>) => ReactNode;
164
+ }
165
+ type StringColumn<TRow> = BaseColumn<TRow> & {
166
+ type: "string";
167
+ };
168
+ type NumberColumn<TRow> = BaseColumn<TRow> & {
169
+ type: "number" | "currency";
170
+ currency?: string;
171
+ };
172
+ type DateColumn<TRow> = BaseColumn<TRow> & {
173
+ type: "date" | "datetime";
174
+ };
175
+ type BooleanColumn<TRow> = BaseColumn<TRow> & {
176
+ type: "boolean";
177
+ };
178
+ interface EnumOption {
179
+ value: string;
180
+ label: string;
181
+ }
182
+ type EnumColumn<TRow> = BaseColumn<TRow> & {
183
+ type: "enum";
184
+ options: EnumOption[];
185
+ };
186
+ /** Discriminated union of every supported column shape, keyed on `type`. */
187
+ type ColumnDef<TRow> = StringColumn<TRow> | NumberColumn<TRow> | DateColumn<TRow> | BooleanColumn<TRow> | EnumColumn<TRow>;
188
+ /** Resolves a column's raw cell value from a row, via `accessorFn` or `accessorKey`. */
189
+ declare function getColumnValue<TRow>(column: ColumnDef<TRow>, row: TRow): unknown;
190
+ /** True only if a column explicitly opts in (`sortable: true`) — the default is false. */
191
+ declare function isSortable<TRow>(column: ColumnDef<TRow>): boolean;
192
+ /** True only if a column explicitly opts in (`filterable: true`) — the default is false. */
193
+ declare function isFilterable<TRow>(column: ColumnDef<TRow>): boolean;
194
+
195
+ /**
196
+ * Default filter widget for `type: "boolean"` columns: a 3-way All/Yes/No
197
+ * select.
198
+ *
199
+ * `bare` (default `false`) skips this component's own Popover/trigger
200
+ * Button, rendering just the select directly — see `EnumFilter`'s identical
201
+ * `bare` doc for why (`renderDefaultFilterWidget` passes this automatically
202
+ * outside `filterDisplay: "row"`).
203
+ */
204
+ declare function BooleanFilter<TRow>({ column, value, onChange, bare, }: FilterWidgetProps<BooleanColumn<TRow>> & {
205
+ bare?: boolean;
206
+ }): ReactElement;
207
+
208
+ /**
209
+ * Default filter widget for `type: "date"` / `type: "datetime"` columns: a
210
+ * date-range picker with Today / Last 7 days / This month presets, plus a
211
+ * calendar for a custom range. Always emits the `between` operator
212
+ * (inclusive both ends, matching evaluateFilter/sql.py/meili.py semantics).
213
+ *
214
+ * `today` defaults to the real current date; it's exposed as a prop purely
215
+ * so tests can pin preset computation to a fixed date without mocking the
216
+ * system clock (and the timer machinery that comes with that).
217
+ *
218
+ * `bare` (default `false`) skips this component's own Popover/trigger
219
+ * Button, rendering just the presets + calendar directly — see
220
+ * `EnumFilter`'s identical `bare` doc for why (`renderDefaultFilterWidget`
221
+ * passes this automatically outside `filterDisplay: "row"`).
222
+ */
223
+ declare function DateRangeFilter<TRow>({ column, value, onChange, today, bare, }: FilterWidgetProps<DateColumn<TRow>> & {
224
+ today?: Date;
225
+ bare?: boolean;
226
+ }): ReactElement;
227
+
228
+ /**
229
+ * Default filter widget for `type: "enum"` columns: a single Excel-style
230
+ * checkbox list, regardless of how many `column.options` there are (no
231
+ * short-list/long-list branch) —
232
+ *
233
+ * - a search input filters which options *render* below via a plain
234
+ * substring match against each option's label; it never touches the
235
+ * committed filter on its own.
236
+ * - a tri-state "Select all" checkbox acts on whatever subset is currently
237
+ * visible after that search, not the full option set — matching Excel's
238
+ * own column-filter behavior ("select all of what's shown").
239
+ * - every visible option gets its own plain checkbox + label row (a real
240
+ * shadcn `Checkbox`, not a menu item), inside a scrollable list so long
241
+ * option lists don't blow up the popover's height.
242
+ * - every toggle emits the updated `FilterDescriptor` immediately — there's
243
+ * no OK/Cancel step, matching every other filter widget in this package.
244
+ *
245
+ * `bare` (default `false`) skips this component's own Popover/trigger
246
+ * Button, rendering just the search input + option list directly — for a
247
+ * caller that already provides its own popover/collapsible (this is what
248
+ * `renderDefaultFilterWidget` passes automatically outside `filterDisplay:
249
+ * "row"`, so `<DataGrid>`'s own header-icon popover isn't nested inside a
250
+ * second one of this component's own).
251
+ */
252
+ declare function EnumFilter<TRow>({ column, value, onChange, bare, }: FilterWidgetProps<EnumColumn<TRow>> & {
253
+ bare?: boolean;
254
+ }): ReactElement;
255
+
256
+ /**
257
+ * An Excel-style alternative to the default `NumberRangeFilter`: an operator
258
+ * dropdown (Greater than / Less than / Equals / ... / Between) plus one
259
+ * number input (two for `Between`), instead of a plain always-visible
260
+ * min/max range. Not wired up as any column type's automatic default —
261
+ * `NumberRangeFilter` still is, so existing columns keep their current
262
+ * behavior. Use this directly as a column's custom filter UI (see
263
+ * `BaseColumn.renderFilter`) for a numeric column where users think in
264
+ * terms of "greater than X" rather than "between X and Y".
265
+ *
266
+ * Local `operator`/text state (rather than deriving straight from `value`
267
+ * on every render, the way `StringFilter` does) is what lets `Between` hold
268
+ * a partially-typed pair — deriving directly would wipe whatever's in the
269
+ * OTHER box the instant the first box's own edit clears `value` back to
270
+ * `undefined` (no filter is emitted until both bounds parse), and would
271
+ * reset the operator dropdown back to the default the same way. The effect
272
+ * below only resyncs from `value` when it changed for some reason other
273
+ * than this widget's own last `onChange` call (an external clear-all, or
274
+ * restoring a saved view) — the same pattern `NumberRangeFilter` uses for
275
+ * the same reason.
276
+ *
277
+ * `bare` (default `true`) skips this component's own Popover/trigger
278
+ * Button, rendering just the operator + input(s) directly — correct under
279
+ * the default `filterDisplay: "popover"`, where `<DataGrid>` already
280
+ * supplies the header filter icon and popover; passing `false` there would
281
+ * nest a second, redundant one inside it. Pass `bare={false}` only when the
282
+ * column also sets `filterDisplay: "row"`, where nothing else provides a
283
+ * trigger — see `NumberHistogramFilter`'s identical `bare` doc for why.
284
+ */
285
+ declare function NumberComparisonFilter<TRow>({ column, value, onChange, bare, }: FilterWidgetProps<NumberColumn<TRow>> & {
286
+ bare?: boolean;
287
+ }): ReactElement;
288
+
289
+ /**
290
+ * Default filter widget for `type: "number"` / `type: "currency"` columns: a
291
+ * min/max range. Emits `between` when both bounds are set (reordered so the
292
+ * lower bound is always <= the upper bound), or `gte`/`lte` when only one
293
+ * is — all three are evaluated identically by evaluateFilter and sql.py/
294
+ * meili.py.
295
+ *
296
+ * The two input boxes track their own local text rather than deriving
297
+ * straight from `value` on every render. If they derived from `value`
298
+ * directly, reordering an inverted min/max (see above) would relocate
299
+ * whatever the user just typed into the *other* box on the very next
300
+ * render — so typing "99" into Min then "9" then "9" into Max (intending
301
+ * "99") would see Max flip to "99" after the first "9" (since a swap makes
302
+ * it the new max), and the second "9" would land in the now-relocated box,
303
+ * producing "909" instead of "99". Keeping local text means what's on
304
+ * screen always matches exactly what was typed, independent of how the
305
+ * emitted FilterDescriptor's bounds get reordered.
306
+ *
307
+ * `bare` (default `false`) skips this component's own Popover/trigger
308
+ * Button, rendering just the two inputs directly — see `EnumFilter`'s
309
+ * identical `bare` doc for why (`renderDefaultFilterWidget` passes this
310
+ * automatically outside `filterDisplay: "row"`). Under `filterDisplay:
311
+ * "row"`, the min/max boxes are always visible at their natural width
312
+ * regardless of a narrower column — wrapping them in a compact trigger
313
+ * (bare: false) is what keeps a plain numeric column from forcing its whole
314
+ * column wider than its data ever needs.
315
+ */
316
+ declare function NumberRangeFilter<TRow>({ column, value, onChange, bare, }: FilterWidgetProps<NumberColumn<TRow>> & {
317
+ bare?: boolean;
318
+ }): ReactElement;
319
+
320
+ type NumberHistogramValuesProps = {
321
+ /**
322
+ * The full set of values to compute the histogram and slider domain
323
+ * from.
324
+ *
325
+ * MUST be computed excluding this column's OWN active filter (every
326
+ * other active filter is fine, even expected — see
327
+ * `facetedNumberValues`, which computes exactly this). Passing values
328
+ * already narrowed by this same filter reproduces the bug this
329
+ * component's design exists to prevent: the domain shrinks every time
330
+ * the user adjusts the range, so they can never widen it back out, and
331
+ * (if the same "all rows" array is reused across multiple numeric
332
+ * columns) filtering one column warps every other numeric column's
333
+ * histogram too.
334
+ *
335
+ * Use this (not `loadValues`) whenever the full, facet-safe row set is
336
+ * already in memory — e.g. `<DataGrid>` in `"client"` mode.
337
+ */
338
+ allValues: (number | null | undefined)[];
339
+ loadValues?: never;
340
+ } | {
341
+ allValues?: never;
342
+ /**
343
+ * Fetches the histogram/slider domain values on demand, called every
344
+ * time the popover opens (not cached across opens, since every OTHER
345
+ * active filter — which the fetch should itself respect — may have
346
+ * changed since the last time it was open).
347
+ *
348
+ * Use this instead of `allValues` whenever the facet-safe row set
349
+ * ISN'T already in memory — the standard case for `<DataGrid>` in
350
+ * `"server"` mode, where the rows the grid holds are just the current
351
+ * page, already narrowed by every active filter (including this
352
+ * column's own), and therefore never safe to pass as `allValues`
353
+ * directly. Typically this closes over a caller's own "fetch facet
354
+ * values for this column, given every other active filter" endpoint.
355
+ */
356
+ loadValues: () => Promise<(number | null | undefined)[]>;
357
+ };
358
+ type NumberHistogramFilterProps<TRow> = FilterWidgetProps<NumberColumn<TRow>> & NumberHistogramValuesProps & {
359
+ /** Formats a bound for display. Defaults to `toLocaleString()`. */
360
+ format?: (value: number) => string;
361
+ /** Number of histogram bars. Defaults to 24. */
362
+ buckets?: number;
363
+ /**
364
+ * Set this to `false` only when the column also sets `filterDisplay:
365
+ * "row"` — there, nothing else provides a trigger, so this needs its
366
+ * own Popover + Button to avoid permanently expanding the histogram
367
+ * inline in the filter row. Defaults to `true` (bare: no own Popover/
368
+ * Button, just the histogram/slider/inputs/clear content) because a
369
+ * custom `BaseColumn.renderFilter` is normally reached for under the
370
+ * *default* `filterDisplay: "popover"`, where `<DataGrid>` already
371
+ * renders a header filter icon that opens a popover around whatever
372
+ * `renderFilter` returns — passing `false` there nests a second,
373
+ * redundant Popover+Button inside the first, so opening the header
374
+ * icon shows nothing but another filter icon to click, not the
375
+ * histogram. There's no `open`/`onOpenChange` state at this level in
376
+ * bare mode — the component simply renders as always-open; the
377
+ * caller's own popover is what mounts/unmounts it.
378
+ */
379
+ bare?: boolean;
380
+ };
381
+ /**
382
+ * A richer alternative to the default `NumberRangeFilter`: a log-scale
383
+ * histogram behind a dual-thumb range slider, plus min/max text inputs.
384
+ * Not wired up as any column type's automatic default (unlike
385
+ * `NumberRangeFilter`) — it needs `allValues`/`loadValues` (see above),
386
+ * which the grid itself doesn't have a way to supply automatically yet, so
387
+ * use it directly as a column's custom filter UI (see `BaseColumn.renderFilter`).
388
+ * By default (`bare` unset) this renders bare — just the histogram/slider/
389
+ * inputs, no own trigger/popover — correct under the default `filterDisplay:
390
+ * "popover"`, where `<DataGrid>` already supplies the header filter icon and
391
+ * popover. Pass `bare={false}` when the column also sets `filterDisplay:
392
+ * "row"`, where nothing else provides a trigger. Compute `allValues` via
393
+ * `facetedNumberValues(data, column, filter)` in client mode, or supply
394
+ * `loadValues` to fetch the facet from the server in server mode.
395
+ */
396
+ declare function NumberHistogramFilter<TRow>({ column, value, onChange, allValues, loadValues, format, buckets, bare, }: NumberHistogramFilterProps<TRow>): ReactElement;
397
+
398
+ type FilterNode = FilterDescriptor | CompositeFilterDescriptor;
399
+ /**
400
+ * Computes the correct `allValues` input for `NumberHistogramFilter` bound
401
+ * to `column`: `data` filtered by every OTHER currently-active filter, with
402
+ * `column`'s own filter excluded, then that column's values extracted.
403
+ *
404
+ * This exists to prevent a specific, easy-to-reach bug: computing a filter
405
+ * widget's own histogram/slider domain from a row set that's already been
406
+ * narrowed BY that same filter means the domain shrinks every time the user
407
+ * adjusts the range — they can never widen it back out — and if one "all
408
+ * values" computation is naively reused across multiple numeric columns
409
+ * (e.g. from `table.getFilteredRowModel()`), setting a filter on column A
410
+ * also warps column B's histogram, since B's domain was computed from rows
411
+ * A already excluded. This is the standard "faceted search" fix: a filter's
412
+ * own facet should reflect "what could I select if I changed just this
413
+ * filter," never a result set that filter has already narrowed.
414
+ *
415
+ * `filter` is the grid's full `GridState.filter` (every column's combined
416
+ * filter) — pass it as-is; this function does the excluding.
417
+ */
418
+ declare function facetedNumberValues<TRow>(data: readonly TRow[], column: ColumnDef<TRow>, filter: FilterNode | null | undefined): (number | null)[];
419
+
420
+ /**
421
+ * Default filter widget for `type: "string"` columns: a text input plus an
422
+ * operator dropdown.
423
+ *
424
+ * `bare` (default `false`) skips this component's own Popover/trigger
425
+ * Button, rendering just the operator + input directly — see `EnumFilter`'s
426
+ * identical `bare` doc for why (`renderDefaultFilterWidget` passes this
427
+ * automatically outside `filterDisplay: "row"`).
428
+ */
429
+ declare function StringFilter<TRow>({ column, value, onChange, bare, }: FilterWidgetProps<StringColumn<TRow>> & {
430
+ bare?: boolean;
431
+ }): ReactElement;
432
+
433
+ /**
434
+ * Picks and renders the default filter widget for a column, based purely on
435
+ * `column.type`. This is what makes filtering "just work" without a column
436
+ * having to configure anything — the discriminated union on `type` lets each
437
+ * branch narrow `column` to the exact widget it needs.
438
+ *
439
+ * Every default widget has its own self-contained Popover+trigger Button
440
+ * (needed under `filterDisplay: "row"`, where nothing else provides one) —
441
+ * everywhere else, `<DataGrid>`'s own header filter icon already opens a
442
+ * popover around whatever this function returns, so passing `bare: true`
443
+ * there (this function is `<DataGrid>`'s only caller, and always knows
444
+ * `column.filterDisplay` already) is what keeps that from nesting a second
445
+ * popover inside the first.
446
+ */
447
+ declare function renderDefaultFilterWidget<TRow>(column: ColumnDef<TRow>, value: FilterDescriptor | undefined, onChange: (next: FilterDescriptor | undefined) => void): ReactElement;
448
+
449
+ /** The default horizontal alignment for a column, based purely on its `type`. */
450
+ declare function defaultAlign<TRow>(column: ColumnDef<TRow>): ColumnAlign;
451
+ /** The literal Tailwind class for a column's default alignment; see `defaultAlign`. */
452
+ declare function alignClassName<TRow>(column: ColumnDef<TRow>): string;
453
+ /**
454
+ * Renders a raw cell value as a display string, using the type-appropriate
455
+ * default formatter for `column.type`. Columns with a custom `cell` render
456
+ * function bypass this entirely — it only backs the built-in renderer.
457
+ */
458
+ declare function defaultFormat<TRow>(column: ColumnDef<TRow>, value: unknown): string;
459
+
460
+ /**
461
+ * Column visibility keyed by `ColumnDef.id`. A missing key means visible —
462
+ * this matches TanStack Table's own `VisibilityState` convention, so a
463
+ * `ColumnVisibility` value can be handed straight to a TanStack table's
464
+ * `state.columnVisibility` if a consumer ever needs to.
465
+ */
466
+ type ColumnVisibility = Record<string, boolean>;
467
+
468
+ interface ColumnSelectorProps<TRow> {
469
+ columns: ColumnDef<TRow>[];
470
+ visibility: ColumnVisibility;
471
+ onVisibilityChange: (visibility: ColumnVisibility) => void;
472
+ /** If set: restore from localStorage on mount, and persist on every subsequent change. */
473
+ persistKey?: string;
474
+ /** Optional custom trigger; defaults to a "Columns" icon button. */
475
+ trigger?: ReactNode;
476
+ /** Optional controlled open state, for callers that want their own trigger entirely. */
477
+ open?: boolean;
478
+ onOpenChange?: (open: boolean) => void;
479
+ }
480
+ /**
481
+ * Controlled column-visibility selector: a modal listing every column,
482
+ * grouped by `column.group` into side-by-side columns (ungrouped columns
483
+ * first, unlabeled, matching `groupColumns`' own ordering), wrapping onto a
484
+ * new row rather than growing the dialog past a reasonable width. Each named
485
+ * group's header is a plain muted label — no bulk-select affordance of any
486
+ * kind lives there (an earlier version had a tri-state checkbox, then an
487
+ * "All"/"None" link pair, both dropped; see AGENTS.md for why). Purely
488
+ * controlled by `visibility`/`onVisibilityChange` — `persistKey`
489
+ * layers an optional localStorage sync on top (restore once on mount, write
490
+ * on every change), it never becomes the source of truth in place of the
491
+ * `visibility` prop.
492
+ *
493
+ * Not rendered inside `<DataGrid>` automatically — wire it up next to the
494
+ * grid yourself, e.g.:
495
+ *
496
+ * ```tsx
497
+ * const [visibility, setVisibility] = useState<ColumnVisibility>({});
498
+ * <ColumnSelector columns={columns} visibility={visibility} onVisibilityChange={setVisibility} persistKey="orders" />
499
+ * <DataGrid columns={columns} columnVisibility={visibility} onColumnVisibilityChange={setVisibility} ... />
500
+ * ```
501
+ */
502
+ declare function ColumnSelector<TRow>({ columns, visibility, onVisibilityChange, persistKey, trigger, open, onOpenChange, }: ColumnSelectorProps<TRow>): ReactElement;
503
+
504
+ /** A missing key means visible, matching TanStack's own VisibilityState convention. */
505
+ declare function isColumnVisible(visibility: ColumnVisibility, columnId: string): boolean;
506
+ /** How many of `columns` are currently visible under `visibility`. */
507
+ declare function countVisible<TRow>(columns: readonly ColumnDef<TRow>[], visibility: ColumnVisibility): number;
508
+ interface ColumnGroup<TRow> {
509
+ /** undefined for the ungrouped section, which is always listed first. */
510
+ group: string | undefined;
511
+ columns: ColumnDef<TRow>[];
512
+ }
513
+ /**
514
+ * Groups columns by `column.group`, preserving each group's first-seen order
515
+ * and each column's original order within its group. The ungrouped section
516
+ * (columns with no `group`) is always returned first, regardless of where
517
+ * ungrouped columns actually fall in the input order.
518
+ */
519
+ declare function groupColumns<TRow>(columns: readonly ColumnDef<TRow>[]): ColumnGroup<TRow>[];
520
+ /** Whether toggling `columnId` off is currently allowed (must leave at least one column visible). */
521
+ declare function canHideColumn<TRow>(columns: readonly ColumnDef<TRow>[], visibility: ColumnVisibility, columnId: string): boolean;
522
+ /** Whether hiding every column in `groupColumnIds` at once is currently allowed. */
523
+ declare function canHideGroup<TRow>(columns: readonly ColumnDef<TRow>[], visibility: ColumnVisibility, groupColumnIds: readonly string[]): boolean;
524
+
525
+ declare function storageKeyFor(persistKey: string): string;
526
+
527
+ /** The row/selection context a menu item's visible/disabled/onSelect handlers are evaluated against. */
528
+ interface MenuItemContext<TRow> {
529
+ row?: TRow;
530
+ selectedRows?: TRow[];
531
+ }
532
+ /**
533
+ * A single entry in a row-actions (per-row kebab menu) or header-actions
534
+ * (toolbar) menu. `rowActions` items are evaluated with `ctx.row` set;
535
+ * `headerActions` items are evaluated with `ctx.selectedRows` set (whatever
536
+ * the grid's current selection is, possibly empty).
537
+ */
538
+ interface MenuItem<TRow> {
539
+ id: string;
540
+ label: string;
541
+ icon?: ComponentType;
542
+ danger?: boolean;
543
+ /** Defaults to always visible when omitted. */
544
+ visible?: (ctx: MenuItemContext<TRow>) => boolean;
545
+ /** Defaults to always enabled when omitted. */
546
+ disabled?: (ctx: MenuItemContext<TRow>) => boolean;
547
+ onSelect: (ctx: MenuItemContext<TRow>) => void;
548
+ }
549
+
550
+ /** A `MenuItem` with its `visible`/`disabled` predicates already evaluated against a context. */
551
+ interface ResolvedMenuItem<TRow> extends MenuItem<TRow> {
552
+ isDisabled: boolean;
553
+ }
554
+ /**
555
+ * Evaluates each item's `visible`/`disabled` predicates against `ctx` (both
556
+ * default to "always" when omitted), dropping items that aren't visible and
557
+ * annotating the rest with `isDisabled`. Pure and UI-free on purpose, so the
558
+ * gating logic itself — as opposed to the DropdownMenu rendering built on
559
+ * top of it — can be unit-tested directly.
560
+ */
561
+ declare function resolveMenuItems<TRow>(items: readonly MenuItem<TRow>[], ctx: MenuItemContext<TRow>): ResolvedMenuItem<TRow>[];
562
+
563
+ interface ActionsMenuProps<TRow> {
564
+ items: readonly MenuItem<TRow>[];
565
+ ctx: MenuItemContext<TRow>;
566
+ /** Accessible label for the trigger button, e.g. "Row actions" or "Bulk actions". */
567
+ triggerLabel: string;
568
+ }
569
+ /**
570
+ * Shared rendering for both `rowActions` (per-row kebab menu) and
571
+ * `headerActions` (toolbar menu) — they differ only in what `ctx` they're
572
+ * evaluated against (`{ row }` vs `{ selectedRows }`) and the trigger's
573
+ * accessible label, so <DataGrid> passes those in rather than this module
574
+ * having two near-identical components.
575
+ */
576
+ declare function ActionsMenu<TRow>({ items, ctx, triggerLabel }: ActionsMenuProps<TRow>): ReactElement | null;
577
+
578
+ /**
579
+ * `"client"`: the grid owns filtering/sorting/pagination and evaluates them
580
+ * in-memory against the full `data` array (via `evaluateFilter`).
581
+ *
582
+ * `"server"`: `data` is just the current page, already filtered/sorted by
583
+ * the caller; the grid calls `onStateChange` on every sort/filter/page
584
+ * change (debounced ~300ms for filter changes) and trusts the result — it
585
+ * never re-filters/re-sorts server-mode data itself.
586
+ */
587
+ type DataSource<TRow> = {
588
+ mode: "client";
589
+ data: TRow[];
590
+ } | {
591
+ mode: "server";
592
+ data: TRow[];
593
+ rowCount: number;
594
+ loading?: boolean;
595
+ onStateChange: (state: GridState) => void;
596
+ };
597
+ interface DataGridVirtualizeOptions {
598
+ /** Enables row virtualization once the row count exceeds this threshold. Defaults to 100. Set to 0 to always virtualize, or Infinity to never. */
599
+ threshold?: number;
600
+ /** Fixed row height estimate in pixels, used by the virtualizer. Defaults to 40. */
601
+ estimatedRowHeight?: number;
602
+ /** Max height of the scrollable table body in pixels. Required for virtualization to have a viewport to scroll within. Defaults to 480. */
603
+ maxBodyHeight?: number;
604
+ /** Extra rows rendered beyond the viewport on each side. Defaults to 10. */
605
+ overscan?: number;
606
+ /**
607
+ * Called once when scrolling reaches the last currently loaded row — the
608
+ * caller's job to fetch more and grow `dataSource.data`; `<DataGrid>`
609
+ * doesn't own pagination state in this mode (works with `"client"` and
610
+ * `"server"` `DataSource` alike, since it's purely about *rendering*, not
611
+ * fetching). Won't fire again for the same `data` length — only once
612
+ * `data` actually grows (or shrinks and regrows) does another scroll to
613
+ * the end re-fire it.
614
+ */
615
+ onEndReached?: () => void;
616
+ /** Whether more rows exist beyond what's currently loaded. `onEndReached` never fires while this is `false`. Defaults to `true`. */
617
+ hasMore?: boolean;
618
+ }
619
+ interface DataGridProps<TRow> {
620
+ columns: ColumnDef<TRow>[];
621
+ dataSource: DataSource<TRow>;
622
+ getRowId: (row: TRow) => string;
623
+ /**
624
+ * Shows a full-overlay spinner over the grid's scrollable body, positioned as a
625
+ * sibling of it (not a child) so it stays fixed over the visible viewport instead of
626
+ * scrolling away with the table content. Independent of `dataSource.mode === "server"`'s
627
+ * own `loading` flag (which only affects the empty-state text) — this works for
628
+ * `"client"` mode too, for a caller whose own `data` array is still being fetched.
629
+ * The two are OR'd together if both happen to be set. Defaults to `false`.
630
+ */
631
+ loading?: boolean;
632
+ /** Per-row kebab menu. Evaluated with `ctx.row` set to that row. */
633
+ rowActions?: MenuItem<TRow>[];
634
+ /** Toolbar menu above the table. Evaluated with `ctx.selectedRows` set. */
635
+ headerActions?: MenuItem<TRow>[];
636
+ /** Allows shift-click to add a secondary sort column instead of replacing. Defaults to false. */
637
+ enableMultiSort?: boolean;
638
+ /** Initial sort/filter/page/pageSize, e.g. restored from a URL. Defaults to no filter, no sort, page 0, pageSize 20. Only read once, at mount — see `gridState` to push updates in afterward. */
639
+ initialState?: Partial<GridState>;
640
+ /**
641
+ * Pushes a full `GridState` into the grid from outside, overriding
642
+ * whatever it currently has — the one way to change sort/filter/page
643
+ * programmatically after mount without remounting `<DataGrid>` (e.g. via
644
+ * a changing `key`). Omit entirely for the grid to fully own its own
645
+ * state, seeded once from `initialState`. Typical server-mode usage
646
+ * mirrors every `dataSource.onStateChange` call into your own state and
647
+ * passes that same value back in here — a fresh object literal recreated
648
+ * on every render instead re-triggers this resync every render, so don't
649
+ * inline a new one; pass the state variable you're already keeping in
650
+ * sync from `onStateChange`. Like a controlled `<input>`: decide once
651
+ * whether a given `<DataGrid>` is controlled this way and stay there for
652
+ * its whole lifetime — toggling this to `undefined` later doesn't hand
653
+ * control back, it just stops resyncing.
654
+ */
655
+ gridState?: GridState;
656
+ /**
657
+ * Controlled column visibility, e.g. driven by a `<ColumnSelector>` next
658
+ * to the grid. Both must be supplied together to actually hide columns —
659
+ * <DataGrid> never owns this state itself, matching how it never owns
660
+ * GridState's persistence either.
661
+ */
662
+ columnVisibility?: ColumnVisibility;
663
+ onColumnVisibilityChange?: (visibility: ColumnVisibility) => void;
664
+ /**
665
+ * Enables an expand/collapse chevron in a dedicated leftmost column; when
666
+ * a row is expanded, its result is rendered directly below that row, in
667
+ * its own full-width `<tr>`. Omit entirely to disable — no chevron column
668
+ * renders at all. Multiple rows can be expanded simultaneously (there's
669
+ * no single-row-at-a-time constraint). Combined with `virtualize`: only
670
+ * the row's own height is dynamically measured, not the expanded detail
671
+ * panel's — size `virtualize.estimatedRowHeight` generously if rows are
672
+ * commonly expanded in a virtualized grid, or expect some visual overlap.
673
+ */
674
+ renderDetail?: (row: TRow) => ReactNode;
675
+ /**
676
+ * Enables row virtualization (`@tanstack/react-virtual`) once the row
677
+ * count crosses `threshold`. Omit entirely for a plain, fully-rendered
678
+ * `<table>` (the default) — most grids never need this; it's for the ones
679
+ * with hundreds+ of rows where rendering every `<tr>` becomes the
680
+ * bottleneck.
681
+ */
682
+ virtualize?: DataGridVirtualizeOptions;
683
+ /**
684
+ * Opt-in: defaults to false. When true, every column gets a drag handle
685
+ * on its trailing edge to resize it, and every column (not just ones with
686
+ * an explicit `width`) gets a concrete pixel width — TanStack Table's own
687
+ * `size: 150` default for any column that doesn't set one, same as if you
688
+ * typed `width: 150` yourself. That's an intentional trade of "natural,
689
+ * content-driven column widths" for "every column resizable," so only
690
+ * turn this on for a grid that actually needs manual column resizing.
691
+ */
692
+ enableColumnResizing?: boolean;
693
+ /** `"onChange"` (default) commits width live while dragging; `"onEnd"` commits once on release. */
694
+ columnResizeMode?: "onChange" | "onEnd";
695
+ /**
696
+ * Controlled column widths, e.g. persisted the same way `columnVisibility`
697
+ * might be. Both must be supplied together to actually drive sizing —
698
+ * omit both to let `<DataGrid>` manage resize state internally.
699
+ */
700
+ columnSizing?: ColumnSizingState;
701
+ onColumnSizingChange?: (sizing: ColumnSizingState) => void;
702
+ /**
703
+ * Renders the built-in Previous/Next pagination footer. Defaults to true.
704
+ * Set to false when a caller renders its own pagination UI instead (e.g.
705
+ * one with specific `data-testid`s an existing test suite depends on) —
706
+ * `<DataGrid>` itself never conditions any other behavior on this, it just
707
+ * stops rendering that one footer.
708
+ */
709
+ showPagination?: boolean;
710
+ /**
711
+ * Controlled row selection, e.g. so a caller can build its own toolbar
712
+ * (a bulk-export button, a "N selected" count) elsewhere on the page
713
+ * instead of only reacting to selection via `headerActions`' `onSelect`
714
+ * closure. Both must be supplied together to actually control it — omit
715
+ * both to let `<DataGrid>` manage selection state internally (the
716
+ * default, unchanged from before this existed).
717
+ */
718
+ selectedIds?: ReadonlySet<string>;
719
+ onSelectedIdsChange?: (ids: ReadonlySet<string>) => void;
720
+ /**
721
+ * Extra props (className, onClick, data-* attributes, etc.) spread onto a
722
+ * row's own `<tr>`. Applied AFTER `<DataGrid>`'s own `data-testid`/
723
+ * `data-index`/`ref`, so those stay intact even if this returns keys that
724
+ * would otherwise collide — use it to make a whole row clickable (an
725
+ * `onClick` `<DataGrid>` has no other way to express — `rowActions` is a
726
+ * kebab menu, not a row-wide click target), or to attach a `className`/
727
+ * other `data-*` attribute an existing test suite already depends on. A
728
+ * `data-testid` returned here specifically is NOT one of those — see
729
+ * `getRowTestId` below for overriding that one. Typed
730
+ * as a plain `Record` (not `HTMLAttributes<HTMLTableRowElement>`) since
731
+ * that interface has no index signature — a return value with only
732
+ * `data-*` keys and no standard HTML attributes would fail TypeScript's
733
+ * "no properties in common" excess-property check against it.
734
+ *
735
+ * `onClick` specifically is safe to combine with `renderDetail`'s expand
736
+ * chevron, a `rowActions` kebab, or `selectedIds`' checkbox — each of
737
+ * those controls stops its own click from bubbling up to this `onClick`.
738
+ * That guard only covers `click`, though: an `onMouseDown`/`onDoubleClick`/
739
+ * `onContextMenu` returned here would still fire from inside any of those
740
+ * controls too, since nothing stops those event types from bubbling.
741
+ */
742
+ getRowProps?: (row: TRow) => Record<string, unknown>;
743
+ /**
744
+ * Overrides the per-row `data-testid` (default `row-${getRowId(row)}`).
745
+ * Note this is NOT something `getRowProps` above can do despite what its
746
+ * own doc says about "a caller-specific data-testid" — `<DataGrid>` always
747
+ * re-applies its own `data-testid` after spreading `getRowProps`'s result,
748
+ * specifically so a colliding key there can't accidentally break e2e
749
+ * selectors depending on the default `row-${id}` convention. Use this prop
750
+ * instead when a consumer's existing test suite selects rows by a
751
+ * different, entity-specific convention (e.g. `import-row-${id}`) that
752
+ * predates adopting `<DataGrid>` — lets that suite need zero changes.
753
+ */
754
+ getRowTestId?: (row: TRow) => string;
755
+ /**
756
+ * `data-testid` for the table's own scrollable container (the element
757
+ * `virtualize.maxBodyHeight` actually applies to, and — with or without
758
+ * virtualization — the element horizontal overflow scrolls within). Omit
759
+ * for no attribute at all. Useful for an e2e test that needs to scroll
760
+ * this specific grid or assert on its scroll position; the outer
761
+ * `data-testid="datagrid"` wrapper isn't it, and isn't unique per grid on
762
+ * a page with more than one.
763
+ */
764
+ testId?: string;
765
+ }
766
+
767
+ /**
768
+ * A single React Table component covering both client- and server-driven
769
+ * data: filtering, sorting, and pagination all flow through one `GridState`,
770
+ * with `dataSource.mode` deciding whether the grid computes the visible page
771
+ * itself (`"client"`) or trusts a page the caller already computed
772
+ * (`"server"`). Column filter UI defaults to the type-appropriate widget
773
+ * from `src/filter` unless a column supplies its own `cell` renderer.
774
+ */
775
+ declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, }: DataGridProps<TRow>): ReactElement;
776
+
777
+ interface GridStateController {
778
+ state: GridState;
779
+ /** Per-column FilterDescriptor, keyed by column id. */
780
+ filtersByColumn: ReadonlyMap<string, FilterDescriptor>;
781
+ setColumnFilter: (columnId: string, next: FilterDescriptor | undefined) => void;
782
+ /**
783
+ * Cycles a column's sort: none -> asc -> desc -> none, or none -> desc -> asc -> none
784
+ * when `descFirst` is true (see `BaseColumn.sortDescFirst`) — for a column whose
785
+ * "interesting" direction is descending (revenue, risk scores, backlog), so a first
786
+ * click surfaces the highest/most-urgent values instead of the lowest/safest ones.
787
+ * Adds/replaces per `enableMultiSort`.
788
+ */
789
+ toggleSort: (columnId: string, additive: boolean, descFirst?: boolean) => void;
790
+ setPage: (page: number) => void;
791
+ setPageSize: (pageSize: number) => void;
792
+ }
793
+ /**
794
+ * Owns `GridState` (filter/sort/page/pageSize) for `<DataGrid>` and, in
795
+ * `"server"` mode, calls `dataSource.onStateChange` on every change —
796
+ * debounced ~300ms for filter changes (typing), immediately for sort/page
797
+ * changes. In `"client"` mode `onStateChange` doesn't exist and is never
798
+ * called; the grid re-filters/re-sorts the in-memory array itself instead.
799
+ *
800
+ * Deliberately takes no `columns` and enforces no `sortable`/`filterable`
801
+ * policy itself — `toggleSort`/`setColumnFilter` will happily act on any
802
+ * column id passed to them. `<DataGrid>` is the only place that consults
803
+ * `isSortable`/`isFilterable` (in its rendered header, gating the sort
804
+ * button's `onClick` and whether a filter icon renders at all), because this
805
+ * hook is a generic state container, not a per-column-policy enforcer. A
806
+ * caller that imports this hook directly (bypassing <DataGrid>'s UI
807
+ * entirely) is responsible for applying that same policy itself before
808
+ * calling these setters, the same way it's responsible for its own markup.
809
+ *
810
+ * `externalState`, when given (from the very first render on), makes this
811
+ * fully caller-controlled: whenever it changes identity, internal `state`
812
+ * is overwritten to match (restoring a saved filter, clearing everything at
813
+ * once, applying a URL-restored state — all without remounting
814
+ * `<DataGrid>`). Pass the *same* value back in that a server-mode
815
+ * `dataSource.onStateChange` call just handed you (mirror it into your own
816
+ * state and pass that straight through) — a fresh object literal recreated
817
+ * every render re-triggers this resync every render too. Like a controlled
818
+ * `<input>`, don't switch between passing `externalState` and omitting it
819
+ * across the component's lifetime — start controlled and stay controlled,
820
+ * or start uncontrolled and stay uncontrolled; toggling to `undefined`
821
+ * later does not hand ownership back, it just stops resyncing.
822
+ */
823
+ declare function useGridState<TRow>(dataSource: DataSource<TRow>, initialState?: Partial<GridState>, externalState?: GridState): GridStateController;
824
+
825
+ interface ClientProcessingResult<TRow> {
826
+ rows: TRow[];
827
+ rowCount: number;
828
+ }
829
+ /**
830
+ * Applies a `GridState` to an in-memory row array: filter (via
831
+ * `evaluateFilter`, matching sql.py/meili.py semantics), then multi-column
832
+ * sort, then pagination. This is the entire implementation of `"client"`
833
+ * DataSource mode — `"server"` mode skips this and trusts the caller.
834
+ */
835
+ declare function processClientData<TRow>(data: readonly TRow[], state: GridState): ClientProcessingResult<TRow>;
836
+
837
+ interface DebouncedCallback<TArgs extends unknown[]> {
838
+ /** Schedules `callback`, resetting the timer if one is already pending. */
839
+ run: (...args: TArgs) => void;
840
+ /**
841
+ * Discards any pending scheduled call without invoking it. Callers that
842
+ * also have an immediate, non-debounced path to the same effect (see
843
+ * `<DataGrid>`'s `useGridState`, where sort/page changes call the
844
+ * underlying callback immediately) need this to prevent an
845
+ * already-in-flight debounced call from firing later with stale state and
846
+ * overwriting the immediate one.
847
+ */
848
+ cancel: () => void;
849
+ }
850
+ /**
851
+ * Returns a stable `{ run, cancel }` pair. `run` delays invoking `callback`
852
+ * until `delayMs` has elapsed since the last call, resetting the timer on
853
+ * every call in between (standard trailing-edge debounce). Used by
854
+ * `<DataGrid>` in `"server"` mode to avoid firing `onStateChange` on every
855
+ * keystroke while a filter widget is being typed into.
856
+ */
857
+ declare function useDebouncedCallback<TArgs extends unknown[]>(callback: (...args: TArgs) => void, delayMs: number): DebouncedCallback<TArgs>;
858
+
859
+ /**
860
+ * Accessor-based node shape, matching `<DataGrid>`'s `getRowId` convention
861
+ * rather than requiring `TRow` to carry specific field names.
862
+ *
863
+ * `hasChildren` and `getChildren` are deliberately independent, mirroring
864
+ * a consuming app's contract tree model (`has_children: boolean` distinct
865
+ * from an optional `children` array): a node can know it *has* children the backend
866
+ * hasn't sent yet. A node only needs lazy-loading (`onLoadChildren` gets
867
+ * called) when `hasChildren(row)` is true AND `getChildren(row)` returns
868
+ * nothing usable yet.
869
+ */
870
+ interface TreeAccessors<TRow> {
871
+ getRowId: (row: TRow) => string;
872
+ /** Already-loaded children, if any — from eager data or a prior `onLoadChildren` call. */
873
+ getChildren: (row: TRow) => TRow[] | undefined;
874
+ /**
875
+ * Whether this node has children at all, independent of whether they're
876
+ * loaded yet. Defaults to `(row) => (getChildren(row)?.length ?? 0) > 0`
877
+ * when omitted — i.e. purely eager trees (no lazy loading) don't need to
878
+ * supply this.
879
+ */
880
+ hasChildren?: (row: TRow) => boolean;
881
+ }
882
+ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
883
+ columns: ColumnDef<TRow>[];
884
+ /** Root-level rows. */
885
+ data: TRow[];
886
+ /**
887
+ * The column whose cell gets the depth indentation + expand/collapse
888
+ * chevron prepended. Defaults to the first column in `columns`.
889
+ */
890
+ treeColumnId?: string;
891
+ /**
892
+ * Fetches a node's children on demand. Omit this entirely for a fully
893
+ * eager tree (all children already present via `getChildren`) — a node
894
+ * with `hasChildren(row)` true but no `onLoadChildren` is treated as a
895
+ * dead end (chevron renders, but expanding it just shows "No children.").
896
+ * Results are cached per node id for the component's lifetime: collapsing
897
+ * and re-expanding a node never refetches.
898
+ */
899
+ onLoadChildren?: (row: TRow) => Promise<TRow[]>;
900
+ /** How many levels to auto-expand on first render (0 = collapsed root only). Defaults to 0. */
901
+ initialExpandedLevel?: number;
902
+ /** Controlled expanded state, keyed by node id. Omit for the grid to own this itself. */
903
+ expanded?: Record<string, boolean>;
904
+ onExpandedChange?: (expanded: Record<string, boolean>) => void;
905
+ /** Pixels of left padding added per depth level to the tree column's cell. Defaults to 20. */
906
+ indentSize?: number;
907
+ /**
908
+ * Extra props (className, onClick, data-* attributes, etc.) spread onto a
909
+ * row's own `<tr>`. Applied AFTER `<TreeDataGrid>`'s own `data-testid`/
910
+ * `ref`, so those stay intact even if this returns keys that would
911
+ * otherwise collide — use it to make a whole row clickable (an `onClick`
912
+ * `<TreeDataGrid>` has no other way to express — `rowActions` is a kebab
913
+ * menu, not a row-wide click target), or to attach a `className`/other
914
+ * `data-*` attribute an existing test suite already depends on. A
915
+ * `data-testid` returned here specifically is NOT one of those — see
916
+ * `getRowTestId` below for overriding that one. `depth` and `index` are
917
+ * the row's tree depth and its position among the currently visible
918
+ * (flattened) rows, e.g. for depth-aware indentation or index-aware
919
+ * striping. Typed as a plain `Record` (not `HTMLAttributes<HTMLTableRowElement>`)
920
+ * since that interface has no index signature — a return value with only
921
+ * `data-*` keys and no standard HTML attributes would fail TypeScript's
922
+ * "no properties in common" excess-property check against it.
923
+ *
924
+ * `onClick` specifically is safe to combine with the expand/collapse
925
+ * chevron or a `rowActions` kebab — each of those controls stops its own
926
+ * click from bubbling up to this `onClick`. That guard only covers
927
+ * `click`, though: an `onMouseDown`/`onDoubleClick`/`onContextMenu`
928
+ * returned here would still fire from inside either control too, since
929
+ * nothing stops those event types from bubbling.
930
+ */
931
+ getRowProps?: (row: TRow, depth: number, index: number) => Record<string, unknown>;
932
+ /**
933
+ * Overrides the per-row `data-testid` (default `tree-row-${getRowId(row)}`).
934
+ * Note this is NOT something `getRowProps` above can do despite what its
935
+ * own doc says about "a caller-specific data-testid" — `<TreeDataGrid>`
936
+ * always re-applies its own `data-testid` after spreading `getRowProps`'s
937
+ * result, specifically so a colliding key there can't accidentally break
938
+ * e2e selectors depending on the default convention. Use this prop instead
939
+ * when a consumer's existing test suite selects rows by a different,
940
+ * entity-specific convention that predates adopting `<TreeDataGrid>`.
941
+ */
942
+ getRowTestId?: (row: TRow) => string;
943
+ /** Per-row kebab menu — same `MenuItem` contract as `<DataGrid>`'s `rowActions`. */
944
+ rowActions?: MenuItem<TRow>[];
945
+ /**
946
+ * Enables row virtualization once the flattened (visible) row count
947
+ * exceeds this threshold. Defaults to 100 — small trees just render every
948
+ * row directly (simpler DOM, no virtualizer overhead); large/deep trees
949
+ * switch to `@tanstack/react-virtual`. Set to `0` to always virtualize, or
950
+ * `Infinity` to never.
951
+ */
952
+ virtualizeThreshold?: number;
953
+ /** Fixed row height estimate in pixels, used by the virtualizer. Defaults to 40. */
954
+ estimatedRowHeight?: number;
955
+ /** Max height of the scrollable table body in pixels. Required for virtualization to have a viewport to scroll within. Defaults to 480. */
956
+ maxBodyHeight?: number;
957
+ }
958
+
959
+ /**
960
+ * A lazy-loading tree grid: generalizes a consuming app's contract tree list
961
+ * (a from-scratch hand-rolled hierarchy table with no shared abstraction)
962
+ * into a reusable component on top of the same `ColumnDef<TRow>` system
963
+ * `<DataGrid>` uses. Deliberately does NOT support sorting or client-side
964
+ * filtering — that original hierarchy table has neither (hierarchy order
965
+ * comes from the backend; "filtering" there means asking the server for a
966
+ * differently-shaped tree, which is an app-level concern, not something
967
+ * this component can generalize).
968
+ *
969
+ * Row indentation + the expand/collapse chevron are prepended to whichever
970
+ * column's cell matches `treeColumnId` (defaults to `columns[0]`); every
971
+ * other column renders exactly like it would in `<DataGrid>`.
972
+ */
973
+ declare function TreeDataGrid<TRow>({ columns, data, treeColumnId, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, }: TreeDataGridProps<TRow>): ReactElement;
974
+
975
+ interface FlatTreeRow<TRow> {
976
+ row: TRow;
977
+ id: string;
978
+ depth: number;
979
+ hasChildren: boolean;
980
+ isExpanded: boolean;
981
+ }
982
+ /**
983
+ * Depth-first flatten of `roots` into the currently *visible* rows only —
984
+ * a collapsed node's descendants never enter the output at all, matching
985
+ * `ContractTreelist`'s `flattenContractTree` (the function this generalizes).
986
+ *
987
+ * `childrenOverride` (a node id -> loaded children map) takes priority over
988
+ * `getChildren(row)` when both exist, since it reflects the most recently
989
+ * fetched batch for a lazily-loaded node — `getChildren` may still return a
990
+ * stale/partial eager batch for the same node.
991
+ */
992
+ declare function flattenTree<TRow>(roots: TRow[], accessors: TreeAccessors<TRow>, expanded: Record<string, boolean>, childrenOverride: ReadonlyMap<string, TRow[]>): FlatTreeRow<TRow>[];
993
+
994
+ interface TreeStateController<TRow> {
995
+ expanded: Record<string, boolean>;
996
+ childrenMap: ReadonlyMap<string, TRow[]>;
997
+ loadingIds: ReadonlySet<string>;
998
+ /** Node id -> the message from its most recent failed `onLoadChildren` call. */
999
+ errorIds: ReadonlyMap<string, string>;
1000
+ toggleExpand: (row: TRow) => void;
1001
+ /** Re-attempts `onLoadChildren` for a node currently in `errorIds`. */
1002
+ retry: (row: TRow) => void;
1003
+ /** Expands every node from the roots down to (but not including) `level`, fetching lazy children along the way. */
1004
+ expandToLevel: (level: number) => Promise<void>;
1005
+ }
1006
+ interface UseTreeStateOptions<TRow> extends TreeAccessors<TRow> {
1007
+ data: TRow[];
1008
+ onLoadChildren?: (row: TRow) => Promise<TRow[]>;
1009
+ expanded?: Record<string, boolean>;
1010
+ onExpandedChange?: (expanded: Record<string, boolean>) => void;
1011
+ }
1012
+ /**
1013
+ * Owns everything `<TreeDataGrid>` needs beyond plain rendering: expand
1014
+ * state (controlled or internal, same pattern as `<ColumnSelector>`'s
1015
+ * `visibility`/`onVisibilityChange`), a per-node lazy-children cache, and
1016
+ * per-node loading/error tracking.
1017
+ *
1018
+ * Deliberately does NOT reset any of this when `data` changes identity —
1019
+ * unlike `ContractTreelist`, which resets `expanded`/`childrenMap` on every
1020
+ * filter change because filtering there reshapes the whole tree server-side.
1021
+ * This hook has no opinion on why `data` changed; a caller that wants a full
1022
+ * reset (e.g. switching to a different root entity entirely) should remount
1023
+ * `<TreeDataGrid>` via a `key` prop rather than rely on implicit magic here.
1024
+ */
1025
+ declare function useTreeState<TRow>({ data, getRowId, getChildren, hasChildren, onLoadChildren, expanded: controlledExpanded, onExpandedChange, }: UseTreeStateOptions<TRow>): TreeStateController<TRow>;
1026
+
1027
+ export { ActionsMenu, type BaseColumn, type BooleanColumn, BooleanFilter, type ClientProcessingResult, type ColumnAlign, type ColumnDef, type ColumnGroup, ColumnSelector, type ColumnSelectorProps, type ColumnVisibility, type CompositeFilterDescriptor, DataGrid, type DataGridProps, type DataSource, type DateColumn, DateRangeFilter, type DebouncedCallback, type EnumColumn, EnumFilter, type EnumOption, type FilterDescriptor, type FilterOperator, type FilterWidgetProps, type FlatTreeRow, type GridState, type GridStateController, type MenuItem, type MenuItemContext, type NumberColumn, NumberComparisonFilter, NumberHistogramFilter, type NumberHistogramFilterProps, NumberRangeFilter, type ResolvedMenuItem, type SortDescriptor, type StringColumn, StringFilter, type TreeAccessors, TreeDataGrid, type TreeDataGridProps, type TreeStateController, UNARY_OPERATORS, type UseTreeStateOptions, alignClassName, canHideColumn, canHideGroup, countVisible, defaultAlign, defaultFormat, evaluateFilter, facetedNumberValues, fieldKey, flattenTree, getColumnValue, groupColumns, isColumnVisible, isCompositeFilterDescriptor, isFilterDescriptor, isFilterable, isSortable, processClientData, renderDefaultFilterWidget, resolveMenuItems, storageKeyFor, useDebouncedCallback, useGridState, useTreeState };