@opencorestack/opengridx 3.8.0 → 3.9.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 (76) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/ai.cjs.map +1 -1
  3. package/dist/ai.es.js.map +1 -1
  4. package/dist/index.d.ts +342 -3
  5. package/dist/opengridx.css +1 -1
  6. package/dist/opengridx.es.js +6952 -6697
  7. package/dist/opengridx.es.js.map +1 -1
  8. package/dist/opengridx.umd.js +17 -17
  9. package/dist/opengridx.umd.js.map +1 -1
  10. package/docs/API_REFERENCE.md +121 -4
  11. package/docs/customization/theming.md +11 -2
  12. package/docs/features/ai-toolkit.md +1 -1
  13. package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
  14. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
  15. package/lib/components/Cell/Cell.css +1 -3
  16. package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
  17. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
  18. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
  19. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
  20. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
  21. package/lib/components/DataGrid/DataGrid.css +1 -28
  22. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
  23. package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
  24. package/lib/components/DataGrid/GridListView.tsx +7 -5
  25. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
  26. package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
  27. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  28. package/lib/components/DataGrid/GridRoot.tsx +8 -1
  29. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  30. package/lib/components/DataGrid/GridViewport.tsx +3 -1
  31. package/lib/components/DataGrid/GridVirtualRows.tsx +3 -2
  32. package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
  33. package/lib/components/FilterPanel/FilterPanel.tsx +25 -56
  34. package/lib/components/Header/ColumnMenu.css +5 -5
  35. package/lib/components/Header/ColumnMenu.tsx +17 -15
  36. package/lib/components/Header/Header.css +2 -11
  37. package/lib/components/Header/Header.tsx +8 -6
  38. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
  39. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
  40. package/lib/components/ListView/ListView.css +0 -16
  41. package/lib/components/ListView/ListViewRow.tsx +3 -1
  42. package/lib/components/Pagination/Pagination.tsx +15 -18
  43. package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
  44. package/lib/components/Row/Row.tsx +9 -4
  45. package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
  46. package/lib/components/Toolbar/GridToolbar.tsx +35 -29
  47. package/lib/components/Toolbar/PivotPanel.tsx +26 -24
  48. package/lib/components/ui/Checkbox.css +5 -5
  49. package/lib/components/ui/DragHandleIcon.tsx +3 -1
  50. package/lib/components/ui/ExpandIcon.css +3 -3
  51. package/lib/components/ui/ExpandIcon.tsx +3 -1
  52. package/lib/hooks/core/useGridRowInteractions.ts +13 -3
  53. package/lib/hooks/core/useGridToolbarStack.ts +2 -2
  54. package/lib/hooks/core/useGridVirtualization.ts +40 -25
  55. package/lib/hooks/core/useShallowStableValue.ts +29 -0
  56. package/lib/hooks/features/useGridAiAssistant.ts +52 -22
  57. package/lib/hooks/features/useGridSpanning.ts +1 -1
  58. package/lib/styles/base/reset.css +6 -7
  59. package/lib/theme/DataGridThemeProvider.tsx +9 -0
  60. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
  61. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
  62. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
  63. package/lib/theme/types.ts +18 -0
  64. package/lib/types/locale.ts +344 -0
  65. package/lib/types/rows.ts +0 -2
  66. package/lib/utils/cellSelection.ts +0 -2
  67. package/lib/utils/editing/index.ts +1 -1
  68. package/lib/utils/filtering/filterInput.ts +45 -0
  69. package/lib/utils/filtering/index.ts +0 -28
  70. package/lib/utils/gridLocale.ts +241 -0
  71. package/lib/utils/pinning/index.ts +0 -107
  72. package/lib/utils/sorting/index.ts +42 -33
  73. package/lib/utils/values.ts +5 -1
  74. package/llms.txt +9 -3
  75. package/package.json +1 -1
  76. package/lib/theme/defaultStyles.ts +0 -0
