@jielga/tmdatagrid 2.0.0-beta.11 → 2.0.0-beta.13

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 +285 -22
  2. package/dist/index.js +2857 -2376
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +1 -2
  6. package/skills/appearance/SKILL.md +1 -1
  7. package/skills/cell-selection/SKILL.md +1 -1
  8. package/skills/columns/SKILL.md +2 -2
  9. package/skills/data/SKILL.md +5 -2
  10. package/skills/editing/SKILL.md +1 -1
  11. package/skills/filtering/SKILL.md +112 -26
  12. package/skills/getting-started/SKILL.md +4 -4
  13. package/skills/grouping/SKILL.md +1 -1
  14. package/skills/options/SKILL.md +3 -1
  15. package/skills/rows/SKILL.md +1 -1
  16. package/skills/server-side/SKILL.md +44 -15
  17. package/skills/testing/SKILL.md +6 -3
  18. package/src/tmdatagrid/components/TMDataGrid.tsx +3 -0
  19. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +1 -1
  20. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +37 -18
  21. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +138 -160
  22. package/src/tmdatagrid/components/TMDataGridFilterSurface.module.css +54 -0
  23. package/src/tmdatagrid/components/TMDataGridFilterSurface.tsx +162 -0
  24. package/src/tmdatagrid/components/TMDataGridFooter.tsx +28 -1
  25. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +10 -0
  26. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -3
  27. package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.module.css +51 -0
  28. package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.tsx +303 -0
  29. package/src/tmdatagrid/components/TMDataGridTable.module.css +9 -41
  30. package/src/tmdatagrid/components/TMDataGridTable.tsx +84 -15
  31. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +37 -11
  32. package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +5 -4
  33. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +22 -5
  34. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +5 -1
  35. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +5 -1
  36. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +44 -28
  37. package/src/tmdatagrid/components/filters/controlLayout.ts +32 -0
  38. package/src/tmdatagrid/components/filters/filterControlFor.ts +65 -0
  39. package/src/tmdatagrid/components/sticky.module.css +44 -0
  40. package/src/tmdatagrid/core/columnOptions.ts +46 -0
  41. package/src/tmdatagrid/core/controlledStateSync.ts +108 -0
  42. package/src/tmdatagrid/core/editEngine.ts +68 -12
  43. package/src/tmdatagrid/core/filterControls.ts +28 -0
  44. package/src/tmdatagrid/core/filterOperators.ts +64 -1
  45. package/src/tmdatagrid/core/filterSurface.ts +99 -0
  46. package/src/tmdatagrid/core/labels.ts +7 -0
  47. package/src/tmdatagrid/core/labelsSv.ts +3 -0
  48. package/src/tmdatagrid/core/pageReset.ts +115 -0
  49. package/src/tmdatagrid/index.ts +13 -0
  50. package/src/tmdatagrid/useTMDataGrid.tsx +191 -11
@@ -1,30 +1,49 @@
1
- /* Floats over the first body rows, anchored to `.tableWrapper`. Sits just
2
- below the header row, so its offset follows the size scale rather than a
3
- fixed pixel count. */
1
+ /* A plain block of controls. Nothing here positions it - the popup and the
2
+ sidebar own their own placement, and a consumer placing the panel by hand
3
+ places it the way any other block. */
4
4
  .filterPanel {
5
- position: absolute;
6
- top: calc(var(--dg-header-height) + 2px);
7
- left: var(--mantine-spacing-md);
8
- z-index: 5;
9
- padding: var(--mantine-spacing-sm);
10
- border-radius: var(--mantine-radius-md);
11
- border: 1px solid var(--mantine-color-default-border);
12
- background-color: var(--mantine-color-body);
13
- box-shadow: var(--mantine-shadow-md);
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--mantine-spacing-xs);
14
8
  }
15
9
 
10
+ /* Column, operator and value side by side, bottom-aligned so their fields line
11
+ up under labels of differing heights. */
16
12
  .filterRow {
17
13
  display: flex;
18
14
  align-items: flex-end;
19
15
  gap: var(--mantine-spacing-xs);
20
16
  }
21
17
 
