@opencorestack/opengridx 2.0.4 → 3.0.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 (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 2.0.4
3
+ # Version: 3.0.0
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -11,10 +11,10 @@
11
11
  OpenGridX is a zero-dependency, high-performance React DataGrid library.
12
12
  It matches and extends the feature set of MUI X DataGrid Pro — offered completely free.
13
13
 
14
- - **React**: 18+ required
14
+ - **React**: 18+ required (react and react-dom are peer dependencies)
15
15
  - **TypeScript**: Full typings shipped in `dist/index.d.ts`
16
- - **Bundle**: 52 KB gzip (ES module). ExcelJS is optional peer dep (lazy-loaded).
17
- - **CSS**: Auto-imported via barrel. If unstyled, add: `import '@opencorestack/opengridx/styles'`
16
+ - **Optional peers**: `exceljs` (exportToExcelAdvanced), `jspdf` + `jspdf-autotable` (exportToPdf). Loaded lazily, never bundled.
17
+ - **CSS**: NOT loaded by the JS bundle. Always add `import '@opencorestack/opengridx/styles'` once in the app root.
18
18
 
19
19
  ---
20
20
 
@@ -22,22 +22,26 @@ It matches and extends the feature set of MUI X DataGrid Pro — offered complet
22
22
 
23
23
  ```bash
24
24
  npm install @opencorestack/opengridx
25
- # Optional — for advanced Excel export only:
25
+ # Optional — only for exportToExcelAdvanced:
26
26
  npm install exceljs
27
+ # Optional — only for exportToPdf:
28
+ npm install jspdf jspdf-autotable
27
29
  ```
28
30
 
29
31
  ```tsx
30
32
  import { DataGrid } from '@opencorestack/opengridx';
31
- // If grid appears unstyled in your bundler:
32
- import '@opencorestack/opengridx/styles';
33
+ import '@opencorestack/opengridx/styles'; // required: the stylesheet is a separate file
33
34
  ```
34
35
 
36
+ Without the stylesheet the grid is unstyled AND the viewport does not scroll, so every row is rendered (virtualization is lost).
37
+
35
38
  ---
36
39
 
37
40
  ## MINIMAL EXAMPLE
38
41
 
39
42
  ```tsx
40
- import { DataGrid, GridColDef } from '@opencorestack/opengridx';
43
+ import { DataGrid, type GridColDef } from '@opencorestack/opengridx';
44
+ import '@opencorestack/opengridx/styles';
41
45
 
42
46
  const columns: GridColDef[] = [
43
47
  { field: 'id', headerName: 'ID', width: 70 },
@@ -56,152 +60,164 @@ export default function App() {
56
60
 
57
61
  ---
58
62
 
59
- ## DataGrid PROPS (DataGridProps<R extends GridRowModel>)
63
+ ## DataGrid PROPS (DataGridProps<R extends GridValidRowModel = GridRowModel>)
60
64
 
61
65
  ### Required
62
66
  | Prop | Type | Description |
63
67
  |-----------|------------------|----------------------------------|
64
- | rows | GridRowModel[] | Data array. Each row needs an id |
68
+ | rows | R[] | Data array. Each row needs an id (or pass getRowId). Rows are never copied or mutated |
65
69
  | columns | GridColDef<R>[] | Column definitions |
66
70
 
67
71
  ### Display
68
72
  | Prop | Type | Default | Description |
69
73
  |-------------------|-----------|---------|----------------------------------------|
70
- | height | number | 500 | Grid height in pixels |
71
- | autoHeight | boolean | false | Fit height to rows |
74
+ | height | number/string | — | Grid height. No default. The grid must have a BOUNDED height or it renders every row (see GOTCHAS) |
75
+ | autoHeight | boolean | false | Fit height to rows (renders every row) |
76
+ | overscanRowCount | number | 3 | Minimum rows rendered outside the viewport (raised automatically while scrolling fast) |
77
+ | ariaLabel | string | — | Accessible name for the grid |
78
+ | localeText | GridLocaleText | — | Override pager / no-rows strings: paginationRowsPerPage, paginationOf(from,to,count), paginationPage(page,count), noRowsLabel |
72
79
  | rowHeight | number | 52 | Row height in pixels |
73
80
  | headerHeight | number | 56 | Header height in pixels |
74
- | loading | boolean | false | Show skeleton loading overlay |
75
- | noRowsLabel | string | — | Empty state message |
76
- | density | 'compact'/'standard'/'comfortable' | 'standard' | Row density |
81
+ | loading | boolean | false | No rows: skeleton rows (or slots.loadingOverlay). Rows shown: they stay, a progress bar (.ogx__loading-bar) runs along the top, or slots.loadingOverlay is shown over them |
82
+ | noRowsLabel | string | 'No Data' | Empty state message |
83
+ | density | 'compact'/'standard'/'comfortable' | 'standard' | 32px / rowHeight / 72px rows |
77
84
 
78
85
  ### Selection
79
86
  | Prop | Type | Description |
80
87
  |----------------------------|-------------------------------|-------------------------------------|
81
- | checkboxSelection | boolean | Enable checkbox column |
88
+ | checkboxSelection | boolean | Enable checkbox column (header checkbox = select all filtered rows) |
89
+ | disableMultipleRowSelection | boolean | Cap selection to one row (checkboxes, Space, apiRef too); removes the select-all checkbox |
90
+ | pinCheckboxColumn | boolean (default true) | Pin the checkbox column left |
82
91
  | rowSelectionModel | GridRowId[] | Controlled selection |
83
- | onRowSelectionModelChange | (model: GridRowId[]) => void | Selection change callback |
92
+ | onRowSelectionModelChange | (model: GridRowId[]) => void | Selection change callback. Also fires once with ids of removed rows pruned (client-owned rows) |
84
93
  | disableRowSelectionOnClick | boolean | Prevent click toggling selection |
94
+ | disableClipboardCopy | boolean | Turn off the grid's Ctrl/Cmd+C copy (copySelectedRows still works) |
85
95
 
86
96
  ### Sorting
87
97
  | Prop | Type | Description |
88
98
  |-------------------|-----------------------------|----------------------------------|
89
99
  | sortModel | GridSortItem[] | Controlled sort state |
100
+ | multiSort | boolean | Plain click appends to sortModel (Shift+click always does) |
90
101
  | onSortModelChange | (model: GridSortItem[]) => void | Sort change callback |
91
- | sortingMode | 'client' / 'server' | Where sorting is applied |
92
- | disableColumnSorting | boolean | Disable sorting for all columns |
102
+ | sortingMode | 'client' / 'server' | 'server': rows are shown in the order given |
93
103
 
94
104
  ### Filtering
95
105
  | Prop | Type | Description |
96
106
  |---------------------|--------------------------------|-----------------------------------|
97
- | filterModel | GridFilterModel | Controlled filter state |
107
+ | filterModel | GridFilterModel | Controlled filter state (optional: the grid keeps its own filter model otherwise) |
98
108
  | onFilterModelChange | (model: GridFilterModel) => void | Filter change callback |
99
- | filterMode | 'client' / 'server' | Where filtering is applied |
100
- | disableColumnFilter | boolean | Disable filter for all columns |
109
+ | filterMode | 'client' / 'server' | 'server': rows are shown unfiltered as given |
101
110
 
102
111
  ### Pagination
103
112
  | Prop | Type | Description |
104
113
  |-------------------------|-----------------------------------------|----------------------------------|
105
- | pagination | boolean | Enable client-side pagination |
106
- | paginationModel | GridPaginationModel | {page, pageSize} controlled |
107
- | onPaginationModelChange | (model: GridPaginationModel) => void | Pagination change callback |
108
- | pageSizeOptions | number[] | Available page sizes |
109
- | paginationMode | 'client' / 'server' / 'infinite' | Pagination mode |
110
- | rowCount | number | Total rows for server pagination |
114
+ | pagination | boolean | Show the pager. Ignored under rowGroupingModel and with paginationMode='infinite' |
115
+ | paginationModel | GridPaginationModel | {page, pageSize} controlled (pass onPaginationModelChange too) |
116
+ | onPaginationModelChange | (model: GridPaginationModel) => void | Pagination change callback (also fires when the grid clamps a page past the end) |
117
+ | pageSizeOptions | number[] (default [10, 25, 50, 100]) | Available page sizes |
118
+ | paginationMode | 'client' / 'server' / 'infinite' | 'server': rows is the current page. 'infinite': needs dataSource |
119
+ | rowCount | number | Server total for paginationMode='server' without dataSource (dataSource response rowCount wins) |
111
120
 
112
121
  ### Column Features
113
122
  | Prop | Type | Description |
114
123
  |---------------------------|---------------------------|-------------------------------------------|
115
- | pinnedColumns | GridColumnPinning | {left: string[], right: string[]} |
124
+ | pinnedColumns | GridColumnPinning | {left: string[], right: string[]}; rendered in that order |
116
125
  | onPinnedColumnsChange | (model) => void | Pin change callback |
117
126
  | columnVisibilityModel | Record<string, boolean> | {field: false} to hide |
118
127
  | onColumnVisibilityModelChange | (model) => void | Visibility change callback |
119
128
  | columnOrder | string[] | Controlled column order |
120
- | onColumnOrderChange | (params) => void | Column reorder callback |
129
+ | onColumnOrderChange | ({column, oldIndex, targetIndex}) => void | One column moved; indices in the full current column order |
130
+ | onColumnOrderModelChange | (columnOrder: string[]) => void | Whole new order after every change, incl. Columns panel Reset (use this to control columnOrder) |
121
131
  | disableColumnReorder | boolean | Disable drag-to-reorder |
122
- | disableColumnResize | boolean | Disable resize handles |
123
- | columnGroupingModel | GridColumnGroupingModel | Multi-level column header groups |
132
+ | columnGroupingModel | GridColumnGroupingModel | Multi-level column header groups; drag-reorder stays within a group |
124
133
 
125
134
  ### Row Features
126
135
  | Prop | Type | Description |
127
136
  |-------------------|---------------------------|-------------------------------------------|
128
- | pinnedRows | GridRowPinning | {top: id[], bottom: id[]} |
129
- | rowReordering | boolean | Enable drag-and-drop row reordering |
130
- | onRowOrderChange | (params) => void | Row reorder callback |
131
- | getRowId | (row: R) => GridRowId | Custom row ID accessor (default: row.id) |
132
- | getRowClassName | (params) => string | Dynamic CSS class per row |
133
- | getRowHeight | (params) => number | Dynamic row height |
137
+ | pinnedRows | GridRowPinning | {top: id[], bottom: id[]}. Ignored under treeData / rowGroupingModel (dev warning) |
138
+ | rowReordering | boolean | Enable drag-and-drop row reordering (no effect in pivot mode) |
139
+ | onRowOrderChange | ({row, oldIndex, targetIndex}) => void | Indices are positions in the `rows` prop |
140
+ | getRowId | (row: R) => GridRowId | Custom row ID accessor (default: row.id). Row objects are NOT given an `id` |
134
141
 
135
142
  ### Cell Editing
136
143
  | Prop | Type | Description |
137
144
  |-------------------------|----------------------------|-----------------------------------------|
138
- | processRowUpdate | (newRow, oldRow) => R | Commit edits. Must return updated row |
139
- | onProcessRowUpdateError | (error) => void | Handle commit errors |
140
- | isCellEditable | (params) => boolean | Fine-grained editability control |
145
+ | processRowUpdate | (newRow, oldRow) => R / Promise<R> | Called once per commit. Must return the updated row |
146
+ | onProcessRowUpdateError | (error) => void | processRowUpdate threw, rejected or returned no row (editor stays open) |
147
+ | isCellEditable | (params: GridCellParams) => boolean | Gates double-click, Enter and Tab editing; rejected cells get aria-readonly |
141
148
 
142
149
  ### Detail Panels (Master-Detail)
143
150
  | Prop | Type | Description |
144
151
  |-------------------------|----------------------------|-----------------------------------------|
145
- | getDetailPanelContent | (params) => ReactNode | Render expanded detail row |
146
- | getDetailPanelHeight | (params) => number / 'auto'| Height of detail panel |
147
- | expandedRowIds | Set<GridRowId> | Controlled expanded rows |
148
- | onDetailPanelExpandedRowIdsChange | (ids) => void | Expand/collapse callback |
152
+ | getDetailPanelContent | (params: {row, id, rowIndex}) => ReactNode | Called for expanded data rows only (never group rows) |
153
+ | pinExpandColumn | boolean (default true) | Pin the detail-panel expand column |
154
+ | getDetailPanelHeight | (params) => number / 'auto'| px, or 'auto' = measured content height. Default 200 |
155
+ | detailPanelExpandedRowIds | Set<GridRowId> | Controlled expanded rows |
156
+ | onDetailPanelExpandedRowIdsChange | (ids: Set<GridRowId>) => void | Expand/collapse callback |
149
157
 
150
158
  ### Grouping & Aggregation
151
159
  | Prop | Type | Description |
152
160
  |------------------------|---------------------------|------------------------------------------|
153
- | rowGroupingModel | string[] | Fields to group by |
154
- | aggregationModel | GridAggregationModel | {field: 'sum'/'avg'/'min'/'max'/'count'} |
155
- | getAggregationPosition | (group) => 'footer'/'inline' | Where to render aggregation row |
156
- | defaultGroupingExpansionDepth | number | Auto-expand depth (default -1 = all) |
161
+ | rowGroupingModel | string[] | Fields to group by (one nesting level per field). Disables pagination. groupable:false fields are skipped |
162
+ | aggregationModel | GridAggregationModel | {field: 'sum'/'avg'/'min'/'max'/'count'/'unique'} |
163
+ | onAggregationModelChange | (model) => void | Aggregation model changed (toolbar Summaries panel) |
164
+ | groupingColDef | Partial<GridColDef<R>> | Adds a dedicated `__group__` column pinned left (rowGroupingModel or treeData); no `field` needed; never exported |
165
+ | getAggregationPosition | (groupNode: GridTreeNode / null) => 'inline'/'footer'/null | Per group: 'inline' (default) on the group row, 'footer' on a subtotal row, null hides. Called with null for the grand total: null hides the footer |
166
+ | defaultGroupingExpansionDepth | number | Initial expansion depth. Default 0 (all collapsed); -1 = expand all |
157
167
 
158
168
  ### Tree Data
159
169
  | Prop | Type | Description |
160
170
  |---------------------------|----------------------------|--------------------------------------|
161
171
  | treeData | boolean | Enable tree data mode |
162
- | getTreeDataPath | (row) => string[] | Return path array for each row |
163
- | defaultGroupingExpansionDepth | number | Same as grouping depth |
172
+ | getTreeDataPath | (row) => string[] | Required with treeData (without it rows show flat). Keep it stable (module scope / useCallback) |
173
+ | defaultGroupingExpansionDepth | number | Same as grouping (default 0) |
174
+
175
+ Missing path segments become auto-created parent rows `{ id }` (id like `auto-group-["a","b"]`); their label is `params.rowMeta.groupLabel`.
164
176
 
165
177
  ### Pivot
166
178
  | Prop | Type | Description |
167
179
  |----------------|----------------|-----------------------------------------------|
168
- | pivotMode | boolean | Enable pivot view |
169
- | pivotModel | GridPivotModel | {rowFields, columnFields, valueFields} |
170
- | onPivotModelChange | (model) => void | Pivot change callback |
180
+ | pivotMode | boolean | Enable pivot view (treeData / rowGrouping / getRowId / dataSource are ignored while pivoting) |
181
+ | pivotModel | GridPivotModel | {rowFields, columnFields, valueFields: {field, aggFn: 'sum'/'avg'/'count'/'min'/'max', headerName?}[]} |
182
+ | onPivotModelChange | (model) => void | Pivot change callback (toolbar Pivot panel) |
183
+
184
+ Pivot row ids look like `__pivot_row__:["North"]`; the Grand Total row is always last and cannot be selected.
171
185
 
172
186
  ### Server-Side
173
187
  | Prop | Type | Description |
174
188
  |------------|-----------------|----------------------------------------------------|
175
- | dataSource | GridDataSource | Adapter for server-side fetching (see below) |
189
+ | dataSource | GridDataSource | Adapter for server-side fetching (see SERVER-SIDE DATA SOURCE) |
176
190
 
177
191
  ### Styling & Customization
178
192
  | Prop | Type | Description |
179
193
  |--------------|-----------------------|------------------------------------------|
180
- | slots | GridSlots | Replace toolbar, pagination, overlays |
181
- | slotProps | GridSlotProps | Props for slot components |
182
- | theme | GridTheme | Override theme tokens |
183
- | sx | React.CSSProperties | Inline styles on the root element |
194
+ | slots | { toolbar, pagination, noRowsOverlay, loadingOverlay, footer } | Replace built-in parts |
195
+ | slotProps | { toolbar, pagination, noRowsOverlay, loadingOverlay, footer } | Extra props for those slots |
196
+ | className | string | Extra CSS class on the root element |
197
+ | style | React.CSSProperties | Inline styles on the root element (use DataGridThemeProvider for theming) |
198
+
199
+ There are no getRowClassName / getCellClassName / theme props: use GridColDef.cellClassName (string or function) and DataGridThemeProvider.
184
200
 
185
201
  ### Callbacks
186
202
  | Prop | Type | Description |
187
203
  |-------------------|-----------------------|-------------------------------|
188
- | onRowClick | (params) => void | Row click |
189
- | onCellClick | (params) => void | Cell click |
190
- | onRowDoubleClick | (params) => void | Row double-click |
191
- | onRowsScrollEnd | (params) => void | Fired near bottom of scroll |
192
- | onStateChange | (state) => void | Fires on any state change |
204
+ | onRowClick | (params: {row, id, rowIndex}) => void | Row click, or Enter on a non-editable cell. Not fired for synthetic group rows (a click toggles them) |
205
+ | onCellClick | (params: GridCellParams) => void | Cell click (not inside an open editor) |
206
+ | onRowDoubleClick | (params) => void | Row double-click (also fires for group rows) |
207
+ | onRowsScrollEnd | (params) => void | Fires once each time the bottom comes into view (re-armed when rows are added or the user scrolls away) |
208
+ | onStateChange | (state: GridState) => void | Fires when sorting, filter, pagination, columns (widths/order/pinning/visibility) or density change |
193
209
 
194
210
  ### State Persistence
195
211
  | Prop | Type | Description |
196
212
  |--------------|------------------|----------------------------------------------|
197
- | initialState | GridInitialState | Restore column widths, sort, filter on mount |
198
- | apiRef | RefObject<GridApi> | Attach imperative API handle |
213
+ | initialState | GridInitialState | Starting sorting, filter, pagination, columns (widths, order, pinning, visibility) and density. Read at mount only |
214
+ | apiRef | MutableRefObject<GridApi> | Imperative API handle from useGridApiRef() |
199
215
 
200
216
  ### List View
201
217
  | Prop | Type | Description |
202
218
  |----------------|--------------------|---------------------------------------------|
203
- | listView | boolean | Render as card/list instead of table |
204
- | listViewColumn | GridListViewColDef | Defines the card renderer |
219
+ | listView | boolean | Render as cards instead of a table (requires listViewColumn; without it the grid view shows + dev warning). Honours loading, noRowsOverlay/loadingOverlay and slots.footer; rows are a roving Tab stop (arrows, Home/End, PageUp/Down, Enter/Space, Alt+Arrow expand); tree/group parents get a chevron. Not virtualized |
220
+ | listViewColumn | { field, renderCell } | Defines the card renderer |
205
221
 
206
222
  ---
207
223
 
@@ -211,31 +227,37 @@ export default function App() {
211
227
  |-------------------|--------------------------------------------------------|--------------------------------------------|
212
228
  | field | string | **Required.** Key in row data |
213
229
  | headerName | string | Column label |
214
- | width | number / string (px or %) | Column width |
215
- | flex | number | Flex grow factor |
216
- | minWidth | number | Minimum width constraint |
217
- | maxWidth | number | Maximum width constraint |
218
- | type | 'string'/'number'/'boolean'/'date'/'singleSelect'/'image' | Data type |
219
- | editable | boolean | Enable double-click editing |
220
- | valueOptions | string[] / {value, label}[] | Options for singleSelect type |
221
- | renderCell | (params: GridRenderCellParams) => ReactNode | Custom cell renderer |
230
+ | description | string | Header tooltip |
231
+ | width | number / string (px or %) | Column width. Neither width nor flex = flex: 1 |
232
+ | flex | number | Flex grow factor; `width` is ignored when flex is set |
233
+ | minWidth | number | Minimum width (layout and resize; resize default 50) |
234
+ | maxWidth | number | Maximum width (layout and resize; no max by default) |
235
+ | type | 'string'/'number'/'boolean'/'date'/'singleSelect'/'image' | Drives filter operators, sorting, editors and, without a valueFormatter, the cell text (date: local calendar date, boolean: Yes/No, singleSelect: option label, image: <img>); formattedValue, list view and exports use the same text |
236
+ | editable | boolean | Enable double-click / Enter editing |
237
+ | valueOptions | (string / number / {value, label})[] | Options for singleSelect type (editor commits the original option value) |
238
+ | renderCell | (params: GridRenderCellParams<R>) => ReactNode | Custom cell renderer (errors contained per cell) |
222
239
  | renderHeader | (params: GridRenderHeaderParams) => ReactNode | Custom header renderer |
223
- | renderEditCell | (params: GridRenderCellParams) => ReactNode | Custom editor component |
224
- | valueGetter | ({row, field, value}) => any | Computed cell value |
225
- | valueFormatter | ({value, row, field}) => string | Format display value |
240
+ | renderEditCell | (params: GridRenderEditCellParams<R>) => ReactNode | Custom editor: call params.onValueChange(v), params.onCommit(), params.onCancel() |
241
+ | valueGetter | ({row, field, value}) => unknown | Computed value; used by sort, filter, quick search, aggregation, copy, export. Must be pure (quick filter caches per row object); a throw reads as undefined + dev warning |
242
+ | valueSetter | ({value, row, field}) => R | Write an edited value back onto the row (editable valueGetter columns) |
243
+ | valueFormatter | ({value, row, field}) => string | Format display value (value is `unknown`: cast before calling methods) |
226
244
  | cellClassName | string / (params) => string | CSS class on cells |
227
245
  | headerClassName | string | CSS class on header cell |
228
- | colSpan | number / (params) => number | Cell column span |
229
- | rowSpan | number / (params) => number | Cell row span |
230
- | align | 'left'/'center'/'right' | Cell content alignment |
231
- | headerAlign | 'left'/'center'/'right' | Header content alignment |
246
+ | colSpan | number / (params) => number | Cell column span (clamped to the pinned section; hidden columns skipped) |
247
+ | rowSpan | number / (params) => number | Cell row span (clamped to the row section) |
248
+ | align | 'left'/'center'/'right' | Cell content alignment |
249
+ | headerAlign | 'left'/'center'/'right' | Header content alignment |
232
250
  | sortable | boolean (default true) | Enable sorting |
233
- | filterable | boolean (default true) | Enable filtering |
234
- | resizable | boolean (default true) | Enable resize handle |
235
- | hideable | boolean (default true) | Allow hiding via toolbar |
236
- | pinnable | boolean (default true) | Allow pinning via column menu |
237
- | exportable | boolean (default true) | Include in CSV/Excel/JSON/Print exports |
251
+ | filterable | boolean (default true) | Enable filtering and quick search |
252
+ | resizable | boolean (default true) | Enable resize handle (mouse, touch, Alt+Arrow on header) |
253
+ | hideable | boolean (default true) | Allow hiding via toolbar / column menu |
254
+ | pinnable | boolean (default true) | Allow pinning via column menu (does not affect drag-reorder) |
255
+ | exportable | boolean (default true) | Include in CSV/Excel/JSON/Print/PDF exports and clipboard copy |
238
256
  | disableColumnMenu | boolean | Hide the ⋮ column menu button |
257
+ | groupable | boolean | false = skipped by rowGroupingModel and the pivot panel |
258
+ | groupingValueFormatter | ({field, value}) => string | Group row label (default "field: value") |
259
+ | aggregable | boolean | false = no aggregation. Summaries panel offers number columns + aggregable: true |
260
+ | availableAggregationFunctions | string[] | Restrict aggregation functions for this column |
239
261
 
240
262
  ---
241
263
 
@@ -244,37 +266,45 @@ export default function App() {
244
266
  ```tsx
245
267
  import { useGridApiRef } from '@opencorestack/opengridx';
246
268
  const apiRef = useGridApiRef();
247
- <DataGrid apiRef={apiRef} ... />
269
+ <DataGrid apiRef={apiRef} rows={rows} columns={columns} />
248
270
 
249
271
  // Row access
250
- apiRef.current.getRow(id) // → GridRowModel | null
251
- apiRef.current.getAllRows() // → GridRowModel[]
252
- apiRef.current.getVisibleRows() // → GridRowModel[]
272
+ apiRef.current.getRow(id) // → GridRowModel | null
273
+ apiRef.current.getAllRows() // → every loaded row
274
+ apiRef.current.getVisibleRows() // → rows on screen: current page + pinned rows
275
+ apiRef.current.getAllFilteredRows() // → every filtered + sorted row, all pages (use for exports); tree data + filter: matching rows only
276
+ apiRef.current.getGroupedExportRows() // → GridGroupedExportRow[] | null (row grouping only)
253
277
 
254
278
  // Column access
255
- apiRef.current.getColumn(field) // → GridColDef | null
256
- apiRef.current.getAllColumns() // → GridColDef[]
257
- apiRef.current.getVisibleColumns() // → GridColDef[]
279
+ apiRef.current.getColumn(field) // → GridColDef | null
280
+ apiRef.current.getAllColumns() // → GridColDef[]
281
+ apiRef.current.getVisibleColumns() // → visible columns in render order (left-pinned, unpinned, right-pinned)
258
282
 
259
283
  // Selection
260
- apiRef.current.selectRow(id, isSelected?) // select/deselect a row
261
- apiRef.current.selectRows(ids, isSelected?) // bulk select
262
- apiRef.current.getSelectedRows() // → GridRowId[]
284
+ apiRef.current.selectRow(id, isSelected?) // select/deselect a row (synthetic group/total ids ignored; no-op fires nothing)
285
+ apiRef.current.selectRows(ids, isSelected?) // bulk select (same rules)
286
+ apiRef.current.getSelectedRows() // → GridRowId[]
263
287
 
264
- // Sort / Filter
288
+ // Sort / Filter (setters update the grid and fire the change callbacks)
265
289
  apiRef.current.sortColumn(field, 'asc' | 'desc' | null)
266
- apiRef.current.getSortModel() // → GridSortItem[]
290
+ apiRef.current.getSortModel() // → GridSortItem[]
267
291
  apiRef.current.setFilterModel(model)
292
+ apiRef.current.getFilterModel() // → GridFilterModel
268
293
 
269
294
  // Pagination
270
295
  apiRef.current.setPage(page)
271
296
  apiRef.current.setPageSize(size)
272
297
 
298
+ // Scroll
299
+ apiRef.current.scrollToIndexes({ rowIndex, colIndex })
300
+
273
301
  // Clipboard
274
- apiRef.current.copySelectedRows() // → Promise<void> (TSV to clipboard)
302
+ apiRef.current.copySelectedRows() // → Promise<void>; rejects if the write fails. Copies every selected row that passes
303
+ // the filter (all pages, collapsed groups, pinned rows), visible columns in screen order
275
304
 
276
- // Aggregation (when aggregationModel is set)
277
- apiRef.current.getAggregationResult() // → GridAggregationResult | null
305
+ // Aggregation
306
+ apiRef.current.getAggregationResult() // → Record<string, unknown> | null (null in pivot mode)
307
+ apiRef.current.getAggregationModel() // → GridAggregationModel | null
278
308
  ```
279
309
 
280
310
  ---
@@ -282,10 +312,13 @@ apiRef.current.getAggregationResult() // → GridAggregationResult | null
282
312
  ## CELL EDITING — FULL EXAMPLE
283
313
 
284
314
  ```tsx
285
- const [rows, setRows] = useState(initialRows);
315
+ // Your own row type: an interface or a type alias, no index signature needed (v3+)
316
+ interface Person { id: number; name: string; age: number; role: string }
317
+
318
+ const [rows, setRows] = useState<Person[]>(initialRows);
286
319
 
287
- const processRowUpdate = (newRow, oldRow) => {
288
- setRows(prev => prev.map(r => r.id === newRow.id ? newRow : r));
320
+ const processRowUpdate = (newRow: Person) => {
321
+ setRows(prev => prev.map(r => (r.id === newRow.id ? newRow : r)));
289
322
  return newRow; // Must return the committed row
290
323
  };
291
324
 
@@ -302,41 +335,57 @@ const processRowUpdate = (newRow, oldRow) => {
302
335
  />
303
336
  ```
304
337
 
338
+ Double-click or Enter opens the editor; Enter / Tab / blur commits; Escape cancels. An edit whose row scrolls, pages or filters away is committed, not dropped.
339
+
305
340
  ---
306
341
 
307
342
  ## SERVER-SIDE DATA SOURCE
308
343
 
344
+ Pass a `dataSource` object to DataGrid; there is no separate data-source hook to import.
345
+
309
346
  ```tsx
310
- import { useGridDataSource } from '@opencorestack/opengridx';
347
+ import { DataGrid, type GridDataSource } from '@opencorestack/opengridx';
311
348
 
312
349
  const dataSource: GridDataSource = {
313
- getRows: async ({ page, pageSize, sortModel, filterModel }) => {
314
- const data = await fetch(`/api/rows?page=${page}&size=${pageSize}`).then(r => r.json());
315
- return { rows: data.items, rowCount: data.total };
350
+ // params: { startRow, endRow, sortModel, filterModel, groupKeys, aggregationModel? }
351
+ getRows: async ({ startRow, endRow, sortModel, filterModel, groupKeys }) => {
352
+ const res = await fetch('/api/rows', {
353
+ method: 'POST',
354
+ body: JSON.stringify({ startRow, endRow, sortModel, filterModel, groupKeys }),
355
+ });
356
+ const data = await res.json();
357
+ return { rows: data.items, rowCount: data.total }; // rowCount optional, needed for server paging
358
+ },
359
+ // Optional: totals for the aggregation footer
360
+ getAggregations: async ({ sortModel, filterModel, aggregationModel }) => {
361
+ return fetch('/api/totals').then(r => r.json());
316
362
  },
317
- // Optional: lazy-load children for server-side tree data
318
- getChildren: async ({ row }) => {
319
- return fetch(`/api/rows/${row.id}/children`).then(r => r.json());
320
- }
321
363
  };
322
364
 
323
365
  <DataGrid
324
366
  columns={columns}
325
- rows={[]} // empty — data comes from dataSource
367
+ rows={[]} // data comes from dataSource
326
368
  dataSource={dataSource}
369
+ pagination
327
370
  paginationMode="server"
328
371
  sortingMode="server"
329
372
  filterMode="server"
330
- rowCount={1000}
331
373
  />
332
374
  ```
333
375
 
376
+ - Requests are page-based: server pagination asks for `[page*pageSize, (page+1)*pageSize)`; client pagination (default modes) asks once for every row (`endRow: Number.MAX_SAFE_INTEGER`); `paginationMode="infinite"` asks for the rows after those loaded, up to `(page+1)*pageSize`, and appends them (advance `paginationModel.page` from `onRowsScrollEnd`).
377
+ - `endRow` is exclusive: return `allRows.slice(startRow, endRow)`.
378
+ - Server tree data: rows with `serverChildrenCount > 0` get an expand toggle; expanding calls `getRows` with the row's path in `groupKeys`. There is no `getChildren`.
379
+ - Refetch happens only when a model's content or the `getRows` function changes; stale responses are discarded; 300 ms debounce.
380
+ - A rejected `getRows` (or a response that cannot be processed) shows the error overlay; its Retry button calls `getRows` again.
381
+ - Row grouping with `paginationMode="server"` fetches every row in one request (`[0, Number.MAX_SAFE_INTEGER)`, dev warning). Server tree data with `defaultGroupingExpansionDepth` auto-expands and loads lazy nodes (one request per node).
382
+
334
383
  ---
335
384
 
336
385
  ## THEMING
337
386
 
338
387
  ```tsx
339
- import { DataGridThemeProvider, darkTheme } from '@opencorestack/opengridx';
388
+ import { DataGridThemeProvider, darkTheme, type GridTheme } from '@opencorestack/opengridx';
340
389
 
341
390
  // Built-in themes: darkTheme, roseTheme, emeraldTheme, amberTheme, compactTheme
342
391
  <DataGridThemeProvider theme={darkTheme}>
@@ -344,25 +393,27 @@ import { DataGridThemeProvider, darkTheme } from '@opencorestack/opengridx';
344
393
  </DataGridThemeProvider>
345
394
 
346
395
  // Custom theme override:
347
- const myTheme = {
396
+ const myTheme: GridTheme = {
397
+ mode: 'light', // base palette the provider pins ('light' default, or 'dark')
348
398
  colors: {
349
399
  primary: '#6366f1',
350
400
  },
351
401
  grid: {
352
- headerBackground: '#1e1e2e',
353
- rowHoverBackground: '#2a2a3e',
402
+ headerBackground: '#eef2ff',
403
+ rowHoverBackground: '#f5f3ff',
404
+ rowHeightStandard: '40px', // theme heights size rows/header; rowHeight/headerHeight props win
354
405
  },
355
406
  overlays: {
356
- background: '#1e1e2e',
357
- border: '#2a2a3e',
358
- }
407
+ background: '#ffffff',
408
+ border: '#e0e7ff',
409
+ },
359
410
  };
360
411
  <DataGridThemeProvider theme={myTheme}>
361
- <DataGrid ... />
412
+ <DataGrid rows={rows} columns={columns} />
362
413
  </DataGridThemeProvider>
363
414
 
364
- // Note: Panels and Dropdowns (Column Menu, Pivot, etc.) intelligently hunt for
365
- // `.ogx-theme-provider` to inherit these variables organically through portals.
415
+ // The provider pins a complete light or dark palette regardless of the OS colour scheme.
416
+ // Panels, menus and tooltips render into `.ogx-theme-provider`, so they inherit the theme.
366
417
  ```
367
418
 
368
419
  ---
@@ -372,21 +423,24 @@ const myTheme = {
372
423
  ```tsx
373
424
  import { GridToolbar } from '@opencorestack/opengridx';
374
425
 
375
- // Replace built-in components:
376
426
  <DataGrid
427
+ rows={rows}
428
+ columns={columns}
377
429
  slots={{
378
- toolbar: MyCustomToolbar,
379
- pagination: MyPagination,
430
+ toolbar: GridToolbar, // or your own component
431
+ pagination: MyPagination, // props: page, pageSize, rowCount, pageSizeOptions, onPageChange, onPageSizeChange, localeText
380
432
  noRowsOverlay: EmptyState,
381
433
  loadingOverlay: Spinner,
382
- footer: CustomFooter,
434
+ footer: CustomFooter, // replaces the pager; see note 14
383
435
  }}
384
436
  slotProps={{
385
- toolbar: { onExport: () => apiRef.current.copySelectedRows() },
437
+ toolbar: { showNonHideableColumns: true },
386
438
  }}
387
439
  />
388
440
  ```
389
441
 
442
+ `GridToolbar` in `slots.toolbar` gets search, Columns, Filters and Summaries (plus Pivot when pivot is configured); they work without any filter/visibility props. Render props: `renderQuickFilter`, `renderColumnsButton`, `renderFilterButton`, `renderAggregationButton`, `renderExportButton`.
443
+
390
444
  ---
391
445
 
392
446
  ## STATE PERSISTENCE
@@ -395,11 +449,11 @@ import { GridToolbar } from '@opencorestack/opengridx';
395
449
  import { useGridStateStorage } from '@opencorestack/opengridx';
396
450
 
397
451
  function MyGrid() {
398
- const { initialState, handleStateChange } = useGridStateStorage('my-grid-v1');
452
+ const { initialState, onStateChange, clearState } = useGridStateStorage('my-grid-v1');
399
453
  return (
400
454
  <DataGrid
401
455
  initialState={initialState}
402
- onStateChange={handleStateChange}
456
+ onStateChange={onStateChange}
403
457
  rows={rows}
404
458
  columns={columns}
405
459
  />
@@ -407,6 +461,8 @@ function MyGrid() {
407
461
  }
408
462
  ```
409
463
 
464
+ Also accepts `{ key, debounceMs?, include?: (keyof GridState)[], storage? }`. `initialState` is read at mount: when the key changes, remount with `<DataGrid key={key} />`. If storage is blocked, the grid works without persistence.
465
+
410
466
  ---
411
467
 
412
468
  ## EXPORT UTILITIES
@@ -414,91 +470,141 @@ function MyGrid() {
414
470
  ```tsx
415
471
  import {
416
472
  exportToCsv,
417
- exportToExcel, // HTML-based .xlsx — zero deps
418
- exportToExcelAdvanced, // Rich .xlsx binary — requires: npm install exceljs
473
+ exportToExcel, // HTML table saved as .xls — zero deps
474
+ exportToExcelAdvanced, // real .xlsx — requires: npm install exceljs
419
475
  exportToJson,
476
+ exportToPdf, // requires: npm install jspdf jspdf-autotable
420
477
  printGrid,
421
478
  } from '@opencorestack/opengridx';
422
479
 
423
- exportToCsv(rows, columns, { fileName: 'export.csv' });
424
- exportToExcel(rows, columns, { fileName: 'export.xlsx' });
425
- exportToExcelAdvanced(rows, columns, {
480
+ const rows = apiRef.current.getAllFilteredRows(); // what the grid shows, all pages
481
+ const columns = apiRef.current.getVisibleColumns(); // visible columns, display order
482
+
483
+ exportToCsv(rows, columns, { fileName: 'export.csv' }); // UTF-8 BOM (bom: false to omit)
484
+ exportToExcel(rows, columns, { fileName: 'export.xls', sheetName: 'Data' });
485
+ await exportToExcelAdvanced(rows, columns, {
426
486
  fileName: 'export.xlsx',
427
- sheetName: 'Data',
487
+ sheets: [{ name: 'Data' }, { name: 'Selected', rows: 'selected' }],
488
+ selectedRows: apiRef.current.getSelectedRows(),
428
489
  columnStyles: {
429
490
  avatar: { embedImage: true, imageWidth: 32, imageHeight: 32 }
430
491
  }
431
492
  });
432
493
  exportToJson(rows, columns, { fileName: 'export.json' });
433
- printGrid(rows, columns, 'My Report Title');
494
+ await exportToPdf(rows, columns, { fileName: 'export', title: 'My Report' });
495
+ await printGrid(rows, columns, 'My Report Title');
434
496
  ```
435
497
 
498
+ - Every export accepts `selectedRows`, `getRowId` (pass the grid's getRowId so ids match), `aggregationResult` + `aggregationModel` (totals row) and `groupedRows` from `apiRef.current.getGroupedExportRows()`.
499
+ - CSV and exportToExcel neutralise formulas: text starting with = + - @ tab or CR gets a leading `'` (`escapeFormulas: false` opts out).
500
+ - exportToExcel writes HTML: a `.xlsx` fileName is saved as `.xls` with a console warning.
501
+ - Columns with `exportable: false` are left out everywhere; `valueGetter` and `valueFormatter` are applied.
502
+
436
503
  ---
437
504
 
438
505
  ## CSS CUSTOMIZATION
439
506
 
440
- All CSS variables use the `--ogx-` prefix. Set them on the grid wrapper:
507
+ All CSS variables use the `--ogx-` prefix and are declared on `:root` by the stylesheet. Override them on a wrapper (or use DataGridThemeProvider, which sets the same variables):
441
508
 
442
509
  ```css
443
510
  .my-grid-wrapper {
444
511
  --ogx-color-primary: #6366f1;
445
- --ogx-color-header-bg: #1e1e2e;
446
- --ogx-color-row-hover: rgba(99, 102, 241, 0.08);
447
- --ogx-color-border: #2a2a3e;
448
- --ogx-font-size-cell: 13px;
449
- --ogx-border-radius: 8px;
512
+ --ogx-grid-header-background: #eef2ff;
513
+ --ogx-grid-row-hover-background: rgba(99, 102, 241, 0.08);
514
+ --ogx-grid-border-color: #e0e7ff;
515
+ --ogx-grid-cell-font-size: 13px;
516
+ --ogx-border-radius-md: 8px;
450
517
  }
451
518
  ```
452
519
 
453
- All class names use the `ogx__` BEM prefix, e.g.:
454
- - `ogx__row`, `ogx__row--selected`, `ogx__row--even`
520
+ Derived tokens (e.g. `--ogx-grid-cell-focus-border`, which defaults to the primary colour) are resolved on `:root`, so override them directly when you change `--ogx-color-primary` on a wrapper.
521
+
522
+ Class names use the `ogx__` BEM prefix, e.g.:
523
+ - `ogx__row`, `ogx__row--selected`, `ogx__row--even`, `ogx__row--odd`
455
524
  - `ogx__cell`, `ogx__cell--focused`, `ogx__cell--editing`
456
525
  - `ogx__header-cell`, `ogx__header-cell--sorted`
526
+ - Sticky wrappers: `ogx__sticky-top` (header + top-pinned rows), `ogx__sticky-bottom` (bottom-pinned rows + aggregation footer)
457
527
 
458
528
  ---
459
529
 
460
530
  ## TYPE REFERENCE
461
531
 
462
532
  ```ts
463
- type GridRowModel = Record<string, any> & { id: GridRowId };
464
533
  type GridRowId = string | number;
534
+ interface GridRowModel { id: GridRowId; [key: string]: unknown; }
465
535
  type GridSortDirection = 'asc' | 'desc' | null;
466
- type GridPinnedPosition = 'left' | 'right' | null;
536
+ type GridPinnedPosition = 'left' | 'right';
537
+
538
+ type GridFilterOperator =
539
+ | 'contains' | 'equals' | 'startsWith' | 'endsWith' // string
540
+ | 'isEmpty' | 'isNotEmpty' | 'isAnyOf'
541
+ | '=' | '!=' | '>' | '>=' | '<' | '<=' // number
542
+ | 'is' | 'not' | 'after' | 'onOrAfter' | 'before' | 'onOrBefore'; // date / boolean / singleSelect
467
543
 
468
544
  interface GridSortItem { field: string; sort: 'asc' | 'desc'; }
469
- interface GridFilterItem { field: string; operator: string; value: any; }
470
- interface GridFilterModel { items: GridFilterItem[]; logicOperator?: 'and' | 'or'; }
545
+ interface GridFilterItem { id?: string | number; field: string; operator: GridFilterOperator; value?: unknown; }
546
+ interface GridFilterGroup { id?: string | number; logicOperator: 'and' | 'or'; items: (GridFilterItem | GridFilterGroup)[]; }
547
+ interface GridFilterModel {
548
+ items?: (GridFilterItem | GridFilterGroup)[];
549
+ logicOperator?: 'and' | 'or';
550
+ quickFilterValues?: string[]; // every term must match some visible, filterable column
551
+ }
471
552
  interface GridPaginationModel { page: number; pageSize: number; }
472
553
  interface GridColumnPinning { left?: string[]; right?: string[]; }
473
554
  interface GridRowPinning { top?: GridRowId[]; bottom?: GridRowId[]; }
474
555
 
475
556
  interface GridRenderCellParams<R = GridRowModel> {
476
- value: any; row: R; field: string;
557
+ value: unknown; formattedValue?: string; row: R; field: string;
477
558
  colDef: GridColDef<R>; rowIndex: number; colIndex: number;
559
+ rowMeta?: GridRowMeta; // hierarchy info for tree-data / grouping rows
560
+ }
561
+ interface GridRowMeta {
562
+ hasChildren?: boolean; treeDepth?: number; groupingField?: string; groupingValue?: unknown;
563
+ groupLabel?: string; descendantCount?: number; isExpanded?: boolean;
564
+ isGroupRow?: boolean; isGroupFooter?: boolean;
478
565
  }
479
566
 
480
- interface GridDataSource {
481
- getRows: (params: GridGetRowsParams) => Promise<GridGetRowsResponse>;
482
- getChildren?: (params: { row: GridRowModel }) => Promise<GridRowModel[]>;
567
+ interface GridDataSource<R = GridRowModel> {
568
+ getRows: (params: GridGetRowsParams) => Promise<GridGetRowsResponse<R>>;
569
+ getAggregations?: (params: Omit<GridGetRowsParams, 'startRow' | 'endRow'>) => Promise<Record<string, unknown>>;
483
570
  }
484
571
  interface GridGetRowsParams {
485
- page: number; pageSize: number;
572
+ startRow: number; endRow: number; // endRow exclusive; Number.MAX_SAFE_INTEGER = all rows
486
573
  sortModel: GridSortItem[]; filterModel: GridFilterModel;
574
+ groupKeys: string[]; // server tree data: path of the parent being expanded
575
+ aggregationModel?: GridAggregationModel;
576
+ }
577
+ interface GridGetRowsResponse<R = GridRowModel> {
578
+ rows: R[]; rowCount?: number; aggregationResults?: Record<string, unknown>;
487
579
  }
488
- interface GridGetRowsResponse { rows: GridRowModel[]; rowCount: number; }
489
580
  ```
490
581
 
491
582
  ---
492
583
 
584
+ ## KEYBOARD
585
+
586
+ Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, PageUp/PageDown; Enter edits an editable cell, otherwise acts like a row click (toggles group/tree rows); Tab leaves the grid (while editing it moves to the next editable cell); Shift+Space selects the row (Space toggles the checkbox or detail panel when those cells are focused); Ctrl/Cmd+A selects all; Ctrl/Cmd+C copies the selection; Alt+ArrowRight/Left expands/collapses; Enter/Space on a header sorts (Shift adds to the sort); Alt+ArrowDown or Ctrl+Enter on a header opens the column menu; Alt+ArrowLeft/Right on a header resizes it.
587
+
588
+ ---
589
+
493
590
  ## KEY NOTES FOR AI AGENTS
494
591
 
495
- 1. **Always `return newRow`** from `processRowUpdate` — failing to do so breaks the internal state update.
496
- 2. **`getRowId` default is `row.id`** — if your data uses a different key, always pass `getRowId`.
497
- 3. **CSS must be imported**: if grid is unstyled, add `import '@opencorestack/opengridx/styles'` to app root.
498
- 4. **ExcelJS is optional**: `exportToExcelAdvanced` requires `npm install exceljs` separately (peer dep).
499
- 5. **`dataSource` + `rows={[]}`**: when using server-side mode, pass empty `rows` array — the grid fills via `dataSource.getRows`.
500
- 6. **`flex` columns**: columns with `flex` expand to fill available space. Setting `width` on a flex column sets its base size but it will still flex.
592
+ 1. **Always `return newRow`** from `processRowUpdate` — returning nothing calls `onProcessRowUpdateError` and keeps the editor open.
593
+ 2. **`getRowId` default is `row.id`** — if your data uses a different key, always pass `getRowId`. The grid does not write an `id` onto your rows, and exports need `getRowId` in their options to match `selectedRows`.
594
+ 3. **CSS must be imported**: always add `import '@opencorestack/opengridx/styles'` to the app root. The JS bundle does not load it; dev builds warn once ("The grid stylesheet is not loaded") when it is missing.
595
+ 4. **Optional peers**: `exportToExcelAdvanced` needs `exceljs`; `exportToPdf` needs `jspdf` + `jspdf-autotable`.
596
+ 5. **`dataSource` + `rows={[]}`**: with a dataSource pass an empty `rows` array — the grid fills it via `dataSource.getRows`.
597
+ 6. **`flex` columns**: columns with `flex` share the free space; `width` is ignored on them (use `minWidth` / `maxWidth`). A column with neither `width` nor `flex` acts like `flex: 1`.
501
598
  7. **`singleSelect` editing**: for dropdown editors in cells, set `type: 'singleSelect'` and provide `valueOptions`.
502
- 8. **`initialState.columns.columnWidths`**: accepts `Record<string, number>` to restore saved column widths.
599
+ 8. **`initialState.columns.columnWidths`**: accepts `Record<string, number>` to restore saved column widths. Every part of `initialState` is optional.
503
600
  9. **`pinnedColumns`** and **`columnWidths`** are independent state — pinning does not reset widths.
504
- 10. **Aggregation** requires `rowGroupingModel` OR a global footer via `getAggregationPosition={() => 'footer'}`.
601
+ 10. **Aggregation**: setting `aggregationModel` alone renders a sticky footer with the totals; with `rowGroupingModel` each group row also shows its own aggregates. `null`/`undefined`, blank strings, booleans and arrays are ignored by sum/avg/min/max; min/max of dates return a `Date`; `valueGetter` columns aggregate the computed value; footer totals go through the column's `valueFormatter` (not for count/unique).
602
+ 11. **GOTCHAS — bounded height**: virtualization only works if the grid's container has a bounded height. A flex child without `min-height: 0` grows to fit every row, so every row is rendered. Pagination hides this, and row grouping turns pagination off. Use `<div style={{ flex: 1, minHeight: 0 }}>` around `height="100%"`, or pass a fixed pixel `height`. Dev builds log a warning when this happens.
603
+ 12. **Pagination is ignored under row grouping** — a grouped grid is one scrollable, virtualized list.
604
+ 13. **`valueFormatter` + `renderCell`**: `renderCell` receives `params.formattedValue` (v2.1+), so a custom renderer can reuse the formatter instead of duplicating it.
605
+ 14. **`slots.footer`** replaces the pagination area and receives `apiRef`, `aggregationModel`, `aggregationResult`, `rowCount` (filtered data rows excluding pinned rows in flat grids — data rows under tree data / grouping; the server total with `paginationMode="server"`), `pagination`, `paginationModel`, `pageSizeOptions` and `onPaginationModelChange`.
606
+ 15. **v3: no underscore row fields** — `params.row._hasChildren` / `_treeDepth` / … are `undefined` since 3.0.0; use `params.rowMeta?.hasChildren` etc. Under grouping and tree data `params.row` is the consumer's own object; never mutate it in render callbacks. Migration guide: `docs/migration/v2-to-v3.md`.
607
+ 16. **Row limit**: the scroll height is `rows × rowHeight` px and browsers cap element height (Chromium 33,554,432 px), so one scrolling grid reaches ~645,000 rows at 52 px. Page or stream beyond that.
608
+ 17. **Server modes without dataSource**: with `paginationMode` / `sortingMode` / `filterMode` = `'server'` the grid shows `rows` as given (no slicing, sorting or filtering). Fetch from the change callbacks and control the models.
609
+ 18. **`valueFormatter` / `valueGetter` params are `unknown`-typed**: cast (`(value as number).toFixed(2)`) before calling methods, or TypeScript strict mode rejects it.
610
+ 19. **Inline `columns` are reused while shallow-equal**: a new array with the same column objects does not re-run filtering/sorting, but mutating a column object in place is not detected — pass new objects when a definition changes.