@opencorestack/opengridx 3.7.1 → 3.8.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 (59) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/ai.cjs.map +1 -1
  3. package/dist/ai.es.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/opengridx.css +1 -1
  6. package/dist/opengridx.es.js +6043 -5979
  7. package/dist/opengridx.es.js.map +1 -1
  8. package/dist/opengridx.umd.js +16 -16
  9. package/dist/opengridx.umd.js.map +1 -1
  10. package/docs/API_REFERENCE.md +28 -0
  11. package/docs/features/filtering.md +2 -0
  12. package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
  13. package/lib/components/Cell/Cell.css +1 -3
  14. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
  15. package/lib/components/DataGrid/DataGrid.css +1 -28
  16. package/lib/components/DataGrid/DataGrid.tsx +2 -4
  17. package/lib/components/DataGrid/GridListView.tsx +1 -1
  18. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  19. package/lib/components/DataGrid/GridVirtualRows.tsx +3 -2
  20. package/lib/components/FilterPanel/FilterPanel.tsx +7 -39
  21. package/lib/components/Header/Header.css +2 -11
  22. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
  23. package/lib/components/ListView/ListView.css +0 -16
  24. package/lib/components/Row/Row.tsx +4 -1
  25. package/lib/hooks/core/useDataGrid.ts +40 -35
  26. package/lib/hooks/core/useGridAggregationApi.ts +1 -1
  27. package/lib/hooks/core/useGridCellSelection.ts +3 -4
  28. package/lib/hooks/core/useGridClipboardPaste.ts +1 -1
  29. package/lib/hooks/core/useGridDetailPanel.ts +1 -1
  30. package/lib/hooks/core/useGridFillHandle.ts +1 -1
  31. package/lib/hooks/core/useGridHierarchy.ts +2 -2
  32. package/lib/hooks/core/useGridKeyboardNavigation.ts +1 -1
  33. package/lib/hooks/core/useGridLiveRowSelection.ts +9 -2
  34. package/lib/hooks/core/useGridRangeEditing.ts +1 -1
  35. package/lib/hooks/core/useGridRowInteractions.ts +3 -2
  36. package/lib/hooks/features/useGridSpanning.ts +4 -4
  37. package/lib/hooks/useRowGrouping.ts +19 -10
  38. package/lib/hooks/useRowReorder.ts +9 -3
  39. package/lib/styles/base/reset.css +6 -7
  40. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
  41. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
  42. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
  43. package/lib/types/rows.ts +0 -2
  44. package/lib/types/state.ts +9 -0
  45. package/lib/utils/cellSelection.ts +0 -2
  46. package/lib/utils/editing/index.ts +1 -1
  47. package/lib/utils/editing/targets.ts +2 -3
  48. package/lib/utils/filtering/filterInput.ts +45 -0
  49. package/lib/utils/filtering/index.ts +0 -28
  50. package/lib/utils/gridApiRows.ts +1 -1
  51. package/lib/utils/grouping/groupedExportRows.ts +1 -1
  52. package/lib/utils/grouping/layeredMap.ts +52 -0
  53. package/lib/utils/pinning/index.ts +0 -107
  54. package/lib/utils/rowIndex.ts +67 -0
  55. package/lib/utils/sorting/index.ts +42 -33
  56. package/lib/utils/values.ts +5 -1
  57. package/llms.txt +7 -3
  58. package/package.json +1 -1
  59. package/lib/theme/defaultStyles.ts +0 -0
