@opencorestack/opengridx 3.6.0 → 3.7.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 (74) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/ai.cjs +3 -3
  3. package/dist/ai.cjs.map +1 -1
  4. package/dist/ai.es.js +3 -3
  5. package/dist/ai.es.js.map +1 -1
  6. package/dist/index.d.ts +30 -0
  7. package/dist/opengridx.css +1 -1
  8. package/dist/opengridx.es.js +8756 -8244
  9. package/dist/opengridx.es.js.map +1 -1
  10. package/dist/opengridx.umd.js +15 -15
  11. package/dist/opengridx.umd.js.map +1 -1
  12. package/docs/API_REFERENCE.md +3 -2
  13. package/docs/architecture/datagrid-orchestration.md +46 -29
  14. package/docs/architecture/grid-row-meta.md +1 -1
  15. package/docs/customization/slots-api.md +1 -1
  16. package/docs/customization/theming.md +43 -0
  17. package/docs/features/virtualization.md +16 -2
  18. package/docs/performance.md +3 -1
  19. package/lib/components/AiAssistant/GridAiAssistantPanel.css +3 -1
  20. package/lib/components/Cell/Cell.css +4 -2
  21. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
  22. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +3 -1
  23. package/lib/components/DataGrid/DataGrid.css +9 -12
  24. package/lib/components/DataGrid/DataGrid.tsx +163 -671
  25. package/lib/components/DataGrid/GridRoot.tsx +239 -0
  26. package/lib/components/DataGrid/GridViewport.tsx +314 -0
  27. package/lib/components/FilterPanel/FilterPanel.css +9 -8
  28. package/lib/components/Header/ColumnMenu.css +4 -3
  29. package/lib/components/Header/Header.css +5 -4
  30. package/lib/components/HeaderFilter/HeaderFilter.css +1 -1
  31. package/lib/components/ListView/ListView.css +4 -4
  32. package/lib/components/Pagination/Pagination.css +1 -1
  33. package/lib/components/Row/Row.css +2 -2
  34. package/lib/components/Toolbar/GridToolbar.css +8 -7
  35. package/lib/components/Toolbar/PivotPanel.css +3 -3
  36. package/lib/hooks/core/useGridDevWarnings.ts +3 -1
  37. package/lib/hooks/core/useGridEditPipeline.ts +55 -0
  38. package/lib/hooks/core/useGridRangeEditing.ts +129 -0
  39. package/lib/hooks/core/useGridRenderWindow.ts +66 -0
  40. package/lib/hooks/core/useGridScrollViewport.ts +42 -0
  41. package/lib/hooks/core/useGridSelectionAndStateApi.ts +61 -0
  42. package/lib/hooks/core/useGridToolbarStack.ts +49 -0
  43. package/lib/hooks/core/useGridViewApis.ts +46 -0
  44. package/lib/hooks/core/useLayout.ts +34 -24
  45. package/lib/styles/base/reset.css +5 -3
  46. package/lib/styles/base/variables.css +12 -0
  47. package/lib/theme/DataGridThemeProvider.tsx +15 -0
  48. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1503 -0
  49. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1503 -0
  50. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1503 -0
  51. package/lib/theme/types.ts +31 -0
  52. package/lib/types/ai.ts +109 -0
  53. package/lib/types/api.ts +169 -0
  54. package/lib/types/columns.ts +341 -0
  55. package/lib/types/dataSource.ts +40 -0
  56. package/lib/types/editing.ts +77 -0
  57. package/lib/types/export.ts +164 -0
  58. package/lib/types/filtering.ts +52 -0
  59. package/lib/types/grouping.ts +27 -0
  60. package/lib/types/index.ts +18 -1702
  61. package/lib/types/locale.ts +51 -0
  62. package/lib/types/props.ts +405 -0
  63. package/lib/types/rows.ts +132 -0
  64. package/lib/types/selection.ts +66 -0
  65. package/lib/types/slots.ts +56 -0
  66. package/lib/types/sorting.ts +25 -0
  67. package/lib/types/state.ts +35 -0
  68. package/lib/utils/export/exportShared.ts +22 -4
  69. package/lib/utils/export/index.ts +8 -4
  70. package/lib/utils/filtering/index.ts +8 -2
  71. package/lib/utils/parsing.ts +24 -1
  72. package/lib/utils/values.ts +31 -0
  73. package/llms.txt +1 -1
  74. package/package.json +4 -2
