@jielga/tmdatagrid 0.2.0 → 0.5.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 +1337 -94
  2. package/dist/index.js +2812 -1189
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +2 -2
  6. package/skills/columns/SKILL.md +1 -1
  7. package/skills/features/SKILL.md +61 -11
  8. package/skills/getting-started/SKILL.md +41 -6
  9. package/skills/options/SKILL.md +12 -2
  10. package/skills/server-side/SKILL.md +1 -1
  11. package/src/tmdatagrid/TMDataGridContext.ts +26 -0
  12. package/src/tmdatagrid/components/TMDataGrid.module.css +21 -1
  13. package/src/tmdatagrid/components/TMDataGrid.tsx +29 -1
  14. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +12 -6
  15. package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
  16. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
  17. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
  18. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +192 -37
  19. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
  20. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +136 -0
  21. package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
  22. package/src/tmdatagrid/components/TMDataGridFooter.tsx +63 -43
  23. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
  24. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +159 -0
  25. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +18 -0
  26. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +249 -44
  27. package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
  28. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +103 -16
  29. package/src/tmdatagrid/components/TMDataGridTable.module.css +160 -5
  30. package/src/tmdatagrid/components/TMDataGridTable.tsx +1380 -43
  31. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +33 -7
  32. package/src/tmdatagrid/components/icons.ts +6 -0
  33. package/src/tmdatagrid/components/sticky.module.css +77 -16
  34. package/src/tmdatagrid/core/autosize.ts +125 -0
  35. package/src/tmdatagrid/core/capabilities.ts +122 -11
  36. package/src/tmdatagrid/core/cellExport.ts +321 -0
  37. package/src/tmdatagrid/core/cellNavigation.ts +103 -0
  38. package/src/tmdatagrid/core/cellRange.ts +118 -0
  39. package/src/tmdatagrid/core/columnOptions.ts +123 -0
  40. package/src/tmdatagrid/core/columnUtils.ts +18 -0
  41. package/src/tmdatagrid/core/expanding.ts +93 -0
  42. package/src/tmdatagrid/core/filterOperators.ts +198 -12
  43. package/src/tmdatagrid/core/grouping.ts +21 -0
  44. package/src/tmdatagrid/core/labels.ts +211 -0
  45. package/src/tmdatagrid/core/labelsSv.ts +108 -0
  46. package/src/tmdatagrid/core/persistence.ts +12 -0
  47. package/src/tmdatagrid/core/rowSelection.ts +202 -0
  48. package/src/tmdatagrid/core/summary.ts +35 -0
  49. package/src/tmdatagrid/index.ts +96 -2
  50. package/src/tmdatagrid/useTMDataGrid.tsx +558 -37
