@jielga/tmdatagrid 1.0.0 → 1.0.2

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 (78) hide show
  1. package/README.md +8 -8
  2. package/dist/index.d.ts +225 -225
  3. package/dist/index.js +58 -50
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +3 -2
  7. package/skills/appearance/SKILL.md +322 -0
  8. package/skills/cell-selection/SKILL.md +240 -0
  9. package/skills/columns/SKILL.md +261 -86
  10. package/skills/data/SKILL.md +289 -0
  11. package/skills/editing/SKILL.md +492 -0
  12. package/skills/editing/references/editing-api.md +124 -0
  13. package/skills/editing/references/editors-and-validation.md +198 -0
  14. package/skills/filtering/SKILL.md +344 -0
  15. package/skills/getting-started/SKILL.md +48 -27
  16. package/skills/grouping/SKILL.md +264 -0
  17. package/skills/options/SKILL.md +31 -20
  18. package/skills/rows/SKILL.md +369 -0
  19. package/skills/rows/references/rows-api.md +117 -0
  20. package/skills/server-side/SKILL.md +7 -7
  21. package/skills/testing/SKILL.md +12 -12
  22. package/src/tmdatagrid/TMDataGridContext.ts +2 -2
  23. package/src/tmdatagrid/components/TMDataGrid.module.css +2 -2
  24. package/src/tmdatagrid/components/TMDataGrid.tsx +5 -5
  25. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +4 -4
  26. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +2 -2
  27. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +6 -6
  28. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
  29. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +4 -4
  30. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +6 -6
  31. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +6 -6
  32. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +2 -2
  33. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +1 -1
  34. package/src/tmdatagrid/components/TMDataGridFooter.module.css +1 -1
  35. package/src/tmdatagrid/components/TMDataGridFooter.tsx +12 -6
  36. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +1 -1
  37. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +5 -5
  38. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +8 -8
  39. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -12
  40. package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +4 -4
  41. package/src/tmdatagrid/components/TMDataGridSearch.tsx +5 -5
  42. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +8 -8
  43. package/src/tmdatagrid/components/TMDataGridTable.module.css +18 -18
  44. package/src/tmdatagrid/components/TMDataGridTable.tsx +116 -116
  45. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +5 -5
  46. package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +1 -1
  47. package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +2 -2
  48. package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +1 -1
  49. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
  50. package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +2 -2
  51. package/src/tmdatagrid/components/editors/editorShared.ts +3 -3
  52. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +1 -1
  53. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +1 -1
  54. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +1 -1
  55. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +2 -2
  56. package/src/tmdatagrid/components/sticky.module.css +8 -8
  57. package/src/tmdatagrid/core/autosize.ts +4 -4
  58. package/src/tmdatagrid/core/capabilities.ts +17 -17
  59. package/src/tmdatagrid/core/cellExport.ts +13 -13
  60. package/src/tmdatagrid/core/cellNavigation.ts +6 -6
  61. package/src/tmdatagrid/core/cellRange.ts +4 -4
  62. package/src/tmdatagrid/core/columnOptions.ts +2 -2
  63. package/src/tmdatagrid/core/columnOrdering.ts +2 -2
  64. package/src/tmdatagrid/core/columnUtils.ts +3 -3
  65. package/src/tmdatagrid/core/editEngine.ts +49 -49
  66. package/src/tmdatagrid/core/expanding.ts +5 -5
  67. package/src/tmdatagrid/core/filterControls.ts +5 -5
  68. package/src/tmdatagrid/core/filterOperators.ts +14 -14
  69. package/src/tmdatagrid/core/labels.ts +8 -8
  70. package/src/tmdatagrid/core/matchHighlight.ts +4 -4
  71. package/src/tmdatagrid/core/persistence.ts +8 -8
  72. package/src/tmdatagrid/core/quickSearch.ts +8 -8
  73. package/src/tmdatagrid/core/rowPinning.ts +3 -3
  74. package/src/tmdatagrid/core/rowSelection.ts +12 -12
  75. package/src/tmdatagrid/core/sizes.ts +1 -1
  76. package/src/tmdatagrid/core/summary.ts +3 -3
  77. package/src/tmdatagrid/useTMDataGrid.tsx +79 -79
  78. package/skills/features/SKILL.md +0 -352
