@opencorestack/opengridx 3.2.2 → 3.4.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 (88) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +16 -2
  3. package/dist/ai.cjs +1 -0
  4. package/dist/ai.cjs.map +1 -0
  5. package/dist/ai.d.ts +166 -0
  6. package/dist/ai.es.js +1 -0
  7. package/dist/ai.es.js.map +1 -0
  8. package/dist/index.d.ts +321 -3
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6483 -5291
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -13
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +132 -3
  15. package/docs/README.md +4 -1
  16. package/docs/architecture/datagrid-orchestration.md +23 -6
  17. package/docs/components/header.md +25 -0
  18. package/docs/customization/theming.md +12 -1
  19. package/docs/features/ai-toolkit.md +119 -0
  20. package/docs/features/cell-selection.md +131 -0
  21. package/docs/features/clipboard.md +90 -0
  22. package/docs/features/editing-reordering.md +50 -0
  23. package/docs/features/keyboard-navigation.md +25 -2
  24. package/docs/features/undo-redo.md +79 -0
  25. package/docs/roadmap.md +64 -7
  26. package/lib/ai/coerce.ts +90 -0
  27. package/lib/ai/columns.ts +90 -0
  28. package/lib/ai/index.ts +16 -0
  29. package/lib/ai/schema.ts +214 -0
  30. package/lib/ai/types.ts +88 -0
  31. package/lib/ai/validate.ts +286 -0
  32. package/lib/components/Cell/Cell.css +35 -0
  33. package/lib/components/Cell/Cell.tsx +25 -13
  34. package/lib/components/Cell/CellErrorBoundary.tsx +3 -0
  35. package/lib/components/Cell/GridEditInputCell.css +12 -4
  36. package/lib/components/Cell/GridEditInputCell.tsx +19 -12
  37. package/lib/components/CellSelectionStats/CellSelectionStats.css +31 -0
  38. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +32 -0
  39. package/lib/components/CellSelectionStats/GridCellSelectionStatsArea.tsx +24 -0
  40. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
  41. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  42. package/lib/components/DataGrid/DataGrid.tsx +123 -3
  43. package/lib/components/DataGrid/GridLiveRegion.tsx +8 -2
  44. package/lib/components/DataGrid/GridPinnedRows.tsx +9 -0
  45. package/lib/components/DataGrid/GridToolbarSlot.tsx +5 -0
  46. package/lib/components/DataGrid/GridVirtualRows.tsx +9 -0
  47. package/lib/components/FilterPanel/FilterPanel.css +14 -0
  48. package/lib/components/FilterPanel/FilterPanel.tsx +5 -3
  49. package/lib/components/Header/Header.css +12 -0
  50. package/lib/components/Header/Header.tsx +17 -4
  51. package/lib/components/ListView/ListView.css +2 -4
  52. package/lib/components/ListView/ListViewRow.tsx +2 -2
  53. package/lib/components/Row/Row.tsx +38 -1
  54. package/lib/components/Toolbar/GlobalSearch.css +16 -0
  55. package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
  56. package/lib/components/Toolbar/GridToolbar.tsx +10 -4
  57. package/lib/components/ui/Checkbox.tsx +26 -10
  58. package/lib/components/ui/Input.css +43 -0
  59. package/lib/components/ui/Input.tsx +24 -5
  60. package/lib/hooks/core/gridApiPlaceholder.ts +14 -1
  61. package/lib/hooks/core/useDataGrid.ts +27 -0
  62. package/lib/hooks/core/useGridCellSelection.ts +631 -0
  63. package/lib/hooks/core/useGridCellSelectionApi.ts +97 -0
  64. package/lib/hooks/core/useGridClipboardApi.ts +8 -2
  65. package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
  66. package/lib/hooks/core/useGridControlledState.ts +38 -0
  67. package/lib/hooks/core/useGridKeyboardNavigation.ts +32 -0
  68. package/lib/hooks/core/useGridRowInteractions.ts +8 -4
  69. package/lib/hooks/core/useGridUndoRedo.ts +220 -0
  70. package/lib/hooks/features/useAggregation.ts +8 -6
  71. package/lib/hooks/features/useGridBatchEdit.ts +217 -0
  72. package/lib/hooks/features/useGridClipboard.ts +47 -2
  73. package/lib/hooks/features/useGridEditing.ts +39 -51
  74. package/lib/index.ts +16 -0
  75. package/lib/styles/base/variables.css +6 -1
  76. package/lib/styles/opengridx.css +3 -1
  77. package/lib/theme/DataGridThemeProvider.tsx +4 -0
  78. package/lib/theme/presets.ts +8 -0
  79. package/lib/theme/types.ts +4 -0
  80. package/lib/types/index.ts +254 -0
  81. package/lib/utils/attempt.ts +14 -0
  82. package/lib/utils/cellSelection.ts +238 -0
  83. package/lib/utils/editing/commit.ts +69 -0
  84. package/lib/utils/editing/targets.ts +109 -0
  85. package/lib/utils/headerWrap.ts +23 -0
  86. package/lib/utils/parsing.ts +252 -0
  87. package/llms.txt +71 -2
  88. package/package.json +14 -4