@@ -0,0 +1,93 @@
1
+ import type { ExpandedState, Row, RowData } from "@tanstack/react-table";
2
+ import type { TMDataGridFeatures } from "../useTMDataGrid";
3
+
4
+ /**
5
+ * Which rows an expand-all control is about.
6
+ *
7
+ * TanStack keeps one `expanded` state, and the grid opens two unrelated things
8
+ * out of it: a group row opens into its children, a data row opens into its
9
+ * detail panel. So `toggleAllRowsExpanded` — which writes the state's
10
+ * whole-table form — is the wrong verb for either control. "Expand all groups"
11
+ * in the tree menu would open every panel in the grid, and the details lane's
12
+ * chevron would unfold the whole tree.
13
+ *
14
+ * These helpers narrow both to the rows their control is for, and leave the
15
+ * other kind exactly as it was.
16
+ */
17
+ export type TMDataGridExpandTarget = "groups" | "details";
18
+
19
+ function isTarget<TData extends RowData>(
20
+ row: Row<TMDataGridFeatures, TData>,
21
+ target: TMDataGridExpandTarget,
22
+ ): boolean {
23
+ return target === "groups" ? row.getIsGrouped() : !row.getIsGrouped();
24
+ }
25
+
26
+ /**
27
+ * The state as a map. `true` is the whole-table form, which has to be written
28
+ * out before one kind of row can be taken back out of it — the same thing
29
+ * TanStack does before toggling a single row.
30
+ */
31
+ function toExpandedMap<TData extends RowData>(
32
+ rows: ReadonlyArray<Row<TMDataGridFeatures, TData>>,
33
+ expanded: ExpandedState,
34
+ ): Record<string, boolean> {
35
+ if (expanded !== true) return { ...expanded };
36
+ const all: Record<string, boolean> = {};
37
+ for (const row of rows) all[row.id] = true;
38
+ return all;
39
+ }
40
+
41
+ export type TMDataGridExpandAllArgs<TData extends RowData> = {
42
+ /**
43
+ * Every row in the model, groups and records alike — `flatRows` off any of
44
+ * them. A grouped model lists its leaves both under their group and in the
45
+ * flat list, so this may repeat rows; both helpers are written so that a
46
+ * repeat costs nothing.
47
+ */
48
+ rows: ReadonlyArray<Row<TMDataGridFeatures, TData>>;
49
+ expanded: ExpandedState;
50
+ target: TMDataGridExpandTarget;
51
+ };
52
+
53
+ /**
54
+ * Whether every row of this kind is open — what the control shows, and what a
55
+ * bare toggle inverts.
56
+ *
57
+ * `false` when there are none of them, matching TanStack's own
58
+ * `getIsAllRowsExpanded()` on an empty state: an empty set is nothing to
59
+ * collapse.
60
+ */
61
+ export function areAllRowsExpanded<TData extends RowData>({
62
+ rows,
63
+ expanded,
64
+ target,
65
+ }: TMDataGridExpandAllArgs<TData>): boolean {
66
+ let found = false;
67
+ for (const row of rows) {
68
+ if (!isTarget(row, target)) continue;
69
+ found = true;
70
+ if (expanded === true) continue;
71
+ if (expanded[row.id] !== true) return false;
72
+ }
73
+ return found;
74
+ }
75
+
76
+ /**
77
+ * The next `expanded` state after an expand-all or collapse-all on one kind of
78
+ * row. Rows of the other kind keep whatever they had.
79
+ */
80
+ export function resolveExpandAll<TData extends RowData>({
81
+ rows,
82
+ expanded,
83
+ target,
84
+ expand,
85
+ }: TMDataGridExpandAllArgs<TData> & { expand: boolean }): ExpandedState {
86
+ const next = toExpandedMap(rows, expanded);
87
+ for (const row of rows) {
88
+ if (!isTarget(row, target)) continue;
89
+ if (expand) next[row.id] = true;
90
+ else delete next[row.id];
91
+ }
92
+ return next;
93
+ }
@@ -7,13 +7,23 @@ import type { Row, RowData, TableFeatures } from "@tanstack/react-table";
7
7
  * inside the filter *value* instead. That keeps the whole filter model plain,
8
8
  * serialisable JSON — which is what makes it portable to a server-side
9
9
  * `manualFiltering` table (just forward `columnFilters` to the API).
10
+ *
11
+ * `value` is a string array under `isAnyOf` / `isNoneOf` (the set the cell is
12
+ * tested against) and a single string everywhere else — dates travel as ISO
13
+ * `YYYY-MM-DD` strings, booleans as `"true"` / `"false"`. Still plain JSON.
10
14
  */
11
15
  export type TMDataGridFilterValue = {
12
16
  operator: TMDataGridFilterOperator;
13
- value: string;
17
+ value: string | ReadonlyArray<string>;
14
18
  };
15
19
 
16
- export type TMDataGridColumnType = "string" | "number";
20
+ export type TMDataGridColumnType =
21
+ | "string"
22
+ | "number"
23
+ | "boolean"
24
+ | "date"
25
+ | "select"
26
+ | "multiSelect";
17
27
 
18
28
  export type TMDataGridFilterOperator =
19
29
  | "contains"
@@ -25,6 +35,12 @@ export type TMDataGridFilterOperator =
25
35
  | "greaterThanOrEqual"
26
36
  | "lessThan"
27
37
  | "lessThanOrEqual"
38
+ | "before"
39
+ | "after"
40
+ | "onOrBefore"
41
+ | "onOrAfter"
42
+ | "isAnyOf"
43
+ | "isNoneOf"
28
44
  | "isEmpty"