@@ -0,0 +1,198 @@
1
+ # Editors and validation
2
+
3
+ Reference for the `editing` skill. Which control a cell opens, and what stops a
4
+ bad value being committed. Both follow from the column.
5
+
6
+ ## The built-in editors
7
+
8
+ `meta.type` picks one, `meta.options` feeds the two select editors from the same
9
+ declaration the filter panel reads.
10
+
11
+ | `meta.type` | Editor | Export |
12
+ | --- | --- | --- |
13
+ | `"string"` (default) | Text input | `TMDataGridStringEditor` |
14
+ | `"number"` | Number input | `TMDataGridNumberEditor` |
15
+ | `"boolean"` | Checkbox | `TMDataGridBooleanEditor` |
16
+ | `"date"` | Native `<input type="date">`, ISO `YYYY-MM-DD` | `TMDataGridDateEditor` |
17
+ | `"select"` | Searchable select, commits on pick under `"cell"` | `TMDataGridSelectEditor` |
18
+ | `"multiSelect"` | Multi-select, same source | `TMDataGridMultiSelectEditor` |
19
+
20
+ ```tsx
21
+ columnHelper.accessor("department", {
22
+ header: "Department",
23
+ meta: { type: "select", options: ["Engineering", "Sales", "Support"] },
24
+ });
25
+ ```
26
+
27
+ `meta.options` takes a static array, `"faceted"` (the distinct values present in
28
+ the data), or a function of the table, column and row.
29
+
30
+ ## The editor contract
31
+
32
+ `meta.editor` fills the same slot the built-ins fill. It is a **component**,
33
+ rendered as JSX, so hooks are legal inside it.
34
+
35
+ ```ts
36
+ type TMDataGridEditorArgs = {
37
+ field: TMDataGridEditField; // the live TanStack Form field
38
+ form: TMDataGridRowEditForm; // the whole row's form, for sibling fields
39
+ cell: Cell;
40
+ row: Row;
41
+ column: Column;
42
+ table: TMDataGridTable;
43
+ commit: () => Promise<boolean>; // what Enter would do
44
+ cancel: () => void; // what Escape would do
45
+ size: TMDataGridSize;
46
+ autoFocus: boolean; // take focus on mount
47
+ seedText?: string; // set when typing opened the editor
48
+ };
49
+ ```
50
+
51
+ Bind any control to `field` exactly as inside any TanStack Form:
52
+ `field.state.value`, `field.state.meta.errors`, `field.handleChange`,
53
+ `field.handleBlur`.
54
+
55
+ ```tsx
56
+ import { Slider } from "@mantine/core";
57
+ import type { TMDataGridEditorComponent } from "@jielga/tmdatagrid";
58
+
59
+ // Module scope. A new identity per render remounts the editor mid-edit.
60
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit, cancel }) => (
61
+ <Slider
62
+ value={Number(field.state.value ?? 0)}
63
+ min={0}
64
+ max={100_000}
65
+ step={1_000}
66
+ onChange={(next) => field.handleChange(next)}
67
+ onChangeEnd={() => void commit()}
68
+ onKeyDown={(event) => {
69
+ if (event.key === "Escape") cancel();
70
+ }}
71
+ />
72
+ );
73
+
74
+ columnHelper.accessor("salary", {
75
+ header: "Salary",
76
+ meta: { type: "number", editor: SalaryEditor },
77
+ });
78
+ ```
79
+
80
+ ## Wrapping a built-in
81
+
82
+ The built-ins take `TMDataGridEditorArgs` as their props, so a custom editor can
83
+ pass the whole object through and add around it rather than start over.
84
+
85
+ ```tsx
86
+ import { Group, Text } from "@mantine/core";
87
+ import {
88
+ TMDataGridNumberEditor,
89
+ TMDataGridSelectEditor,
90
+ TMDataGridStringEditor,
91
+ type TMDataGridEditorComponent,
92
+ } from "@jielga/tmdatagrid";
93
+
94
+ // Decorate one.
95
+ const SalaryEditor: TMDataGridEditorComponent = (args) => (
96
+ <Group gap={4} wrap="nowrap" w="100%">
97
+ <TMDataGridNumberEditor {...args} />
98
+ <Text size="xs" c="dimmed">
99
+ kr
100
+ </Text>
101
+ </Group>
102
+ );
103
+
104
+ // Or choose between them per row.
105
+ const DepartmentEditor: TMDataGridEditorComponent = (args) =>
106
+ args.row.original.status === "Terminated" ? (
107
+ <TMDataGridStringEditor {...args} />
108
+ ) : (
109
+ <TMDataGridSelectEditor {...args} />
110
+ );
111
+ ```
112
+
113
+ Pass `args` through whole. `field` is a live TanStack Form `FieldApi`, not a
114
+ plain object, so spreading or rebuilding it loses the binding its methods rely
115
+ on.
116
+
117
+ ## Field validation
118
+
119
+ `meta.validate` takes TanStack Form's field-validator vocabulary. A bare schema
120
+ (Zod, or any Standard Schema, or a plain function) means `{ onChange: it }`; the
121
+ object form takes every trigger Form defines.
122
+
123
+ ```tsx
124
+ import { z } from "zod";
125
+
126
+ // Bare schema: validates on change.
127
+ meta: { validate: z.string().min(2, "At least two characters") }
128
+
129
+ // Object form: pick the trigger.
130
+ meta: { validate: { onBlur: z.string().email("Not an email address") } }
131
+
132
+ // A plain function works too.
133
+ meta: { validate: ({ value }) => (value > 0 ? undefined : "Must be positive") }
134
+ ```
135
+
136
+ `normalizeFieldValidate(validate)` is exported for consumers building their own
137
+ column factories: it turns a bare schema into Form's validator shape.
138
+
139
+ ## Row validation
140
+
141
+ `rowValidators` is form-level, and where cross-field rules live.
142
+
143
+ ```tsx
144
+ const grid = useTMDataGrid({
145
+ data: employees,
146
+ columns,
147
+ getRowId: (row) => String(row.id),
148
+ editMode: "row",
149
+ rowValidators: {
150
+ onSubmit: z
151
+ .object({ salary: z.number().positive(), status: z.string() })
152
+ .refine((row) => row.status !== "Terminated" || row.salary === 0, {
153
+ message: "A terminated employee has no salary",
154
+ }),
155
+ },
156
+ onEditCommit,
157
+ });
158
+ ```
159
+
160
+ Issues with a path land on the matching column's cell; pathless issues land on
161
+ the row. A nested schema's issues follow the same rule, so a `address.city`
162
+ issue lands on the column whose `editField` is `"address.city"`.
163
+
164
+ Cross-field rules need a mode that commits the whole row at once. Under `"cell"`
165
+ each cell commits alone, so the rule is evaluated against the other column's
166
+ unedited value and can never pass. Use `"row"` or `"batch"`.
167
+
168
+ ## Server-side errors
169
+
170
+ `rowValidators.onSubmitAsync` returns TanStack Form's `{ form, fields }` shape,
171
+ so an API's field errors land on the right cells without translation.
172
+
173
+ ```tsx
174
+ rowValidators: {
175
+ onSubmitAsync: async ({ value }) => {
176
+ const response = await api.validateEmployee(value);
177
+ if (response.ok) return undefined;
178
+ return {
179
+ form: response.message,
180
+ fields: response.fieldErrors, // { salary: "Above the band ceiling" }
181
+ };
182
+ },
183
+ },
184
+ ```
185
+
186
+ A commit blocked by validation keeps the editor open with the message on the
187
+ input. A rejected `onEditCommit` keeps the draft too, with the error on the row.
188
+
189
+ ## Where the state shows
190
+
191
+ | Marker | Means |
192
+ | --- | --- |
193
+ | Blue cell corner | The field is dirty against its original value |
194
+ | Red cell corner | The field carries a validation error |
195
+ | Row error text | A pathless rule failed, or a commit was rejected |
196
+
197
+ The same information is readable from `edit.store`: `rows[rowId].dirtyFields`,
198
+ `rows[rowId].errorFields`, `rows[rowId].isSubmitting`.
@@ -0,0 +1,344 @@
1
+ ---
2
+ name: filtering
3
+ description: >
4
+ Narrow the rows a TMDataGrid shows. Covers the shared tmDataGrid filter
5
+ function and its {operator, value} model, the eighteen operators and which
6
+ meta.type offers each, meta.defaultFilterOperator, isFilterActive and the
7
+ half-typed filter, the filter panel, TMDataGrid.FilterButton,
8
+ TMDataGrid.FilterPills with its api prop, openColumnFilter, replacing a value
9
+ control with DgRangeSliderFilter / DgDateRangeFilter / DgAutocompleteFilter /
10
+ DgTriStateFilter or a meta.filterControl component, per-column filterFn, and
11
+ the quick search: TMDataGrid.Search, quickSearchMode fuzzy or contains,
12
+ fuzzyGlobalFilterFn, enableMatchHighlighting and enableGlobalFilter. Load when
13
+ adding filters, choosing operators, building a filter control, showing active
14
+ filters outside the grid, or wiring a search box.
15
+ metadata:
16
+ type: core
17
+ library: '@jielga/tmdatagrid'
18
+ library_version: '1.0.2'
19
+ sources:
20
+ - 'Jielga/TMDataGrid:src/docs/filtering.md'
21
+ - 'Jielga/TMDataGrid:src/docs/quick-search.md'
22
+ - 'Jielga/TMDataGrid:src/tmdatagrid/core/filterOperators.ts'
23
+ - 'Jielga/TMDataGrid:src/tmdatagrid/core/quickSearch.ts'
24
+ ---
25
+
26
+ # TMDataGrid - Filtering
27
+
28
+ Two independent narrowings: per-column filters built from an operator and a
29
+ value, and one quick-search box across every column. Both write standard
30
+ TanStack state, so a `manualFiltering` grid forwards either to a server.
31
+
32
+ ## How a filter is stored
33
+
34
+ All columns share one filter function. The operator lives **in the value**
35
+ rather than being selected through `filterFn`:
36
+
37
+ ```ts
38
+ type TMDataGridFilterValue = {
39
+ operator: TMDataGridFilterOperator;
40
+ value: string | ReadonlyArray<string>;
41
+ };
42
+ ```
43
+
44
+ The model is therefore plain JSON: dates as ISO `YYYY-MM-DD`, booleans as
45
+ `"true"` / `"false"`, and an array only under `isAnyOf` / `isNoneOf` (the set the
46
+ cell is tested against) and `between` (a `[min, max]` pair, an empty string
47
+ leaving that end open).
48
+
49
+ A filter with an empty value **stays in state** so the panel keeps showing its
50
+ row while the reader types. It matches every row, does not light the header
51
+ indicator and gets no pill. `isFilterActive(value)` is the test for that state -
52
+ presence in `columnFilters` is not.
53
+
54
+ ## Operators
55
+
56
+ `meta.type` selects which operators a column offers.
57
+
58
+ | Column type | Operators |
59
+ | --- | --- |
60
+ | `string` | `contains`, `equals`, `notEquals`, `startsWith`, `endsWith` |
61
+ | `number` | `equals`, `notEquals`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `between` |
62
+ | `date` | `equals`, `notEquals`, `before`, `after`, `onOrBefore`, `onOrAfter`, `between` |
63
+ | `boolean` | `equals`, `notEquals` |
64
+ | `select`, `multiSelect` | `isAnyOf`, `isNoneOf` |
65
+
66
+ Every type also offers `isEmpty` and `isNotEmpty`, which ignore the value input.
67
+
68
+ String comparisons are case-insensitive. Date comparisons are by calendar day,
69
+ and `equals` on a `date` column plays the role of "is". On a `multiSelect`
70
+ column, whose cells hold arrays, `isAnyOf` is an intersection test and `isNoneOf`
71
+ its complement. `between` is inclusive at both ends.
72
+
73
+ `meta.defaultFilterOperator` sets which one a fresh filter opens on, and must be
74
+ one of the type's own:
75
+
76
+ ```tsx
77
+ columnHelper.accessor("salary", {
78
+ header: "Salary",
79
+ meta: { type: "number", defaultFilterOperator: "between" },
80
+ });
81
+ ```
82
+
83
+ ## The panel and the pills
84
+
85
+ `TMDataGrid.FilterPanel` is column, operator and value rows under a "Filters"
86
+ header, above "Add filter" and "Clear all". It is rendered by
87
+ `TMDataGrid.Table`; Escape and a click outside close it, with `FilterButton`
88
+ exempt from the click-away so it stays a toggle. Closing only hides it - the
89
+ filters stay. **Clear all** drops every filter, half-typed ones included.
90
+
91
+ `TMDataGrid.FilterPills` takes the grid as an `api` prop rather than reading
92
+ context, so active filters can live in a page header or anywhere else:
93
+
94
+ ```tsx
95
+ import { TMDataGridFilterPills } from "@jielga/tmdatagrid";
96
+
97
+ <TMDataGridFilterPills api={grid} onPillClick={(columnId) => focus(columnId)} />;
98
+ ```
99
+
100
+ One pill per **active** filter - `First name: Sofia ✕` - where ✕ clears it and a
101
+ click on the label reopens the panel on its column. The label spells the
102
+ operator out unless it is the type's default: `Age is greater than 30`, but
103
+ `First name: Sofia`. `openColumnFilter(api, columnId)` does the same reopening
104
+ from anywhere, seeding an empty row if the column has none.
105
+
106
+ ## Replacing the value control
107
+
108
+ The built-in control follows the type and operator. Four alternatives ship as
109
+ named exports:
110
+
111
+ | Export | For | Renders |
112
+ | --- | --- | --- |
113
+ | `DgRangeSliderFilter` | `number` | Range slider seeded from the data's min/max, writing the `between` pair |
114
+ | `DgDateRangeFilter` | `date` | A From/To pair of native date inputs |
115
+ | `DgAutocompleteFilter` | `string` | Free text with faceted values as suggestions |
116
+ | `DgTriStateFilter` | `boolean` | All / Yes / No segments, All clearing the filter |
117
+
118
+ ```tsx
119
+ meta: {
120
+ type: "number",
121
+ filterControl: DgRangeSliderFilter,
122
+ defaultFilterOperator: "between",
123
+ }
124
+ ```
125
+
126
+ Pair the range-shaped ones with `defaultFilterOperator: "between"` so the filter
127
+ opens on them. For operators outside their shape every built-in falls back to
128
+ `TMDataGridFilterValueInput`, which is exported so a custom control can take the
129
+ same escape.
130
+
131
+ `meta.filterControl` is a **component**, rendered as JSX, so hooks are legal
132
+ inside. It is a **value-only contract**: it reads `operator` to shape itself and
133
+ writes the bare value through `onChange`, and the grid composes the stored
134
+ `{ operator, value }` around it. The column and operator dropdowns stay the
135
+ panel's.
136
+
137
+ ```tsx
138
+ import type { TMDataGridFilterControlComponent } from "@jielga/tmdatagrid";
139
+
140
+ // Module scope: a new identity per render remounts the control mid-typing.
141
+ const StatusFilter: TMDataGridFilterControlComponent = ({
142
+ operator,
143
+ value,
144
+ options,
145
+ onChange,
146
+ }) => (
147
+ <Chip.Group
148
+ multiple
149
+ value={Array.isArray(value) ? [...value] : []}
150
+ onChange={onChange}
151
+ >
152
+ {options.map((option) => (
153
+ <Chip key={option.value} value={option.value}>
154
+ {option.label}
155
+ </Chip>
156
+ ))}
157
+ </Chip.Group>
158
+ );
159
+
160
+ meta: { type: "select", filterControl: StatusFilter }
161
+ ```
162
+
163
+ `args.options` arrives pre-resolved for a column that declares `meta.options` or
164
+ is select-shaped; `args.table` is the escape hatch for a control that must reach
165
+ further.
166
+
167
+ To give a column its own *matching* rather than its own control, set `filterFn`
168
+ on the column definition. The grid only provides `"tmDataGrid"` as the default.
169
+
170
+ ## Quick search
171
+
172
+ `TMDataGrid.Search` is a debounced input writing TanStack's `globalFilter`. No
173
+ option turns it on: you render it or you do not.
174
+
175
+ ```tsx
176
+ <TMDataGrid.Toolbar>
177
+ <TMDataGrid.Search placeholder="Search employees" debounce={0} />
178
+ </TMDataGrid.Toolbar>
179
+ ```
180
+
181
+ Matching is **fuzzy by default**: typos and skipped characters are forgiven, and
182
+ while the search is the only thing narrowing the grid (no sort, no grouping) the
183
+ rows order by match quality, best first. That ordering is derived, never written
184
+ into `sorting` - no column claims `aria-sort`, nothing lands in the persisted
185
+ slices, and the reader's next sort click takes over just by existing.
186
+
187
+ `quickSearchMode: "contains"` restores plain substring matching. An explicit
188
+ `globalFilterFn` overrides both, and switches the rank ordering off with it.
189
+ `fuzzyGlobalFilterFn` is exported for a custom input over the same matching.
190
+
191
+ `enableMatchHighlighting: true` marks the matched slice of a cell's text, under
192
+ the quick search or a `contains` / `startsWith` / `endsWith` column filter. What
193
+ gets marked is the contiguous, case-insensitive occurrence, so a fuzzy
194
+ typo-match with no contiguous occurrence shows no highlight. It applies to
195
+ **default-rendered cells only**: a column with its own `cell` renderer opts out
196
+ by existing.
197
+
198
+ ## Turning it off
199
+
200
+ `enableColumnFilters: false` removes the panel, the button and the menu item;
201
+ `enableColumnFilter: false` on a column takes that column out.
202
+ `enableGlobalFilter: false` removes the search input, or on a column removes its
203
+ participation in the search. Generated lanes already opt out.
204
+
205
+ ## Common mistakes
206
+
207
+ ### CRITICAL Treating presence in `columnFilters` as an active filter
208
+
209
+ A half-typed filter stays in state on purpose, so the panel keeps its row while
210
+ the reader types. Counting entries reports filters that are narrowing nothing,
211
+ and forwarding them to a server sends `{ operator: "contains", value: "" }` as a
212
+ real constraint.
213
+
214
+ Wrong:
215
+
216
+ ```tsx
217
+ const activeCount = columnFilters.length;
218
+ ```
219
+
220
+ Correct:
221
+
222
+ ```tsx
223
+ import { isFilterActive } from "@jielga/tmdatagrid";
224
+
225
+ const active = columnFilters.filter((filter) => isFilterActive(filter.value));
226
+ ```
227
+
228
+ Source: `src/docs/filtering.md` (How a filter is stored).
229
+
230
+ ### HIGH Assuming `value` is always a string
231
+
232
+ `value` is a `[min, max]` pair under `between` and an array under `isAnyOf` /
233
+ `isNoneOf`. Code written against the single-string case compiles against the
234
+ union and fails at runtime on the first range filter.
235
+
236
+ Correct:
237
+
238
+ ```tsx
239
+ import {
240
+ operatorTakesArrayValue,
241
+ operatorTakesRangeValue,
242
+ } from "@jielga/tmdatagrid";
243
+
244
+ if (operatorTakesRangeValue(operator)) {
245
+ const [min, max] = value as ReadonlyArray<string>;
246
+ }
247
+ ```
248
+
249
+ Source: `src/tmdatagrid/core/filterOperators.ts`.
250
+
251
+ ### HIGH A numeric column with no `meta.type`
252
+
253
+ `getColumnType` defaults to `"string"`, so a numeric column that omits
254
+ `meta: { type: "number" }` offers only the string operators. `greaterThan` and
255
+ `between` never appear in the panel, and comparisons run as text - `"9"` sorts
256
+ above `"10"`.
257
+
258
+ Source: `src/docs/filtering.md` (Operators).
259
+
260
+ ### HIGH A filter control defined inside the component
261
+
262
+ `meta.filterControl` is rendered as JSX, so its identity is its component type.
263
+ An inline arrow is a new type every render, which remounts the control and loses
264
+ what was being typed into it.
265
+
266
+ Correct:
267
+
268
+ ```tsx
269
+ // Module scope, referenced by name.
270
+ const StatusFilter: TMDataGridFilterControlComponent = (args) => { /* … */ };
271
+
272
+ meta: { filterControl: StatusFilter }
273
+ ```
274
+
275
+ Source: `src/docs/filtering.md` (Writing your own).
276
+
277
+ ### MEDIUM Writing the whole filter from a custom control
278
+
279
+ The contract is value-only: `onChange` takes the bare value, and the grid
280
+ composes `{ operator, value }` around it. Passing an object writes a filter
281
+ whose `value` is itself an object, which no operator can match against.
282
+
283
+ Wrong:
284
+
285
+ ```tsx
286
+ onChange({ operator: "isAnyOf", value: picked });
287
+ ```
288
+
289
+ Correct:
290
+
291
+ ```tsx
292
+ onChange(picked);
293
+ ```
294
+
295
+ Source: `src/docs/filtering.md` (Writing your own).
296
+
297
+ ### MEDIUM Expecting match highlighting in a custom cell
298
+
299
+ The grid replicates the default value-to-string render with marks added, and
300
+ never rummages inside a custom renderer's output. A column with a `cell`
301
+ renderer shows no marks however `enableMatchHighlighting` is set. Equality
302
+ operators highlight nothing either.
303
+
304
+ Source: `src/docs/quick-search.md` (Match highlighting).
305
+
306
+ ### MEDIUM Expecting fuzzy ranking to survive a sort
307
+
308
+ The rank ordering applies only while the search is the sole narrowing. Any sort
309
+ or grouping takes over, by design - the ordering is derived and never written
310
+ into `sorting`, so there is nothing to clear afterwards.
311
+
312
+ Source: `src/docs/quick-search.md` (Fuzzy by default).
313
+
314
+ ## Reference
315
+
316
+ | Name | Kind | Type | Default | What it does |
317
+ | --- | --- | --- | --- | --- |
318
+ | `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the panel, the button and the menu item. |
319
+ | `enableColumnFilter` | Column option | `boolean` | `true` | `false` takes one column out of filtering. |
320
+ | `meta.type` | Column meta | `TMDataGridColumnType` | `"string"` | Selects the operators and the value control. |
321
+ | `meta.defaultFilterOperator` | Column meta | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter opens on. |
322
+ | `meta.filterControl` | Column meta | `TMDataGridFilterControlComponent` | By type and operator | Replaces the value control. Module scope. |
323
+ | `filterFn` | Column option | name or fn | `"tmDataGrid"` | Custom matching for one column. |
324
+ | `quickSearchMode` | Option | `"fuzzy" \| "contains"` | `"fuzzy"` | How the quick search matches. |
325
+ | `enableMatchHighlighting` | Option | `boolean` | `false` | Mark matched text in default-rendered cells. |
326
+ | `enableGlobalFilter` | Table option | `boolean` | `true` | Also a column option. Removes the input, or one column's participation. |
327
+ | `globalFilterFn` | Table option | filter fn | fuzzy | Overrides the matching, and the ranking with it. |
328
+ | `TMDataGrid.FilterPanel` | Component | – | – | The panel of filter rows. |
329
+ | `TMDataGrid.FilterButton` | Component | – | – | Toolbar button opening the panel, with an active count. |
330
+ | `TMDataGrid.FilterPills` | Component | `api`, `size`, `showClearAll`, `onPillClick`, `className` | – | Active filters as removable pills, renderable anywhere. |
331
+ | `TMDataGrid.Search` | Component | `placeholder`, `debounce` (`250`), `w` (`220`) | – | The debounced quick-search input. |
332
+ | `openColumnFilter` | Export | `(api, columnId) => void` | – | Opens the panel on a column. |
333
+ | `isFilterActive` | Export | `(value) => boolean` | – | Whether a filter value is doing anything. |
334
+ | `getOperatorsForType` | Export | `(type) => operators` | – | The operator list a type offers. |
335
+ | `FILTER_OPERATOR_LABELS` | Export | record | – | The label shown for each operator. |
336
+ | `formatFilterLabel` | Export | `({ label, type, filter }) => string` | – | The one-line description used on the pills. |
337
+ | `emptyValueForOperator` · `operatorNeedsValue` · `operatorTakesArrayValue` · `operatorTakesRangeValue` | Exports | – | – | What shape of value an operator wants. |
338
+ | `TMDataGridFilterValueInput` | Export | component | – | The default value control, for falling back to. |
339
+ | `DgRangeSliderFilter` · `DgDateRangeFilter` · `DgAutocompleteFilter` · `DgTriStateFilter` | Exports | components | – | The four ready-made controls. |
340
+ | `fuzzyGlobalFilterFn` | Export | filter fn | – | The default matcher, for a custom input. |
341
+ | `--dg-match-highlight-bg` | CSS variable | colour | Themed yellow | The mark colour. |
342
+
343
+ See also: the `columns` skill for `meta.type` and `meta.options`, and the
344
+ `server-side` skill for forwarding `columnFilters` to an API.