@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
@@ -0,0 +1,73 @@
1
+ /* The grid root: the size scale every other module reads, and the frame.
2
+ Each component owns its own module; these variables cascade into all of
3
+ them, and the pinned-column boundary shared by header and body cells lives
4
+ in sticky.module.css. */
5
+
6
+ /* Size scale. Row heights mirror SIZE_ROW_HEIGHT in core/sizes.ts — the
7
+ virtualizer needs the number in JS, so the two have to agree. */
8
+ .root[data-size="xs"] {
9
+ --dg-row-height: 34px;
10
+ --dg-header-height: 32px;
11
+ --dg-font-size: var(--mantine-font-size-xs);
12
+ --dg-padding: 6px;
13
+ }
14
+
15
+ .root[data-size="sm"] {
16
+ --dg-row-height: 42px;
17
+ --dg-header-height: 38px;
18
+ --dg-font-size: var(--mantine-font-size-sm);
19
+ --dg-padding: 8px;
20
+ }
21
+
22
+ .root[data-size="md"] {
23
+ --dg-row-height: 52px;
24
+ --dg-header-height: 44px;
25
+ --dg-font-size: var(--mantine-font-size-sm);
26
+ --dg-padding: 10px;
27
+ }
28
+
29
+ .root[data-size="lg"] {
30
+ --dg-row-height: 62px;
31
+ --dg-header-height: 52px;
32
+ --dg-font-size: var(--mantine-font-size-md);
33
+ --dg-padding: 14px;
34
+ }
35
+
36
+ .root[data-size="xl"] {
37
+ --dg-row-height: 72px;
38
+ --dg-header-height: 60px;
39
+ --dg-font-size: var(--mantine-font-size-lg);
40
+ --dg-padding: 18px;
41
+ }
42
+
43
+ .root {
44
+ /* Fallbacks, so the parts still work if rendered outside a sized root. */
45
+ --dg-row-height: 52px;
46
+ --dg-header-height: 44px;
47
+ --dg-font-size: var(--mantine-font-size-sm);
48
+ --dg-padding: 10px;
49
+ /* Background of a selected row, when `showSelectedBackground` is on. Override
50
+ it on the grid:
51
+ <TMDataGrid style={{ "--dg-row-selected-bg": "var(--mantine-color-blue-0)" }} /> */
52
+ --dg-row-selected-bg: var(--mantine-primary-color-light);
53
+ /* Background of the highlighted row, under a selectionMode that has one.
54
+ Stronger than the selected background by default, since under
55
+ "checkboxAndHighlight" the two show up together. */
56
+ --dg-row-highlight-bg: var(--mantine-primary-color-light-hover);
57
+ /* Background of a group row. Transparent by default — the bold label and the
58
+ chevron are enough, and with the tree collapsed every row on screen is a
59
+ group row, so a tint would grey out the whole grid. Set it to give groups a
60
+ band of their own. */
61
+ --dg-row-group-bg: var(--mantine-color-body);
62
+
63
+ display: flex;
64
+ flex-direction: column;
65
+ min-height: 0;
66
+ overflow: hidden;
67
+ /* Header text is a stretched flex item, so an inherited `text-align: center`
68
+ from the host page would centre headers away from their cell content. */
69
+ text-align: left;
70
+ border: 1px solid var(--mantine-color-default-border);
71
+ border-radius: var(--mantine-radius-md);
72
+ background-color: var(--mantine-color-body);
73
+ }
@@ -2,8 +2,9 @@ import type { RowData } from "@tanstack/react-table";
2
2
  import { type CSSProperties, type ReactNode, useMemo } from "react";
3
3
  import classes from "./TMDataGrid.module.css";
4
4
  import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
5
- import { TMDataGridContextProvider } from "./TMDataGridContext";
5
+ import { TMDataGridContextProvider } from "../TMDataGridContext";
6
6
  import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
7
+ import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
7
8
  import { TMDataGridFooter } from "./TMDataGridFooter";
8
9
  import { TMDataGridTable } from "./TMDataGridTable";
9
10
  import {
@@ -18,8 +19,8 @@ import {
18
19
  SIZE_CONTROL_SIZE,
19
20
  SIZE_ROW_HEIGHT,
20
21
  type TMDataGridSize,
21
- } from "./sizes";
22
- import type { TMDataGridApi } from "./useTMDataGrid";
22
+ } from "../core/sizes";
23
+ import type { TMDataGridApi } from "../useTMDataGrid";
23
24
 
