@jielga/tmdatagrid 0.1.0 → 0.3.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 (47) hide show
  1. package/README.md +35 -2
  2. package/dist/index.d.ts +792 -327
  3. package/dist/index.js +1581 -892
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +15 -7
  7. package/skills/columns/SKILL.md +8 -7
  8. package/skills/features/SKILL.md +104 -16
  9. package/skills/getting-started/SKILL.md +78 -20
  10. package/skills/options/SKILL.md +12 -7
  11. package/skills/server-side/SKILL.md +5 -2
  12. package/src/tmdatagrid/TMDataGridContext.ts +1 -1
  13. package/src/tmdatagrid/components/TMDataGrid.module.css +73 -0
  14. package/src/tmdatagrid/{TMDataGrid.tsx → components/TMDataGrid.tsx} +15 -4
  15. package/src/tmdatagrid/components/TMDataGridColumnsPanel.module.css +25 -0
  16. package/src/tmdatagrid/{TMDataGridColumnsPanel.tsx → components/TMDataGridColumnsPanel.tsx} +10 -4
  17. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +35 -0
  18. package/src/tmdatagrid/{TMDataGridFilterPanel.tsx → components/TMDataGridFilterPanel.tsx} +91 -14
  19. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
  20. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
  21. package/src/tmdatagrid/components/TMDataGridFooter.module.css +18 -0
  22. package/src/tmdatagrid/components/TMDataGridFooter.tsx +171 -0
  23. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
  24. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
  25. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +159 -0
  26. package/src/tmdatagrid/{TMDataGridHeaderCell.tsx → components/TMDataGridHeaderCell.tsx} +99 -14
  27. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +177 -0
  28. package/src/tmdatagrid/components/TMDataGridTable.module.css +137 -0
  29. package/src/tmdatagrid/components/TMDataGridTable.tsx +720 -0
  30. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +12 -0
  31. package/src/tmdatagrid/{TMDataGridToolbar.tsx → components/TMDataGridToolbar.tsx} +8 -5
  32. package/src/tmdatagrid/{icons.ts → components/icons.ts} +5 -0
  33. package/src/tmdatagrid/components/sticky.module.css +45 -0
  34. package/src/tmdatagrid/core/capabilities.ts +215 -0
  35. package/src/tmdatagrid/{columnOrdering.ts → core/columnOrdering.ts} +2 -2
  36. package/src/tmdatagrid/{columnUtils.ts → core/columnUtils.ts} +1 -1
  37. package/src/tmdatagrid/{filterOperators.ts → core/filterOperators.ts} +25 -0
  38. package/src/tmdatagrid/core/grouping.ts +21 -0
  39. package/src/tmdatagrid/{persistence.ts → core/persistence.ts} +92 -2
  40. package/src/tmdatagrid/core/rowSelection.ts +202 -0
  41. package/src/tmdatagrid/index.ts +39 -9
  42. package/src/tmdatagrid/useTMDataGrid.tsx +318 -66
  43. package/src/tmdatagrid/TMDataGrid.module.css +0 -410
  44. package/src/tmdatagrid/TMDataGridFooter.tsx +0 -74
  45. package/src/tmdatagrid/TMDataGridTable.tsx +0 -297
  46. package/src/tmdatagrid/capabilities.ts +0 -117
  47. /package/src/tmdatagrid/{sizes.ts → core/sizes.ts} +0 -0
@@ -3,40 +3,43 @@ import { flexRender, type Header } from "@tanstack/react-table";
3
3
  import { useSelector } from "@tanstack/react-store";
4
4
  import { shallow } from "@tanstack/store";
5
5
  import { type DragEvent, type ReactNode, useRef, useState } from "react";
6
- import classes from "./TMDataGrid.module.css";
6
+ import classes from "./TMDataGridHeaderCell.module.css";
7
+ import sticky from "./sticky.module.css";
7
8
  import {
8
9
  type TMDataGridRowData,
9
10
  useTMDataGridContext,
10
- } from "./TMDataGridContext";
11
+ } from "../TMDataGridContext";
11
12
  import type { TMDataGridColumnLayout } from "./TMDataGridTable";
