@opencorestack/opengridx 2.1.0 → 3.0.1

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 (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  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 -166
  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 +6 -7
  29. package/docs/components/datagrid.md +15 -13
  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 +90 -38
  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 +126 -39
  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 +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -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 +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
  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 +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
package/lib/index.ts CHANGED
@@ -1,21 +1,31 @@
1
1
 
2
- // Auto-import styles — this ensures consumers do NOT need a manual CSS import
2
+ // Pulls the stylesheet into the library build. Vite library mode extracts it to dist/opengridx.css
3
+ // and drops this import from the JS bundle, so consumers must import '@opencorestack/opengridx/styles'.
3
4
  import './styles/opengridx.css';
4
5
 
5
6
  export { DataGrid } from './components/DataGrid/DataGrid';
6
7
  export { Cell } from './components/Cell/Cell';
8
+ export type { CellProps } from './components/Cell/Cell';
7
9
  export { Row } from './components/Row/Row';
10
+ export type { RowProps } from './components/Row/Row';
8
11
  export { Header } from './components/Header/Header';
12
+ export type { HeaderProps } from './components/Header/Header';
9
13
  export { Skeleton } from './components/Skeleton/Skeleton';
14
+ export type { SkeletonProps } from './components/Skeleton/Skeleton';
10
15
  export { FilterPanel } from './components/FilterPanel';
16
+ export type { FilterPanelProps } from './components/FilterPanel';
17
+ export { Pagination } from './components/Pagination/Pagination';
18
+ export type { PaginationProps } from './components/Pagination/Pagination';
11
19
  export { GridToolbar } from './components/Toolbar/GridToolbar';
12
20
  export type { GridToolbarProps, ToolbarButtonRenderProps, ToolbarQuickFilterRenderProps } from './components/Toolbar/GridToolbar';
13
21
  export { GridTooltip } from './components/Tooltip/Tooltip';
22
+ export type { GridTooltipProps } from './components/Tooltip/Tooltip';
14
23
  export { ColumnVisibilityPanel } from './components/ColumnVisibilityPanel/ColumnVisibilityPanel';
15
24
  export type { ColumnVisibilityPanelProps } from './components/ColumnVisibilityPanel/ColumnVisibilityPanel';
16
25
 
17
26
  export { useGridApiRef } from './hooks/core/useGridApiRef';
18
27
  export { Button, Input, Checkbox } from './components/ui';
28
+ export type { ButtonProps, InputProps, CheckboxProps } from './components/ui';
19
29
 
20
30
  export {
21
31
  exportToCsv,
@@ -38,7 +48,7 @@ export { usePivot } from './hooks/features/usePivot';
38
48
  export type { UsePivotReturn } from './hooks/features/usePivot';
39
49
 
40
50
  export { DataGridThemeProvider } from './theme';
41
- export type { DataGridThemeProviderProps, GridTheme, GridThemeColors, GridThemeTypography, GridThemeSpacing, GridThemeBorders, GridThemeShadows, GridThemeGrid, GridThemeTransitions } from './theme';
51
+ export type { DataGridThemeProviderProps, GridTheme, GridThemeColors, GridThemeTypography, GridThemeSpacing, GridThemeBorders, GridThemeShadows, GridThemeGrid, GridThemeTransitions, GridThemeToolbar, GridThemeOverlays, GridThemeScrollbar, GridThemeSkeleton, GridThemeGrayScale } from './theme';
42
52
  export { darkTheme, roseTheme, emeraldTheme, amberTheme, compactTheme } from './theme';
43
53
 
44
54
  export { useGridStateStorage } from './state';
@@ -50,6 +60,7 @@ export type {
50
60
  GridPaginationState,
51
61
  GridColumnsState,
52
62
  GridDensityState,
63
+ GridDataSourceState,
53
64
  UseGridStateStorageOptions,
54
65
  UseGridStateStorageReturn,
55
66
  } from './state';
@@ -75,8 +86,10 @@ export type {
75
86
  GridFilterGroup,
76
87
  GridPaginationModel,
77
88
  GridRenderCellParams,
89
+ GridRenderEditCellParams,
78
90
  GridRenderHeaderParams,
79
91
  GridValueGetterParams,
92
+ GridValueSetterParams,
80
93
  GridValueFormatterParams,
81
94
  GridRowParams,
82
95
  GridCellParams,
@@ -85,11 +98,14 @@ export type {
85
98
  GridPinnedPosition,
86
99
  GridColumnVisibilityModel,
87
100
  GridRowSelectionModel,
101
+ GridColumnOrder,
88
102
  GridColumnOrderChangeParams,
89
103
  GridRowOrderChangeParams,
104
+ GridRowScrollEndParams,
90
105
  GridRowGroupingModel,
91
106
  GridAggregationModel,
92
107
  GridAggregationResult,
108
+ GridAggregationPosition,
93
109
  DataGridProps,
94
110
  GridDataSource,
95
111
  GridGetRowsParams,
@@ -105,7 +121,17 @@ export type {
105
121
 
106
122
  GridApi,
107
123
  GridTreeNode,
124
+ GridGroupedExportRow,
108
125
  GridDetailPanelParams,
126
+ GridDetailPanelHeight,
127
+ GridSlots,
128
+ GridSlotProps,
129
+ GridToolbarSlotProps,
130
+ GridPaginationSlotProps,
131
+ GridOverlaySlotProps,
132
+ GridFooterSlotProps,
133
+ GridValidRowModel,
134
+ DataGridUntypedColumnsProps,
109
135
  GridRowMeta,
110
136
  GridLocaleText,
111
137
  PdfExportOptions,
@@ -6,6 +6,7 @@ export type {
6
6
  GridPaginationState,
7
7
  GridColumnsState,
8
8
  GridDensityState,
9
+ GridDataSourceState,
9
10
  } from './types';
10
11
 
11
12
  export {
@@ -45,4 +45,10 @@ export interface GridState {
45
45
  dataSource?: GridDataSourceState;
46
46
  }
47
47
 
48
- export type GridInitialState = GridState;
48
+ /**
49
+ * The state the grid starts from. Every part is optional, including each column field, so a
50
+ * partial state such as `{ columns: { columnVisibilityModel: { age: false } } }` is valid.
51
+ */
52
+ export interface GridInitialState extends Omit<GridState, 'columns'> {
53
+ columns?: Partial<GridColumnsState>;
54
+ }
@@ -1,8 +1,12 @@
1
1
 
2
- import { useRef, useCallback, useMemo, useEffect } from 'react';
2
+ import { useRef, useCallback, useMemo, useEffect, useLayoutEffect, useState } from 'react';
3
3
  import type { GridState } from './types';
4
4
 
5
5
  export interface UseGridStateStorageOptions {
6
+ /**
7
+ * Storage key. The grid reads `initialState` only when it mounts, so when the key can
8
+ * change (per user, per view) remount the grid with it: `<DataGrid key={key} … />`.
9
+ */
6
10
  key: string;
7
11
  debounceMs?: number;
8
12
  include?: (keyof GridState)[];
@@ -19,12 +23,27 @@ export interface UseGridStateStorageReturn {
19
23
  clearState: () => void;
20
24
  }
21
25
 
26
+ type GridStateStorage = NonNullable<UseGridStateStorageOptions['storage']>;
27
+
28
+ /**
29
+ * Reading `window.localStorage` itself throws a SecurityError when site data is blocked
30
+ * (cookie blocking, sandboxed or partitioned iframes). Treat that as "no persistence".
31
+ */
32
+ function resolveDefaultStorage(): GridStateStorage | undefined {
33
+ if (typeof window === 'undefined') return undefined;
34
+ try {
35
+ return window.localStorage;
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
22
41
  function readFromStorage(
23
42
  key: string,
24
- storage: UseGridStateStorageOptions['storage']
43
+ storage: GridStateStorage
25
44
  ): GridState | undefined {
26
45
  try {
27
- const raw = storage!.getItem(key);
46
+ const raw = storage.getItem(key);
28
47
  if (!raw) return undefined;
29
48
  return JSON.parse(raw) as GridState;
30
49
  } catch {
@@ -37,7 +56,7 @@ function writeToStorage(
37
56
  key: string,
38
57
  state: GridState,
39
58
  include: (keyof GridState)[] | undefined,
40
- storage: UseGridStateStorageOptions['storage']
59
+ storage: GridStateStorage
41
60
  ) {
42
61
  try {
43
62
  const toWrite = include
@@ -45,12 +64,17 @@ function writeToStorage(
45
64
  Object.entries(state).filter(([k]) => include.includes(k as keyof GridState))
46
65
  )
47
66
  : state;
48
- storage!.setItem(key, JSON.stringify(toWrite));
67
+ storage.setItem(key, JSON.stringify(toWrite));
49
68
  } catch {
50
69
  // swallow localStorage write errors (private browsing, quota exceeded)
51
70
  }
52
71
  }
53
72
 
73
+ interface PendingWrite {
74
+ key: string;
75
+ state: GridState;
76
+ }
77
+
54
78
  export function useGridStateStorage(
55
79
  options: UseGridStateStorageOptions | string
56
80
  ): UseGridStateStorageReturn {
@@ -63,27 +87,55 @@ export function useGridStateStorage(
63
87
  key,
64
88
  debounceMs = 300,
65
89
  include,
66
- storage = typeof window !== 'undefined' ? window.localStorage : undefined,
90
+ storage: storageOption,
67
91
  } = opts;
68
92
 
69
- const initialState = useMemo(() => {
70
- if (!storage) return undefined;
71
- return readFromStorage(key, storage);
72
-
73
- }, [key, storage]);
93
+ // Resolved once so its identity is stable across renders.
94
+ const [defaultStorage] = useState(resolveDefaultStorage);
95
+ const storage = storageOption ?? defaultStorage;
96
+
97
+ // Latest-value refs: `include` and `storage` are often written inline, so they must not
98
+ // drive callback identities (that used to flush storage on every re-render).
99
+ const includeRef = useRef(include);
100
+ const storageRef = useRef(storage);
101
+ useLayoutEffect(() => {
102
+ includeRef.current = include;
103
+ storageRef.current = storage;
104
+ });
105
+
106
+ // Read once per key. A new inline `storage` object on each render must not re-parse storage.
107
+ const [loaded, setLoaded] = useState<{ key: string; state: GridState | undefined }>(() => ({
108
+ key,
109
+ state: storage ? readFromStorage(key, storage) : undefined,
110
+ }));
111
+ let initialState = loaded.state;
112
+ if (loaded.key !== key) {
113
+ initialState = storage ? readFromStorage(key, storage) : undefined;
114
+ setLoaded({ key, state: initialState });
115
+ }
74
116
 
75
117
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
76
- const latestStateRef = useRef<GridState | null>(null);
118
+ // The pending write remembers the key it was made for, so a debounced write never lands in
119
+ // a different key after the key changes.
120
+ const pendingRef = useRef<PendingWrite | null>(null);
77
121
 
78
122
  const flush = useCallback(() => {
79
- if (latestStateRef.current && storage) {
80
- writeToStorage(key, latestStateRef.current, include, storage);
123
+ if (timerRef.current) {
124
+ clearTimeout(timerRef.current);
125
+ timerRef.current = null;
126
+ }
127
+ const pending = pendingRef.current;
128
+ pendingRef.current = null;
129
+ const target = storageRef.current;
130
+ if (pending && target) {
131
+ writeToStorage(pending.key, pending.state, includeRef.current, target);
81
132
  }
82
- }, [key, include, storage]);
133
+ }, []);
83
134
 
84
135
  const onStateChange = useCallback(
85
136
  (state: GridState) => {
86
- latestStateRef.current = state;
137
+ if (pendingRef.current && pendingRef.current.key !== key) flush();
138
+ pendingRef.current = { key, state };
87
139
 
88
140
  if (timerRef.current) {
89
141
  clearTimeout(timerRef.current);
@@ -91,23 +143,30 @@ export function useGridStateStorage(
91
143
 
92
144
  timerRef.current = setTimeout(flush, debounceMs);
93
145
  },
94
- [flush, debounceMs]
146
+ [key, flush, debounceMs]
95
147
  );
96
148
 
97
- useEffect(() => {
98
- return () => {
99
- if (timerRef.current) {
100
- clearTimeout(timerRef.current);
101
- flush();
102
- }
103
- };
104
- }, [flush]);
149
+ // Flush a pending write on unmount only; nothing is written when no write is pending.
150
+ useEffect(() => flush, [flush]);
105
151
 
106
152
  const clearState = useCallback(() => {
107
- if (storage) {
108
- storage.removeItem(key);
153
+ if (timerRef.current) {
154
+ clearTimeout(timerRef.current);
155
+ timerRef.current = null;
156
+ }
157
+ pendingRef.current = null;
158
+ const target = storageRef.current;
159
+ if (target) {
160
+ try {
161
+ target.removeItem(key);
162
+ } catch {
163
+ // storage became unavailable — nothing to clear
164
+ }
109
165
  }
110
- }, [key, storage]);
166
+ }, [key]);
111
167
 
112
- return { initialState, onStateChange, clearState };
168
+ return useMemo(
169
+ () => ({ initialState, onStateChange, clearState }),
170
+ [initialState, onStateChange, clearState]
171
+ );
113
172
  }
@@ -1,8 +1,42 @@
1
1
  import React from 'react';
2
2
  import type { GridTheme } from './types';
3
+ import { GridThemeContext } from './gridThemeContext';
3
4
 
4
- const lightBase: Record<string, string> = {
5
+ // Tokens that follow the primary colour. They are written as var() / color-mix() of the primary
6
+ // tokens, so a theme that only sets colors.primary* recolours the toolbar, menus, selection and
7
+ // focus ring too; a theme can still override any of them directly.
8
+ const primaryDerived: Record<string, string> = {
9
+ '--ogx-grid-row-selected-background': 'var(--ogx-color-primary-light)',
10
+ '--ogx-grid-row-selected-hover-background': 'color-mix(in srgb, var(--ogx-color-primary) 14%, var(--ogx-color-primary-light))',
11
+ '--ogx-grid-cell-focus-border': 'var(--ogx-color-primary)',
12
+ '--ogx-toolbar-btn-primary-bg': 'var(--ogx-color-primary)',
13
+ '--ogx-toolbar-btn-primary-hover': 'var(--ogx-color-primary-dark)',
14
+ '--ogx-toolbar-input-focus-border': 'var(--ogx-color-primary)',
15
+ '--ogx-toolbar-input-focus-shadow': '0 0 0 3px color-mix(in srgb, var(--ogx-color-primary) 12%, transparent)',
16
+ '--ogx-toolbar-chip-active-bg': 'color-mix(in srgb, var(--ogx-color-primary) 12%, transparent)',
17
+ '--ogx-toolbar-chip-active-text': 'var(--ogx-color-primary-dark)',
18
+ '--ogx-overlay-item-selected-bg': 'color-mix(in srgb, var(--ogx-color-primary) 8%, transparent)',
19
+ '--ogx-overlay-item-selected-text': 'var(--ogx-color-primary-dark)',
20
+ };
5
21
 
22
+ // Tokens that follow the grid surface colours, so a theme that sets grid.* restyles the toolbar
23
+ // to match unless it sets toolbar.* itself.
24
+ const surfaceDerived: Record<string, string> = {
25
+ '--ogx-toolbar-background': 'var(--ogx-grid-header-background)',
26
+ '--ogx-toolbar-text': 'var(--ogx-grid-header-text)',
27
+ '--ogx-toolbar-border': 'var(--ogx-grid-border-color)',
28
+ '--ogx-toolbar-btn-bg': 'transparent',
29
+ '--ogx-toolbar-btn-hover': 'var(--ogx-grid-header-hover-background)',
30
+ '--ogx-toolbar-btn-text': 'var(--ogx-grid-header-text)',
31
+ '--ogx-toolbar-input-bg': 'var(--ogx-grid-background)',
32
+ '--ogx-toolbar-input-text': 'var(--ogx-grid-row-text)',
33
+ '--ogx-toolbar-chip-bg': 'transparent',
34
+ '--ogx-toolbar-chip-text': 'var(--ogx-grid-row-text)',
35
+ };
36
+
37
+ // The complete light palette. The provider pins every colour token, so a light theme stays light
38
+ // (and readable) when the operating system is in dark mode, where :root switches to dark values.
39
+ const lightBase: Record<string, string> = {
6
40
  '--ogx-color-primary': '#3b82f6',
7
41
  '--ogx-color-primary-dark': '#2563eb',
8
42
  '--ogx-color-primary-light': '#eff6ff',
@@ -14,6 +48,16 @@ const lightBase: Record<string, string> = {
14
48
 
15
49
  '--ogx-color-white': '#ffffff',
16
50
  '--ogx-color-black': '#0f172a',
51
+ '--ogx-color-gray-50': '#f8fafc',
52
+ '--ogx-color-gray-100': '#f1f5f9',
53
+ '--ogx-color-gray-200': '#e2e8f0',
54
+ '--ogx-color-gray-300': '#cbd5e1',
55
+ '--ogx-color-gray-400': '#94a3b8',
56
+ '--ogx-color-gray-500': '#64748b',
57
+ '--ogx-color-gray-600': '#475569',
58
+ '--ogx-color-gray-700': '#334155',
59
+ '--ogx-color-gray-800': '#1e293b',
60
+ '--ogx-color-gray-900': '#0f172a',
17
61
 
18
62
  '--ogx-border-color': '#e2e8f0',
19
63
  '--ogx-border-color-hover': '#cbd5e1',
@@ -31,9 +75,6 @@ const lightBase: Record<string, string> = {
31
75
  '--ogx-grid-row-text': '#1e293b',
32
76
  '--ogx-grid-row-hover-background': '#f8fafc',
33
77
  '--ogx-grid-row-alternate-background': 'transparent',
34
- '--ogx-grid-row-selected-background': '#eff6ff',
35
- '--ogx-grid-row-selected-hover-background': '#dbeafe',
36
- '--ogx-grid-cell-focus-border': '#3b82f6',
37
78
 
38
79
  '--ogx-checkbox-bg': '#ffffff',
39
80
  '--ogx-checkbox-border': '#94a3b8',
@@ -42,27 +83,12 @@ const lightBase: Record<string, string> = {
42
83
  '--ogx-grid-pinned-right-shadow': '-4px 0 24px -4px rgba(0, 0, 0, 0.05)',
43
84
 
44
85
  // Toolbars
45
- '--ogx-toolbar-background': 'transparent',
46
- '--ogx-toolbar-text': '#1e293b',
47
- '--ogx-toolbar-border': '#e2e8f0',
48
- '--ogx-toolbar-btn-bg': 'transparent',
49
- '--ogx-toolbar-btn-hover': '#f1f5f9',
50
- '--ogx-toolbar-btn-text': '#334155',
51
- '--ogx-toolbar-btn-primary-bg': '#3b82f6',
52
- '--ogx-toolbar-btn-primary-hover': '#2563eb',
86
+ ...surfaceDerived,
53
87
  '--ogx-toolbar-btn-primary-text': '#ffffff',
54
88
  '--ogx-toolbar-btn-danger-bg': 'rgba(239, 68, 68, 0.08)',
55
89
  '--ogx-toolbar-btn-danger-hover': 'rgba(239, 68, 68, 0.15)',
56
90
  '--ogx-toolbar-btn-danger-text': '#ef4444',
57
- '--ogx-toolbar-input-bg': '#ffffff',
58
- '--ogx-toolbar-input-text': '#1e293b',
59
91
  '--ogx-toolbar-input-border': '#cbd5e1',
60
- '--ogx-toolbar-input-focus-border': '#3b82f6',
61
- '--ogx-toolbar-input-focus-shadow': '0 0 0 3px rgba(59, 130, 246, 0.12)',
62
- '--ogx-toolbar-chip-bg': '#f1f5f9',
63
- '--ogx-toolbar-chip-text': '#475569',
64
- '--ogx-toolbar-chip-active-bg': 'rgba(59, 130, 246, 0.12)',
65
- '--ogx-toolbar-chip-active-text': '#2563eb',
66
92
 
67
93
  // Overlays & Menus
68
94
  '--ogx-overlay-background': '#ffffff',
@@ -71,23 +97,93 @@ const lightBase: Record<string, string> = {
71
97
  '--ogx-overlay-shadow': '0 4px 6px -1px rgba(0,0,0,0.08), 0 2px 4px -1px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.06)',
72
98
  '--ogx-overlay-item-hover-bg': '#f8fafc',
73
99
  '--ogx-overlay-item-hover-text': '#0f172a',
74
- '--ogx-overlay-item-selected-bg': 'rgba(59, 130, 246, 0.08)',
75
- '--ogx-overlay-item-selected-text': '#2563eb',
76
100
  '--ogx-overlay-item-danger-bg': 'rgba(239, 68, 68, 0.08)',
77
101
  '--ogx-overlay-item-danger-text': '#ef4444',
78
102
 
79
103
  // Scrollbars
80
- '--ogx-scrollbar-thumb': 'rgba(148, 163, 184, 0.5)',
104
+ '--ogx-scrollbar-thumb': '#cbd5e1',
81
105
  '--ogx-scrollbar-track': 'transparent',
82
- '--ogx-scrollbar-size': '10px',
106
+ '--ogx-scrollbar-size': '8px',
83
107
 
84
108
  // Skeletons
85
109
  '--ogx-skeleton-base': '#f1f5f9',
86
110
  '--ogx-skeleton-highlight': '#ffffff',
87
- '--ogx-skeleton-dark-base': '#e2e8f0',
88
- '--ogx-skeleton-dark-highlight': '#f8fafc',
111
+
112
+ ...primaryDerived,
89
113
  };
90
114
 
115
+ // The complete dark palette (theme.mode === 'dark'): the values the stylesheet uses under
116
+ // prefers-color-scheme: dark, plus the toolbar, overlay, scrollbar and skeleton tokens.
117
+ const darkBase: Record<string, string> = {
118
+ ...lightBase,
119
+ '--ogx-color-primary': '#60a5fa',
120
+ '--ogx-color-primary-dark': '#3b82f6',
121
+ '--ogx-color-primary-light': '#1e3a5f',
122
+ '--ogx-color-primary-focus': 'rgba(96, 165, 250, 0.4)',
123
+
124
+ '--ogx-color-white': '#0f172a',
125
+ '--ogx-color-black': '#f8fafc',
126
+ '--ogx-color-gray-50': '#1e293b',
127
+ '--ogx-color-gray-100': '#334155',
128
+ '--ogx-color-gray-200': '#475569',
129
+ '--ogx-color-gray-300': '#64748b',
130
+ '--ogx-color-gray-400': '#94a3b8',
131
+ '--ogx-color-gray-500': '#cbd5e1',
132
+ '--ogx-color-gray-600': '#e2e8f0',
133
+ '--ogx-color-gray-700': '#e2e8f0',
134
+ '--ogx-color-gray-800': '#f1f5f9',
135
+ '--ogx-color-gray-900': '#f8fafc',
136
+
137
+ '--ogx-border-color': '#334155',
138
+ '--ogx-border-color-hover': '#475569',
139
+
140
+ '--ogx-shadow-sm': '0 1px 2px 0 rgba(0, 0, 0, 0.3)',
141
+ '--ogx-shadow-md': '0 4px 6px -1px rgba(0, 0, 0, 0.3)',
142
+ '--ogx-shadow-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.3)',
143
+
144
+ '--ogx-grid-background': '#0f172a',
145
+ '--ogx-grid-border-color': '#334155',
146
+ '--ogx-grid-header-background': '#1e293b',
147
+ '--ogx-grid-header-text': '#e2e8f0',
148
+ '--ogx-grid-header-hover-background': '#334155',
149
+ '--ogx-grid-header-sorted-background': '#1e3a5f',
150
+ '--ogx-grid-row-text': '#cbd5e1',
151
+ '--ogx-grid-row-hover-background': '#1e293b',
152
+ '--ogx-grid-row-alternate-background': '#162032',
153
+
154
+ '--ogx-checkbox-bg': '#1e293b',
155
+ '--ogx-checkbox-border': '#64748b',
156
+
157
+ '--ogx-grid-pinned-left-shadow': '4px 0 24px -4px rgba(0, 0, 0, 0.3)',
158
+ '--ogx-grid-pinned-right-shadow': '-4px 0 24px -4px rgba(0, 0, 0, 0.3)',
159
+
160
+ '--ogx-toolbar-btn-danger-bg': 'rgba(239, 68, 68, 0.15)',
161
+ '--ogx-toolbar-btn-danger-hover': 'rgba(239, 68, 68, 0.25)',
162
+ '--ogx-toolbar-btn-danger-text': '#fca5a5',
163
+ '--ogx-toolbar-input-border': '#334155',
164
+
165
+ '--ogx-overlay-background': '#1e293b',
166
+ '--ogx-overlay-text': '#cbd5e1',
167
+ '--ogx-overlay-border': '#334155',
168
+ '--ogx-overlay-shadow': '0 4px 6px -1px rgba(0,0,0,0.4), 0 2px 4px -1px rgba(0,0,0,0.3), 0 0 0 1px rgba(0,0,0,0.2)',
169
+ '--ogx-overlay-item-hover-bg': '#334155',
170
+ '--ogx-overlay-item-hover-text': '#f1f5f9',
171
+ '--ogx-overlay-item-danger-bg': 'rgba(239, 68, 68, 0.15)',
172
+ '--ogx-overlay-item-danger-text': '#fca5a5',
173
+
174
+ '--ogx-scrollbar-thumb': '#475569',
175
+
176
+ '--ogx-skeleton-base': '#1e293b',
177
+ '--ogx-skeleton-highlight': '#334155',
178
+
179
+ ...primaryDerived,
180
+ // On a dark surface the lighter primary reads better than primary-dark.
181
+ '--ogx-toolbar-chip-active-text': 'var(--ogx-color-primary)',
182
+ '--ogx-overlay-item-selected-text': 'var(--ogx-color-primary)',
183
+ };
184
+
185
+ const GRAY_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900] as const;
186
+
91
187
  function themeToCSS(theme: GridTheme): Record<string, string> {
92
188
  const vars: Record<string, string> = {};
93
189
 
@@ -110,6 +206,11 @@ function themeToCSS(theme: GridTheme): Record<string, string> {
110
206
  set('--ogx-color-warning', c.warning);
111
207
  set('--ogx-color-error', c.error);
112
208
  set('--ogx-color-info', c.info);
209
+ set('--ogx-color-white', c.white);
210
+ set('--ogx-color-black', c.black);
211
+ if (c.gray) {
212
+ for (const step of GRAY_STEPS) set(`--ogx-color-gray-${step}`, c.gray[step]);
213
+ }
113
214
  }
114
215
 
115
216
  if (theme.typography) {
@@ -156,6 +257,8 @@ function themeToCSS(theme: GridTheme): Record<string, string> {
156
257
 
157
258
  if (theme.grid) {
158
259
  const g = theme.grid;
260
+ // Row and header heights also reach the grid's layout through GridThemeContext
261
+ // (useGridThemeDimensions); these variables serve custom CSS outside the grid.
159
262
  set('--ogx-grid-row-height-compact', g.rowHeightCompact);
160
263
  set('--ogx-grid-row-height-standard', g.rowHeightStandard);
161
264
  set('--ogx-row-height', g.rowHeightStandard);
@@ -164,6 +267,8 @@ function themeToCSS(theme: GridTheme): Record<string, string> {
164
267
  set('--ogx-header-height', g.headerHeight);
165
268
  set('--ogx-grid-cell-padding-x', g.cellPaddingX);
166
269
  set('--ogx-grid-cell-padding-y', g.cellPaddingY);
270
+ set('--ogx-grid-cell-font-size', g.cellFontSize);
271
+ set('--ogx-grid-header-font-size', g.headerFontSize);
167
272
  set('--ogx-grid-background', g.background);
168
273
  set('--ogx-grid-border-color', g.borderColor);
169
274
  set('--ogx-grid-header-background', g.headerBackground);
@@ -240,8 +345,6 @@ function themeToCSS(theme: GridTheme): Record<string, string> {
240
345
  const sk = theme.skeleton;
241
346
  set('--ogx-skeleton-base', sk.baseColor);
242
347
  set('--ogx-skeleton-highlight', sk.highlightColor);
243
- set('--ogx-skeleton-dark-base', sk.darkBaseColor);
244
- set('--ogx-skeleton-dark-highlight', sk.darkHighlightColor);
245
348
  }
246
349
 
247
350
  return vars;
@@ -260,17 +363,19 @@ export function DataGridThemeProvider({
260
363
  className,
261
364
  style
262
365
  }: DataGridThemeProviderProps) {
263
- const cssVars = React.useMemo(() => {
264
-
265
- return { ...lightBase, ...themeToCSS(theme) };
266
- }, [theme]);
366
+ const cssVars = React.useMemo(
367
+ () => ({ ...(theme.mode === 'dark' ? darkBase : lightBase), ...themeToCSS(theme) }),
368
+ [theme],
369
+ );
267
370
 
268
371
  return (
269
- <div
270
- className={`ogx-theme-provider ${className || ''}`.trim()}
271
- style={{ ...style, ...cssVars } as React.CSSProperties}
272
- >
273
- {children}
274
- </div>
372
+ <GridThemeContext.Provider value={theme}>
373
+ <div
374
+ className={`ogx-theme-provider ${className || ''}`.trim()}
375
+ style={{ ...style, ...cssVars } as React.CSSProperties}
376
+ >
377
+ {children}
378
+ </div>
379
+ </GridThemeContext.Provider>
275
380
  );
276
381
  }
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+ import type { GridTheme } from './types';
3
+
4
+ /** The theme of the closest DataGridThemeProvider, for the values the grid reads in JS (heights). */
5
+ export const GridThemeContext = createContext<GridTheme | null>(null);
@@ -7,6 +7,11 @@ export type {
7
7
  GridThemeShadows,
8
8
  GridThemeGrid,
9
9
  GridThemeTransitions,
10
+ GridThemeToolbar,
11
+ GridThemeOverlays,
12
+ GridThemeScrollbar,
13
+ GridThemeSkeleton,
14
+ GridThemeGrayScale,
10
15
  } from './types';
11
16
 
12
17
  export { DataGridThemeProvider } from './DataGridThemeProvider';
@@ -1,6 +1,7 @@
1
1
  import type { GridTheme } from './types';
2
2
 
3
3
  export const darkTheme: GridTheme = {
4
+ mode: 'dark',
4
5
  colors: {
5
6
  primary: '#60a5fa',
6
7
  primaryDark: '#3b82f6',
@@ -32,11 +33,11 @@ export const darkTheme: GridTheme = {
32
33
  md: '0 4px 6px -1px rgba(0, 0, 0, 0.3)',
33
34
  },
34
35
  toolbar: {
35
- background: 'transparent',
36
+ background: '#1e293b',
36
37
  text: '#cbd5e1',
37
38
  border: '#334155',
38
39
  buttonBackground: 'transparent',
39
- buttonHoverBackground: '#1e293b',
40
+ buttonHoverBackground: '#334155',
40
41
  buttonText: '#e2e8f0',
41
42
  buttonPrimaryBackground: '#3b82f6',
42
43
  buttonPrimaryHoverBackground: '#2563eb',
@@ -73,8 +74,6 @@ export const darkTheme: GridTheme = {
73
74
  skeleton: {
74
75
  baseColor: '#1e293b',
75
76
  highlightColor: '#334155',
76
- darkBaseColor: '#0f172a',
77
- darkHighlightColor: '#1e293b',
78
77
  }
79
78
  };
80
79
 
@@ -140,6 +139,8 @@ export const compactTheme: GridTheme = {
140
139
  headerHeight: '40px',
141
140
  cellPaddingX: '8px',
142
141
  cellPaddingY: '4px',
142
+ cellFontSize: '12px',
143
+ headerFontSize: '12px',
143
144
  },
144
145
  typography: {
145
146
  fontSizeSm: '0.8rem',