@jielga/tmdatagrid 2.2.0 → 2.4.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 (50) hide show
  1. package/dist/index.d.ts +302 -11
  2. package/dist/index.js +2338 -2097
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/docs/columns.md +9 -1
  6. package/docs/components.md +1 -0
  7. package/docs/filtering.md +5 -1
  8. package/docs/grouping.md +1 -1
  9. package/docs/localization.md +3 -3
  10. package/docs/persistence.md +189 -2
  11. package/docs/row-details.md +5 -0
  12. package/docs/row-interaction.md +1 -1
  13. package/docs/row-styling.md +17 -0
  14. package/docs/typed-deep-links.md +169 -0
  15. package/docs/use-tm-data-grid.md +6 -2
  16. package/package.json +1 -1
  17. package/skills/appearance/SKILL.md +1 -1
  18. package/skills/appearance/references/labels.md +3 -3
  19. package/skills/cell-selection/SKILL.md +1 -1
  20. package/skills/columns/SKILL.md +6 -3
  21. package/skills/columns/references/columns-api.md +1 -0
  22. package/skills/data/SKILL.md +1 -1
  23. package/skills/editing/SKILL.md +1 -1
  24. package/skills/filtering/SKILL.md +26 -13
  25. package/skills/getting-started/SKILL.md +2 -2
  26. package/skills/grouping/SKILL.md +2 -2
  27. package/skills/migrating-to-2/SKILL.md +1 -1
  28. package/skills/options/SKILL.md +105 -10
  29. package/skills/options/references/typed-links.md +134 -0
  30. package/skills/rows/SKILL.md +18 -9
  31. package/skills/server-side/SKILL.md +1 -1
  32. package/skills/testing/SKILL.md +1 -1
  33. package/skills/testing-components/SKILL.md +1 -1
  34. package/skills/testing-editing/SKILL.md +1 -1
  35. package/src/components/TMDataGridFilterPills.module.css +9 -7
  36. package/src/components/TMDataGridFilterPills.tsx +15 -1
  37. package/src/components/TMDataGridGroupColumn.tsx +19 -4
  38. package/src/components/TMDataGridHeaderFilterRow.tsx +7 -1
  39. package/src/components/TMDataGridTable.module.css +15 -6
  40. package/src/components/TMDataGridTable.tsx +23 -11
  41. package/src/components/sticky.module.css +27 -7
  42. package/src/core/filterControls.ts +8 -0
  43. package/src/core/filterOperators.ts +54 -18
  44. package/src/core/grouping.ts +11 -3
  45. package/src/core/labels.ts +4 -3
  46. package/src/core/persistence.ts +330 -26
  47. package/src/core/urlState.ts +439 -0
  48. package/src/core/valueText.ts +46 -0
  49. package/src/index.ts +14 -0
  50. package/src/useTMDataGrid.tsx +145 -21