12
- import { getColumnCapabilities, getGridCapabilities } from "./capabilities";
13
+ import { getColumnCapabilities, getGridCapabilities } from "../core/capabilities";
13
14
  import {
14
15
  getColumnRegion,
15
16
  getStepTargetColumn,
16
17
  moveColumn,
17
18
  moveColumnByStep,
18
19
  type TMDataGridDropSide,
19
- } from "./columnOrdering";
20
- import { getColumnAlign, getColumnLabel } from "./columnUtils";
21
- import { isFilterActive } from "./filterOperators";
20
+ } from "../core/columnOrdering";
21
+ import { getColumnAlign, getColumnLabel } from "../core/columnUtils";
22
+ import { isFilterActive } from "../core/filterOperators";
22
23
  import {
23
24
  ArrowDownIcon,
24
25
  ArrowUpIcon,
26
+ CollapseAllIcon,
25
27
  ColumnsIcon,
26
28
  DotsVerticalIcon,
29
+ ExpandAllIcon,
27
30
  EyeOffIcon,
28
31
  FilterIcon,
32
+ GroupIcon,
29
33
  MoveLeftIcon,
30
34
  MoveRightIcon,
31
35
  PinLeftIcon,
32
36
  PinOffIcon,
33
37
  PinRightIcon,
38
+ UngroupIcon,
34
39
  } from "./icons";
35
- import {
36
- type TMDataGridFeatures,
37
- openColumnFilter,
38
- SELECT_COLUMN_ID,
39
- } from "./useTMDataGrid";
40
+ import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
41
+ import { SELECT_COLUMN_ID } from "./TMDataGridSelectColumn";
42
+ import { type TMDataGridFeatures, openColumnFilter } from "../useTMDataGrid";
40
43
 
41
44
  export type TMDataGridHeader = Header<
42
45
  TMDataGridFeatures,
