@opencorestack/opengridx 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -0
  3. package/dist/ai.cjs +2 -1
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +203 -3
  6. package/dist/ai.es.js +2 -1
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +248 -4
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +7104 -5726
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -16
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +88 -7
  15. package/docs/README.md +3 -1
  16. package/docs/architecture/datagrid-orchestration.md +9 -4
  17. package/docs/components/filter-panel.md +1 -0
  18. package/docs/components/header.md +4 -1
  19. package/docs/components/toolbar.md +7 -0
  20. package/docs/customization/theming.md +3 -1
  21. package/docs/features/ai-toolkit.md +99 -5
  22. package/docs/features/cell-selection.md +8 -2
  23. package/docs/features/fill-handle.md +125 -0
  24. package/docs/features/filtering.md +3 -0
  25. package/docs/features/header-filters.md +138 -0
  26. package/docs/features/keyboard-navigation.md +20 -2
  27. package/docs/roadmap.md +2 -3
  28. package/lib/ai/agentTools.ts +160 -0
  29. package/lib/ai/index.ts +19 -2
  30. package/lib/ai/promptHandler.ts +109 -0
  31. package/lib/ai/types.ts +89 -25
  32. package/lib/ai/validate.ts +10 -0
  33. package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
  34. package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
  35. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
  36. package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
  37. package/lib/components/Cell/Cell.css +28 -0
  38. package/lib/components/Cell/Cell.tsx +6 -2
  39. package/lib/components/DataGrid/DataGrid.tsx +76 -3
  40. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
  41. package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
  42. package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
  43. package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
  44. package/lib/components/Header/Header.tsx +44 -23
  45. package/lib/components/Header/headerColumnStyle.ts +28 -0
  46. package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
  47. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
  48. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
  49. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
  50. package/lib/components/Toolbar/GridToolbar.tsx +51 -0
  51. package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
  52. package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
  53. package/lib/hooks/core/useGridApiMethods.ts +39 -0
  54. package/lib/hooks/core/useGridAriaRows.ts +5 -3
  55. package/lib/hooks/core/useGridCellSelection.ts +77 -11
  56. package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
  57. package/lib/hooks/core/useGridControlledState.ts +27 -0
  58. package/lib/hooks/core/useGridFillHandle.ts +323 -0
  59. package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
  60. package/lib/hooks/core/useGridToolbarProps.ts +19 -0
  61. package/lib/hooks/features/useGridAiAssistant.ts +284 -0
  62. package/lib/hooks/features/useGridBatchEdit.ts +2 -2
  63. package/lib/index.ts +3 -0
  64. package/lib/styles/opengridx.css +2 -0
  65. package/lib/theme/DataGridThemeProvider.tsx +2 -0
  66. package/lib/theme/types.ts +2 -0
  67. package/lib/types/index.ts +219 -1
  68. package/lib/utils/aiAssistant.ts +205 -0
  69. package/lib/utils/aiBrand.ts +17 -0
  70. package/lib/utils/cellSelection.ts +25 -5
  71. package/lib/utils/editing/fill.ts +238 -0
  72. package/lib/utils/filtering/index.ts +14 -0
  73. package/lib/utils/focus/index.ts +9 -3
  74. package/lib/utils/headerFilters.ts +189 -0
  75. package/llms.txt +44 -4
  76. package/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ # Fill Handle