@@ -0,0 +1,134 @@
1
+ # Typed links
2
+
3
+ Reference for the `options` skill: `createTMDataGridUrlState`, what a link
4
+ may name, the definition's members, and the TanStack Router setup.
5
+
6
+ `createTMDataGridUrlState({ key, columns, slices? })` defines a grid's deep
7
+ links from its columns. `columns` is the array the grid renders, built with
8
+ `createTMDataGridColumnHelper`; each helper column carries its id, `meta.type`,
9
+ value type, `meta.filter.operators` and its `enableColumnFilter`,
10
+ `enableSorting` and `meta.filter.url` switches in its type only
11
+ (`TMDataGridColumnInfo`, read with `TMDataGridColumnInfoOf`; the brand type is
12
+ `TMDataGridTypedColumn`).
13
+
14
+ ```tsx
15
+ const columns = columnHelper.columns([
16
+ columnHelper.accessor("department", { header: "Department", meta: { type: "select" } }),
17
+ columnHelper.accessor("salary", { header: "Salary", meta: { type: "number" } }),
18
+ columnHelper.accessor("email", { header: "Email", enableColumnFilter: false }),
19
+ ]);
20
+
21
+ export const employeesUrl = createTMDataGridUrlState({
22
+ key: "employees",
23
+ columns,
24
+ slices: ["columnFilters", "sorting"],
25
+ });
26
+
27
+ const persist = { urlKey: employeesUrl.urlKey } satisfies TMDataGridPersistence;
28
+
29
+ const href = `/employees?${employeesUrl.searchParams({
30
+ filters: { department: { operator: "isAnyOf", value: ["Design"] } },
31
+ sorting: [{ id: "salary", desc: true }],
32
+ })}`;
33
+ ```
34
+
35
+ What a link may name (`TMDataGridUrlInput`); anything else is a type error:
36
+
37
+ - `filters` (`TMDataGridUrlFiltersInput`): only columns whose
38
+ `enableColumnFilter` is not `false` and `meta.filter.url` is not `false`.
39
+ With no such column, `filters` accepts no key at all.
40
+ - The operator: one the column's `meta.type` offers, narrowed by
41
+ `meta.filter.operators`.
42
+ - The value: a `number` for a number column (`between` takes
43
+ `[number | null, number | null]`, `null` leaving that end open); a `Date` or a
44
+ `YYYY-MM-DD` string for a date column (`TMDataGridDateInput`; a `Date` is
45
+ written as its local calendar day, a longer string is cut to 10 characters);
46
+ a `boolean` for a boolean column; a `string` for a string column; an array of
47
+ the column's own values (the element type for an array value) for `isAnyOf` /
48
+ `isNoneOf`; no `value` for `isEmpty` / `isNotEmpty`.
49
+ - `sorting` (`TMDataGridUrlSortableId`): only columns whose `enableSorting` is
50
+ not `false`. `desc` defaults to `false`.
51
+ - `globalFilter` and `pagination` (`{ pageIndex, pageSize }`): only when the
52
+ definition carries those slices. `slices` defaults to every data slice.
53
+ - A column with an accessor function is named by its `id`; without an `id` it
54
+ cannot be named. A dotted accessor key is named with underscores
55
+ (`address_city`).
56
+ - Only literal values count: `enableSorting: isAdmin` reads as not sortable.
57
+ Columns built without the helper offer nothing. A table-level
58
+ `enableColumnFilters` or `enableSorting` is not seen.
59
+
60
+ The definition (`TMDataGridUrlState`):
61
+
62
+ | Member | Type | Description |
63
+ | --- | --- | --- |
64
+ | `key` | `string` | The URL search param. |
65
+ | `slices` | `TMDataGridDataSlice[]` | The data slices the param carries. Default: every data slice. |
66
+ | `urlKey` | `TMDataGridPersistKey` | For `persist.urlKey`: the key, or `[key, slices]` when `slices` is set. |
67
+ | `searchParams(input, { serialize? })` | `URLSearchParams` | The link as search params, encoded with `serialize` (default `JSON.stringify`, as the grid's default). Empty when the input sets nothing. Pass the `serialize` given to `persist`, if any. |
68
+ | `search(input)` | `{ [key]: TMDataGridUrlPayload \| undefined }` | The link as a router search object. |
69
+ | `payload(input)` | `TMDataGridUrlPayload \| undefined` | The param's value in the grid's state shape (filters as `{ id, value: { operator, value } }` with string values), or `undefined` when the input sets nothing. |
70
+ | `validateSearch(search)` | `{ [key]?: TMDataGridUrlPayload }` | A route search validator: keeps the param when it is a plain object, drops anything else. The grid checks each slice itself on read. |
71
+ | `persist(storage?)` | `{ urlKey, urlStorage? }` | Spread into `persist`. Without `storage`, only `urlKey`, and the grid uses the browser's URL. |
72
+
73
+ `persist(storage)` takes a `TMDataGridUrlSearchStorage`,
74
+ `{ get(name): unknown; set(name, value: TMDataGridUrlPayload | undefined): void }`,
75
+ for a router that keeps objects in its search and encodes them itself, such as
76
+ TanStack Router. `get` returns the param's value or `undefined` (a string value
77
+ is passed to the grid unchanged); `set` receives the payload object, or
78
+ `undefined` to remove the param. The bridge converts with JSON, so leave
79
+ `serialize` and `deserialize` at their defaults with it.
80
+
81
+ ```tsx
82
+ // TanStack Router: the route validates, the grid writes through the router.
83
+ const employeesRoute = createRoute({
84
+ getParentRoute: () => rootRoute,
85
+ path: "/employees",
86
+ validateSearch: employeesUrl.validateSearch,
87
+ component: EmployeesPage,
88
+ });
89
+
90
+ function EmployeesPage() {
91
+ const router = useRouter();
92
+ const persist = useMemo(
93
+ () =>
94
+ employeesUrl.persist({
95
+ get: (name) => (router.state.location.search as Record<string, unknown>)[name],
96
+ set: (name, value) =>
97
+ void router.navigate({
98
+ to: ".",
99
+ search: (prev: Record<string, unknown>) => ({ ...prev, [name]: value }),
100
+ replace: true,
101
+ }),
102
+ }),
103
+ [router],
104
+ );
105
+ const grid = useTMDataGrid({ data, columns, persist });
106
+ // ...
107
+ }
108
+
109
+ <Link
110
+ to="/employees"
111
+ search={employeesUrl.search({
112
+ filters: { salary: { operator: "between", value: [40_000, 60_000] } },
113
+ })}
114
+ />;
115
+ ```
116
+
117
+ - A route with its own search params spreads the validator:
118
+ `validateSearch: (s) => ({ ...employeesUrl.validateSearch(s), tab: ... })`.
119
+ - With `createHashHistory()` the param sits after the `#` with the rest of the
120
+ router's search.
121
+ - For URLs without percent escapes, give the router `parseSearch:
122
+ parseSearchWith(parse)` from `jsurl2`, and a `stringifySearch` that encodes
123
+ each object value with JSURL2's `stringify` and joins the pairs with
124
+ `encodeURIComponent`. TanStack Router's own `stringifySearchWith(stringify)`
125
+ joins them with `URLSearchParams`, which escapes JSURL2's `(`, `)`, `~` and
126
+ `!` again. JSURL2's `parse` also reads JSON, so older links still open. The
127
+ grid is unaffected. The full `stringifySearch` is on the
128
+ `typed-deep-links.md` docs page.
129
+ - The grid reads its param once, on mount. A link to the route the grid is
130
+ already on changes the URL but not the grid; link from another route, or
131
+ remount the grid on that navigation.
132
+
133
+ Source: `packages/tmdatagrid/docs/persistence.md` (Typed links) and
134
+ `packages/tmdatagrid/docs/typed-deep-links.md`.
@@ -8,16 +8,16 @@ description: >
8
8
  reading a selection through the table store, onRowClick / onCellClick /
