@opencorestack/opengridx 3.8.1 → 3.9.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 (53) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +340 -1
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +7529 -7285
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +93 -4
  9. package/docs/customization/theming.md +11 -2
  10. package/docs/features/ai-toolkit.md +1 -1
  11. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
  12. package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
  13. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
  14. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
  15. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
  16. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
  17. package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
  18. package/lib/components/DataGrid/GridListView.tsx +7 -5
  19. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
  20. package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
  21. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  22. package/lib/components/DataGrid/GridRoot.tsx +8 -1
  23. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  24. package/lib/components/DataGrid/GridViewport.tsx +3 -1
  25. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  26. package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
  27. package/lib/components/FilterPanel/FilterPanel.tsx +18 -17
  28. package/lib/components/Header/ColumnMenu.css +5 -5
  29. package/lib/components/Header/ColumnMenu.tsx +17 -15
  30. package/lib/components/Header/Header.tsx +8 -6
  31. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
  32. package/lib/components/ListView/ListViewRow.tsx +3 -1
  33. package/lib/components/Pagination/Pagination.tsx +15 -18
  34. package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
  35. package/lib/components/Row/Row.tsx +5 -3
  36. package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
  37. package/lib/components/Toolbar/GridToolbar.tsx +35 -29
  38. package/lib/components/Toolbar/PivotPanel.tsx +26 -24
  39. package/lib/components/ui/Checkbox.css +5 -5
  40. package/lib/components/ui/DragHandleIcon.tsx +3 -1
  41. package/lib/components/ui/ExpandIcon.css +3 -3
  42. package/lib/components/ui/ExpandIcon.tsx +3 -1
  43. package/lib/hooks/core/useGridRowInteractions.ts +13 -3
  44. package/lib/hooks/core/useGridToolbarStack.ts +2 -2
  45. package/lib/hooks/core/useGridVirtualization.ts +40 -25
  46. package/lib/hooks/core/useShallowStableValue.ts +29 -0
  47. package/lib/hooks/features/useGridAiAssistant.ts +52 -22
  48. package/lib/theme/DataGridThemeProvider.tsx +9 -0
  49. package/lib/theme/types.ts +18 -0
  50. package/lib/types/locale.ts +344 -0
  51. package/lib/utils/gridLocale.ts +241 -0
  52. package/llms.txt +3 -1
  53. package/package.json +1 -1
@@ -73,33 +73,23 @@ export function useGridVirtualization<R extends GridRowModel>(
73
73
  maxContentHeight = Infinity,
74
74
  } = params;
75
75
 