@@ -0,0 +1,241 @@
1
+ import type { GridFilterOperator, GridLocaleText } from '../types';
2
+ import { HEADER_FILTER_OPERATOR_LABELS } from './headerFilters';
3
+ import { defaultCellSelectionAnnouncement } from './cellSelection';
4
+
5
+ /** `localeText` keys that hold a lookup record; the resolved locale has a function for each. */
6
+ type GridLocaleRecordKey = 'headerFilterOperators' | 'filterPanelOperators' | 'toolbarAggregationFunctions' | 'pivotAggregationFunctions';
7
+ type GridLocaleScalarKey = Exclude<keyof GridLocaleText, GridLocaleRecordKey>;
8
+
9
+ /** Every `localeText` string with its English default filled in. */
10
+ export type GridLocaleScalars = Required<Pick<GridLocaleText, GridLocaleScalarKey>>;
11
+
12
+ /** The grid's resolved strings: every key of `GridLocaleText`, lookup records turned into functions. */
13
+ export interface ResolvedGridLocale extends GridLocaleScalars {
14
+ /** Operator name in the header filter row. */
15
+ headerFilterOperator: (operator: GridFilterOperator) => string;
16
+ /** Operator name in the filter panel's operator select. */
17
+ filterPanelOperator: (operator: GridFilterOperator) => string;
18
+ /** Aggregation function name in the toolbar's summaries panel. */
19
+ toolbarAggregationFunction: (fn: string) => string;
20
+ /** Aggregation function name in the pivot panel. */
21
+ pivotAggregationFunction: (fn: string) => string;
22
+ }
23
+
24
+ const plural = (count: number, one: string, many: string) => (count === 1 ? one : many);
25
+
26
+ /** The built-in English strings. */
27
+ export const GRID_LOCALE_EN: GridLocaleScalars = {
28
+ paginationRowsPerPage: 'Rows per page:',
29
+ paginationOf: (from, to, count) => (count === 0 ? `0 of ${count}` : `${from}–${to} of ${count}`),
30
+ paginationPage: (page, pageCount) => `Page ${page} of ${pageCount}`,
31
+ noRowsLabel: 'No Data',
32
+ cellSelectionAnnouncement: defaultCellSelectionAnnouncement,
33
+ aiAssistantButton: 'Ask AI',
34
+ headerFilterCustom: 'Custom filter',
35
+ headerFilterCustomTooltip: 'Set in the filter panel',
36
+ headerFilterCellLabel: h => `Filter ${h}`,
37
+ headerFilterValueLabel: h => `Filter value for ${h}`,
38
+ headerFilterOperatorButtonLabel: (h, op) => `Filter operator for ${h}: ${op}`,
39
+ headerFilterOperatorMenuLabel: h => `Filter operator for ${h}`,
40
+ headerFilterClearLabel: h => `Clear filter for ${h}`,
41
+ headerFilterClearTooltip: 'Clear',
42
+ headerFilterAny: 'Any',
43
+ headerFilterTrue: 'Yes',
44
+ headerFilterFalse: 'No',
45
+
46
+ gridLabel: 'Data grid',
47
+ listViewLabel: 'Data grid list view',
48
+ listViewItemCount: n => `${n} ${plural(n, 'item', 'items')}`,
49
+ listViewPage: (p, pc) => `page ${p} of ${pc}`,
50
+ listViewSelectedCount: n => `${n} selected`,
51
+ loadingLabel: 'Loading data',
52
+ liveRegionLoading: 'Loading data...',
53
+ liveRegionError: m => `Error: ${m}`,
54
+ liveRegionRowsFound: n => `${n} ${plural(n, 'row', 'rows')} found`,
55
+ errorOverlayTitle: 'Oops! Something went wrong',
56
+ errorOverlayRetry: 'Retry',
57
+ aggregationFooterLabel: 'Aggregation totals',
58
+ cellErrorLabel: field => `Error in cell: ${field}`,
59
+ cellErrorTooltip: 'Render error',
60
+
61
+ headerRowReorderLabel: 'Row reorder handle',
62
+ headerRowReorderTooltip: 'Drag to reorder rows',
63
+ headerDetailPanelLabel: 'Expand row details',
64
+ headerSelectAllLabel: 'Select all rows',
65
+ headerDeselectAllLabel: 'Deselect all rows',
66
+ headerSortDescription: (d, p, n) => `Sorted ${d === 'asc' ? 'ascending' : 'descending'}, sort priority ${p} of ${n}`,
67
+ headerColumnMenuButtonLabel: h => `Open column menu for ${h}`,
68
+ headerColumnResizeLabel: f => `Resize ${f} column`,
69
+ headerFilterNoFilterLabel: h => `No filter for ${h}`,
70
+
71
+ rowSelectLabel: id => `Select row ${id}`,
72
+ rowDeselectLabel: id => `Deselect row ${id}`,
73
+ rowReorderLabel: 'Drag to reorder row',
74
+ rowDetailPanelExpandLabel: 'Expand row details',
75
+ rowDetailPanelCollapseLabel: 'Collapse row details',
76
+ rowExpandLabel: 'Expand row',
77
+ rowCollapseLabel: 'Collapse row',
78
+
79
+ columnMenuLabel: h => `Column options for ${h}`,
80
+ columnMenuSortAscending: 'Sort Ascending',
81
+ columnMenuSortDescending: 'Sort Descending',
82
+ columnMenuUnsort: 'Unsort',
83
+ columnMenuPinColumn: 'Pin column',
84
+ columnMenuPinLeft: 'Pin to Left',
85
+ columnMenuPinLeftLabel: 'Pin to left',
86
+ columnMenuPinRight: 'Pin to Right',
87
+ columnMenuPinRightLabel: 'Pin to right',
88
+ columnMenuUnpin: 'Unpin',
89
+ columnMenuUnpinLabel: 'Unpin column',
90
+ columnMenuHideColumn: 'Hide Column',
91
+ columnMenuHideColumnLabel: h => `Hide ${h} column`,
92
+ columnMenuManageColumns: 'Manage columns',
93
+ columnMenuManageColumnsLabel: 'Manage all columns',
94
+
95
+ columnsPanelSearchPlaceholder: 'Search',
96
+ columnsPanelDragToReorder: 'Drag to reorder',
97
+ columnsPanelShowHideAll: 'Show/Hide All',
98
+ columnsPanelReset: 'Reset',
99
+ columnsPanelClose: 'Close',
100
+
101
+ filterPanelValueLabel: h => `Filter value for ${h}`,
102
+ filterPanelOperatorLabel: h => `Filter operator for ${h}`,
103
+ filterPanelClearLabel: h => `Clear filter for ${h}`,
104
+ filterPanelClearTooltip: 'Clear',
105
+ filterPanelAny: 'Any',
106
+ filterPanelTrue: 'true',
107
+ filterPanelFalse: 'false',
108
+ filterPanelNumberPlaceholder: 'Number…',
109
+ filterPanelValuePlaceholder: 'Value…',
110
+ filterPanelMatch: 'Match:',
111
+ filterPanelAnd: 'ALL (AND)',
112
+ filterPanelOr: 'ANY (OR)',
113
+ filterPanelHiddenConditions: n => (n === 1
114
+ ? '1 more condition (a filter group or a second condition on a column) is applied and not shown here.'
115
+ : `${n} more conditions (filter groups or further conditions on a column) are applied and not shown here.`),
116
+ filterPanelNoColumns: 'No filterable columns.',
117
+
118
+ toolbarLabel: 'Grid toolbar',
119
+ toolbarSearchPlaceholder: 'Search...',
120
+ toolbarSearchLabel: 'Global Search',
121
+ toolbarSearchClear: 'Clear search',
122
+ toolbarColumns: 'Columns',
123
+ toolbarColumnsLabel: 'Manage columns',
124
+ toolbarColumnsPanelLabel: 'Column visibility',
125
+ toolbarFilters: 'Filters',
126
+ toolbarFiltersLabel: 'Advanced filters',
127
+ toolbarFiltersActive: 'Filters active',
128
+ toolbarFiltersClearAll: 'Clear all',
129
+ toolbarFiltersClearAllTooltip: 'Clear all filters',
130
+ toolbarFiltersClose: 'Close',
131
+ toolbarFiltersCloseTooltip: 'Close filters',
132
+ toolbarFiltersCloseLabel: 'Close filters panel',
133
+ toolbarPivot: 'Pivot',
134
+ toolbarPivotLabel: 'Configure pivot',
135
+ toolbarPivotActive: 'Pivot active',
136
+ toolbarSummaries: 'Summaries',
137
+ toolbarSummariesLabel: 'Configure summaries',
138
+ toolbarSummariesActive: n => `${n} active summaries`,
139
+ toolbarSummariesPanelLabel: 'Summaries configuration',
140
+ toolbarSummariesClearAll: 'Clear all',
141
+ toolbarSummariesClearAllTooltip: 'Clear all summaries',
142
+ toolbarSummariesEmpty: 'No aggregable columns.',
143
+ toolbarSummariesEmptyHintBefore: 'Set',
144
+ toolbarSummariesEmptyHintAfter: 'on a column to enable.',
145
+
146
+ pivotPanelLabel: 'Pivot configuration',
147
+ pivotMode: 'Pivot Mode',
148
+ pivotReset: 'Reset',
149
+ pivotResetTooltip: 'Reset pivot',
150
+ pivotRowFields: 'Row Fields',
151
+ pivotColumnFields: 'Column Fields',
152
+ pivotValueFields: 'Value Fields (aggregated)',
153
+ pivotRowFieldsPlaceholder: 'Add fields here — each becomes a row label',
154
+ pivotColumnFieldsPlaceholder: 'Add fields here — unique values become column headers',
155
+ pivotValueFieldsPlaceholder: 'Add numeric fields here — values are aggregated',
156
+ pivotRemoveTooltip: h => `Remove ${h}`,
157
+ pivotRemoveRowLabel: h => `Remove ${h} from Row Fields`,
158
+ pivotRemoveColumnLabel: h => `Remove ${h} from Column Fields`,
159
+ pivotRemoveValueLabel: (h, fn) => `Remove ${h} (${fn}) from Value Fields`,
160
+ pivotAggregationTooltip: 'Aggregation function',
161
+ pivotAggregationLabel: h => `Aggregation function for ${h}`,
162
+ pivotAvailableFields: 'Available Fields',
163
+ pivotAllFieldsInUse: 'All fields are in use.',
164
+ pivotAddRow: 'Row',
165
+ pivotAddColumn: 'Col',
166
+ pivotAddValue: 'Val',
167
+ pivotAddRowTooltip: 'Add to Row Fields',
168
+ pivotAddColumnTooltip: 'Add to Column Fields',
169
+ pivotAddValueTooltip: 'Add to Value Fields',
170
+ pivotAddRowLabel: h => `Add ${h} to Row Fields`,
171
+ pivotAddColumnLabel: h => `Add ${h} to Column Fields`,
172
+ pivotAddValueLabel: h => `Add ${h} to Value Fields`,
173
+
174
+ aiAssistantTitle: 'Ask AI',
175
+ aiAssistantClose: 'Close',
176
+ aiAssistantPromptLabel: 'Prompt',
177
+ aiAssistantPlaceholder: 'Ask about this table…',
178
+ aiAssistantStartDictation: 'Dictate a prompt',
179
+ aiAssistantStopDictation: 'Stop dictation',
180
+ aiAssistantAsk: 'Ask',
181
+ aiAssistantStop: 'Stop',
182
+ aiAssistantSuggestions: 'Suggestions',
183
+ aiAssistantAppliedChanges: 'Applied changes',
184
+ aiAssistantRemoveChange: label => `Remove ${label}`,
185
+ aiAssistantUndo: 'Undo',
186
+ aiAssistantIgnored: 'Ignored',
187
+ aiAssistantIgnoredItem: m => `Ignored: ${m}`,
188
+ aiAssistantEarlier: 'Earlier',
189
+ aiAssistantThinking: 'Thinking…',
190
+ aiAssistantStopped: 'Stopped',
191
+ aiAssistantUndone: 'Undone',
192
+ aiAssistantChangeRemoved: 'Change removed',
193
+ aiAssistantApplied: (count, ignored) => {
194
+ const applied = count === 0 ? 'No changes' : `Applied ${count} ${plural(count, 'change', 'changes')}`;
195
+ return ignored > 0 ? `${applied}, ${ignored} ignored` : applied;
196
+ },
197
+ aiAssistantNotValidated: 'The reply was not validated',
198
+ aiAssistantFailed: m => `The assistant could not answer: ${m}`,
199
+ aiAssistantUnknownError: 'Unknown error',
200
+
201
+ paginationLabel: 'Pagination',
202
+ paginationFirstPageLabel: 'Go to first page',
203
+ paginationFirstPageTooltip: 'First page',
204
+ paginationPreviousPageLabel: 'Go to previous page',
205
+ paginationPreviousPageTooltip: 'Previous page',
206
+ paginationNextPageLabel: 'Go to next page',
207
+ paginationNextPageTooltip: 'Next page',
208
+ paginationLastPageLabel: 'Go to last page',
209
+ paginationLastPageTooltip: 'Last page',
210
+
211
+ cellSelectionStatsCount: 'Count',
212
+ cellSelectionStatsSum: 'Sum',
213
+ cellSelectionStatsAverage: 'Average',
214
+ };
215
+
216
+ const SCALAR_KEYS = Object.keys(GRID_LOCALE_EN) as GridLocaleScalarKey[];
217
+
218
+ function pickScalar<K extends GridLocaleScalarKey>(target: GridLocaleScalars, localeText: GridLocaleText, key: K): void {
219
+ const value = localeText[key];
220
+ if (value !== undefined) target[key] = value as GridLocaleScalars[K];
221
+ }
222
+
223
+ /**
224
+ * Fills every string `localeText` leaves out with its English default. Pure; memoise the result
225
+ * per `localeText` object.
226
+ */
227
+ export function resolveGridLocale(localeText?: GridLocaleText): ResolvedGridLocale {
228
+ const scalars: GridLocaleScalars = { ...GRID_LOCALE_EN };
229
+ if (localeText) SCALAR_KEYS.forEach(key => pickScalar(scalars, localeText, key));
230
+ const headerFilterOperators = localeText?.headerFilterOperators;
231
+ const filterPanelOperators = localeText?.filterPanelOperators;
232
+ const toolbarFunctions = localeText?.toolbarAggregationFunctions;
233
+ const pivotFunctions = localeText?.pivotAggregationFunctions;
234
+ return {
235
+ ...scalars,
236
+ headerFilterOperator: op => headerFilterOperators?.[op] ?? HEADER_FILTER_OPERATOR_LABELS[op] ?? op,
237
+ filterPanelOperator: op => filterPanelOperators?.[op] ?? op,
238
+ toolbarAggregationFunction: fn => toolbarFunctions?.[fn] ?? fn,
239
+ pivotAggregationFunction: fn => pivotFunctions?.[fn] ?? fn.toUpperCase(),
240
+ };
241
+ }
@@ -137,67 +137,6 @@ export function getPinnedEdgeFields<R extends GridRowModel = GridRowModel>(
137
137
  return { lastLeft, firstRight };
138
138
  }