22
- .filterPanelHeader {
23
- display: flex;
24
- align-items: center;
25
- justify-content: space-between;
26
- gap: var(--mantine-spacing-md);
27
- padding-bottom: var(--mantine-spacing-xs);
18
+ /* The narrow-host layout: the same three fields one under the other, each
19
+ filling the width, with the ✕ beside the column it removes. A card rather
20
+ than a bare stack, because without the side-by-side alignment there is
21
+ nothing else saying where one filter ends and the next begins. */
22
+ .filterRow[data-layout="stacked"] {
23
+ display: grid;
24
+ grid-template-columns: 1fr auto;
25
+ align-items: end;
26
+ gap: 6px var(--mantine-spacing-xs);
27
+ padding: var(--mantine-spacing-xs);
28
+ border: 1px solid var(--mantine-color-default-border);
29
+ border-radius: var(--mantine-radius-sm);
30
+ }
31
+
32
+ .filterRow[data-layout="stacked"] .filterRemove {
33
+ grid-column: 2;
34
+ grid-row: 1;
35
+ }
36
+
37
+ .filterRow[data-layout="stacked"] .filterColumnSlot {
38
+ grid-column: 1;
39
+ grid-row: 1;
40
+ min-width: 0;
41
+ }
42
+
43
+ .filterRow[data-layout="stacked"] .filterOperatorSlot,
44
+ .filterRow[data-layout="stacked"] .filterValueSlot {
45
+ grid-column: 1 / -1;
46
+ min-width: 0;
28
47
  }
29
48
 
30
49
  .filterPanelFooter {
@@ -1,19 +1,10 @@
1
- import {
2
- ActionIcon,
3
- Button,
4
- Select,
5
- Stack,
6
- Text,
7
- TextInput,
8
- } from "@mantine/core";
1
+ import { ActionIcon, Button, Select, Stack, TextInput } from "@mantine/core";
9
2
  import { useSelector } from "@tanstack/react-store";
10
3
  import { useEffect, useRef } from "react";
11
4
  import classes from "./TMDataGridFilterPanel.module.css";
12
5
  import { useTMDataGridContext } from "../TMDataGridContext";
13
- import { resolveColumnOptions } from "../core/columnOptions";
14
6
  import {
15
7
  getColumnDefaultOperator,
16
- getColumnFilterControl,
17
8
  getColumnLabel,
18
9
  getColumnType,
19
10
  } from "../core/columnUtils";
@@ -21,14 +12,15 @@ import {
21
12
  type TMDataGridFilterOperator,
22
13
  type TMDataGridFilterValue,
23
14
  emptyValueForOperator,
15
+ filterValueShape,
24
16
  getDefaultOperator,
25
17
  getOperatorsForType,
26
18
  isTMDataGridFilterValue,
27
19
  operatorNeedsValue,
28
- operatorTakesArrayValue,
29
- operatorTakesRangeValue,
30
20
  } from "../core/filterOperators";
21
+ import type { TMDataGridFilterPanelLayout } from "../core/filterControls";
31
22
  import { CloseIcon } from "./icons";
23
+ import { filterControlFor } from "./filters/filterControlFor";
32
24
  import { TMDataGridFilterValueInput } from "./filters/TMDataGridFilterValueInput";
33
25
 
34
26
  const FALLBACK_FILTER: TMDataGridFilterValue = { operator: "contains", value: "" };
@@ -37,55 +29,63 @@ function asFilterValue(value: unknown): TMDataGridFilterValue {
37
29
  return isTMDataGridFilterValue(value) ? value : FALLBACK_FILTER;
38
30
  }
39
31
 
40
- /**
41
- * The three value shapes an operator can take. A typed value survives an
42
- * operator or column change only within its shape - a set is not a range,
43
- * even though both are arrays.
44
- */
45
- function valueShape(
46
- operator: TMDataGridFilterOperator,
47
- ): "scalar" | "set" | "range" {
48
- if (operatorTakesArrayValue(operator)) return "set";
49
- if (operatorTakesRangeValue(operator)) return "range";
50
- return "scalar";
51
- }
32
+ export type TMDataGridFilterPanelProps = {
33
+ /**
34
+ * How one filter row is laid out.
35
+ *
36
+ * `"row"` - the default - puts column, operator and value side by side,
37
+ * which wants about 550px. `"stacked"` puts them one under the other, each
38
+ * filling the width, for a host too narrow for that: the sidebar surface
39
+ * uses it, and so should a panel you place in a drawer or a narrow column.
40
+ *
41
+ * Passed through to every value control as its `layout`, so a
42
+ * `meta.filter.control` can size itself to the same decision.
43
+ */
44
+ layout?: TMDataGridFilterPanelLayout;
45
+ };
52
46
 
53
47
  /**
54
48
  * The MUI-style filter surface: one row per active column filter, each row a
55
- * column / operator / value triple. It only ever reads and writes the table's
56
- * `columnFilters` state, so a `manualFiltering` grid gets the same panel for
57
- * free - the state is forwarded to the server instead of a row model.
49
+ * column / operator / value triple, over an "Add filter" / "Clear all" footer.
50
+ *
51
+ * A plain block of controls, the way `TMDataGrid.ColumnsPanel` is - it renders
52
+ * whenever it is mounted and knows nothing about floating. The chrome that
53
+ * makes a floating surface (a title, a close button, click-away, Escape)
54
+ * belongs to the popup and the sidebar, so this can equally be dropped into a
55
+ * drawer, a page column or a form. See the `filters.surface` option.
56
+ *
57
+ * It only ever reads and writes the table's `columnFilters` state, so a
58
+ * `manualFiltering` grid gets the same panel for free - the state is forwarded
59
+ * to the server instead of a row model.
58
60
  */
59
- export function TMDataGridFilterPanel() {
61
+ export function TMDataGridFilterPanel({
62
+ layout = "row",
63
+ }: TMDataGridFilterPanelProps = {}) {
60
64
  const { table, ui, labels, controlSize } = useTMDataGridContext();
61
- const opened = useSelector(ui, (state) => state.filterPanelOpen);
62
65
  const columnFilters = useSelector(
63
66
  table.store,
64
67
  (state) => state.columnFilters,
65
68
  );
69
+ const focusColumnId = useSelector(ui, (state) => state.filterPanelColumnId);
66
70
  const panelRef = useRef<HTMLDivElement>(null);
71
+ const stacked = layout === "stacked";
67
72
 
68
- // Clicking away hides the panel, the way any floating surface behaves. On
69
- // pointerdown rather than click, so a press that starts outside dismisses it
70
- // even when the pointer is released somewhere else.
73
+ // Whoever pointed at a column - the column menu, a pill, the toolbar button -
74
+ // meant "let me type here", so the value slot takes the focus. Cleared once
75
+ // taken, which is what lets the same column be pointed at twice.
71
76
  useEffect(() => {
72
- if (!opened) return;
73
-
74
- const handlePointerDown = (event: PointerEvent) => {
75
- const target = event.target;
76
- if (!(target instanceof Element)) return;
77
- if (panelRef.current?.contains(target)) return;
78
- // The toolbar button toggles the panel itself. Closing from here first
79
- // would leave its click reopening what the user meant to close.
80
- if (target.closest('[data-dg-part="filter-button"]')) return;
81
- ui.actions.closeFilterPanel();
82
- };
83
-
84
- document.addEventListener("pointerdown", handlePointerDown);
85
- return () => document.removeEventListener("pointerdown", handlePointerDown);
86
- }, [opened, ui]);
87
-
88
- if (!opened) return null;
77
+ if (focusColumnId === null) return;
78
+ const row = panelRef.current?.querySelector<HTMLElement>(
79
+ `[data-dg-part="filter-row"][data-column-id="${CSS.escape(focusColumnId)}"]`,
80
+ );
81
+ const slot = row?.querySelector<HTMLElement>("[data-dg-filter-value-slot]");
82
+ slot
83
+ ?.querySelector<HTMLElement>(
84
+ "input:not([type='hidden']), textarea, select, [tabindex]:not([tabindex='-1'])",
85
+ )
86
+ ?.focus();
87
+ ui.actions.focusPanelFilter(null);
88
+ }, [focusColumnId, ui]);
89
89
 
90
90
  const filterableColumns = table
91
91
  .getAllLeafColumns()
@@ -97,9 +97,9 @@ export function TMDataGridFilterPanel() {
97
97
  }));