76
- return useMemo<GridVirtualizationResult<R>>(() => {
77
- const {
78
- unpinnedAccWidths,
79
- unpinnedColsWithWidth,
80
- leftPinnedCols,
81
- rightPinnedCols,
82
- totalWidth,
83
- leftWidth,
84
- rightWidth,
85
- unpinnedTotalWidth,
86
- systemColumnsWidth,
87
- cumulativeHeights,
88
- pinnedTopHeight,
89
- pinnedBottomHeight,
90
- unpinnedRowsHeight,
91
- rowHeights,
92
- unpinnedRowsLength,
93
- } = layout;
94
-
76
+ const {
77
+ unpinnedAccWidths,
78
+ unpinnedColsWithWidth,
79
+ leftPinnedCols,
80
+ rightPinnedCols,
81
+ leftWidth,
82
+ unpinnedTotalWidth,
83
+ } = layout;
84
+
85
+ // The column window has its own memo: it depends only on column widths and the horizontal scroll,
86
+ // so a row change (an edit) or a vertical scroll keeps the same `virtualColumns` array, and rows
87
+ // memoised on their columns do not re-render.
88
+ const columnWindow = useMemo(() => {
95
89
  const viewportWidth = rawViewportWidth || 1000;
96
- const viewportHeight = autoHeight
97
- ? pinnedTopHeight + unpinnedRowsHeight + pinnedBottomHeight + 50
98
- : rawViewportHeight || 600;
99
-
100
- const overscanCols = 6;
101
90
  const visibleLocalStart = Math.max(0, scrollLeft - leftWidth);
102
91
  const visibleLocalEnd = scrollLeft + viewportWidth - leftWidth;
92
+ const overscanCols = 6;
103
93
 
104
94
  let firstUnpinnedIndex = 0;
105
95
  let lastUnpinnedIndex = unpinnedColsWithWidth.length - 1;
@@ -129,6 +119,28 @@ export function useGridVirtualization<R extends GridRowModel>(
129
119
  ...(rightSpacerWidth > 0 ? [{ field: '__spacer_right__', width: rightSpacerWidth, isSpacer: true }] : []),
130
120
  ...rightPinnedCols,
131
121
  ];
122
+ return { firstUnpinnedIndex, lastUnpinnedIndex, virtualColumns };
123
+ }, [unpinnedAccWidths, unpinnedColsWithWidth, leftPinnedCols, rightPinnedCols, leftWidth, unpinnedTotalWidth, scrollLeft, rawViewportWidth]);
124
+
125
+ return useMemo<GridVirtualizationResult<R>>(() => {
126
+ const {
127
+ totalWidth,
128
+ rightWidth,
129
+ systemColumnsWidth,
130
+ cumulativeHeights,
131
+ pinnedTopHeight,
132
+ pinnedBottomHeight,
133
+ unpinnedRowsHeight,
134
+ rowHeights,
135
+ unpinnedRowsLength,
136
+ } = layout;
137
+
138
+ const viewportWidth = rawViewportWidth || 1000;
139
+ const viewportHeight = autoHeight
140
+ ? pinnedTopHeight + unpinnedRowsHeight + pinnedBottomHeight + 50
141
+ : rawViewportHeight || 600;
142
+
143
+ const { firstUnpinnedIndex, lastUnpinnedIndex, virtualColumns } = columnWindow;
132
144
 
133
145
  const hasHorizontalScroll = viewportWidth > 0 && totalWidth > viewportWidth;
134
146
  const scrollbarBuffer = hasHorizontalScroll ? 16 : 0;
@@ -192,8 +204,11 @@ export function useGridVirtualization<R extends GridRowModel>(
192
204
  };
193
205
  }, [
194
206
  layout,
207
+ columnWindow,
208
+ leftWidth,
209
+ unpinnedAccWidths,
210
+ unpinnedColsWithWidth,
195
211
  scrollTop,
196
- scrollLeft,
197
212
  rawViewportWidth,
198
213
  rawViewportHeight,
199
214
  autoHeight,
@@ -0,0 +1,29 @@
1
+ import { useState } from 'react';
2
+
3
+ /** Equal when both hold the same keys with `Object.is`-equal values, one level of plain records deep. */
4
+ function isShallowEqual(a: unknown, b: unknown, depth = 1): boolean {
5
+ if (Object.is(a, b)) return true;
6
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) || Array.isArray(b)) return false;
7
+ const ra = a as Record<string, unknown>;
8
+ const rb = b as Record<string, unknown>;
9
+ const keys = Object.keys(ra);
10
+ if (keys.length !== Object.keys(rb).length) return false;
11
+ return keys.every(key => Object.prototype.hasOwnProperty.call(rb, key)
12
+ && (Object.is(ra[key], rb[key]) || (depth > 0 && isShallowEqual(ra[key], rb[key], depth - 1))));
13
+ }
14
+
15
+ /**
16
+ * `value`, or the previous one while the new one holds the same entries. A consumer passing an
17
+ * inline object (`localeText={{ … }}`) then keeps one identity across renders, so what is memoised
18
+ * on it (the resolved locale, and every row that reads it) does not update needlessly. Functions
19
+ * inside are compared by identity: an inline function still counts as a change.
20
+ */
21
+ export function useShallowStableValue<T>(value: T): T {
22
+ const [stable, setStable] = useState(value);
23
+ if (stable !== value && !isShallowEqual(stable, value)) {
24
+ // Adjusting state while rendering (React's documented pattern for derived state).
25
+ setStable(value);
26
+ return value;
27
+ }
28
+ return stable;
29
+ }
@@ -10,7 +10,9 @@ import type {
10
10
  GridAiValidationError,
11
11
  GridApi,
12
12
  GridColDef,
13
+ GridLocaleText,
13
14
  } from '../../types';
15
+ import { resolveGridLocale, type ResolvedGridLocale } from '../../utils/gridLocale';
14
16
  import { attempt } from '../../utils/attempt';
15
17
  import { isGridAiValidated } from '../../utils/aiBrand';
16
18
  import {
@@ -32,6 +34,8 @@ export interface UseGridAiAssistantParams {
32
34
  columns: readonly GridColDef[];
33
35
  onApply?: (result: GridAiPromptResult) => void;
34
36
  onError?: (error: unknown) => void;
37
+ /** The grid's `localeText`, for the status line and the placeholder. */
38
+ localeText?: GridLocaleText;
35
39
  }
36
40
 
37
41
  export interface UseGridAiAssistantResult {
@@ -48,7 +52,6 @@ export interface UseGridAiAssistantResult {
48
52
  announcement: string;
49
53
  }
50
54
 
51
- const DEFAULT_PLACEHOLDER = 'Ask about this table…';
52
55
  const NO_SUGGESTIONS: string[] = [];
53
56
  const NO_ERRORS: GridAiValidationError[] = [];
54
57
  const NOT_VALIDATED = 'The reply was not validated';
@@ -69,13 +72,38 @@ function isAbortError(error: unknown): boolean {
69
72
  return typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'AbortError';
70
73
  }
71
74
 
72
- function errorText(error: unknown): string {
73
- return error instanceof Error && error.message ? error.message : 'Unknown error';
74
- }
75
+ /** What the status line says, turned into text with the grid's locale at render time. */
76
+ type StatusLine =
77
+ | { kind: 'none' }
78
+ | { kind: 'thinking' }
79
+ | { kind: 'stopped' }
80
+ | { kind: 'undone' }
81
+ | { kind: 'removed' }
82
+ | { kind: 'applied'; count: number; ignored: number }
83
+ | { kind: 'notValidated' }
84
+ | { kind: 'failed'; error: unknown };
85
+
86
+ const NO_STATUS: StatusLine = { kind: 'none' };
87
+ const THINKING: StatusLine = { kind: 'thinking' };
88
+ const STOPPED: StatusLine = { kind: 'stopped' };
89
+ const UNDONE: StatusLine = { kind: 'undone' };
90
+ const REMOVED: StatusLine = { kind: 'removed' };
91
+ const NOT_VALIDATED_STATUS: StatusLine = { kind: 'notValidated' };
75
92
 
76
- function changesText(count: number, ignored: number): string {
77
- const applied = count === 0 ? 'No changes' : `Applied ${count} ${count === 1 ? 'change' : 'changes'}`;
78
- return ignored > 0 ? `${applied}, ${ignored} ignored` : applied;
93
+ function statusLineText(line: StatusLine, locale: ResolvedGridLocale): string {
94
+ switch (line.kind) {
95
+ case 'none': return '';
96
+ case 'thinking': return locale.aiAssistantThinking;
97
+ case 'stopped': return locale.aiAssistantStopped;
98
+ case 'undone': return locale.aiAssistantUndone;
99
+ case 'removed': return locale.aiAssistantChangeRemoved;
100
+ case 'applied': return locale.aiAssistantApplied(line.count, line.ignored);
101
+ case 'notValidated': return locale.aiAssistantNotValidated;
102
+ case 'failed': {
103
+ const { error } = line;
104
+ return locale.aiAssistantFailed(error instanceof Error && error.message ? error.message : locale.aiAssistantUnknownError);
105
+ }
106
+ }
79
107
  }
80
108
 
81
109
  /** Calls a consumer callback; a throw is warned about (development) and otherwise ignored. */
@@ -92,11 +120,13 @@ function notify<T>(callback: ((value: T) => void) | undefined, value: T, name: s
92
120
  * `closeAiAssistant`.
93
121
  */
94
122
  export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiAssistantResult {
95
- const { apiRef, options, columns } = params;
123
+ const { apiRef, options, columns, localeText } = params;
124
+ const locale = useMemo(() => resolveGridLocale(localeText), [localeText]);
96
125
  const enabled = Boolean(options);
97
126
  const [open, setOpen] = useState(false);
98
127
  const [status, setStatus] = useState<GridAiAssistantStatus>('idle');
99
- const [statusText, setStatusText] = useState('');
128
+ const [statusLine, setStatusLine] = useState<StatusLine>(NO_STATUS);
129
+ const statusText = statusLineText(statusLine, locale);
100
130
  const [message, setMessage] = useState<string | null>(null);
101
131
  const [errors, setErrors] = useState<GridAiValidationError[]>(NO_ERRORS);
102
132
  const [applied, setApplied] = useState<Applied | null>(null);
@@ -135,7 +165,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
135
165
  if (controllerRef.current) {
136
166
  abortRequest();
137
167
  setStatus('stopped');
138
- setStatusText('Stopped');
168
+ setStatusLine(STOPPED);
139
169
  }
140
170
  setOpen(false);
141
171
  const target = returnFocusRef.current;
@@ -148,9 +178,9 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
148
178
  else openPanel(trigger);
149
179
  }, [openPanel, closePanel]);
150
180
 
151
- const fail = useCallback((text: string, error: unknown) => {
181
+ const fail = useCallback((line: StatusLine, error: unknown) => {
152
182
  setStatus('error');
153
- setStatusText(text);
183
+ setStatusLine(line);
154
184
  setMessage(null);
155
185
  setErrors(NO_ERRORS);
156
186
  notify(latestRef.current.params.onError, error, 'onAiAssistantError');
@@ -163,7 +193,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
163
193
  if (process.env.NODE_ENV !== 'production') {
164
194
  console.warn('[OpenGridX] aiAssistant: the onPrompt result was not applied because its state was not validated. Return the result of createGridAiPromptHandler, or pass the model reply through validateGridAiState (from @opencorestack/opengridx/ai) and return its state.');
165
195
  }
166
- fail(NOT_VALIDATED, new Error(NOT_VALIDATED));
196
+ fail(NOT_VALIDATED_STATUS, new Error(NOT_VALIDATED));
167
197
  return;
168
198
  }
169
199
  const allowed = pickAllowedParts(result.state, current.options?.parts ?? AI_PARTS);
@@ -177,7 +207,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
177
207
  setMessage(typeof result.message === 'string' && result.message ? result.message : null);
178
208
  setHistory((list) => [...list, { prompt, applied: state }]);
179
209
  setStatus('applied');
180
- setStatusText(changesText(buildAiChips(state, (f) => f).length, ignored.length));
210
+ setStatusLine({ kind: 'applied', count: buildAiChips(state, (f) => f).length, ignored: ignored.length });
181
211
  notify(current.onApply, result, 'onAiAssistantApply');
182
212
  }, [apiRef, fail]);
183
213
 
@@ -190,7 +220,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
190
220
  const controller = new AbortController();
191
221
  controllerRef.current = controller;
192
222
  setStatus('running');
193
- setStatusText('Thinking…');
223
+ setStatusLine(THINKING);
194
224
  setMessage(null);
195
225
  setErrors(NO_ERRORS);
196
226
  const context = { currentState: apiRef.current.getGridAiState(), history: earlier, signal: controller.signal };
@@ -207,10 +237,10 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
207
237
  controllerRef.current = null;
208
238
  if (controller.signal.aborted || isAbortError(error)) {
209
239
  setStatus('stopped');
210
- setStatusText('Stopped');
240
+ setStatusLine(STOPPED);
211
241
  return;
212
242
  }
213
- fail(`The assistant could not answer: ${errorText(error)}`, error);
243
+ fail({ kind: 'failed', error }, error);
214
244
  },
215
245
  );
216
246
  }, [apiRef, abortRequest, applyResult, fail]);
@@ -219,7 +249,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
219
249
  if (!controllerRef.current) return;
220
250
  abortRequest();
221
251
  setStatus('stopped');
222
- setStatusText('Stopped');
252
+ setStatusLine(STOPPED);
223
253
  }, [abortRequest]);