package/lib/types/rows.ts CHANGED
@@ -5,8 +5,6 @@ export type GridRowId = string | number;
5
5
  export interface GridRowModel {
6
6
  /** Uniquely identifies the row. */
7
7
  id: GridRowId;
8
- /** Internal: row is a skeleton placeholder during infinite-scroll loading. */
9
- _isSkeleton?: boolean;
10
8
  /** Internal: server-reported child count for lazy-loaded tree nodes. */
11
9
  serverChildrenCount?: number;
12
10
  /** Any other dynamic row properties. */
@@ -2,11 +2,20 @@ import type { GridRowId, GridRowModel, GridPinnedRows } from './rows';
2
2
  import type { GridColDef, GridColumnVisibilityModel, GridPinnedColumns } from './columns';
3
3
 
4
4
  export interface GridInternalState {
5
+ /**
6
+ * The row store. Cell edits (`REPLACE_ROW` / `REPLACE_ROWS`) update `idRowsLookup`, `idByRow`
7
+ * and `indexById` IN PLACE and only replace `rows`: never rely on those maps changing identity
8
+ * to detect an edit; depend on `rows` (or `state.rows`) instead. `SET_ROWS` builds new maps.
9
+ */
5
10
  rows: {
6
11
  idRowsLookup: Map<GridRowId, GridRowModel>;
7
12
  allRows: GridRowId[];
8
13
  /** Row object → its getRowId() key. Rows are stored untouched, so this is how ids are resolved. */
9
14
  idByRow: Map<GridRowModel, GridRowId>;
15
+ /** The stored rows in `allRows` order. */
16
+ rows: GridRowModel[];
17
+ /** Row id → its index in `allRows` / `rows`. */
18
+ indexById: Map<GridRowId, number>;
10
19
  };
11
20
  pagination: {
12
21
  /** Server-reported total (dataSource responses); undefined until one arrives. */
@@ -17,8 +17,6 @@ export function isRangeColumnField(field: string): boolean {
17
17
  export const isSameCoordinates = (a: GridCellCoordinates | null | undefined, b: GridCellCoordinates | null | undefined): boolean =>
18
18
  a === b || (!!a && !!b && a.id === b.id && a.field === b.field);
19
19
 
20
- export const isSingleCellRange = (range: GridCellRange): boolean => isSameCoordinates(range.anchor, range.head);
21
-
22
20
  /** A model holding the same range(s) as `b`. */
23
21
  export function isSameCellSelectionModel(a: GridCellSelectionModel, b: GridCellSelectionModel): boolean {
24
22
  if (a === b) return true;
@@ -20,7 +20,7 @@ export function resolveCellEditable<R extends GridRowModel>(
20
20
  ): boolean {
21
21
  const { row, field, value, colDef, rowIndex, colIndex, rowMeta } = params;
22
22
  if (!colDef.editable) return false;
23
- if (rowMeta?.isGroupRow || row._isSkeleton) return false;
23
+ if (rowMeta?.isGroupRow) return false;
24
24
  if (!isCellEditable) return true;
25
25
  try {
26
26
  return Boolean(isCellEditable({ row, field, value, colDef, rowIndex, colIndex }));
@@ -5,6 +5,7 @@ import { isSystemField } from '../focus';
5
5
  import { isSyntheticRowId } from '../syntheticRows';
6
6
  import { resolveCellEditable } from './index';
7
7
  import { getCellValue } from '../values';
8
+ import { getRowIndex } from '../rowIndex';
8
9
 
9
10
  /**
10
11
  * Where batch edits (paste, range clear, undo/redo) land: the renderable rows and the range columns
@@ -26,9 +27,7 @@ export function buildEditTargetContext<R extends GridRowModel>(
26
27
  getRowId: (row: R) => GridRowId,
27
28
  navigationColumns: ReadonlyArray<GridColDef<R>>,
28
29
  ): GridEditTargetContext<R> {
29
- const rowIds = rows.map(getRowId);
30
- const rowIndexById = new Map<GridRowId, number>();
31
- rowIds.forEach((id, index) => { if (!rowIndexById.has(id)) rowIndexById.set(id, index); });
30
+ const { rowIds, rowIndexById } = getRowIndex(rows, getRowId, 'renderable');
32
31
  const columns = navigationColumns.filter(c => isRangeColumnField(c.field));
33
32
  const colIndexByField = new Map<string, number>();
34
33
  columns.forEach((c, index) => { colIndexByField.set(c.field, index); });
@@ -0,0 +1,45 @@
1
+ import type { GridColDef, GridFilterOperator } from '../../types';
2
+
3
+ /**
4
+ * Helpers shared by the filter panel and the header filter row: both edit a filter item's value
5
+ * through a text box or a select of the column's `valueOptions`.
6
+ */
7
+
8
+ /** One entry of a `valueOptions` select. `key` is the option's index, used as the `<option>` value. */
9
+ export interface FilterValueOption {
10
+ key: string;
11
+ value: unknown;
12
+ label: string;
13
+ }
14
+
15
+ export function getFilterValueOptions(col: Pick<GridColDef, 'valueOptions'>): FilterValueOption[] {
16
+ return (col.valueOptions ?? []).map((option, index) => (
17
+ option !== null && typeof option === 'object'
18
+ ? { key: String(index), value: option.value, label: option.label }
19
+ : { key: String(index), value: option, label: String(option) }
20
+ ));
21
+ }
22
+
23
+ /**
24
+ * Whether a filter value selects this option: the same value, or the same text ignoring case (a
25
+ * value typed or restored from a URL is a string). A list matches by its first entry.
26
+ */
27
+ export function matchesFilterValueOption(option: FilterValueOption, value: unknown): boolean {
28
+ const single = Array.isArray(value) ? value[0] : value;
29
+ if (Object.is(option.value, single)) return true;
30
+ return single != null && String(option.value).toLowerCase() === String(single).toLowerCase();
31
+ }
32
+
33
+ /** The item value for typed text. `isAnyOf` takes a comma-separated list. */
34
+ export function filterValueFromInputText(text: string, operator: GridFilterOperator): unknown {
35
+ if (operator === 'isAnyOf') return text.split(',').map(part => part.trim()).filter(part => part !== '');
36
+ return text;
37
+ }
38
+
39
+ /** Equal filter values: the same value, dates at the same time, or lists of equal values. */
40
+ export function isSameFilterValue(a: unknown, b: unknown): boolean {
41
+ if (Object.is(a, b)) return true;
42
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
43
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => isSameFilterValue(v, b[i]));
44
+ return false;
45
+ }
@@ -362,19 +362,6 @@ export function applyQuickFilter<R extends GridRowModel>(
362
362
  return predicate ? predicate(row) : true;
363
363
  }
364
364
 
365
- /**
366
- * Check if a row matches the filter model. Compiles the model on every call: to test many rows,
367
- * build a predicate once with createRowFilter.
368
- */
369
- export function isRowMatchingFilter<R extends GridRowModel>(
370
- row: R,
371
- filterModel: GridFilterModel | GridFilterGroup,
372
- columns?: GridColumnLookup
373
- ): boolean {
374
- const predicate = createRowFilter(filterModel, columns);
375
- return predicate ? predicate(row) : true;
376
- }
377
-
378
365
  export function filterRows<R extends GridRowModel>(
379
366
  rows: R[],
380
367
  filterModel: GridFilterModel,
@@ -396,21 +383,6 @@ export function filterRows<R extends GridRowModel>(
396
383
  return rows.filter(row => predicate(row));
397
384
  }
398
385
 
399
- export function getDefaultOperator(type?: string): GridFilterOperator {
400
- switch (type) {
401
- case 'number':
402
- return '=';
403
- case 'date':
404
- return 'is';
405
- case 'boolean':
406
- return 'is';
407
- case 'singleSelect':
408
- return 'isAnyOf';
409
- default:
410
- return 'contains';
411
- }
412
- }
413
-
414
386
  export function getOperatorsForType(type?: string): GridFilterOperator[] {
415
387
  switch (type) {
416
388
  case 'number':
@@ -9,7 +9,7 @@ export interface CollectAllFilteredRowsParams<R extends GridRowModel> {
9
9
  /** Every unpinned row that passes the filter, sorted, across all pages. */
10
10
  sortedUnpinnedRows: R[];
11
11
  pinnedBottomRows: R[];
12
- rowMetaMap: Map<GridRowId, GridRowMeta>;
12
+ rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
13
13
  getRowId: (row: GridRowModel) => GridRowId;
14
14
  /**
15
15
  * With a hierarchy: the rows that pass the filter (the set select-all and the footer use).
@@ -16,7 +16,7 @@ export interface BuildGroupedExportRowsParams<R extends GridRowModel> {
16
16
  */
17
17
  rows: R[];
18
18
  getRowId: (row: R) => GridRowId;
19
- rowMetaMap: Map<GridRowId, GridRowMeta>;
19
+ rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
20
20
  columns: GridColDef<R>[];
21
21
  aggregationModel: GridAggregationModel;
22
22
  /** The footer's grand total (already over the filtered data rows); omitted when null. */
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A read-only view of two maps with disjoint keys, read as one: `top` first, then `base`. Row
3
+ * grouping keeps one entry per leaf row in `base` (built once per tree) and the group rows in
4
+ * `top` (rebuilt on every expand / collapse), so toggling a group at 1M rows does not copy a
5
+ * 1M-entry map. Iteration yields `base` entries, then `top` entries.
6
+ */
7
+ export class LayeredMap<K, V> implements ReadonlyMap<K, V> {
8
+ private readonly base: ReadonlyMap<K, V>;
9
+ private readonly top: ReadonlyMap<K, V>;
10
+
11
+ constructor(base: ReadonlyMap<K, V>, top: ReadonlyMap<K, V>) {
12
+ this.base = base;
13
+ this.top = top;
14
+ }
15
+
16
+ get size(): number {
17
+ return this.base.size + this.top.size;
18
+ }
19
+
20
+ get(key: K): V | undefined {
21
+ const value = this.top.get(key);
22
+ return value !== undefined || this.top.has(key) ? value : this.base.get(key);
23
+ }
24
+
25
+ has(key: K): boolean {
26
+ return this.top.has(key) || this.base.has(key);
27
+ }
28
+
29
+ forEach(callback: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void {
30
+ this.base.forEach((value, key) => callback.call(thisArg, value, key, this));
31
+ this.top.forEach((value, key) => callback.call(thisArg, value, key, this));
32
+ }
33
+
34
+ *entries(): MapIterator<[K, V]> {
35
+ yield* this.base.entries();
36
+ yield* this.top.entries();
37
+ }
38
+
39
+ *keys(): MapIterator<K> {
40
+ yield* this.base.keys();
41
+ yield* this.top.keys();
42
+ }
43
+
44
+ *values(): MapIterator<V> {
45
+ yield* this.base.values();
46
+ yield* this.top.values();
47
+ }
48
+
49
+ [Symbol.iterator](): MapIterator<[K, V]> {
50
+ return this.entries();
51
+ }
52
+ }
@@ -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.
@@ -0,0 +1,67 @@
1
+ import type { GridRowId } from '../types';
2
+
3
+ /** The ids of a row array in order, and each id's first index. */
4
+ export interface GridRowIndex {
5
+ rowIds: GridRowId[];
6
+ rowIndexById: Map<GridRowId, number>;
7
+ }
8
+
9
+ interface CacheEntry {
10
+ rows: readonly object[];
11
+ value: GridRowIndex;
12
+ }
13
+
14
+ // Last result per id resolver and view. The grid's resolver keeps its identity while the row store
15
+ // keeps its id maps (cell edits do), so the entry lives exactly as long as it can be reused.
16
+ const cache = new WeakMap<(row: never) => GridRowId, Map<string, CacheEntry>>();
17
+
18
+ function build<R extends object>(rows: readonly R[], getRowId: (row: R) => GridRowId): GridRowIndex {
19
+ const rowIds = new Array<GridRowId>(rows.length);
20
+ const rowIndexById = new Map<GridRowId, number>();
21
+ for (let index = 0; index < rows.length; index++) {
22
+ const id = getRowId(rows[index]);
23
+ rowIds[index] = id;
24
+ if (!rowIndexById.has(id)) rowIndexById.set(id, index);
25
+ }
26
+ return { rowIds, rowIndexById };
27
+ }
28
+
29
+ /**
30
+ * `rows.map(getRowId)` plus an id → first index map, reusing the previous result for the same
31
+ * resolver and `view` when the ids are unchanged position by position. A cell edit replaces row
32
+ * objects but never moves or re-keys rows, so after an edit only the replaced positions are
33
+ * resolved (a reference comparison per row instead of a map lookup per row, ~20× cheaper at 1M
34
+ * rows), and callers memoized on the returned object do not recompute. Treat the result as
35
+ * read-only: it may be shared.
36
+ */
37
+ export function getRowIndex<R extends object>(
38
+ rows: readonly R[],
39
+ getRowId: (row: R) => GridRowId,
40
+ view: string,
41
+ ): GridRowIndex {
42
+ let byView = cache.get(getRowId);
43
+ const previous = byView?.get(view);
44
+ if (previous && previous.rows.length === rows.length) {
45
+ const prevRows = previous.rows as readonly R[];
46
+ const prevIds = previous.value.rowIds;
47
+ let same = true;
48
+ for (let index = 0; index < rows.length; index++) {
49
+ const row = rows[index];
50
+ if (row !== prevRows[index] && getRowId(row) !== prevIds[index]) {
51
+ same = false;
52
+ break;
53
+ }
54
+ }
55
+ if (same) {
56
+ previous.rows = rows;
57
+ return previous.value;
58
+ }
59
+ }
60
+ const value = build(rows, getRowId);
61
+ if (!byView) {
62
+ byView = new Map();
63
+ cache.set(getRowId, byView);
64
+ }
65
+ byView.set(view, { rows, value });
66
+ return value;
67
+ }
@@ -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.8.1
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -721,9 +721,13 @@ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "3.7.1",
3
+ "version": "3.8.1",
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