98
98
 
99
99
  function removeFilter(columnId: string) {
100
- const remaining = columnFilters.filter((filter) => filter.id !== columnId);
101
- table.setColumnFilters(remaining);
102
- if (remaining.length === 0) ui.actions.closeFilterPanel();
100
+ table.setColumnFilters(
101
+ columnFilters.filter((filter) => filter.id !== columnId),
102
+ );
103
103
  }
104
104
 
105
105
  function changeFilterColumn(fromColumnId: string, toColumnId: string) {
@@ -114,7 +114,7 @@ export function TMDataGridFilterPanel() {
114
114
  // operator - a text needle has no meaning to an `isAnyOf` set, or a set
115
115
  // to a text input.
116
116
  const value =
117
- valueShape(operator) === valueShape(current.operator)
117
+ filterValueShape(operator) === filterValueShape(current.operator)
118
118
  ? current.value
119
119
  : emptyValueForOperator(operator);
120
120
  table.setColumnFilters(
@@ -129,7 +129,9 @@ export function TMDataGridFilterPanel() {
129
129
  : filter,
130
130
  ),
131
131
  );
132
- ui.actions.openFilterPanel(toColumnId);
132
+ // The row now names a different column; put the caret back in its value
133
+ // slot rather than leaving it on a dropdown whose meaning just changed.
134
+ ui.actions.focusPanelFilter(toColumnId);
133
135
  }
134
136
 
135
137
  function patchFilter(columnId: string, patch: Partial<TMDataGridFilterValue>) {
@@ -149,20 +151,12 @@ export function TMDataGridFilterPanel() {
149
151
  // Same arity rule as changing the column: keep the value across operators
150
152
  // of the same shape, reset it across any shape boundary.
151
153
  const value =
152
- valueShape(operator) === valueShape(current.operator)
154
+ filterValueShape(operator) === filterValueShape(current.operator)
153
155
  ? current.value
154
156
  : emptyValueForOperator(operator);
155
157
  patchFilter(columnId, { operator, value });
156
158
  }
157
159
 
158
- /** Clears every filter, including half-typed ones the pills never showed. */
159
- function clearAllFilters() {
160
- table.setColumnFilters([]);
161
- // Same exit as removing the last filter row by hand: an empty panel has
162
- // nothing to show but its own buttons.
163
- ui.actions.closeFilterPanel();
164
- }
165
-
166
160
  function addFilter() {
167
161
  const nextColumn = filterableColumns.find(
168
162
  (column) => !columnFilters.some((filter) => filter.id === column.id),
@@ -179,6 +173,7 @@ export function TMDataGridFilterPanel() {
179
173
  } satisfies TMDataGridFilterValue,
180
174
  },
181
175
  ]);
176
+ ui.actions.focusPanelFilter(nextColumn.id);
182
177
  }