24
25
  export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
25
26
  children: ReactNode;
@@ -29,7 +30,12 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
29
30
  */
30
31
  size?: TMDataGridSize;
31
32
  className?: string;
32
- style?: CSSProperties;
33
+ /**
34
+ * Inline styles. Widened to accept CSS variables, which is how the grid's
35
+ * colours are themed — `--dg-row-selected-bg` for the selected row
36
+ * background. Plain `CSSProperties` rejects `--*` keys.
37
+ */
38
+ style?: CSSProperties & Record<`--${string}`, string | number>;
33
39
  };
34
40
 
35
41
  /**
@@ -96,6 +102,11 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
96
102
  Footer: TMDataGridFooter,
97
103
  /** Rendered by `TMDataGrid.Table`; exported for custom layouts. */
98
104
  FilterPanel: TMDataGridFilterPanel,
105
+ /**
106
+ * Takes the grid as an `api` prop rather than from context, so it can be
107
+ * rendered outside `<TMDataGrid>` — a page header, for instance.
108
+ */
109
+ FilterPills: TMDataGridFilterPills,
99
110
  /** Rendered by `TMDataGrid.ColumnsButton`; exported for custom layouts. */
100
111
  ColumnsPanel: TMDataGridColumnsPanel,
101
112
  });
@@ -0,0 +1,25 @@
1
+ .columnsPanel {
2
+ width: 280px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ .columnsPanelSearch {
8
+ padding: var(--mantine-spacing-sm);
9
+ }
10
+
11
+ .columnsPanelList {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--mantine-spacing-sm);
15
+ padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-sm);
16
+ }
17
+
18
+ .columnsPanelFooter {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ gap: var(--mantine-spacing-sm);
23
+ padding: var(--mantine-spacing-sm);
24
+ border-top: 1px solid var(--mantine-color-default-border);
25
+ }
@@ -1,9 +1,10 @@
1
1
  import { Button, Checkbox, ScrollArea, Text, TextInput } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
3
  import { useState } from "react";
4
- import classes from "./TMDataGrid.module.css";
5
- import { useTMDataGridContext } from "./TMDataGridContext";
6
- import { getColumnLabel } from "./columnUtils";
4
+ import classes from "./TMDataGridColumnsPanel.module.css";
5
+ import { useTMDataGridContext } from "../TMDataGridContext";
6
+ import { getColumnLabel } from "../core/columnUtils";
7
+ import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
7
8
  import { SearchIcon } from "./icons";
8
9
 
9
10
  /**
@@ -20,7 +21,12 @@ export function TMDataGridColumnsPanel() {
20
21
  (state) => state.columnVisibility,
21
22
  );
22
23
 
23
- const columns = table.getAllLeafColumns();
24
+ // The tree column is left out rather than listed and disabled: its visibility
25
+ // is not a setting at all, it follows the grouping state, so an unchecked box
26
+ // that cannot be ticked would only invite the question.
27
+ const columns = table
28
+ .getAllLeafColumns()
29
+ .filter((column) => column.id !== GROUP_COLUMN_ID);
24
30
  const needle = search.trim().toLowerCase();
25
31
  const visibleInPanel = needle
26
32
  ? columns.filter((column) =>
@@ -0,0 +1,35 @@
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. */
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);
14
+ }
15
+
16
+ .filterRow {
17
+ display: flex;
18
+ align-items: flex-end;
19
+ gap: var(--mantine-spacing-xs);
20
+ }
21
+
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);
28
+ }
29
+
30
+ .filterPanelFooter {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ gap: var(--mantine-spacing-md);
35
+ }
@@ -1,8 +1,9 @@
1
- import { ActionIcon, Button, Select, Stack, TextInput } from "@mantine/core";
1
+ import { ActionIcon, Button, Select, Stack, Text, TextInput } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
- import classes from "./TMDataGrid.module.css";
4
- import { useTMDataGridContext } from "./TMDataGridContext";
5
- import { getColumnLabel, getColumnType } from "./columnUtils";
3
+ import { useEffect, useRef } from "react";
4
+ import classes from "./TMDataGridFilterPanel.module.css";
5
+ import { useTMDataGridContext } from "../TMDataGridContext";
6
+ import { getColumnLabel, getColumnType } from "../core/columnUtils";
6
7
  import {
7
8
  type TMDataGridFilterOperator,
8
9
  type TMDataGridFilterValue,
@@ -11,7 +12,7 @@ import {
11
12
  getOperatorsForType,
12
13
  isTMDataGridFilterValue,
13
14
  operatorNeedsValue,
14
- } from "./filterOperators";
15
+ } from "../core/filterOperators";
15
16
  import { CloseIcon } from "./icons";
