@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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 (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -0,0 +1,92 @@
1
+ # Loading and empty states
2
+
3
+ A grid can have nothing to show for four different reasons, and the body says
4
+ which one it is: still fetching, emptied by a filter, or holding no data.
5
+
6
+ ```demo
7
+ file: data/LoadingAndEmpty.tsx
8
+ hint: Switch to loaded, then search for something that cannot match, to see the other branch.
9
+ extraSources: data/employeeColumns.tsx
10
+ ```
11
+
12
+ ## Precedence
13
+
14
+ An empty body shows exactly one thing, decided in this order:
15
+
16
+ 1. **Loading** - `meta.loading` is true: a centred loader. A grid that is
17
+ fetching never reports itself as empty.
18
+ 2. **Entry rows** - an open entry row from `edit.addRow()`: the entry block
19
+ only, with no message beside the form.
20
+ 3. **`renderEmptyState`** - your node, centred where the message would be.
21
+ 4. **Filtered-empty** - a filter or search is active: a search icon and
22
+ `labels.noResults` ("No rows match your filters").
23
+ 5. **Truly-empty** - no data at all: `labels.noRows` ("No rows to show").
24
+
25
+ ## Replacing the message
26
+
27
+ `renderEmptyState` replaces states 4 and 5 with one render prop.
28
+ `hasActiveFilters` says which of the two it is replacing, so one prop can render
29
+ two different messages:
30
+
31
+ ```tsx
32
+ <TMDataGrid.Table<Employee>
33
+ renderEmptyState={({ hasActiveFilters, table }) =>
34
+ hasActiveFilters ? (
35
+ <Stack align="center" gap="xs">
36
+ <Text c="dimmed">Nothing matches your filters</Text>
37
+ <Button variant="light" onClick={() => table.resetColumnFilters()}>
38
+ Clear filters
39
+ </Button>
40
+ </Stack>
41
+ ) : (
42
+ <Stack align="center" gap="xs">
43
+ <Text c="dimmed">No employees yet</Text>
44
+ <Button onClick={openCreateModal}>Add the first one</Button>
45
+ </Stack>
46
+ )
47
+ }
48
+ />
49
+ ```
50
+
51
+ ## Loading with rows on screen
52
+
53
+ The body's loading state only appears while the grid is **empty**. A
54
+ server-driven grid refetching with rows still on screen keeps showing them
55
+ rather than blanking the body on every page change.
56
+
57
+ `TMDataGrid.LoadingIndicator` covers that case: a small spinner while
58
+ `meta.loading` is true, and nothing otherwise. Place it where you want it,
59
+ typically after `Spacer`.
60
+
61
+ ```tsx
62
+ <TMDataGrid.Toolbar>
63
+ <TMDataGrid.SummaryCount />
64
+ <TMDataGrid.Spacer />
65
+ <TMDataGrid.LoadingIndicator />
66
+ </TMDataGrid.Toolbar>
67
+ ```
68
+
69
+ ## Counting what is there
70
+
71
+ `TMDataGrid.SummaryCount` shows visible rows out of the total. The total is
72
+ `meta.totalRowCount` when you provide it, and the pre-filtered row count
73
+ otherwise. A [server-side](/docs/server-side) grid must provide it, because the
74
+ client only holds one page.
75
+
76
+ ```tsx
77
+ <TMDataGrid.SummaryCount>
78
+ {visible} of {total} employees
79
+ </TMDataGrid.SummaryCount>
80
+ ```
81
+
82
+ ## Reference
83
+
84
+ | Name | Kind | Type | Default | What it does |
85
+ | --- | --- | --- | --- | --- |
86
+ | `meta.loading` | Option | `boolean` | `false` | A fetch is in flight. Takes precedence over every empty message. |
87
+ | `meta.noResultsLabel` | Option | `string` | `labels.noResults` | The filtered-empty message, without a render prop. |
88
+ | `meta.totalRowCount` | Option | `number` | Pre-filtered count | The total `SummaryCount` reports. |
89
+ | `renderEmptyState` | Table prop | `({ hasActiveFilters, table }) => ReactNode` | – | Replaces both built-in empty messages. |
90
+ | `TMDataGrid.LoadingIndicator` | Component | – | – | Spinner while `meta.loading`, for when the body has rows. |
91
+ | `TMDataGrid.SummaryCount` | Component | `children` replaces the text | – | Visible rows out of total. |
92
+ | `labels.noRows` · `labels.noResults` | Labels | `string` | English defaults | The two built-in messages. See [Localization](/docs/localization). |
@@ -0,0 +1,79 @@
1
+ # Localization
2
+
3
+ Every string the grid renders - menu items, panels, tooltips, the pager and
4
+ every `aria-label` - comes from one labels object. English by default.
5
+
6
+ ```tsx
7
+ const grid = useTMDataGrid({
8
+ data,
9
+ columns,
10
+ labels: { noResults: "Inga rader matchar dina filter" },
11
+ });
12
+ ```
13
+
14
+ `labels` takes **any subset** and merges it over the defaults.
15
+
16
+ ```demo
17
+ file: customization/Localization.tsx
18
+ extraSources: data/employeeColumns.tsx
19
+ ```
20
+
21
+ ## A complete translation
22
+
23
+ A full Swedish dictionary ships as `TMDATAGRID_LABELS_SV`:
24
+
25
+ ```tsx
26
+ import { TMDATAGRID_LABELS_SV } from "@jielga/tmdatagrid";
27
+
28
+ const grid = useTMDataGrid({ data, columns, labels: TMDATAGRID_LABELS_SV });
29
+ ```
30
+
31
+ `TMDATAGRID_LABELS_EN` is the English base, and `TMDataGridLabels` is the full
32
+ dictionary type, so a missing key in a new translation is a compile error.
33
+
34
+ ## Labels that carry a value
35
+
36
+ These labels are functions, so each language can place the value where its
37
+ grammar requires:
38
+
39
+ ```tsx
40
+ labels: {
41
+ groupBy: (column) => `Gruppera på ${column}`,
42
+ pageRange: ({ from, to, total }) => `${from}–${to} av ${total}`,
43
+ }
44
+ ```
45
+
46
+ ## Keep the object stable
47
+
48
+ Define it at module scope, or memoize it. The grid re-renders when the labels
49
+ object changes identity.
50
+
51
+ ```tsx
52
+ const labels = { noResults: "Inga träffar" } satisfies TMDataGridLabelsOverride;
53
+ ```
54
+
55
+ ## Reading them yourself
56
+
57
+ The resolved dictionary comes back from the hook as `grid.labels`, and from
58
+ context as `useTMDataGridContext().labels`, so a
59
+ [toolbar component of your own](/docs/toolbar) uses the same strings as the
60
+ built-in parts, in whatever language is configured.
61
+
62
+ `mergeLabels(base, override)` is the merge itself, exported for composing
63
+ dictionaries before passing one in.
64
+
65
+ `meta.noResultsLabel` remains as a per-instance override of
66
+ `labels.noResults`.
67
+
68
+ ## Reference
69
+
70
+ | Name | Kind | Type | Default | What it does |
71
+ | --- | --- | --- | --- | --- |
72
+ | `labels` | Option | `TMDataGridLabelsOverride` | English | Any subset, merged over the defaults. Keep it stable. |
73
+ | `grid.labels` | Hook return | `TMDataGridLabels` | – | The resolved dictionary. |
74
+ | `TMDATAGRID_LABELS_EN` | Export | `TMDataGridLabels` | – | The English base. |
75
+ | `TMDATAGRID_LABELS_SV` | Export | `TMDataGridLabels` | – | A complete Swedish dictionary. |
76
+ | `TMDataGridLabels` | Export | type | – | The full dictionary. What a new translation must cover. |
77
+ | `TMDataGridLabelsOverride` | Export | type | – | A partial dictionary. |
78
+ | `mergeLabels` | Export | `(base, override) => TMDataGridLabels` | – | The merge, for composing dictionaries. |
79
+ | `meta.noResultsLabel` | Option | `string` | `labels.noResults` | Per-instance override of the filtered-empty message. |
package/docs/menu.md ADDED
@@ -0,0 +1,143 @@
1
+ # Grid menu
2
+
3
+ `TMDataGrid.Menu` is the burger button at the end of the toolbar and the Mantine `Menu` it opens.
4
+ Its children are the dropdown: Mantine `Menu.Item`s of your own, and the built-in items under `TMDataGrid.Menu.*`.
5
+
6
+ ```tsx
7
+ import { Menu } from "@mantine/core";
8
+
9
+ <TMDataGrid.Toolbar>
10
+ <TMDataGrid.SummaryCount />
11
+ <TMDataGrid.Spacer />
12
+ <TMDataGrid.FilterButton />
13
+ <TMDataGrid.Menu>
14
+ <TMDataGrid.Menu.Export />
15
+ <Menu.Item onClick={saveView}>Save view</Menu.Item>
16
+ <Menu.Divider />
17
+ <Menu.Label>Columns</Menu.Label>
18
+ <TMDataGrid.Menu.Columns />
19
+ </TMDataGrid.Menu>
20
+ </TMDataGrid.Toolbar>
21
+ ```
22
+
23
+ ```demo
24
+ file: customization/GridMenu.tsx
25
+ hint: The burger holds the app's own items above the column chooser.
26
+ ```
27
+
28
+ A custom item reads the grid from context, the same way a [toolbar button](/docs/toolbar#buttons-of-your-own) does.
29
+ `TMDataGrid.Menu.Export` and `TMDataGrid.Menu.ExportSelected` are the built-in export items; their props and formats are on [Export](/docs/export).
30
+ Mantine's `Menu.Divider`, `Menu.Label` and `Menu.Sub` work as they do in any Mantine menu; the grid wraps nothing of Mantine's.
31
+
32
+ `TMDataGrid.Menu` always renders: it cannot see whether its children render anything.
33
+ A menu holding only `TMDataGrid.Menu.Columns` on a grid with `enableHiding: false` opens empty, so hide it with the same check the built-in buttons use:
34
+
35
+ ```tsx
36
+ const { table, features } = useTMDataGridContext();
37
+ const { canHideAny } = getGridCapabilities(table, features);
38
+
39
+ {canHideAny && (
40
+ <TMDataGrid.Menu>
41
+ <TMDataGrid.Menu.Columns />
42
+ </TMDataGrid.Menu>
43
+ )}
44
+ ```
45
+
46
+ ## The column chooser as menu items
47
+
48
+ `TMDataGrid.Menu.Columns` is the whole chooser: one checkbox item per column that can be hidden, a search box once there are six of them, **Show/Hide All** and **Reset layout**.
49
+ It renders nothing when no column can be hidden.
50
+ The pieces it is made of are exported for menus that want only some of them.
51
+
52
+ | Component | Renders |
53
+ | --- | --- |
54
+ | `TMDataGrid.Menu.Columns` | `Menu.Search`, the toggles, a divider, show/hide all and reset layout. By default the search box shows from six columns; `searchable` shows it always and `searchable={false}` never. |
55
+ | `TMDataGrid.Menu.ColumnToggles` | One item per hideable column, with a checkbox that shows both states. `search` narrows the list to the labels containing it. |
56
+ | `TMDataGrid.Menu.ShowHideAll` | One checkbox item over the same list. An indeterminate box marks a partial state, and a click then shows all. |
57
+ | `TMDataGrid.Menu.ResetLayout` | One item calling `resetSettings()`: visibility, order, pinning and widths. |
58
+
59
+ Each of them needs a Mantine `Menu` around it and reads the grid from context, so they work in any Mantine menu rendered inside `TMDataGrid`, not only in the burger:
60
+
61
+ ```tsx
62
+ <Menu width={260} withinPortal>
63
+ <Menu.Target>
64
+ <Button size="compact-xs" variant="subtle">
65
+ View
66
+ </Button>
67
+ </Menu.Target>
68
+ <Menu.Dropdown>
69
+ <Menu.Item onClick={() => setDensity("compact")}>Compact rows</Menu.Item>
70
+ <Menu.Divider />
71
+ <TMDataGrid.Menu.ColumnToggles />
72
+ <Menu.Divider />
73
+ <TMDataGrid.Menu.ShowHideAll />
74
+ <TMDataGrid.Menu.ResetLayout />
75
+ </Menu.Dropdown>
76
+ </Menu>
77
+ ```
78
+
79
+ ### As a submenu
80
+
81
+ Every column header's menu has **Manage columns**, a `Menu.Sub` holding `TMDataGrid.Menu.Columns`.
82
+ The same composition works in a menu of your own:
83
+
84
+ ```tsx
85
+ <Menu.Sub>
86
+ <Menu.Sub.Target>
87
+ <Menu.Sub.Item>Manage columns</Menu.Sub.Item>
88
+ </Menu.Sub.Target>
89
+ <Menu.Sub.Dropdown>
90
+ <TMDataGrid.Menu.Columns searchable={false} />
91
+ </Menu.Sub.Dropdown>
92
+ </Menu.Sub>
93
+ ```
94
+
95
+ `searchable` is for a block at the top level of a dropdown.
96
+ `Menu.Search` registers on the root menu, which switches off type-ahead and the arrow keys of every dropdown of that menu, so a search box inside a submenu breaks the keyboard behaviour of the menu around it.
97
+ ArrowDown from the search box moves to the first listed column, whatever sits above the block.
98
+
99
+ ### The panel instead
100
+
101
+ `TMDataGrid.ColumnsPanel` is the same chooser as plain controls, for a host that is not a menu: a Popover, a Drawer, or a settings page.
102
+
103
+ ```tsx
104
+ const [opened, setOpened] = useState(false);
105
+
106
+ <Popover opened={opened} onChange={setOpened} withinPortal trapFocus>
107
+ <Popover.Target>
108
+ <ActionIcon aria-label="Columns" onClick={() => setOpened((open) => !open)}>
109
+ <IconColumns3 size={18} />
110
+ </ActionIcon>
111
+ </Popover.Target>
112
+ <Popover.Dropdown p={0}>
113
+ <TMDataGrid.ColumnsPanel />
114
+ </Popover.Dropdown>
115
+ </Popover>
116
+ ```
117
+
118
+ ## Labels
119
+
120
+ `labels.menuButton` is the burger's tooltip and `aria-label`.
121
+ The chooser uses the panel's strings: `columnsSearchPlaceholder`, `columnsNoMatch`, `columnsShowHideAll` and `columnsReset`.
122
+ See [Localization](/docs/localization).
123
+
124
+ ## Testing
125
+
126
+ The burger is `data-dg-part="menu-button"`.
127
+ The items publish the same parts as the panel: `columns-search`, `columns-toggle` with `data-column-id`, `columns-toggle-all` and `columns-reset`, so a test written against the panel reads the same on the menu.
128
+ The export items are `menu-export` and `menu-export-selected`.
129
+ See [Testing](/docs/testing).
130
+
131
+ ## Reference
132
+
133
+ | Name | Kind | Type | Default | What it does |
134
+ | --- | --- | --- | --- | --- |
135
+ | `TMDataGrid.Menu` | Component | Mantine `MenuProps` without `children`, plus `children`, `icon`, `label` | `position="bottom-end"`, `shadow="md"`, `width={260}`, `withinPortal` | The burger and its dropdown. `icon` replaces the burger; `label` is the tooltip and `aria-label`, default `labels.menuButton`. |
136
+ | `TMDataGrid.Menu.Columns` | Component | `searchable?: boolean \| "auto"` | `"auto"` | The whole column chooser as menu items. `"auto"` shows the search box from six columns. Renders nothing when no column can be hidden. |
137
+ | `TMDataGrid.Menu.ColumnToggles` | Component | `search?: string` | – | One checkbox item per hideable column. |
138
+ | `TMDataGrid.Menu.ShowHideAll` | Component | – | – | One checkbox item over every hideable column. |
139
+ | `TMDataGrid.Menu.ResetLayout` | Component | – | – | Calls `resetSettings()`. |
140
+ | `TMDataGrid.Menu.Export` · `.ExportSelected` | Components | `TMDataGridExportOptions` | – | The export items. Props on [Export](/docs/export). |
141
+ | `TMDataGrid.ColumnsPanel` | Component | – | – | The chooser as plain controls, for a host that is not a menu. |
142
+ | `labels.menuButton` | Option | `string` | `"Menu"` | The burger's tooltip and `aria-label`. |
143
+ | `TMDataGridMenuProps` · `TMDataGridMenuColumnsProps` · `TMDataGridMenuExportProps` | Export | types | – | The prop types. |
@@ -0,0 +1,144 @@
1
+ # Pagination
2
+
3
+ **Off by default.** The grid renders every filtered and sorted row and relies
4
+ on [virtualization](/docs/scrolling), which handles any row count. Turn paging
5
+ on when users should move through the data a page at a time, not to keep a
6
+ large grid responsive.
7
+
8
+ There are three modes.
9
+
10
+ **No pagination** - the default. `TMDataGrid.Footer` renders nothing.
11
+
12
+ ```tsx
13
+ const grid = useTMDataGrid({ data, columns });
14
+ ```
15
+
16
+ **Client pagination** - the table pages the data itself, and the Footer renders
17
+ its pager. Initial page size is 25, configurable through
18
+ `initialState.pagination`.
19
+
20
+ ```tsx
21
+ const grid = useTMDataGrid({ data, columns, enablePagination: true });
22
+ ```
23
+
24
+ **Manual pagination** - the server pages, and the grid stops.
25
+ `manualPagination: true` implies `enablePagination`, so no extra flag is
26
+ needed. See [Server-side data](/docs/server-side).
27
+
28
+ ```tsx
29
+ const grid = useTMDataGrid({
30
+ data: page.rows,
31
+ columns,
32
+ manualPagination: true,
33
+ rowCount: page.total,
34
+ state: { pagination },
35
+ onPaginationChange: setPagination,
36
+ });
37
+ ```
38
+
39
+ ```demo
40
+ file: data/Pagination.tsx
41
+ extraSources: data/employeeColumns.tsx
42
+ ```
43
+
44
+ `enablePagination` is defined by the grid rather than by TanStack, which ships
45
+ the pagination state and APIs but no `enable` option.
46
+
47
+ ## Replacing the pager
48
+
49
+ `TMDataGrid.Footer` takes a `renderPagination` slot, handed three things: the
50
+ `state` the pager is showing, the `actions` it can take, and `Controls` - the
51
+ built-in pieces, already wired.
52
+
53
+ ```tsx
54
+ <TMDataGrid.Footer
55
+ renderPagination={({ state, actions, Controls }) => (
56
+ <Group>
57
+ <Controls.PageSize />
58
+ <Button onClick={actions.previousPage} disabled={!state.canPreviousPage}>
59
+ Back
60
+ </Button>
61
+ <Text>
62
+ {state.pageIndex + 1} / {state.pageCount}
63
+ </Text>
64
+ <Button onClick={actions.nextPage} disabled={!state.canNextPage}>
65
+ Next
66
+ </Button>
67
+ </Group>
68
+ )}
69
+ />
70
+ ```
71
+
72
+ The default footer renders `PageSize`, `Range` and `Pager`, in that order. A
73
+ control kept from `Controls` behaves as it does in the default footer, including
74
+ greying out while a grouping suspends paging.
75
+
76
+ | Member | Renders |
77
+ | --- | --- |
78
+ | `Controls.PageSize` | "Rows per page" and its select |
79
+ | `Controls.Range` | The "1–25 of 300" label |
80
+ | `Controls.PageNumber` | The "Page 3 of 200" label |
81
+ | `Controls.Pager` | The previous and next buttons |
82
+
83
+ `PageNumber` is not in the default footer.
84
+ It is the label a server-paged grid usually shows in place of a row range, so it is put in through the slot:
85
+
86
+ ```tsx
87
+ <TMDataGrid.Footer
88
+ renderPagination={({ Controls }) => (
89
+ <>
90
+ <Controls.PageSize />
91
+ <Controls.PageNumber />
92
+ <Controls.Pager />
93
+ </>
94
+ )}
95
+ />
96
+ ```
97
+
98
+ It reads `pageCount`, so a grid declaring `pageCount: -1` shows "Page 3" alone.
99
+
100
+ `state` carries `pageIndex`, `pageSize`, `pageCount`, `rowCount`,
101
+ `canPreviousPage`, `canNextPage`, the `from` / `to` bounds of the current page,
102
+ and `isPagingActive`. `actions` carries `setPageIndex`, `setPageSize`,
103
+ `previousPage`, `nextPage`, `firstPage` and `lastPage`.
104
+
105
+ `getTMDataGridPaginationApi(table)` returns the same `{ state, actions }`
106
+ outside the Footer, for a pager that lives elsewhere on the page. `Controls` is
107
+ not included: those components are bound to the grid's context and work only
108
+ inside the slot.
109
+
110
+ ## Grouping
111
+
112
+ While a column is grouped the pager greys itself out and the range is replaced
113
+ with `Grouped · all N rows`. See
114
+ [Grouping](/docs/grouping#grouping-and-pagination).
115
+
116
+ A custom pager can grey itself out the same way:
117
+
118
+ ```tsx
119
+ <TMDataGrid.Footer
120
+ renderPagination={({ state, actions }) => (
121
+ <MyPager {...state} {...actions} disabled={!state.isPagingActive} />
122
+ )}
123
+ />
124
+ ```
125
+
126
+ `isPagingActive` is live state: whether the pager is currently slicing
127
+ anything. `getGridCapabilities(...).canPaginate` is configuration: whether
128
+ paging is switched on at all. The two differ while a grouping is active.
129
+
130
+ ## Reference
131
+
132
+ | Name | Kind | Type | Default | What it does |
133
+ | --- | --- | --- | --- | --- |
134
+ | `enablePagination` | Option | `boolean` | `false` | Client-side paging and the Footer's pager. Grid-defined. |
135
+ | `manualPagination` | Table option | `boolean` | `false` | The server pages. Implies `enablePagination`. |
136
+ | `rowCount` | Table option | `number` | – | The true total, required under `manualPagination`. |
137
+ | `initialState.pagination` | Table option | `{ pageIndex, pageSize }` | `{ 0, 25 }` | Where paging starts. A data slice, so it persists. |
138
+ | `onPaginationChange` | Table option | `OnChangeFn` | – | Controls the pagination state. |
139
+ | `resetPageOnQueryChange` | Option | `boolean` | `true` | Back to page 1 when a filter, the quick search, the sort or the grouping changes. `false` keeps the page. See [Server-side data](/docs/server-side#the-page-index). |
140
+ | `TMDataGrid.Footer` | Component | – | – | The footer bar. Renders nothing when paging is off. |
141
+ | `Footer` `renderPagination` | Slot | `({ state, actions, Controls }) => ReactNode` | Built-in pager | Replaces the pager, and hands over its pieces. |
142
+ | `getTMDataGridPaginationApi` | Export | `(table) => { state, actions }` | – | The pager API, outside the Footer. |
143
+ | `TMDataGridPaginationState` · `TMDataGridPaginationActions` · `TMDataGridPaginationControls` | Exports | types | – | The three parts of the slot's argument. |
144
+ | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything right now. |
@@ -0,0 +1,111 @@
1
+ # Persistence
2
+
3
+ Restores table state on mount and writes it back on every change, so users
4
+ return to the grid as they left it.
5
+
6
+ State is split across **two keys**: `settingsKey` for the column layout, which
7
+ stays valid indefinitely, and `dataKey` for filters, sorting and pagination,
8
+ which go stale as the data underneath them changes. Either can be cleared
9
+ without touching the other.
10
+
11
+ ```tsx
12
+ // Module scope: the object is a dependency of the write subscription.
13
+ const persist = {
14
+ dataKey: "employees.data",
15
+ settingsKey: "employees.settings",
16
+ } satisfies TMDataGridPersistence;
17
+
18
+ const grid = useTMDataGrid({ data, columns, persist });
19
+ ```
20
+
21
+ Both keys are optional - pass only `settingsKey` to remember the layout but
22
+ never the filters.
23
+
24
+ ```demo
25
+ file: data/Persistence.tsx
26
+ hint: Sort, filter and hide a column, then reload the page. It all comes back; the page index does not.
27
+ extraSources: data/employeeColumns.tsx
28
+ ```
29
+
30
+ ## The two groups
31
+
32
+ | Group | Slices | Lifetime |
33
+ | --- | --- | --- |
34
+ | `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination`, `expanded` | As long as the data means the same thing |
35
+ | `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning`, `grouping` | Indefinite. This is the user's layout |
36
+
37
+ `DATA_STATE_SLICES` and `SETTINGS_STATE_SLICES` are exported with the same
38
+ values. Slice names are typed per group, so only valid names compile.
39
+
40
+ Settings saved by 1.x with the old `columnPinning` keys `left` and `right` are
41
+ read and migrated to `start` and `end`.
42
+
43
+ ## Persisting only some of it
44
+
45
+ A key on its own persists every slice in its group. Pass a tuple to narrow it:
46
+
47
+ ```tsx
48
+ const persist = {
49
+ // Restore filters and sorting, but always start on the first page.
50
+ dataKey: ["employees.data", ["columnFilters", "sorting"]],
51
+ // Restore column layout but not widths.
52
+ settingsKey: ["employees.settings", ["columnVisibility", "columnOrder"]],
53
+ storageMode: "sessionStorage",
54
+ } satisfies TMDataGridPersistence;
55
+ ```
56
+
57
+ Only the selected slices are read back, so a payload written before you narrowed
58
+ the selection cannot reintroduce slices you have since opted out of.
59
+
60
+ ## Behaviour
61
+
62
+ **Restoring happens once**, on mount, through `initialState`. Writing is a
63
+ subscription to the table store, so state changed directly through the table
64
+ API is persisted too.
65
+
66
+ **A payload from another version is dropped whole**, not migrated. Payloads
67
+ carry the exported `PERSIST_PAYLOAD_VERSION`, and anything else, including
68
+ everything written by a 0.x build, which had no stamp, is discarded.
69
+
70
+ **Restored state is realigned against the columns that exist.** Entries naming a
71
+ column removed between deploys are dropped: a stale id in the order, a width for
72
+ a column that no longer exists, or a sort or filter that would be active with no
73
+ column to show it. New columns need no handling, since TanStack appends columns
74
+ missing from `columnOrder` in definition order.
75
+
76
+ **Storage access is guarded.** If storage is unavailable, disabled or full,
77
+ persistence is skipped rather than throwing.
78
+
79
+ **Keys are not namespaced.** Include a tenant or user identifier if several
80
+ people can share a browser profile.
81
+
82
+ ## Resetting
83
+
84
+ `resetSettings()` puts the settings state back to what a first visit with clean
85
+ storage would have shown (your `initialState` plus the structural lanes), and,
86
+ with persistence configured, writes through to storage like any other change.
87
+ The columns panel's **Reset layout** button calls it.
88
+
89
+ TanStack's own `resetColumnX()` family cannot do it on a persisted grid: those
90
+ reset to `initialState`, which the mount built *from* the restored payload.
91
+
92
+ ## Relation to Mantine
93
+
94
+ Persistence does not use Mantine's `useLocalStorage`; the table owns the state
95
+ and storage only mirrors it. The option names follow Mantine's
96
+ `UseStorageOptions` where they apply, and `storageMode` takes the same values as
97
+ its `StorageType`.
98
+
99
+ ## Reference
100
+
101
+ | Name | Kind | Type | Default | What it does |
102
+ | --- | --- | --- | --- | --- |
103
+ | `persist` | Option | `TMDataGridPersistence` | – | The whole configuration. Keep it referentially stable. |
104
+ | `dataKey` | persist field | `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
105
+ | `settingsKey` | persist field | `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
106
+ | `storageMode` | persist field | `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. `"sessionStorage"` is per tab. |
107
+ | `serialize` | persist field | `(value) => string` | `JSON.stringify` | Serializes a payload before storing. |
108
+ | `deserialize` | persist field | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
109
+ | `resetSettings` | Hook return | `() => void` | – | Back to a clean first visit, written through to storage. |
110
+ | `DATA_STATE_SLICES` · `SETTINGS_STATE_SLICES` | Exports | `string[]` | – | The slice names of each group. |
111
+ | `PERSIST_PAYLOAD_VERSION` | Export | `number` | – | The stamp. A payload from another version is dropped. |
@@ -0,0 +1,94 @@
1
+ # A portfolio rebalancer
2
+
3
+ A holdings book where one column is a decision and the rest are consequences.
4
+ The user edits **Target**; the grid recomputes drift and the trade to place, totals each sector, and refuses an edit that would allocate more than the whole book.
5
+
6
+ ```demo
7
+ file: recipes/PortfolioRebalancer.tsx
8
+ hint: Type 40 into a Target cell - the commit is refused with the total it would have produced.
9
+ height: 620
10
+ ```
11
+
12
+ ## Deriving the rows
13
+
14
+ `accessorFn` is handed one row, so a column whose value depends on the other rows - a weight as a share of the portfolio - cannot be written as one.
15
+ Derive the whole collection once and hand the grid the finished shape:
16
+
17
+ ```tsx
18
+ const positions = useMemo(() => {
19
+ const valued = holdings.map((h) => ({ ...h, marketValue: h.price * h.shares }));
20
+ const total = valued.reduce((sum, h) => sum + h.marketValue, 0);
21
+
22
+ return valued.map((h) => ({
23
+ ...h,
24
+ currentPct: (h.marketValue / total) * 100,
25
+ drift: h.targetPct - (h.marketValue / total) * 100,
26
+ }));
27
+ }, [holdings]);
28
+ ```
29
+
30
+ `editing.onCommit` writes back to `holdings`, the source array, and the derived rows arrive through `data` on the next render.
31
+ Under `editing.mode: "cell"` with no draft store, every dependent column follows the keystroke that committed.
32
+
33
+ ## Gating the one editable column
34
+
35
+ `editing.columns` lists what takes edits. Everything else is market data and stays read-only whatever its own meta says.
36
+
37
+ ```tsx
38
+ editing: {
39
+ mode: "cell",
40
+ columns: ["targetPct"],
41
+ onCommit: ({ rowId, value }) => save(rowId, value.targetPct),
42
+ }
43
+ ```
44
+
45
+ ## The rule that needs the other rows
46
+
47
+ A target weight is only valid against the rest of the book, so the rule is `editing.tableValidators`, not `meta.edit.validate`.
48
+ `rows` is the collection as it would stand if the commit landed, so the committing row's drafted value is already in it.
49
+
50
+ ```tsx
51
+ tableValidators: {
52
+ onSubmit: ({ rows }) => {
53
+ const total = rows.reduce((sum, r) => sum + Number(r.value.targetPct ?? 0), 0);
54
+
55
+ return total > 100.005
56
+ ? { fields: { targetPct: `Targets would total ${pct(total)}` } }
57
+ : undefined;
58
+ },
59
+ }
60
+ ```
61
+
62
+ The bound on a single cell - between 0 and 100 - stays on the column as [`meta.edit.validate`](/docs/editors), because it needs nothing but the value.
63
+
64
+ ## Totals in two places
65
+
66
+ Sector totals come from `aggregationFn` on each column; the portfolio total comes from a `footer` and [`aggregateColumn`](/docs/summary-row), which follows the filters.
67
+
68
+ ```tsx
69
+ columnHelper.accessor("marketValue", {
70
+ aggregationFn: "sum",
71
+ footer: ({ table }) =>
72
+ money.format(Number(aggregateColumn({ table, columnId: "marketValue" }))),
73
+ });
74
+ ```
75
+
76
+ Summing `targetPct` is meaningful only because the values are shares of one whole: a footer reading `100.0%` says the book is fully allocated.
77
+
78
+ ## Reading drift
79
+
80
+ Drift is signed, so the cell renders a tint whose strength follows the size of the miss and whose colour follows its direction.
81
+ Rows more than three points out take `--row-bg` as well, which composes under hover and selection instead of replacing them.
82
+
83
+ ```tsx
84
+ <TMDataGrid.Table<Position>
85
+ rowStyle={(row) =>
86
+ !row.getIsGrouped() && Math.abs(row.original.drift) >= 3
87
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-yellow-6) 10%, transparent)" }
88
+ : undefined
89
+ }
90
+ />
91
+ ```
92
+
93
+ A group row is handed to `rowStyle` too, and its `original` is an arbitrary child's record, so the callback guards with `row.getIsGrouped()`.
94
+ [Row styling](/docs/row-styling) covers the rest of the vocabulary.