@@ -65,7 +68,7 @@ export function TMDataGridHeaderCell({
65
68
  // surrounding tree is memoized (React Compiler) or the table lives elsewhere.
66
69
  // `columnOrder` is in the list because the move menu items are derived from
67
70
  // the column's neighbours, which is exactly what an order change invalidates.
68
- const { sorting, columnFilters, resizingColumnId, columnPinning } =
71
+ const { sorting, columnFilters, resizingColumnId, columnPinning, grouping } =
69
72
  useSelector(
70
73
  table.store,
71
74
  (state) => ({
@@ -74,6 +77,7 @@ export function TMDataGridHeaderCell({
74
77
  resizingColumnId: state.columnResizing.isResizingColumn,
75
78
  columnPinning: state.columnPinning,
76
79
  columnOrder: state.columnOrder,
80
+ grouping: state.grouping,
77
81
  }),
78
82
  { compare: shallow },
79
83
  );
@@ -99,9 +103,18 @@ export function TMDataGridHeaderCell({
99
103
  const canPin = isLeaf && capabilities.canPin;
100
104
  const canResize = capabilities.canResize;
101
105
  const canReorder = isLeaf && capabilities.canReorder;
106
+ const canGroup = isLeaf && capabilities.canGroup;
102
107
  const canManageColumns =
103
108
  isLeaf && getGridCapabilities(table, features).canHideAny;
104
109
 
110
+ // Read off the subscribed slice, not from `column.getIsGrouped()`: that is a
111
+ // call on a `column` whose identity survives a grouping change, so the React
112
+ // Compiler would cache the answer and the menu would keep offering to group
113
+ // a column that already is. Same reason the sort and filter flags above are
114
+ // derived from state rather than from `getIsSorted()`.
115
+ const isGrouped = grouping.includes(column.id);
116
+ const isGroupingActive = grouping.length > 0;
117
+
105
118
  // A column only moves inside its own pinned lane, so a header in another one
106
119
  // never lights up as a target. See TMDataGridColumnRegion.
107
120
  const isDragged = draggedColumnId === column.id;
@@ -207,6 +220,64 @@ export function TMDataGridHeaderCell({
207
220
  <Menu.Divider key="filter-divider" />,
208
221
  );
209
222
  }
223
+ if (canGroup) {
224
+ menuItems.push(
225
+ <Menu.Item
226
+ key="group"
227
+ className={isGrouped ? classes.menuItemActive : undefined}
228
+ leftSection={
229
+ isGrouped ? (
230
+ <UngroupIcon size={16} stroke={1.6} />
231
+ ) : (
232
+ <GroupIcon size={16} stroke={1.6} />
233
+ )
234
+ }
235
+ onClick={column.getToggleGroupingHandler()}
236
+ >
237
+ {isGrouped ? `Ungroup ${label}` : `Group by ${label}`}
238
+ </Menu.Item>,
239
+ );
240
+ }
241
+ if (isGroupingActive) {
242
+ // Under the default `groupedColumnMode: "remove"` a grouped column is taken
243
+ // out of the grid, so its own header — and the Ungroup item on it — is no
244
+ // longer reachable. The tree column that replaced it carries them instead,
245
+ // which is also where someone would look for them.
246
+ if (column.id === GROUP_COLUMN_ID) {
247
+ for (const groupedId of grouping) {
248
+ const groupedColumn = table.getColumn(groupedId);
249
+ if (!groupedColumn) continue;
250
+ menuItems.push(
251
+ <Menu.Item
252
+ key={`ungroup-${groupedId}`}
253
+ leftSection={<UngroupIcon size={16} stroke={1.6} />}
254
+ onClick={() => groupedColumn.toggleGrouping()}
255
+ >
256
+ {`Ungroup ${getColumnLabel(groupedColumn)}`}
257
+ </Menu.Item>,
258
+ );
259
+ }
260
+ }
261
+ menuItems.push(
262
+ <Menu.Item
263
+ key="expand-all"
264
+ leftSection={<ExpandAllIcon size={16} stroke={1.6} />}
265
+ onClick={() => table.toggleAllRowsExpanded(true)}
266
+ >
267
+ Expand all groups
268
+ </Menu.Item>,
269
+ <Menu.Item
270
+ key="collapse-all"
271
+ leftSection={<CollapseAllIcon size={16} stroke={1.6} />}
272
+ onClick={() => table.toggleAllRowsExpanded(false)}
273
+ >
274
+ Collapse all groups
275
+ </Menu.Item>,
276
+ );
277
+ }
278
+ if (canGroup || isGroupingActive) {
279
+ menuItems.push(<Menu.Divider key="group-divider" />);
280
+ }
210
281
  if (canPin) {
211
282
  menuItems.push(
212
283
  <Menu.Item
@@ -311,8 +382,8 @@ export function TMDataGridHeaderCell({
311
382
  isDragged ? classes.headerCellDragging : "",
312
383
  dropSide === "before" ? classes.headerCellDropBefore : "",
313
384
  dropSide === "after" ? classes.headerCellDropAfter : "",
314
- layout.isBoundary && pinnedAt === "left" ? classes.stickyLeft : "",
315
- layout.isBoundary && pinnedAt === "right" ? classes.stickyRight : "",
385
+ layout.isBoundary && pinnedAt === "left" ? sticky.stickyLeft : "",
386
+ layout.isBoundary && pinnedAt === "right" ? sticky.stickyRight : "",
316
387
  ]
317
388
  .filter(Boolean)
318
389
  .join(" ");
@@ -324,6 +395,20 @@ export function TMDataGridHeaderCell({
324
395
  data-testid={`dg-header-${column.id}`}
325
396
  data-active={isSorted || isFiltered}
326
397
  data-align={align}
398
+ // The checkbox lane is a fixed track, so it cannot take the cell padding
399
+ // the scale grows for text. See TMDataGridHeaderCell.module.css.
400
+ data-select-column={column.id === SELECT_COLUMN_ID}
401
+ // Only meaningful on a sortable column: "none" advertises that this
402
+ // header sorts, which is a lie on one that doesn't.
403
+ aria-sort={
404
+ canSort
405
+ ? isSorted
406
+ ? sortDirection
407
+ ? "descending"
408
+ : "ascending"
409
+ : "none"
410
+ : undefined
411
+ }
327
412
  className={cellClass}
328
413
  // Dropped while a resize is running, so that dragging the separator
329
414
  // resizes the column instead of starting to move it. The resize handler
@@ -0,0 +1,177 @@
1
+ import { Checkbox } from "@mantine/core";
2
+ import { useSelector } from "@tanstack/react-store";
3
+ import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
4
+ import { useTMDataGridContext } from "../TMDataGridContext";
5
+ import {
6
+ getDisplayedRows,
7
+ getSelectableRowIds,
8
+ resolveRowSelectionClick,
9
+ } from "../core/rowSelection";
10
+ import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
11
+
12
+ export const SELECT_COLUMN_ID = "__select__";
13
+
14
+ /**
15
+ * Both checkboxes subscribe to the table store themselves rather than reading
16
+ * the selection off the render they were handed.
17
+ *
18
+ * A cell is rendered by `flexRender`, which mounts the cell function as its own
19
+ * component — but the body cell above it keeps the same props across a
20
+ * selection change (row, column and layout identities all survive it), so the
21
+ * React Compiler reuses the memoized element and the subtree never re-renders.
22
+ * The row highlight would update, from state `TMDataGridTable` subscribes to,
23
+ * while the box below it stayed unchecked.
24
+ *
25
+ * Selection is derived inside the selector, not from a method call in the
26
+ * component body, for the same reason: `table` and `row` identities survive the
27
+ * change, so a call in the body would be cached along with them.
28
+ */
29
+ function SelectAllCheckbox<TData extends RowData>({
30
+ table,
31
+ }: {
32
+ table: TMDataGridTable<TData>;
33
+ }) {
34
+ const allSelected = useSelector(table.store, () =>
35
+ table.getIsAllRowsSelected(),
36
+ );
37
+ const someSelected = useSelector(table.store, () =>
38
+ table.getIsSomeRowsSelected(),
39
+ );
40
+
41
+ return (
42
+ <Checkbox
43
+ size="xs"
44
+ aria-label="Select all rows"
45
+ checked={allSelected}
46
+ indeterminate={someSelected && !allSelected}
47
+ onChange={table.getToggleAllRowsSelectedHandler()}
48
+ />
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Drops the select-all box when only one row may be selected. Read from the
54
+ * feature flags rather than `table.options.enableMultiRowSelection` directly:
55
+ * the flags are re-derived from a fresh object every render, so the switch
56
+ * cannot be cached along with a `table` identity that survives an options
57
+ * change — the same reason the rest of the chrome reads them. See
58
+ * readFeatureFlags.
59
+ */
60
+ function SelectAllHeader<TData extends RowData>({
61
+ table,
62
+ }: {
63
+ table: TMDataGridTable<TData>;
64
+ }) {
65
+ const { features } = useTMDataGridContext();
66
+ if (!features.multiRowSelection) return null;
67
+ return <SelectAllCheckbox table={table} />;
68
+ }
69
+
70
+ function SelectRowCheckbox<TData extends RowData>({
71
+ row,
72
+ }: {
73
+ row: Row<TMDataGridFeatures, TData>;
74
+ }) {
75
+ // Cells render inside the grid's provider, so the checkbox can reach the
76
+ // chrome store — it needs the shift-click pivot, and the feature flags to know
77
+ // which row model a range is measured over.
78
+ const { ui, features } = useTMDataGridContext();
79
+ const isGroupRow = row.subRows.length > 0;
80
+
81
+ // A group row is never selected by id: `rowSelection` only ever holds the
82
+ // leaves, and TanStack's `getIsSelected()` is a plain lookup in that map. So
83
+ // a group asks about its descendants instead, which is also the honest
84
+ // reading — the box means "all of these", and it goes indeterminate as soon
85
+ // as that stops being true.
86
+ const selected = useSelector(row.table.store, () =>
87
+ isGroupRow ? row.getIsAllSubRowsSelected() : row.getIsSelected(),
88
+ );
89
+ const someSelected = useSelector(row.table.store, () =>
90
+ row.getIsSomeSelected(),
91
+ );
92
+
93
+ // The resolver expands a tick back out to the descendant ids; all the group
94
+ // case needs here is to know whether any of them may be selected at all.
95
+ const selectableIds = getSelectableRowIds(row);
96
+
97
+ // Single-select has no way to express "this group": one box for several rows
98
+ // would either overrun the limit or lie about what it did.
99
+ if (isGroupRow && !row.getCanMultiSelect()) return null;
100
+
101
+ return (
102
+ <Checkbox
103
+ size="xs"
104
+ aria-label={isGroupRow ? "Select group" : "Select row"}
105
+ checked={selected}
106
+ disabled={selectableIds.length === 0}
107
+ indeterminate={someSelected && !selected}
108
+ // Every tick goes through the resolver, shift held or not — plain becomes
109
+ // a toggle that moves the pivot, so a later shift-click extends from the
110
+ // box the user last touched.
111
+ //
112
+ // All of it in `onChange`, not split with an `onClick`: React derives a
113
+ // checkbox's change event from the click, so the modifier keys ride along
114
+ // on the native event, and a `preventDefault()` in a click handler cannot
115
+ // stop the change from firing anyway. Trying to do the range in `onClick`
116
+ // silently loses the clicked row, which `onChange` then toggles back off.
117
+ onChange={(event) => {
118
+ const native = event.nativeEvent;
119
+ const isMouse = native instanceof MouseEvent;
120
+ const resolved = resolveRowSelectionClick({
121
+ rows: getDisplayedRows(row.table, features),
122
+ rowId: row.id,
123
+ anchorRowId: ui.state.selectionAnchorRowId,
124
+ modifiers: {
125
+ toggle: isMouse && (native.ctrlKey || native.metaKey),
126
+ extend: isMouse && native.shiftKey,
127
+ },
128
+ selection: row.table.store.state.rowSelection,
129
+ // A checkbox is only ever additive — ticking one has never cleared
130
+ // the others, so `canReplaceSelection` stays false whatever is held.
131
+ canReplaceSelection: false,
132
+ });
133
+ row.table.setRowSelection(resolved.selection);
134
+ ui.actions.setSelectionAnchor(resolved.anchorRowId);
135
+ }}
136
+ // A row can select on click too; the checkbox must not toggle twice.
137
+ onClick={(event) => event.stopPropagation()}
138
+ />
139
+ );
140
+ }
141
+
142
+ /**
143
+ * The generated checkbox column, prepended under
144
+ * `selectionMode: "checkbox"` (the default) or `"checkboxAndHighlight"`.
145
+ */
146
+ export function createSelectColumn<TData extends RowData>(): ColumnDef<
147
+ TMDataGridFeatures,
148
+ TData,
149
+ unknown
150
+ > {
151
+ return {
152
+ id: SELECT_COLUMN_ID,
153
+ meta: {
154
+ label: "Checkbox selection",
155
+ align: "center",
156
+ // Structurally the first column; it also anchors the left pinned lane, so
157
+ // no other column can be moved in front of it.
158
+ enableOrdering: false,
159
+ },
160
+ size: 48,
161
+ minSize: 48,
162
+ maxSize: 48,
163
+ enableResizing: false,
164
+ enableSorting: false,
165
+ enableColumnFilter: false,
166
+ enableGlobalFilter: false,
167
+ // Structurally pinned to the left; users shouldn't be able to move it.
168
+ enablePinning: false,
169
+ header: ({ table }) => <SelectAllHeader table={table} />,
170
+ cell: ({ row }) => <SelectRowCheckbox row={row} />,
171
+ // Every cell on a group row that is not the grouped column counts as
172
+ // aggregated, this lane included, and an aggregated cell with nothing
173
+ // declared renders blank. Without this the checkbox would disappear from
174
+ // exactly the rows that select a whole group. See renderCellContent.
175
+ aggregatedCell: ({ row }) => <SelectRowCheckbox row={row} />,
176
+ };
177
+ }
@@ -0,0 +1,137 @@
1
+ /* Anchors the filter panel, which floats over the first body rows. */
2
+ .tableWrapper {
3
+ position: relative;
4
+ flex: 1;
5
+ min-height: 0;
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+
10
+ .scrollContainer {
11
+ position: relative;
12
+ flex: 1;
13
+ min-height: 0;
14
+ overflow: auto;
15
+ }
16
+
17
+ /* Column tracks are set inline via gridTemplateColumns. */
18
+ .grid {
19
+ display: grid;
20
+ }
21
+
22
+ /* Rows are real layout boxes (not display: contents) so position: sticky works,
23
+ and subgrid keeps their cells on the parent's column tracks. */
24
+ .headerRow {
25
+ display: grid;
26
+ grid-column: 1 / -1;
27
+ grid-template-columns: subgrid;
28
+ position: sticky;
29
+ top: 0;
30
+ z-index: 3;
31
+ }
32
+
33
+ .bodyRow {
34
+ --row-bg: var(--mantine-color-body);
35
+ display: grid;
36
+ grid-column: 1 / -1;
37
+ grid-template-columns: subgrid;
38
+ align-items: stretch;
39
+ }
40
+
41
+ .bodyRow:hover {
42
+ --row-bg: var(--mantine-color-default-hover);
43
+ }
44
+
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"] {
67
+ --row-bg: var(--dg-row-selected-bg);
68
+ }
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
+
78
+ /* Row selection mode: the row is the control, so it takes focus and must not
79
+ start a text selection on the click that toggles it. */
80
+ .bodyRow[data-selects-on-click="true"] {
81
+ user-select: none;
82
+ }
83
+
84
+ .bodyRow:focus-visible {
85
+ outline: 2px solid var(--mantine-primary-color-filled);
86
+ outline-offset: -2px;
87
+ }
88
+
89
+ /* Clipping lives on .cellContent, not here — a pinned cell has to let its
90
+ boundary gradient escape past its own right/left edge. */
91
+ .bodyCell {
92
+ display: flex;
93
+ align-items: center;
94
+ padding: 0 var(--dg-padding);
95
+ border-bottom: 1px solid var(--mantine-color-default-border);
96
+ /* Inherited from .bodyRow — sticky cells need an opaque background. */
97
+ background-color: var(--row-bg);
98
+ font-size: var(--dg-font-size);
99
+ }
100
+
101
+ /* The checkbox column is a fixed 48px track holding a control of a known width,
102
+ not text that can ellipsize. Cell padding is sized for text and grows with
103
+ the scale, so at `xl` its 18px a side left 12px for a 16px box and the
104
+ checkbox was clipped. The lane centres its control instead. */
105
+ .bodyCell[data-select-column="true"] {
106
+ padding: 0;
107
+ }
108
+
109
+ .cellContent {
110
+ min-width: 0;
111
+ overflow: hidden;
112
+ white-space: nowrap;
113
+ text-overflow: ellipsis;
114
+ }
115
+
116
+ .bodyCell[data-align="right"] {
117
+ justify-content: flex-end;
118
+ }
119
+
120
+ .bodyCell[data-align="center"] {
121
+ justify-content: center;
122
+ }
123
+
124
+ /* Only stretch to fill when left-aligned, so right/centre stay put. */
125
+ .bodyCell[data-align="left"] .cellContent {
126
+ flex: 1 1 auto;
127
+ }
128
+
129
+ .messageRow {
130
+ grid-column: 1 / -1;
131
+ display: flex;
132
+ flex-direction: column;
133
+ align-items: center;
134
+ justify-content: center;
135
+ gap: var(--mantine-spacing-xs);
136
+ padding: var(--mantine-spacing-xl);
137
+ }