2
+
3
+ With [`cellSelection`](cell-selection.md) on, the selected range has a small square on its bottom-right corner: the **fill handle** (v3.5). Drag it down, up, right or left to fill the next cells from the range, as in Excel. **Ctrl/Cmd+D** and **Ctrl/Cmd+R** fill from the keyboard. Every fill is written like a paste, through `valueSetter` and `processRowUpdate`, and is one [undo](undo-redo.md) step. Demo: **Fill Handle** (`/fill-handle`).
4
+
5
+ ```tsx
6
+ import { DataGrid, type GridFillResult } from '@opencorestack/opengridx';
7
+
8
+ <DataGrid
9
+ rows={rows}
10
+ columns={columns} // fills only write editable columns
11
+ cellSelection // the fill handle comes with range selection
12
+ undoRedo // optional: one fill = one undo step
13
+ onFill={(result: GridFillResult) => console.log(result.direction, result.updated, result.skipped)}
14
+ processRowUpdate={saveRow} // called once per filled row
15
+ />
16
+ ```
17
+
18
+ ## Dragging the handle
19
+
20
+ - The handle sits on the bottom-right cell of the range, or on the only selected cell. It is shown while at least one column is `editable` and no editor is open.
21
+ - Drag it beyond the range. The range grows in **one direction**: the side of the range the cell under the pointer lies beyond. When that cell lies beyond both a row edge and a column edge (a diagonal drag), the larger pointer movement wins.
22
+ - Past the edge of the grid the body auto-scrolls, as in a range drag, also over rows and columns that are not rendered yet.
23
+ - Release to fill. Releasing over the original range, pressing Escape or losing the pointer writes nothing and restores the range.
24
+ - Hold **Alt** while releasing to **copy** instead of continuing a series (Excel uses Ctrl for this, which the grid keeps for range shortcuts).
25
+ - The handle has a 24 px hit area and `touch-action: none`, so a touch drag on it fills instead of scrolling the grid.
26
+
27
+ After the fill, the range covers the source and the filled cells.
28
+
29
+ ## What is written
30
+
31
+ Each line along the fill (each column for a fill down or up, each row for a fill right or left) is filled from its source cells, read towards the new cells:
32
+
33
+ | Source cells of the line | Fill |
34
+ | :--- | :--- |
35
+ | One cell | Copies the value |
36
+ | Two or more numbers with an exactly equal step | Continues the series: 2, 4, 6 → 8, 10. Decimals are rounded to the source's decimal places, so 0.1, 0.2 → 0.3, not 0.30000000000000004 |
37
+ | Two or more dates on the same day of the month, an equal number of months apart | Continues by months: 15 Jan, 15 Feb → 15 Mar. The day is clamped to the month's end (31 Jan, 31 Mar → 31 May; 31 Dec, 31 Jan → 28 Feb) |
38
+ | Two or more dates at the same time of day, an equal number of days apart | Continues by days: Mon 5 Oct, Tue 6 Oct → Wed 7 Oct (calendar days, so a daylight-saving change does not break it) |
39
+ | Anything else (text, booleans, unequal steps, mixed types, empty cells) | Repeats the source pattern: A, B → A, B, A |
40
+
41
+ Filling up or left reads the source in that direction: 7, 9 filled up gives 5, 3.
42
+
43
+ Values are typed already, so `valueParser` is not involved: a number copied into a text column stays a number. Synthetic rows (group headers, subtotals, tree auto-parents) in the source are left out of the series.
44
+
45
+ ### `GridColDef.fillValue`
46
+
47
+ A column can decide what a fill writes into its cells:
48
+
49
+ ```ts
50
+ {
51
+ field: 'amount', type: 'number', editable: true,
52
+ // params.value is what the grid would write; return it to keep it.
53
+ fillValue: ({ value, sourceValues, index, direction, copy, row, id }) =>
54
+ typeof value === 'number' ? Math.max(0, Math.round(value)) : value,
55
+ }
56
+ ```
57
+
58
+ | Param | Description |
59
+ | :--- | :--- |
60
+ | `value` | The grid's choice: the next value of the series, the repeated pattern or the copy |
61
+ | `sourceValues` | The line's source values in fill order (the value next to the first filled cell is last) |
62
+ | `index` | Position of the cell among the filled cells of its line, from 0 |
63
+ | `direction` | `'down'`, `'up'`, `'right'` or `'left'` |
64
+ | `copy` | `true` when Alt was held or the fill came from Ctrl/Cmd+D or Ctrl/Cmd+R |
65
+ | `row`, `id`, `field`, `colDef` | The cell being filled |
66
+
67
+ A `fillValue` that throws skips that cell with reason `'invalidValue'` (and warns once in development).
68
+
69
+ ## Keyboard
70
+
71
+ While focus is on a cell of the grid and no editor is open:
72
+
73
+ | Key | Action |
74
+ | :--- | :--- |
75
+ | **Ctrl+D** (Cmd+D) | Copies the range's top row into the rows below it |
76
+ | **Ctrl+R** (Cmd+R) | Copies the range's left column into the columns right of it |
77
+
78
+ The grid takes these keys (`preventDefault`, so no bookmark dialog or page reload) whenever a range or focused cell exists, `cellSelection` is on and `disableFillHandle` is not set. A range of one row (Ctrl+D) or one column (Ctrl+R) has nothing to fill. Inside an editor or a text field you render in a cell, the keys keep their native meaning.
79
+
80
+ ## How it writes
81
+
82
+ A fill is a batch edit, the same path as [paste](clipboard.md):
83
+
84
+ - `valueSetter` (when the column has one) builds each row, and `processRowUpdate` runs **once per row**; rows run in parallel, and a row that throws or rejects is reported to `onProcessRowUpdateError` without stopping the others.
85
+ - All rows that succeeded are stored in one update.
86
+ - With `undoRedo`, the whole fill is **one undo step**.
87
+ - Cells that cannot be written are skipped with a reason:
88
+
89
+ | Reason | Cell |
90
+ | :--- | :--- |
91
+ | `'notEditable'` | The column is not `editable`, `isCellEditable` returned false, the row is synthetic, or a span covers the cell |
92
+ | `'invalidValue'` | The column's `fillValue` threw |
93
+
94
+ `onFill` receives the result plus the direction:
95
+
96
+ ```ts
97
+ interface GridFillResult extends GridBatchEditResult {
98
+ direction: 'down' | 'up' | 'right' | 'left';
99
+ }
100
+ // { updated: [3, 4, 5], failed: [], skipped: [{ id: 3, field: 'owner', reason: 'notEditable' }], direction: 'down' }
101
+ ```
102
+
103
+ ## Opting out
104
+
105
+ `disableFillHandle` hides the handle and leaves Ctrl/Cmd+D and Ctrl/Cmd+R to the browser. A grid without `cellSelection` has no fill handle.
106
+
107
+ ## Styling
108
+
109
+ | Hook | Description |
110
+ | :--- | :--- |
111
+ | `.ogx__cell-fill-handle` | The handle: a 24 × 24 px hit area in the corner of the cell (`cursor: crosshair`, `touch-action: none`). The visible square is its `::after` |
112
+ | `--ogx-fill-handle-color` | The square's colour. Defaults to the range border (`--ogx-range-border`); theme key `grid.fillHandleColor` |
113
+
114
+ ```css
115
+ .my-grid { --ogx-fill-handle-color: #16a34a; }
116
+ .my-grid .ogx__cell-fill-handle::after { width: 9px; height: 9px; }
117
+ ```
118
+
119
+ ## API
120
+
121
+ | Member | Description |
122
+ | :--- | :--- |
123
+ | `disableFillHandle?: boolean` | Hides the handle and turns off Ctrl/Cmd+D and Ctrl/Cmd+R. Default `false` |
124
+ | `onFill?(result: GridFillResult)` | Fired after every fill |
125
+ | `GridColDef.fillValue?(params: GridFillValueParams): unknown` | Decides the value written into a cell of the column |
@@ -7,6 +7,7 @@ OpenGridX provides a robust filtering system with support for both client-side a
7
7
  - **Column Filters**: Specific operators for different data types (string, number, date, etc.).