16
17
 
17
18
  const FALLBACK_FILTER: TMDataGridFilterValue = { operator: "contains", value: "" };
@@ -33,6 +34,27 @@ export function TMDataGridFilterPanel() {
33
34
  table.store,
34
35
  (state) => state.columnFilters,
35
36
  );
37
+ const panelRef = useRef<HTMLDivElement>(null);
38
+
39
+ // Clicking away hides the panel, the way any floating surface behaves. On
40
+ // pointerdown rather than click, so a press that starts outside dismisses it
41
+ // even when the pointer is released somewhere else.
42
+ useEffect(() => {
43
+ if (!opened) return;
44
+
45
+ const handlePointerDown = (event: PointerEvent) => {
46
+ const target = event.target;
47
+ if (!(target instanceof Element)) return;
48
+ if (panelRef.current?.contains(target)) return;
49
+ // The toolbar button toggles the panel itself. Closing from here first
50
+ // would leave its click reopening what the user meant to close.
51
+ if (target.closest("[data-dg-filter-toggle]")) return;
52
+ ui.actions.closeFilterPanel();
53
+ };
54
+
55
+ document.addEventListener("pointerdown", handlePointerDown);
56
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
57
+ }, [opened, ui]);
36
58
 
37
59
  if (!opened) return null;
38
60
 
@@ -86,6 +108,14 @@ export function TMDataGridFilterPanel() {
86
108
  );
87
109
  }
88
110
 
111
+ /** Clears every filter, including half-typed ones the pills never showed. */
112
+ function clearAllFilters() {
113
+ table.setColumnFilters([]);
114
+ // Same exit as removing the last filter row by hand: an empty panel has
115
+ // nothing to show but its own buttons.
116
+ ui.actions.closeFilterPanel();
117
+ }
118
+
89
119
  function addFilter() {
90
120
  const nextColumn = filterableColumns.find(
91
121
  (column) => !columnFilters.some((filter) => filter.id === column.id),
@@ -103,8 +133,39 @@ export function TMDataGridFilterPanel() {
103
133
  ]);
104
134
  }
105
135
 