224
254
 
225
255
  const undo = useCallback(() => {
@@ -230,7 +260,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
230
260
  setErrors(NO_ERRORS);
231
261
  setMessage(null);
232
262
  setStatus('idle');
233
- setStatusText('Undone');
263
+ setStatusLine(UNDONE);
234
264
  }, [apiRef]);
235
265
 
236
266
  const removeChip = useCallback((id: string) => {
@@ -239,7 +269,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
239
269
  const state = removeAiChip(last.state, id);
240
270
  applyGridAiState(apiRef.current, effectiveState(state, last.previous));
241
271
  setApplied({ state, previous: last.previous });
242
- setStatusText('Change removed');
272
+ setStatusLine(REMOVED);
243
273
  }, [apiRef]);
244
274
 
245
275
  useLayoutEffect(() => {
@@ -261,7 +291,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
261
291
  chips,
262
292
  history,
263
293
  suggestions: options.suggestions ?? NO_SUGGESTIONS,
264
- placeholder: options.placeholder ?? DEFAULT_PLACEHOLDER,
294
+ placeholder: options.placeholder ?? locale.aiAssistantPlaceholder,
265
295
  voiceAvailable: options.voice !== false && hasSpeechRecognition(),
266
296
  canUndo: applied !== null,
267
297
  submit,
@@ -270,7 +300,7 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
270
300
  removeChip,
271
301
  close: closePanel,
272
302
  };
273
- }, [isOpen, options, status, statusText, message, errors, chips, history, applied, submit, stop, undo, removeChip, closePanel]);
303
+ }, [isOpen, options, status, statusText, message, errors, chips, history, applied, submit, stop, undo, removeChip, closePanel, locale]);
274
304
 