@@ -0,0 +1,56 @@
1
+ import type { GridToolbarProps } from '../components/Toolbar/GridToolbar';
2
+ import type { PaginationProps } from '../components/Pagination/Pagination';
3
+ import type { GridPaginationModel } from './dataSource';
4
+ import type { GridAggregationModel, GridAggregationResult } from './grouping';
5
+ import type { DataGridProps } from './props';
6
+ import type { GridApi } from './api';
7
+
8
+ /**
9
+ * Props the grid passes to `slots.toolbar`, before `slotProps.toolbar` is merged over them: the
10
+ * `GridToolbarProps` it controls (columns, models and their change handlers) plus `apiRef`.
11
+ * @since v3.0
12
+ */
13
+ export interface GridToolbarSlotProps extends GridToolbarProps {
14
+ apiRef: React.MutableRefObject<GridApi>;
15
+ }
16
+
17
+ /**
18
+ * Props the grid passes to `slots.pagination`, before `slotProps.pagination` is merged over them.
19
+ * @since v3.0
20
+ */
21
+ export type GridPaginationSlotProps = PaginationProps;
22
+
23
+ /**
24
+ * Props of `slots.noRowsOverlay` and `slots.loadingOverlay`: the grid passes none of its own, only
25
+ * `slotProps.noRowsOverlay` / `slotProps.loadingOverlay`.
26
+ * @since v3.0
27
+ */
28
+ export type GridOverlaySlotProps = Record<string, unknown>;
29
+
30
+ /**
31
+ * Props the grid passes to `slots.footer`, before `slotProps.footer` is merged over them.
32
+ * @since v3.0
33
+ */
34
+ export interface GridFooterSlotProps {
35
+ apiRef: React.MutableRefObject<GridApi>;
36
+ /** The active aggregation model (`{}` when none). */
37
+ aggregationModel: GridAggregationModel;
38
+ /** The grand-total aggregation result, or `null` when no aggregation is active. */
39
+ aggregationResult: GridAggregationResult | null;
40
+ /**
41
+ * The built-in pager's count: rows after filtering, pinned rows excluded (`rowCount` from the
42
+ * server in server pagination mode). Under tree data / row grouping: the filtered data rows.
43
+ */
44
+ rowCount: number;
45
+ /** Whether pagination is in effect (false under row grouping and infinite scroll). */
46
+ pagination: boolean;
47
+ paginationModel: GridPaginationModel;
48
+ pageSizeOptions: number[];
49
+ onPaginationModelChange: (model: GridPaginationModel) => void;
50
+ }
51
+
52
+ /** The `slots` prop of `DataGrid`: components that replace built-in parts of the grid. */
53
+ export type GridSlots = NonNullable<DataGridProps['slots']>;
54
+
55
+ /** The `slotProps` prop of `DataGrid`: props for each slot, keyed like `GridSlots`. */
56
+ export type GridSlotProps = NonNullable<DataGridProps['slotProps']>;
@@ -0,0 +1,25 @@
1
+ import type { GridRowId, GridRowModel, GridValidRowModel } from './rows';
2
+
3
+ export type GridSortDirection = 'asc' | 'desc' | null;
4
+
5
+ /**
6
+ * Parameters passed to `GridColDef.sortComparator` for each side of a comparison. For a row-grouping
7
+ * group row, `row` is the synthetic group row and `value` its grouping value.
8
+ * @since v3.1
9
+ */
10
+ export interface GridSortCellParams<R extends GridValidRowModel = GridRowModel> {
11
+ id: GridRowId;
12
+ field: string;
13
+ row: R;
14
+ value: unknown;
15
+ }
16
+
17
+ export interface GridSortModel {
18
+ field: string;
19
+ sort: GridSortDirection;
20
+ }
21
+
22
+ export interface GridSortItem {
23
+ field: string;
24
+ sort: 'asc' | 'desc';
25
+ }
@@ -0,0 +1,35 @@
1
+ import type { GridRowId, GridRowModel, GridPinnedRows } from './rows';
2
+ import type { GridColDef, GridColumnVisibilityModel, GridPinnedColumns } from './columns';
3
+
4
+ export interface GridInternalState {
5
+ rows: {
6
+ idRowsLookup: Map<GridRowId, GridRowModel>;
7
+ allRows: GridRowId[];
8
+ /** Row object → its getRowId() key. Rows are stored untouched, so this is how ids are resolved. */
9
+ idByRow: Map<GridRowModel, GridRowId>;
10
+ };
11
+ pagination: {
12
+ /** Server-reported total (dataSource responses); undefined until one arrives. */
13
+ rowCount?: number;
14
+ };
15
+ columns: {
16
+ all: GridColDef[];
17
+ lookup: Map<string, GridColDef>;
18
+ orderedFields: string[];
19
+ columnVisibilityModel: GridColumnVisibilityModel;
20
+ };
21
+ pinning: {
22
+ pinnedColumns: GridPinnedColumns;
23
+ pinnedRows: GridPinnedRows;
24
+ };
25
+ dimensions: {
26
+ rowHeight: number;
27
+ headerHeight: number;
28
+ viewportWidth: number;
29
+ viewportHeight: number;
30
+ };
31
+ dataSource: {
32
+ loading: boolean;
33
+ error: unknown;
34
+ };
35
+ }
@@ -1,6 +1,6 @@
1
1
  import type { GridAggregationModel, GridColDef, GridGroupedExportRow, GridRowId, GridRowModel } from '../../types';
