@opencorestack/opengridx 3.5.1 → 3.6.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 (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -0
  3. package/dist/ai.cjs +4 -2
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +2 -2
  6. package/dist/ai.es.js +4 -2
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +32 -0
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6573 -6390
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -16
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +14 -1
  15. package/docs/README.md +2 -0
  16. package/docs/features/ai-toolkit.md +2 -2
  17. package/docs/features/export-guide.md +2 -0
  18. package/docs/features/header-filters.md +34 -0
  19. package/docs/features/keyboard-navigation.md +2 -0
  20. package/docs/features/nextjs.md +241 -0
  21. package/docs/features/pdf-export.md +2 -0
  22. package/docs/features/pinning.md +2 -0
  23. package/docs/features/rtl.md +58 -0
  24. package/docs/performance.md +86 -118
  25. package/docs/roadmap.md +4 -2
  26. package/lib/ai/promptHandler.ts +12 -8
  27. package/lib/ai/validate.ts +5 -2
  28. package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
  29. package/lib/components/Cell/Cell.css +29 -9
  30. package/lib/components/Cell/Cell.tsx +2 -2
  31. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
  32. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
  33. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
  34. package/lib/components/DataGrid/DataGrid.css +12 -6
  35. package/lib/components/DataGrid/DataGrid.tsx +1 -0
  36. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
  37. package/lib/components/DataGrid/GridListView.tsx +3 -1
  38. package/lib/components/Header/ColumnMenu.css +1 -1
  39. package/lib/components/Header/ColumnMenu.tsx +14 -6
  40. package/lib/components/Header/Header.css +18 -10
  41. package/lib/components/Header/Header.tsx +16 -9
  42. package/lib/components/Header/headerColumnStyle.ts +3 -2
  43. package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
  44. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
  45. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
  46. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
  47. package/lib/components/ListView/ListViewRow.tsx +1 -1
  48. package/lib/components/Pagination/Pagination.css +4 -0
  49. package/lib/components/Pagination/Pagination.tsx +4 -4
  50. package/lib/components/Row/Row.tsx +3 -3
  51. package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
  52. package/lib/components/Toolbar/GridToolbar.tsx +8 -28
  53. package/lib/components/Toolbar/PivotPanel.tsx +4 -7
  54. package/lib/components/ui/ExpandIcon.css +13 -0
  55. package/lib/components/ui/ExpandIcon.tsx +2 -4
  56. package/lib/hooks/core/useGridCellSelection.ts +20 -5
  57. package/lib/hooks/core/useGridColumns.tsx +2 -2
  58. package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
  59. package/lib/hooks/core/useGridScrollSync.ts +28 -5
  60. package/lib/hooks/core/useLayout.ts +13 -6
  61. package/lib/hooks/useRowGrouping.ts +9 -9
  62. package/lib/types/index.ts +30 -0
  63. package/lib/utils/aggregation/index.ts +56 -1
  64. package/lib/utils/direction.ts +52 -0
  65. package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
  66. package/lib/utils/export/exportToPdf.ts +2 -1
  67. package/lib/utils/filtering/index.ts +58 -6
  68. package/lib/utils/headerFilterLocale.ts +37 -0
  69. package/lib/utils/scroll/index.ts +5 -3
  70. package/lib/utils/sorting/index.ts +141 -26
  71. package/lib/utils/values.ts +10 -6
  72. package/llms.txt +4 -2
  73. package/package.json +1 -1
@@ -358,8 +358,15 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
358
358
  groupSubtotalFillColor = '#f0f4ff',
359
359
  } = options;
360
360
 
361
- // Lazy-load ExcelJS to keep initial bundle lean
362
- const ExcelJS = (await import('exceljs')).default;
361
+ // Lazy-load ExcelJS to keep initial bundle lean. The import must stay inside `try`: webpack
362
+ // (Next.js 15, plain webpack 5) and Turbopack (Next.js 16) treat an unresolvable import() in a
363
+ // try block as optional, so apps without the peer still build. Outside `try` the build fails.
364
+ let ExcelJS: typeof import('exceljs');
365
+ try {
366
+ ExcelJS = (await import('exceljs')).default;
367
+ } catch {
368
+ throw new Error("exportToExcelAdvanced requires 'exceljs'. Run: npm install exceljs");
369
+ }
363
370
  const workbook = new ExcelJS.Workbook();
