@opencorestack/opengridx 3.2.2 → 3.4.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 (88) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +16 -2
  3. package/dist/ai.cjs +1 -0
  4. package/dist/ai.cjs.map +1 -0
  5. package/dist/ai.d.ts +166 -0
  6. package/dist/ai.es.js +1 -0
  7. package/dist/ai.es.js.map +1 -0
  8. package/dist/index.d.ts +321 -3
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6483 -5291
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -13
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +132 -3
  15. package/docs/README.md +4 -1
  16. package/docs/architecture/datagrid-orchestration.md +23 -6
  17. package/docs/components/header.md +25 -0
  18. package/docs/customization/theming.md +12 -1
  19. package/docs/features/ai-toolkit.md +119 -0
  20. package/docs/features/cell-selection.md +131 -0
  21. package/docs/features/clipboard.md +90 -0
  22. package/docs/features/editing-reordering.md +50 -0
  23. package/docs/features/keyboard-navigation.md +25 -2
  24. package/docs/features/undo-redo.md +79 -0
  25. package/docs/roadmap.md +64 -7
  26. package/lib/ai/coerce.ts +90 -0
  27. package/lib/ai/columns.ts +90 -0
  28. package/lib/ai/index.ts +16 -0
  29. package/lib/ai/schema.ts +214 -0
  30. package/lib/ai/types.ts +88 -0
  31. package/lib/ai/validate.ts +286 -0
  32. package/lib/components/Cell/Cell.css +35 -0
  33. package/lib/components/Cell/Cell.tsx +25 -13
  34. package/lib/components/Cell/CellErrorBoundary.tsx +3 -0
  35. package/lib/components/Cell/GridEditInputCell.css +12 -4
  36. package/lib/components/Cell/GridEditInputCell.tsx +19 -12
  37. package/lib/components/CellSelectionStats/CellSelectionStats.css +31 -0
  38. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +32 -0
  39. package/lib/components/CellSelectionStats/GridCellSelectionStatsArea.tsx +24 -0
  40. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
  41. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  42. package/lib/components/DataGrid/DataGrid.tsx +123 -3
  43. package/lib/components/DataGrid/GridLiveRegion.tsx +8 -2
  44. package/lib/components/DataGrid/GridPinnedRows.tsx +9 -0
  45. package/lib/components/DataGrid/GridToolbarSlot.tsx +5 -0
  46. package/lib/components/DataGrid/GridVirtualRows.tsx +9 -0
  47. package/lib/components/FilterPanel/FilterPanel.css +14 -0
  48. package/lib/components/FilterPanel/FilterPanel.tsx +5 -3
  49. package/lib/components/Header/Header.css +12 -0
  50. package/lib/components/Header/Header.tsx +17 -4
  51. package/lib/components/ListView/ListView.css +2 -4
  52. package/lib/components/ListView/ListViewRow.tsx +2 -2
  53. package/lib/components/Row/Row.tsx +38 -1
  54. package/lib/components/Toolbar/GlobalSearch.css +16 -0
  55. package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
  56. package/lib/components/Toolbar/GridToolbar.tsx +10 -4
  57. package/lib/components/ui/Checkbox.tsx +26 -10
  58. package/lib/components/ui/Input.css +43 -0
  59. package/lib/components/ui/Input.tsx +24 -5
  60. package/lib/hooks/core/gridApiPlaceholder.ts +14 -1
  61. package/lib/hooks/core/useDataGrid.ts +27 -0
  62. package/lib/hooks/core/useGridCellSelection.ts +631 -0
  63. package/lib/hooks/core/useGridCellSelectionApi.ts +97 -0
  64. package/lib/hooks/core/useGridClipboardApi.ts +8 -2
  65. package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
  66. package/lib/hooks/core/useGridControlledState.ts +38 -0
  67. package/lib/hooks/core/useGridKeyboardNavigation.ts +32 -0
  68. package/lib/hooks/core/useGridRowInteractions.ts +8 -4
  69. package/lib/hooks/core/useGridUndoRedo.ts +220 -0
  70. package/lib/hooks/features/useAggregation.ts +8 -6
  71. package/lib/hooks/features/useGridBatchEdit.ts +217 -0
  72. package/lib/hooks/features/useGridClipboard.ts +47 -2
  73. package/lib/hooks/features/useGridEditing.ts +39 -51
  74. package/lib/index.ts +16 -0
  75. package/lib/styles/base/variables.css +6 -1
  76. package/lib/styles/opengridx.css +3 -1
  77. package/lib/theme/DataGridThemeProvider.tsx +4 -0
  78. package/lib/theme/presets.ts +8 -0
  79. package/lib/theme/types.ts +4 -0
  80. package/lib/types/index.ts +254 -0
  81. package/lib/utils/attempt.ts +14 -0
  82. package/lib/utils/cellSelection.ts +238 -0
  83. package/lib/utils/editing/commit.ts +69 -0
  84. package/lib/utils/editing/targets.ts +109 -0
  85. package/lib/utils/headerWrap.ts +23 -0
  86. package/lib/utils/parsing.ts +252 -0
  87. package/llms.txt +71 -2
  88. package/package.json +14 -4