29
45
  | "isNotEmpty";
30
46
 
@@ -38,6 +54,12 @@ export const FILTER_OPERATOR_LABELS: Record<TMDataGridFilterOperator, string> =
38
54
  greaterThanOrEqual: "is greater than or equal to",
39
55
  lessThan: "is less than",
40
56
  lessThanOrEqual: "is less than or equal to",
57
+ before: "is before",
58
+ after: "is after",
59
+ onOrBefore: "is on or before",
60
+ onOrAfter: "is on or after",
61
+ isAnyOf: "is any of",
62
+ isNoneOf: "is none of",
41
63
  isEmpty: "is empty",
42
64
  isNotEmpty: "is not empty",
43
65
  };
@@ -63,28 +85,95 @@ const NUMBER_OPERATORS: readonly TMDataGridFilterOperator[] = [
63
85
  "isNotEmpty",
64
86
  ];
65
87
 
88
+ // `equals` doubles as a date's "is": it compares by calendar day whenever the
89
+ // cell holds a Date (see matchesFilter), so a separate `is` operator would be
90
+ // the same comparison under a second name.
91
+ const DATE_OPERATORS: readonly TMDataGridFilterOperator[] = [
92
+ "equals",
93
+ "notEquals",
94
+ "before",
95
+ "after",
96
+ "onOrBefore",
97
+ "onOrAfter",
98
+ "isEmpty",
99
+ "isNotEmpty",
100
+ ];
101
+
102
+ const BOOLEAN_OPERATORS: readonly TMDataGridFilterOperator[] = [
103
+ "equals",
104
+ "notEquals",
105
+ "isEmpty",
106
+ "isNotEmpty",
107
+ ];
108
+
109
+ // One set for both select types: the operators are about membership in the
110
+ // filter's set, which reads the same whether the cell holds one value or many.
111
+ const SELECT_OPERATORS: readonly TMDataGridFilterOperator[] = [
112
+ "isAnyOf",
113
+ "isNoneOf",
114
+ "isEmpty",
115
+ "isNotEmpty",
116
+ ];
117
+
66
118
  /** Operators that ignore the value input (the input is disabled for these). */
67
119
  const VALUELESS_OPERATORS: readonly TMDataGridFilterOperator[] = [
68
120
  "isEmpty",
69
121
  "isNotEmpty",
70
122
  ];
71
123
 
124
+ /** Operators whose value is a set of strings rather than one. */
125
+ const ARRAY_OPERATORS: readonly TMDataGridFilterOperator[] = [
126
+ "isAnyOf",
127
+ "isNoneOf",
128
+ ];
129
+
130
+ const OPERATORS_BY_TYPE: Record<
131
+ TMDataGridColumnType,
132
+ readonly TMDataGridFilterOperator[]
133
+ > = {
134
+ string: STRING_OPERATORS,
135
+ number: NUMBER_OPERATORS,
136
+ boolean: BOOLEAN_OPERATORS,
137
+ date: DATE_OPERATORS,
138
+ select: SELECT_OPERATORS,
139
+ multiSelect: SELECT_OPERATORS,
140
+ };
141
+
142
+ const DEFAULT_OPERATOR_BY_TYPE: Record<
143
+ TMDataGridColumnType,
144
+ TMDataGridFilterOperator
145
+ > = {
146
+ string: "contains",
147
+ number: "equals",
148
+ boolean: "equals",
149
+ date: "equals",
150
+ select: "isAnyOf",
151
+ multiSelect: "isAnyOf",
152
+ };
153
+
72
154
  export function getOperatorsForType(
73
155
  type: TMDataGridColumnType,
74
156
  ): readonly TMDataGridFilterOperator[] {
75
- return type === "number" ? NUMBER_OPERATORS : STRING_OPERATORS;
157
+ return OPERATORS_BY_TYPE[type];
76
158
  }
77
159
 
78
160
  export function getDefaultOperator(
79
161
  type: TMDataGridColumnType,
80
162
  ): TMDataGridFilterOperator {
81
- return type === "number" ? "equals" : "contains";
163
+ return DEFAULT_OPERATOR_BY_TYPE[type];
82
164
  }