136
+ const canAddFilter = filterableColumns.some(
137
+ (column) => !columnFilters.some((filter) => filter.id === column.id),
138
+ );
139
+
106
140
  return (
107
- <div className={classes.filterPanel}>
141
+ <div
142
+ ref={panelRef}
143
+ role="group"
144
+ aria-label="Filters"
145
+ className={classes.filterPanel}
146
+ // Escape is what closes a floating surface, and every control that can
147
+ // hold focus in here sits inside this element.
148
+ onKeyDown={(event) => {
149
+ if (event.key !== "Escape") return;
150
+ event.stopPropagation();
151
+ ui.actions.closeFilterPanel();
152
+ }}
153
+ >
154
+ <div className={classes.filterPanelHeader}>
155
+ <Text size={controlSize} fw={600}>
156
+ Filters
157
+ </Text>
158
+ <ActionIcon
159
+ variant="subtle"
160
+ color="gray"
161
+ size="sm"
162
+ aria-label="Close filters"
163
+ onClick={ui.actions.closeFilterPanel}
164
+ >
165
+ <CloseIcon size={16} stroke={1.6} />
166
+ </ActionIcon>
167
+ </div>
168
+
108
169
  <Stack gap="xs">
109
170
  {columnFilters.map((filter) => {
110
171
  const column = table.getColumn(filter.id);
@@ -129,6 +190,10 @@ export function TMDataGridFilterPanel() {
129
190
  size={controlSize}
130
191
  w={160}
131
192
  allowDeselect={false}
193
+ // Both dropdowns render inside the panel: a portalled one is
194
+ // outside it in the DOM, and picking an option would read as a
195
+ // click away and close the panel under the user.
196
+ comboboxProps={{ withinPortal: false }}
132
197
  data={columnOptions}
133
198
  value={filter.id}
134
199
  onChange={(next) => next && changeFilterColumn(filter.id, next)}
@@ -139,6 +204,7 @@ export function TMDataGridFilterPanel() {
139
204
  size={controlSize}
140
205
  w={170}
141
206
  allowDeselect={false}
207
+ comboboxProps={{ withinPortal: false }}
142
208
  data={getOperatorsForType(type).map((operator) => ({
143
209
  value: operator,
144
210
  label: FILTER_OPERATOR_LABELS[operator],
@@ -168,14 +234,25 @@ export function TMDataGridFilterPanel() {
168
234
  );
169
235
  })}
170
236
 
171
- <Button
172
- variant="subtle"
173
- size="compact-sm"
174
- style={{ alignSelf: "flex-start" }}
175
- onClick={addFilter}
176
- >
177
- Add filter
178
- </Button>
237
+ <div className={classes.filterPanelFooter}>
238
+ <Button
239
+ variant="subtle"
240
+ size="compact-sm"
241
+ disabled={!canAddFilter}
242
+ onClick={addFilter}
243
+ >
244
+ Add filter
245
+ </Button>
246
+ <Button
247
+ variant="subtle"
248
+ color="gray"
249
+ size="compact-sm"
250
+ disabled={columnFilters.length === 0}
251
+ onClick={clearAllFilters}
252
+ >
253
+ Clear all
254
+ </Button>
255
+ </div>
179
256
  </Stack>
180
257
  </div>
181
258
  );
@@ -0,0 +1,35 @@
1
+ /* Wraps rather than scrolls: the pills are a summary, and a summary that goes
2
+ off-screen is not one. */
3
+ .filterPills {
4
+ display: flex;
5
+ align-items: center;
6
+ flex-wrap: wrap;
7
+ gap: var(--mantine-spacing-xs);
8
+ }
9
+
10
+ /* Mantine's dark-scheme pill is `dark-7` — the same colour as the body — so a
11
+ pill sitting on the page rather than inside a `PillsInput` disappears. Both
12
+ schemes are restated here so the two stay a pair, with a border to keep the
13
+ shape readable against any surface. Scoped through `.filterPills` for the
14
+ specificity to beat Mantine's own rule whichever stylesheet loads last. */
15
+ .filterPills .pill {
16
+ background-color: light-dark(
17
+ var(--mantine-color-gray-1),
18
+ var(--mantine-color-dark-5)
19
+ );
20
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
21
+ border: 1px solid
22
+ light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3));
23
+ }
24
+
25
+ /* The label is a button so a pill can be clicked back open, but it must read as
26
+ part of the pill — Mantine's own label styles carry the font. */
27
+ .pillLabel {
28
+ font: inherit;
29
+ color: inherit;
30
+ cursor: pointer;
31
+ }
32
+
33
+ .pillLabel:hover {
34
+ text-decoration: underline;
35
+ }
@@ -0,0 +1,135 @@
1
+ import { Button, Pill, UnstyledButton } from "@mantine/core";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ import { useSelector } from "@tanstack/react-store";
4
+ import classes from "./TMDataGridFilterPills.module.css";
5
+ import { getColumnLabel, getColumnType } from "../core/columnUtils";
6
+ import {
7
+ formatFilterLabel,
8
+ isFilterActive,
9
+ isTMDataGridFilterValue,
10
+ } from "../core/filterOperators";
11
+ import type { TMDataGridSize } from "../core/sizes";
12
+ import { openColumnFilter, type TMDataGridApi } from "../useTMDataGrid";
13
+
14
+ export type TMDataGridFilterPillsProps<TData extends RowData> = {
15
+ /** The object returned by `useTMDataGrid`. */
16
+ api: TMDataGridApi<TData>;
17
+ /** Mantine size of the pills. Defaults to `"sm"`. */
18
+ size?: TMDataGridSize;
19
+ /** "Clear all" button, shown once two filters are active. Defaults to `true`. */
20
+ showClearAll?: boolean;
21
+ /**
22
+ * Replaces what a click on a pill's label does. The default opens the grid's
23
+ * filter panel on that column.
24
+ */
25
+ onPillClick?: (columnId: string) => void;
26
+ className?: string;
27
+ };
28
+
29
+ /**
30
+ * One pill per active filter — `First name: Sofia ✕` — with the ✕ clearing that
31
+ * filter and a click on the label opening the filter panel on its column.
32
+ *
33
+ * The one grid component that takes the api as a prop instead of reading
34
+ * context, so it can live anywhere: a page header, a card title, a breadcrumb
35
+ * row. Renders nothing while no filter is active.
36
+ *
37
+ * ```tsx
38
+ * const grid = useTMDataGrid({ data, columns });
39
+ *
40
+ * <Group>
41
+ * <Title order={3}>Employees</Title>
42
+ * <TMDataGridFilterPills api={grid} />
43
+ * </Group>
44
+ * ```
45
+ *
46
+ * Half-typed filters are left out: a filter that is not narrowing the rows yet
47
+ * has nothing to report, which is the same rule the funnel indicator uses.
48
+ */
49
+ export function TMDataGridFilterPills<TData extends RowData>({
50
+ api,
51
+ size = "sm",
52
+ showClearAll = true,
53
+ onPillClick,
54
+ className,
55
+ }: TMDataGridFilterPillsProps<TData>) {
56
+ const { table } = api;
57
+ const columnFilters = useSelector(
58
+ table.store,
59
+ (state) => state.columnFilters,
60
+ );
61
+
62
+ const activeFilters = columnFilters.filter((filter) =>
63
+ isFilterActive(filter.value),
64
+ );
65
+ if (activeFilters.length === 0) return null;
66
+
67
+ return (
68
+ <div
69
+ role="group"
70
+ aria-label="Active filters"
71
+ className={[classes.filterPills, className].filter(Boolean).join(" ")}
72
+ >
73
+ <Pill.Group size={size}>
74
+ {activeFilters.map((filter) => {
75
+ const column = table.getColumn(filter.id);
76
+ const label = column ? getColumnLabel(column) : filter.id;
77
+ const value = filter.value;
78
+ // isFilterActive already vouched for the shape; this narrows it.
79
+ if (!isTMDataGridFilterValue(value)) return null;
80
+
81
+ return (
82
+ <Pill
83
+ key={filter.id}
84
+ size={size}
85
+ className={classes.pill}
86
+ withRemoveButton
87
+ onRemove={() =>
88
+ table.setColumnFilters(
89
+ columnFilters.filter((other) => other.id !== filter.id),
90
+ )
91
+ }
92
+ // Mantine hides its remove button from assistive tech and from
93
+ // the tab order, because inside a `PillsInput` Backspace is the
94
+ // way out. These pills stand on their own, so the ✕ has to be a
95
+ // real button.
96
+ removeButtonProps={{
97
+ "aria-label": `Clear ${label} filter`,
98
+ "aria-hidden": false,
99
+ tabIndex: 0,
100
+ }}
101
+ >
102
+ <UnstyledButton
103
+ className={classes.pillLabel}
104
+ onClick={() =>
105
+ onPillClick
106
+ ? onPillClick(filter.id)
107
+ : openColumnFilter(api, filter.id)
108
+ }
109
+ >
110
+ {formatFilterLabel({
111
+ label,
112
+ type: column ? getColumnType(column) : "string",
113
+ filter: value,
114
+ })}
115
+ </UnstyledButton>
116
+ </Pill>
117
+ );
118
+ })}
119
+ </Pill.Group>
120
+
121
+ {showClearAll && activeFilters.length > 1 && (
122
+ // Below two pills their own ✕ is the shorter path, so the button would
123
+ // only be a second way to do the same thing.
124
+ <Button
125
+ variant="subtle"
126
+ color="gray"
127
+ size="compact-xs"
128
+ onClick={() => table.setColumnFilters([])}
129
+ >
130
+ Clear all
131
+ </Button>
132
+ )}
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,18 @@
1
+ .footer {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: flex-end;
5
+ gap: var(--mantine-spacing-lg);
6
+ padding: 4px var(--mantine-spacing-sm);
7
+ min-height: 48px;
8
+ flex-shrink: 0;
9
+ border-top: 1px solid var(--mantine-color-default-border);
10
+ }
11
+
12
+ /* Grouping suspends paging. The pager stays put and fades rather than
13
+ disappearing, so the footer reads as switched off rather than as having
14
+ vanished — and the row it holds does not change height under the grid. Its
15
+ controls carry their own `disabled`; this is the label half. */
16
+ .footer[data-paging-suspended="true"] {
17
+ opacity: 0.55;
18
+ }