9
9
  onCellDoubleClick / onCellContextMenu, the renderRowContextMenu slot with its
10
10
  internalItems handback, renderColumnMenuItems and rowContextMenuProps, per-row
11
- styling with rowStyle, rowClassName, striped and the --row-bg rule, the row
12
- details panel through renderDetails, detailsColumnPosition and
13
- DETAILS_COLUMN_ID, row pinning with enableRowPinning and row.pin, and the
14
- row-number gutter through enableRowNumbers. Load when selecting rows, reacting
15
- to a click, opening a detail panel, colouring rows by their data, pinning rows
16
- to an edge, or numbering them.
11
+ styling with rowStyle, rowClassName, striped, highlightOnHover and the
12
+ --row-bg rule, the row details panel through renderDetails,
13
+ detailsColumnPosition and DETAILS_COLUMN_ID, row pinning with enableRowPinning
14
+ and row.pin, and the row-number gutter through enableRowNumbers. Load when
15
+ selecting rows, reacting to a click, opening a detail panel, colouring rows by
16
+ their data, pinning rows to an edge, or numbering them.
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.2.0'
20
+ library_version: '2.4.0'
21
21
  sources:
22
22
  - 'Jielga/react:packages/tmdatagrid/docs/row-selection.md'
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/row-interaction.md'
@@ -213,8 +213,15 @@ them and they stop showing. `striped` follows position in the view, so it
213
213
  survives sorting and filtering rather than sticking to records, and pinned rows
