@opencorestack/opengridx 1.0.2 → 1.0.3

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.0.3] — July 30, 2026 📚
9
+
10
+ ### Fixed
11
+ - **API reference expanded to full surface coverage**: `docs/API_REFERENCE.md` grew from 188 to 1,132 lines. Added 8 new top-level sections — server-side data source (`GridDataSource`, `GridGetRowsParams`, `GridGetRowsResponse`), all event callback param types (`GridRowParams`, `GridCellParams`, `GridColumnOrderChangeParams`, `GridRowOrderChangeParams`, `GridDetailPanelParams`), column and row pinning types, full filter model deep reference (per-type operator table, `GridFilterGroup` nesting), grid state and initial state slice reference, aggregation reference (all 6 built-in functions, `getAggregationPosition` semantics), row grouping, column group headers, and list view. DataGrid props section restructured into 13 feature-area sub-tables covering every previously undocumented prop. Developers building server-side sorting + aggregation + pinned columns no longer need to read TypeScript source.
12
+
13
+ ---
14
+
8
15
  ## [1.0.2] — July 30, 2026 📚
9
16
 
10
17
  ### Fixed
@@ -39,6 +39,129 @@ The main component for displaying and interacting with data.
39
39
  | `processRowUpdate` | `(new, old) => R \| Promise<R>` | — | Fired after a cell edit is committed. |
40
40
  | `dataSource` | `GridDataSource` | — | Remote data provider interface. |
41
41
 