364
371
  workbook.creator = 'OpenGridX';
365
372
  workbook.created = new Date();
@@ -206,7 +206,8 @@ export async function runPdfExport<R extends GridRowModel>(
206
206
  };
207
207
  checkAbort();
208
208
 
209
- // Lazy-load peer deps — provides a clear error if not installed
209
+ // Lazy-load peer deps — provides a clear error if not installed. Keep the imports inside `try`:
210
+ // webpack and Turbopack only build an app without the peers when the import() sits in a try block.
210
211
  let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
211
212
  let autoTable: AutoTableFn;
212
213
  try {
@@ -255,15 +255,56 @@ function getCachedQuickFilterTexts(row: GridRowModel, columns: GridColumnLookup
255
255
  return texts;
256
256
  }
257
257
 
258
- function compileQuickFilter(values: readonly string[] | undefined, columns: GridColumnLookup | undefined): RowPredicate | null {
259
- const terms = (values ?? [])
258
+ function quickFilterTerms(values: readonly string[] | undefined): string[] {
259
+ return (values ?? [])
260
260
  .map(value => String(value ?? '').trim().toLowerCase())
261
261
  .filter(term => term !== '');
262
+ }
263
+
264
+ function textsMatchTerms(texts: readonly string[], terms: readonly string[]): boolean {
265
+ // Plain loops: this runs once per row on every keystroke.
266
+ for (let t = 0; t < terms.length; t++) {
267
+ const term = terms[t];
268
+ let found = false;
269
+ for (let i = 0; i < texts.length; i++) {
270
+ if (texts[i].includes(term)) {
271
+ found = true;
272
+ break;
273
+ }
274
+ }
275
+ if (!found) return false;
276
+ }
277
+ return true;
278
+ }
279
+
280
+ function compileQuickFilter(values: readonly string[] | undefined, columns: GridColumnLookup | undefined): RowPredicate | null {
281
+ const terms = quickFilterTerms(values);
262
282
  if (terms.length === 0) return null;
263
- return (row) => {
264
- const texts = getCachedQuickFilterTexts(row, columns);
265
- return terms.every(term => texts.some(text => text.includes(term)));
266
- };
283
+ return (row) => textsMatchTerms(getCachedQuickFilterTexts(row, columns), terms);
284
+ }
285
+
286
+ // filterRows also keeps the texts of the last rows array it searched, by position, per column
287
+ // lookup. Looking a text up by index is much cheaper than one WeakMap entry per row, and after an
288
+ // edit (a new array with mostly the same row objects at the same positions) only the rows whose
289
+ // object changed are read again.
290
+ interface QuickFilterArrayTexts {
291
+ rows: readonly GridRowModel[];
292
+ texts: string[][];
293
+ }
294
+ const quickFilterArrayCache = new WeakMap<GridColumnLookup, QuickFilterArrayTexts>();
295
+
296
+ function getQuickFilterTextsByIndex(rows: readonly GridRowModel[], columns: GridColumnLookup): string[][] {
297
+ const previous = quickFilterArrayCache.get(columns);
298
+ if (previous && previous.rows === rows) return previous.texts;
299
+ const texts = new Array<string[]>(rows.length);
300
+ for (let i = 0; i < rows.length; i++) {
301
+ const row = rows[i];
302
+ texts[i] = previous && i < previous.rows.length && previous.rows[i] === row
303
+ ? previous.texts[i]
304
+ : getQuickFilterTexts(row, columns);
305
+ }
306
+ quickFilterArrayCache.set(columns, { rows, texts });
307
+ return texts;
267
308
  }
268
309
 
269
310
  /**
@@ -333,6 +374,17 @@ export function filterRows<R extends GridRowModel>(
333
374
  filterModel: GridFilterModel,
334
375
  columns?: GridColumnLookup
335
376
  ): R[] {
377
+ const terms = columns && filterModel ? quickFilterTerms(filterModel.quickFilterValues) : [];
378
+ if (columns && terms.length > 0) {
379
+ // The same result as createRowFilter's predicate: the quick filter and then the items.
380
+ const itemsPredicate = compileGroup(filterModel.items ?? [], filterModel.logicOperator ?? 'and', { columns, warned: new Set() });
381
+ const texts = getQuickFilterTextsByIndex(rows, columns);
382
+ const result: R[] = [];
383
+ for (let i = 0; i < rows.length; i++) {
384
+ if (textsMatchTerms(texts[i], terms) && (!itemsPredicate || itemsPredicate(rows[i]))) result.push(rows[i]);
385
+ }
386
+ return result;
387
+ }
336
388
  const predicate = createRowFilter(filterModel, columns);
337
389
  if (!predicate) return rows;
338
390
  return rows.filter(row => predicate(row));
@@ -0,0 +1,37 @@
1
+ import type { GridFilterOperator, GridLocaleText } from '../types';
2
+ import { HEADER_FILTER_OPERATOR_LABELS } from './headerFilters';
3
+
4
+ /** The header filter row's strings, with the English defaults filled in. */
5
+ export interface HeaderFilterLocale {
6
+ custom: string;
7
+ customTooltip: string;
8
+ cellLabel: (header: string) => string;
9
+ valueLabel: (header: string) => string;
10
+ operatorButtonLabel: (header: string, operatorLabel: string) => string;
11
+ operatorMenuLabel: (header: string) => string;
12
+ clearLabel: (header: string) => string;
13
+ clearTooltip: string;
14
+ any: string;
15
+ yes: string;
16
+ no: string;
17
+ operator: (operator: GridFilterOperator) => string;
18
+ }
19
+
20
+ /** Resolves the header filter strings of `localeText`, falling back to English. */
21
+ export function resolveHeaderFilterLocale(localeText: GridLocaleText | undefined): HeaderFilterLocale {
22
+ const operators = localeText?.headerFilterOperators;
23
+ return {
24
+ custom: localeText?.headerFilterCustom ?? 'Custom filter',
25
+ customTooltip: localeText?.headerFilterCustomTooltip ?? 'Set in the filter panel',
26
+ cellLabel: localeText?.headerFilterCellLabel ?? (h => `Filter ${h}`),
27
+ valueLabel: localeText?.headerFilterValueLabel ?? (h => `Filter value for ${h}`),
28
+ operatorButtonLabel: localeText?.headerFilterOperatorButtonLabel ?? ((h, op) => `Filter operator for ${h}: ${op}`),
29
+ operatorMenuLabel: localeText?.headerFilterOperatorMenuLabel ?? (h => `Filter operator for ${h}`),
30
+ clearLabel: localeText?.headerFilterClearLabel ?? (h => `Clear filter for ${h}`),
31
+ clearTooltip: localeText?.headerFilterClearTooltip ?? 'Clear',
32
+ any: localeText?.headerFilterAny ?? 'Any',
33
+ yes: localeText?.headerFilterTrue ?? 'Yes',
34
+ no: localeText?.headerFilterFalse ?? 'No',
35
+ operator: op => operators?.[op] ?? HEADER_FILTER_OPERATOR_LABELS[op] ?? op,
36
+ };
37
+ }
@@ -1,3 +1,4 @@
1
+ import { getScrollStart, setScrollStart } from '../direction';
1
2
  /**
2
3
  * Height of the sticky block at the bottom of the viewport (bottom-pinned rows, their expanded
3
4
  * detail panels and the aggregation footer), measured from the DOM.
@@ -68,11 +69,12 @@ export function scrollColumnIntoView(viewport: HTMLElement, unpinnedIndex: numbe
68
69
  const colLeft = contentStart + (unpinnedIndex > 0 ? unpinnedAccWidths[unpinnedIndex - 1] : 0);
69
70
  const colRight = contentStart + unpinnedAccWidths[unpinnedIndex];
70
71
  const stickyLeft = pinnedSystemColumnsWidth + leftWidth;
71
- const { scrollLeft, clientWidth } = viewport;
72
+ const { clientWidth } = viewport;
73
+ const scrollLeft = getScrollStart(viewport);
72
74
 
73
75
  if (colLeft < scrollLeft + stickyLeft) {
74
- viewport.scrollLeft = Math.max(0, colLeft - stickyLeft);
76
+ setScrollStart(viewport, Math.max(0, colLeft - stickyLeft));
75
77
  } else if (colRight > scrollLeft + clientWidth - rightWidth) {
76
- viewport.scrollLeft = colRight - clientWidth + rightWidth;
78
+ setScrollStart(viewport, colRight - clientWidth + rightWidth);
77
79
  }
78
80
  }
@@ -154,16 +154,20 @@ export function sortRows<R extends GridRowModel>(
154
154
  if (sortModel.length === 0) {
155
155
  return rows;
156
156
  }
157
- return sortItemsBySortModel(rows, sortModel, (row, sortItem) => {
157
+ if (rows.length < 2) return [...rows];
158
+ const sortColumns = sortModel.map(sortItem => {
158
159
  const colDef = columns?.byField.get(sortItem.field);
159
- const value = getCellValue(row, sortItem.field, colDef);
160
- return {
161
- value,
162
- type: colDef?.type,
163
- comparator: colDef?.sortComparator,
164
- params: colDef?.sortComparator ? { id: getRowId(row), field: sortItem.field, row, value } : undefined,
165
- };
160
+ const comparator = colDef?.sortComparator;
161
+ if (comparator) {
162
+ return genericSortColumn(rows.map(row => {
163
+ const value = getCellValue(row, sortItem.field, colDef);
164
+ return { value, type: colDef.type, comparator, params: { id: getRowId(row), field: sortItem.field, row, value } };
165
+ }));
166
+ }
167
+ const type = colDef?.type;
168
+ return encodeSortColumn(rows.length, i => getCellValue(rows[i], sortItem.field, colDef), () => type);
166
169
  });
170
+ return sortByColumns(rows, sortModel, sortColumns);
167
171
  }
168
172
 
169
173
  /** The value an item sorts by for one sort key, and how to compare it. */
@@ -194,26 +198,137 @@ export function sortItemsBySortModel<T>(
194
198
  return [...items];
195
199
  }
196
200
 
197
- // Read and normalize every sort value once, instead of calling valueGetter and parsing dates
198
- // O(n log n) times inside the comparator.
199
- const values = sortModel.map(sortItem => items.map(item => readValue(item, sortItem)));
200
- const keys = values.map(column => column.map(({ value, type }) => toSortKey(value, type)));
201
-
202
- const order = items.map((_, index) => index);
203
- order.sort((ia, ib) => {
204
- for (let k = 0; k < sortModel.length; k++) {
205
- const a = values[k][ia];
206
- const b = values[k][ib];
207
- const ascending = a.comparator && a.comparator === b.comparator && a.params && b.params
208
- ? callSortComparator(a.comparator, a.params, b.params)
209
- : compareSortKeys(keys[k][ia], keys[k][ib]);
210
- const comparison = applySortDirection(ascending, sortModel[k].sort);
211
- if (comparison !== 0) return comparison;
212
- }
213
- return ia - ib;
201
+ // Read every sort value once, instead of calling valueGetter and parsing dates O(n log n) times
202
+ // inside the comparator.
203
+ const columns = sortModel.map(sortItem => {
204
+ const values = items.map(item => readValue(item, sortItem));
205
+ // A key where no item carries a comparator compares by the built-in order only, so it can be
206
+ // encoded up front.
207
+ return values.some(v => v.comparator && v.params)
208
+ ? genericSortColumn(values)
209
+ : encodeSortColumn(values.length, i => values[i].value, i => values[i].type);
214
210
  });
211
+ return sortByColumns(items, sortModel, columns);
212
+ }
213
+
214
+ /** One sort key, ready to compare: encoded into typed arrays, or kept as values when a sortComparator applies. */
215
+ type SortColumn =
216
+ | { kind: 'encoded'; group: Uint8Array; num: Float64Array }
217
+ | { kind: 'generic'; values: readonly GridSortValue[]; keys: readonly SortKey[] };
218
+
219
+ /** The group of the empty bucket: after every rank, as in compareSortKeys. */
220
+ const GROUP_EMPTY = RANK_OTHER + 1;
221
+
222
+ /** Give each item of `buckets` (distinct text → item indexes) its text's position in collator order. */
223
+ function rankTexts(buckets: Map<string, number[]>, num: Float64Array): void {
224
+ const distinct = [...buckets.keys()].sort(collator.compare);
225
+ // Texts the collator calls equal share a position, so they tie exactly as in compareSortKeys.
226
+ let position = 0;
227
+ for (let d = 0; d < distinct.length; d++) {
228
+ if (d > 0 && collator.compare(distinct[d - 1], distinct[d]) !== 0) position++;
229
+ for (const i of buckets.get(distinct[d]) ?? []) num[i] = position;
230
+ }
231
+ }
232
+
233
+ function addToBucket(buckets: Map<string, number[]>, text: string, index: number): void {
234
+ const bucket = buckets.get(text);
235
+ if (bucket) bucket.push(index);
236
+ else buckets.set(text, [index]);
237
+ }
238
+
239
+ /**
240
+ * Encode one key as (group, number) per item. The group is the value's rank, with the empty bucket
241
+ * last; the number is the value itself for numbers, dates and booleans, and for strings (and other
242
+ * values) the position of its text among the key's distinct texts in collator order. Comparing
243
+ * (group, number) gives exactly compareSortKeys' order, while the collator runs only to sort the
244
+ * distinct texts instead of in every row comparison.
245
+ */
246
+ function encodeSortColumn(count: number, valueAt: (i: number) => unknown, typeAt: (i: number) => GridColDef['type']): SortColumn {
247
+ const group = new Uint8Array(count);
248
+ const num = new Float64Array(count);
249
+ const strings = new Map<string, number[]>();
250
+ const others = new Map<string, number[]>();
251
+ for (let i = 0; i < count; i++) {
252
+ const value = valueAt(i);
253
+ const type = typeAt(i);
254
+ if (typeof value === 'number' && type !== 'date') {
255
+ // toSortKey for a number, without the allocation.
256
+ if (Number.isNaN(value)) {
257
+ group[i] = GROUP_EMPTY;
258
+ } else {
259
+ group[i] = RANK_NUMBER;
260
+ num[i] = value;
261
+ }
262
+ continue;
263
+ }
264
+ const key = toSortKey(value, type);
265
+ if (key === null) {
266
+ group[i] = GROUP_EMPTY;
267
+ } else {
268
+ group[i] = key.rank;
269
+ if (key.rank === RANK_STRING) addToBucket(strings, key.str, i);
270
+ else if (key.rank === RANK_OTHER) addToBucket(others, key.str, i);
271
+ else num[i] = key.num;
272
+ }
273
+ }
274
+ rankTexts(strings, num);
275
+ rankTexts(others, num);
276
+ return { kind: 'encoded', group, num };
277
+ }
278
+
279
+ function genericSortColumn(values: readonly GridSortValue[]): SortColumn {
280
+ return { kind: 'generic', values, keys: values.map(({ value, type }) => toSortKey(value, type)) };
281
+ }
282
+
283
+ /** Ascending comparison of items `ia` and `ib` on one key. */
284
+ function compareInColumn(column: SortColumn, ia: number, ib: number): number {
285
+ if (column.kind === 'encoded') {
286
+ const ga = column.group[ia];
287
+ const gb = column.group[ib];
288
+ if (ga !== gb) return ga - gb;
289
+ const na = column.num[ia];
290
+ const nb = column.num[ib];
291
+ return na < nb ? -1 : na > nb ? 1 : 0;
292
+ }
293
+ const a = column.values[ia];
294
+ const b = column.values[ib];
295
+ return a.comparator && a.comparator === b.comparator && a.params && b.params
296
+ ? callSortComparator(a.comparator, a.params, b.params)
297
+ : compareSortKeys(column.keys[ia], column.keys[ib]);
298
+ }
215
299
 
216
- return order.map(index => items[index]);
300
+ /** Stable sort by prepared keys: the first key that differs decides, ties keep the input order. */
301
+ function sortByColumns<T>(items: readonly T[], sortModel: readonly GridSortItem[], columns: readonly SortColumn[]): T[] {
302
+ const n = items.length;
303
+ const order = new Array<number>(n);
304
+ for (let i = 0; i < n; i++) order[i] = i;
305
+ const first = columns[0];
306
+ if (columns.length === 1 && first.kind === 'encoded') {
307
+ // The common single-key case, without the per-comparison key loop.
308
+ const { group, num } = first;
309
+ const sign = sortModel[0].sort === 'asc' ? 1 : -1;
310
+ order.sort((ia, ib) => {
311
+ const ga = group[ia];
312
+ const gb = group[ib];
313
+ if (ga !== gb) return (ga - gb) * sign;
314
+ const na = num[ia];
315
+ const nb = num[ib];
316
+ if (na < nb) return -sign;
317
+ if (na > nb) return sign;
318
+ return ia - ib;
319
+ });
320
+ } else {
321
+ order.sort((ia, ib) => {
322
+ for (let k = 0; k < columns.length; k++) {
323
+ const comparison = applySortDirection(compareInColumn(columns[k], ia, ib), sortModel[k].sort);
324
+ if (comparison !== 0) return comparison;
325
+ }
326
+ return ia - ib;
327
+ });
328
+ }
329
+ const result = new Array<T>(n);
330
+ for (let i = 0; i < n; i++) result[i] = items[order[i]];
331
+ return result;
217
332
  }
218
333
 
219
334
  /**
@@ -53,6 +53,9 @@ export function tryGetCellValue<R extends GridRowModel>(
53
53
  * per column), so one bad record cannot take the whole grid down.
54
54
  */
55
55
  export function getCellValue<R extends GridRowModel>(row: R, field: string, colDef?: ValueGetterColumn<R>): unknown {
56
+ // Plain columns read the field directly: this runs once per row and column in every
57
+ // whole-dataset pass (sort, filter, grouping, aggregation), so it must not allocate.
58
+ if (!colDef?.valueGetter) return row[field];
56
59
  const { value, error } = tryGetCellValue(row, field, colDef);
57
60
  if (error !== undefined) {
58
61
  warnCallbackFailure(`valueGetter:${field}`, `valueGetter for column "${field}" threw; the value is read as undefined`, error);
@@ -72,12 +75,13 @@ export function getFormattedValue<R extends GridRowModel>(
72
75
  ): string | undefined {
73
76
  const formatter = colDef?.valueFormatter;
74
77
  if (!formatter) return undefined;
75
- return callSafely(
76
- () => formatter({ value, row, field }),
77
- undefined,
78
- `valueFormatter:${field}`,
79
- `valueFormatter for column "${field}" threw; the value is shown unformatted`,
80
- );
78
+ // callSafely inlined: this runs for every cell of the quick filter's first search.
79
+ try {
80
+ return formatter({ value, row, field });
81
+ } catch (error) {
82
+ warnCallbackFailure(`valueFormatter:${field}`, `valueFormatter for column "${field}" threw; the value is shown unformatted`, error);
83
+ return undefined;
84
+ }
81
85
  }
82
86
 
83
87
  /**
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 3.5.1
3
+ # Version: 3.6.0
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -75,7 +75,7 @@ export default function App() {
75
75
  | autoHeight | boolean | false | Fit height to rows (renders every row) |
76
76
  | overscanRowCount | number | 3 | Minimum rows rendered outside the viewport (raised automatically while scrolling fast) |
77
77
  | ariaLabel | string | — | Accessible name for the grid |
78
- | localeText | GridLocaleText | — | Override pager / no-rows strings: paginationRowsPerPage, paginationOf(from,to,count), paginationPage(page,count), noRowsLabel |
78
+ | localeText | GridLocaleText | — | Override strings: paginationRowsPerPage, paginationOf(from,to,count), paginationPage(page,count), noRowsLabel, cellSelectionAnnouncement, aiAssistantButton; header filter row (3.6): headerFilterCustom, headerFilterCustomTooltip, headerFilterCellLabel(h), headerFilterValueLabel(h), headerFilterOperatorButtonLabel(h, op), headerFilterOperatorMenuLabel(h), headerFilterClearLabel(h), headerFilterClearTooltip, headerFilterAny, headerFilterTrue, headerFilterFalse, headerFilterOperators (Partial<Record<GridFilterOperator,string>>) |
79
79
  | rowHeight | number | 52 | Row height in pixels |
80
80
  | headerHeight | number | 56 | Header height in pixels |
81
81
  | wrapHeaderText | boolean | false | Wrap header titles within headerHeight (2 lines at 56px, 3 at 72, 4 at 88; ellipsis on the last line). Header never grows: raise headerHeight for more lines. Class ogx__header-cell--wrap. Per column: GridColDef.wrapHeaderText (3.3.0+) |
@@ -725,3 +725,5 @@ Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, Pa
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
+ 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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "3.5.1",
3
+ "version": "3.6.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",