2
2
  import { computeAggregations, formatAggregateForColumn } from '../aggregation';
3
- import { formatValueByType } from '../values';
3
+ import { formatValueByType, getCellValue } from '../values';
4
4
  import { isSystemField } from '../focus';
5
5
 
6
6
  /**
@@ -77,10 +77,12 @@ export function aggregationForExport<R extends GridRowModel>(
77
77
  return computeAggregations(exportedRows, aggregationModel, lookup, 'export');
78
78
  }
79
79
 
80
- /** The cell's raw value: `row[field]`, then the column's `valueGetter`. */
80
+ /**
81
+ * The cell's raw value, read like the grid reads it (`getCellValue`): a `valueGetter` that throws
82
+ * exports as an empty value instead of aborting the export.
83
+ */
81
84
  export function getRawExportValue<R extends GridRowModel>(row: R, col: GridColDef<R>): unknown {
82
- const value: unknown = (row as GridRowModel)[col.field];
83
- return col.valueGetter ? col.valueGetter({ row, field: col.field, value }) : value;
85
+ return getCellValue(row, col.field, col);
84
86
  }
85
87
 
86
88
  /**
@@ -159,6 +161,22 @@ export function neutralizeFormula(text: string, fromNonText = false): string {
159
161
  return `'${text}`;
160
162
  }
161
163
 
164
+ /**
165
+ * Text that reads as a formatted number (digits, signs, separators, brackets, %, currency symbols),
166
+ * which no spreadsheet runs as a formula. One character class: linear on any input.
167
+ */
168
+ const NUMERIC_DISPLAY = /^[\d\s.,'%+\-()\p{Sc}]*$/u;
169
+
170
+ /**
171
+ * Whether a data cell's text may skip formula escaping: the grid formatted a number, date or
172
+ * boolean itself, or a `valueFormatter` turned such a value into plain numeric text ("-$3").
173
+ * Anything else a formatter writes is escaped like text, since it may come from user data.
174
+ */
175
+ export function isTrustedNonText(raw: unknown, text: string, hasFormatter: boolean): boolean {
176
+ if (!isNonTextValue(raw)) return false;
177
+ return !hasFormatter || NUMERIC_DISPLAY.test(text);
178
+ }
179
+
162
180
  /** Whether a raw value is a number, bigint, boolean or Date, whose formatted text is not user-typed. */
163
181
  export function isNonTextValue(value: unknown): boolean {
164
182
  return typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean' || value instanceof Date;
@@ -10,6 +10,7 @@ import {
10
10
  getRawExportValue,
11
11
  hasSelection,
12
12
  isNonTextValue,
13
+ isTrustedNonText,
13
14
  neutralizeFormula,
14
15
  pickExportedAggregates,
15
16
  rowsForExport,
@@ -55,11 +56,14 @@ export interface CsvExportOptions<R extends GridValidRowModel = GridRowModel> {
55
56
  interface ExportCell {
56
57
  text: string;
57
58
  raw: unknown;
59
+ /** The text came from the grid's own formatting of a number, date or boolean (see `isTrustedNonText`). */
60
+ fromNonText: boolean;
58
61
  }
59
62
 
60
63
  function exportCell<R extends GridRowModel>(row: R, col: GridColDef<R>): ExportCell {
61
64
  const raw = getRawExportValue(row, col);
62
- return { text: formatExportValue(row, col, raw), raw };
65
+ const text = formatExportValue(row, col, raw);
66
+ return { text, raw, fromNonText: isTrustedNonText(raw, text, !!col.valueFormatter) };
63
67
  }
64
68
 
65
69
  /** Aggregate text per export column for one subtotal / total record. */
@@ -111,7 +115,7 @@ export function exportToCsv<R extends GridRowModel>(
111
115
  const row = entry.row as R;
112
116
  const cells = exportColumns.map((col, i) => {
113
117
  const cell = exportCell(row, col);
114
- const text = escapeFormulas ? neutralizeFormula(cell.text, isNonTextValue(cell.raw)) : cell.text;
118
+ const text = escapeFormulas ? neutralizeFormula(cell.text, cell.fromNonText) : cell.text;
115
119
  return escapeCSV((i === 0 ? indent : '') + text, delimiter);
116
120
  });
117
121
  csvContent += cells.join(delimiter) + '\n';
@@ -138,7 +142,7 @@ export function exportToCsv<R extends GridRowModel>(
138
142
  rowsToExport.forEach(row => {
139
143
  const values = exportColumns.map(col => {
140
144
  const cell = exportCell(row, col);
141
- return field(cell.text, isNonTextValue(cell.raw));
145
+ return field(cell.text, cell.fromNonText);
142
146
  });
143
147
  csvContent += values.join(delimiter) + '\n';
144
148
  });
@@ -255,7 +259,7 @@ export function exportToExcel<R extends GridRowModel>(
255
259
  html += '<tbody>';
256
260
 
257
261
  const dataCell = (cell: ExportCell, col: GridColDef<R>, prefix = ''): string => {
258
- const content = prefix + cellText(cell.text, isNonTextValue(cell.raw));
262
+ const content = prefix + cellText(cell.text, cell.fromNonText);
259
263
  return isXlsTextCell(col, cell.raw) ? textTd(content) : `<td>${content}</td>`;
260
264
  };
261
265
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  import type { GridRowModel, GridFilterItem, GridFilterModel, GridFilterGroup, GridFilterOperator, GridColDef } from '../../types';
3
3
  import type { GridColumnLookup } from '../columnLookup';
4
- import { getCellValue, getFormattedValue, toLocalDateString, toLocalDayKey, toNumber } from '../values';
4
+ import { getCellValue, formatValueByType, getFormattedValue, toLocalDateString, toLocalDayKey, toNumber } from '../values';
5
5
 
6
6
  /** Compares a cell value with the filter item's value. */
7
7
  export type GridFilterOperatorFn = (value: unknown, filterValue?: unknown) => boolean;
@@ -230,7 +230,13 @@ function getQuickFilterTexts(row: GridRowModel, columns: GridColumnLookup | unde
230
230
  for (const col of columns.quickFilterColumns) {
231
231
  const value = getCellValue(row, col.field, col);
232
232
  const formatted = getFormattedValue(row, col.field, value, col);
233
- if (formatted != null && formatted !== '') texts.push(String(formatted).toLowerCase());
233
+ if (formatted != null && formatted !== '') {
234
+ texts.push(String(formatted).toLowerCase());
235
+ } else if (col.type === 'date' || col.type === 'boolean' || col.type === 'singleSelect') {
236
+ // Without a formatter the grid shows a locale date, Yes / No or the option label: search that too.
237
+ const shown = formatValueByType(value, col);
238
+ if (shown !== '') texts.push(shown.toLowerCase());
239
+ }
234
240
  pushSearchText(texts, value);
235
241
  }
236
242
  return texts;
@@ -131,6 +131,19 @@ export function parseNumberText(text: string): GridParsedValue {
131
131
 
132
132
  const ISO_DATE_PREFIX = /^(\d{4}-\d{2}-\d{2})/;
133
133
  const pad2 = (n: number) => String(n).padStart(2, '0');
134
+ /** Whether an ISO date-time string ends with `Z` or a `±hh:mm` / `±hhmm` offset. A loop, not a regex. */
135
+ function hasZone(text: string): boolean {
136
+ const end = text.trimEnd();
137
+ if (end.endsWith('Z') || end.endsWith('z')) return true;
138
+ const tIndex = end.indexOf('T');
139
+ if (tIndex < 0) return false;
140
+ for (let i = end.length - 1; i > tIndex; i--) {
141
+ const ch = end[i];
142
+ if (ch === '+' || ch === '-') return true;
143
+ if (!(ch >= '0' && ch <= '9') && ch !== ':') return false;
144
+ }
145
+ return false;
146
+ }
134
147
  const localDay = (date: Date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
135
148
 
136
149
  /**
@@ -147,7 +160,17 @@ export function parseDateText(text: string, current?: unknown): GridParsedValue
147
160
  if (typeof current === 'number') return valid(date.getTime());
148
161
  if (typeof current === 'string') {
149
162
  const day = localDay(date);
150
- return valid(ISO_DATE_PREFIX.test(current) && current.length > 10 ? day + current.slice(10) : day);
163
+ if (!ISO_DATE_PREFIX.test(current) || current.length <= 10) return valid(day);
164
+ // A zoned date-time ("…Z", "…+05:30") is shown as a local day: keep its local time of day on
165
+ // the pasted day and write UTC, or west of UTC the cell would show the day before.
166
+ const held = hasZone(current) ? new Date(current) : null;
167
+ if (held && !Number.isNaN(held.getTime())) {
168
+ return valid(new Date(
169
+ date.getFullYear(), date.getMonth(), date.getDate(),
170
+ held.getHours(), held.getMinutes(), held.getSeconds(), held.getMilliseconds(),
171
+ ).toISOString());
172
+ }
173
+ return valid(day + current.slice(10));
151
174
  }
152
175
  return valid(date);
153
176
  }
@@ -132,6 +132,7 @@ export function toDate(value: unknown): Date | null {
132
132
  // Reject roll-overs such as 2024-02-31.
133
133
  if (date.getMonth() !== month || date.getDate() !== day) return null;
134
134
  } else {
135
+ if (!hasOnlyDateWords(trimmed)) return null;
135
136
  date = new Date(trimmed);
136
137
  }
137
138
  } else {
@@ -140,6 +141,36 @@ export function toDate(value: unknown): Date | null {
140
141
  return Number.isNaN(date.getTime()) ? null : date;
141
142
  }
142
143
 
144
+ const DATE_WORDS = [
145
+ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october',
146
+ 'november', 'december', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
147
+ ];
148
+ const DATE_TOKENS = new Set(['t', 'z', 'am', 'pm', 'gmt', 'utc', 'sept']);
149
+
150
+ function isDateWord(word: string): boolean {
151
+ if (DATE_TOKENS.has(word)) return true;
152
+ return word.length >= 3 && DATE_WORDS.some(name => name.startsWith(word));
153
+ }
154
+
155
+ /**
156
+ * Whether every run of letters in `text` is a month or weekday name (or abbreviation), or one of
157
+ * T, Z, AM, PM, GMT, UTC. `Date.parse` accepts free text such as "hello 5" (as a 2001 date in V8),
158
+ * which must not become a date. A loop, not a regex.
159
+ */
160
+ function hasOnlyDateWords(text: string): boolean {
161
+ let word = '';
162
+ for (let i = 0; i <= text.length; i++) {
163
+ const ch = i < text.length ? text[i].toLowerCase() : '';
164
+ if (ch >= 'a' && ch <= 'z') {
165
+ word += ch;
166
+ continue;
167
+ }
168
+ if (word !== '' && !isDateWord(word)) return false;
169
+ word = '';
170
+ }
171
+ return true;
172
+ }
173
+
143
174
  /** A sortable integer for the local calendar day of a date (YYYYMMDD), or null when unparsable. */
144
175
  export function toLocalDayKey(value: unknown): number | null {
145
176
  const date = toDate(value);
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 3.6.0
3
+ # Version: 3.7.0
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "3.6.0",
3
+ "version": "3.7.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",
@@ -59,6 +59,7 @@
59
59
  "test:browser": "vitest run --project browser",
60
60
  "test:all": "vitest run",
61
61
  "test:smoke": "node scripts/smoke.mjs",
62
+ "test:demo": "npm run build && playwright test --config e2e/playwright.demo.config.ts",
62
63
  "bench": "node bench/run.mjs",
63
64
  "test:watch": "vitest --project unit",
64
65
  "coverage": "vitest run --coverage",
@@ -147,6 +148,7 @@
147
148
  "vitest": "^4.1.11"
148
149
  },
149
150
  "overrides": {
150
- "uuid": "^11.1.1"
151
+ "uuid": "^11.1.1",
152
+ "source-map-js": "^1.2.2"
151
153
  }
152
154
  }