@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
@@ -0,0 +1,111 @@
1
+ # `useGridScrollSync`
2
+
3
+ Internal hook. Manages all scroll-related state and the `handleScroll` callback for the grid viewport — including RAF-batched tick updates that trigger virtualization recomputes and the `onRowsScrollEnd` threshold check for infinite scroll.
4
+
5
+ **File:** `lib/hooks/core/useGridScrollSync.ts`
6
+
7
+ ---
8
+
9
+ ## Purpose
10
+
11
+ Before extraction, three pieces of scroll logic lived inline in `DataGrid.tsx`:
12
+ 1. `scrollPosRef` / `scrollRafRef` / `scrollTick` state setup (~5 lines)
13
+ 2. A cleanup effect canceling the pending RAF on unmount (~4 lines)
14
+ 3. `handleScroll` callback with RAF scheduling and scroll-end threshold (~22 lines)
15
+
16
+ Extracting them gives the scroll logic its own test surface and removes 31 lines from `DataGrid.tsx`.
17
+
18
+ ---
19
+
20
+ ## Parameters
21
+
22
+ ```ts
23
+ interface UseGridScrollSyncParams {
24
+ onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
25
+ /** Minimum overscan rows — the adaptive algorithm never goes below this. Defaults to 3. */
26
+ overscanRowCount?: number;
27
+ }
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Returns
33
+
34
+ ```ts
35
+ interface UseGridScrollSyncResult {
36
+ scrollTop: number;
37
+ scrollLeft: number;
38
+ /** Dynamically computed overscan row count based on current scroll velocity. */
39
+ overscanRows: number;
40
+ handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
41
+ }
42
+ ```
43
+
44
+ | Return | Used by |
45
+ |--------|---------|
46
+ | `scrollTop` | `useGridVirtualization` — vertical scroll offset for computing the visible row window |
47
+ | `scrollLeft` | `useGridVirtualization` — horizontal scroll offset for computing the visible column window |
48
+ | `overscanRows` | `useGridVirtualization` — adaptive overscan count, set from current scroll velocity |
49
+ | `handleScroll` | Passed to the viewport `<div onScroll={handleScroll}>` |
50
+
51
+ ---
52
+
53
+ ## RAF batching pattern
54
+
55
+ `handleScroll` fires on every scroll event (can be 60+ per second). Rather than recomputing the virtual window on every event, the hook schedules a single RAF per frame:
56
+
57
+ ```
58
+ scroll event → measure velocity → cancel pending RAF → schedule new RAF
59
+ → RAF fires → setScrollState({ scrollTop, scrollLeft, overscanRows })
60
+ ```
61
+
62
+ All three values are bundled into a single `setScrollState` call so `useGridVirtualization` recomputes exactly once per frame. Scroll velocity (px/ms) is computed as `deltaPos / deltaTime` from the previous scroll event, then mapped to an overscan tier via `velocityToOverscan()`:
63
+
64
+ | Velocity (px/ms) | `overscanRows` |
65
+ |---|---|
66
+ | < 0.5 | 3 |
67
+ | 0.5 – 3 | 5 |
68
+ | 3 – 15 | 12 |
69
+ | 15 – 40 | 20 |
70
+ | > 40 | 30 |
71
+
72
+ The state holds only the velocity-derived count (0 at rest); the returned value is `Math.max(velocityCount, overscanRowCount)`, computed during render, so a new `overscanRowCount` applies immediately.
73
+
74
+ ---
75
+
76
+ ## Decay timer
77
+
78
+ 200 ms after the last scroll event, a `setTimeout` callback resets `overscanRows` back to `overscanRowCount`. This prevents the grid from holding a large overscan buffer while the user is idle.
79
+
80
+ ---
81
+
82
+ ## `onRowsScrollEnd` threshold
83
+
84
+ When a **vertical** scroll brings the viewport within 100px of the bottom (`scrollHeight - scrollTop - clientHeight < 100`), the hook calls `onRowsScrollEnd` synchronously (not RAF-batched) so the consumer can load the next page immediately. A latch makes it fire once per arrival: it re-arms when the viewport leaves the threshold or `rowCount` changes. After every `rowCount` change an effect re-checks the viewport, so a page that does not fill it still fires (skipped with `autoHeight` or a 0px viewport). Horizontal-only scroll events are ignored.
85
+
86
+ ## Viewport remounts
87
+
88
+ `attachViewport` is the viewport's callback ref (combined by `useGridViewport` with `useGridViewportSize`, which attaches a `ResizeObserver` to every viewport element that mounts). When the viewport remounts (list view switched off again) it restores the last scroll position onto the new element; if the browser clamps it, the state is synced to the element.
89
+
90
+ ---
91
+
92
+ ## Cleanup
93
+
94
+ The hook registers a single `useEffect` with an empty dependency array to cancel any pending RAF and decay timer on unmount:
95
+
96
+ ```ts
97
+ useEffect(() => {
98
+ return () => {
99
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
100
+ if (decayRef.current !== null) clearTimeout(decayRef.current);
101
+ };
102
+ }, []);
103
+ ```
104
+
105
+ This prevents a `setState` call on an unmounted component if the grid is removed mid-scroll.
106
+
107
+ ---
108
+
109
+ ## 🔗 Related
110
+ - [useGridVirtualization](use-grid-virtualization.md)
111
+ - [useGridStateSnapshot](use-grid-state-snapshot.md)
@@ -0,0 +1,90 @@
1
+ # `useGridStateSnapshot`
2
+
3
+ Internal hook. Fires the `onStateChange` callback whenever the *value* of any tracked grid state changes — sorting, filtering, pagination, column configuration or density. Packages the current state into a `GridState` snapshot and delivers it to the caller.
4
+
5
+ **File:** `lib/hooks/core/useGridStateSnapshot.ts`
6
+
7
+ ---
8
+
9
+ ## Purpose
10
+
11
+ Before extraction, ~28 lines of state-snapshot logic lived inline in `DataGrid.tsx` — a `useRef` to hold the latest `onStateChange` callback plus a `useEffect` that fired on state changes. Extracting them makes the snapshot contract explicit and independently testable.
12
+
13
+ ---
14
+
15
+ ## Parameters
16
+
17
+ ```ts
18
+ interface UseGridStateSnapshotParams {
19
+ onStateChange?: (state: GridState) => void;
20
+ sortModel: GridSortItem[];
21
+ filterModel: GridFilterModel;
22
+ effectivePaginationModel: GridPaginationModel;
23
+ columnWidths: Record<string, number>;
24
+ effectiveColumnOrder: string[];
25
+ columnVisibilityModel: Record<string, boolean>;
26
+ pinnedColumns: GridColumnPinning; // the user's pins, not the grid's effective pins
27
+ density: 'compact' | 'standard' | 'comfortable';
28
+ }
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Returns
34
+
35
+ `void` — this is a side-effect-only hook.
36
+
37
+ ---
38
+
39
+ ## Snapshot shape
40
+
41
+ The hook assembles a `GridState` object on every tracked change:
42
+
43
+ ```ts
44
+ {
45
+ sorting: { sortModel },
46
+ filter: { filterModel },
47
+ pagination: { paginationModel: effectivePaginationModel },
48
+ columns: {
49
+ columnWidths,
50
+ columnOrder: effectiveColumnOrder,
51
+ columnVisibilityModel,
52
+ pinnedColumns,
53
+ },
54
+ density: { density },
55
+ }
56
+ ```
57
+
58
+ Grid-owned columns never appear in it: the synthetic `__group__` column (added when `groupingColDef` is set under row grouping) is removed from `columnOrder`, `pinnedColumns`, `columnWidths` and `columnVisibilityModel`, so the persisted state only names the consumer's own columns.
59
+
60
+ This matches the `GridState` interface from `lib/state/types.ts` and is the same structure accepted by `initialState` / `GridInitialState` — so a caller can persist the snapshot and restore it directly.
61
+
62
+ ---
63
+
64
+ ## Fires on value changes, not identity changes
65
+
66
+ Inline model props (`filterModel={{ items: [] }}`, `paginationModel={{ page, pageSize }}`) get a new identity on every parent render. The hook keeps the JSON of the last emitted snapshot in a ref and skips `onStateChange` when the new snapshot serialises the same (the snapshot holds only plain data). Without this, an `onStateChange` that stores the state in the parent re-renders the parent, which creates new inline props, which fires `onStateChange` again — an endless loop.
67
+
68
+ ---
69
+
70
+ ## Ref-stabilized callback
71
+
72
+ `onStateChange` is stored in a ref (updated via `useLayoutEffect`) rather than being included directly in the snapshot effect's dependency array:
73
+
74
+ ```ts
75
+ const onStateChangeRef = useRef(onStateChange);
76
+ useLayoutEffect(() => {
77
+ onStateChangeRef.current = onStateChange;
78
+ });
79
+ ```
80
+
81
+ **Why:** Without a ref, if the consumer passes an inline arrow function (`onStateChange={() => {...}}`), the snapshot effect would fire on every render (the function identity changes each time) rather than only when state actually changes. The ref decouples the callback identity from the effect deps.
82
+
83
+ `useLayoutEffect` (not `useEffect`) is used for the ref update so the ref is always current before the snapshot effect reads it, avoiding a one-render lag on the first fire.
84
+
85
+ ---
86
+
87
+ ## 🔗 Related
88
+ - [useGridScrollSync](use-grid-scroll-sync.md)
89
+ - [useGridControlledState](use-grid-controlled-state.md)
90
+ - [useGridColumns](use-grid-columns.md)
@@ -0,0 +1,115 @@
1
+ # `useGridVirtualization`
2
+
3
+ Internal hook. Converts scroll position, viewport dimensions, and the current row/column layout into a render context — the minimal window of rows and columns that need to be in the DOM at any given moment.
4
+
5
+ **File:** `lib/hooks/core/useGridVirtualization.ts`
6
+
7
+ ---
8
+
9
+ ## Purpose
10
+
11
+ Row virtualization is the core performance mechanism: instead of rendering all rows, only the visible slice is rendered. Columns are virtualized too: of the unpinned (centre) columns, only those in the horizontal viewport plus an overscan of **6 columns on each side** are rendered, with spacer columns standing in for the rest. Pinned columns (left and right) are always rendered. This hook encapsulates the binary search and offset math that determines both slices.
12
+
13
+ Row indices in `renderContext` and `cumulativeHeights` cover the **centre (unpinned) rows only**; top- and bottom-pinned rows are laid out separately (`pinnedTopHeight` / `pinnedBottomHeight`).
14
+
15
+ Before extraction, ~120 lines of `useMemo` logic lived inside `DataGrid.tsx`. The hook makes the math independently readable and testable.
16
+
17
+ ---
18
+
19
+ ## Parameters
20
+
21
+ ```ts
22
+ interface UseGridVirtualizationParams<R extends GridRowModel> {
23
+ layout: LayoutResult<R>; // from useLayout: row heights, column widths
24
+ scrollTop: number; // current vertical scroll position in pixels
25
+ scrollLeft: number; // current horizontal scroll position in pixels
26
+ viewportWidth: number;
27
+ viewportHeight: number;
28
+ autoHeight: boolean;
29
+ rowReordering: boolean;
30
+ hasDetailPanel: boolean;
31
+ checkboxSelection: boolean;
32
+ pinCheckboxColumn: boolean;
33
+ pinExpandColumn: boolean;
34
+ /** Adaptive overscan row count from useGridScrollSync. Defaults to 5 when omitted. */
35
+ overscanRows?: number;
36
+ }
37
+ ```
38
+
39
+ > `scrollTop`, `scrollLeft`, and `overscanRows` all come from `useGridScrollSync`, which batches scroll events via RAF and bundles all three into a single state update that fires at most once per animation frame. `overscanRows` there is velocity-based and never below the grid's `overscanRowCount` prop (default 3). Column overscan is fixed at 6 and not configurable.
40
+
41
+ ---
42
+
43
+ ## Returns
44
+
45
+ ```ts
46
+ interface GridVirtualizationResult {
47
+ renderContext: {
48
+ firstRowIndex: number;
49
+ lastRowIndex: number;
50
+ firstColumnIndex: number;
51
+ lastColumnIndex: number;
52
+ };
53
+ offsetTop: number; // translateY for the virtual row block
54
+ offsetLeft: number; // always 0: horizontal offset is carried by spacer columns
55
+ totalHeight: number; // full scrollable height
56
+ pinnedTopHeight: number;
57
+ pinnedBottomHeight: number;
58
+ totalWidth: number;
59
+ rowHeights: number[];
60
+ cumulativeHeights: number[]; // inclusive prefix sums of centre-row heights (from useLayout)
61
+ virtualColumns: (GridColDef<R> & { width: number })[]; // left-pinned, [__spacer_left__], window, [__spacer_right__], right-pinned
62
+ columnMetrics: {
63
+ leftPinnedWidth: number;
64
+ rightPinnedWidth: number;
65
+ unpinnedAccWidths: number[]; // inclusive prefix sums of unpinned column widths
66
+ unpinnedCols: { field: string; width: number }[];
67
+ totalSpecialsWidth: number; // combined width of checkbox / expand / reorder columns
68
+ pinnedSpecialsWidth: number;
69
+ };
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Binary search algorithm
76
+
77
+ The hook uses binary search on the `cumulativeHeights` prefix-sum array to find the first visible row in O(log n) instead of scanning all rows:
78
+
79
+ ```
80
+ cumulativeHeights[i] = sum of heights of centre rows 0..i (inclusive: the bottom edge of row i)
81
+
82
+ firstVisible = first index where cumulativeHeights[i] >= scrollTop
83
+ firstRowIndex = max(0, firstVisible - overscanRows)
84
+ offsetTop = firstRowIndex > 0 ? cumulativeHeights[firstRowIndex - 1] : 0
85
+ lastRowIndex = (first index where cumulativeHeights[i] >= scrollTop + viewportHeight) + overscanRows,
86
+ clamped to the last centre row
87
+ ```
88
+
89
+ Row heights include an expanded detail panel's height, so the prefix sums account for open panels.
90
+
91
+ Columns use the same search on `unpinnedAccWidths` (also inclusive), relative to the left edge of the unpinned area (`scrollLeft - leftPinnedWidth`), widened by 6 columns on each side. The skipped widths become `__spacer_left__` / `__spacer_right__` entries (`isSpacer: true`) in `virtualColumns`.
92
+
93
+ When a column declares `colSpan` / `rowSpan`, `DataGrid` widens this window afterwards (`useGridSpanRowWindow` / `useGridSpanColumnWindow`) so a span whose origin is outside the window is not cut.
94
+
95
+ ---
96
+
97
+ ## `autoHeight` mode
98
+
99
+ When `autoHeight={true}`, the viewport height used for the row window is the sum of all row heights (pinned and centre, plus 50px) rather than the measured container height. Row virtualization is effectively disabled — all rows are in the render window — so the height adjusts to content. Column virtualization still applies. Without `autoHeight`, a viewport that has not been measured yet falls back to 600px high and 1000px wide.
100
+
101
+ ---
102
+
103
+ ## Relationship to DataGrid
104
+
105
+ `DataGrid.tsx` calls this hook and uses:
106
+ - `renderContext` (after the span widening above) to slice the centre rows to the visible subset; `useGridVisibleRows` then merges them with the pinned rows
107
+ - `offsetTop` as the `translateY` value on the virtual row container
108
+ - `totalHeight` and `totalWidth` to set the scroll container's inner dimensions
109
+ - `columnMetrics` to lay out pinned and unpinned column groups
110
+
111
+ ---
112
+
113
+ ## 🔗 Related
114
+ - [useGridRowPipeline](use-grid-row-pipeline.md)
115
+ - [Virtualization](../features/virtualization.md)
@@ -0,0 +1,85 @@
1
+ # `useGridVisibleRows`
2
+
3
+ Internal hook. Merges the pinned top rows, the virtual center window, and the pinned bottom rows into a single `{ row, rowIndex }[]` array — exactly what the viewport JSX iterates to render `<Row>` components.
4
+
5
+ **File:** `lib/hooks/core/useGridVisibleRows.ts`
6
+
7
+ ---
8
+
9
+ ## Purpose
10
+
11
+ The render window is the intersection of what the row pipeline produced and what the virtualization engine says is visible. Before this hook, ~40 lines of `useMemo` logic lived inline in `DataGrid.tsx`. Extracting it makes the merge strategy explicit and independently testable.
12
+
13
+ ---
14
+
15
+ ## Parameters
16
+
17
+ ```ts
18
+ interface UseGridVisibleRowsParams<R extends GridRowModel> {
19
+ renderContext: {
20
+ firstRowIndex: number; // from useGridVirtualization
21
+ lastRowIndex: number;
22
+ };
23
+ pinnedTopRows: R[]; // from useGridRowPipeline
24
+ pinnedBottomRows: R[]; // from useGridRowPipeline
25
+ paginatedUnpinnedRows: R[]; // from useGridRowPipeline
26
+ sortedUnpinnedRows: R[]; // from useGridRowPipeline
27
+ pagination: boolean;
28
+ }
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Returns
34
+
35
+ ```ts
36
+ GridVisibleRow<R>[]
37
+ // where GridVisibleRow<R> = { row: R; rowIndex: number }
38
+ ```
39
+
40
+ `rowIndex` is the position in the renderable rows (top-pinned + the current page's centre rows + bottom-pinned) — it becomes the row's `data-rowindex`, the `rowIndex` in `onRowClick` / `getDetailPanelContent` params and the zebra-stripe class. `aria-rowindex` is computed separately (`lib/utils/aria`, `getAriaRowLayout`): it adds the header rows and, for centre rows, the rows on earlier pages, and puts bottom-pinned rows after every page. `GridPinnedRows` receives the bottom rows' offset (`topPinned + centerCount`) so they no longer restart at 0.
41
+
42
+ ---
43
+
44
+ ## Merge strategy
45
+
46
+ ```
47
+ topPinned: rows 0 .. pinnedTopRows.length - 1 (always fully rendered)
48
+ center: rows [firstRowIndex - topPinnedCount, lastRowIndex - topPinnedCount]
49
+ (clipped to the virtual window)
50
+ bottomPinned: rows (topPinned + centerCount) .. end (always fully rendered)
51
+ ```
52
+
53
+ Pinned rows are NOT subject to virtualization — they are always in the DOM regardless of scroll position.
54
+
55
+ ---
56
+
57
+ ## Deduplication guard
58
+
59
+ After merging, the hook filters out any row whose `id` has already been seen:
60
+
61
+ ```ts
62
+ const seenIds = new Set<GridRowId>();
63
+ return combined.filter(item => {
64
+ if (seenIds.has(item.row.id)) return false;
65
+ seenIds.add(item.row.id);
66
+ return true;
67
+ });
68
+ ```
69
+
70
+ **Why:** If a user passes the same row in both `rows` and `pinnedRows`, it would appear in both `pinnedTopRows` and `paginatedUnpinnedRows`. Two `<Row key={id}>` elements with the same key cause React reconciliation bugs. The guard ensures each ID appears at most once — the pinned version takes precedence since it comes first in the merged array.
71
+
72
+ Entries from the pinned sections carry `pinned: 'top' | 'bottom'`. `GridVirtualRows` renders only the untagged (center) entries, in one linear pass; it does not de-duplicate again and does not consult the raw `pinnedRows` prop, so under tree data / row grouping (where nothing is pinned) every row still renders in its place.
73
+
74
+ ---
75
+
76
+ ## Relationship to DataGrid
77
+
78
+ `DataGrid.tsx` calls this hook after `useGridVirtualization` produces the render context and after `useGridRowPipeline` produces the pinned/paginated row groups. The returned `visibleRows` array is iterated directly in the viewport JSX.
79
+
80
+ ---
81
+
82
+ ## 🔗 Related
83
+ - [useGridRowPipeline](use-grid-row-pipeline.md)
84
+ - [useGridVirtualization](use-grid-virtualization.md)
85
+ - [useGridColumns](use-grid-columns.md)
Binary file
Binary file
Binary file
Binary file
@@ -6,24 +6,29 @@ Internal component that renders the sticky aggregation totals row pinned to the
6
6
 
7
7
  | Prop | Type | Description |
8
8
  | :--- | :--- | :--- |
9
- | `columns` | `GridColDef[]` | Ordered list of visible column definitions used to lay out cells. |
9
+ | `columns` | `GridColDef[]` | The columns exactly as the rows lay them out (`virtualization.virtualColumns`): visible columns only, left-pinned first and right-pinned last, with the horizontal-virtualization spacer entries. |
10
10
  | `aggregationModel` | `GridAggregationModel` | Maps field names to function names (`'sum' \| 'avg' \| 'count' \| 'min' \| 'max' \| 'unique'`). |
11
11
  | `aggregationResult` | `GridAggregationResult` | Computed values keyed by field name, produced by `useAggregation`. |
12
- | `columnWidths` | `Record<string, number>` | Runtime pixel widths for each field, kept in sync with user resize actions. |
12
+ | `columnWidths` | `Record<string, number>` | User resize overrides, as passed to the rows; otherwise each column's layout width is used. |
13
13
  | `rowHeight` | `number` | Sets `min-height` on the footer row, matching the grid's configured row height. |
14
- | `checkboxSelection` | `boolean` | When `true`, inserts a 48px spacer at the start to align with the checkbox column. |
15
- | `hasDetailPanel` | `boolean` | When `true`, inserts a 48px spacer to align with the expand/collapse column. |
16
- | `rowReordering` | `boolean` | When `true`, inserts a 48px spacer to align with the drag handle column. |
14
+ | `checkboxSelection` | `boolean` | When `true`, inserts a 48px spacer aligned with the checkbox column. |
15
+ | `hasDetailPanel` | `boolean` | When `true`, inserts a 48px spacer aligned with the expand/collapse column. |
16
+ | `rowReordering` | `boolean` | When `true`, inserts a 48px spacer aligned with the drag handle column (always sticky, like the row's handle). |
17
+ | `pinCheckboxColumn` | `boolean` | Default `true`. Whether the checkbox spacer sticks to the left, matching the row's checkbox column; also used for the pinned-column offsets. |
18
+ | `pinExpandColumn` | `boolean` | Default `true`. The same for the expand/collapse spacer. |
19
+ | `pinnedColumns` | `GridColumnPinning` | Pinned columns get sticky totals at the same offsets as the pinned body cells. |
20
+ | `loading` | `boolean` | A server aggregation request is pending: sets `aria-busy="true"` and the class `ogx__aggregation-footer--loading`. |
17
21
 
18
22
  ## 🔄 When it renders
19
23
 
20
- `GridAggregationFooter` renders when `aggregationModel` has at least one entry and `aggregationResult` contains the corresponding computed values. The footer is sticky — it remains visible when the user scrolls vertically.
24
+ `GridAggregationFooter` renders when `aggregationModel` has at least one entry, except in pivot mode (the pivot's Grand Total row is the aggregate there). The footer is sticky — it remains visible when the user scrolls vertically. A column without an entry in `aggregationModel` gets an empty cell, so every total sits under its header.
21
25
 
22
26
  ## 🔢 Value formatting
23
27
 
24
- Values are formatted by `formatAggregationValue` from `useAggregation`:
25
- - `count` → integer (e.g. `42`)
26
- - `sum` / `avg` / `min` / `max` → `toLocaleString()` with up to 2 decimal places
28
+ Values are formatted by `formatAggregateForColumn` (`lib/utils/aggregation`), the same formatting used by group rows, pivot cells and exports:
29
+ - `sum` / `avg` / `min` / `max` go through the column's `valueFormatter`, with the footer totals (`aggregationResult`) as its `row`. If the formatter throws, the default format is used.
30
+ - `count` / `unique` are plain counts (the column's `valueFormatter` is not applied).
31
+ - Without a formatter: `toLocaleString()` (`avg` to at most 2 decimal places); dates as locale dates; a missing value shows `—`.
27
32
 
28
33
  ## 🎨 Usage via DataGrid prop
29
34
 
@@ -41,7 +46,7 @@ The footer renders automatically. No additional configuration required.
41
46
 
42
47
  ## ♿ Accessibility
43
48
 
44
- The footer row uses `role="row"` and `aria-label="Aggregation totals"`. Each cell uses `role="gridcell"`. The container uses `aria-live="polite"` so screen readers announce value updates when the data changes.
49
+ The footer row uses `role="row"` and `aria-label="Aggregation totals"`. Each cell uses `role="gridcell"`. The container uses `aria-live="polite"` so screen readers announce value updates when the data changes, and `aria-busy="true"` while server totals are loading. The system-column spacers are `aria-hidden`.
45
50
 
46
51
  ## 🔗 Related
47
52
  - [DataGrid](datagrid.md)
@@ -4,18 +4,25 @@ The atomic unit of the grid. Handles rendering, formatting, editing, and selecti
4
4
 
5
5
  ## ⚙️ Props
6
6
 
7
+ `Cell` and its props type `CellProps` are exported. The main props:
8
+
7
9
  | Prop | Type | Description |
8
10
  | :--- | :--- | :--- |
9
- | `value` | `any` | The raw value to display. |
11
+ | `value` | `unknown` | The cell value (after `valueGetter`). |
10
12
  | `row` | `GridRowModel` | The full row data object. |
11
13
  | `colDef` | `GridColDef` | Configuration for the column. |
12
14
  | `rowIndex` | `number` | 0-indexed position in the current view. |
15
+ | `colIndex` | `number` | 0-indexed position of the column among the visible data columns in render order (system columns excluded). Passed to `renderCell`, `cellClassName`, `onCellClick` and `isCellEditable`. |
16
+ | `ariaColIndex` | `number` | 1-based `aria-colindex`, system columns included. Defaults to `colIndex + 1`. |
13
17
  | `isSelected` | `boolean` | Whether the parent row is selected. |
14
- | `isFocused` | `boolean` | Whether the cell has keyboard focus. |
15
- | `isEditable` | `boolean` | Enables inline editing for this cell. |
16
- | `isEditing` | `boolean` | Whether the cell is currently in edit mode. |
18
+ | `isFocused` | `boolean` | Whether the cell is the grid's focus position (drives `.ogx__cell--focused`). Since v3.0 the cell does not move DOM focus onto itself; the grid does, and only while it owns focus. |
19
+ | `isEditable` | `boolean` | Whether this cell may enter edit mode. `<Row />` resolves it per cell from `colDef.editable`, the row (synthetic group rows are never editable) and `isCellEditable`; it also drives `aria-readonly`. |
20
+ | `isEditing` | `boolean` | Whether the cell is currently in edit mode. While editing, clicks and double-clicks inside the cell belong to the editor and are not handled as cell clicks. |
21
+ | `onEditStop` | `(cancel?: boolean, field?: string) => void` | Ends this cell's edit (commit, or discard when `cancel` is true). The cell passes its own `field` so a late call cannot end another cell's edit. Also called (commit) when the cell unmounts while editing. |
17
22
  | `width` | `number` | Calculated width including resizing and flex. |
18
23
  | `pinnedPosition` | `'left' \| 'right' \| null` | Sticky positioning state. |
24
+ | `rowMeta` | `GridRowMeta` | Hierarchy metadata, passed to `renderCell` as `params.rowMeta`. |
25
+ | `onCellClick` | `(params: GridCellParams) => void` | Click handler (not called for clicks inside an open editor). |
19
26
 
20
27
  ## 🧩 Modifiers & Classes
21
28
 
@@ -25,6 +32,7 @@ The `<Cell />` component applies various BEM classes for styling:
25
32
  - `.ogx__cell--editing`: During active cell editing.
26
33
  - `.ogx__cell--pinned-left`: Sticky to the left.
27
34
  - `.ogx__cell--pinned-right`: Sticky to the right.
35
+ - `.ogx__cell-image`: The default `<img>` of a `type: 'image'` column without `renderCell`.
28
36
 
29
37
  ## 🛠️ Custom Rendering
30
38
 
@@ -45,9 +53,11 @@ const columns: GridColDef[] = [
45
53
 
46
54
  ## 🛡️ Error containment
47
55
 
48
- Custom `renderCell` callbacks are wrapped in a `CellErrorBoundary`. If a renderCell throws during render, that single cell shows a `⚠` indicator (CSS class `ogx__cell-error`) with the error message as a tooltip. The rest of the grid continues rendering normally.
56
+ Custom `renderCell` and `renderEditCell` callbacks are wrapped in a `CellErrorBoundary`. If a renderCell throws during render, that single cell shows a `⚠` indicator (CSS class `ogx__cell-error`, `role="img"` with an `aria-label`) with the error message as a tooltip. The rest of the grid continues rendering normally.
57
+
58
+ The cell renders again (and the indicator clears if the renderer now succeeds) when the row object, the column's `renderCell` function or the cell value changes, so passing fixed columns recovers without replacing every row (v3.0).
49
59
 
50
- Default cell display (no `renderCell`) does not go through the boundary — it cannot throw.
60
+ A column's `valueGetter` or `valueFormatter` that throws for a row is contained the same way: that cell shows the `⚠` indicator and the rest of the grid renders. On synthetic rows (row-grouping group and subtotal rows, auto-created tree parents) `valueGetter` is not called (the row already holds its grouping value and aggregates under the column's field), and `valueFormatter` is only called for the values the row holds, not for its empty cells.
51
61
 
52
62
  To style the error indicator:
53
63
 
@@ -1,50 +1,77 @@
1
- # `<ColumnGroupHeader />`
1
+ # Column Group Headers
2
2
 
3
- The `ColumnGroupHeader` component handles the rendering of nested header groups in multi-level column layouts. It allows for advanced data organization where multiple columns are grouped under a single, semantic parent.
4
-
5
- ## 📑 Overview
6
- - **Hierarchical Layout**: Supports an unlimited depth of nested groupings (Year → Quarter → Metric).
7
- - **Sticky Behavior**: Automatically stays within the grid's header section during horizontal scroll.
8
- - **Dynamic Calculation**: Positions itself based on the combined width of its child columns.
9
- - **Reflective Styles**: Inherits header-level styling while providing specialized group-level CSS hooks.
10
-
11
- ---
3
+ Column group headers are spanning header cells above the regular column headers, so related columns can sit under a shared label (Year → Quarter → Metric). They are rendered by `<Header />` from the `columnGroupingModel` prop on `<DataGrid />`; there is no separate component to import or replace (the old internal `ColumnGroupHeader` component was removed in v3.0).
12
4
 
13
5
  ## 🛠️ Usage
14
6
 
15
- ### Define Multi-Level Grouping
16
- In your columns definition, include nested `columnGroups`.
7
+ Pass a `columnGroupingModel`. Each group lists column `field`s, or nested groups, in `children`:
17
8
 
18
9
  ```tsx
19
- const columns = [
20
- { field: 'id', headerName: 'ID', width: 100 },
21
- { field: 'q1', headerName: 'Q1 Sales', width: 120 },
22
- { field: 'q2', headerName: 'Q2 Sales', width: 120 },
10
+ import { DataGrid } from '@opencorestack/opengridx';
11
+ import type { GridColDef, GridColumnGroupingModel } from '@opencorestack/opengridx';
12
+
13
+ const columns: GridColDef[] = [
14
+ { field: 'name', headerName: 'Name', width: 160 },
15
+ { field: 'q1Sales', headerName: 'Sales', type: 'number' },
16
+ { field: 'q1Target', headerName: 'Target', type: 'number' },
17
+ { field: 'q2Sales', headerName: 'Sales', type: 'number' },
18
+ { field: 'q2Target', headerName: 'Target', type: 'number' },
19
+ ];
20
+
21
+ const columnGroupingModel: GridColumnGroupingModel = [
22
+ {
23
+ groupId: '2024',
24
+ headerName: '2024 Performance',
25
+ children: [
26
+ { groupId: 'q1', headerName: 'Q1', children: ['q1Sales', 'q1Target'] },
27
+ { groupId: 'q2', headerName: 'Q2', children: ['q2Sales', 'q2Target'], headerClassName: 'my-q2-header' },
28
+ ],
29
+ },
23
30
  ];
24
31
 
25
- // Define your mapping logic in your component or use the columns API:
26
- // ColumnGroupHeader is typically used by the DataGrid automatically when
27
- // nested structures are detected.
32
+ <DataGrid rows={rows} columns={columns} columnGroupingModel={columnGroupingModel} height={500} />
28
33
  ```
29
34
 
30
- ### Direct Customization
31
- While the grid uses this component internally, you can customize its appearance via specific CSS classes or by replacing it in the `slots` API.
35
+ This renders two group rows (`2024 Performance`, then `Q1` / `Q2`) above the column header row: one group row per nesting level. `name` is not in any group, so it gets an empty filler cell in both group rows.
36
+
37
+ See [`GridColumnGroup`](../API_REFERENCE.md#️-column-group-headers) for the type.
32
38
 
33
39
  ---
34
40
 
35
- ## ⚙️ How it Works
41
+ ## ⚙️ How it works
36
42
 
37
- 1. **Hierarchy Resolution**: The component takes a structure and determines which headers need to be "merged" into groups.
38
- 2. **Width Aggregation**: Each group header calculates its own total width by summing the widths of all its children (including other nested groups).
39
- 3. **Alignment**: Ensuring that the group labels are centered over their respective child columns for a clean visual hierarchy.
43
+ - **Order, visibility and pinning**: a group cell covers its visible member columns in their rendered order. When the members are not adjacent (because of reordering, hiding or a pinned-section boundary), the group renders one labelled cell per run of adjacent members. Group cells over pinned columns are sticky, so they stay above their columns on horizontal scroll.
44
+ - **Widths**: a group cell is exactly as wide as its member columns, including flex, `auto` and percentage widths and manual resizes.
45
+ - **Reordering**: header drag and the toolbar / Columns-panel reorder keep groups intact: a column can only be moved onto a column of the same innermost group, and an ungrouped column only onto another ungrouped column.
46
+ - **Accessibility**: group rows are `role="row"` with `aria-rowindex`; group cells are `role="columnheader"` with `aria-colspan` (visible member columns) and `aria-colindex`; filler cells are `aria-hidden`. The grid's `aria-rowcount` includes the group rows.
40
47
 
41
48
  ---
42
49
 
43
- ## 🎨 Customization
50
+ ## 🎨 Styling
51
+
52
+ Group headers cannot be replaced through the `slots` API. Style them with CSS:
53
+
54
+ | Class | Element |
55
+ | :--- | :--- |
56
+ | `.ogx-col-group-row` | One group header row (one per nesting level). |
57
+ | `.ogx-col-group-cell` | Every cell in a group row. |
58
+ | `.ogx-col-group-cell--group` | A cell that shows a group label. |
59
+ | `.ogx-col-group-cell--filler` | An empty cell over columns that are not in a group at that level. |
60
+ | `.ogx-col-group-cell--pinned` | A cell over pinned columns (sticky), with `--pinned-left` or `--pinned-right`. |
61
+ | `.ogx-col-group-cell__label` | The label text inside a group cell. |
44
62
 
45
- Column group headers are rendered internally; they cannot be replaced via the slots API. Style them through CSS targeting `.ogx__column-group-header`, or use the `columnGroupingModel` to control grouping structure.
63
+ A group's `headerClassName` is added to its group cells, next to `ogx-col-group-cell` and `ogx-col-group-cell--group`:
64
+
65
+ ```css
66
+ .ogx-col-group-cell--group {
67
+ font-weight: 600;
68
+ }
69
+ .my-q2-header {
70
+ background: #eef2ff;
71
+ }
72
+ ```
46
73
 
47
74
  ## 📝 Best Practices
48
75
  - **Logical Labels**: Use clear, concise labels for groups like `2024 Performance` or `Contact Info`.
49
76
  - **Type-Based Grouping**: Align related data types (e.g., all currency fields under a `Finance` group) for better user comprehension.
50
- - **Limit Depth**: While technically unlimited, more than 3-4 levels of nesting can become difficult for users to follow on small screens.
77
+ - **Limit Depth**: More than 3-4 levels of nesting becomes hard to follow, especially on small screens.