@opencorestack/opengridx 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +10 -3
  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 +7334 -5906
  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 +11 -5
  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/features/virtualization.md +5 -5
  28. package/docs/performance.md +212 -1
  29. package/docs/roadmap.md +3 -4
  30. package/lib/ai/agentTools.ts +160 -0
  31. package/lib/ai/index.ts +19 -2
  32. package/lib/ai/promptHandler.ts +109 -0
  33. package/lib/ai/types.ts +89 -25
  34. package/lib/ai/validate.ts +10 -0
  35. package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
  36. package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
  37. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
  38. package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
  39. package/lib/components/Cell/Cell.css +28 -0
  40. package/lib/components/Cell/Cell.tsx +6 -2
  41. package/lib/components/DataGrid/DataGrid.tsx +84 -4
  42. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
  43. package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
  44. package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
  45. package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
  46. package/lib/components/Header/Header.tsx +44 -23
  47. package/lib/components/Header/headerColumnStyle.ts +28 -0
  48. package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
  49. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
  50. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
  51. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
  52. package/lib/components/Toolbar/GridToolbar.tsx +51 -0
  53. package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
  54. package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
  55. package/lib/hooks/core/useGridApiMethods.ts +39 -0
  56. package/lib/hooks/core/useGridAriaRows.ts +5 -3
  57. package/lib/hooks/core/useGridCellSelection.ts +77 -11
  58. package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
  59. package/lib/hooks/core/useGridControlledState.ts +27 -0
  60. package/lib/hooks/core/useGridDevWarnings.ts +21 -6
  61. package/lib/hooks/core/useGridFillHandle.ts +323 -0
  62. package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
  63. package/lib/hooks/core/useGridMaxContentHeight.ts +16 -0
  64. package/lib/hooks/core/useGridToolbarProps.ts +19 -0
  65. package/lib/hooks/core/useGridVirtualization.ts +19 -0
  66. package/lib/hooks/features/useGridAiAssistant.ts +284 -0
  67. package/lib/hooks/features/useGridBatchEdit.ts +2 -2
  68. package/lib/index.ts +3 -0
  69. package/lib/styles/opengridx.css +2 -0
  70. package/lib/theme/DataGridThemeProvider.tsx +2 -0
  71. package/lib/theme/types.ts +2 -0
  72. package/lib/types/index.ts +219 -1
  73. package/lib/utils/aiAssistant.ts +205 -0
  74. package/lib/utils/aiBrand.ts +17 -0
  75. package/lib/utils/cellSelection.ts +25 -5
  76. package/lib/utils/editing/fill.ts +238 -0
  77. package/lib/utils/filtering/index.ts +14 -0
  78. package/lib/utils/focus/index.ts +9 -3
  79. package/lib/utils/headerFilters.ts +189 -0
  80. package/lib/utils/maxElementHeight.ts +53 -0
  81. package/llms.txt +44 -4
  82. package/package.json +3 -2