8
8
  - **Multi-Filter Groups**: Support for `AND`/`OR` logic operators.
9
9
  - **Server-Side Filtering**: Offload complex queries to your backend.
10
+ - **Header Filters** (v3.5): a filter row under the column headers (`headerFilters`) that edits the same model. See [Header Filters](header-filters.md).
10
11
 
11
12
  ---
12
13
 
@@ -121,3 +122,5 @@ The filter panel is accessible through the built-in `GridToolbar`. To use your o
121
122
  ```
122
123
 
123
124
  The `FilterPanel` component is also exported from `@opencorestack/opengridx` if you need to embed it inside a custom layout. See [`<FilterPanel />`](../components/filter-panel.md) for how it edits the model.
125
+
126
+ For a filter control on every column, set `headerFilters`: a row under the headers whose cells own the root items `id: 'header:<field>'` of the same `filterModel`, so the panel, the quick filter and the header row stay in sync. See [Header Filters](header-filters.md).
@@ -0,0 +1,138 @@
1
+ # Header Filters
2
+
3
+ *Since v3.5.0*
4
+
5
+ A filter row under the column headers, as in Excel's AutoFilter row: each column gets a small control (a text box, a number or date box, or a select), an operator button and a clear button. It reads and writes the grid's one `filterModel`, so the [filter panel](../components/filter-panel.md), the quick filter, `apiRef.setFilterModel` and server-side filtering keep working alongside it.
6
+
7
+ ```tsx
8
+ import { DataGrid } from '@opencorestack/opengridx';
9
+
10
+ <DataGrid
11
+ rows={rows}
12
+ columns={columns}
13
+ headerFilters
14
+ height={500}
15
+ />
16
+ ```
17
+
18
+ The row is off unless `headerFilters` is set. Without it the grid renders and behaves exactly as before.
19
+
20
+ ## Controls per column type
21
+
22
+ | Column `type` | Control | Starting operator |
23
+ | :--- | :--- | :--- |
24
+ | `string` (default) | text box (`Input`) | `contains` |
25
+ | `number` | text box (`Input`, decimal keyboard on touch devices) | `=` |
26
+ | `date` | date box (`Input type="date"`) | `is` |
27
+ | `boolean` | select: Any / Yes / No | `is` |
28
+ | `singleSelect` | select of `valueOptions` (Any + every option) | `is` |
29
+ | `image` | none (empty cell) | — |
30
+
31
+ - The **operator button** left of the box shows the current operator as a symbol (`∋` contains, `=`, `≥`, `≠` …) and opens a menu of the operators the column type allows (the same list as the filter panel). `isEmpty` / `isNotEmpty` need no value: the box is replaced by the operator's name.
32
+ - The **clear button** (×) appears while the column has a filter and removes it.
33
+ - Typing is **debounced by 300 ms**, as in the filter panel. Selects apply at once. Typing that is still waiting is committed if the column scrolls out of the render window or the row is turned off.
34
+ - A `singleSelect` column without `valueOptions` gets a text box.
35
+
36
+ ### Column options
37
+
38
+ | `GridColDef` field | Description |
39
+ | :--- | :--- |
40
+ | `headerFilter?: boolean` | `false` leaves the column's cell empty. Columns with `filterable: false` and `type: 'image'` have no control either. |
41
+ | `headerFilterOperator?: GridFilterOperator` | The operator a new filter starts with, instead of the type's default (for example `'>='` for amounts, `'startsWith'` for codes). |
42
+
43
+ ### Grid props
44
+
45
+ | Prop | Type | Default | Description |
46
+ | :--- | :--- | :--- | :--- |
47
+ | `headerFilters` | `boolean` | `false` | Renders the filter row. |
48
+ | `headerFilterHeight` | `number` | `40` | Height of the filter row in pixels (sets `--ogx-header-filter-height`). |
49
+
50
+ ## One shared filter model
51
+
52
+ The row does not keep a model of its own. Each cell **owns one root-level item**, the one with `id: 'header:<field>'`:
53
+
54
+ ```ts
55
+ // After typing "glo" under Customer and choosing "≥ 100" under Total:
56
+ {
57
+ items: [
58
+ { id: 'header:customer', field: 'customer', operator: 'contains', value: 'glo' },
59
+ { id: 'header:total', field: 'total', operator: '>=', value: '100' },
60
+ ],
61
+ }
62
+ ```
63
+
64
+ - A cell adds its item at the end of `items`, updates it in place and removes it when cleared. Every other item and group, `logicOperator` and `quickFilterValues` pass through untouched.
65
+ - Values are stored as the filter panel stores them: typed text as a string (number operators parse it), dates as `YYYY-MM-DD`, booleans as `true` / `false`, `singleSelect` values as the option's `value` (a one-item list for `isAnyOf`).
66
+ - The filter panel shows and edits the same item: it edits the first root item of each column and keeps its `id`, so a header filter changed in the panel is still the header filter.
67
+ - An item with an empty value does not filter. An empty box with the starting operator removes the item; with another operator the item stays, so the chosen operator is kept while the value is retyped.
68
+
69
+ ### Models the row cannot show
70
+
71
+ The row never rewrites a model it cannot represent. A cell shows a read-only **"Custom filter"** label instead of its control when:
72
+
73
+ - the root `logicOperator` is `'or'` (every cell: adding an item would widen the OR);
74
+ - the column's field appears inside a filter group;
75
+ - the column has a root item that is not `header:<field>` (one added by the filter panel, `apiRef`, an AI assistant or your own code), or more than one root item.
76
+
77
+ With a built-in `GridToolbar` in `slots.toolbar`, the label is a button that opens the toolbar's filter panel, where the condition can be edited. Without one it is plain text. Clearing the model (the panel's *Clear all*, `setFilterModel({ items: [] })`) brings the controls back.
78
+
79
+ ## Server-side filtering
80
+
81
+ With `filterMode="server"` the grid does not filter rows itself; header filter changes reach `onFilterModelChange` (or the `dataSource`'s `getRows`) like any other filter change. Items are tagged `header:<field>`, which the server can ignore.
82
+
83
+ ```tsx
84
+ <DataGrid
85
+ rows={pageRows}
86
+ columns={columns}
87
+ headerFilters
88
+ filterMode="server"
89
+ filterModel={filterModel}
90
+ onFilterModelChange={(model) => { setFilterModel(model); refetch(model); }}
91
+ />
92
+ ```
93
+
94
+ ## Keyboard and screen readers
95
+
96
+ The filter row is a header row (`role="row"`) between the column headers and the first data row. Its cells are `role="columnheader"`, labelled **"Filter &lt;header name&gt;"**, with the same `aria-colindex` as their column; `aria-rowcount` and the rows' `aria-rowindex` count it.
97
+
98
+ | Key | On a filter cell |
99
+ | :--- | :--- |
100
+ | ArrowDown on a column header | Moves to that column's filter cell |
101
+ | ArrowDown / ArrowUp | First data row / the column header |
102
+ | ArrowLeft / ArrowRight, Home / End | Along the filter row |
103
+ | Enter or Space | Focuses the cell's control ("Custom filter": opens the filter panel) |
104
+ | Typing a character | Starts a new value in the text box (text and number columns) |
105
+ | Alt+ArrowDown or Ctrl/Cmd+Enter | Opens the operator menu (arrows move, Enter picks, Escape closes) |
106
+ | Delete / Backspace | Clears the column's filter |
107
+
108
+ Inside a control, arrow keys edit the text (or move through a select) and do not move the grid's focus, as in a cell editor. **Escape** returns focus to the filter cell. **Tab** leaves the grid: the grid stays a single Tab stop and every control in the row has `tabIndex={-1}`.
109
+
110
+ ## Layout
111
+
112
+ - The row sits inside the sticky header block (`.ogx__header-wrap`), under any column group rows and the column header row, so it stays at the top while rows scroll.
113
+ - Each cell takes its column's width, flex and sticky offset from the same calculation as the column header, so filter cells line up with their columns with pinned columns, column virtualization and horizontal scrolling.
114
+ - Scrolling a row into view (keyboard navigation, `apiRef.scrollToIndexes`, cell range auto-scroll) measures the sticky header, so rows are never left under the taller header.
115
+ - With `pivotMode` active the row is not rendered: pivot columns are generated and are not the fields the filter model names.
116
+
117
+ ## Styling
118
+
119
+ | Class | Element |
120
+ | :--- | :--- |
121
+ | `ogx__header-filter-row` | The row |
122
+ | `ogx__header-filter-cell` | Every cell, plus `--focused`, `--active` (has a filter), `--custom` ("Custom filter"), `--empty` (no control), `--system` (under the checkbox, expand and reorder columns), `--pinned`, `--pinned-left`, `--pinned-right`, `--pinned-left-last`, `--pinned-right-first` |
123
+ | `ogx__header-filter-operator` | The operator button (`--open` while its menu is open) |
124
+ | `ogx__header-filter-menu` | The operator menu (also `ogx-column-menu`, so it looks like the column menu) |
125
+ | `ogx__header-filter-input-wrapper` / `ogx__header-filter-input` | The `Input` wrapper and its `<input>` |
126
+ | `ogx__header-filter-select` | The boolean and `singleSelect` select |
127
+ | `ogx__header-filter-operator-label` | The operator name shown for `isEmpty` / `isNotEmpty` |
128
+ | `ogx__header-filter-clear` | The clear button |
129
+ | `ogx__header-filter-custom` | The "Custom filter" label or button |
130
+
131
+ The row uses the header's theme variables (`--ogx-grid-header-background`, `--ogx-grid-border-color`, `--ogx-grid-header-text`, `--ogx-grid-header-hover-background`) and the shared input variables, so every built-in theme and dark mode style it without new variables. `--ogx-header-filter-height` holds its height.
132
+
133
+ ## See also
134
+
135
+ - [Filtering & Search](filtering.md) — operators, AND/OR groups and the quick filter
136
+ - [Filter Panel](../components/filter-panel.md)
137
+ - [Keyboard & Accessibility](keyboard-navigation.md)
138
+ - Demo: *Header Filters* (`/header-filters`)
@@ -39,6 +39,9 @@ With [`cellSelection`](cell-selection.md) on (v3.3), on a data cell:
39
39
  | **Escape** | Collapse the range to the anchor (when not editing). |
40
40
  | **Ctrl+V** (Cmd+V) | Paste tab-separated text into the editable cells from the range's top-left cell (v3.4, see [Clipboard](clipboard.md#-paste-from-excel-and-google-sheets-v34)). Off with `disableClipboardPaste`. |
41
41
  | **Delete** / **Backspace** | On a range of more than one cell: empty its editable cells in one edit (v3.4). Off with `disableRangeClear`. |
42
+ | **Ctrl+D** (Cmd+D) | Fill down: copy the range's top row into the rows below it, in one edit (v3.5, see [Fill Handle](fill-handle.md)). The browser's bookmark shortcut does not fire. Off with `disableFillHandle`. |
43
+ | **Ctrl+R** (Cmd+R) | Fill right: copy the range's left column into the columns right of it, in one edit (v3.5). The browser's reload shortcut does not fire. Off with `disableFillHandle`. |
44
+ | **Escape** during a fill-handle drag | Cancel the fill: nothing is written and the range goes back (v3.5). |
42
45
 
43
46
  With `undoRedo` on (v3.4, see [Undo & Redo](undo-redo.md)), while focus is on a cell and no editor is open:
44
47
 
@@ -70,10 +73,25 @@ When an edit ends, focus returns to the edited cell unless you moved it elsewher
70
73
  | **Enter** / **Space** | Sort the column (asc → desc → none), replacing the sort model; with `multiSort`, or with **Shift** held, the column is added to (or updated or removed from) the sort model instead, as a header click or shift-click does. On the select-all header: select or clear all rows. |
71
74
  | **Alt+ArrowDown** or **Ctrl+Enter** (Cmd+Enter) | Open the column menu. |
72
75
  | **Alt+ArrowRight** / **Alt+ArrowLeft** | Widen / narrow the column by 10px (with **Shift**: 50px). Not for `resizable: false` columns. |
73
- | **ArrowDown** | Move to the first row. |
76
+ | **ArrowDown** | Move to the first row; with `headerFilters`, to the column's filter cell. |
74
77
 
75
78
  In the column menu, **ArrowUp** / **ArrowDown** / **Home** / **End** move between items, **Enter** / **Space** choose one, and **Escape** or **Tab** closes the menu. Focus returns to the column header.
76
79
 
80
+ ### Header filter row
81
+
82
+ With `headerFilters` (v3.5, see [Header Filters](header-filters.md)) the filter row sits between the column headers and the first row. Its cells are navigation stops like any other cell:
83
+
84
+ | Key | Action |
85
+ | :--- | :--- |
86
+ | **ArrowDown** / **ArrowUp** | To the first row / back to the column header. Arrow keys from the first row go up to the filter row. |
87
+ | **ArrowLeft** / **ArrowRight**, **Home** / **End** | Along the filter row. |
88
+ | **Enter** / **Space** | Focus the cell's text box or select. On "Custom filter": open the toolbar's filter panel. |
89
+ | Typing a character | Start a new value in the text box (text and number columns). |
90
+ | **Alt+ArrowDown** or **Ctrl+Enter** (Cmd+Enter) | Open the operator menu; it works like the column menu. |
91
+ | **Delete** / **Backspace** | Clear the column's filter. |
92
+
93
+ Inside the text box or select, the arrow keys belong to the control, as in a cell editor. **Escape** returns focus to the filter cell; **Tab** leaves the grid (the controls are not Tab stops).
94
+
77
95
  ### List view
78
96
 
79
97
  In `listView`, focus moves between whole rows instead of cells:
@@ -95,7 +113,7 @@ Keys pressed in content your `renderCell` puts in a row (inputs, buttons) are le
95
113
  | Element | Attributes |
96
114
  | :--- | :--- |
97
115
  | Viewport | `role="grid"`, `aria-rowcount`, `aria-colcount`, `aria-multiselectable` (true when several rows can be selected), `aria-busy`. |
98
- | Header rows | `role="row"` with `aria-rowindex`: column-group rows first, then the column header row. |
116
+ | Header rows | `role="row"` with `aria-rowindex`: column-group rows first, then the column header row, then the header filter row (`headerFilters`), whose cells are `role="columnheader"` labelled "Filter &lt;header&gt;". |
99
117
  | Data rows | `role="row"`, `aria-rowindex` counted across the whole dataset (page 2 continues after page 1; bottom-pinned rows come after every page), `aria-selected`. Tree-data and row-grouping rows add `aria-level`; rows with children add `aria-expanded`. |
100
118
  | Cells and headers | `aria-colindex`, identical for a header and the body cells below it. The system columns (reorder handle, detail-panel toggle, checkbox) come first. |
101
119
  | Sorted headers | Only the primary sort column has `aria-sort="ascending"` / `"descending"`; with several sort keys every sorted header also gets an `aria-description` such as "Sorted descending, sort priority 2 of 2". |
package/docs/roadmap.md CHANGED
@@ -84,7 +84,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
84
84
  | :--- | :--- | :--- | :--- |
85
85
  | **3.3.0** ✅ | shipped 7 Oct | **Cell range selection** (opt-in `cellSelection` prop): mouse drag, Shift+click, Shift+arrows; Ctrl+C / Cmd+C copies the block as TSV | Selection status bar (count, sum, average of selected cells); wrapped header text; React Compiler compatibility check |
86
86
  | **3.4.0** ✅ | shipped 8 Oct | **Paste from Excel / Google Sheets** into the selection, through `processRowUpdate` (values parsed per column type or `valueParser`, non-editable cells skipped, `onClipboardPaste` summary); **Delete clears a range**; built-in inputs and editors use the shared `Input` / `Checkbox` (`Input variant="cell"`). **Undo / redo** for cell edits and pastes (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y, `apiRef.undo()` / `redo()`) | **AI toolkit, part 1** in a new `@opencorestack/opengridx/ai` entry point: `getGridAiSchema(columns)` (JSON Schema of the filter, sort, grouping, aggregation, pivot and visibility models the columns allow) and `validateGridAiState(json, columns)` (drops unknown fields and operators, coerces values, returns errors) |
87
- | **3.5.0** | 15 Dec | **AI toolkit, part 2**: `aiAssistant={{ onPrompt }}` prompt panel that calls the app's own model, shows the proposed changes as removable chips and applies them with undo; `createGridAgentTools(apiRef)` (plain tool objects for CopilotKit, the Vercel AI SDK or WebMCP) | Header filter row; **fill handle** as a stretch goal (moves to Q1 2027 if 3.3–3.4 run late) |
87
+ | **3.5.0** ✅ | shipped 9 Oct | **AI toolkit, part 2**: `aiAssistant={{ onPrompt }}` Ask AI panel (chips, Undo, voice) that calls the app's own model through `createGridAiPromptHandler`; only validated replies apply; `createGridAgentTools(apiRef, columns)` (plain tool objects for CopilotKit, the Vercel AI SDK, provider tool calling or WebMCP) | **Header filter row** (`headerFilters`); **fill handle** (series, Alt copy, Ctrl/Cmd+D / R); `rowGroupingModel` controlled/uncontrolled pair |
88
88
  | Freeze | 19 Dec – 5 Jan | Bug fixes only | — |
89
89
 
90
90
  ### Across the quarter
@@ -106,7 +106,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
106
106
 
107
107
  * OpenGridX never calls an AI service and never bundles an AI SDK. The app passes a function that calls whichever model it uses.
108
108
  * By default only column definitions and grid state are sent, never row data. Example values are opt-in per column and documented as real data.
109
- * Model output is always validated against the schema and shown to the user before it changes the grid, with one-click undo.
109
+ * Model output is always validated against the schema (the grid applies only a validated, branded result), and every change it makes is shown as a removable chip with one-click undo.
110
110
  * The schema carries a `schemaVersion`, since apps will cache prompts against it.
111
111
 
112
112
  ### Risks
@@ -118,7 +118,6 @@ This document tracks the current status of features in `OpenGridX` and outlines
118
118
  ## 🔭 Next (Q1 2027)
119
119
 
120
120
  * **Row reordering inside tree data and row groups**, touch-friendly (MUI #4821, 405 👍, not in MUI X).
121
- * **Fill handle**, if it does not make 3.5.0.
122
121
  * **Find & highlight** (Ctrl+F through cells), **right-click context menu**, **Excel-style value (set) filter**.
123
122
  * **Calculated columns** (`[price] * [qty]` defined by users).
124
123
  * **Interactive pivot builder**: drag-and-drop panel for pivot and grouping.
@@ -0,0 +1,160 @@
1
+ // createGridAgentTools: plain tool objects ({ name, description, inputSchema, execute }) that let an AI
2
+ // agent read and change the grid through its apiRef. No SDK: they fit the Vercel AI SDK, CopilotKit,
3
+ // OpenAI or Anthropic tool calling and WebMCP. Every input is validated; execute never throws.
4
+ import type { GridAiState, GridRowModel } from '../types';
5
+ import { getCellValue } from '../utils/values';
6
+
7
+ type CellColumn = Parameters<typeof getCellValue>[2];
8
+ import { hasOwn, isPlainObject, PART_KEYS, usableColumns } from './columns';
9
+ import { getGridAiSchema } from './schema';
10
+ import type {
11
+ GridAgentApi,
12
+ GridAgentTool,
13
+ GridAgentToolResult,
14
+ GridAgentToolsOptions,
15
+ GridAiColumn,
16
+ GridAiJsonSchema,
17
+ GridAiPart,
18
+ } from './types';
19
+ import { validateGridAiState } from './validate';
20
+
21
+ const DEFAULT_MAX_ROWS = 20;
22
+ const MAX_ROWS_LIMIT = 1000;
23
+
24
+ const SET_TOOLS: { name: string; part: GridAiPart; description: string }[] = [
25
+ { name: 'set_filter', part: 'filter', description: 'Replace the filter model of the grid (filters and quick-filter words).' },
26
+ { name: 'set_sort', part: 'sort', description: 'Replace the sort model of the grid, most significant field first. An empty list clears sorting.' },
27
+ { name: 'set_grouping', part: 'grouping', description: 'Group the rows by these fields, outermost first. An empty list removes grouping.' },
28
+ { name: 'set_aggregation', part: 'aggregation', description: 'Replace the summaries: field to aggregation function. An empty object removes them.' },
29
+ { name: 'set_column_visibility', part: 'columnVisibility', description: 'Show (true) or hide (false) columns. Columns not listed keep their visibility.' },
30
+ ];
31
+
32
+ type ApiRef = { readonly current: GridAgentApi | null | undefined };
33
+
34
+ function failure(message: string, path = ''): GridAgentToolResult {
35
+ return { ok: false, errors: [{ path, message }] };
36
+ }
37
+
38
+ /** Runs a tool body; a throw becomes `{ ok: false }`. */
39
+ async function run(body: () => GridAgentToolResult): Promise<GridAgentToolResult> {
40
+ try {
41
+ return body();
42
+ } catch (error) {
43
+ return failure(error instanceof Error && error.message ? error.message : 'The tool failed');
44
+ }
45
+ }
46
+
47
+ function apiOf(apiRef: ApiRef): GridAgentApi | null {
48
+ const api = apiRef?.current;
49
+ return api && typeof api.getGridAiState === 'function' ? api : null;
50
+ }
51
+
52
+ /** A JSON-safe cell value: primitives as they are, a Date as an ISO string, anything else null. */
53
+ function jsonValue(value: unknown): unknown {
54
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
55
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
56
+ return typeof value === 'string' || typeof value === 'boolean' ? value : null;
57
+ }
58
+
59
+ function setTool(apiRef: ApiRef, columns: readonly GridAiColumn[], name: string, part: GridAiPart, description: string): GridAgentTool | null {
60
+ const key = PART_KEYS[part] as keyof GridAiState;
61
+ const schema = getGridAiSchema(columns, { include: [part] });
62
+ if (!hasOwn(schema.properties, key)) return null;
63
+ const { $schema: _version, schemaVersion: _schemaVersion, title: _title, description: _description, ...inputSchema } = schema;
64
+ return {
65
+ name,
66
+ description,
67
+ inputSchema: { ...inputSchema, required: [key] },
68
+ execute: (input) => run(() => {
69
+ const api = apiOf(apiRef);
70
+ if (!api) return failure('The grid is not ready');
71
+ if (!isPlainObject(input) || !hasOwn(input, key)) return failure(`Expected an object with "${key}"`, key);
72
+ const { state, errors } = validateGridAiState({ [key]: input[key] }, columns, { include: [part] });
73
+ // All or nothing: the agent gets the errors back and can correct its call.
74
+ if (errors.length > 0) return { ok: false, errors };
75
+ const applied: GridAiState = {};
76
+ if (state.filterModel) api.setFilterModel(applied.filterModel = state.filterModel);
77
+ if (state.sortModel) api.setSortModel(applied.sortModel = state.sortModel);
78
+ if (state.rowGroupingModel) api.setRowGroupingModel(applied.rowGroupingModel = state.rowGroupingModel);
79
+ if (state.aggregationModel) api.setAggregationModel(applied.aggregationModel = state.aggregationModel);
80
+ if (state.columnVisibilityModel) {
81
+ applied.columnVisibilityModel = { ...api.getGridAiState().columnVisibilityModel, ...state.columnVisibilityModel };
82
+ api.setColumnVisibilityModel(applied.columnVisibilityModel);
83
+ }
84
+ return { ok: true, applied };
85
+ }),
86
+ };
87
+ }
88
+
89
+ const NO_INPUT: GridAiJsonSchema = { type: 'object', properties: {}, additionalProperties: false };
90
+
91
+ /**
92
+ * Tools for an AI agent: `get_grid_state`, `set_filter`, `set_sort`, `set_grouping`, `set_aggregation`,
93
+ * `set_column_visibility`, `clear_filters`, and `get_row_summary` only with `allowRowAccess: true` (the
94
+ * one tool that sends row values). Each tool is `{ name, description, inputSchema, execute }`; inputs are
95
+ * checked with `validateGridAiState` and applied through `apiRef` only when valid. `execute` never throws.
96
+ * @since v3.5
97
+ */
98
+ export function createGridAgentTools<C extends GridAiColumn>(
99
+ apiRef: ApiRef,
100
+ columns: readonly C[],
101
+ options: GridAgentToolsOptions = {},
102
+ ): GridAgentTool[] {
103
+ const cols = usableColumns(columns);
104
+ const tools: GridAgentTool[] = [{
105
+ name: 'get_grid_state',
106
+ description: 'Read the current filter, sort, grouping, aggregation, pivot and column visibility of the grid, and its columns.',
107
+ inputSchema: NO_INPUT,
108
+ execute: () => run(() => {
109
+ const api = apiOf(apiRef);
110
+ if (!api) return failure('The grid is not ready');
111
+ return {
112
+ ok: true,
113
+ state: api.getGridAiState(),
114
+ columns: cols.map((c) => ({ field: c.field, headerName: typeof c.headerName === 'string' ? c.headerName : c.field, type: typeof c.type === 'string' ? c.type : 'string' })),
115
+ };
116
+ }),
117
+ }];
118
+ for (const { name, part, description } of SET_TOOLS) {
119
+ const tool = setTool(apiRef, cols, name, part, description);
120
+ if (tool) tools.push(tool);
121
+ }
122
+ tools.push({
123
+ name: 'clear_filters',
124
+ description: 'Remove every filter and the quick-filter words.',
125
+ inputSchema: NO_INPUT,
126
+ execute: () => run(() => {
127
+ const api = apiOf(apiRef);
128
+ if (!api) return failure('The grid is not ready');
129
+ const filterModel = { items: [] };
130
+ api.setFilterModel(filterModel);
131
+ return { ok: true, applied: { filterModel } };
132
+ }),
133
+ });
134
+ if (options.allowRowAccess === true) {
135
+ const maxRows = typeof options.maxRows === 'number' && options.maxRows >= 1 ? Math.min(Math.floor(options.maxRows), MAX_ROWS_LIMIT) : DEFAULT_MAX_ROWS;
136
+ const allowed = Array.isArray(options.fields) ? cols.filter((c) => options.fields?.includes(c.field)) : cols;
137
+ tools.push({
138
+ name: 'get_row_summary',
139
+ description: `Count the rows that pass the current filter and read the first ones (at most ${maxRows}, fields: ${allowed.map((c) => c.field).join(', ')}).`,
140
+ inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: maxRows } }, additionalProperties: false },
141
+ execute: (input) => run(() => {
142
+ const api = apiOf(apiRef);
143
+ if (!api) return failure('The grid is not ready');
144
+ const raw = isPlainObject(input) && hasOwn(input, 'limit') ? input.limit : maxRows;
145
+ const limit = typeof raw === 'number' && raw >= 1 ? Math.min(Math.floor(raw), maxRows) : maxRows;
146
+ const all = api.getAllFilteredRows();
147
+ const list = Array.isArray(all) ? all : [];
148
+ const rows = list.slice(0, limit).map((row) => {
149
+ const out: Record<string, unknown> = {};
150
+ if (!isPlainObject(row)) return out;
151
+ // Through the column's valueGetter, as the grid shows the cell.
152
+ for (const col of allowed) out[col.field] = jsonValue(getCellValue(row as GridRowModel, col.field, col as CellColumn));
153
+ return out;
154
+ });
155
+ return { ok: true, rowCount: list.length, rows };
156
+ }),
157
+ });
158
+ }
159
+ return tools;
160
+ }
package/lib/ai/index.ts CHANGED
@@ -1,12 +1,23 @@
1
- // @opencorestack/opengridx/ai: schema and validator for driving the grid from a language model.
2
- // No React, no dependencies and no AI SDK; the app sends the schema to its own model.
1
+ // @opencorestack/opengridx/ai: schema, validator, prompt handler and agent tools for driving the grid
2
+ // from a language model. No React, no dependencies and no AI SDK; the app calls its own model.
3
3
  export { getGridAiSchema } from './schema';
4
4
  export { validateGridAiState } from './validate';
5
+ export { createGridAiPromptHandler } from './promptHandler';
6
+ export { createGridAgentTools } from './agentTools';
5
7
  export type {
8
+ GridAgentApi,
9
+ GridAgentTool,
10
+ GridAgentToolResult,
11
+ GridAgentToolsOptions,
6
12
  GridAiColumn,
13
+ GridAiHistoryEntry,
7
14
  GridAiJsonSchema,
15
+ GridAiModelRequest,
8
16
  GridAiPart,
9
17
  GridAiPartOptions,
18
+ GridAiPromptContext,
19
+ GridAiPromptHandlerOptions,
20
+ GridAiPromptResult,
10
21
  GridAiSchema,
11
22
  GridAiSchemaOptions,
12
23
  GridAiState,
@@ -14,3 +25,9 @@ export type {
14
25
  GridAiValidationError,
15
26
  GridAiValidationResult,
16
27
  } from './types';
28
+ export type {
29
+ GridAiAssistantOptions,
30
+ GridAiAssistantPanelProps,
31
+ GridAiAssistantStatus,
32
+ GridAiChip,
33
+ } from '../types';