139
139
 
140
- export function isColumnPinnable<R extends GridRowModel = GridRowModel>(
141
- colDef: GridColDef<R>
142
- ): boolean {
143
- return colDef.pinnable !== false;
144
- }
145
-
146
- export function pinColumn(
147
- field: string,
148
- position: GridPinnedPosition,
149
- currentPinning?: GridColumnPinning
150
- ): GridColumnPinning {
151
- const newPinning: GridColumnPinning = {
152
- left: [...(currentPinning?.left || [])],
153
- right: [...(currentPinning?.right || [])]
154
- };
155
-
156
- newPinning.left = (newPinning.left || []).filter(f => f !== field);
157
- newPinning.right = (newPinning.right || []).filter(f => f !== field);
158
-
159
- if (position === 'left') {
160
- if (!newPinning.left) newPinning.left = [];
161
- newPinning.left.push(field);
162
- } else {
163
- if (!newPinning.right) newPinning.right = [];
164
- newPinning.right.push(field);
165
- }
166
-
167
- return newPinning;
168
- }
169
-
170
- export function unpinColumn(
171
- field: string,
172
- currentPinning?: GridColumnPinning
173
- ): GridColumnPinning {
174
- if (!currentPinning) return {};
175
-
176
- return {
177
- left: currentPinning.left?.filter(f => f !== field) || [],
178
- right: currentPinning.right?.filter(f => f !== field) || []
179
- };
180
- }
181
-
182
- export function getPinnedColumnsWidth(
183
- columns: GridColDef[],
184
- columnWidths: Record<string, number>,
185
- position: GridPinnedPosition,
186
- pinnedColumns?: GridColumnPinning
187
- ): number {
188
- if (!pinnedColumns) return 0;
189
-
190
- const pinnedFields = position === 'left' ? pinnedColumns.left : pinnedColumns.right;
191
- if (!pinnedFields) return 0;
192
-
193
- return pinnedFields.reduce((total, field) => {
194
- const col = columns.find(c => c.field === field);
195
- if (!col) return total;
196
-
197
- return total + getRenderedColumnWidth(col, columnWidths);
198
- }, 0);
199
- }
200
-
201
140
  export function isRowPinned(
202
141
  rowId: GridRowId,
203
142
  pinnedRows?: GridRowPinning
@@ -263,52 +202,6 @@ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
263
202
  return { top, center, bottom };
264
203
  }