@@ -0,0 +1,145 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import type { GridAiAssistantPanelProps } from '../../types';
3
+ import { Button } from '../ui/Button';
4
+ import { Input } from '../ui/Input';
5
+ import { useSpeechInput } from './useSpeechInput';
6
+
7
+ /** The sparkle of the Ask AI button and the panel title. */
8
+ export function AiSparkleIcon() {
9
+ return (
10
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" aria-hidden="true" focusable="false">
11
+ <path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z" />
12
+ <path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8z" />
13
+ </svg>
14
+ );
15
+ }
16
+
17
+ function MicIcon() {
18
+ return (
19
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true" focusable="false">
20
+ <rect x="9" y="3" width="6" height="11" rx="3" />
21
+ <path d="M5 11a7 7 0 0 0 14 0M12 18v3" />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ /**
27
+ * The built-in `aiAssistant` panel: prompt input (Enter sends), suggestions, microphone where the
28
+ * browser has speech recognition, Stop while a request runs, the reply, the applied changes as
29
+ * removable chips with Undo, what was ignored, and the session's earlier prompts.
30
+ */
31
+ export function GridAiAssistantPanel(props: GridAiAssistantPanelProps) {
32
+ const {
33
+ status, statusText, message, errors, chips, history, suggestions, placeholder, voiceAvailable, canUndo,
34
+ submit, stop, undo, removeChip, close,
35
+ } = props;
36
+ const [text, setText] = useState('');
37
+ const inputRef = useRef<HTMLInputElement>(null);
38
+ const speech = useSpeechInput(setText);
39
+ const running = status === 'running';
40
+
41
+ useEffect(() => {
42
+ if (inputRef.current) inputRef.current.focus();
43
+ }, []);
44
+
45
+ const send = (prompt: string) => {
46
+ setText(prompt);
47
+ submit(prompt);
48
+ };
49
+
50
+ // The control a click acts on goes away (Stop, a chip, Undo): focus stays in the panel.
51
+ const thenFocusInput = (action: () => void) => () => {
52
+ action();
53
+ if (inputRef.current) inputRef.current.focus();
54
+ };
55
+
56
+ const handleSubmit = (event: React.FormEvent) => {
57
+ event.preventDefault();
58
+ if (!running) submit(text);
59
+ };
60
+
61
+ return (
62
+ <div className="ogx-ai-panel" role="dialog" aria-label="Ask AI">
63
+ <div className="ogx-ai-panel__header">
64
+ <span className="ogx-ai-panel__title"><AiSparkleIcon />Ask AI</span>
65
+ <Button size="small" className="ogx-ai-panel__close" aria-label="Close" onClick={close}>×</Button>
66
+ </div>
67
+
68
+ <form className="ogx-ai-panel__form" onSubmit={handleSubmit}>
69
+ <Input
70
+ ref={inputRef}
71
+ className="ogx-ai-panel__input"
72
+ value={text}
73
+ placeholder={placeholder}
74
+ aria-label="Prompt"
75
+ autoComplete="off"
76
+ onChange={(event) => setText(event.target.value)}
77
+ />
78
+ {voiceAvailable && (
79
+ <Button
80
+ size="small"
81
+ variant="outlined"
82
+ className={`ogx-ai-panel__mic${speech.listening ? ' ogx-ai-panel__mic--on' : ''}`}
83
+ aria-label={speech.listening ? 'Stop dictation' : 'Dictate a prompt'}
84
+ aria-pressed={speech.listening}
85
+ onClick={speech.toggle}
86
+ >
87
+ <MicIcon />
88
+ </Button>
89
+ )}
90
+ {/* Separate keys: one reused <button> would turn into the submit button during the Stop click and submit again. */}
91
+ {running
92
+ ? <Button key="stop" size="small" variant="outlined" onClick={thenFocusInput(stop)}>Stop</Button>
93
+ : <Button key="ask" size="small" variant="contained" color="primary" type="submit" disabled={text.trim() === ''}>Ask</Button>}
94
+ </form>
95
+
96
+ {suggestions.length > 0 && (
97
+ <div className="ogx-ai-panel__suggestions" aria-label="Suggestions" role="group">
98
+ {suggestions.map((suggestion) => (
99
+ <button key={suggestion} type="button" className="ogx-ai-panel__suggestion" disabled={running} onClick={() => send(suggestion)}>
100
+ {suggestion}
101
+ </button>
102
+ ))}
103
+ </div>
104
+ )}
105
+
106
+ {statusText && <div className={`ogx-ai-panel__status ogx-ai-panel__status--${status}`}>{statusText}</div>}
107
+ {message && <p className="ogx-ai-panel__message">{message}</p>}
108
+
109
+ {(chips.length > 0 || canUndo) && (
110
+ <div className="ogx-ai-panel__changes">
111
+ <ul className="ogx-ai-panel__chips" aria-label="Applied changes">
112
+ {chips.map((chip) => (
113
+ <li key={chip.id} className="ogx-ai-chip" data-part={chip.part}>
114
+ <span className="ogx-ai-chip__label">{chip.label}</span>
115
+ <button type="button" className="ogx-ai-chip__remove" aria-label={`Remove ${chip.label}`} onClick={thenFocusInput(() => removeChip(chip.id))}>×</button>
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ {canUndo && <Button size="small" variant="outlined" className="ogx-ai-panel__undo" onClick={thenFocusInput(undo)}>Undo</Button>}
120
+ </div>
121
+ )}
122
+
123
+ {errors.length > 0 && (
124
+ <ul className="ogx-ai-panel__errors" aria-label="Ignored">
125
+ {errors.map((error, i) => (
126
+ <li key={`${error.path}:${i}`}>{error.path ? `Ignored: ${error.message}` : error.message}</li>
127
+ ))}
128
+ </ul>
129
+ )}
130
+
131
+ {history.length > 0 && (
132
+ <div className="ogx-ai-panel__history">
133
+ <div className="ogx-ai-panel__history-title">Earlier</div>
134
+ <ul>
135
+ {history.map((entry, i) => (
136
+ <li key={`${i}:${entry.prompt}`}>
137
+ <button type="button" className="ogx-ai-panel__history-item" onClick={() => setText(entry.prompt)}>{entry.prompt}</button>
138
+ </li>
139
+ ))}
140
+ </ul>
141
+ </div>
142
+ )}
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,74 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { attempt } from '../../utils/attempt';
3
+
4
+ /** The part of the Web Speech API's SpeechRecognition the panel uses. */
5
+ interface SpeechRecognitionLike {
6
+ lang: string;
7
+ interimResults: boolean;
8
+ maxAlternatives: number;
9
+ onresult: ((event: { results: ArrayLike<ArrayLike<{ transcript: string }>> }) => void) | null;
10
+ onend: (() => void) | null;
11
+ onerror: (() => void) | null;
12
+ start: () => void;
13
+ stop: () => void;
14
+ abort: () => void;
15
+ }
16
+
17
+ type SpeechRecognitionConstructor = new () => SpeechRecognitionLike;
18
+
19
+ function getSpeechRecognition(): SpeechRecognitionConstructor | null {
20
+ if (typeof window === 'undefined') return null;
21
+ const w = window as unknown as { SpeechRecognition?: SpeechRecognitionConstructor; webkitSpeechRecognition?: SpeechRecognitionConstructor };
22
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
23
+ }
24
+
25
+ /**
26
+ * Dictation for the prompt input. The browser asks for the microphone only when `toggle` starts it
27
+ * (a click), never on load; without speech recognition `toggle` does nothing.
28
+ */
29
+ export function useSpeechInput(onText: (text: string) => void): { listening: boolean; toggle: () => void } {
30
+ const [listening, setListening] = useState(false);
31
+ const recognitionRef = useRef<SpeechRecognitionLike | null>(null);
32
+ const onTextRef = useRef(onText);
33
+ useLayoutEffect(() => {
34
+ onTextRef.current = onText;
35
+ });
36
+
37
+ useEffect(() => () => {
38
+ const recognition = recognitionRef.current;
39
+ recognitionRef.current = null;
40
+ if (recognition) attempt(() => recognition.abort());
41
+ }, []);
42
+
43
+ const toggle = useCallback(() => {
44
+ const running = recognitionRef.current;
45
+ if (running) {
46
+ attempt(() => running.stop());
47
+ return;
48
+ }
49
+ const Recognition = getSpeechRecognition();
50
+ if (!Recognition) return;
51
+ const created = attempt(() => new Recognition());
52
+ if (!created.ok) return;
53
+ const recognition = created.value;
54
+ recognition.lang = typeof navigator === 'undefined' ? 'en-US' : navigator.language;
55
+ recognition.interimResults = false;
56
+ recognition.maxAlternatives = 1;
57
+ recognition.onresult = (event) => {
58
+ const first = event.results[0];
59
+ const transcript = first && first[0] ? first[0].transcript : '';
60
+ if (transcript) onTextRef.current(transcript);
61
+ };
62
+ const done = () => {
63
+ if (recognitionRef.current === recognition) recognitionRef.current = null;
64
+ setListening(false);
65
+ };
66
+ recognition.onend = done;
67
+ recognition.onerror = done;
68
+ recognitionRef.current = recognition;
69
+ setListening(true);
70
+ if (!attempt(() => recognition.start()).ok) done();
71
+ }, []);
72
+
73
+ return { listening, toggle };
74
+ }
@@ -52,6 +52,34 @@
52
52
  .ogx__cell--range-right {
53
53
  --ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
54
54
  }
55
+ /*
56
+ * Fill handle (v3.5): the square on the bottom-right cell of the range. The cell clips its overflow,
57
+ * so the 24px hit area lies inside the corner and the visible square sits in its corner.
58
+ * touch-action: none lets a touch drag on it fill instead of scrolling.
59
+ */
60
+ .ogx__cell-fill-handle {
61
+ position: absolute;
62
+ right: 0;
63
+ bottom: 0;
64
+ width: 24px;
65
+ height: 24px;
66
+ max-width: 100%;
67
+ max-height: 100%;
68
+ cursor: crosshair;
69
+ touch-action: none;
70
+ z-index: 1;
71
+ }
72
+ .ogx__cell-fill-handle::after {
73
+ content: '';
74
+ position: absolute;
75
+ right: 1px;
76
+ bottom: 1px;
77
+ width: 7px;
78
+ height: 7px;
79
+ box-sizing: border-box;
80
+ background-color: var(--ogx-fill-handle-color, var(--ogx-range-border));
81
+ border: 1px solid var(--ogx-grid-background, #fff);
82
+ }
55
83
  /* While a pointer drag selects cells, the browser must not select text as well. */
56
84
  .ogx--range-dragging,
57
85
  .ogx--range-dragging * {
@@ -7,7 +7,7 @@ import { GridEditInputCell } from './GridEditInputCell';
7
7
  import { CellErrorBoundary } from './CellErrorBoundary';
8
8
  import { formatValueByType } from '../../utils/values';
9
9
  import { attempt } from '../../utils/attempt';
10
- import { RANGE_BOTTOM, RANGE_CELL, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
10
+ import { RANGE_BOTTOM, RANGE_CELL, RANGE_FILL_HANDLE, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
11
11
 
12
12
  export interface CellProps<R extends GridRowModel = GridRowModel> {
13
13
  onCellClick?: (params: GridCellParams<R>) => void;
@@ -50,7 +50,8 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
50
50
  valueError?: unknown;
51
51
  /**
52
52
  * Place of the cell in the selected cell range (`cellSelection`), as bit flags: in range 1, top
53
- * edge 2, bottom 4, left 8, right 16. 0 or absent: not in a range. @since v3.3
53
+ * edge 2, bottom 4, left 8, right 16; 32 draws the fill handle (v3.5). 0 or absent: not in a range.
54
+ * @since v3.3
54
55
  */
55
56
  rangeFlags?: number;
56
57
  }
@@ -341,6 +342,9 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
341
342
  formattedValue
342
343
  )}
343
344
  </div>
345
+ {(rangeFlags & RANGE_FILL_HANDLE) !== 0 && !isEditing && (
346
+ <div className="ogx__cell-fill-handle" aria-hidden="true" />
347
+ )}
344
348
  </div>
345
349
  );
346
350
  }
@@ -4,6 +4,8 @@ import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavig
4
4
  import { useGridControlledState } from '../../hooks/core/useGridControlledState';
5
5
  import { useGridThemeDimensions } from '../../hooks/core/useGridThemeDimensions';
6
6
  import { useGridRowPipeline, type GridRowPipelineResult } from '../../hooks/core/useGridRowPipeline';
7
+ import { useGridMaxContentHeight } from '../../hooks/core/useGridMaxContentHeight';
8
+ import { countReachableRows } from '../../utils/maxElementHeight';
7
9
  import { useGridVirtualization } from '../../hooks/core/useGridVirtualization';
8
10
  import { useGridColumns } from '../../hooks/core/useGridColumns';
9
11
  import { useGridVisibleRows } from '../../hooks/core/useGridVisibleRows';
@@ -30,6 +32,7 @@ import { useGridEditing } from '../../hooks/features/useGridEditing';
30
32
  import { useGridBatchEdit } from '../../hooks/features/useGridBatchEdit';
31
33
  import { useGridClipboardPaste } from '../../hooks/core/useGridClipboardPaste';
32
34
  import { useGridUndoRedo, useGridEditCommitChannel, UNDO_HISTORY_LIMIT } from '../../hooks/core/useGridUndoRedo';
35
+ import { useGridFillHandle } from '../../hooks/core/useGridFillHandle';
33
36
  import { useGridSpanning } from '../../hooks/features/useGridSpanning';
34
37
  import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
35
38
  import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
@@ -43,7 +46,7 @@ import { GridLoadingOverlay } from './GridLoadingOverlay';
43
46
  import { GridPinnedRows } from './GridPinnedRows';
44
47
  import { GridVirtualRows } from './GridVirtualRows';
45
48
  import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
46
- import { resolveGridModes, EMPTY_ROW_GROUPING_MODEL } from '../../utils/gridModes';
49
+ import { resolveGridModes } from '../../utils/gridModes';
47
50
  import { useGridGroupingColumn } from '../../hooks/core/useGridGroupingColumn';
48
51
  import { useGridRowIdOf, getDefaultRowId } from '../../hooks/core/useGridRowIdOf';
49
52
  import { useGridApiRefBinding } from '../../hooks/core/useGridApiRefBinding';
@@ -65,6 +68,9 @@ import { GridPaginationArea } from './GridPaginationArea';
65
68
  import { GridLiveRegion } from './GridLiveRegion';
66
69
  import { useGridAriaRows } from '../../hooks/core/useGridAriaRows';
67
70
  import { useGridToolbarProps } from '../../hooks/core/useGridToolbarProps';
71
+ import { useGridAiAssistant } from '../../hooks/features/useGridAiAssistant';
72
+ import { GridAiAssistantArea } from '../AiAssistant/GridAiAssistantArea';
73
+ import { DEFAULT_HEADER_FILTER_HEIGHT } from '../../utils/headerFilters';
68
74
  import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridRowId, GridColDef } from '../../types';
69
75
 
70
76
  /**
@@ -83,6 +89,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
83
89
  rowHeight: rowHeightProp,
84
90
  headerHeight: headerHeightProp,
85
91
  wrapHeaderText = false,
92
+ headerFilters = false,
93
+ headerFilterHeight = DEFAULT_HEADER_FILTER_HEIGHT,
86
94
  autoHeight = false,
87
95
  density,
88
96
  checkboxSelection = false,
@@ -98,6 +106,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
98
106
  onClipboardPaste,
99
107
  onBeforeClipboardPaste,
100
108
  undoRedo = false,
109
+ disableFillHandle = false,
110
+ onFill,
101
111
  onHistoryChange,
102
112
  rowSelectionModel: propRowSelectionModel,
103
113
  onRowSelectionModelChange: propOnRowSelectionModelChange,
@@ -138,6 +148,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
138
148
  groupingColDef,
139
149
 
140
150
  rowGroupingModel: propRowGroupingModel,
151
+ onRowGroupingModelChange,
141
152
 
142
153
  aggregationModel: propAggregationModel,
143
154
  onAggregationModelChange,
@@ -177,12 +188,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
177
188
  localeText,
178
189
  apiRef: propApiRef,
179
190
  overscanRowCount = 3,
191
+ aiAssistant,
192
+ onAiAssistantApply,
193
+ onAiAssistantError,
180
194
  } = props;
181
195
  // Equal inline column definitions keep one identity, so the row passes memoised on it do not re-run.
182
196
  const columns = useStableColumns(columnsProp);
183
197
 
184
198
  const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
185
- const rowGroupingModel = propRowGroupingModel || EMPTY_ROW_GROUPING_MODEL;
186
199
 
187
200
  const controlledState = useGridControlledState({
188
201
  initialState,
@@ -192,6 +205,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
192
205
  onFilterModelChange,
193
206
  aggregationModel: propAggregationModel,
194
207
  onAggregationModelChange,
208
+ rowGroupingModel: propRowGroupingModel,
209
+ onRowGroupingModelChange,
195
210
  columnVisibilityModel: propColumnVisibilityModel,
196
211
  onColumnVisibilityModelChange,
197
212
  pinnedColumns: propPinnedColumns,
@@ -213,6 +228,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
213
228
  selectedRowIds: selectionModelRowIds,
214
229
  handleRowSelectionModelChange,
215
230
  density: effectiveDensity,
231
+ rowGroupingModel, handleRowGroupingModelChange,
216
232
  } = controlledState;
217
233
 
218
234
  const pivot = useGridPivot({
@@ -226,6 +242,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
226
242
  hierarchyRequested: treeData || rowGroupingModel.length > 0,
227
243
  });
228
244
  const isPivotActive = pivot.isActive;
245
+ // The header filter row filters the source columns; pivot columns are generated, so it is off then.
246
+ const showHeaderFilters = headerFilters && !isPivotActive;
229
247
  const {
230
248
  rowReordering, hierarchyRowGroupingModel, isTreeDataRequested, isTreeData,
231
249
  isRowGrouping, isHierarchyEnabled, hasGroupingColumn, pagination, listView,
@@ -459,6 +477,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
459
477
  dataRows,
460
478
  }),
461
479
  visibleColumns: renderOrderedColumns as unknown as GridColDef[],
480
+ rowGroupingModel,
481
+ onRowGroupingModelChange: handleRowGroupingModelChange,
482
+ aggregationModel,
483
+ onAggregationModelChange: handleAggregationModelChange,
484
+ columnVisibilityModel,
485
+ onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
486
+ pivotModel: currentPivotModel,
487
+ onPivotModelChange: handlePivotModelChange,
462
488
  });
463
489
 
464
490
  // Indices address the consumer's own rows, whatever the sort, filter, page or pinning.
@@ -561,12 +587,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
561
587
  onRowsScrollEnd, overscanRowCount, rowCount: layout.unpinnedRowsLength, autoHeight, setDimensions, viewportRef,
562
588
  });
563
589
 
590
+ const maxContentHeight = useGridMaxContentHeight(containerRef);
564
591
  const virtualization = useGridVirtualization({
565
592
  layout, scrollTop, scrollLeft,
566
593
  viewportWidth: state.dimensions.viewportWidth,
567
594
  viewportHeight: state.dimensions.viewportHeight,
568
595
  autoHeight, rowReordering, hasDetailPanel, checkboxSelection, pinCheckboxColumn, pinExpandColumn,
569
596
  overscanRows,
597
+ maxContentHeight: maxContentHeight ?? Infinity,
570
598
  });
571
599
 
572
600
 
@@ -596,6 +624,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
596
624
  pinExpandColumn,
597
625
  rowHeight: effectiveRowHeight,
598
626
  cellSelection: cellSelection.keyboard,
627
+ headerFilters: showHeaderFilters,
599
628
  });
600
629
 
601
630
  const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
@@ -650,6 +679,23 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
650
679
  pinnedTopRowCount: pinnedTopRows.length,
651
680
  });
652
681
 
682
+ // Fill handle (cellSelection): drag the range's corner, Ctrl/Cmd+D and Ctrl/Cmd+R.
683
+ const fillHandle = useGridFillHandle<R>({
684
+ containerRef, viewportRef,
685
+ selection: cellSelection,
686
+ disableFillHandle,
687
+ isEditing: editingHandlers.editingCell !== null,
688
+ allRenderableRows,
689
+ getRowId: getRowIdOf,
690
+ navigationColumns: navigationColumns as unknown as GridColDef<R>[],
691
+ columnIndexMap,
692
+ isCellEditable,
693
+ rowMetaMap,
694
+ getSpanOrigin: spanning.getSpanOrigin,
695
+ batchEdit,
696
+ onFill,
697
+ });
698
+
653
699
  const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
654
700
  columnVisibilityModel,
655
701
  onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
@@ -659,6 +705,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
659
705
 
660
706
  const ariaRows = useGridAriaRows({
661
707
  columnGroupingModel,
708
+ headerFilterRow: showHeaderFilters,
662
709
  pinnedTopCount: pinnedTopRows.length,
663
710
  pinnedBottomCount: pinnedBottomRows.length,
664
711
  pagination,
@@ -700,6 +747,9 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
700
747
  totalRowCount: pinnedTopRows.length + centerRows.length + pinnedBottomRows.length,
701
748
  pinnedRowsIgnored: isHierarchyEnabled && Boolean(pinnedRows?.top?.length || pinnedRows?.bottom?.length),
702
749
  scrollHeight: virtualization.totalHeight,
750
+ maxContentHeight,
751
+ reachableRowCount: countReachableRows(virtualization.cumulativeHeights, virtualization.virtualContainerHeight),
752
+ unpinnedRowCount: virtualization.cumulativeHeights.length,
703
753
  listViewWithoutColumn: listViewRequested && !listViewColumn,
704
754
  listViewRowCount: listView && listViewColumn ? allRenderableRows.length : 0,
705
755
  });
@@ -713,6 +763,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
713
763
  const columnsPanel = useGridColumnsPanel();
714
764
  useGridStylesheetWarning(containerRef);
715
765
 
766
+ // The aiAssistant prompt panel (v3.5): applies validated replies through apiRef; apiRef.openAiAssistant.
767
+ const aiAssistantPanel = useGridAiAssistant({
768
+ apiRef,
769
+ options: aiAssistant,
770
+ columns: activeColumns as unknown as GridColDef[],
771
+ onApply: onAiAssistantApply,
772
+ onError: onAiAssistantError,
773
+ });
774
+
716
775
  const NoRowsOverlaySlot = slots?.noRowsOverlay;
717
776
  const LoadingOverlaySlot = slots?.loadingOverlay;
718
777
  const loadingOverlay = LoadingOverlaySlot ? <LoadingOverlaySlot {...slotProps?.loadingOverlay} /> : undefined;
@@ -743,6 +802,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
743
802
  disableColumnReorder, columnGroupingModel, moveColumn, resetColumnOrder,
744
803
  forceColumnsOpen: columnsPanel.toolbarPanelRequested,
745
804
  onColumnsPanelClose: columnsPanel.closeToolbarPanel,
805
+ aiAssistantEnabled: aiAssistantPanel.enabled,
806
+ aiAssistantOpen: aiAssistantPanel.open,
807
+ onAiAssistantToggle: aiAssistantPanel.togglePanel,
808
+ aiAssistantLabel: localeText?.aiAssistantButton,
809
+ forceFiltersOpen: columnsPanel.toolbarFiltersRequested,
810
+ onFiltersPanelClose: columnsPanel.closeToolbarFiltersPanel,
746
811
  });
747
812
 
748
813
  // Props the row renderers (top-pinned, centre and bottom-pinned rows) all take.
@@ -764,6 +829,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
764
829
  rowMetaMap,
765
830
  cellRange: cellSelection.displayRange,
766
831
  cellRangeHasRowSpan: cellSelection.hasRowSpan,
832
+ fillHandle: fillHandle.handle,
767
833
  };
768
834
 
769
835
  return (
@@ -774,7 +840,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
774
840
  ...style,
775
841
  height: height ?? style?.height,
776
842
  '--ogx-row-height': `${effectiveRowHeight}px`,
777
- '--ogx-header-height': `${headerHeight}px`
843
+ '--ogx-header-height': `${headerHeight}px`,
844
+ ...(showHeaderFilters ? { '--ogx-header-filter-height': `${headerFilterHeight}px` } : undefined),
778
845
  } as unknown as React.CSSProperties}
779
846
  aria-busy={effectiveLoading}
780
847
  >
@@ -784,6 +851,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
784
851
  </GridToolbarHostContext.Provider>
785
852
  )}
786
853
 
854
+ {aiAssistantPanel.panelProps && (
855
+ <GridAiAssistantArea
856
+ panelProps={aiAssistantPanel.panelProps}
857
+ slot={slots?.aiAssistantPanel}
858
+ slotProps={slotProps?.aiAssistantPanel}
859
+ />
860
+ )}
861
+
787
862
  {/* Used by the column menu's Manage columns when no GridToolbar can show the panel */}
788
863
  <GridStandaloneColumnPanel<R>
789
864
  isOpen={columnsPanel.standalonePanelOpen}
@@ -912,6 +987,10 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
912
987
  onPinColumn={handlePinColumn}
913
988
  wrapHeaderText={wrapHeaderText}
914
989
  headerHeight={headerHeight}
990
+ headerFilters={showHeaderFilters}
991
+ filterModel={filterModel}
992
+ onFilterModelChange={handleFilterModelChange}
993
+ onOpenFilterPanel={columnsPanel.openFiltersPanel}
915
994
  />
916
995
 
917
996
  <GridPinnedRows<R>
@@ -936,7 +1015,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
936
1015
 
937
1016
  <GridVirtualRows<R>
938
1017
  {...rowRenderProps}
939
- virtualContainerHeight={virtualization.totalHeight - virtualization.pinnedTopHeight - virtualization.pinnedBottomHeight}
1018
+ virtualContainerHeight={virtualization.virtualContainerHeight}
940
1019
  offsetTop={spanRowWindow.offsetTop}
941
1020
  effectiveLoading={effectiveLoading}
942
1021
  visibleRows={visibleRows}
@@ -1035,6 +1114,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1035
1114
  dataRowCount={dataRows.length}
1036
1115
  filterModel={filterModel}
1037
1116
  cellSelectionAnnouncement={cellSelection.announcement}
1117
+ aiAssistantAnnouncement={aiAssistantPanel.announcement}
1038
1118
  />
1039
1119
 
1040
1120
  {showLoadingOverRows && (
@@ -13,13 +13,15 @@ interface GridLiveRegionProps {
13
13
  filterModel: GridFilterModel | undefined;
14
14
  /** The size of the selected cell range (`cellSelection`), already debounced. */
15
15
  cellSelectionAnnouncement?: string;
16
+ /** The `aiAssistant` status line ("Thinking…", "Applied 3 changes", an error). */
17
+ aiAssistantAnnouncement?: string;
16
18
  }
17
19
 
18
20
  /**
19
21
  * Polite status announcements: loading, a load error, no rows, or the row count of an active filter;
20
- * and the size of the selected cell range.
22
+ * the size of the selected cell range; and the AI assistant's status.
21
23
  */
22
- export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement }: GridLiveRegionProps) {
24
+ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement, aiAssistantAnnouncement }: GridLiveRegionProps) {
23
25
  return (
24
26
  <div className="ogx-aria-live-status" role="status" aria-live="polite">
25
27
  {loading ? 'Loading data...' : ''}
@@ -32,6 +34,7 @@ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount,
32
34
  : ''
33
35
  )}
34
36
  {cellSelectionAnnouncement ? <span className="ogx-aria-live-status__cells"> {cellSelectionAnnouncement}</span> : null}
37
+ {aiAssistantAnnouncement ? <span className="ogx-aria-live-status__ai"> {aiAssistantAnnouncement}</span> : null}
35
38
  </div>
36
39
  );
37
40
  }
@@ -17,6 +17,7 @@ import type {
17
17
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
18
18
  import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
19
19
  import { getRowCellRange } from '../../utils/cellSelection';
20
+ import type { GridFillHandlePosition } from '../../utils/cellSelection';
20
21
  import type { GridEditingState } from '../../hooks/features/useGridEditing';
21
22
 
22
23
  interface PinnedRowEditingHandlers {
@@ -64,6 +65,8 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
64
65
  cellRange?: GridDisplayCellRange | null;
65
66
  /** The grid has row spans: rows tell their cells how far the range bottom is. */
66
67
  cellRangeHasRowSpan?: boolean;
68
+ /** Where the fill handle is drawn (v3.5), or null. */
69
+ fillHandle?: GridFillHandlePosition | null;
67
70
  /** Pinned rows are edited like any other row. */
68
71
  editingHandlers?: PinnedRowEditingHandlers;
69
72
  isCellEditable?: (params: GridCellParams<R>) => boolean;
@@ -111,6 +114,7 @@ export function GridPinnedRows<R extends GridRowModel>({
111
114
  rowReordering = false,
112
115
  cellRange,
113
116
  cellRangeHasRowSpan = false,
117
+ fillHandle,
114
118
  }: GridPinnedRowsProps<R>) {
115
119
  if (rows.length === 0) return null;
116
120
 
@@ -156,7 +160,7 @@ export function GridPinnedRows<R extends GridRowModel>({
156
160
  onEditCellValueChange={editingHandlers?.setEditCellValue}
157
161
  isCellEditable={isCellEditable}
158
162
  rowReordering={rowReordering}
159
- cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan)}
163
+ cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan, fillHandle)}
160
164
  />
161
165
  );
162
166
  })}