183
178
 
184
179
  const canAddFilter = filterableColumns.some(
@@ -191,31 +186,9 @@ export function TMDataGridFilterPanel() {
191
186
  role="group"
192
187
  aria-label={labels.filters}
193
188
  data-dg-part="filter-panel"
189
+ data-layout={layout}
194
190
  className={classes.filterPanel}
195
- // Escape is what closes a floating surface, and every control that can
196
- // hold focus in here sits inside this element.
197
- onKeyDown={(event) => {
198
- if (event.key !== "Escape") return;
199
- event.stopPropagation();
200
- ui.actions.closeFilterPanel();
201
- }}
202
191
  >
203
- <div className={classes.filterPanelHeader}>
204
- <Text span size={controlSize} fw={600}>
205
- {labels.filters}
206
- </Text>
207
- <ActionIcon
208
- variant="subtle"
209
- color="gray"
210
- size="sm"
211
- aria-label={labels.closeFilters}
212
- data-dg-part="filter-panel-close"
213
- onClick={ui.actions.closeFilterPanel}
214
- >
215
- <CloseIcon size={16} stroke={1.6} />
216
- </ActionIcon>
217
- </div>
218
-
219
192
  <Stack gap="xs">
220
193
  {columnFilters.map((filter) => {
221
194
  const column = table.getColumn(filter.id);
@@ -223,21 +196,11 @@ export function TMDataGridFilterPanel() {
223
196
  const type = column ? getColumnType(column) : "string";
224
197
  const needsValue = operatorNeedsValue(value.operator);
225
198
  const scalarValue = typeof value.value === "string" ? value.value : "";
226
- // Pre-resolved only where options mean something out of the box -
227
- // a declared set, or a select-shaped column's faceted values. A
228
- // custom control wanting faceted values elsewhere resolves them
229
- // itself; resolving here would build the faceted index for every
230
- // filtered column.
231
- const options =
232
- column &&
233
- (column.columnDef.meta?.options !== undefined ||
234
- type === "select" ||
235
- type === "multiSelect")
236
- ? resolveColumnOptions({ table, column, fallback: "faceted" })
237
- : [];
238
- const ValueControl =
239
- (column === undefined ? undefined : getColumnFilterControl(column)) ??
240
- TMDataGridFilterValueInput;
199
+ // The same options-and-control decision the header cells make.
200
+ const { options, ValueControl } =
201
+ column === undefined
202
+ ? { options: [], ValueControl: TMDataGridFilterValueInput }
203
+ : filterControlFor(table, column);
241
204
 
242
205
  return (
243
206
  <div
@@ -247,12 +210,14 @@ export function TMDataGridFilterPanel() {
247
210
  // count on is which row it is in.
248
211
  data-dg-part="filter-row"
249
212
  data-column-id={filter.id}
213
+ data-layout={layout}
250
214
  className={classes.filterRow}
251
215
  >
252
216
  <ActionIcon
253
217
  variant="subtle"
254
218
  color="gray"
255
219
  size="lg"
220
+ className={classes.filterRemove}
256
221
  aria-label={labels.removeFilter}
257
222
  data-dg-part="filter-remove"
258
223
  onClick={() => removeFilter(filter.id)}
@@ -260,69 +225,82 @@ export function TMDataGridFilterPanel() {
260
225
  <CloseIcon size={18} stroke={1.6} />
261
226
  </ActionIcon>
262
227
 
263
- <Select
264
- label={labels.filterColumn}
265
- size={controlSize}
266
- w={160}
267
- allowDeselect={false}
268
- // Both dropdowns render inside the panel: a portalled one is
269
- // outside it in the DOM, and picking an option would read as a
270
- // click away and close the panel under the user.
271
- comboboxProps={{ withinPortal: false }}
272
- data-dg-part="filter-column"
273
- data={columnOptions}
274
- value={filter.id}
275
- onChange={(next) => next && changeFilterColumn(filter.id, next)}
276
- />
277
-
278
- <Select
279
- label={labels.filterOperator}
280
- size={controlSize}
281
- w={170}
282
- allowDeselect={false}
283
- comboboxProps={{ withinPortal: false }}
284
- data-dg-part="filter-operator"
285
- data={getOperatorsForType(type).map((operator) => ({
286
- value: operator,
287
- label: labels.operators[operator],
288
- }))}
289
- value={value.operator}
290
- onChange={(next) =>
291
- next &&
292
- changeOperator(filter.id, next as TMDataGridFilterOperator)
293
- }
294
- />
295
-
296
- {column ? (
297
- // The value slot: `meta.filter.control` if the column declares
298
- // one, the built-in shape-by-operator input otherwise - both
299
- // through the same value-only contract.
300
- <ValueControl
301
- column={column}
302
- table={table}
303
- operator={value.operator}
304
- value={value.value}
305
- onChange={(next) => patchFilter(filter.id, { value: next })}
306
- options={options}
228
+ <div className={classes.filterColumnSlot}>
229
+ <Select
230
+ label={labels.filterColumn}
307
231
  size={controlSize}
308
- labels={labels}
232
+ w={stacked ? "100%" : 160}
233
+ allowDeselect={false}
234
+ // Both dropdowns portal, like every dropdown in the panel:
235
+ // drawn inline they are clipped by the grid frame's
236
+ // `overflow: hidden` under the popup, or by the sidebar's
237
+ // scroller. The popup's click-away exempts portal nodes.
238
+ data-dg-part="filter-column"
239
+ data={columnOptions}
240
+ value={filter.id}
241
+ onChange={(next) =>
242
+ next && changeFilterColumn(filter.id, next)
243
+ }
309
244
  />
310
- ) : (
311
- // The column is gone from the definition; the row survives
312
- // only to be re-pointed or removed.
313
- <TextInput
314
- label={labels.filterValue}
245
+ </div>
246
+
247
+ <div className={classes.filterOperatorSlot}>
248
+ <Select
249
+ label={labels.filterOperator}
315
250
  size={controlSize}
316
- w={180}
317
- data-dg-part="filter-value"
318
- disabled={!needsValue}
319
- placeholder={needsValue ? labels.filterValuePlaceholder : ""}
320
- value={needsValue ? scalarValue : ""}
321
- onChange={(event) =>
322
- patchFilter(filter.id, { value: event.currentTarget.value })
251
+ w={stacked ? "100%" : 170}
252
+ allowDeselect={false}
253
+ data-dg-part="filter-operator"
254
+ data={getOperatorsForType(type).map((operator) => ({
255
+ value: operator,
256
+ label: labels.operators[operator],
257
+ }))}
258
+ value={value.operator}
259
+ onChange={(next) =>
260
+ next &&
261
+ changeOperator(filter.id, next as TMDataGridFilterOperator)
323
262
  }
324
263
  />
325
- )}
264
+ </div>
265
+
266
+ {/* What the focus effect above aims at. A `meta.filter.control`
267
+ may render anything, so the slot is the only landmark the
268
+ panel can count on. */}
269
+ <div className={classes.filterValueSlot} data-dg-filter-value-slot>
270
+ {column ? (
271
+ // `meta.filter.control` if the column declares one, the
272
+ // built-in shape-by-operator input otherwise - both through
273
+ // the same value-only contract.
274
+ <ValueControl
275
+ column={column}
276
+ table={table}
277
+ operator={value.operator}
278
+ value={value.value}
279
+ onChange={(next) => patchFilter(filter.id, { value: next })}
280
+ options={options}
281
+ size={controlSize}
282
+ labels={labels}
283
+ layout={layout}
284
+ />
285
+ ) : (
286
+ // The column is gone from the definition; the row survives
287
+ // only to be re-pointed or removed.
288
+ <TextInput
289
+ label={labels.filterValue}
290
+ size={controlSize}
291
+ w={stacked ? "100%" : 180}
292
+ data-dg-part="filter-value"
293
+ disabled={!needsValue}
294
+ placeholder={needsValue ? labels.filterValuePlaceholder : ""}
295
+ value={needsValue ? scalarValue : ""}
296
+ onChange={(event) =>
297
+ patchFilter(filter.id, {
298
+ value: event.currentTarget.value,
299
+ })
300
+ }
301
+ />
302
+ )}
303
+ </div>
326
304
  </div>
327
305
  );
328
306
  })}
@@ -343,7 +321,7 @@ export function TMDataGridFilterPanel() {
343
321
  size="compact-sm"
344
322
  disabled={columnFilters.length === 0}
345
323
  data-dg-part="filter-clear-all"
346
- onClick={clearAllFilters}
324
+ onClick={() => table.setColumnFilters([])}
347
325
  >
348
326
  {labels.clearAllFilters}
349
327
  </Button>
@@ -0,0 +1,54 @@
1
+ /* Chrome shared by the two automatic surfaces. The panel inside them is a
2
+ plain block; everything that makes one float or sit beside the rows is
3
+ here. */
4
+
5
+ .surfaceHeader {
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: space-between;
9
+ gap: var(--mantine-spacing-md);
10
+ padding-bottom: var(--mantine-spacing-xs);
11
+ }
12
+
13
+ /* Floats over the first body rows, anchored to `.tableWrapper`. Sits below the
14
+ whole header - group rows and the filter row included - which the table
15
+ measures onto the wrapper as `--dg-header-stack-height`. The size scale's
16
+ single-row value is the fallback for the frame before the first
17
+ measurement. */
18
+ .filterPopup {
19
+ position: absolute;
20
+ top: calc(var(--dg-header-stack-height, var(--dg-header-height)) + 2px);
21
+ left: var(--mantine-spacing-md);
22
+ z-index: var(--dg-z-filter-popup, 20);
23
+ padding: var(--mantine-spacing-sm);
24
+ border-radius: var(--mantine-radius-md);
25
+ border: 1px solid var(--mantine-color-default-border);
26
+ background-color: var(--mantine-color-body);
27
+ box-shadow: var(--mantine-shadow-md);
28
+ }
29
+
30
+ /* A column of the table wrapper rather than a layer over it: the rows give up
31
+ the width instead of being covered by it. `flex: none` so the declared
32
+ width holds whatever the panel inside asks for. */
33
+ .filterSidebar {
34
+ display: flex;
35
+ flex-direction: column;
36
+ flex: none;
37
+ min-width: 0;
38
+ padding: var(--mantine-spacing-sm);
39
+ background-color: var(--mantine-color-body);
40
+ border-left: 1px solid var(--mantine-color-default-border);
41
+ }
42
+
43
+ /* On the left it is the same panel with its divider on the other edge, and
44
+ ahead of the scroll container in the flex order. */
45
+ .filterSidebar[data-side="left"] {
46
+ order: -1;
47
+ border-left: none;
48
+ border-right: 1px solid var(--mantine-color-default-border);
49
+ }
50
+
51
+ .filterSidebarBody {
52
+ flex: 1;
53
+ min-height: 0;
54
+ }