214
214
  are not striped.
215
215
 
216
+ The hover tint follows the gestures a row answers: a row takes it under
217
+ `selectionMode: "row"` or a highlight mode, `onRowClick` or
218
+ `renderRowContextMenu`, and a group row never does. A grid with none of those
219
+ shows no hover tint. `highlightOnHover` on the Table overrides that: `true`
220
+ tints every row, `false` none.
221
+
216
222
  Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
217
- `data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
223
+ `data-grouped`, `data-depth`, `data-context-menu`, `data-highlight-on-hover`
224
+ and `data-row-id`, so a
218
225
  stylesheet can target any of it without a callback. The boolean attributes are
219
226
  present, with the value `"true"`, only while they apply - `data-grouped` only
220
227
  on group rows - so `[data-grouped]` and `[data-grouped="true"]` are
@@ -247,7 +254,9 @@ Which rows are open is TanStack's own `expanded` state, so anything can open
247
254
  one - `row.toggleExpanded()` from a menu item, `initialState.expanded`,
248
255
  `table.toggleAllRowsExpanded()` - and it persists as a `data` slice. The panel
249
256
  is a cell spanning the row, not a row: `aria-rowcount` still counts records, and
250
- a click inside it does not select the row underneath. Group rows have no panel;
257
+ a click inside it does not select the row underneath. It has no padding: content
258
+ fills the row edge to edge, and content that should line up with the cell text
259
+ pads itself with `padding: var(--dg-padding)`. Group rows have no panel;
251
260
  expanding one opens its children.
252
261
 
253
262
  Rows are virtualized. A row that leaves the viewport and the `overscan`
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.2.0'
18
+ library_version: '2.4.0'
19
19
  sources:
20
20
  - 'Jielga/react:packages/tmdatagrid/docs/server-side.md'
21
21
  - 'Jielga/react:packages/tmdatagrid/docs/server-query.md'
@@ -13,7 +13,7 @@ description: >
13
13
  metadata:
14
14
  type: core
15
15
  library: '@jielga/tmdatagrid'
16
- library_version: '2.2.0'
16
+ library_version: '2.4.0'
17
17
  sources:
18
18
  - 'Jielga/react:packages/tmdatagrid/docs/testing.md'
19
19
  - 'Jielga/react:playwright/support/DataGrid.ts'
@@ -12,7 +12,7 @@ description: >
12
12
  metadata:
13
13
  type: core
14
14
  library: '@jielga/tmdatagrid'
15
- library_version: '2.2.0'
15
+ library_version: '2.4.0'
16
16
  sources:
17
17
  - 'Jielga/react:packages/tmdatagrid/docs/testing.md'
18
18
  - 'Jielga/react:packages/tmdatagrid/test/gallery/main.tsx'
@@ -12,7 +12,7 @@ description: >
12
12
  metadata:
13
13
  type: core
14
14
  library: '@jielga/tmdatagrid'
15
- library_version: '2.2.0'
15
+ library_version: '2.4.0'
16
16
  sources:
17
17
  - 'Jielga/react:packages/tmdatagrid/docs/testing.md'
18
18
  - 'Jielga/react:packages/tmdatagrid/docs/editing.md'
@@ -13,13 +13,15 @@
13
13
  shape readable against any surface. Scoped through `.filterPills` for the
14
14
  specificity to beat Mantine's own rule whichever stylesheet loads last. */
15
15
  .filterPills .pill {
16
- background-color: light-dark(
17
- var(--mantine-color-gray-1),
18
- var(--mantine-color-dark-5)
19
- );
20
- color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
21
- border: 1px solid
22
- light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3));
16
+ background-color: var(--mantine-color-gray-1);
17
+ color: var(--mantine-color-black);
18
+ border: 1px solid var(--mantine-color-gray-3);
19
+ }
20
+
21
+ :where([data-mantine-color-scheme="dark"]) .filterPills .pill {
22
+ background-color: var(--mantine-color-dark-5);
23
+ color: var(--mantine-color-dark-0);
24
+ border-color: var(--mantine-color-dark-3);
23
25
  }
24
26
 
25
27
  /* The label is a button so a pill can be clicked back open, but it must read as
@@ -2,6 +2,7 @@ import { Box, type BoxProps, Button, Pill, UnstyledButton } from "@mantine/core"
2
2
  import type { RowData } from "@tanstack/react-table";
3
3
  import { useSelector } from "@tanstack/react-store";
4
4
  import classes from "./TMDataGridFilterPills.module.css";
5
+ import { resolveColumnOptions } from "../core/columnOptions";
5
6
  import { getColumnLabel, getColumnType } from "../core/columnUtils";
6
7
  import {
7
8
  formatFilterLabel,
@@ -9,7 +10,12 @@ import {
9
10
  isTMDataGridFilterValue,
10
11
  } from "../core/filterOperators";
11
12
  import type { TMDataGridSize } from "../core/sizes";
12
- import { openColumnFilter, type TMDataGridApi } from "../useTMDataGrid";
13
+ import type { TMDataGridRowData } from "../TMDataGridContext";
14
+ import {
15
+ openColumnFilter,
16
+ type TMDataGridApi,
17
+ type TMDataGridTable,
18
+ } from "../useTMDataGrid";
13
19
 
14
20
  /** Mantine's style props (`mb="sm"`, `hiddenFrom="sm"`) are set on the wrapper. */
15
21
  export type TMDataGridFilterPillsProps<TData extends RowData> = BoxProps & {
@@ -55,6 +61,9 @@ export function TMDataGridFilterPills<TData extends RowData>({
55
61
  ...others
56
62
  }: TMDataGridFilterPillsProps<TData>) {
57
63
  const { table, labels } = api;
64
+ // The options readers take the row-type-erased table, like the rest of the
65
+ // chrome does through context.
66
+ const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
58
67
  const columnFilters = useSelector(
59
68
  table.store,
60
69
  (state) => state.columnFilters,
@@ -76,6 +85,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
76
85
  <Pill.Group size={size}>
77
86
  {activeFilters.map((filter) => {
78
87
  const column = table.getColumn(filter.id);
88
+ const erasedColumn = erased.getColumn(filter.id);
79
89
  const label = column ? getColumnLabel(column) : filter.id;
80
90
  const value = filter.value;
81
91
  // isFilterActive already vouched for the shape; this narrows it.
@@ -125,6 +135,10 @@ export function TMDataGridFilterPills<TData extends RowData>({
125
135
  type: column ? getColumnType(column) : "string",
126
136
  filter: value,
127
137
  operatorLabels: labels.operators,
138
+ options: erasedColumn
139
+ ? resolveColumnOptions({ table: erased, column: erasedColumn })
140
+ : undefined,
141
+ labels,
128
142
  })}
129
143
  </UnstyledButton>
130
144
  </Pill>
@@ -8,7 +8,8 @@ import { useSelector } from "@tanstack/react-store";
8
8
  import { shallow } from "@tanstack/store";
9
9
  import classes from "./TMDataGridGroupColumn.module.css";
10
10
  import { ChevronRightIcon } from "./icons";
11
- import { getColumnLabel } from "../core/columnUtils";
11
+ import { resolveColumnOptions } from "../core/columnOptions";
12
+ import { getColumnLabel, getColumnType } from "../core/columnUtils";
12
13
  import { formatGroupValue, getGroupDataRows } from "../core/grouping";
13
14
  import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
14
15
 
@@ -30,7 +31,7 @@ export function GroupCell<TData extends RowData>({
30
31
  }: {
31
32
  row: Row<TMDataGridFeatures, TData>;
32
33
  }) {
33
- const { labels } = useTMDataGridContext();
34
+ const { table, labels } = useTMDataGridContext();
34
35
  const tabIndex = useBodyControlTabIndex();
35
36
  const expanded = useSelector(row.table.store, () => row.getIsExpanded());
36
37
 
@@ -38,7 +39,21 @@ export function GroupCell<TData extends RowData>({
38
39
  // the indent alone is what places them under their group.
39
40
  if (!row.getIsGrouped()) return null;
40
41
 
41
- const label = formatGroupValue(row.groupingValue, labels.blankGroupValue);
42
+ // `groupingValue` is TanStack's map key, the value already turned into a
43
+ // string: `true` arrives as "true", a Date as its English timestamp, `null`
44
+ // as "null". The first record's grouping value is the value itself.
45
+ const columnId = row.groupingColumnId;
46
+ const column = columnId === undefined ? undefined : table.getColumn(columnId);
47
+ const dataRows = getGroupDataRows(row);
48
+ const value =
49
+ columnId === undefined || dataRows[0] === undefined
50
+ ? row.groupingValue
51
+ : dataRows[0].getGroupingValue(columnId);
52
+ const label = formatGroupValue(value, labels.blankGroupValue, {
53
+ type: column ? getColumnType(column) : undefined,
54
+ options: column ? resolveColumnOptions({ table, column }) : undefined,
55
+ labels,
56
+ });
42
57
 
43
58
  return (
44
59
  <UnstyledButton
@@ -68,7 +83,7 @@ export function GroupCell<TData extends RowData>({
68
83
  {/* Data rows, not direct children and not `getLeafRows()`: a nested group
69
84
  counts the records under it, never the sub-groups in between. */}
70
85
  <span className={classes.groupCount}>
71
- ({getGroupDataRows(row).length})
86
+ ({dataRows.length})
72
87
  </span>
73
88
  </UnstyledButton>
74
89
  );
@@ -268,7 +268,13 @@ export function TMDataGridHeaderFilterRow({
268
268
  <div
269
269
  key={header.id}
270
270
  role="columnheader"
271
- aria-label={labels.filterOn(getColumnLabel(column))}
271
+ // Named only where there is a filter: a generated lane or a column
272
+ // that does not filter holds no control to name.
273
+ aria-label={
274
+ column.getCanFilter()
275
+ ? labels.filterOn(getColumnLabel(column))
276
+ : undefined
277
+ }
272
278
  data-dg-part="header-filter-cell"
273
279
  data-column-id={column.id}
274
280
  data-control-column={isControlColumn(column.id) || undefined}
@@ -84,7 +84,13 @@
84
84
  --row-bg: var(--dg-row-new-bg, var(--mantine-color-body));
85
85
  }
86
86
 
87
- .bodyRow:hover {
87
+ /* The hover tint, on the rows that carry `data-highlight-on-hover` - by default
88
+ the rows that answer a click or a right-click, see the Table's
89
+ `highlightOnHover`. `:where()` adds no specificity, so the rule stays at the
90
+ weight of the state rules around it: declared after the stripe and the entry
91
+ tint, which it wins, and before the selection and highlight rules, which win
92
+ over it. */
93
+ .bodyRow:where([data-highlight-on-hover="true"]):hover {
88
94
  --row-bg: var(--mantine-color-default-hover);
89
95
  }
90
96
 
@@ -320,7 +326,9 @@
320
326
  /* Base and tint, exactly as .bodyCell paints them. */
321
327
  background-color: var(--mantine-color-body);
322
328
  background-image: linear-gradient(var(--row-bg), var(--row-bg));
323
- padding: var(--dg-padding);
329
+ /* No padding: the panel is the user's content, and a nested grid or a tab
330
+ strip wants the full width. Content that should line up with the cell
331
+ text pads itself with var(--dg-padding). */
324
332
  /* The row above may be the selection control, which turns text selection off
325
333
  for everything inside it. A panel is content, so it takes both back. */
326
334
  user-select: text;
@@ -477,14 +485,15 @@
477
485
  the browser's default so the colour is the grid's to state - override with
478
486
  `--dg-match-highlight-bg` on the grid. */
479
487
  .matchHighlight {
480
- background-color: var(
481
- --dg-match-highlight-bg,
482
- light-dark(var(--mantine-color-yellow-2), rgba(250, 176, 5, 0.35))
483
- );
488
+ background-color: var(--dg-match-highlight-bg, var(--mantine-color-yellow-2));
484
489
  color: inherit;
485
490
  border-radius: 2px;
486
491
  }
487
492
 
493
+ :where([data-mantine-color-scheme="dark"]) .matchHighlight {
494
+ background-color: var(--dg-match-highlight-bg, rgba(250, 176, 5, 0.35));
495
+ }
496
+
488
497
  /* The summary row - one sticky row of column footers along the bottom edge,
489
498
  rendered only while at least one column defines `footer`. Same subgrid
490
499
  placement as the header, so its cells sit exactly on the body's tracks.
@@ -90,7 +90,7 @@ import {
90
90
  resolveRowSelectionClick,
91
91
  } from "../core/rowSelection";
92
92
  import { readPinnedRows } from "../core/rowPinning";
93
- import { isFilterActive, isTMDataGridFilterValue } from "../core/filterOperators";
93
+ import { filterNarrowsRows } from "../core/filterOperators";
94
94
  import { buildMatchNeedles, findMatchRanges } from "../core/matchHighlight";
95
95
  import { ROW_NUMBER_COLUMN_ID } from "./TMDataGridRowNumberColumn";
96
96
  import { SearchIcon } from "./icons";
@@ -631,6 +631,14 @@ export type TMDataGridTableProps<TData extends RowData> = {
631
631
  * stay put while the virtualizer mounts and unmounts rows.
632
632
  */
633
633
  striped?: boolean;
634
+ /**
635
+ * Whether a body row takes the hover tint under the pointer. Unset, a row
636
+ * takes it when the row itself answers a gesture: `selectionMode: "row"`,
637
+ * a highlight mode, `onRowClick` or `renderRowContextMenu`. Group rows sit
638
+ * out those gestures and so sit out the tint too. `true` tints every row,
639
+ * `false` none. The row carries `data-highlight-on-hover` while it applies.
640
+ */
641
+ highlightOnHover?: boolean;
634
642
  /**
635
643
  * Called when the scroll *arrives* at an edge - once per arrival, not per
636
644
  * scroll event, and not on mount (the grid starts at the top-left edge).
@@ -751,6 +759,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
751
759
  rowClassName,
752
760
  rowStyle,
753
761
  striped = false,
762
+ highlightOnHover,
754
763
  onScrollToTop,
755
764
  onScrollToBottom,
756
765
  onScrollToLeft,
@@ -1480,18 +1489,12 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
1480
1489
  const isEmpty = rows.length === 0 && pinnedRowCount === 0;
1481
1490
  // Which emptiness this is. A column filter counts by the rule the pills, the
1482
1491
  // header indicator and the page reset use, so the grid has one definition
1483
- // of "active": an envelope counts once `isFilterActive` says so - the
1484
- // half-typed `{ operator, value: "" }` the filter panel seeds filters
1485
- // nothing - and a value that is not an envelope always counts, since a raw
1486
- // value written by consumer code narrows the rows as far as the grid can
1487
- // tell. The body already subscribes to the whole table store, so these
1488
- // reads are reactive.
1492
+ // of "active" - see `filterNarrowsRows`. The half-typed
1493
+ // `{ operator, value: "" }` the filter panel seeds filters nothing. The body
1494
+ // already subscribes to the whole table store, so these reads are reactive.
1489
1495
  const emptyGlobalFilter = table.store.state.globalFilter;
1490
1496
  const hasActiveFilters =
1491
- table.store.state.columnFilters.some(
1492
- (entry) =>
1493
- !isTMDataGridFilterValue(entry.value) || isFilterActive(entry.value),
1494
- ) ||
1497
+ table.store.state.columnFilters.some(filterNarrowsRows) ||
1495
1498
  (typeof emptyGlobalFilter === "string"
1496
1499
  ? emptyGlobalFilter.trim() !== ""
1497
1500
  : emptyGlobalFilter != null);
@@ -2833,6 +2836,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
2833
2836
  ) => {
2834
2837
  const isGroupRow = row.getIsGrouped();
2835
2838
  const isInteractive = rowGesturesFor(row);
2839
+ // The hover tint promises that the row answers the pointer, so
2840
+ // by default it follows the gestures the row takes: the ones
2841
+ // behind `isInteractive`, and the context menu - which a group
2842
+ // row sits out as well, see the row's onContextMenu below.
2843
+ const highlightsOnHover =
2844
+ highlightOnHover ??
2845
+ (isInteractive ||
2846
+ (renderRowContextMenu !== undefined && !isGroupRow));
2836
2847
  const rowCells = [
2837
2848
  ...row.getStartVisibleCells(),
2838
2849
  ...row.getCenterVisibleCells(),
@@ -2940,6 +2951,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
2940
2951
  data-selects-on-click={
2941
2952
  (selectsOnRowClick && !isGroupRow) || undefined
2942
2953
  }
2954
+ data-highlight-on-hover={highlightsOnHover || undefined}
2943
2955
  aria-selected={
2944
2956
  features.rowSelection ? row.getIsSelected() : undefined
2945
2957
  }
@@ -5,9 +5,11 @@
5
5
  A box-shadow is drawn per cell, and its blur fades in at each cell's own top
6
6
  and bottom edge - stacked down a column that reads as banding at every row
7
7
  seam. Instead: a gradient pseudo element pinned to `top: 0; bottom: 0` so
8
- consecutive cells butt together into one unbroken band. Mantine sets
9
- `color-scheme` on the root, so light-dark() resolves without a second
10
- selector.
8
+ consecutive cells butt together into one unbroken band.
9
+
10
+ The dark scheme is a second rule under Mantine's `data-mantine-color-scheme`
11
+ attribute, not the `light-dark` function: scripts/package-stylesheets.test.mjs
12
+ says why.
11
13
 
12
14
  No border. A 1px line the colour of the grid's own borders drew a hard rule
13
15
  between the pinned lanes and the data, so the pinned columns read as a
@@ -31,8 +33,8 @@
31
33
  left: 100%;
32
34
  background: linear-gradient(
33
35
  to right,
34
- light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.26)),
35
- light-dark(rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.09)) 45%,
36
+ rgba(0, 0, 0, 0.07),
37
+ rgba(0, 0, 0, 0.025) 45%,
36
38
  transparent
37
39
  );
38
40
  }
@@ -41,8 +43,26 @@
41
43
  right: 100%;
42
44
  background: linear-gradient(
43
45
  to left,
44
- light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.26)),
45
- light-dark(rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.09)) 45%,
46
+ rgba(0, 0, 0, 0.07),
47
+ rgba(0, 0, 0, 0.025) 45%,
48
+ transparent
49
+ );
50
+ }
51
+
52
+ :where([data-mantine-color-scheme="dark"]) .stickyLeft::after {
53
+ background: linear-gradient(
54
+ to right,
55
+ rgba(0, 0, 0, 0.26),
56
+ rgba(0, 0, 0, 0.09) 45%,
57
+ transparent
58
+ );
59
+ }
60
+
61
+ :where([data-mantine-color-scheme="dark"]) .stickyRight::after {
62
+ background: linear-gradient(
63
+ to left,
64
+ rgba(0, 0, 0, 0.26),
65
+ rgba(0, 0, 0, 0.09) 45%,
46
66
  transparent
47
67
  );
48
68
  }
@@ -106,4 +106,12 @@ export type TMDataGridColumnFilterOptions = {
106
106
  * See {@link TMDataGridFilterControlComponent}.
107
107
  */
108
108
  control?: TMDataGridFilterControlComponent;
109
+ /**
110
+ * Whether this column's filter goes into the `persist.urlKey` search param.
111
+ * Defaults to `true`. `false` keeps it out of the param, on read and on
112
+ * write. Only has an effect when `urlKey` carries `columnFilters`, and since such
113
+ * a slice is not stored under `dataKey` either, the filter is then not
114
+ * persisted anywhere.
115
+ */
116
+ url?: boolean;
109
117
  };