@@ -20,6 +20,7 @@ import type { GridEditingState } from '../../hooks/features/useGridEditing';
20
20
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
21
21
  import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
22
22
  import { getRowCellRange } from '../../utils/cellSelection';
23
+ import type { GridFillHandlePosition } from '../../utils/cellSelection';
23
24
 
24
25
  interface RowEditingHandlers {
25
26
  editingCell: GridEditingState['editingCell'];
@@ -77,6 +78,8 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
77
78
  cellRange?: GridDisplayCellRange | null;
78
79
  /** The grid has row spans: rows tell their cells how far the range bottom is. */
79
80
  cellRangeHasRowSpan?: boolean;
81
+ /** Where the fill handle is drawn (v3.5), or null. */
82
+ fillHandle?: GridFillHandlePosition | null;
80
83
  }
81
84
 
82
85
  export function GridVirtualRows<R extends GridRowModel>({
@@ -123,6 +126,7 @@ export function GridVirtualRows<R extends GridRowModel>({
123
126
  columnIndexMap,
124
127
  cellRange,
125
128
  cellRangeHasRowSpan = false,
129
+ fillHandle,
126
130
  }: GridVirtualRowsProps<R>) {
127
131
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
128
132
  ? baseColumns
@@ -214,7 +218,7 @@ export function GridVirtualRows<R extends GridRowModel>({
214
218
  rowSpanningCaches={rowSpanningCaches}
215
219
  rowHeight={rowHeight}
216
220
  rowMeta={rowMetaMap.get(id)}
217
- cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan)}
221
+ cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan, fillHandle)}
218
222
  />
219
223
  ))
220
224
  )}
@@ -6,8 +6,7 @@ import type {
6
6
  GridColDef,
7
7
  GridFilterOperator
8
8
  } from '../../types';
9
- import { getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
- import { toLocalDateString } from '../../utils/values';
9
+ import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
11
10
  import { Input } from '../ui/Input';
12
11
 
13
12
 
@@ -74,13 +73,7 @@ function matchesOption(option: ValueOption, value: unknown): boolean {
74
73
 
75
74
  /** Text shown in the value input for a committed value. Never used to emit anything. */
76
75
  function toInputText(value: unknown, col: GridColDef): string {
77
- if (value == null) return '';
78
- if (col.type === 'date' || value instanceof Date) {
79
- const day = toLocalDateString(value);
80
- return day || (typeof value === 'string' ? value : '');
81
- }
82
- if (Array.isArray(value)) return value.map(v => String(v)).join(', ');
83
- return String(value);
76
+ return filterValueToInputText(value, col.type);
84
77
  }
85
78
 
86
79
  /** The item value for typed text. isAnyOf takes a comma-separated list. */