@@ -74,6 +74,7 @@
74
74
  font-family: var(--ogx-font-family, system-ui, sans-serif);
75
75
  font-size: var(--ogx-font-size-sm, 13px);
76
76
  color: var(--ogx-toolbar-input-text, var(--ogx-grid-row-text, #1e293b));
77
+ line-height: revert;
77
78
  outline: none;
78
79
  opacity: 0;
79
80
  pointer-events: none;
@@ -88,6 +89,21 @@
88
89
  font-size: var(--ogx-font-size-sm, 13px);
89
90
  }
90
91
 
92
+ /* The input renders through the shared Input: its wrapper is the flex item and the container
93
+ draws the box, so the wrapper has none of its own. */
94
+ .ogx-global-search .ogx-input-wrapper {
95
+ flex: 1 1 auto;
96
+ min-width: 0;
97
+ height: 100%;
98
+ border: none;
99
+ border-radius: 0;
100
+ background: transparent;
101
+ transition: none;
102
+ }
103
+ .ogx-global-search .ogx-input-wrapper:focus-within {
104
+ box-shadow: none;
105
+ }
106
+
91
107
  /* ─── Clear button ────────────────────────────────────────────────────────── */
92
108
  .ogx-global-search__clear {
93
109
  position: absolute;
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
2
+ import { Input } from '../ui/Input';
2
3
 
3
4
 
4
5
  interface GlobalSearchProps {
@@ -143,9 +144,9 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
143
144
  <div className="ogx-global-search__icon-wrapper">
144
145
  <SearchIcon />
145
146
  </div>
146
- <input
147
+ <Input
147
148
  ref={inputRef}
148
- className="ogx-global-search__input"
149
+ inputClassName="ogx-global-search__input"
149
150
  value={localValue}
150
151
  onChange={(e) => setLocalValue(e.target.value)}
151
152
  onFocus={handleFocus}
@@ -474,9 +474,11 @@ function FilterPanelWrapper({
474
474
  }
475
475
 
476
476
  const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
477
+ // Module-level default, so `columns` is the same frozen value on every render (React Compiler).
478
+ const NO_COLUMNS: GridColDef[] = [];
477
479
 
478
480
  export function GridToolbar({
479
- columns = [],
481
+ columns = NO_COLUMNS,
480
482
  baseColumns,
481
483
  aggregationModel = {},
482
484
  onAggregationModelChange,
@@ -504,7 +506,7 @@ export function GridToolbar({
504
506
  // At most one panel is open. Every way the Columns panel closes (its button, a custom
505
507
  // button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
506
508
  // grid can ask for it again with forceColumnsOpen.
507
- const [openPanel, setOpenPanel] = useState<ToolbarPanel | null>(null);
509
+ const [openPanel, setOpenPanel] = useState<ToolbarPanel | null>(forceColumnsOpen ? 'columns' : null);
508
510
  const aggOpen = openPanel === 'summaries';
509
511
  const pivotOpen = openPanel === 'pivot';
510
512
  const colsOpen = openPanel === 'columns';
@@ -517,9 +519,13 @@ export function GridToolbar({
517
519
  const togglePanel = (panel: ToolbarPanel) => switchPanel(openPanel === panel ? null : panel);
518
520
  const closePanel = (panel: ToolbarPanel) => { if (openPanel === panel) switchPanel(null); };
519
521
 
520
- useEffect(() => {
522
+ // forceColumnsOpen turning true opens the Columns panel. Adjusted during render (not in an
523
+ // effect) so the panel opens in the same commit.
524
+ const [seenForceColumnsOpen, setSeenForceColumnsOpen] = useState(forceColumnsOpen);
525
+ if (seenForceColumnsOpen !== forceColumnsOpen) {
526
+ setSeenForceColumnsOpen(forceColumnsOpen);
521
527
  if (forceColumnsOpen) setOpenPanel('columns');
522
- }, [forceColumnsOpen]);
528
+ }
523
529
 
524
530
  // Tell the grid this toolbar shows the Columns panel for the column menu; it can only do so
525
531
  // when the grid's forceColumnsOpen reaches it and the Columns panel is enabled.
@@ -4,28 +4,42 @@ import React from 'react';
4
4
  export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
5
  indeterminate?: boolean;
6
6
  label?: string;
7
+ /** A ref to the `<input>`; the forwarded `ref` reaches it as well. */
7
8
  inputRef?: React.Ref<HTMLInputElement>;
9
+ /** Classes added to the `<input>` itself, next to `ogx-checkbox__input`. `className` goes to the wrapper `<label>`. */
10
+ inputClassName?: string;
8
11
  }
9
12
 
10
- export const Checkbox: React.FC<CheckboxProps> = ({
13
+ /** Points a callback or object ref at `element`. Outside the component, so it does not mutate a prop in render scope. */
14
+ function assignRef(ref: React.Ref<HTMLInputElement> | undefined, element: HTMLInputElement | null): void {
15
+ if (typeof ref === 'function') {
16
+ ref(element);
17
+ } else if (ref) {
18
+ (ref as React.MutableRefObject<HTMLInputElement | null>).current = element;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * A styled checkbox: a visually hidden `<input type="checkbox">` inside a `<label>`. Other props
24
+ * (`onChange`, `onMouseDown`, `tabIndex`, `aria-*`, …) go to the `<input>`, and so does `ref`.
25
+ */
26
+ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox({
11
27
  indeterminate = false,
12
28
  label,
13
29
  className = '',
30
+ inputClassName = '',
14
31
  inputRef,
15
32
  ...props
16
- }) => {
33
+ }, forwardedRef) {
17
34
  const internalRef = React.useRef<HTMLInputElement>(null);
18
35
 
19
36
  // Combine refs
20
37
  const setRef = React.useCallback((element: HTMLInputElement | null) => {
21
38
  internalRef.current = element;
22
39
 
23
- if (typeof inputRef === 'function') {
24
- inputRef(element);
25
- } else if (inputRef) {
26
- (inputRef as React.MutableRefObject<HTMLInputElement | null>).current = element;
27
- }
28
- }, [inputRef]);
40
+ assignRef(inputRef, element);
41
+ assignRef(forwardedRef, element);
42
+ }, [inputRef, forwardedRef]);
29
43
 
30
44
  React.useEffect(() => {
31
45
  if (internalRef.current) {
@@ -46,7 +60,7 @@ export const Checkbox: React.FC<CheckboxProps> = ({
46
60
  <input
47
61
  ref={setRef}
48
62
  type="checkbox"
49
- className="ogx-checkbox__input"
63
+ className={['ogx-checkbox__input', inputClassName].filter(Boolean).join(' ')}
50
64
  {...props}
51
65
  />
52
66
  <span className="ogx-checkbox__box">
@@ -64,4 +78,6 @@ export const Checkbox: React.FC<CheckboxProps> = ({
64
78
  {label && <span className="ogx-checkbox__label">{label}</span>}
65
79
  </label>
66
80
  );
67
- };
81
+ });
82
+
83
+ Checkbox.displayName = 'Checkbox';
@@ -57,3 +57,46 @@
57
57
  .ogx-input-wrapper--disabled .ogx-input {
58
58
  cursor: not-allowed;
59
59
  }
60
+
61
+ /* ── Cell variant: an editor that fills a grid cell ───────────────────── */
62
+ .ogx-input-wrapper--cell {
63
+ /* Not positioned: the editing cell's focus outline must stay painted over the editor. */
64
+ position: static;
65
+ display: flex;
66
+ width: 100%;
67
+ height: 100%;
68
+ border: none;
69
+ border-radius: 0;
70
+ background: transparent;
71
+ box-shadow: none;
72
+ transition: none;
73
+ }
74
+ .ogx-input-wrapper--cell:hover:not(.ogx-input-wrapper--disabled),
75
+ .ogx-input-wrapper--cell:focus-within:not(.ogx-input-wrapper--disabled) {
76
+ box-shadow: none;
77
+ }
78
+ .ogx-input-wrapper--cell .ogx-input {
79
+ width: 100%;
80
+ min-width: 0;
81
+ height: 100%;
82
+ padding: 0 10px;
83
+ font-family: inherit;
84
+ font-size: inherit;
85
+ line-height: normal;
86
+ color: inherit;
87
+ }
88
+ .ogx-input-wrapper--cell .ogx-input__adornment--start {
89
+ padding-left: 10px;
90
+ }
91
+ .ogx-input-wrapper--cell .ogx-input__adornment--start + .ogx-input {
92
+ padding-left: 4px;
93
+ }
94
+ .ogx-input-wrapper--cell .ogx-input__adornment--end {
95
+ padding-right: 10px;
96
+ }
97
+ .ogx-input-wrapper--cell.ogx-input-wrapper--error,
98
+ .ogx-input-wrapper--cell.ogx-input-wrapper--error:focus-within:not(.ogx-input-wrapper--disabled) {
99
+ /* Positioned, so the error edge is painted over the cell's focus outline. */
100
+ position: relative;
101
+ box-shadow: inset 0 0 0 2px var(--ogx-color-error);
102
+ }
@@ -1,4 +1,3 @@
1
-
2
1
  import React from 'react';
3
2
 
4
3
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -6,24 +5,40 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
6
5
  error?: boolean;
7
6
  startAdornment?: React.ReactNode;
8
7
  endAdornment?: React.ReactNode;
8
+ /** Classes added to the `<input>` itself, next to `ogx-input`. `className` goes to the wrapper. */
9
+ inputClassName?: string;
10
+ /**
11
+ * `'field'` (default): a bordered form field. `'cell'`: fills a grid cell with no border,
12
+ * background or focus ring, in the cell's font, for use in `renderEditCell`.
13
+ */
14
+ variant?: 'field' | 'cell';
9
15
  }
10
16
 
11
- export const Input: React.FC<InputProps> = ({
17
+ /**
18
+ * A text field: an `<input>` inside an `.ogx-input-wrapper` that holds the adornments.
19
+ * `ref` reaches the `<input>` (React 18 and 19).
20
+ */
21
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input({
12
22
  fullWidth = false,
13
23
  error = false,
14
24
  startAdornment,
15
25
  endAdornment,
16
26
  className = '',
27
+ inputClassName = '',
28
+ variant = 'field',
17
29
  ...props
18
- }) => {
30
+ }, ref) {
19
31
  const wrapperClassNames = [
20
32
  'ogx-input-wrapper',
33
+ variant === 'cell' && 'ogx-input-wrapper--cell',
21
34
  fullWidth && 'ogx-input-wrapper--full-width',
22
35
  error && 'ogx-input-wrapper--error',
23
36
  props.disabled && 'ogx-input-wrapper--disabled',
24
37
  className
25
38
  ].filter(Boolean).join(' ');
26
39
 
40
+ const inputClassNames = ['ogx-input', inputClassName].filter(Boolean).join(' ');
41
+
27
42
  return (
28
43
  <div className={wrapperClassNames}>
29
44
  {startAdornment && (
@@ -32,7 +47,9 @@ export const Input: React.FC<InputProps> = ({
32
47
  </div>
33
48
  )}
34
49
  <input
35
- className="ogx-input"
50
+ ref={ref}
51
+ className={inputClassNames}
52
+ aria-invalid={error ? true : undefined}
36
53
  {...props}
37
54
  />
38
55
  {endAdornment && (
@@ -42,4 +59,6 @@ export const Input: React.FC<InputProps> = ({
42
59
  )}
43
60
  </div>
44
61
  );
45
- };
62
+ });
63
+
64
+ Input.displayName = 'Input';
@@ -1,6 +1,7 @@
1
- import type { GridApi, GridRowModel, GridValidRowModel } from '../../types';
1
+ import type { GridApi, GridBatchEditResult, GridRowModel, GridValidRowModel } from '../../types';
2
2
 
3
3
  const noop = () => {};
4
+ const emptyEditResult = (): Promise<GridBatchEditResult> => Promise.resolve({ updated: [], failed: [], skipped: [] });
4
5
 
5
6
  /**
6
7
  * A complete GridApi whose methods are safe no-ops. `useGridApiRef()` starts with one so
@@ -32,5 +33,17 @@ export function createGridApiPlaceholder<R extends GridValidRowModel = GridRowMo
32
33
  autosizeColumn: noop,
33
34
  autosizeColumns: noop,
34
35
  copySelectedRows: () => Promise.resolve(),
36
+ getCellSelectionModel: () => [],
37
+ setCellSelectionModel: noop,
38
+ selectCellRange: noop,
39
+ clearCellSelection: noop,
40
+ getSelectedCells: () => [],
41
+ copySelectedCells: () => Promise.resolve(),
42
+ pasteText: emptyEditResult,
43
+ undo: emptyEditResult,
44
+ redo: emptyEditResult,
45
+ canUndo: () => false,
46
+ canRedo: () => false,
47
+ clearHistory: noop,
35
48
  };
36
49
  }
@@ -18,6 +18,7 @@ type GetRowId = (row: GridRowModel) => GridRowId;
18
18
  type GridAction =
19
19
  | { type: 'SET_ROWS'; payload: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]); getRowId: GetRowId }
20
20
  | { type: 'REPLACE_ROW'; id: GridRowId; row: GridRowModel }
21
+ | { type: 'REPLACE_ROWS'; rows: ReadonlyMap<GridRowId, GridRowModel> }
21
22
  | { type: 'SET_COLUMNS'; payload: GridColDef[] }
22
23
  | { type: 'SET_DIMENSIONS'; payload: { viewportWidth: number; viewportHeight: number } }
23
24
  | { type: 'SET_DATASOURCE_LOADING'; payload: boolean }
@@ -134,6 +135,26 @@ function gridReducer(state: GridState, action: GridAction): GridState {
134
135
  };
135
136
  }
136
137
 
138
+ case 'REPLACE_ROWS': {
139
+ // A batch edit (paste, range clear, undo, redo): every stored row in one update.
140
+ let changed = false;
141
+ const idRowsLookup = new Map(state.rows.idRowsLookup);
142
+ const idByRow = new Map(state.rows.idByRow);
143
+ for (const [id, row] of action.rows) {
144
+ const previous = idRowsLookup.get(id);
145
+ if (previous === undefined) continue;
146
+ changed = true;
147
+ idRowsLookup.set(id, row);
148
+ idByRow.delete(previous);
149
+ idByRow.set(row, id);
150
+ }
151
+ if (!changed) return state;
152
+ return {
153
+ ...state,
154
+ rows: { ...state.rows, idRowsLookup, idByRow },
155
+ };
156
+ }
157
+
137
158
  case 'SET_COLUMNS': {
138
159
  const columnLookup = new Map<string, GridColDef>();
139
160
  const orderedFields: string[] = [];
@@ -270,6 +291,11 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
270
291
  dispatch({ type: 'REPLACE_ROW', id, row });
271
292
  }, []);
272
293
 
294
+ /** Replaces several stored rows in one update (a batch edit); ids no longer stored are ignored. */
295
+ const replaceRows = useCallback((rowsById: ReadonlyMap<GridRowId, GridRowModel>) => {
296
+ if (rowsById.size > 0) dispatch({ type: 'REPLACE_ROWS', rows: rowsById });
297
+ }, []);
298
+
273
299
  const setColumns = useCallback((newColumns: GridColDef[]) => {
274
300
  dispatch({ type: 'SET_COLUMNS', payload: newColumns });
275
301
  }, []);
@@ -307,6 +333,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
307
333
  apiRef,
308
334
  setRows,
309
335
  replaceRow,
336
+ replaceRows,
310
337
  setColumns,
311
338
  setDimensions,
312
339
  setDataSourceLoading,