@@ -0,0 +1,252 @@
1
+ import type { GridColDef, GridValidRowModel } from '../types';
2
+ import { toDate } from './values';
3
+
4
+ /**
5
+ * Text → typed values: the TSV reader clipboard paste uses, and the per-column-type parsers paste,
6
+ * `GridColDef.valueParser` defaults and the `/ai` validator share. Pure and React-free.
7
+ *
8
+ * Every pattern here is linear (CodeQL scans every push): no adjacent quantifiers that can match the
9
+ * same characters.
10
+ */
11
+
12
+ /** A parsed value, or `{ ok: false }` when the text does not fit the column type. */
13
+ export type GridParsedValue = { ok: true; value: unknown } | { ok: false };
14
+
15
+ const INVALID: GridParsedValue = { ok: false };
16
+ const valid = (value: unknown): GridParsedValue => ({ ok: true, value });
17
+
18
+ /**
19
+ * Splits tab-separated text the way Excel, Google Sheets and LibreOffice write it: tabs between
20
+ * columns, `\r\n`, `\n` or `\r` between rows. A field that starts with a double quote is quoted: it
21
+ * may hold tabs, line breaks and doubled quotes (`""` → `"`), the reverse of `escapeTsvField`. A
22
+ * quote inside an unquoted field is kept as is. A trailing empty line is ignored, so `"a\tb\n"` is one
23
+ * row. Empty text gives no rows.
24
+ */
25
+ export function parseTsv(text: string): string[][] {
26
+ const rows: string[][] = [];
27
+ if (text === '') return rows;
28
+ let row: string[] = [];
29
+ let field = '';
30
+ let i = 0;
31
+ const n = text.length;
32
+ let fieldStart = true;
33
+ while (i < n) {
34
+ const ch = text[i];
35
+ if (fieldStart && ch === '"') {
36
+ // A quoted field: read to the closing quote, `""` being one quote.
37
+ let j = i + 1;
38
+ let value = '';
39
+ let closed = false;
40
+ while (j < n) {
41
+ const q = text[j];
42
+ if (q === '"') {
43
+ if (text[j + 1] === '"') {
44
+ value += '"';
45
+ j += 2;
46
+ continue;
47
+ }
48
+ closed = true;
49
+ j += 1;
50
+ break;
51
+ }
52
+ value += q;
53
+ j += 1;
54
+ }
55
+ if (!closed) {
56
+ // No closing quote: not a quoted field after all, read the quote as text.
57
+ field += ch;
58
+ i += 1;
59
+ fieldStart = false;
60
+ continue;
61
+ }
62
+ field += value;
63
+ i = j;
64
+ fieldStart = false;
65
+ continue;
66
+ }
67
+ fieldStart = false;
68
+ if (ch === '\t') {
69
+ row.push(field);
70
+ field = '';
71
+ fieldStart = true;
72
+ i += 1;
73
+ } else if (ch === '\n' || ch === '\r') {
74
+ row.push(field);
75
+ rows.push(row);
76
+ row = [];
77
+ field = '';
78
+ fieldStart = true;
79
+ i += ch === '\r' && text[i + 1] === '\n' ? 2 : 1;
80
+ } else {
81
+ field += ch;
82
+ i += 1;
83
+ }
84
+ }
85
+ // The last line, unless the text ended with a line break (Excel always adds one).
86
+ if (!fieldStart || row.length > 0 || field !== '') {
87
+ row.push(field);
88
+ rows.push(row);
89
+ }
90
+ return rows;
91
+ }
92
+
93
+ const PLAIN_NUMBER = /^\d+(?:\.\d*)?(?:e[+-]?\d+)?$/i;
94
+ const LEADING_DOT_NUMBER = /^\.\d+(?:e[+-]?\d+)?$/i;
95
+ const GROUPED_NUMBER = /^\d{1,3}(?:,\d{3})+(?:\.\d*)?$/;
96
+ // Currency symbols (Unicode Sc) and every kind of space, including the no-break spaces Excel uses
97
+ // as a thousands separator.
98
+ const STRIPPED_CHARS = /[\p{Sc}\s]/gu;
99
+
100
+ /**
101
+ * A number as spreadsheets copy it: `1234.5`, `1,234.5` (comma thousands groups only), `-12`,
102
+ * `(12)` (accounting negative), `12%` (→ 0.12), `1e3`, with currency symbols and spaces stripped.
103
+ * Empty text is `null`. `1,5` is not accepted (it is ambiguous): use a `valueParser` for
104
+ * decimal-comma locales.
105
+ */
106
+ export function parseNumberText(text: string): GridParsedValue {
107
+ let s = text.replace(STRIPPED_CHARS, '');
108
+ if (s === '') return text.trim() === '' ? valid(null) : INVALID;
109
+ let negative = false;
110
+ if (s.startsWith('(') && s.endsWith(')')) {
111
+ negative = true;
112
+ s = s.slice(1, -1);
113
+ }
114
+ if (s.startsWith('-') || s.startsWith('+') || s.startsWith('−')) {
115
+ if (s[0] !== '+') negative = !negative;
116
+ s = s.slice(1);
117
+ }
118
+ let percent = false;
119
+ if (s.endsWith('%')) {
120
+ percent = true;
121
+ s = s.slice(0, -1);
122
+ }
123
+ if (GROUPED_NUMBER.test(s)) s = s.split(',').join('');
124
+ else if (!PLAIN_NUMBER.test(s) && !LEADING_DOT_NUMBER.test(s)) return INVALID;
125
+ let value = Number(s);
126
+ if (!Number.isFinite(value)) return INVALID;
127
+ if (percent) value /= 100;
128
+ if (negative) value = -value;
129
+ return valid(value);
130
+ }
131
+
132
+ const ISO_DATE_PREFIX = /^(\d{4}-\d{2}-\d{2})/;
133
+ const pad2 = (n: number) => String(n).padStart(2, '0');
134
+ const localDay = (date: Date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
135
+
136
+ /**
137
+ * A date: ISO `2026-10-07` (read as that local calendar day) or anything `Date.parse` accepts. Empty
138
+ * text is `null`. The result takes the kind of `current` (the value the cell holds now), as the date
139
+ * editor does: a timestamp stays a timestamp, a date string stays a `YYYY-MM-DD` string (an ISO
140
+ * date-time string keeps its time part); otherwise it is a `Date`.
141
+ */
142
+ export function parseDateText(text: string, current?: unknown): GridParsedValue {
143
+ const trimmed = text.trim();
144
+ if (trimmed === '') return valid(null);
145
+ const date = toDate(trimmed);
146
+ if (!date) return INVALID;
147
+ if (typeof current === 'number') return valid(date.getTime());
148
+ if (typeof current === 'string') {
149
+ const day = localDay(date);
150
+ return valid(ISO_DATE_PREFIX.test(current) && current.length > 10 ? day + current.slice(10) : day);
151
+ }
152
+ return valid(date);
153
+ }
154
+
155
+ /** Text read as `true` / `false`, besides the grid's own `Yes` / `No` cell labels. Lower case. */
156
+ const TRUE_WORDS = new Set(['true', 'yes', '1']);
157
+ const FALSE_WORDS = new Set(['false', 'no', '0']);
158
+
159
+ export interface GridBooleanLabels {
160
+ /** Text shown for `true` (the grid shows `Yes`). */
161
+ trueLabel?: string;
162
+ /** Text shown for `false` (the grid shows `No`). */
163
+ falseLabel?: string;
164
+ }
165
+
166
+ /**
167
+ * A boolean: `true`/`false`, `yes`/`no` (the grid's own `Yes` / `No` cell labels), `1`/`0` and any
168
+ * `labels` given, case-insensitive. Empty text is `null`.
169
+ */
170
+ export function parseBooleanText(text: string, labels?: GridBooleanLabels): GridParsedValue {
171
+ const s = text.trim().toLowerCase();
172
+ if (s === '') return valid(null);
173
+ if (labels?.trueLabel !== undefined && s === labels.trueLabel.trim().toLowerCase()) return valid(true);
174
+ if (labels?.falseLabel !== undefined && s === labels.falseLabel.trim().toLowerCase()) return valid(false);
175
+ if (TRUE_WORDS.has(s)) return valid(true);
176
+ if (FALSE_WORDS.has(s)) return valid(false);
177
+ return INVALID;
178
+ }
179
+
180
+ type ValueOption = NonNullable<GridColDef['valueOptions']>[number];
181
+
182
+ /**
183
+ * A `singleSelect` value: the option whose value or label matches the text, case-insensitive (values
184
+ * first). Returns the option's own value, so a numeric option stays a number. Empty text is `null`.
185
+ */
186
+ export function parseSingleSelectText(text: string, options: readonly ValueOption[] | undefined): GridParsedValue {
187
+ const s = text.trim();
188
+ if (s === '') return valid(null);
189
+ if (!options || options.length === 0) return INVALID;
190
+ const lower = s.toLowerCase();
191
+ for (const option of options) {
192
+ const value = typeof option === 'object' ? option.value : option;
193
+ if (String(value).toLowerCase() === lower) return valid(value);
194
+ }
195
+ for (const option of options) {
196
+ if (typeof option === 'object' && option.label.toLowerCase() === lower) return valid(option.value);
197
+ }
198
+ return INVALID;
199
+ }
200
+
201
+ type ParserColumn = Pick<GridColDef, 'type' | 'valueOptions'>;
202
+
203
+ export interface GridParseTextOptions {
204
+ /** The value the cell holds now: a parsed date takes its kind (see `parseDateText`). */
205
+ current?: unknown;
206
+ /** Extra boolean labels, besides `Yes` / `No`. */
207
+ booleanLabels?: GridBooleanLabels;
208
+ }
209
+
210
+ /**
211
+ * Parses cell text by the column `type`, without `valueParser`:
212
+ *
213
+ * | type | accepted |
214
+ * | :--- | :--- |
215
+ * | `string` (default), `image` | the text as is (an empty cell is `''`) |
216
+ * | `number` | `parseNumberText` |
217
+ * | `date` | `parseDateText` |
218
+ * | `boolean` | `parseBooleanText` |
219
+ * | `singleSelect` | `parseSingleSelectText` |
220
+ *
221
+ * An empty cell is `null` for every type but `string` / `image`.
222
+ */
223
+ export function parseValueByType(text: string, colDef: ParserColumn | undefined, options?: GridParseTextOptions): GridParsedValue {
224
+ switch (colDef?.type) {
225
+ case 'number': return parseNumberText(text);
226
+ case 'date': return parseDateText(text, options?.current);
227
+ case 'boolean': return parseBooleanText(text, options?.booleanLabels);
228
+ case 'singleSelect': return parseSingleSelectText(text, colDef.valueOptions);
229
+ default: return valid(text);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Parses the text pasted into one cell: the column's `valueParser` when it has one (a throw, or a
235
+ * returned `undefined`, rejects the text), otherwise `parseValueByType`.
236
+ */
237
+ export function parseCellText<R extends GridValidRowModel>(
238
+ text: string,
239
+ colDef: GridColDef<R>,
240
+ row: R,
241
+ options?: GridParseTextOptions,
242
+ ): GridParsedValue {
243
+ const { valueParser } = colDef;
244
+ if (!valueParser) return parseValueByType(text, colDef as unknown as ParserColumn, options);
245
+ let value: unknown;
246
+ try {
247
+ value = valueParser(text, { row, field: colDef.field, colDef });
248
+ } catch {
249
+ return INVALID;
250
+ }
251
+ return value === undefined ? INVALID : valid(value);
252
+ }
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 3.2.2
3
+ # Version: 3.4.0
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -78,6 +78,7 @@ export default function App() {
78
78
  | localeText | GridLocaleText | — | Override pager / no-rows strings: paginationRowsPerPage, paginationOf(from,to,count), paginationPage(page,count), noRowsLabel |
79
79
  | rowHeight | number | 52 | Row height in pixels |
80
80
  | headerHeight | number | 56 | Header height in pixels |
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+) |
81
82
  | loading | boolean | false | No rows: skeleton rows (or slots.loadingOverlay). Rows shown: they stay, a progress bar (.ogx__loading-bar) runs along the top, or slots.loadingOverlay is shown over them |
82
83
  | noRowsLabel | string | 'No Data' | Empty state message |
83
84
  | density | 'compact'/'standard'/'comfortable' | 'standard' | 32px / rowHeight / 72px rows |
@@ -92,6 +93,16 @@ export default function App() {
92
93
  | onRowSelectionModelChange | (model: GridRowId[]) => void | Selection change callback. Also fires once with ids of removed rows pruned (client-owned rows) |
93
94
  | disableRowSelectionOnClick | boolean | Prevent click toggling selection |
94
95
  | disableClipboardCopy | boolean | Turn off the grid's Ctrl/Cmd+C copy (copySelectedRows still works) |
96
+ | cellSelection | boolean (3.3.0+) | Spreadsheet-style cell ranges: drag (auto-scrolls), Shift+click, Shift+arrows/Home/End/PageUp/PageDown, Ctrl/Cmd+A = every data cell of the page, Escape collapses. Ctrl/Cmd+C then copies the range as TSV (no header line) instead of the selected rows. Ranges never change the row selection |
97
+ | cellSelectionModel | GridCellSelectionModel | Controlled range: readonly { anchor: { id, field }, head: { id, field } }[] (0 or 1 range). Corners are ids/fields: the range follows sorting; cleared ('dataChange') when a corner is no longer displayed |
98
+ | onCellSelectionModelChange | (model, { reason }) => void | reason: 'pointer' | 'keyboard' | 'selectAll' | 'api' | 'clear' | 'dataChange' |
99
+ | showCellSelectionStats | boolean | Status bar: Count (non-empty), Sum and Average (numeric) of the range; slots.cellSelectionStats replaces it |
100
+ | disableClipboardPaste | boolean (3.4.0+) | With cellSelection, Ctrl/Cmd+V pastes TSV (Excel/Sheets) into editable cells: one value fills a range; a block goes from the range's top-left (or focused) cell, cut to the range and grid edges; no rows created. Parsed per column type, then valueSetter + processRowUpdate once per row. true = shortcut off (apiRef.pasteText still works) |
101
+ | onClipboardPaste | (result) => void (3.4.0+) | After every paste: { updated: id[], failed: {id, error}[], skipped: {id, field, reason: 'notEditable'/'invalidValue'}[], text } |
102
+ | onBeforeClipboardPaste | ({ text, anchor }) => boolean / string | Return false to cancel, a string to paste instead; a throw cancels |
103
+ | disableRangeClear | boolean (3.4.0+) | With cellSelection, Delete/Backspace on a multi-cell range empties editable cells ('' strings, null otherwise) as one edit; true = off |
104
+ | undoRedo | boolean / { limit } (3.4.0+) | Undo/redo of edits, pastes, range clears (default off, 100 actions). Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y on a cell with no editor open. Recorded only after processRowUpdate succeeds; replayed through valueSetter + processRowUpdate; cells changed since are skipped ('changed', compared by value), removed rows ('missing') |
105
+ | onHistoryChange | ({ canUndo, canRedo, size }) => void | History change (size = undoable actions) |
95
106
 
96
107
  ### Sorting
97
108
  | Prop | Type | Description |
@@ -240,6 +251,7 @@ There are no getRowClassName / getCellClassName / theme props: use GridColDef.ce
240
251
  | renderEditCell | (params: GridRenderEditCellParams<R>) => ReactNode | Custom editor: call params.onValueChange(v), params.onCommit(), params.onCancel() |
241
252
  | valueGetter | ({row, field, value}) => unknown | Computed value; used by sort, filter, quick search, aggregation, copy, export. Must be pure (quick filter caches per row object); a throw reads as undefined + dev warning |
242
253
  | valueSetter | ({value, row, field}) => R | Write an edited value back onto the row (editable valueGetter columns) |
254
+ | valueParser | (text, { row, field, colDef }) => unknown | Paste parser for this column (3.4.0+), e.g. "1.234,5" → 1234.5. Throw/undefined skips the cell ('invalidValue'). Default by type: number "1,234.5"/"12%"/"$5"; date ISO or Date.parse (keeps the cell's kind); boolean true/false/yes/no/1/0; singleSelect value or label; empty → null ('' for strings) |
243
255
  | valueFormatter | ({value, row, field}) => string | Format display value (value is `unknown`: cast before calling methods) |
244
256
  | cellClassName | string / (params) => string | CSS class on cells |
245
257
  | headerClassName | string | CSS class on header cell |
@@ -247,6 +259,7 @@ There are no getRowClassName / getCellClassName / theme props: use GridColDef.ce
247
259
  | rowSpan | number / (params) => number | Cell row span (clamped to the row section) |
248
260
  | align | 'left'/'center'/'right' | Cell content alignment |
249
261
  | headerAlign | 'left'/'center'/'right' | Header content alignment |
262
+ | wrapHeaderText | boolean | Wrap this header title; overrides the grid prop (3.3.0+) |
250
263
  | sortable | boolean (default true) | Enable sorting |
251
264
  | sortComparator | (v1, v2, params1, params2) => number | Custom ascending comparator (3.1.0+). v1/v2 are valueGetter values incl. null/undefined; params { id, field, row, value }. Negated for desc; multi-sort tie-break chain; groups by grouping value; tree siblings; pivot row labels. Throw/NaN = 0. Ignored in server sortingMode |
252
265
  | filterable | boolean (default true) | Enable filtering and quick search |
@@ -259,6 +272,7 @@ There are no getRowClassName / getCellClassName / theme props: use GridColDef.ce
259
272
  | groupingValueFormatter | ({field, value}) => string | Group row label (default "field: value") |
260
273
  | aggregable | boolean | false = no aggregation. Summaries panel offers number columns + aggregable: true |
261
274
  | availableAggregationFunctions | string[] | Restrict aggregation functions for this column |
275
+ | aiExamples | unknown[] | Example values put into getGridAiSchema's schema (3.4.0+). SENT TO THE MODEL: real data, opt-in. Primitives only, max 10. Unused by the grid |
262
276
 
263
277
  ---
264
278
 
@@ -307,6 +321,22 @@ apiRef.current.autosizeColumns(fields?)
307
321
  apiRef.current.copySelectedRows() // → Promise<void>; rejects if the write fails. Copies every selected row that passes
308
322
  // the filter (all pages, collapsed groups, pinned rows), visible columns in screen order
309
323
 
324
+ // Cell range selection (cellSelection, 3.3.0+); inert while cellSelection is off
325
+ apiRef.current.selectCellRange({ id: 1, field: 'a' }, { id: 5, field: 'c' }) // anchor = active cell
326
+ apiRef.current.setCellSelectionModel(model)
327
+ apiRef.current.getCellSelectionModel() // → GridCellSelectionModel
328
+ apiRef.current.getSelectedCells() // → { id, field, value }[]; synthetic rows and span-covered cells left out
329
+ apiRef.current.copySelectedCells() // → Promise<void>; TSV, no header line
330
+ apiRef.current.clearCellSelection()
331
+
332
+ // Paste and undo/redo (3.4.0+)
333
+ apiRef.current.pasteText(text, anchor?) // → Promise<GridBatchEditResult>; anchor default: range top-left, else focused cell
334
+ apiRef.current.undo() // → Promise<GridBatchEditResult> (undoRedo)
335
+ apiRef.current.redo()
336
+ apiRef.current.canUndo() // → boolean
337
+ apiRef.current.canRedo()
338
+ apiRef.current.clearHistory()
339
+
310
340
  // Aggregation
311
341
  apiRef.current.getAggregationResult() // → Record<string, unknown> | null (null in pivot mode)
312
342
  apiRef.current.getAggregationModel() // → GridAggregationModel | null
@@ -342,6 +372,22 @@ const processRowUpdate = (newRow: Person) => {
342
372
 
343
373
  Double-click or Enter opens the editor; Enter / Tab / blur commits; Escape cancels. An edit whose row scrolls, pages or filters away is committed, not dropped.
344
374
 
375
+ Custom editors (3.3.1+): build them on the exported `Input` with `variant="cell"` — it fills the cell with no border/background/focus ring, like the built-in editors. `ref` reaches the `<input>` (forwardRef, React 18 + 19), `inputClassName` styles the `<input>` (`className` the wrapper), `startAdornment` / `endAdornment` add a unit, `error` adds an inset error edge and `aria-invalid`. Stop propagation of the Enter/Escape you handle.
376
+
377
+ ```tsx
378
+ import { Input } from '@opencorestack/opengridx';
379
+ function PriceEditor({ value, onValueChange, onCommit, onCancel }: GridRenderEditCellParams) {
380
+ const [text, setText] = useState(String(value ?? ''));
381
+ const ref = useRef<HTMLInputElement>(null);
382
+ useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
383
+ const valid = text.trim() !== '' && Number(text) >= 0;
384
+ return <Input ref={ref} variant="cell" startAdornment="$" error={!valid} value={text}
385
+ onChange={e => { setText(e.target.value); const n = Number(e.target.value); if (e.target.value.trim() !== '' && n >= 0) onValueChange(n); }}
386
+ onKeyDown={e => { if (e.key === 'Enter') { e.stopPropagation(); if (valid) onCommit(); } if (e.key === 'Escape') { e.stopPropagation(); onCancel(); } }}
387
+ onBlur={() => (valid ? onCommit() : onCancel())} />;
388
+ }
389
+ ```
390
+
345
391
  ---
346
392
 
347
393
  ## SERVER-SIDE DATA SOURCE
@@ -510,6 +556,27 @@ await printGrid(rows, columns, 'My Report Title');
510
556
 
511
557
  ---
512
558
 
559
+ ## AI TOOLKIT (@opencorestack/opengridx/ai, 3.4.0+)
560
+
561
+ Separate entry point: no React, no dependencies, ~4.5 kB gzip; never loaded by the core import. OpenGridX calls no AI service: the app sends { prompt, schema, currentState } to its own model (structured output) and validates the reply.
562
+
563
+ ```ts
564
+ import { getGridAiSchema, validateGridAiState } from '@opencorestack/opengridx/ai';
565
+
566
+ const schema = getGridAiSchema(columns, { include: ['filter', 'sort', 'grouping', 'aggregation'] });
567
+ const reply = await fetch('/api/grid-assistant', { method: 'POST', body: JSON.stringify({ prompt, schema, currentState }) });
568
+ const { state, errors } = validateGridAiState(await reply.text(), columns);
569
+ if (state.filterModel) setFilterModel(state.filterModel); // apply only the parts present
570
+ if (state.sortModel) setSortModel(state.sortModel);
571
+ ```
572
+
573
+ - getGridAiSchema(columns, { include?, exclude?, descriptions = true, examples = true }) → JSON Schema draft 2020-12 with `schemaVersion: 1`. Parts (option name → key): filter → filterModel (items as oneOf per field, AND/OR groups ≤ 3 levels, quickFilterValues), sort → sortModel, grouping → rowGroupingModel, aggregation → aggregationModel, pivot → pivotModel, columnVisibility → columnVisibilityModel. Fields from filterable / sortable / groupable / hideable / aggregable !== false; operators = the column type's filter operators; aggregation: number (or aggregable: true) columns get sum/avg/count/min/max/unique, others count, limited by availableAggregationFunctions; singleSelect valueOptions become enums. Fields starting with `__` are skipped. Deterministic; reads no rows.
574
+ - validateGridAiState(json | jsonString, columns, { include?, exclude? }) → { state: GridAiState, errors: { path, message }[] }. Drops unknown parts, fields, disallowed fields, wrong operators, enum values outside the list; coerces values like clipboard paste ("1,200"/"$1,200" → 1200, "12%" → 0.12, yes/no/1/0 → boolean, dates → "YYYY-MM-DD", singleSelect by value or label, case-insensitive); removes empty groups; lists capped at 200 entries, errors at 100. Never throws; never reads `__proto__`.
575
+ - Privacy: no row data by default; `aiExamples` and `valueOptions` ARE sent; always apply `state`, never the raw reply.
576
+ - Types (from /ai): GridAiColumn, GridAiPart, GridAiSchema, GridAiSchemaOptions, GridAiState, GridAiValidateOptions, GridAiValidationError, GridAiValidationResult.
577
+
578
+ ---
579
+
513
580
  ## CSS CUSTOMIZATION
514
581
 
515
582
  All CSS variables use the `--ogx-` prefix and are declared on `:root` by the stylesheet. Override them on a wrapper (or use DataGridThemeProvider, which sets the same variables):
@@ -530,8 +597,10 @@ Derived tokens (e.g. `--ogx-grid-cell-focus-border`, which defaults to the prima
530
597
  Class names use the `ogx__` BEM prefix, e.g.:
531
598
  - `ogx__row`, `ogx__row--selected`, `ogx__row--even`, `ogx__row--odd`
532
599
  - `ogx__cell`, `ogx__cell--focused`, `ogx__cell--editing`
533
- - `ogx__header-cell`, `ogx__header-cell--sorted`
600
+ - Cell ranges (3.3.0+): `ogx__cell--range`, `ogx__cell--range-top|bottom|left|right` (inset box-shadow outline), `ogx--range-dragging` on the root during a drag; variables `--ogx-range-background`, `--ogx-range-border` (theme keys `grid.rangeBackground`, `grid.rangeBorder`)
601
+ - `ogx__header-cell`, `ogx__header-cell--sorted`, `ogx__header-cell--wrap` (wrapHeaderText, 3.3.0+)
534
602
  - Sticky wrappers: `ogx__sticky-top` (header + top-pinned rows), `ogx__sticky-bottom` (bottom-pinned rows + aggregation footer)
603
+ - Form controls (3.3.1+): the toolbar search, column-panel search, filter value box and text/number/date editors render through `Input`: the `<input>` keeps its class (`ogx-global-search__input`, `ogx-column-visibility-panel__search-input`, `ogx-filter__value-input`, `ogx__edit-input`) inside a `div.ogx-input-wrapper` (`ogx-input-wrapper--cell` for editors) — use descendant selectors (`.ogx-global-search input`), not `> input`. The boolean editor and list-view row checkbox are the shared `Checkbox` (`.ogx-checkbox__box`; hidden `<input>`)
535
604
 
536
605
  ---
537
606
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "3.2.2",
3
+ "version": "3.4.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",
@@ -12,6 +12,11 @@
12
12
  "import": "./dist/opengridx.es.js",
13
13
  "require": "./dist/opengridx.umd.js"
14
14
  },
15
+ "./ai": {
16
+ "types": "./dist/ai.d.ts",
17
+ "import": "./dist/ai.es.js",
18
+ "require": "./dist/ai.cjs"
19
+ },
15
20
  "./styles": {
16
21
  "types": "./dist/opengridx.css.d.ts",
17
22
  "default": "./dist/opengridx.css"
@@ -23,6 +28,7 @@
23
28
  "!lib/**/*.test.ts",
24
29
  "!lib/**/*.test.tsx",
25
30
  "!lib/**/__screenshots__",
31
+ "!lib/**/__snapshots__",
26
32
  "docs/architecture",
27
33
  "docs/assets",
28
34
  "docs/components",
@@ -45,7 +51,7 @@
45
51
  "scripts": {
46
52
  "dev": "vite",
47
53
  "build": "tsc --project tsconfig.build.json && vite build",
48
- "build:lib": "tsc --project tsconfig.build.json && vite build --config vite.config.lib.js && cp lib/styles/opengridx.css.d.ts dist/opengridx.css.d.ts && node scripts/check-bundle.mjs",
54
+ "build:lib": "tsc --project tsconfig.build.json && vite build --config vite.config.lib.js && vite build --config vite.config.lib.js --mode ai && cp lib/styles/opengridx.css.d.ts dist/opengridx.css.d.ts && node scripts/check-bundle.mjs",
49
55
  "lint": "eslint .",
50
56
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit && tsc -p e2e/tsconfig.json",
51
57
  "preview": "vite preview",
@@ -55,7 +61,9 @@
55
61
  "test:smoke": "node scripts/smoke.mjs",
56
62
  "test:watch": "vitest --project unit",
57
63
  "coverage": "vitest run --coverage",
58
- "prepublishOnly": "npm run build:lib"
64
+ "prepublishOnly": "npm run build:lib",
65
+ "wiki:build": "node scripts/build-wiki.mjs",
66
+ "check:compiler": "node scripts/check-react-compiler.mjs"
59
67
  },
60
68
  "keywords": [
61
69
  "react",
@@ -106,6 +114,7 @@
106
114
  }
107
115
  },
108
116
  "devDependencies": {
117
+ "@babel/core": "^7.29.7",
109
118
  "@eslint/js": "^9.39.1",
110
119
  "@playwright/test": "^1.58.2",
111
120
  "@testing-library/jest-dom": "^7.0.0",
@@ -117,8 +126,9 @@
117
126
  "@vitejs/plugin-react": "^5.1.1",
118
127
  "@vitest/browser-playwright": "^4.1.11",
119
128
  "@vitest/coverage-v8": "^4.1.11",
129
+ "babel-plugin-react-compiler": "^1.0.0",
120
130
  "eslint": "^9.39.1",
121
- "eslint-plugin-react-hooks": "^7.0.1",
131
+ "eslint-plugin-react-hooks": "^7.1.1",
122
132
  "eslint-plugin-react-refresh": "^0.4.24",
123
133
  "exceljs": "^4.4.0",
124
134
  "globals": "^16.5.0",