42
+ #### Sorting, Filtering & Pagination
43
+
44
+ | Prop | Type | Default | Description |
45
+ | :--- | :--- | :--- | :--- |
46
+ | `sortingMode` | `'client' \| 'server'` | `'client'` | Set to `'server'` when your backend handles sorting. The grid calls `onSortModelChange` but does not re-sort rows locally. |
47
+ | `filterMode` | `'client' \| 'server'` | `'client'` | Set to `'server'` when your backend handles filtering. The grid calls `onFilterModelChange` but does not re-filter rows locally. |
48
+ | `onSortModelChange` | `(model: GridSortItem[]) => void` | — | Fired when the active sort model changes. |
49
+ | `onFilterModelChange` | `(model: GridFilterModel) => void` | — | Fired when the active filter model changes. |
50
+
51
+ #### Row Selection
52
+
53
+ | Prop | Type | Default | Description |
54
+ | :--- | :--- | :--- | :--- |
55
+ | `rowSelectionModel` | `GridRowSelectionModel` | `[]` | Controlled selection state — array of selected row IDs. |
56
+ | `onRowSelectionModelChange` | `(model: GridRowSelectionModel) => void` | — | Fired when the selection changes. |
57
+ | `disableRowSelectionOnClick` | `boolean` | `false` | When `true`, clicking a row does not toggle its selection. |
58
+ | `disableMultipleRowSelection` | `boolean` | `false` | When `true`, at most one row can be selected at a time. |
59
+
60
+ #### Column Visibility
61
+
62
+ | Prop | Type | Default | Description |
63
+ | :--- | :--- | :--- | :--- |
64
+ | `columnVisibilityModel` | `GridColumnVisibilityModel` | — | Map of `field → boolean` controlling which columns are visible (`false` = hidden). |
65
+ | `onColumnVisibilityModelChange` | `(model: GridColumnVisibilityModel) => void` | — | Fired when the visibility of any column changes. |
66
+
67
+ #### Column & Row Reordering
68
+
69
+ | Prop | Type | Default | Description |
70
+ | :--- | :--- | :--- | :--- |
71
+ | `disableColumnReorder` | `boolean` | `false` | Disables drag-and-drop column reordering. |
72
+ | `columnOrder` | `GridColumnOrder` | — | Controlled ordered array of column field names. |
73
+ | `onColumnOrderChange` | `(params: GridColumnOrderChangeParams) => void` | — | Fired after a column is dragged to a new position. |
74
+ | `rowReordering` | `boolean` | `false` | Enables drag-and-drop row reordering. |
75
+ | `onRowOrderChange` | `(params: GridRowOrderChangeParams) => void` | — | Fired after a row is dragged to a new position. |
76
+
77
+ #### Pinning
78
+
79
+ | Prop | Type | Default | Description |
80
+ | :--- | :--- | :--- | :--- |
81
+ | `pinnedColumns` | `GridColumnPinning` | — | Columns pinned to the left or right viewport edges. See [Column & Row Pinning](#-column--row-pinning). |
82
+ | `onPinnedColumnsChange` | `(model: GridColumnPinning) => void` | — | Fired when column pinning changes. |
83
+ | `pinnedRows` | `GridRowPinning` | — | Row IDs pinned to the top or bottom of the viewport. |
84
+ | `onPinnedRowsChange` | `(model: GridRowPinning) => void` | — | Fired when row pinning changes. |
85
+ | `pinCheckboxColumn` | `boolean` | `false` | Keeps the checkbox column visible during horizontal scrolling. |
86
+ | `pinExpandColumn` | `boolean` | `false` | Keeps the Master-Detail expansion column visible during horizontal scrolling. |
87
+
88
+ #### Inline Editing
89
+
90
+ | Prop | Type | Default | Description |
91
+ | :--- | :--- | :--- | :--- |
92
+ | `isCellEditable` | `(params: GridCellParams) => boolean` | — | Per-cell predicate. Return `false` to make a specific cell read-only even when the column has `editable: true`. |
93
+ | `onProcessRowUpdateError` | `(error: unknown) => void` | — | Fired if `processRowUpdate` throws or returns a rejected Promise. Use to display validation errors or restore the previous row value. |
94
+
95
+ #### Master-Detail
96
+
97
+ | Prop | Type | Default | Description |
98
+ | :--- | :--- | :--- | :--- |
99
+ | `getDetailPanelContent` | `(params: GridDetailPanelParams) => ReactNode` | — | Returns the JSX content rendered inside the expandable detail panel. |
100
+ | `getDetailPanelHeight` | `(params: GridDetailPanelParams) => number \| 'auto'` | `'auto'` | Controls the panel height in pixels, or `'auto'` to fit content. |
101
+ | `detailPanelExpandedRowIds` | `Set<GridRowId>` | — | Controlled set of currently-expanded detail panel row IDs. |
102
+ | `onDetailPanelExpandedRowIdsChange` | `(ids: Set<GridRowId>) => void` | — | Fired when detail panels expand or collapse. |
103
+
104
+ #### Tree Data
105
+
106
+ | Prop | Type | Default | Description |
107
+ | :--- | :--- | :--- | :--- |
108
+ | `treeData` | `boolean` | `false` | Enables hierarchical tree data display. |
109
+ | `getTreeDataPath` | `(row: R) => string[]` | — | Returns the hierarchy path for a row (e.g. `['Engineering', 'Frontend']`). |
110
+ | `groupingColDef` | `GridColDef` | — | Overrides the auto-generated tree expand/collapse column definition. |
111
+ | `defaultGroupingExpansionDepth` | `number` | `0` | Number of tree levels expanded on initial render (`-1` = all). |
112
+
113
+ #### Row Grouping & Aggregation
114
+
115
+ | Prop | Type | Default | Description |
116
+ | :--- | :--- | :--- | :--- |
117
+ | `rowGroupingModel` | `GridRowGroupingModel` | `[]` | Array of field names to group rows by (e.g. `['department', 'team']`). See [Row Grouping](#️-row-grouping). |
118
+ | `onRowGroupingModelChange` | `(model: GridRowGroupingModel) => void` | — | Fired when the grouping model changes. |
119
+ | `aggregationModel` | `GridAggregationModel` | — | Map of `field → aggFn` (e.g. `{ salary: 'sum', age: 'avg' }`). See [Aggregation Reference](#-aggregation-reference). |
120
+ | `onAggregationModelChange` | `(model: GridAggregationModel) => void` | — | Fired when the aggregation model changes. |
121
+ | `getAggregationPosition` | `(groupNode: GridTreeNode \| null) => 'inline' \| 'footer' \| null` | — | Controls where aggregation results appear. `'inline'` = inside the group row, `'footer'` = a dedicated row below the group, `null` = hidden. Pass `null` groupNode = grand-total (root) position. |
122
+
123
+ #### Pivot
124
+
125
+ | Prop | Type | Default | Description |
126
+ | :--- | :--- | :--- | :--- |
127
+ | `pivotMode` | `boolean` | `false` | Switches the grid to multidimensional Pivot Mode. |
128
+ | `pivotModel` | `GridPivotModel` | — | Controlled pivot configuration (row fields, column fields, value fields). |
129
+ | `onPivotModelChange` | `(model: GridPivotModel) => void` | — | Fired when the pivot model changes. |
130
+
131
+ #### Scroll & Viewport
132
+
133
+ | Prop | Type | Default | Description |
134
+ | :--- | :--- | :--- | :--- |
135
+ | `onRowsScrollEnd` | `(params: GridRowScrollEndParams) => void` | — | Fired when the user scrolls to the bottom of the grid viewport. Use this to trigger the next page in infinite-scroll mode. |
136
+
137
+ #### Accessibility & Appearance
138
+
139
+ | Prop | Type | Default | Description |
140
+ | :--- | :--- | :--- | :--- |
141
+ | `ariaLabel` | `string` | — | ARIA label for the grid container element. |
142
+ | `noRowsLabel` | `string` | `'No rows'` | Message shown in the empty-state overlay when there are no rows. |
143
+ | `className` | `string` | — | Additional CSS class applied to the outermost grid wrapper element. |
144
+ | `style` | `React.CSSProperties` | — | Inline styles applied to the grid wrapper element. |
145
+
146
+ #### Imperative Ref
147
+
148
+ | Prop | Type | Default | Description |
149
+ | :--- | :--- | :--- | :--- |
150
+ | `apiRef` | `React.MutableRefObject<GridApi>` | — | Reactive ref to the imperative API. Create with `useGridApiRef()`. |
151
+
152
+ #### List View
153
+
154
+ | Prop | Type | Default | Description |
155
+ | :--- | :--- | :--- | :--- |
156
+ | `listView` | `boolean` | `false` | Renders the grid as a single-column list of cards. Designed for mobile and responsive layouts. |
157
+ | `listViewColumn` | `GridListViewColDef` | — | Column definition for list view mode. Must provide a `renderCell` function. The `field` value is used as a key. |
158
+
159
+ #### Column Group Headers
160
+
161
+ | Prop | Type | Default | Description |
162
+ | :--- | :--- | :--- | :--- |
163
+ | `columnGroupingModel` | `GridColumnGroupingModel` | — | Defines spanning header rows above the regular column headers. See [Column Group Headers](#️-column-group-headers). |
164
+
42
165
  ---
43
166
 
44
167
  ## 🕹️ Imperative API (`GridApi`)
@@ -387,3 +510,623 @@ interface GridPivotModel {
387
510
  valueFields: { field: string; aggFn: string; headerName?: string }[];
388
511
  }
389
512
  ```
513
+
514
+ ---
515
+
516
+ ## 📡 Event Callback Types
517
+
518
+ These interfaces describe the parameter objects passed to every event callback prop.
519
+
520
+ ### `GridRowParams<R>`
521
+ Passed to `onRowClick`.
522
+
523
+ | Property | Type | Description |
524
+ | :--- | :--- | :--- |
525
+ | `row` | `R` | The full row data object. |
526
+ | `id` | `GridRowId` | Unique identifier of the row. |
527
+ | `rowIndex` | `number` | Zero-based index of the row in the visible dataset. |
528
+
529
+ ### `GridCellParams<R>`
530
+ Passed to `onCellClick` and `isCellEditable`.
531
+
532
+ | Property | Type | Description |
533
+ | :--- | :--- | :--- |
534
+ | `row` | `R` | The full row data object. |
535
+ | `field` | `string` | The column field name. |
536
+ | `value` | `unknown` | The cell value (after `valueGetter`, before `valueFormatter`). |
537
+ | `colDef` | `GridColDef<R>` | The column definition. |
538
+ | `rowIndex` | `number` | Zero-based row index in the visible dataset. |
539
+ | `colIndex` | `number` | Zero-based column index. |
540
+
541
+ ### `GridColumnOrderChangeParams`
542
+ Passed to `onColumnOrderChange`.
543
+
544
+ | Property | Type | Description |
545
+ | :--- | :--- | :--- |
546
+ | `column` | `GridColDef` | The column definition that was moved. |
547
+ | `oldIndex` | `number` | Previous column index. |
548
+ | `targetIndex` | `number` | New column index after the move. |
549
+
550
+ ### `GridRowOrderChangeParams<R>`
551
+ Passed to `onRowOrderChange`.
552
+
553
+ | Property | Type | Description |
554
+ | :--- | :--- | :--- |
555
+ | `row` | `R` | The row data object that was moved. |
556
+ | `oldIndex` | `number` | Previous row index. |
557
+ | `targetIndex` | `number` | New row index after the move. |
558
+
559
+ ### `GridRowScrollEndParams`
560
+ Passed to `onRowsScrollEnd`.
561
+
562
+ | Property | Type | Description |
563
+ | :--- | :--- | :--- |
564
+ | `visibleTop` | `number` | Pixel offset of the topmost visible row. |
565
+ | `visibleBottom` | `number` | Pixel offset of the bottommost visible row. |
566
+ | `viewportHeight` | `number` | Current height of the scroll viewport in pixels. |
567
+
568
+ ### `GridDetailPanelParams<R>`
569
+ Passed to `getDetailPanelContent` and `getDetailPanelHeight`.
570
+
571
+ | Property | Type | Description |
572
+ | :--- | :--- | :--- |
573
+ | `row` | `R` | The full row data object. |
574
+ | `id` | `GridRowId` | Unique identifier of the row. |
575
+ | `rowIndex` | `number` | Zero-based index of the row in the visible dataset. |
576
+
577
+ ---
578
+
579
+ ## 🌐 Server-Side Data Source (`GridDataSource`)
580
+
581
+ The `dataSource` prop is the primary integration point for connecting the grid to a remote API. When set, the grid delegates data fetching — including pagination, sorting, filtering, and optionally aggregation — to your `getRows` function instead of processing the data locally.
582
+
583
+ ### `GridDataSource<R>`
584
+
585
+ | Property | Type | Required | Description |
586
+ | :--- | :--- | :--- | :--- |
587
+ | `getRows` | `(params: GridGetRowsParams) => Promise<GridGetRowsResponse<R>>` | Yes | Called whenever the grid needs data. The promise must resolve with rows and optionally a total row count. |
588
+ | `getAggregations` | `(params: Omit<GridGetRowsParams, 'startRow' \| 'endRow'>) => Promise<Record<string, unknown>>` | No | Called to fetch aggregate values for the full dataset (e.g. column totals). If omitted, aggregation results from `getRows.aggregationResults` are used instead. |
589
+
590
+ ### `GridGetRowsParams`
591
+ The object passed to `getRows` by the grid on every data fetch.
592
+
593
+ | Property | Type | Description |
594
+ | :--- | :--- | :--- |
595
+ | `startRow` | `number` | Zero-based index of the first row to fetch (based on `page * pageSize`). |
596
+ | `endRow` | `number` | Zero-based index of the last row to fetch (exclusive). |
597
+ | `sortModel` | `GridSortItem[]` | Active sort configuration. Empty array when no sort is applied. |
598
+ | `filterModel` | `GridFilterModel` | Active filter state. Has empty `items` when no filters are applied. |
599
+ | `groupKeys` | `string[]` | Path of grouping key values for the current group level (used in lazy-loaded tree/grouping). Empty array for the root level. |
600
+ | `aggregationModel` | `GridAggregationModel \| undefined` | Active aggregation configuration, forwarded so the server can compute column summaries. |
601
+
602
+ ### `GridGetRowsResponse<R>`
603
+ The object your `getRows` function must resolve with.
604
+
605
+ | Property | Type | Required | Description |
606
+ | :--- | :--- | :--- | :--- |
607
+ | `rows` | `R[]` | Yes | The rows for the requested page/range. |
608
+ | `rowCount` | `number` | No | Total number of rows in the full dataset. Required for server-side pagination; optional for infinite scroll. |
609
+ | `aggregationResults` | `Record<string, unknown>` | No | Column aggregation values for this response (e.g. `{ salary: 84500, age: 34.2 }`). |
610
+
611
+ ### Complete Server-Side Usage Example
612
+
613
+ ```tsx
614
+ import {
615
+ DataGrid,
616
+ GridDataSource,
617
+ GridGetRowsParams,
618
+ GridGetRowsResponse,
619
+ GridRowModel,
620
+ GridColDef,
621
+ } from '@opencorestack/opengridx';
622
+
623
+ interface Employee extends GridRowModel {
624
+ id: number;
625
+ name: string;
626
+ department: string;
627
+ salary: number;
628
+ }
629
+
630
+ const columns: GridColDef<Employee>[] = [
631
+ { field: 'name', headerName: 'Name', flex: 1 },
632
+ { field: 'department', headerName: 'Department', width: 160 },
633
+ { field: 'salary', headerName: 'Salary', type: 'number', width: 130 },
634
+ ];
635
+
636
+ const dataSource: GridDataSource<Employee> = {
637
+ getRows: async (params: GridGetRowsParams): Promise<GridGetRowsResponse<Employee>> => {
638
+ const response = await fetch('/api/employees', {
639
+ method: 'POST',
640
+ headers: { 'Content-Type': 'application/json' },
641
+ body: JSON.stringify(params),
642
+ });
643
+ const data = await response.json();
644
+ // data must have shape { rows: Employee[], total: number }
645
+ return { rows: data.rows, rowCount: data.total };
646
+ },
647
+
648
+ getAggregations: async (params) => {
649
+ const response = await fetch('/api/employees/aggregations', {
650
+ method: 'POST',
651
+ headers: { 'Content-Type': 'application/json' },
652
+ body: JSON.stringify(params),
653
+ });
654
+ return response.json(); // e.g. { salary: 4250000 }
655
+ },
656
+ };
657
+
658
+ export default function EmployeeGrid() {
659
+ return (
660
+ <DataGrid<Employee>
661
+ rows={[]}
662
+ columns={columns}
663
+ dataSource={dataSource}
664
+ paginationMode="server"
665
+ sortingMode="server"
666
+ filterMode="server"
667
+ pagination
668
+ pageSizeOptions={[25, 50, 100]}
669
+ height={600}
670
+ />
671
+ );
672
+ }
673
+ ```
674
+
675
+ **How the grid decides when to call `getRows`:** The hook fires (with a 300 ms debounce) whenever `paginationMode`, `sortingMode`, or `filterMode` is set to `'server'` AND a `dataSource` is provided. Any change to `sortModel`, `filterModel`, or `paginationModel` re-triggers the fetch. Stale responses from superseded requests are silently discarded — only the latest request updates the grid.
676
+
677
+ ---
678
+
679
+ ## 📌 Column & Row Pinning
680
+
681
+ ### `GridColumnPinning`
682
+
683
+ Pinned columns stay visible at the left or right edge of the grid during horizontal scrolling.
684
+
685
+ ```typescript
686
+ interface GridColumnPinning {
687
+ left?: string[]; // field names pinned to the left edge
688
+ right?: string[]; // field names pinned to the right edge
689
+ }
690
+ ```
691
+
692
+ ### `GridRowPinning`
693
+
694
+ Pinned rows stay visible at the top or bottom of the viewport during vertical scrolling. The values are **row IDs**, not full row objects.
695
+
696
+ ```typescript
697
+ interface GridRowPinning {
698
+ top?: GridRowId[]; // IDs of rows pinned to the top
699
+ bottom?: GridRowId[]; // IDs of rows pinned to the bottom
700
+ }
701
+ ```
702
+
703
+ ### Column-level pinning control
704
+
705
+ Set `pinnable: false` on a `GridColDef` to prevent that column from being pinned via the UI menu.
706
+
707
+ ### Usage Example
708
+
709
+ ```tsx
710
+ import { DataGrid, GridColumnPinning, GridRowPinning } from '@opencorestack/opengridx';
711
+
712
+ <DataGrid
713
+ rows={rows}
714
+ columns={columns}
715
+ // Pin 'name' to the left and 'actions' to the right
716
+ pinnedColumns={{ left: ['name'], right: ['actions'] }}
717
+ // Pin rows with id=1 to the top, id=99 to the bottom
718
+ pinnedRows={{ top: [1], bottom: [99] }}
719
+ // Checkbox and expand columns stay visible while scrolling horizontally
720
+ checkboxSelection
721
+ pinCheckboxColumn
722
+ pinExpandColumn
723
+ />
724
+ ```
725
+
726
+ ---
727
+
728
+ ## 🔍 Filter Model Deep Reference
729
+
730
+ ### `GridFilterModel`
731
+
732
+ The top-level filter state passed to `filterModel` and `onFilterModelChange`.
733
+
734
+ ```typescript
735
+ interface GridFilterModel {
736
+ items?: (GridFilterItem | GridFilterGroup)[];
737
+ logicOperator?: 'and' | 'or'; // default: 'and'
738
+ quickFilterValues?: string[]; // global search terms
739
+ }
740
+ ```
741
+
742
+ | Property | Type | Default | Description |
743
+ | :--- | :--- | :--- | :--- |
744
+ | `items` | `(GridFilterItem \| GridFilterGroup)[]` | `[]` | Root-level filter conditions or nested groups. |
745
+ | `logicOperator` | `'and' \| 'or'` | `'and'` | How to combine the root `items`. |
746
+ | `quickFilterValues` | `string[]` | `[]` | Each string must match at least one field in the row (all strings must match). Applied before `items`. |
747
+
748
+ ### `GridFilterItem`
749
+
750
+ A single column filter condition.
751
+
752
+ | Property | Type | Required | Description |
753
+ | :--- | :--- | :--- | :--- |
754
+ | `id` | `string \| number` | No | Optional stable identifier for this item (useful for controlled updates). |
755
+ | `field` | `string` | Yes | The column field to filter on. |
756
+ | `operator` | `GridFilterOperator` | Yes | The comparison operator to apply. |
757
+ | `value` | `unknown` | No | The value to compare against. Not required for `isEmpty` / `isNotEmpty`. |
758
+
759
+ ### `GridFilterGroup`
760
+
761
+ A nested group of filter conditions combined with a logical operator. Groups can be nested arbitrarily.
762
+
763
+ | Property | Type | Required | Description |
764
+ | :--- | :--- | :--- | :--- |
765
+ | `id` | `string \| number` | No | Optional stable identifier. |
766
+ | `logicOperator` | `'and' \| 'or'` | Yes | How to combine the child `items` in this group. |
767
+ | `items` | `(GridFilterItem \| GridFilterGroup)[]` | Yes | Child conditions or nested groups. |
768
+
769
+ ### `GridFilterOperator`
770
+
771
+ ```typescript
772
+ type GridFilterOperator =
773
+ | 'contains' | 'equals' | 'startsWith' | 'endsWith'
774
+ | 'isEmpty' | 'isNotEmpty' | 'isAnyOf'
775
+ | '>' | '>=' | '<' | '<=' | '!='
776
+ | 'is' | 'not';
777
+ ```
778
+
779
+ ### Default Operators per Column Type
780
+
781
+ The filter panel shows only the operators relevant to each column's `type`. These are also the operators used when the grid applies client-side filtering.
782
+
783
+ | Column `type` | Default operator | Available operators |
784
+ | :--- | :--- | :--- |
785
+ | `string` (default) | `contains` | `contains`, `equals`, `startsWith`, `endsWith`, `isEmpty`, `isNotEmpty` |
786
+ | `number` | `=` (numeric eq.) | `=`, `!=`, `>`, `>=`, `<`, `<=`, `isEmpty`, `isNotEmpty` |
787
+ | `date` | `is` | `is`, `not`, `after`, `onOrAfter`, `before`, `onOrBefore`, `isEmpty`, `isNotEmpty` |
788
+ | `boolean` | `is` | `is` |
789
+ | `singleSelect` | `isAnyOf` | `isAnyOf`, `is`, `not` |
790
+
791
+ ### Nested Group Example
792
+
793
+ ```tsx
794
+ import { DataGrid, GridFilterModel } from '@opencorestack/opengridx';
795
+
796
+ const filterModel: GridFilterModel = {
797
+ logicOperator: 'and',
798
+ items: [
799
+ // Simple item: department equals Engineering
800
+ { id: 1, field: 'department', operator: 'equals', value: 'Engineering' },
801
+ // Nested group: salary > 80000 OR title contains 'Lead'
802
+ {
803
+ id: 2,
804
+ logicOperator: 'or',
805
+ items: [
806
+ { id: 3, field: 'salary', operator: '>', value: 80000 },
807
+ { id: 4, field: 'title', operator: 'contains', value: 'Lead' },
808
+ ],
809
+ },
810
+ ],
811
+ };
812
+
813
+ <DataGrid rows={rows} columns={columns} filterModel={filterModel} filterMode="client" />
814
+ ```
815
+
816
+ ---
817
+
818
+ ## 💾 `GridInitialState` and `GridState`
819
+
820
+ `GridInitialState` is a type alias for `GridState`. Pass it to `initialState` to restore persisted grid state on mount (e.g. from `localStorage`). Each key corresponds to a feature slice — all are optional.
821
+
822
+ ```typescript
823
+ type GridInitialState = GridState;
824
+
825
+ interface GridState {
826
+ sorting?: GridSortingState;
827
+ filter?: GridFilterState;
828
+ pagination?: GridPaginationState;
829
+ columns?: GridColumnsState;
830
+ density?: GridDensityState;
831
+ dataSource?: GridDataSourceState;
832
+ }
833
+ ```
834
+
835
+ ### State Slices
836
+
837
+ | Slice key | Interface | Shape |
838
+ | :--- | :--- | :--- |
839
+ | `sorting` | `GridSortingState` | `{ sortModel: GridSortItem[] }` |
840
+ | `filter` | `GridFilterState` | `{ filterModel: GridFilterModel }` |
841
+ | `pagination` | `GridPaginationState` | `{ paginationModel: GridPaginationModel; rowCount?: number }` |
842
+ | `columns` | `GridColumnsState` | `{ columnWidths: Record<string, number>; columnOrder: string[]; pinnedColumns?: GridColumnPinning; columnVisibilityModel?: Record<string, boolean> }` |
843
+ | `density` | `GridDensityState` | `{ density: 'compact' \| 'standard' \| 'comfortable' }` |
844
+ | `dataSource` | `GridDataSourceState` | `{ loading: boolean; error?: unknown }` (read-only — not meaningful to restore) |
845
+
846
+ ### Restoring State from `localStorage`
847
+
848
+ The recommended way is `useGridStateStorage` (see Hooks section), but you can also drive it manually:
849
+
850
+ ```tsx
851
+ import { DataGrid, GridState, GridInitialState } from '@opencorestack/opengridx';
852
+ import { useState, useCallback } from 'react';
853
+
854
+ const STORAGE_KEY = 'my-grid-state';
855
+
856
+ function loadState(): GridInitialState | undefined {
857
+ try {
858
+ const raw = localStorage.getItem(STORAGE_KEY);
859
+ return raw ? (JSON.parse(raw) as GridInitialState) : undefined;
860
+ } catch {
861
+ return undefined;
862
+ }
863
+ }
864
+
865
+ export default function PersistentGrid() {
866
+ const [initialState] = useState<GridInitialState | undefined>(loadState);
867
+
868
+ const handleStateChange = useCallback((state: GridState) => {
869
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
870
+ }, []);
871
+
872
+ return (
873
+ <DataGrid
874
+ rows={rows}
875
+ columns={columns}
876
+ initialState={initialState}
877
+ onStateChange={handleStateChange}
878
+ />
879
+ );
880
+ }
881
+ ```
882
+
883
+ ---
884
+
885
+ ## ∑ Aggregation Reference
886
+
887
+ ### `GridAggregationModel`
888
+
889
+ A plain object mapping column field names to aggregation function names.
890
+
891
+ ```typescript
892
+ interface GridAggregationModel {
893
+ [field: string]: string; // e.g. { salary: 'sum', age: 'avg', name: 'count' }
894
+ }
895
+ ```
896
+
897
+ ### `GridAggregationResult`
898
+
899
+ The computed output, also a plain object mapping field names to their computed values.
900
+
901
+ ```typescript
902
+ type GridAggregationResult = Record<string, unknown>;
903
+ // e.g. { salary: 420000, age: 34.2, name: 12 }
904
+ ```
905
+
906
+ ### Built-In Aggregation Functions
907
+
908
+ | Function | Input | Output | Description |
909
+ | :--- | :--- | :--- | :--- |
910
+ | `sum` | numeric values | `number` | Sum of all non-null numeric values in the group. |
911
+ | `avg` | numeric values | `number \| null` | Arithmetic mean. Returns `null` if there are no numeric values. |
912
+ | `min` | numeric values | `number \| null` | Smallest numeric value. Returns `null` if there are no numeric values. |
913
+ | `max` | numeric values | `number \| null` | Largest numeric value. Returns `null` if there are no numeric values. |
914
+ | `count` | any values | `number` | Count of non-null values. |
915
+ | `unique` | any values | `number` | Count of distinct non-null values (uses a `Set`). |
916
+
917
+ All built-in functions are exported as the `BuiltInAggFn` type: `'sum' | 'avg' | 'count' | 'min' | 'max' | 'unique'`.
918
+
919
+ Use `formatAggregationValue(value, fnName)` to turn a raw result into a display string. `avg` is formatted to 2 decimal places; all other numbers use `toLocaleString()`.
920
+
921
+ ### `getAggregationPosition` Callback
922
+
923
+ ```typescript
924
+ getAggregationPosition?: (groupNode: GridTreeNode | null) => 'inline' | 'footer' | null
925
+ ```
926
+
927
+ Called once per group node (and once with `null` for the grand-total / root level) to decide where the aggregated row appears:
928
+
929
+ | Return value | Effect |
930
+ | :--- | :--- |
931
+ | `'inline'` | Aggregation values appear inside the group header row itself. |
932
+ | `'footer'` | A separate aggregation row is rendered below the group's last row. |
933
+ | `null` | Aggregation result is hidden for this group. |
934
+
935
+ ### Aggregation + Row Grouping Example
936
+
937
+ ```tsx
938
+ import {
939
+ DataGrid,
940
+ GridColDef,
941
+ GridRowModel,
942
+ GridTreeNode,
943
+ GridAggregationModel,
944
+ GridRowGroupingModel,
945
+ } from '@opencorestack/opengridx';
946
+
947
+ interface Employee extends GridRowModel {
948
+ id: number;
949
+ name: string;
950
+ department: string;
951
+ salary: number;
952
+ }
953
+
954
+ const columns: GridColDef<Employee>[] = [
955
+ { field: 'name', headerName: 'Name', flex: 1 },
956
+ { field: 'department', headerName: 'Department', width: 160, groupable: true },
957
+ { field: 'salary', headerName: 'Salary', type: 'number', width: 130, aggregable: true },
958
+ ];
959
+
960
+ const aggregationModel: GridAggregationModel = { salary: 'sum' };
961
+ const rowGroupingModel: GridRowGroupingModel = ['department'];
962
+
963
+ export default function GroupedGrid() {
964
+ return (
965
+ <DataGrid<Employee>
966
+ rows={rows}
967
+ columns={columns}
968
+ rowGroupingModel={rowGroupingModel}
969
+ aggregationModel={aggregationModel}
970
+ getAggregationPosition={(groupNode: GridTreeNode | null) => {
971
+ // Show grand total at the footer, group totals inline
972
+ return groupNode === null ? 'footer' : 'inline';
973
+ }}
974
+ />
975
+ );
976
+ }
977
+ ```
978
+
979
+ ---
980
+
981
+ ## 🗂️ Row Grouping
982
+
983
+ ### `GridRowGroupingModel`
984
+
985
+ A simple array of field names that defines which columns the grid groups rows by, in order.
986
+
987
+ ```typescript
988
+ type GridRowGroupingModel = string[];
989
+ // e.g. ['department'] → one level of grouping
990
+ // e.g. ['department', 'team'] → two levels (department → team → rows)
991
+ ```
992
+
993
+ The referenced fields must have `groupable: true` (the default) on their `GridColDef`.
994
+
995
+ ### Usage Example
996
+
997
+ ```tsx
998
+ import { DataGrid, GridRowGroupingModel, GridAggregationModel } from '@opencorestack/opengridx';
999
+ import { useState } from 'react';
1000
+
1001
+ export default function GroupingExample() {
1002
+ const [rowGroupingModel, setRowGroupingModel] = useState<GridRowGroupingModel>(['department']);
1003
+ const [aggregationModel, setAggregationModel] = useState<GridAggregationModel>({
1004
+ salary: 'sum',
1005
+ age: 'avg',
1006
+ });
1007
+
1008
+ return (
1009
+ <DataGrid
1010
+ rows={rows}
1011
+ columns={columns}
1012
+ rowGroupingModel={rowGroupingModel}
1013
+ onRowGroupingModelChange={setRowGroupingModel}
1014
+ aggregationModel={aggregationModel}
1015
+ onAggregationModelChange={setAggregationModel}
1016
+ />
1017
+ );
1018
+ }
1019
+ ```
1020
+
1021
+ ### Interaction with `aggregationModel`
1022
+
1023
+ When `rowGroupingModel` is set, the grid creates group nodes (see `GridTreeNode`). The `aggregationModel` controls which columns show summaries on those nodes. The `getAggregationPosition` callback gives per-group control over whether the summary appears inline (in the group header row) or as a footer row below the group. For server-side grouping, return aggregation values in `GridGetRowsResponse.aggregationResults`.
1024
+
1025
+ ---
1026
+
1027
+ ## 🗃️ Column Group Headers
1028
+
1029
+ Column groups render spanning header cells above the normal column header row, allowing multi-level column organisation.
1030
+
1031
+ ### `GridColumnGroup`
1032
+
1033
+ ```typescript
1034
+ interface GridColumnGroup {
1035
+ groupId: string; // unique identifier for this group
1036
+ headerName: string; // text shown in the spanning header cell
1037
+ headerClassName?: string; // optional CSS class for custom styling
1038
+ children: Array<string | GridColumnGroup>; // field names (leaf) or nested groups
1039
+ }
1040
+
1041
+ type GridColumnGroupingModel = GridColumnGroup[];
1042
+ ```
1043
+
1044
+ The `children` array can contain:
1045
+ - **`string`** — a column `field` name, making this a leaf group that spans exactly those columns.
1046
+ - **`GridColumnGroup`** — a nested sub-group, enabling multi-level spanning headers.
1047
+
1048
+ ### Usage Example
1049
+
1050
+ ```tsx
1051
+ import { DataGrid, GridColumnGroup, GridColumnGroupingModel } from '@opencorestack/opengridx';
1052
+
1053
+ const columnGroupingModel: GridColumnGroupingModel = [
1054
+ {
1055
+ groupId: 'personal',
1056
+ headerName: 'Personal Info',
1057
+ children: ['firstName', 'lastName', 'age'],
1058
+ },
1059
+ {
1060
+ groupId: 'compensation',
1061
+ headerName: 'Compensation',
1062
+ children: [
1063
+ { groupId: 'base', headerName: 'Base', children: ['salary', 'bonus'] },
1064
+ { groupId: 'equity', headerName: 'Equity', children: ['options', 'rsu'] },
1065
+ ],
1066
+ },
1067
+ ];
1068
+
1069
+ export default function GroupedHeadersGrid() {
1070
+ return (
1071
+ <DataGrid
1072
+ rows={rows}
1073
+ columns={columns}
1074
+ columnGroupingModel={columnGroupingModel}
1075
+ />
1076
+ );
1077
+ }
1078
+ ```
1079
+
1080
+ The grid renders two header rows: the spanning group row on top, and the regular per-column header row below. Groups that are not referenced in `columnGroupingModel` are rendered without a spanning header.
1081
+
1082
+ ---
1083
+
1084
+ ## 📋 List View
1085
+
1086
+ List view renders the grid as a single-column list of cards, replacing all normal column layout. It is designed for narrow viewports and mobile layouts.
1087
+
1088
+ ### `GridListViewColDef<R>`
1089
+
1090
+ ```typescript
1091
+ interface GridListViewColDef<R extends GridRowModel = GridRowModel> {
1092
+ field: string;
1093
+ renderCell: (params: GridRenderCellParams<R>) => React.ReactNode;
1094
+ }
1095
+ ```
1096
+
1097
+ The `renderCell` function receives the full `GridRenderCellParams` (including `row`), so you have access to every field of the row to build a rich card layout.
1098
+
1099
+ ### Usage Example
1100
+
1101
+ ```tsx
1102
+ import { DataGrid, GridListViewColDef, GridRenderCellParams } from '@opencorestack/opengridx';
1103
+
1104
+ interface Employee extends GridRowModel {
1105
+ id: number;
1106
+ name: string;
1107
+ department: string;
1108
+ salary: number;
1109
+ }
1110
+
1111
+ const listViewColumn: GridListViewColDef<Employee> = {
1112
+ field: 'card',
1113
+ renderCell: (params: GridRenderCellParams<Employee>) => (
1114
+ <div style={{ padding: '12px 16px' }}>
1115
+ <strong>{params.row.name}</strong>
1116
+ <span> — {params.row.department}</span>
1117
+ <div>${params.row.salary.toLocaleString()}</div>
1118
+ </div>
1119
+ ),
1120
+ };
1121
+
1122
+ export default function MobileGrid() {
1123
+ return (
1124
+ <DataGrid<Employee>
1125
+ rows={rows}
1126
+ columns={columns}
1127
+ listView
1128
+ listViewColumn={listViewColumn}
1129
+ />
1130
+ );
1131
+ }
1132
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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",