@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
@@ -1,6 +1,15 @@
1
1
  import { Checkbox } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
3
  import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
4
+ import {
5
+ useCellControlTabIndex,
6
+ useTMDataGridContext,
7
+ } from "../TMDataGridContext";
8
+ import {
9
+ getDisplayedRows,
10
+ getSelectableRowIds,
11
+ resolveRowSelectionClick,
12
+ } from "../core/rowSelection";
4
13
  import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
5
14
 
6
15
  export const SELECT_COLUMN_ID = "__select__";
@@ -25,6 +34,7 @@ function SelectAllCheckbox<TData extends RowData>({
25
34
  }: {
26
35
  table: TMDataGridTable<TData>;
27
36
  }) {
37
+ const { labels } = useTMDataGridContext();
28
38
  const allSelected = useSelector(table.store, () =>
29
39
  table.getIsAllRowsSelected(),
30
40
  );
@@ -35,7 +45,7 @@ function SelectAllCheckbox<TData extends RowData>({
35
45
  return (
36
46
  <Checkbox
37
47
  size="xs"
38
- aria-label="Select all rows"
48
+ aria-label={labels.selectAllRows}
39
49
  checked={allSelected}
40
50
  indeterminate={someSelected && !allSelected}
41
51
  onChange={table.getToggleAllRowsSelectedHandler()}
@@ -43,24 +53,95 @@ function SelectAllCheckbox<TData extends RowData>({
43
53
  );
44
54
  }
45
55
 
56
+ /**
57
+ * Drops the select-all box when only one row may be selected. Read from the
58
+ * feature flags rather than `table.options.enableMultiRowSelection` directly:
59
+ * the flags are re-derived from a fresh object every render, so the switch
60
+ * cannot be cached along with a `table` identity that survives an options
61
+ * change — the same reason the rest of the chrome reads them. See
62
+ * readFeatureFlags.
63
+ */
64
+ function SelectAllHeader<TData extends RowData>({
65
+ table,
66
+ }: {
67
+ table: TMDataGridTable<TData>;
68
+ }) {
69
+ const { features } = useTMDataGridContext();
70
+ if (!features.multiRowSelection) return null;
71
+ return <SelectAllCheckbox table={table} />;
72
+ }
73
+
46
74
  function SelectRowCheckbox<TData extends RowData>({
47
75
  row,
48
76
  }: {
49
77
  row: Row<TMDataGridFeatures, TData>;
50
78
  }) {
51
- const selected = useSelector(row.table.store, () => row.getIsSelected());
79
+ // Cells render inside the grid's provider, so the checkbox can reach the
80
+ // chrome store — it needs the shift-click pivot, and the feature flags to know
81
+ // which row model a range is measured over.
82
+ const { ui, features, labels } = useTMDataGridContext();
83
+ // Out of the tab order once the grid has a cell cursor — see
84
+ // useCellControlTabIndex. Enter on the lane still steps in, and Space on any
85
+ // cell of the row ticks it.
86
+ const tabIndex = useCellControlTabIndex();
87
+ const isGroupRow = row.subRows.length > 0;
88
+
89
+ // A group row is never selected by id: `rowSelection` only ever holds the
90
+ // leaves, and TanStack's `getIsSelected()` is a plain lookup in that map. So
91
+ // a group asks about its descendants instead, which is also the honest
92
+ // reading — the box means "all of these", and it goes indeterminate as soon
93
+ // as that stops being true.
94
+ const selected = useSelector(row.table.store, () =>
95
+ isGroupRow ? row.getIsAllSubRowsSelected() : row.getIsSelected(),
96
+ );
52
97
  const someSelected = useSelector(row.table.store, () =>
53
98
  row.getIsSomeSelected(),
54
99
  );
55
100
 
101
+ // The resolver expands a tick back out to the descendant ids; all the group
102
+ // case needs here is to know whether any of them may be selected at all.
103
+ const selectableIds = getSelectableRowIds(row);
104
+
105
+ // Single-select has no way to express "this group": one box for several rows
106
+ // would either overrun the limit or lie about what it did.
107
+ if (isGroupRow && !row.getCanMultiSelect()) return null;
108
+
56
109
  return (
57
110
  <Checkbox
58
111
  size="xs"
59
- aria-label="Select row"
112
+ tabIndex={tabIndex}
113
+ aria-label={isGroupRow ? labels.selectGroup : labels.selectRow}
60
114
  checked={selected}
61
- disabled={!row.getCanSelect()}
115
+ disabled={selectableIds.length === 0}
62
116
  indeterminate={someSelected && !selected}
63
- onChange={row.getToggleSelectedHandler()}
117
+ // Every tick goes through the resolver, shift held or not — plain becomes
118
+ // a toggle that moves the pivot, so a later shift-click extends from the
119
+ // box the user last touched.
120
+ //
121
+ // All of it in `onChange`, not split with an `onClick`: React derives a
122
+ // checkbox's change event from the click, so the modifier keys ride along
123
+ // on the native event, and a `preventDefault()` in a click handler cannot
124
+ // stop the change from firing anyway. Trying to do the range in `onClick`
125
+ // silently loses the clicked row, which `onChange` then toggles back off.
126
+ onChange={(event) => {
127
+ const native = event.nativeEvent;
128
+ const isMouse = native instanceof MouseEvent;
129
+ const resolved = resolveRowSelectionClick({
130
+ rows: getDisplayedRows(row.table, features),
131
+ rowId: row.id,
132
+ anchorRowId: ui.state.selectionAnchorRowId,
133
+ modifiers: {
134
+ toggle: isMouse && (native.ctrlKey || native.metaKey),
135
+ extend: isMouse && native.shiftKey,
136
+ },
137
+ selection: row.table.store.state.rowSelection,
138
+ // A checkbox is only ever additive — ticking one has never cleared
139
+ // the others, so `canReplaceSelection` stays false whatever is held.
140
+ canReplaceSelection: false,
141
+ });
142
+ row.table.setRowSelection(resolved.selection);
143
+ ui.actions.setSelectionAnchor(resolved.anchorRowId);
144
+ }}
64
145
  // A row can select on click too; the checkbox must not toggle twice.
65
146
  onClick={(event) => event.stopPropagation()}
66
147
  />
@@ -69,32 +150,38 @@ function SelectRowCheckbox<TData extends RowData>({
69
150
 
70
151
  /**
71
152
  * The generated checkbox column, prepended under
72
- * `rowSelectionMode: "checkbox"` — the default whenever row selection is on.
153
+ * `selectionMode: "checkbox"` (the default) or `"checkboxAndHighlight"`.
73
154
  */
74
- export function createSelectColumn<TData extends RowData>(): ColumnDef<
75
- TMDataGridFeatures,
76
- TData,
77
- unknown
78
- > {
155
+ export function createSelectColumn<TData extends RowData>(
156
+ label = "Checkbox selection",
157
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
79
158
  return {
80
159
  id: SELECT_COLUMN_ID,
81
160
  meta: {
82
- label: "Checkbox selection",
161
+ label,
83
162
  align: "center",
84
163
  // Structurally the first column; it also anchors the left pinned lane, so
85
164
  // no other column can be moved in front of it.
86
165
  enableOrdering: false,
87
166
  },
88
- size: 48,
89
- minSize: 48,
90
- maxSize: 48,
167
+ // A system lane: as wide as the control it holds and no wider. Fixed at
168
+ // every scale — the control does not grow with the font size, so neither
169
+ // should its track.
170
+ size: 36,
171
+ minSize: 36,
172
+ maxSize: 36,
91
173
  enableResizing: false,
92
174
  enableSorting: false,
93
175
  enableColumnFilter: false,
94
176
  enableGlobalFilter: false,
95
177
  // Structurally pinned to the left; users shouldn't be able to move it.
96
178
  enablePinning: false,
97
- header: ({ table }) => <SelectAllCheckbox table={table} />,
179
+ header: ({ table }) => <SelectAllHeader table={table} />,
98
180
  cell: ({ row }) => <SelectRowCheckbox row={row} />,
181
+ // Every cell on a group row that is not the grouped column counts as
182
+ // aggregated, this lane included, and an aggregated cell with nothing
183
+ // declared renders blank. Without this the checkbox would disappear from
184
+ // exactly the rows that select a whole group. See renderCellContent.
185
+ aggregatedCell: ({ row }) => <SelectRowCheckbox row={row} />,
99
186
  };
100
187
  }
@@ -27,7 +27,7 @@
27
27
  grid-template-columns: subgrid;
28
28
  position: sticky;
29
29
  top: 0;
30
- z-index: 3;
30
+ z-index: var(--dg-z-header, 6);
31
31
  }
32
32
 
33
33
  .bodyRow {
@@ -42,13 +42,39 @@
42
42
  --row-bg: var(--mantine-color-default-hover);
43
43
  }
44
44
 
45
- /* Selection and highlight are separate: a row is always `data-selected`, but it
46
- only takes the highlight colour when `highlightSelectedRows` is on — the
47
- default in row selection mode, off with checkboxes. */
48
- .bodyRow[data-highlighted="true"] {
45
+ /* A group row takes no background of its own. Collapsed, every row on screen is
46
+ a group row, so a tint would grey out the whole grid and read as if
47
+ everything were selected. The bold label and the chevron carry the
48
+ distinction instead — and `--dg-row-group-bg` is there for a consumer who
49
+ wants the tint back. Declared before the selection rules, so a selected or
50
+ highlighted group still reads as one. */
51
+ .bodyRow[data-grouped="true"] {
52
+ --row-bg: var(--dg-row-group-bg);
53
+ }
54
+
55
+ /* The row an open context menu is about. The pointer has usually left it by
56
+ then, so without this the menu would be the only thing saying which row it
57
+ acts on. Declared before the selection rules, which are the stronger
58
+ statement and win at equal specificity. */
59
+ .bodyRow[data-context-menu="true"] {
60
+ --row-bg: var(--mantine-color-default-hover);
61
+ }
62
+
63
+ /* Being selected and being painted for it are separate: a row is always
64
+ `data-selected`, but only takes the background when `showSelectedBackground`
65
+ is on — the default under `"row"`, off with checkboxes. */
66
+ .bodyRow[data-selected-bg="true"] {
49
67
  --row-bg: var(--dg-row-selected-bg);
50
68
  }
51
69
 
70
+ /* The highlighted row wins over the selected background: under
71
+ `"checkboxAndHighlight"` the two coexist, and the highlighted row is the one
72
+ the detail panel is about, so it must stay distinguishable inside a
73
+ selection. Declared after, so the later rule takes it at equal specificity. */
74
+ .bodyRow[data-highlighted="true"] {
75
+ --row-bg: var(--dg-row-highlight-bg);
76
+ }
77
+
52
78
  /* Row selection mode: the row is the control, so it takes focus and must not
53
79
  start a text selection on the click that toggles it. */
54
80
  .bodyRow[data-selects-on-click="true"] {
@@ -72,6 +98,83 @@
72
98
  font-size: var(--dg-font-size);
73
99
  }
74
100
 
101
+ /* A control lane — the checkbox, the details chevron — is a fixed 36px track
102
+ holding a control of a known width, not text that can ellipsize. Cell padding
103
+ is sized for text and grows with the scale, so at `xl` its 18px a side left
104
+ 12px for a 16px box and the checkbox was clipped. The lane centres its
105
+ control instead. */
106
+ .bodyCell[data-control-column="true"] {
107
+ padding: 0;
108
+ }
109
+
110
+ /* A drag is laying down a range. Text selection would otherwise follow the
111
+ pointer across every cell it passes. */
112
+ .grid[data-range-dragging] {
113
+ user-select: none;
114
+ }
115
+
116
+ /* The selected block, under `cellSelection: "range"`.
117
+
118
+ A tint over the row's own background rather than instead of it, so a
119
+ selected cell on a highlighted row still reads as both. `color-mix` keeps
120
+ one declaration for light and dark: it thins the theme's primary colour into
121
+ whatever `--row-bg` the row resolved to. */
122
+ .bodyCell[data-selected="true"] {
123
+ background-color: color-mix(
124
+ in srgb,
125
+ var(--mantine-primary-color-filled) 14%,
126
+ var(--row-bg)
127
+ );
128
+ }
129
+
130
+ /* The outline around the block, drawn edge by edge: only the cells on a side
131
+ of the rectangle draw that side, so they add up to one border around the
132
+ whole thing however it is shaped. A single overlay box would have to be
133
+ positioned against a scrolling grid with sticky lanes in it.
134
+
135
+ Four shadows are always laid down and each is transparent until its edge
136
+ says otherwise. `box-shadow` does not accumulate across rules — a second
137
+ declaration replaces the first — so the alternative is one rule per corner
138
+ combination, and a cell can be on any of fifteen of them. */
139
+ .bodyCell[data-selected="true"] {
140
+ --dg-edge-top: transparent;
141
+ --dg-edge-bottom: transparent;
142
+ --dg-edge-left: transparent;
143
+ --dg-edge-right: transparent;
144
+ box-shadow:
145
+ inset 0 1px 0 0 var(--dg-edge-top),
146
+ inset 0 -1px 0 0 var(--dg-edge-bottom),
147
+ inset 1px 0 0 0 var(--dg-edge-left),
148
+ inset -1px 0 0 0 var(--dg-edge-right);
149
+ }
150
+
151
+ .bodyCell[data-edge-top="true"] {
152
+ --dg-edge-top: var(--mantine-primary-color-filled);
153
+ }
154
+
155
+ .bodyCell[data-edge-bottom="true"] {
156
+ --dg-edge-bottom: var(--mantine-primary-color-filled);
157
+ }
158
+
159
+ .bodyCell[data-edge-left="true"] {
160
+ --dg-edge-left: var(--mantine-primary-color-filled);
161
+ }
162
+
163
+ .bodyCell[data-edge-right="true"] {
164
+ --dg-edge-right: var(--mantine-primary-color-filled);
165
+ }
166
+
167
+ /* The cell the keyboard is on, under `cellSelection`.
168
+
169
+ Painted from `data-focused` rather than `:focus-visible`, because the ring
170
+ has to stay while the focus steps into a control inside the cell — that is
171
+ still where the arrow keys resume from. The offset keeps it inside the
172
+ cell's own box, so a pinned neighbour cannot clip it. */
173
+ .bodyCell[data-focused="true"] {
174
+ outline: 2px solid var(--mantine-primary-color-filled);
175
+ outline-offset: -2px;
176
+ }
177
+
75
178
  .cellContent {
76
179
  min-width: 0;
77
180
  overflow: hidden;
@@ -92,6 +195,23 @@
92
195
  flex: 1 1 auto;
93
196
  }
94
197
 
198
+ /* The panel an expanded row opens. Its own grid row inside .bodyRow, spanning
199
+ every column track — so it is measured together with the row above it.
200
+
201
+ It takes `--row-bg` rather than a colour of its own: the panel belongs to its
202
+ row, and a selected or highlighted row whose panel stayed white would read as
203
+ two separate things. */
204
+ .detailsCell {
205
+ grid-column: 1 / -1;
206
+ border-bottom: 1px solid var(--mantine-color-default-border);
207
+ background-color: var(--row-bg);
208
+ padding: var(--dg-padding);
209
+ /* The row above may be the selection control, which turns text selection off
210
+ for everything inside it. A panel is content, so it takes both back. */
211
+ user-select: text;
212
+ cursor: auto;
213
+ }
214
+
95
215
  .messageRow {
96
216
  grid-column: 1 / -1;
97
217
  display: flex;
@@ -101,3 +221,38 @@
101
221
  gap: var(--mantine-spacing-xs);
102
222
  padding: var(--mantine-spacing-xl);
103
223
  }
224
+
225
+ /* The summary row — one sticky row of column footers along the bottom edge,
226
+ rendered only while at least one column defines `footer`. Same subgrid
227
+ placement as the header, so its cells sit exactly on the body's tracks. */
228
+ .summaryRow {
229
+ display: grid;
230
+ grid-column: 1 / -1;
231
+ grid-template-columns: subgrid;
232
+ position: sticky;
233
+ bottom: 0;
234
+ z-index: var(--dg-z-summary-row, 4);
235
+ }
236
+
237
+ .summaryCell {
238
+ display: flex;
239
+ align-items: center;
240
+ padding: 0 var(--dg-padding);
241
+ border-top: 1px solid var(--mantine-color-default-border);
242
+ /* Opaque: the body scrolls underneath it. */
243
+ background-color: var(--mantine-color-body);
244
+ font-size: var(--dg-font-size);
245
+ font-weight: 600;
246
+ }
247
+
248
+ .summaryCell[data-control-column="true"] {
249
+ padding: 0;
250
+ }
251
+
252
+ .summaryCell[data-align="right"] {
253
+ justify-content: flex-end;
254
+ }
255
+
256
+ .summaryCell[data-align="center"] {
257
+ justify-content: center;
258
+ }