83
165
 
84
166
  export function operatorNeedsValue(operator: TMDataGridFilterOperator): boolean {
85
167
  return !VALUELESS_OPERATORS.includes(operator);
86
168
  }
87
169
 
170
+ /** Whether the operator's value is a string array — `isAnyOf` / `isNoneOf`. */
171
+ export function operatorTakesArrayValue(
172
+ operator: TMDataGridFilterOperator,
173
+ ): boolean {
174
+ return ARRAY_OPERATORS.includes(operator);
175
+ }
176
+
88
177
  export function isTMDataGridFilterValue(
89
178
  value: unknown,
90
179
  ): value is TMDataGridFilterValue {
@@ -103,23 +192,113 @@ export function isTMDataGridFilterValue(
103
192
  */
104
193
  export function isFilterActive(value: unknown): boolean {
105
194
  if (!isTMDataGridFilterValue(value)) return false;
106
- return !operatorNeedsValue(value.operator) || value.value.trim() !== "";
195
+ if (!operatorNeedsValue(value.operator)) return true;
196
+ return Array.isArray(value.value)
197
+ ? value.value.length > 0
198
+ : typeof value.value === "string" && value.value.trim() !== "";
199
+ }
200
+
201
+ /**
202
+ * One-line description of a single filter, as shown on a filter pill.
203
+ *
204
+ * The type's default operator is left implicit — "First name: Sofia" reads the
205
+ * way a person would say it — while any other operator is spelled out, since
206
+ * that is the part a reader cannot guess.
207
+ */
208
+ export function formatFilterLabel({
209
+ label,
210
+ type,
211
+ filter,
212
+ operatorLabels = FILTER_OPERATOR_LABELS,
213
+ }: {
214
+ label: string;
215
+ type: TMDataGridColumnType;
216
+ filter: TMDataGridFilterValue;
217
+ /** Localized operator names — `labels.operators`. Defaults to English. */
218
+ operatorLabels?: Record<TMDataGridFilterOperator, string>;
219
+ }): string {
220
+ const { operator, value } = filter;
221
+ if (!operatorNeedsValue(operator)) {
222
+ return `${label} ${operatorLabels[operator]}`;
223
+ }
224
+ const text = Array.isArray(value) ? value.join(", ") : String(value).trim();
225
+ if (operator === getDefaultOperator(type)) return `${label}: ${text}`;
226
+ return `${label} ${operatorLabels[operator]} ${text}`;
227
+ }
228
+
229
+ /**
230
+ * A value's calendar day as `YYYY-MM-DD`, or `null` for anything that has no
231
+ * day to speak of. A `Date` is read in local time — `sv-SE` formats as ISO,
232
+ * which is the shape `<input type="date">` produced on the other side — and a
233
+ * string contributes its leading ISO day, so both `2026-07-31` and a full
234
+ * timestamp compare by day.
235
+ */
236
+ function toIsoDay(value: unknown): string | null {
237
+ if (value instanceof Date) {
238
+ return Number.isNaN(value.getTime())
239
+ ? null
240
+ : value.toLocaleDateString("sv-SE");
241
+ }
242
+ if (typeof value === "string") {
243
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(value.trim());
244
+ return match ? match[1] : null;
245
+ }
246
+ return null;
247
+ }
248
+
249
+ function isEmptyCell(cellValue: unknown): boolean {
250
+ return (
251
+ cellValue === null ||
252
+ cellValue === undefined ||
253
+ cellValue === "" ||
254
+ (Array.isArray(cellValue) && cellValue.length === 0)
255
+ );
107
256
  }
108
257
 
109
258
  export function matchesFilter(
110
259
  cellValue: unknown,
111
260
  { operator, value }: TMDataGridFilterValue,
112
261
  ): boolean {
113
- if (operator === "isEmpty") {
114
- return cellValue === null || cellValue === undefined || cellValue === "";
115
- }
116
- if (operator === "isNotEmpty") {
117
- return cellValue !== null && cellValue !== undefined && cellValue !== "";
262
+ if (operator === "isEmpty") return isEmptyCell(cellValue);
263
+ if (operator === "isNotEmpty") return !isEmptyCell(cellValue);
264
+
265
+ // Set membership. The cell side is also a list — a multiSelect cell holds an
266
+ // array — so "any of" is an intersection test and "none of" its complement.
267
+ if (operator === "isAnyOf" || operator === "isNoneOf") {
268
+ const set = (Array.isArray(value) ? value : [value])
269
+ .map((entry) => String(entry).toLowerCase())
270
+ .filter((entry) => entry !== "");
271
+ if (set.length === 0) return true;
272
+ const cellEntries = Array.isArray(cellValue) ? cellValue : [cellValue];
273
+ const hit = cellEntries.some((entry) =>
274
+ set.includes(String(entry ?? "").toLowerCase()),
275
+ );
276
+ return operator === "isAnyOf" ? hit : !hit;
118
277
  }
119
278
 
120
- const filterText = value.trim();
279
+ const filterText =
280
+ typeof value === "string" ? value.trim() : value.join(",").trim();
121
281
  if (filterText === "") return true;
122
282
 
283
+ // Day ordering. ISO days compare correctly as strings, which keeps this
284
+ // free of timezone arithmetic — the whole comparison lives in local days.
285
+ switch (operator) {
286
+ case "before":
287
+ case "after":
288
+ case "onOrBefore":
289
+ case "onOrAfter": {
290
+ const cellDay = toIsoDay(cellValue);
291
+ const filterDay = toIsoDay(filterText);
292
+ if (cellDay === null || filterDay === null) return false;
293
+ if (operator === "before") return cellDay < filterDay;
294
+ if (operator === "after") return cellDay > filterDay;
295
+ if (operator === "onOrBefore") return cellDay <= filterDay;
296
+ return cellDay >= filterDay;
297
+ }
298
+ default:
299
+ break;
300
+ }
301
+
123
302
  const numericCell = typeof cellValue === "number" ? cellValue : Number.NaN;
124
303
  const numericFilter = Number(filterText);
125
304
  const compareNumeric =
@@ -138,7 +317,14 @@ export function matchesFilter(
138
317
  break;
139
318
  }
140
319
 
141
- const cellText = String(cellValue ?? "").toLowerCase();
320
+ // A Date compares by its calendar day, which is what makes `equals` on a
321
+ // date column mean "is this day" rather than a doomed string comparison
322
+ // against the Date's default stringification.
323
+ const cellText = (
324
+ cellValue instanceof Date
325
+ ? (toIsoDay(cellValue) ?? "")
326
+ : String(cellValue ?? "")
327
+ ).toLowerCase();
142
328
  const needle = filterText.toLowerCase();
143
329
 
144
330
  switch (operator) {
@@ -0,0 +1,21 @@
1
+ import type { Row, RowData } from "@tanstack/react-table";
2
+ import type { TMDataGridFeatures } from "../useTMDataGrid";
3
+
4
+ /**
5
+ * The data rows under a group row, at any depth.
6
+ *
7
+ * Not `row.getLeafRows()`, despite the name: that flattens the whole subtree
8
+ * and keeps the branches, so a group nested two deep reports its sub-groups
9
+ * alongside the records. Grouping `city` then `name` would have Stockholm
10
+ * counting four rows and three names as seven, and one tick on it selecting
11
+ * ids that hold no record.
12
+ *
13
+ * A row with no subRows is a data row, which also makes this the identity on an
14
+ * ungrouped grid.
15
+ */
16
+ export function getGroupDataRows<TData extends RowData>(
17
+ row: Row<TMDataGridFeatures, TData>,
18
+ ): Array<Row<TMDataGridFeatures, TData>> {
19
+ if (row.subRows.length === 0) return [row];
20
+ return row.getLeafRows().filter((leaf) => leaf.subRows.length === 0);
21
+ }
@@ -0,0 +1,211 @@
1
+ import {
2
+ FILTER_OPERATOR_LABELS,
3
+ type TMDataGridFilterOperator,
4
+ } from "./filterOperators";
5
+
6
+ /**
7
+ * Every user-facing string in the grid — menu items, panels, tooltips, the
8
+ * pager, and the `aria-label`s a screen reader speaks. One flat object, so an
9
+ * override is a spread rather than a deep merge; `operators` is the single
10
+ * nested record and {@link mergeLabels} folds it separately.
11
+ *
12
+ * Labels that carry a value are functions rather than template strings, so a
13
+ * language is free to put the value wherever its grammar wants it.
14
+ */
15
+ export type TMDataGridLabels = {
16
+ /** Filter operator names, as shown in the operator dropdown and on pills. */
17
+ operators: Record<TMDataGridFilterOperator, string>;
18
+
19
+ // Toolbar
20
+ /** "Manage columns" — the burger button and the header menu item. */
21
+ manageColumns: string;
22
+ /** "Filters" — the funnel button and the filter panel's title. */
23
+ filters: string;
24
+ searchPlaceholder: string;
25
+ searchLabel: string;
26
+ clearSearch: string;
27
+
28
+ // Columns panel
29
+ columnsSearchPlaceholder: string;
30
+ columnsNoMatch: (search: string) => string;
31
+ columnsShowHideAll: string;
32
+ columnsReset: string;
33
+
34
+ // Filter panel
35
+ filterColumn: string;
36
+ filterOperator: string;
37
+ filterValue: string;
38
+ filterValuePlaceholder: string;
39
+ /** What a boolean column's `true` reads as — its filter choice and cell text. */
40
+ booleanTrue: string;
41
+ booleanFalse: string;
42
+ addFilter: string;
43
+ clearAllFilters: string;
44
+ closeFilters: string;
45
+ removeFilter: string;
46
+
47
+ // Filter pills
48
+ activeFilters: string;
49
+ clearFilter: (column: string) => string;
50
+
51
+ // Header menu
52
+ sortAsc: string;
53
+ sortDesc: string;
54
+ filterMenuItem: string;
55
+ groupBy: (column: string) => string;
56
+ ungroup: (column: string) => string;
57
+ expandAllGroups: string;
58
+ collapseAllGroups: string;
59
+ pinLeft: string;
60
+ pinRight: string;
61
+ unpin: string;
62
+ moveLeft: string;
63
+ moveRight: string;
64
+ autosizeColumn: string;
65
+ hideColumn: string;
66
+ filterOn: (column: string) => string;
67
+ sortColumn: (column: string) => string;
68
+ columnMenu: (column: string) => string;
69
+
70
+ // Footer / pager
71
+ rowsPerPage: string;
72
+ pageRange: (args: { from: number; to: number; total: number }) => string;
73
+ groupedAllRows: (total: number) => string;
74
+ pagingSuspendedHint: string;
75
+ previousPage: string;
76
+ nextPage: string;
77
+
78
+ // Body
79
+ noResults: string;
80
+ loading: string;
81
+
82
+ // Cell selection menu
83
+ cellCount: (count: number) => string;
84
+ copy: string;
85
+ exportCsv: string;
86
+ includeHeaders: string;
87
+
88
+ // Generated checkbox lane
89
+ selectColumnLabel: string;
90
+ selectAllRows: string;
91
+ selectRow: string;
92
+ selectGroup: string;
93
+
94
+ // Generated details lane
95
+ detailsColumnLabel: string;
96
+ showDetails: string;
97
+ hideDetails: string;
98
+ expandAllDetails: string;
99
+ collapseAllDetails: string;
100
+
101
+ // Generated tree lane
102
+ groupColumnLabel: string;
103
+ blankGroupValue: string;
104
+ expandGroup: (label: string) => string;
105
+ collapseGroup: (label: string) => string;
106
+ };
107
+
108
+ /**
109
+ * What the `labels` option accepts: any subset, merged over the English
110
+ * defaults — so `{ noResults: "Inga rader" }` is a complete configuration.
111
+ */
112
+ export type TMDataGridLabelsOverride = Partial<
113
+ Omit<TMDataGridLabels, "operators">
114
+ > & {
115
+ operators?: Partial<Record<TMDataGridFilterOperator, string>>;
116
+ };
117
+
118
+ /** The built-in defaults. Also the reference for what a preset must cover. */
119
+ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
120
+ operators: FILTER_OPERATOR_LABELS,
121
+
122
+ manageColumns: "Manage columns",
123
+ filters: "Filters",
124
+ searchPlaceholder: "Search",
125
+ searchLabel: "Search rows",
126
+ clearSearch: "Clear search",
127
+
128
+ columnsSearchPlaceholder: "Search",
129
+ columnsNoMatch: (search) => `No columns match “${search}”`,
130
+ columnsShowHideAll: "Show/Hide All",
131
+ columnsReset: "RESET",
132
+
133
+ filterColumn: "Column",
134
+ filterOperator: "Operator",
135
+ filterValue: "Value",
136
+ filterValuePlaceholder: "Filter value",
137
+ booleanTrue: "Yes",
138
+ booleanFalse: "No",
139
+ addFilter: "Add filter",
140
+ clearAllFilters: "Clear all",
141
+ closeFilters: "Close filters",
142
+ removeFilter: "Remove filter",
143
+
144
+ activeFilters: "Active filters",
145
+ clearFilter: (column) => `Clear ${column} filter`,
146
+
147
+ sortAsc: "Sort by ASC",
148
+ sortDesc: "Sort by DESC",
149
+ filterMenuItem: "Filter",
150
+ groupBy: (column) => `Group by ${column}`,
151
+ ungroup: (column) => `Ungroup ${column}`,
152
+ expandAllGroups: "Expand all groups",
153
+ collapseAllGroups: "Collapse all groups",
154
+ pinLeft: "Pin to left",
155
+ pinRight: "Pin to right",
156
+ unpin: "Unpin",
157
+ moveLeft: "Move left",
158
+ moveRight: "Move right",
159
+ autosizeColumn: "Autosize column",
160
+ hideColumn: "Hide column",
161
+ filterOn: (column) => `Filter on ${column}`,
162
+ sortColumn: (column) => `Sort ${column}`,
163
+ columnMenu: (column) => `${column} column menu`,
164
+
165
+ rowsPerPage: "Rows per page:",
166
+ pageRange: ({ from, to, total }) => `${from}–${to} of ${total}`,
167
+ groupedAllRows: (total) => `Grouped · all ${total} rows`,
168
+ pagingSuspendedHint:
169
+ "Paging is off while the rows are grouped: the whole tree is rendered and virtualized. Ungroup to page again.",
170
+ previousPage: "Previous page",
171
+ nextPage: "Next page",
172
+
173
+ noResults: "No rows match your filters",
174
+ loading: "Loading",
175
+
176
+ cellCount: (count) => (count === 1 ? "1 cell" : `${count} cells`),
177
+ copy: "Copy",
178
+ exportCsv: "Export as CSV for Excel",
179
+ includeHeaders: "Include headers",
180
+
181
+ selectColumnLabel: "Checkbox selection",
182
+ selectAllRows: "Select all rows",
183
+ selectRow: "Select row",
184
+ selectGroup: "Select group",
185
+
186
+ detailsColumnLabel: "Details",
187
+ showDetails: "Show details",
188
+ hideDetails: "Hide details",
189
+ expandAllDetails: "Expand all details",
190
+ collapseAllDetails: "Collapse all details",
191
+
192
+ groupColumnLabel: "Group",
193
+ blankGroupValue: "(Blank)",
194
+ expandGroup: (label) => `Expand ${label}`,
195
+ collapseGroup: (label) => `Collapse ${label}`,
196
+ };
197
+
198
+ /** A partial override folded over the English defaults. */
199
+ export function mergeLabels(
200
+ override?: TMDataGridLabelsOverride,
201
+ ): TMDataGridLabels {
202
+ if (!override) return TMDATAGRID_LABELS_EN;
203
+ return {
204
+ ...TMDATAGRID_LABELS_EN,
205
+ ...override,
206
+ operators: {
207
+ ...TMDATAGRID_LABELS_EN.operators,
208
+ ...override.operators,
209
+ },
210
+ };
211
+ }