Repository navigation
Expand file tree
/
Copy pathllms.txt
More file actions
738 lines (615 loc) · 59.3 KB
/
Copy pathllms.txt
File metadata and controls
738 lines (615 loc) · 59.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# OpenGridX — AI Agent Context
# Package: @opencorestack/opengridx
# Version: 3.10.1
# Docs: https://opencorestack.github.io/OpenGridX/
# This file is machine-readable. AI developers: start here.
---
## OVERVIEW
OpenGridX is a zero-dependency, high-performance React DataGrid library.
It matches and extends the feature set of MUI X DataGrid Pro — offered completely free.
- **React**: 18+ required (react and react-dom are peer dependencies)
- **TypeScript**: Full typings shipped in `dist/index.d.ts`
- **Optional peers**: `exceljs` (exportToExcelAdvanced), `jspdf` + `jspdf-autotable` (exportToPdf). Loaded lazily, never bundled.
- **CSS**: NOT loaded by the JS bundle. Always add `import '@opencorestack/opengridx/styles'` once in the app root.
---
## INSTALLATION
```bash
npm install @opencorestack/opengridx
# Optional — only for exportToExcelAdvanced:
npm install exceljs
# Optional — only for exportToPdf:
npm install jspdf jspdf-autotable
```
```tsx
import { DataGrid } from '@opencorestack/opengridx';
import '@opencorestack/opengridx/styles'; // required: the stylesheet is a separate file
```
Without the stylesheet the grid is unstyled AND the viewport does not scroll, so every row is rendered (virtualization is lost).
---
## MINIMAL EXAMPLE
```tsx
import { DataGrid, type GridColDef } from '@opencorestack/opengridx';
import '@opencorestack/opengridx/styles';
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'name', headerName: 'Name', width: 180 },
{ field: 'age', headerName: 'Age', width: 100, type: 'number', editable: true },
];
const rows = [
{ id: 1, name: 'Jon Snow', age: 25 },
{ id: 2, name: 'Arya Stark', age: 18 },
];
export default function App() {
return <DataGrid rows={rows} columns={columns} height={400} />;
}
```
---
## DataGrid PROPS (DataGridProps<R extends GridValidRowModel = GridRowModel>)
### Required
| Prop | Type | Description |
|-----------|------------------|----------------------------------|
| rows | R[] | Data array. Each row needs an id (or pass getRowId). Rows are never copied or mutated |
| columns | GridColDef<R>[] | Column definitions |
### Display
| Prop | Type | Default | Description |
|-------------------|-----------|---------|----------------------------------------|
| height | number/string | — | Grid height. No default: fills its container (class ogx--fill, 3.0.1+); the container must have a BOUNDED height or it renders every row (see GOTCHAS) |
| autoHeight | boolean | false | Fit height to rows (renders every row) |
| overscanRowCount | number | 3 | Minimum rows rendered outside the viewport (raised automatically while scrolling fast) |
| ariaLabel | string | — | Accessible name for the grid |
| 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>>) |
| rowHeight | number | 52 | Row height in pixels |
| headerHeight | number | 56 | Header height in pixels |
| 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+) |
| 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 |
| noRowsLabel | string | 'No Data' | Empty state message |
| density | 'compact'/'standard'/'comfortable' | 'standard' | 32px / rowHeight / 72px rows |
### Selection
| Prop | Type | Description |
|----------------------------|-------------------------------|-------------------------------------|
| checkboxSelection | boolean | Enable checkbox column (header checkbox = select all filtered rows) |
| disableMultipleRowSelection | boolean | Cap selection to one row (checkboxes, Space, apiRef too); removes the select-all checkbox |
| pinCheckboxColumn | boolean (default true) | Pin the checkbox column left |
| rowSelectionModel | GridRowId[] | Controlled selection |
| onRowSelectionModelChange | (model: GridRowId[]) => void | Selection change callback. Also fires once with ids of removed rows pruned (client-owned rows) |
| disableRowSelectionOnClick | boolean | Prevent click toggling selection |
| disableClipboardCopy | boolean | Turn off the grid's Ctrl/Cmd+C copy (copySelectedRows still works) |
| 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 |
| 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 |
| onCellSelectionModelChange | (model, { reason }) => void | reason: 'pointer' | 'keyboard' | 'selectAll' | 'api' | 'clear' | 'dataChange' |
| showCellSelectionStats | boolean | Status bar: Count (non-empty), Sum and Average (numeric) of the range; slots.cellSelectionStats replaces it |
| 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) |
| onClipboardPaste | (result) => void (3.4.0+) | After every paste: { updated: id[], failed: {id, error}[], skipped: {id, field, reason: 'notEditable'/'invalidValue'}[], text } |
| onBeforeClipboardPaste | ({ text, anchor }) => boolean / string | Return false to cancel, a string to paste instead; a throw cancels |
| 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 |
| 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') |
| onHistoryChange | ({ canUndo, canRedo, size }) => void | History change (size = undoable actions) |
| disableFillHandle | boolean (3.5.0+) | With cellSelection, the range's bottom-right cell has a fill handle: drag down/up/right/left (one direction; auto-scroll; touch) fills the new cells from the range — numbers and dates with an exactly equal step continue (2,4 → 6,8; 15 Jan,15 Feb → 15 Mar; 5 Oct,6 Oct → 7 Oct), anything else repeats, one cell copies; Alt at release copies; Escape cancels. Ctrl/Cmd+D fills down from the range's top row, Ctrl/Cmd+R right from its left column. valueSetter + processRowUpdate once per row, one undo step, non-editable/synthetic cells skipped ('notEditable'). true = no handle, keys left to the browser |
| onFill | (result) => void (3.5.0+) | After every fill: { updated, failed, skipped, direction: 'down'/'up'/'right'/'left' } |
### Sorting
| Prop | Type | Description |
|-------------------|-----------------------------|----------------------------------|
| sortModel | GridSortItem[] | Controlled sort state |
| multiSort | boolean | Plain click appends to sortModel (Shift+click always does) |
| onSortModelChange | (model: GridSortItem[]) => void | Sort change callback |
| sortingMode | 'client' / 'server' | 'server': rows are shown in the order given |
### Filtering
| Prop | Type | Description |
|---------------------|--------------------------------|-----------------------------------|
| filterModel | GridFilterModel | Controlled filter state (optional: the grid keeps its own filter model otherwise) |
| onFilterModelChange | (model: GridFilterModel) => void | Filter change callback |
| filterMode | 'client' / 'server' | 'server': rows are shown unfiltered as given |
| headerFilters | boolean (default false) | Filter row under the column headers (3.5.0+): Input for string/number/date, Any/Yes/No select for boolean, valueOptions select for singleSelect, operator menu, clear button; 300 ms debounce. Each cell owns the root filterModel item with id 'header:<field>'; root logicOperator 'or', the field inside a group or another root item on the field shows a read-only "Custom filter" (opens the GridToolbar filter panel). Not rendered in pivot mode |
| headerFilterHeight | number (default 40) | Height of the header filter row (--ogx-header-filter-height) |
### Pagination
| Prop | Type | Description |
|-------------------------|-----------------------------------------|----------------------------------|
| pagination | boolean | Show the pager. Ignored under rowGroupingModel and with paginationMode='infinite' |
| paginationModel | GridPaginationModel | {page, pageSize} controlled (pass onPaginationModelChange too) |
| onPaginationModelChange | (model: GridPaginationModel) => void | Pagination change callback (also fires when the grid clamps a page past the end) |
| pageSizeOptions | number[] (default [10, 25, 50, 100]) | Available page sizes |
| paginationMode | 'client' / 'server' / 'infinite' | 'server': rows is the current page. 'infinite': needs dataSource |
| rowCount | number | Server total for paginationMode='server' without dataSource (dataSource response rowCount wins) |
### Column Features
| Prop | Type | Description |
|---------------------------|---------------------------|-------------------------------------------|
| pinnedColumns | GridColumnPinning | {left: string[], right: string[]}; rendered in that order |
| onPinnedColumnsChange | (model) => void | Pin change callback |
| columnVisibilityModel | Record<string, boolean> | {field: false} to hide |
| onColumnVisibilityModelChange | (model) => void | Visibility change callback |
| columnOrder | string[] | Controlled column order |
| onColumnOrderChange | ({column, oldIndex, targetIndex}) => void | One column moved; indices in the full current column order |
| onColumnOrderModelChange | (columnOrder: string[]) => void | Whole new order after every change, incl. Columns panel Reset (use this to control columnOrder) |
| disableColumnReorder | boolean | Disable drag-to-reorder |
| columnGroupingModel | GridColumnGroupingModel | Multi-level column header groups; drag-reorder stays within a group |
### Row Features
| Prop | Type | Description |
|-------------------|---------------------------|-------------------------------------------|
| pinnedRows | GridRowPinning | {top: id[], bottom: id[]}. Ignored under treeData / rowGroupingModel (dev warning) |
| rowReordering | boolean | Enable drag-and-drop row reordering (no effect in pivot mode) |
| onRowOrderChange | ({row, oldIndex, targetIndex}) => void | Indices are positions in the `rows` prop |
| getRowId | (row: R) => GridRowId | Custom row ID accessor (default: row.id). Row objects are NOT given an `id` |
### Cell Editing
| Prop | Type | Description |
|-------------------------|----------------------------|-----------------------------------------|
| processRowUpdate | (newRow, oldRow) => R / Promise<R> | Called once per commit. Must return the updated row |
| onProcessRowUpdateError | (error) => void | processRowUpdate threw, rejected or returned no row (editor stays open) |
| isCellEditable | (params: GridCellParams) => boolean | Gates double-click, Enter and Tab editing; rejected cells get aria-readonly |
### Detail Panels (Master-Detail)
| Prop | Type | Description |
|-------------------------|----------------------------|-----------------------------------------|
| getDetailPanelContent | (params: {row, id, rowIndex}) => ReactNode | Called for expanded data rows only (never group rows) |
| pinExpandColumn | boolean (default true) | Pin the detail-panel expand column |
| getDetailPanelHeight | (params) => number / 'auto'| px, or 'auto' = measured content height. Default 200 |
| detailPanelExpandedRowIds | Set<GridRowId> | Controlled expanded rows |
| onDetailPanelExpandedRowIdsChange | (ids: Set<GridRowId>) => void | Expand/collapse callback |
### Grouping & Aggregation
| Prop | Type | Description |
|------------------------|---------------------------|------------------------------------------|
| rowGroupingModel | string[] | Fields to group by (one nesting level per field). Disables pagination. groupable:false fields are skipped. Passing it controls grouping; without it the grid keeps its own model (3.5.0+: apiRef.setRowGroupingModel, AI assistant) |
| onRowGroupingModelChange | (model: string[]) => void | Grouping changed through apiRef.setRowGroupingModel or the AI assistant (3.5.0+) |
| aggregationModel | GridAggregationModel | {field: 'sum'/'avg'/'min'/'max'/'count'/'unique'} |
| onAggregationModelChange | (model) => void | Aggregation model changed (toolbar Summaries panel) |
| groupingColDef | Partial<GridColDef<R>> | Adds a dedicated `__group__` column pinned left (rowGroupingModel or treeData); no `field` needed; never exported |
| getAggregationPosition | (groupNode: GridTreeNode / null) => 'inline'/'footer'/null | Per group: 'inline' (default) on the group row, 'footer' on a subtotal row, null hides. Called with null for the grand total: null hides the footer |
| defaultGroupingExpansionDepth | number | Initial expansion depth. Default 0 (all collapsed); -1 = expand all |
### Tree Data
| Prop | Type | Description |
|---------------------------|----------------------------|--------------------------------------|
| treeData | boolean | Enable tree data mode |
| getTreeDataPath | (row) => string[] | Required with treeData (without it rows show flat). Keep it stable (module scope / useCallback) |
| defaultGroupingExpansionDepth | number | Same as grouping (default 0) |
Missing path segments become auto-created parent rows `{ id }` (id like `auto-group-["a","b"]`); their label is `params.rowMeta.groupLabel`.
### Pivot
| Prop | Type | Description |
|----------------|----------------|-----------------------------------------------|
| pivotMode | boolean | Enable pivot view (treeData / rowGrouping / getRowId / dataSource are ignored while pivoting) |
| pivotModel | GridPivotModel | {rowFields, columnFields, valueFields: {field, aggFn: 'sum'/'avg'/'count'/'min'/'max', headerName?}[]} |
| onPivotModelChange | (model) => void | Pivot change callback (toolbar Pivot panel) |
Pivot row ids look like `__pivot_row__:["North"]`; the Grand Total row is always last and cannot be selected.
### Server-Side
| Prop | Type | Description |
|------------|-----------------|----------------------------------------------------|
| dataSource | GridDataSource | Adapter for server-side fetching (see SERVER-SIDE DATA SOURCE) |
### Styling & Customization
| Prop | Type | Description |
|--------------|-----------------------|------------------------------------------|
| slots | { toolbar, pagination, noRowsOverlay, loadingOverlay, footer } | Replace built-in parts |
| slotProps | { toolbar, pagination, noRowsOverlay, loadingOverlay, footer } | Extra props for those slots |
| className | string | Extra CSS class on the root element |
| style | React.CSSProperties | Inline styles on the root element (use DataGridThemeProvider for theming) |
There are no getRowClassName / getCellClassName / theme props: use GridColDef.cellClassName (string or function) and DataGridThemeProvider.
### Callbacks
| Prop | Type | Description |
|-------------------|-----------------------|-------------------------------|
| onRowClick | (params: {row, id, rowIndex}) => void | Row click, or Enter on a non-editable cell. Not fired for synthetic group rows (a click toggles them) |
| onCellClick | (params: GridCellParams) => void | Cell click (not inside an open editor) |
| onRowDoubleClick | (params) => void | Row double-click (also fires for group rows) |
| onRowsScrollEnd | (params) => void | Fires once each time the bottom comes into view (re-armed when rows are added or the user scrolls away) |
| onStateChange | (state: GridState) => void | Fires when sorting, filter, pagination, columns (widths/order/pinning/visibility) or density change |
### State Persistence
| Prop | Type | Description |
|--------------|------------------|----------------------------------------------|
| initialState | GridInitialState | Starting sorting, filter, pagination, columns (widths, order, pinning, visibility) and density. Read at mount only |
| apiRef | MutableRefObject<GridApi<R>> or MutableRefObject<GridApi> | Imperative API handle from useGridApiRef<R>() (typed row getters) or useGridApiRef() |
### List View
| Prop | Type | Description |
|----------------|--------------------|---------------------------------------------|
| listView | boolean | Render as cards instead of a table (requires listViewColumn; without it the grid view shows + dev warning). Honours loading, noRowsOverlay/loadingOverlay and slots.footer; rows are a roving Tab stop (arrows, Home/End, PageUp/Down, Enter/Space, Alt+Arrow expand); tree/group parents get a chevron. Not virtualized (dev warning above 2,000 items; paginate) |
| listViewColumn | { field, renderCell } | Defines the card renderer |
---
## GridColDef<R> PROPERTIES
| Property | Type | Description |
|-------------------|--------------------------------------------------------|--------------------------------------------|
| field | string | **Required.** Key in row data |
| headerName | string | Column label |
| description | string | Header tooltip |
| width | number / string (px or %) | Column width. Neither width nor flex = flex: 1 |
| flex | number | Flex grow factor; `width` is ignored when flex is set |
| minWidth | number | Minimum width (layout and resize; resize default 50) |
| maxWidth | number | Maximum width (layout and resize; no max by default) |
| type | 'string'/'number'/'boolean'/'date'/'singleSelect'/'image' | Drives filter operators, sorting, editors and, without a valueFormatter, the cell text (date: local calendar date, boolean: Yes/No, singleSelect: option label, image: <img>); formattedValue, list view and exports use the same text |
| editable | boolean | Enable double-click / Enter editing |
| valueOptions | (string / number / {value, label})[] | Options for singleSelect type (editor commits the original option value) |
| renderCell | (params: GridRenderCellParams<R>) => ReactNode | Custom cell renderer (errors contained per cell) |
| renderHeader | (params: GridRenderHeaderParams) => ReactNode | Custom header renderer |
| renderEditCell | (params: GridRenderEditCellParams<R>) => ReactNode | Custom editor: call params.onValueChange(v), params.onCommit(), params.onCancel() |
| 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 |
| valueSetter | ({value, row, field}) => R | Write an edited value back onto the row (editable valueGetter columns) |
| 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) |
| fillValue | ({ value, sourceValues, index, direction, copy, row, id, field, colDef }) => unknown | Fill handle (3.5.0+): what a fill writes into this column's cell. value = the grid's choice (series / pattern / copy); return it to keep it. Throw skips the cell ('invalidValue') |
| valueFormatter | ({value, row, field}) => string | Format display value (value is `unknown`: cast before calling methods) |
| cellClassName | string / (params) => string | CSS class on cells |
| headerClassName | string | CSS class on header cell |
| colSpan | number / (params) => number | Cell column span (clamped to the pinned section; hidden columns skipped) |
| rowSpan | number / (params) => number | Cell row span (clamped to the row section) |
| align | 'left'/'center'/'right' | Cell content alignment |
| headerAlign | 'left'/'center'/'right' | Header content alignment |
| wrapHeaderText | boolean | Wrap this header title; overrides the grid prop (3.3.0+) |
| sortable | boolean (default true) | Enable sorting |
| 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 |
| filterable | boolean (default true) | Enable filtering and quick search |
| headerFilter | boolean (default true) | false: empty cell in the header filter row (3.5.0+) |
| headerFilterOperator | GridFilterOperator | Starting header filter operator (default: contains / = / is by type) (3.5.0+) |
| resizable | boolean (default true) | Enable resize handle (mouse, touch, Alt+Arrow on header); double-click or Enter on the handle auto-sizes to content (3.1.0+) |
| hideable | boolean (default true) | Allow hiding via toolbar / column menu |
| pinnable | boolean (default true) | Allow pinning via column menu (does not affect drag-reorder) |
| exportable | boolean (default true) | Include in CSV/Excel/JSON/Print/PDF exports and clipboard copy |
| disableColumnMenu | boolean | Hide the ⋮ column menu button |
| groupable | boolean | false = skipped by rowGroupingModel and the pivot panel |
| groupingValueFormatter | ({field, value}) => string | Group row label (default "field: value") |
| aggregable | boolean | false = no aggregation. Summaries panel offers number columns + aggregable: true |
| availableAggregationFunctions | string[] | Restrict aggregation functions for this column |
| 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 |
---
## GridApi METHODS (via useGridApiRef)
```tsx
import { useGridApiRef } from '@opencorestack/opengridx';
const apiRef = useGridApiRef();
<DataGrid apiRef={apiRef} rows={rows} columns={columns} />
// Row access
apiRef.current.getRow(id) // → R | null with useGridApiRef<R>(), else GridRowModel | null
apiRef.current.getAllRows() // → every loaded row
apiRef.current.getVisibleRows() // → rows on screen: current page + pinned rows
apiRef.current.getAllFilteredRows() // → every filtered + sorted row, all pages (use for exports); tree data + filter: matching rows only
apiRef.current.getGroupedExportRows() // → GridGroupedExportRow[] | null (row grouping only)
// Column access
apiRef.current.getColumn(field) // → GridColDef | null
apiRef.current.getAllColumns() // → GridColDef[]
apiRef.current.getVisibleColumns() // → visible columns in render order (left-pinned, unpinned, right-pinned)
// Selection
apiRef.current.selectRow(id, isSelected?) // select/deselect a row (synthetic group/total ids ignored; no-op fires nothing)
apiRef.current.selectRows(ids, isSelected?) // bulk select (same rules)
apiRef.current.getSelectedRows() // → GridRowId[]
// Sort / Filter (setters update the grid and fire the change callbacks)
apiRef.current.sortColumn(field, 'asc' | 'desc' | null)
apiRef.current.getSortModel() // → GridSortItem[]
apiRef.current.setFilterModel(model)
apiRef.current.getFilterModel() // → GridFilterModel
// Model setters (3.5.0+): each fires its change callback, like the UI
apiRef.current.setSortModel(model)
apiRef.current.setRowGroupingModel(fields)
apiRef.current.setAggregationModel(model)
apiRef.current.setColumnVisibilityModel(model)
apiRef.current.setPivotModel(model)
apiRef.current.getGridAiState() // → { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel }
apiRef.current.openAiAssistant() // opens the aiAssistant panel (no-op without aiAssistant); closeAiAssistant()
// Pagination
apiRef.current.setPage(page)
apiRef.current.setPageSize(size)
// Scroll
apiRef.current.scrollToIndexes({ rowIndex, colIndex })
// Auto-size to header + rendered cells, clamped to minWidth/maxWidth (3.1.0+)
apiRef.current.autosizeColumn(field)
apiRef.current.autosizeColumns(fields?)
// Clipboard
apiRef.current.copySelectedRows() // → Promise<void>; rejects if the write fails. Copies every selected row that passes
// the filter (all pages, collapsed groups, pinned rows), visible columns in screen order
// Cell range selection (cellSelection, 3.3.0+); inert while cellSelection is off
apiRef.current.selectCellRange({ id: 1, field: 'a' }, { id: 5, field: 'c' }) // anchor = active cell
apiRef.current.setCellSelectionModel(model)
apiRef.current.getCellSelectionModel() // → GridCellSelectionModel
apiRef.current.getSelectedCells() // → { id, field, value }[]; synthetic rows and span-covered cells left out
apiRef.current.copySelectedCells() // → Promise<void>; TSV, no header line
apiRef.current.clearCellSelection()
// Paste and undo/redo (3.4.0+)
apiRef.current.pasteText(text, anchor?) // → Promise<GridBatchEditResult>; anchor default: range top-left, else focused cell
apiRef.current.undo() // → Promise<GridBatchEditResult> (undoRedo)
apiRef.current.redo()
apiRef.current.canUndo() // → boolean
apiRef.current.canRedo()
apiRef.current.clearHistory()
// Aggregation
apiRef.current.getAggregationResult() // → Record<string, unknown> | null (null in pivot mode)
apiRef.current.getAggregationModel() // → GridAggregationModel | null
```
---
## CELL EDITING — FULL EXAMPLE
```tsx
// Your own row type: an interface or a type alias, no index signature needed (v3+)
interface Person { id: number; name: string; age: number; role: string }
const [rows, setRows] = useState<Person[]>(initialRows);
const processRowUpdate = (newRow: Person) => {
setRows(prev => prev.map(r => (r.id === newRow.id ? newRow : r)));
return newRow; // Must return the committed row
};
<DataGrid
rows={rows}
columns={[
{ field: 'name', headerName: 'Name', editable: true },
{ field: 'age', headerName: 'Age', editable: true, type: 'number' },
{ field: 'role', headerName: 'Role', editable: true, type: 'singleSelect',
valueOptions: ['Engineer', 'Manager', 'Analyst'] },
]}
processRowUpdate={processRowUpdate}
onProcessRowUpdateError={console.error}
/>
```
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.
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.
```tsx
import { Input } from '@opencorestack/opengridx';
function PriceEditor({ value, onValueChange, onCommit, onCancel }: GridRenderEditCellParams) {
const [text, setText] = useState(String(value ?? ''));
const ref = useRef<HTMLInputElement>(null);
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
const valid = text.trim() !== '' && Number(text) >= 0;
return <Input ref={ref} variant="cell" startAdornment="$" error={!valid} value={text}
onChange={e => { setText(e.target.value); const n = Number(e.target.value); if (e.target.value.trim() !== '' && n >= 0) onValueChange(n); }}
onKeyDown={e => { if (e.key === 'Enter') { e.stopPropagation(); if (valid) onCommit(); } if (e.key === 'Escape') { e.stopPropagation(); onCancel(); } }}
onBlur={() => (valid ? onCommit() : onCancel())} />;
}
```
---
## SERVER-SIDE DATA SOURCE
Pass a `dataSource` object to DataGrid; there is no separate data-source hook to import.
```tsx
import { DataGrid, type GridDataSource } from '@opencorestack/opengridx';
const dataSource: GridDataSource = {
// params: { startRow, endRow, sortModel, filterModel, groupKeys, aggregationModel? }
getRows: async ({ startRow, endRow, sortModel, filterModel, groupKeys }) => {
const res = await fetch('/api/rows', {
method: 'POST',
body: JSON.stringify({ startRow, endRow, sortModel, filterModel, groupKeys }),
});
const data = await res.json();
return { rows: data.items, rowCount: data.total }; // rowCount optional, needed for server paging
},
// Optional: totals for the aggregation footer
getAggregations: async ({ sortModel, filterModel, aggregationModel }) => {
return fetch('/api/totals').then(r => r.json());
},
};
<DataGrid
columns={columns}
rows={[]} // data comes from dataSource
dataSource={dataSource}
pagination
paginationMode="server"
sortingMode="server"
filterMode="server"
/>
```
- Requests are page-based: server pagination asks for `[page*pageSize, (page+1)*pageSize)`; client pagination (default modes) asks once for every row (`endRow: Number.MAX_SAFE_INTEGER`); `paginationMode="infinite"` asks for the rows after those loaded, up to `(page+1)*pageSize`, and appends them (advance `paginationModel.page` from `onRowsScrollEnd`).
- `endRow` is exclusive: return `allRows.slice(startRow, endRow)`.
- Server tree data: rows with `serverChildrenCount > 0` get an expand toggle; expanding calls `getRows` with the row's path in `groupKeys`. There is no `getChildren`.
- Refetch happens only when a model's content or the `getRows` function changes; stale responses are discarded; 300 ms debounce.
- A rejected `getRows` (or a response that cannot be processed) shows the error overlay; its Retry button calls `getRows` again.
- Row grouping with `paginationMode="server"` fetches every row in one request (`[0, Number.MAX_SAFE_INTEGER)`, dev warning). Server tree data with `defaultGroupingExpansionDepth` auto-expands and loads lazy nodes (one request per node).
---
## THEMING
```tsx
import { DataGridThemeProvider, darkTheme, type GridTheme } from '@opencorestack/opengridx';
// Built-in themes: darkTheme, roseTheme, emeraldTheme, amberTheme, compactTheme
<DataGridThemeProvider theme={darkTheme}>
<DataGrid rows={rows} columns={columns} />
</DataGridThemeProvider>
// Custom theme override:
const myTheme: GridTheme = {
mode: 'light', // base palette the provider pins ('light' default, or 'dark')
colors: {
primary: '#6366f1',
},
grid: {
headerBackground: '#eef2ff',
rowHoverBackground: '#f5f3ff',
rowHeightStandard: '40px', // theme heights size rows/header; rowHeight/headerHeight props win
},
overlays: {
background: '#ffffff',
border: '#e0e7ff',
},
};
<DataGridThemeProvider theme={myTheme}>
<DataGrid rows={rows} columns={columns} />
</DataGridThemeProvider>
// The provider pins a complete light or dark palette regardless of the OS colour scheme.
// Panels, menus and tooltips render into `.ogx-theme-provider`, so they inherit the theme.
```
---
## SLOTS CUSTOMIZATION
```tsx
import { GridToolbar } from '@opencorestack/opengridx';
<DataGrid
rows={rows}
columns={columns}
slots={{
toolbar: GridToolbar, // or your own component
pagination: MyPagination, // props: page, pageSize, rowCount, pageSizeOptions, onPageChange, onPageSizeChange, localeText
noRowsOverlay: EmptyState,
loadingOverlay: Spinner,
footer: CustomFooter, // replaces the pager; see note 14
}}
slotProps={{
toolbar: { showNonHideableColumns: true },
}}
/>
```
`GridToolbar` in `slots.toolbar` gets search, Columns, Filters and Summaries (plus Pivot when pivot is configured); they work without any filter/visibility props. Render props: `renderQuickFilter`, `renderColumnsButton`, `renderFilterButton`, `renderAggregationButton`, `renderExportButton`.
---
## STATE PERSISTENCE
```tsx
import { useGridStateStorage } from '@opencorestack/opengridx';
function MyGrid() {
const { initialState, onStateChange, clearState } = useGridStateStorage('my-grid-v1');
return (
<DataGrid
initialState={initialState}
onStateChange={onStateChange}
rows={rows}
columns={columns}
/>
);
}
```
Also accepts `{ key, debounceMs?, include?: (keyof GridState)[], storage? }`. `initialState` is read at mount: when the key changes, remount with `<DataGrid key={key} />`. If storage is blocked, the grid works without persistence.
---
## EXPORT UTILITIES
```tsx
import {
exportToCsv,
exportToExcel, // HTML table saved as .xls — zero deps
exportToExcelAdvanced, // real .xlsx — requires: npm install exceljs
exportToJson,
exportToPdf, // requires: npm install jspdf jspdf-autotable
printGrid,
} from '@opencorestack/opengridx';
const rows = apiRef.current.getAllFilteredRows(); // what the grid shows, all pages
const columns = apiRef.current.getVisibleColumns(); // visible columns, display order
exportToCsv(rows, columns, { fileName: 'export.csv' }); // UTF-8 BOM (bom: false to omit)
exportToExcel(rows, columns, { fileName: 'export.xls', sheetName: 'Data' });
await exportToExcelAdvanced(rows, columns, {
fileName: 'export.xlsx',
sheets: [{ name: 'Data' }, { name: 'Selected', rows: 'selected' }],
selectedRows: apiRef.current.getSelectedRows(),
columnStyles: {
avatar: { embedImage: true, imageWidth: 32, imageHeight: 32 }
}
});
exportToJson(rows, columns, { fileName: 'export.json' });
await exportToPdf(rows, columns, { fileName: 'export', title: 'My Report' });
// 3.2.0+: large PDFs — progress, cancel, size warning (never throws)
await exportToPdf(rows, columns, { onProgress: ({ phase, done, total }) => {}, signal: controller.signal, maxRows: 20000 });
await printGrid(rows, columns, 'My Report Title');
```
- Every export accepts `selectedRows`, `getRowId` (pass the grid's getRowId so ids match), `aggregationResult` + `aggregationModel` (totals row) and `groupedRows` from `apiRef.current.getGroupedExportRows()`.
- exportToPdf (3.2.0+) works in ~30 ms slices and yields between them (prepare and render phases), so the page stays responsive; only jsPDF's final save is one synchronous step. `onProgress({ phase: 'prepare'|'render'|'save', done, total })`; `signal` abort rejects with DOMException 'AbortError' and saves nothing; above `maxRows` (default 20000) a dev console.warn recommends exportToCsv / exportToExcelAdvanced. Output (pages, positions) is the same as before.
- CSV and exportToExcel neutralise formulas: text starting with = + - @ tab or CR gets a leading `'` (`escapeFormulas: false` opts out).
- exportToExcel writes HTML: a `.xlsx` fileName is saved as `.xls` with a console warning.
- Columns with `exportable: false` are left out everywhere; `valueGetter` and `valueFormatter` are applied.
---
## AI TOOLKIT (@opencorestack/opengridx/ai, 3.4.0+)
Separate entry point: no React, no dependencies, ~6.7 kB gzip with the 3.5.0 additions; 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.
```ts
import { getGridAiSchema, validateGridAiState } from '@opencorestack/opengridx/ai';
const schema = getGridAiSchema(columns, { include: ['filter', 'sort', 'grouping', 'aggregation'] });
const reply = await fetch('/api/grid-assistant', { method: 'POST', body: JSON.stringify({ prompt, schema, currentState }) });
const { state, errors } = validateGridAiState(await reply.text(), columns);
if (state.filterModel) setFilterModel(state.filterModel); // apply only the parts present
if (state.sortModel) setSortModel(state.sortModel);
```
- 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.
- 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__`.
- Privacy: no row data by default; `aiExamples` and `valueOptions` ARE sent; always apply `state`, never the raw reply.
- Types (from /ai): GridAiColumn, GridAiPart, GridAiSchema, GridAiSchemaOptions, GridAiState, GridAiValidateOptions, GridAiValidationError, GridAiValidationResult.
### AI toolkit part 2 (3.5.0+): Ask AI panel and agent tools
```tsx
import { createGridAiPromptHandler, createGridAgentTools } from '@opencorestack/opengridx/ai';
const onPrompt = createGridAiPromptHandler({
columns,
callModel: async (request, { signal }) => (await fetch('/api/grid-ai', { method: 'POST', body: JSON.stringify(request), signal })).json(),
});
<DataGrid columns={columns} rows={rows} slots={{ toolbar: GridToolbar }} aiAssistant={{ onPrompt, suggestions: ['Top 10 by revenue'] }} />
const tools = createGridAgentTools(apiRef, columns); // [{ name, description, inputSchema, execute }]
```
- `aiAssistant` prop: { onPrompt(prompt, { currentState, history, signal }) => Promise<{ state, errors, message? }>, placeholder?, suggestions?, voice? = true, parts? }. GridToolbar shows an "Ask AI" button (localeText.aiAssistantButton); apiRef.openAiAssistant() without a toolbar; slots.aiAssistantPanel replaces the panel (GridAiAssistantPanelProps). Events: onAiAssistantApply(result), onAiAssistantError(error).
- BRAND RULE: the grid applies a result only if `result.state` carries Symbol.for('opengridx.ai.validated'), set (non-enumerable) by validateGridAiState on the result and its state. Unbranded → not applied, "The reply was not validated" + dev warning. `{ ...result, message }` keeps the brand; `{ ...result.state }` loses it.
- Applying: immediately, one undoable step (Undo restores the models from before the reply; separate from the cell-edit undoRedo history). Each change is a removable chip; removing one re-applies the reply without it. Parts replace the grid's models, except columnVisibilityModel (merged). Parts outside `parts`, and parts the validator emptied (all entries dropped), are not applied. Voice: mic button only where SpeechRecognition / webkitSpeechRecognition exists; the microphone is requested only on click. Panel: role=dialog "Ask AI", Escape closes and returns focus, status in the grid's live region; Stop aborts `signal`.
- createGridAiPromptHandler({ columns, callModel, parts?, schemaOptions? }): schema built once; callModel({ prompt, schema, currentState, history }, { signal }); reply = state object | JSON string | text with a ```json fence (surrounding text / a `message` key → result.message; plain text → message only); validated + branded. callModel throw → { state: {}, errors: [{ path: '', message }] }; abort → rejects AbortError.
- createGridAgentTools(apiRef, columns, { allowRowAccess?, fields?, maxRows? = 20 }): get_grid_state, set_filter, set_sort, set_grouping, set_aggregation, set_column_visibility (merged), clear_filters, and get_row_summary ONLY with allowRowAccess: true (the one tool that sends rows). Inputs validated with validateGridAiState; any error → nothing applied, { ok: false, errors }. execute never throws. Works with the Vercel AI SDK, CopilotKit, OpenAI / Anthropic tool calling, WebMCP.
- More types (from /ai): GridAiPromptContext, GridAiPromptResult, GridAiHistoryEntry, GridAiAssistantOptions, GridAiAssistantPanelProps, GridAiChip, GridAiModelRequest, GridAiPromptHandlerOptions, GridAgentTool, GridAgentToolResult, GridAgentToolsOptions, GridAgentApi. /ai is ~6.7 kB gzip (budget 8 kB).
---
## CSS CUSTOMIZATION
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):
```css
.my-grid-wrapper {
--ogx-color-primary: #6366f1;
--ogx-grid-header-background: #eef2ff;
--ogx-grid-row-hover-background: rgba(99, 102, 241, 0.08);
--ogx-grid-border-color: #e0e7ff;
--ogx-grid-cell-font-size: 13px;
--ogx-border-radius-md: 8px;
}
```
Derived tokens (e.g. `--ogx-grid-cell-focus-border`, which defaults to the primary colour) are resolved on `:root`, so override them directly when you change `--ogx-color-primary` on a wrapper.
Class names use the `ogx__` BEM prefix, e.g.:
- `ogx__row`, `ogx__row--selected`, `ogx__row--even`, `ogx__row--odd`
- `ogx__cell`, `ogx__cell--focused`, `ogx__cell--editing`
- 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`)
- Fill handle (3.5.0+): `ogx__cell-fill-handle` (24px hit area in the corner cell, visible square is `::after`), variable `--ogx-fill-handle-color` (defaults to `--ogx-range-border`; theme key `grid.fillHandleColor`)
- `ogx__header-cell`, `ogx__header-cell--sorted`, `ogx__header-cell--wrap` (wrapHeaderText, 3.3.0+)
- Header filter row (3.5.0+): `ogx__header-filter-row`, `ogx__header-filter-cell` (`--focused`, `--active`, `--custom`, `--empty`, `--system`, `--pinned*`), `ogx__header-filter-operator`, `ogx__header-filter-menu`, `ogx__header-filter-input-wrapper` / `ogx__header-filter-input`, `ogx__header-filter-select`, `ogx__header-filter-clear`, `ogx__header-filter-custom`; height variable `--ogx-header-filter-height`
- Sticky wrappers: `ogx__sticky-top` (header + top-pinned rows), `ogx__sticky-bottom` (bottom-pinned rows + aggregation footer)
- 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>`)
---
## TYPE REFERENCE
```ts
type GridRowId = string | number;
interface GridRowModel { id: GridRowId; [key: string]: unknown; }
type GridSortDirection = 'asc' | 'desc' | null;
type GridPinnedPosition = 'left' | 'right';
type GridFilterOperator =
| 'contains' | 'equals' | 'startsWith' | 'endsWith' // string
| 'isEmpty' | 'isNotEmpty' | 'isAnyOf'
| '=' | '!=' | '>' | '>=' | '<' | '<=' // number
| 'is' | 'not' | 'after' | 'onOrAfter' | 'before' | 'onOrBefore'; // date / boolean / singleSelect
interface GridSortItem { field: string; sort: 'asc' | 'desc'; }
interface GridFilterItem { id?: string | number; field: string; operator: GridFilterOperator; value?: unknown; }
interface GridFilterGroup { id?: string | number; logicOperator: 'and' | 'or'; items: (GridFilterItem | GridFilterGroup)[]; }
interface GridFilterModel {
items?: (GridFilterItem | GridFilterGroup)[];
logicOperator?: 'and' | 'or';
quickFilterValues?: string[]; // every term must match some visible, filterable column
}
interface GridPaginationModel { page: number; pageSize: number; }
interface GridColumnPinning { left?: string[]; right?: string[]; }
interface GridRowPinning { top?: GridRowId[]; bottom?: GridRowId[]; }
interface GridRenderCellParams<R = GridRowModel> {
value: unknown; formattedValue?: string; row: R; field: string;
colDef: GridColDef<R>; rowIndex: number; colIndex: number;
rowMeta?: GridRowMeta; // hierarchy info for tree-data / grouping rows
}
interface GridRowMeta {
hasChildren?: boolean; treeDepth?: number; groupingField?: string; groupingValue?: unknown;
groupLabel?: string; descendantCount?: number; isExpanded?: boolean;
isGroupRow?: boolean; isGroupFooter?: boolean;
}
interface GridDataSource<R = GridRowModel> {
getRows: (params: GridGetRowsParams) => Promise<GridGetRowsResponse<R>>;
getAggregations?: (params: Omit<GridGetRowsParams, 'startRow' | 'endRow'>) => Promise<Record<string, unknown>>;
}
interface GridGetRowsParams {
startRow: number; endRow: number; // endRow exclusive; Number.MAX_SAFE_INTEGER = all rows
sortModel: GridSortItem[]; filterModel: GridFilterModel;
groupKeys: string[]; // server tree data: path of the parent being expanded
aggregationModel?: GridAggregationModel;
}
interface GridGetRowsResponse<R = GridRowModel> {
rows: R[]; rowCount?: number; aggregationResults?: Record<string, unknown>;
}
```
---
## KEYBOARD
Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, PageUp/PageDown; Enter edits an editable cell, otherwise acts like a row click (toggles group/tree rows); Tab leaves the grid (while editing it moves to the next editable cell); Shift+Space selects the row (Space toggles the checkbox or detail panel when those cells are focused); Ctrl/Cmd+A selects all; Ctrl/Cmd+C copies the selection; Alt+ArrowRight/Left expands/collapses; Enter/Space on a header sorts (Shift adds to the sort); Alt+ArrowDown or Ctrl+Enter on a header opens the column menu; Alt+ArrowLeft/Right on a header resizes it. With headerFilters (3.5.0+): ArrowDown on a header moves to its filter cell, ArrowDown again to the first row; Enter/Space or typing focuses the filter control, Escape returns to the cell, Alt+ArrowDown opens the operator menu, Delete clears the filter; controls are not Tab stops (Tab leaves the grid).
---
## KEY NOTES FOR AI AGENTS
1. **Always `return newRow`** from `processRowUpdate` — returning nothing calls `onProcessRowUpdateError` and keeps the editor open.
2. **`getRowId` default is `row.id`** — if your data uses a different key, always pass `getRowId`. The grid does not write an `id` onto your rows, and exports need `getRowId` in their options to match `selectedRows`.
3. **CSS must be imported**: always add `import '@opencorestack/opengridx/styles'` to the app root. The JS bundle does not load it; dev builds warn once ("The grid stylesheet is not loaded") when it is missing.
4. **Optional peers**: `exportToExcelAdvanced` needs `exceljs`; `exportToPdf` needs `jspdf` + `jspdf-autotable`.
5. **`dataSource` + `rows={[]}`**: with a dataSource pass an empty `rows` array — the grid fills it via `dataSource.getRows`.
6. **`flex` columns**: columns with `flex` share the free space; `width` is ignored on them (use `minWidth` / `maxWidth`). A column with neither `width` nor `flex` acts like `flex: 1`.
7. **`singleSelect` editing**: for dropdown editors in cells, set `type: 'singleSelect'` and provide `valueOptions`.
8. **`initialState.columns.columnWidths`**: accepts `Record<string, number>` to restore saved column widths. Every part of `initialState` is optional.
9. **`pinnedColumns`** and **`columnWidths`** are independent state — pinning does not reset widths.
10. **Aggregation**: setting `aggregationModel` alone renders a sticky footer with the totals; with `rowGroupingModel` each group row also shows its own aggregates. `null`/`undefined`, blank strings, booleans and arrays are ignored by sum/avg/min/max; min/max of dates return a `Date`; `valueGetter` columns aggregate the computed value; footer totals go through the column's `valueFormatter` (not for count/unique).
11. **GOTCHAS — bounded height**: virtualization only works if the grid's container has a bounded height. A flex child without `min-height: 0` grows to fit every row, so every row is rendered. Pagination hides this, and row grouping turns pagination off. Without a `height` prop the grid fills its container (3.0.1+), so use `<div style={{ flex: 1, minHeight: 0 }}>` around the grid, or pass a fixed pixel `height`. The list view never virtualizes: paginate it. Dev builds log a warning when this happens.
12. **Pagination is ignored under row grouping** — a grouped grid is one scrollable, virtualized list.
13. **`valueFormatter` + `renderCell`**: `renderCell` receives `params.formattedValue` (v2.1+), so a custom renderer can reuse the formatter instead of duplicating it.
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`.
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`.
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×.
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.
18. **`valueFormatter` / `valueGetter` params are `unknown`-typed**: cast (`(value as number).toFixed(2)`) before calling methods, or TypeScript strict mode rejects it.
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.
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`.
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`.
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.
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.
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`.
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.
26. **localeText covers every UI string (3.9+)**: ~200 optional keys (column menu, toolbar, filter / columns / pivot panels, AI panel, pagination, row and header checkboxes, overlays, announcements), English by default. An inline object of strings is fine; define function entries (`rowSelectLabel: id => …`) outside the render so rows stay memoised. Components used outside a grid show English.
27. **Theme keys added in 3.9**: `grid.checkboxCheckedBackground`, `checkboxCheckmark`, `expandIconColor`, `expandIconHoverColor`; `overlays.itemIndicatorColor` (menu selection dot), `itemSelectedIcon`, `iconColor`, `sectionLabelText`, `dividerColor`. The demo's Theme Builder edits all of them.
28. **3.10**: the AI assistant's change chips are `localeText` keys too (`aiAssistantChipSort(field, direction)`, `aiAssistantChipSummary(fn, field)`, …). Select-all at 1M rows takes ~130 ms. The library forbids `as unknown as` outside documented boundaries and `any` everywhere (ESLint), and CI checks that `llms.txt`, CLAUDE.md and the CHANGELOG name the `package.json` version.
29. **Large client-side datasets**: sorting or filtering 1M rows blocks for ~0.4–0.5 s (one native sort). There is no time-sliced mode (measured and rejected: 1.5–2× slower overall). For interactive sorting and filtering at that size use a `dataSource`.
30. **3.10.1**: at 1M rows select-all ~60–90 ms, deselect-all < 50 ms, applying a grouping ~190 ms, the quick filter's first search ~1.6–2 s (later keystrokes ~210 ms). Grouping creates leaf tree nodes and row metadata only when read, still one `params.rowMeta` object per row. A Next.js 16 App Router fixture in the package smoke suite re-checks the Next.js guide on every CI run.