265
204
 
266
- export function pinRow(
267
- rowId: GridRowId,
268
- position: 'top' | 'bottom',
269
- currentPinning?: GridRowPinning
270
- ): GridRowPinning {
271
- const newPinning: GridRowPinning = {
272
- top: [...(currentPinning?.top || [])],
273
- bottom: [...(currentPinning?.bottom || [])]
274
- };
275
-
276
- newPinning.top = (newPinning.top || []).filter(id => id !== rowId);
277
- newPinning.bottom = (newPinning.bottom || []).filter(id => id !== rowId);
278
-
279
- if (position === 'top') {
280
- if (!newPinning.top) newPinning.top = [];
281
- newPinning.top.push(rowId);
282
- } else {
283
- if (!newPinning.bottom) newPinning.bottom = [];
284
- newPinning.bottom.push(rowId);
285
- }
286
-
287
- return newPinning;
288
- }
289
-
290
- export function unpinRow(
291
- rowId: GridRowId,
292
- currentPinning?: GridRowPinning
293
- ): GridRowPinning {
294
- if (!currentPinning) return {};
295
-
296
- return {
297
- top: currentPinning.top?.filter(id => id !== rowId) || [],
298
- bottom: currentPinning.bottom?.filter(id => id !== rowId) || []
299
- };
300
- }
301
-
302
- export function getPinnedRowsCount(
303
- position: 'top' | 'bottom',
304
- pinnedRows?: GridRowPinning
305
- ): number {
306
- if (!pinnedRows) return 0;
307
-
308
- const pinnedIds = position === 'top' ? pinnedRows.top : pinnedRows.bottom;
309
- return pinnedIds?.length || 0;
310
- }
311
-
312
205
  /**
313
206
  * The column pinning model after the column menu pins `field` to `side` (appended after the
314
207
  * columns already pinned there) or unpins it (`null`). The field is removed from the other side.
@@ -18,7 +18,8 @@ const RANK_STRING = 3;
18
18
  const RANK_OTHER = 4;
19
19
 
20
20
  /** A normalized sort value. null is the empty bucket (null, undefined, NaN, Invalid Date, unparsable). */