275
305
  return {
276
306
  enabled,
@@ -303,6 +303,10 @@ function themeToCSS(theme: GridTheme): Record<string, string> {
303
303
  set('--ogx-grid-pinned-right-shadow', g.pinnedRightShadow);
304
304
  set('--ogx-checkbox-bg', g.checkboxBg);
305
305
  set('--ogx-checkbox-border', g.checkboxBorder);
306
+ set('--ogx-checkbox-checked-bg', g.checkboxCheckedBackground);
307
+ set('--ogx-checkbox-checkmark', g.checkboxCheckmark);
308
+ set('--ogx-expand-icon-color', g.expandIconColor);
309
+ set('--ogx-expand-icon-hover-color', g.expandIconHoverColor);
306
310
  }
307
311
 
308
312
  if (theme.transitions) {
@@ -350,6 +354,11 @@ function themeToCSS(theme: GridTheme): Record<string, string> {
350
354
  set('--ogx-overlay-item-selected-text', o.itemSelectedText);
351
355
  set('--ogx-overlay-item-danger-bg', o.itemDangerBackground);
352
356
  set('--ogx-overlay-item-danger-text', o.itemDangerText);
357
+ set('--ogx-overlay-item-indicator', o.itemIndicatorColor);
358
+ set('--ogx-overlay-item-selected-icon', o.itemSelectedIcon);
359
+ set('--ogx-overlay-icon', o.iconColor);
360
+ set('--ogx-overlay-section-label-text', o.sectionLabelText);
361
+ set('--ogx-overlay-divider', o.dividerColor);
353
362
  set('--ogx-overlay-font-family', o.fontFamily);
354
363
  set('--ogx-overlay-font-weight', o.fontWeight);
355
364
  set('--ogx-overlay-font-size', o.fontSize);
@@ -90,6 +90,16 @@ export interface GridThemeOverlays {
90
90
  itemSelectedText?: string;
91
91
  itemDangerBackground?: string;
92
92
  itemDangerText?: string;
93
+ /** The dot before the selected option of a menu group (`--ogx-overlay-item-indicator`). Default: `itemSelectedText`. @since v3.9 */
94
+ itemIndicatorColor?: string;
95
+ /** The icon of the selected option (`--ogx-overlay-item-selected-icon`). Default: the primary colour. @since v3.9 */
96
+ itemSelectedIcon?: string;
97
+ /** Menu item icons (`--ogx-overlay-icon`). Default: gray-400. @since v3.9 */
98
+ iconColor?: string;
99
+ /** Section labels such as "Pin column" (`--ogx-overlay-section-label-text`). Default: gray-400. @since v3.9 */
100
+ sectionLabelText?: string;
101
+ /** Lines between menu sections (`--ogx-overlay-divider`). Default: `border`. @since v3.9 */
102
+ dividerColor?: string;
93
103
 
94
104
  /** Font of column menus, header filter menus, toolbar panels and the AI panel (`--ogx-overlay-font-family`). Default: the grid font. @since v3.7 */
95
105
  fontFamily?: string;
@@ -200,6 +210,14 @@ export interface GridThemeGrid {
200
210
  pinnedRightShadow?: string;
201
211
  checkboxBg?: string;
202
212
  checkboxBorder?: string;
213
+ /** Fill and border of a checked or indeterminate checkbox (`--ogx-checkbox-checked-bg`). Default: the primary colour. @since v3.9 */
214
+ checkboxCheckedBackground?: string;
215
+ /** The check mark and indeterminate dash (`--ogx-checkbox-checkmark`). Default: white. @since v3.9 */
216
+ checkboxCheckmark?: string;
217
+ /** The expand / collapse chevron of group and tree rows (`--ogx-expand-icon-color`). Default: the header text colour. @since v3.9 */
218
+ expandIconColor?: string;
219
+ /** The chevron on hover (`--ogx-expand-icon-hover-color`). Default: the row text colour. @since v3.9 */
220
+ expandIconHoverColor?: string;
203
221
  }
204
222
 
205
223
  export interface GridThemeTransitions {