21
- type SortKey = { rank: number; num: number; str: string } | null;
21
+ /** `big`: the exact value of a bigint key, whose `num` is only its nearest double. */
22
+ type SortKey = { rank: number; num: number; str: string; big?: bigint } | null;
22
23
 
23
24
  function numberKey(rank: number, num: number): SortKey {
24
25
  return { rank, num, str: '' };
@@ -32,6 +33,7 @@ function numberKey(rank: number, num: number): SortKey {
32
33
  function toSortKey(value: unknown, type?: GridColDef['type']): SortKey {
33
34
  if (value == null) return null;
34
35
  if (type === 'number') {
36
+ if (typeof value === 'bigint') return { rank: RANK_NUMBER, num: Number(value), str: '', big: value };
35
37
  const n = toNumber(value);
36
38
  return n === null ? null : numberKey(RANK_NUMBER, n);
37
39
  }
@@ -43,7 +45,7 @@ function toSortKey(value: unknown, type?: GridColDef['type']): SortKey {
43
45
  case 'number':
44
46
  return Number.isNaN(value) ? null : numberKey(RANK_NUMBER, value);
45
47
  case 'bigint':
46
- return numberKey(RANK_NUMBER, Number(value));
48
+ return { rank: RANK_NUMBER, num: Number(value), str: '', big: value };
47
49
  case 'boolean':
48
50
  return numberKey(RANK_BOOLEAN, value ? 1 : 0);
49
51
  case 'string':
@@ -65,9 +67,29 @@ function compareSortKeys(a: SortKey, b: SortKey): number {
65
67
  if (b === null) return -1;
66
68
  if (a.rank !== b.rank) return a.rank - b.rank;
67
69
  if (a.rank === RANK_STRING || a.rank === RANK_OTHER) return collator.compare(a.str, b.str);
68
- return a.num < b.num ? -1 : a.num > b.num ? 1 : 0;
70
+ if (a.num !== b.num) return a.num < b.num ? -1 : 1;
71
+ // Number(bigint) rounds monotonically, so only equal doubles can hide an order between bigints.
72
+ return a.big !== undefined || b.big !== undefined ? compareExact(a.big ?? a.num, b.big ?? b.num) : 0;
69
73
  }
70
74
 
75
+ /** Exact ascending comparison of numbers and bigints (no rounding of either). */
76
+ function compareExact(a: number | bigint, b: number | bigint): number {
77
+ if (typeof a === 'number' && typeof b === 'number') return a < b ? -1 : a > b ? 1 : 0;
78
+ if (typeof a === 'bigint' && typeof b === 'bigint') return a < b ? -1 : a > b ? 1 : 0;
79
+ if (typeof a === 'number') return -compareExact(b, a);
80
+ // a is a bigint, b a number.
81
+ const n = b as number;
82
+ if (!Number.isFinite(n)) return n > 0 ? -1 : 1;
83
+ const floor = Math.floor(n);
84
+ const whole = BigInt(floor);
85
+ if (a < whole) return -1;
86
+ if (a > whole) return 1;
87
+ return n > floor ? -1 : 0;
88
+ }
89
+
90
+ const isUnsafeBigInt = (value: unknown): value is bigint =>
91
+ typeof value === 'bigint' && (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER));
92
+
71
93
  /** Reverse an ascending comparison for 'desc'. */
72
94
  export function applySortDirection(comparison: number, direction: 'asc' | 'desc'): number {
73
95
  if (comparison === 0) return 0;
@@ -115,36 +137,6 @@ export function callSortComparator(
115
137
 
116
138
  const defaultRowId = (row: GridRowModel): GridRowId => row.id;
117
139
 
118
- /**
119
- * Compare two rows by a sort model, reading cells through `valueGetter` and using each column's
120
- * `type` (or its `sortComparator`). Returns 0 when every key is equal.
121
- */
122
- export function compareRowsBySortModel(
123
- a: GridRowModel,
124
- b: GridRowModel,
125
- sortModel: readonly GridSortItem[],
126
- columns?: GridColumnLookup,
127
- getRowId: (row: GridRowModel) => GridRowId = defaultRowId
128
- ): number {
129
- for (const sortItem of sortModel) {
130
- const colDef = columns?.byField.get(sortItem.field);
131
- const va = getCellValue(a, sortItem.field, colDef);
132
- const vb = getCellValue(b, sortItem.field, colDef);
133
- const comparison = colDef?.sortComparator
134
- ? applySortDirection(
135
- callSortComparator(
136
- colDef.sortComparator,
137
- { id: getRowId(a), field: sortItem.field, row: a, value: va },
138
- { id: getRowId(b), field: sortItem.field, row: b, value: vb }
139
- ),
140
- sortItem.sort
141
- )
142
- : compareValues(va, vb, sortItem.sort, colDef?.type);
143
- if (comparison !== 0) return comparison;
144
- }
145
- return 0;
146
- }
147
-
148
140
  export function sortRows<R extends GridRowModel>(
149
141
  rows: R[],
150
142
  sortModel: GridSortItem[],
@@ -230,6 +222,19 @@ function rankTexts(buckets: Map<string, number[]>, num: Float64Array): void {
230
222
  }
231
223
  }
232
224
 
225
+ /** Replaces the numbers of a column holding unsafe bigints by their exact ascending position. */
226
+ function rankNumbersExactly(group: Uint8Array, num: Float64Array, bigints: ReadonlyMap<number, bigint>): void {
227
+ const indices: number[] = [];
228
+ for (let i = 0; i < group.length; i++) if (group[i] === RANK_NUMBER) indices.push(i);
229
+ const exact = (i: number): number | bigint => bigints.get(i) ?? num[i];
230
+ indices.sort((ia, ib) => compareExact(exact(ia), exact(ib)));
231
+ let position = 0;
232
+ for (let k = 0; k < indices.length; k++) {
233
+ if (k > 0 && compareExact(exact(indices[k - 1]), exact(indices[k])) !== 0) position++;
234
+ num[indices[k]] = position;
235
+ }
236
+ }
237
+
233
238
  function addToBucket(buckets: Map<string, number[]>, text: string, index: number): void {
234
239
  const bucket = buckets.get(text);
235
240
  if (bucket) bucket.push(index);
@@ -248,9 +253,12 @@ function encodeSortColumn(count: number, valueAt: (i: number) => unknown, typeAt
248
253
  const num = new Float64Array(count);
249
254
  const strings = new Map<string, number[]>();
250
255
  const others = new Map<string, number[]>();
256
+ // Bigints beyond 2^53 lose precision as doubles: if any appears, numbers are re-ranked exactly.
257
+ let bigints: Map<number, bigint> | null = null;
251
258
  for (let i = 0; i < count; i++) {
252
259
  const value = valueAt(i);
253
260
  const type = typeAt(i);
261
+ if (isUnsafeBigInt(value) && type !== 'date') (bigints ??= new Map()).set(i, value);
254
262
  if (typeof value === 'number' && type !== 'date') {
255
263
  // toSortKey for a number, without the allocation.
256
264
  if (Number.isNaN(value)) {
@@ -273,6 +281,7 @@ function encodeSortColumn(count: number, valueAt: (i: number) => unknown, typeAt
273
281
  }
274
282
  rankTexts(strings, num);
275
283
  rankTexts(others, num);
284
+ if (bigints) rankNumbersExactly(group, num, bigints);
276
285
  return { kind: 'encoded', group, num };
277
286
  }
278
287
 
@@ -84,6 +84,8 @@ export function getFormattedValue<R extends GridRowModel>(
84
84
  }
85
85
  }
86
86
 
87
+ const RADIX_PREFIX = /^[+-]?0[box]/i;
88
+
87
89
  /**
88
90
  * Coerce a cell or filter value to a number. Returns null for null/undefined, blank strings,
89
91
  * NaN and anything that does not parse, so an empty cell is never read as 0.
@@ -94,8 +96,10 @@ export function toNumber(value: unknown): number | null {
94
96
  if (typeof value === 'string') {
95
97
  const trimmed = value.trim();
96
98
  if (trimmed === '') return null;
99
+ // Number() also reads "0x10", "0b1", "0o7" and "Infinity", which nobody types as a number.
100
+ if (RADIX_PREFIX.test(trimmed)) return null;
97
101
  const n = Number(trimmed);
98
- return Number.isNaN(n) ? null : n;
102
+ return Number.isFinite(n) ? n : null;
99
103
  }
100
104
  if (typeof value === 'boolean') return null;
101
105
  if (value instanceof Date) {
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 3.7.0
3
+ # Version: 3.9.0
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -721,9 +721,15 @@ Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, Pa
721
721
  13. **`valueFormatter` + `renderCell`**: `renderCell` receives `params.formattedValue` (v2.1+), so a custom renderer can reuse the formatter instead of duplicating it.
722
722
  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`.
723
723
  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`.
724
- 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.
724
+ 16. **Row limit**: the scroll height is `rows × rowHeight` px and browsers cap element height: Chromium and WebKit about 33.5M px (~645,000 rows at 52 px), Firefox about 17.9M px (~344,000 rows). Since 3.5.1 the grid caps its content height to the measured limit and warns in development, naming how many rows are reachable. Beyond that use a `dataSource` (server pages or infinite scroll) or pagination; `density="compact"` stretches each limit about 1.6×.
725
725
  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.
726
726
  18. **`valueFormatter` / `valueGetter` params are `unknown`-typed**: cast (`(value as number).toFixed(2)`) before calling methods, or TypeScript strict mode rejects it.
727
727
  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.
728
728
  20. **Next.js App Router**: render `<DataGrid>` from a `'use client'` file (column functions such as `valueFormatter` cannot cross from a Server Component) and import the stylesheet in `app/layout.tsx`. Rows fetched in a Server Component pass as props (serialisable data only). The ES bundle starts with `'use client'` (3.6+), so a Server Component may also render `<DataGrid>` with serialisable props. `next build` fails with `Can't resolve 'exceljs'` unless the optional peers are installed or aliased in `next.config.ts` (`turbopack.resolveAlias` to an empty module; webpack `resolve.alias[name] = false`). `@opencorestack/opengridx/ai` runs on the server too, but the validation brand is a Symbol that does not survive JSON: validate on the client (`createGridAiPromptHandler`). Guide: `docs/features/nextjs.md`.
729
- 21. **Right-to-left**: wrap the grid in `dir="rtl"` (or set `document.dir`); no prop. Works without pinned columns (3.6+): column order, text alignment (`align: 'left'`/'right' = start/end), horizontal scrolling + column virtualization, ArrowLeft = next column, resize handle on the left edge, fill handle bottom-left, toolbar panels open rightwards. **Pinned columns are not supported in RTL** (they do not stick); do not pin in RTL grids. Guide: `docs/features/rtl.md`.
729
+ 21. **Right-to-left**: wrap the grid in `dir="rtl"` (or set `document.dir`); no prop. Works without pinned columns (3.6+): column order, text alignment (`align: 'left'`/'right' = start/end), horizontal scrolling + column virtualization, ArrowLeft = next column, resize handle on the left edge, fill handle bottom-left, toolbar panels open rightwards. Pinned columns work in RTL (3.6+): `pinnedColumns.left` / `right` are logical (inline start / end), so `left` sticks to the right edge in RTL. Guide: `docs/features/rtl.md`.
730
+ 22. **Rows are replaced, not mutated** (3.8+ relies on it): change a row by passing a new object (`rows.map(r => r.id === id ? { ...r, ...changes } : r)`). The quick filter caches each row object's searched text, and an in-place mutation keeps the old text. A `valueFormatter` that reads outside state needs new column definitions when that state changes.
731
+ 23. **Large grids (3.8+)**: committing an edit, paste, undo, redo or fill at 1,000,000 rows takes ~35–50 ms; expanding a group ~135 ms, collapsing ~1 ms; select-all ~270 ms (it builds the selection model array). Sorting is ~0.4–0.5 s at 1M rows.
732
+ 24. **Styling buttons inside the grid (3.8.1+)**: the grid's button reset sets `background` and `border` at zero specificity, so any class you put on a button inside the grid (custom toolbar slot, `renderCell`) wins without `!important`.
733
+ 25. **Quick filter text** (3.7.1+): besides the raw value and any `valueFormatter` text, `singleSelect` columns match their option label, `boolean` columns Yes / No, and `date` columns the locale date shown. Exports read values through the same `valueGetter` handling as the grid (a throw exports an empty cell), and CSV / HTML-Excel escape formula text from a `valueFormatter` unless it reads as a plain number.
734
+ 26. **localeText covers every UI string (3.9+)**: ~200 optional keys (column menu, toolbar, filter / columns / pivot panels, AI panel, pagination, row and header checkboxes, overlays, announcements), English by default. An inline object of strings is fine; define function entries (`rowSelectLabel: id => …`) outside the render so rows stay memoised. Components used outside a grid show English.
735
+ 27. **Theme keys added in 3.9**: `grid.checkboxCheckedBackground`, `checkboxCheckmark`, `expandIconColor`, `expandIconHoverColor`; `overlays.itemIndicatorColor` (menu selection dot), `itemSelectedIcon`, `iconColor`, `sectionLabelText`, `dividerColor`. The demo's Theme Builder edits all of them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
5
5
  "type": "module",
6
6
  "main": "./dist/opengridx.umd.js",
File without changes