@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
package/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## [3.5.1] — 2026-10-12
9
+
10
+ ### Fixed
11
+
12
+ - **Large grids did not scroll at all in Firefox.** Firefox lays out elements up to about 17.9M px and collapses a taller one instead of clamping it, so a grid with more content (about 344,000 rows at the default 52 px row height, 559,000 at 32 px) showed only its first screen of rows and had nothing to scroll. The grid now measures the browser's height limit once per page (an invisible probe, `lib/utils/maxElementHeight.ts`) and caps its scroll content to it: Firefox scrolls normally and reaches every row above the limit, as Chromium and WebKit already did up to theirs (about 33.5M px). Rows below the limit are still not reachable in a single scrolling grid; use pagination or server-side loading. Nothing changes for grids under the limit.
13
+ - The dev warning about very tall content now appears only when rows are actually cut off in the current browser, and says how many rows can be scrolled to ("Only the first 344,000 of 500,000 rows can be scrolled to in this browser").
14
+
15
+ ### Internal
16
+
17
+ - **Large-dataset benchmark** — `npm run bench` (`bench/run.mjs`) packs the library, installs it into `bench/app` (a Vite + React 19 production app, like the smoke fixtures; port 4320) and measures it with Playwright in Chromium, Firefox and WebKit at 10k, 100k, 500k and 1M rows (1M also at 32 px): mount, sort, filter, quick filter, group + `sum`, a scripted 5 s scroll (frame-time percentiles and missed frames), DOM size, the furthest reachable row, and long tasks and JS heap in Chromium. Times run from the call to the first painted frame showing the result; medians of 5 runs after a warm-up. `bench/report.mjs` writes the Markdown tables; results and method are in `docs/performance.md` → Benchmarks. Flags `--rows`, `--engine`, `--repeat`, `--quick`. The weekly workflow runs a Chromium `--quick` pass and uploads the JSON; it never fails on numbers. Packing and tarball install are shared with the smoke suite (`scripts/lib/package-fixture.mjs`).
18
+ - **Docs: the browser height limit, as measured** — `docs/performance.md`, `docs/features/virtualization.md` and the README now give the benchmark's numbers instead of unmeasured claims ("60fps for 100k+ rows"), and correct the Firefox limit: past about 17.9M px of content (about 344,000 rows at 52 px) Firefox does not stop at the cap like Chromium and WebKit; the content collapses and the grid does not scroll at all.
19
+
20
+ ## [3.5.0] — 2026-10-09
21
+
22
+ ### Added
23
+
24
+ - **AI toolkit, part 2: the Ask AI panel** — new `aiAssistant` prop (`{ onPrompt, placeholder?, suggestions?, voice?, parts? }`). `GridToolbar` shows an **Ask AI** button (`localeText.aiAssistantButton`) that opens a prompt panel under the toolbar; `apiRef.openAiAssistant()` / `closeAiAssistant()` open and close it without a toolbar, and `slots.aiAssistantPanel` replaces it (`GridAiAssistantPanelProps`). The panel has a prompt input (Enter sends), suggestion chips, a microphone button only where the browser has `SpeechRecognition` (the microphone is requested on click, never on load; `voice: false` hides it), Stop (aborts `context.signal`), the reply text, the session's earlier prompts, and the list of ignored parts. A reply is applied at once as one undoable step: every change is a removable chip (removing one re-applies the reply without it) and **Undo** restores the models from before the reply. `role="dialog"` labelled "Ask AI", focus to the input on open and back on close, Escape closes, status in the grid's live region. New `onAiAssistantApply(result)` and `onAiAssistantError(error)`. Grids without `aiAssistant` are unchanged, and the core bundle still holds no AI code.
25
+ - **Only validated replies are applied.** `validateGridAiState` now brands its result and the result's `state` with the non-enumerable `Symbol.for('opengridx.ai.validated')` (shape, equality and JSON unchanged); the panel applies a state only when it carries it, and otherwise shows "The reply was not validated" with a development warning.
26
+ - **`createGridAiPromptHandler({ columns, callModel, parts?, schemaOptions? })`** in `@opencorestack/opengridx/ai` — builds `onPrompt` for the app's own model: the schema once, then `callModel({ prompt, schema, currentState, history }, { signal })`; the reply may be a state object, a JSON string or text with a fenced JSON block (the text or a `message` key becomes the reply message). A `callModel` failure becomes an error result; an abort rejects with an `AbortError`.
27
+ - **`createGridAgentTools(apiRef, columns, options?)`** in `/ai` — plain tool objects (`name`, `description`, `inputSchema`, `execute`) for the Vercel AI SDK, CopilotKit, OpenAI or Anthropic tool calling and WebMCP: `get_grid_state`, `set_filter`, `set_sort`, `set_grouping`, `set_aggregation`, `set_column_visibility`, `clear_filters`, and `get_row_summary` only with `allowRowAccess: true` (the one tool that sends row data). Inputs are validated and applied all or nothing; `execute` never throws.
28
+ - **`GridApi`**: `setSortModel`, `setRowGroupingModel`, `setAggregationModel`, `setColumnVisibilityModel`, `setPivotModel` (each fires its change callback) and `getGridAiState()`, plus `openAiAssistant()` / `closeAiAssistant()`.
29
+ - **`onRowGroupingModelChange`** — `rowGroupingModel` is now a controlled/uncontrolled pair: without the prop the grid keeps its own grouping model, which `apiRef.setRowGroupingModel` and the assistant change. A grid that passes `rowGroupingModel` stays controlled. (v2.0 removed this callback because nothing called it; the assistant and `apiRef` now do.)
30
+ - New types: `GridAiAssistantOptions`, `GridAiAssistantPanelProps`, `GridAiAssistantStatus`, `GridAiChip`, `GridAiPromptContext`, `GridAiPromptResult`, `GridAiHistoryEntry`, `GridAiModelRequest`, `GridAiPromptHandlerOptions`, `GridAgentTool`, `GridAgentToolResult`, `GridAgentToolsOptions`, `GridAgentApi` (exported from `/ai`); `GridToolbarProps.onAiAssistantToggle` / `aiAssistantOpen` / `aiAssistantLabel`; `GridLocaleText.aiAssistantButton`.
31
+ - Demo page *AI Assistant* (`/ai-assistant`): a simulated, rule-based model (no API key, no network), suggestion chips, the real `fetch` integration sample, and a scripted agent driving the grid through `createGridAgentTools`. Package smoke suite: `ai-assistant` scenario in both fixtures.
32
+ - **Header filter row** — `headerFilters` (default off) adds a filter row under the column headers, inside the sticky header: a text box for `string`, a number box for `number` and a date box for `date` (the shared `Input`), an Any / Yes / No select for `boolean` and a `valueOptions` select for `singleSelect` (none for `image`), each with an operator button that opens a menu of the type's operators and a clear button. Typing is debounced by 300 ms; typing still pending is committed if the column scrolls out of the render window. New `GridColDef.headerFilter` (`false`: empty cell) and `GridColDef.headerFilterOperator` (starting operator; defaults `contains`, `=`, `is`), and `headerFilterHeight` (default 40, CSS variable `--ogx-header-filter-height`).
33
+ - **One shared `filterModel`.** Each cell owns only the root-level item `id: 'header:<field>'`: it is added at the end, updated in place and removed when cleared, and every other item, group, `logicOperator` and `quickFilterValues` passes through. The filter panel edits the same item and keeps its id. When the root `logicOperator` is `'or'`, the field sits inside a group, or the field has an untagged or second root item, the cell shows a read-only "Custom filter" label instead (a button that opens the `GridToolbar` filter panel); the row never rewrites a model it cannot show. With `filterMode="server"` changes are reported through `onFilterModelChange` as usual. Not rendered while `pivotMode` is active.
34
+ - **Layout.** Filter cells take each column's width, flex and sticky offset from the same calculation as the column headers, so they line up with pinned columns, column virtualization and horizontal scrolling. Scroll-into-view (keyboard, `scrollToIndexes`) and range auto-scroll measure the sticky header, so rows never end up under the taller header.
35
+ - **Keyboard and ARIA.** The row is a header row (`role="row"`, counted in `aria-rowcount` and the rows' `aria-rowindex`) of `role="columnheader"` cells labelled "Filter &lt;header&gt;". ArrowDown on a column header moves to its filter cell and on into the first row; Enter, Space or typing focuses the control, Escape returns to the cell, Alt+ArrowDown or Ctrl/Cmd+Enter opens the operator menu, Delete clears the filter. Controls have `tabIndex={-1}`: the grid stays a single Tab stop and Tab from a control leaves the grid.
36
+ - Public classes `ogx__header-filter-row`, `ogx__header-filter-cell` (`--focused`, `--active`, `--custom`, `--empty`, `--system`, `--pinned`, `--pinned-left`, `--pinned-right`, `--pinned-left-last`, `--pinned-right-first`), `ogx__header-filter-operator` (`--open`), `ogx__header-filter-menu`, `ogx__header-filter-input-wrapper`, `ogx__header-filter-input`, `ogx__header-filter-select`, `ogx__header-filter-operator-label`, `ogx__header-filter-clear`, `ogx__header-filter-custom`. The row uses the header's theme variables, so every built-in theme and dark mode style it.
37
+ - `GridToolbarProps.forceFiltersOpen` / `onFiltersPanelClose` (the grid passes them so "Custom filter" can open the panel); `HeaderProps.headerFilters`, `filterModel`, `onFilterModelChange`, `onOpenFilterPanel`, and `focusedCell.headerFilter`.
38
+ - Guide `docs/features/header-filters.md`, demo page *Header Filters* (`/header-filters`), and a `header-filters` scenario in both package smoke fixtures.
39
+ - **Fill handle** — with `cellSelection` on, the range's bottom-right cell (or the only selected cell) carries a fill handle. Drag it down, up, right or left (one direction: the side the pointer's cell lies beyond, the larger pointer movement on a diagonal; auto-scrolls past the grid edge like a range drag; touch drags work, `touch-action: none`) to fill the new cells from the range: numbers and dates with an exactly equal step continue as a series (decimals rounded to the source's places; dates by month on the same day of the month, clamped to the month end, else by calendar day), anything else repeats the source pattern, one cell copies. Alt at release copies instead of continuing; Escape cancels. **Ctrl/Cmd+D** fills down from the range's top row and **Ctrl/Cmd+R** right from its left column (focus in the grid, no editor open; the browser's bookmark and reload keys are prevented only then). Fills write through `valueSetter` and `processRowUpdate` once per row as one batch edit and one undo step; non-editable, synthetic and span-covered cells are skipped (`'notEditable'`); the range becomes the source plus the filled cells. New props `disableFillHandle` and `onFill(result)` (`GridBatchEditResult` plus `direction`), new `GridColDef.fillValue(params)` to decide a column's filled values (a throw skips the cell, `'invalidValue'`), new types `GridFillDirection`, `GridFillResult`, `GridFillValueParams`. Public class `ogx__cell-fill-handle` (24 px hit area), variable `--ogx-fill-handle-color` (defaults to the range border; theme key `grid.fillHandleColor`, set by every built-in theme). Guide: `docs/features/fill-handle.md`. Demo page *Fill Handle* (`/fill-handle`).
40
+ - Package smoke suite: `fill` scenario (handle drag series, Ctrl/Cmd+D, undo) in both fixtures.
41
+
42
+ ### Changed
43
+
44
+ - `GridAiState`, `GridAiPart` and `GridAiValidationError` are declared in the grid's types so the `aiAssistant` prop can use them; `@opencorestack/opengridx/ai` re-exports them, so existing imports keep working.
45
+ - The `/ai` size budget in `check-bundle` is 8 kB gzipped (was 5 kB); the entry point is about 6.7 kB with the handler and tools.
46
+
47
+ ---
48
+
8
49
  ## [3.4.0] — 2026-10-08
9
50
 
10
51
  ### Added
@@ -36,6 +77,10 @@
36
77
  - `GridApi` has six new members (`pasteText`, `undo`, `redo`, `canUndo`, `canRedo`, `clearHistory`); an object written to satisfy `GridApi` by hand must add them. `useGridApiRef()`'s placeholder has no-op versions.
37
78
  - A committed single-cell edit is stored through the same path as batch edits; behaviour is unchanged.
38
79
 
80
+ ### Fixed
81
+
82
+ - **Cell range selection: a drag that auto-scrolled could end short of the pointer** (since 3.3.0). Dragging past the bottom of the grid scrolls it; on a slow machine the rows scrolled into view were not rendered yet when the pointer was hit-tested, so the range's head stayed on an earlier row (and the release could leave it there). When no cell is under the pointer during a drag, the head row is now worked out from the scroll position and the row heights.
83
+
39
84
  ## [3.3.0] — 2026-10-07
40
85
 
41
86
  ### Added
package/README.md CHANGED
@@ -68,7 +68,7 @@ Without a `height` prop the grid fills its container (`height: 100%`, or the fre
68
68
 
69
69
  ## 🚀 Key Features
70
70
 
71
- - **High-Performance Virtualization**: Row and column virtualization for 100,000+ rows. (Browsers cap an element's height, so a single scrolling grid reaches about 645,000 rows at the default 52 px row height; page or stream larger datasets.)
71
+ - **High-Performance Virtualization**: Row and column virtualization, [benchmarked](docs/performance.md#benchmarks) from 10k to 1M rows. (Browsers cap an element's height, so a single scrolling grid reaches 645,277 rows at the default 52 px row height in Chromium and Safari and about 344,000 in Firefox; page or stream larger datasets.)
72
72
  - **Advanced Layouts**: Native support for **Row & Column Spanning**, Grouping, and Tree Data.
73
73
  - **Data Orchestration**: 19 filter operators across string, number, date, boolean and select columns, AND/OR filter groups, multi-column sorting, and client or server pagination.
74
74
  - **Zero UI Dependencies**: 100% vanilla CSS (BEM) and pure React/TypeScript logic.
@@ -76,6 +76,7 @@ Without a `height` prop the grid fills its container (`height: 100%`, or the fre
76
76
  - **Fully Customizable**: Slots system for replacing any component (pagination, overlays, toolbar).
77
77
  - **Export Functionality**: Built-in CSV, Excel (HTML `.xls`, or real `.xlsx` with the optional ExcelJS peer), JSON, PDF (optional jsPDF peer) and Print export.
78
78
  - **Clipboard**: `Ctrl+C` / `Cmd+C` copies the selected rows' visible columns as TSV for Excel/Sheets.
79
+ - **Header Filters** (v3.5): `headerFilters` adds a filter row under the column headers (text, number, date and select controls with an operator menu) that edits the same `filterModel` as the filter panel and the quick filter.
79
80
  - **Cell Range Selection** (v3.3): drag, Shift+click or Shift+arrows select a rectangle of cells; copy it as TSV and see its count, sum and average in a status bar (`cellSelection`, `showCellSelectionStats`).
80
81
  - **AI Toolkit** (v3.4): `@opencorestack/opengridx/ai` turns your columns into a JSON Schema for your own model and validates its reply before the grid applies it (`getGridAiSchema`, `validateGridAiState`). No AI SDK, no row data sent by default.
81
82
  - **Accessibility**: WCAG 2.1 AA — full ARIA roles and keyboard navigation.
@@ -157,6 +158,8 @@ This means that **Cursor**, **GitHub Copilot**, **Windsurf**, and other AI agent
157
158
  | `sortable` | `boolean` | `true` | Enable/disable column sorting. |
158
159
  | `sortComparator` | `(v1, v2, p1, p2) => number` | — | Custom ascending comparator (sees nulls; negated for desc). v3.1.0+ |
159
160
  | `filterable` | `boolean` | `true` | Enable/disable column filtering. |
161
+ | `headerFilter` | `boolean` | `true` | `false` leaves the column's cell in the header filter row empty (v3.5). |
162
+ | `headerFilterOperator` | `GridFilterOperator` | by type | Operator a new header filter starts with (v3.5). |
160
163
  | `resizable` | `boolean` | `true` | Allow drag-resize; double-click the handle to auto-size to content (v3.1.0+). |
161
164
  | `hideable` | `boolean` | `true` | Allow hiding via the column panel. |
162
165
  | `pinnable` | `boolean` | `true` | Allow pinning via the UI. |
@@ -188,6 +191,8 @@ This means that **Cursor**, **GitHub Copilot**, **Windsurf**, and other AI agent
188
191
  | `onSortModelChange` | `(model: GridSortItem[]) => void` | Fires on sort change. |
189
192
  | `filterModel` | `GridFilterModel` | Controlled filter model. |
190
193
  | `onFilterModelChange` | `(model: GridFilterModel) => void` | Fires on filter change. |
194
+ | `headerFilters` | `boolean` | Filter row under the column headers, sharing `filterModel` (v3.5). |
195
+ | `headerFilterHeight` | `number` | Height of the header filter row. Default `40`. |
191
196
  | `multiSort` | `boolean` | Every header click adds to the sort instead of replacing it (Shift+click always does). |
192
197
 
193
198
  ### Columns
@@ -495,7 +500,8 @@ Built-in themes: `darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compac
495
500
  - **Tree-shaking Ready**: ES Module build — bundlers (Vite, Webpack) only include what you use.
496
501
  - **Zero UI Dependencies**: No MUI, Ant Design, or Radix. Pure React + vanilla CSS.
497
502
  - **Lazy Advanced Export**: ExcelJS and jsPDF are optional peer deps — not bundled, loaded only when you call `exportToExcelAdvanced` / `exportToPdf`.
498
- - **Efficient Rendering**: Row and column virtualization keeps the DOM small for 100k+ rows (needs a bounded grid height).
503
+ - **Efficient Rendering**: Row and column virtualization keeps the DOM small (needs a bounded grid height): about 19 rows at rest and under 90 while scrolling fast, at 10k or 1M rows.
504
+ - **[Benchmarks](docs/performance.md#benchmarks)** (`npm run bench`, Apple M1, Chromium, production build): at 100k rows the grid mounts in 89 ms, sorts in 102–160 ms, filters in 28–33 ms and groups with a sum in 87 ms; at 1M rows mount takes 0.7 s and a sort 1.1–1.3 s. Scrolling missed no frames at any size in Chromium. Past the browser's height cap (645,277 rows at 52 px in Chromium and Safari, about 344,000 in Firefox) rows cannot be scrolled to; page or stream larger datasets.
499
505
 
500
506
  ---
501
507
 
@@ -510,7 +516,7 @@ Full documentation at 👉 **[opencorestack.github.io/OpenGridX](https://opencor
510
516
  - **[Column Visibility](docs/components/column-visibility.md)** — Column show/hide panel
511
517
 
512
518
  ### 🚀 Features
513
- - **[Virtualization](docs/features/virtualization.md)** — 60fps rendering for 100k+ rows
519
+ - **[Virtualization](docs/features/virtualization.md)** — row and column virtualization; [measured](docs/performance.md#benchmarks) from 10k to 1M rows
514
520
  - **[Filtering & Search](docs/features/filtering.md)** — Quick filters and advanced operators
515
521
  - **[Sorting & Pagination](docs/features/sorting-pagination.md)** — Multi-column sorting
516
522
  - **[Editing & Reordering](docs/features/editing-reordering.md)** — Inline edits and DnD
@@ -523,6 +529,7 @@ Full documentation at 👉 **[opencorestack.github.io/OpenGridX](https://opencor
523
529
  - **[Clipboard](docs/features/clipboard.md)** — Copy rows as TSV, paste from Excel / Sheets, clear a range with Delete
524
530
  - **[Cell Range Selection](docs/features/cell-selection.md)** — Select, total and copy cell ranges
525
531
  - **[Undo & Redo](docs/features/undo-redo.md)** — Undo and redo edits, pastes and range clears
532
+ - **[Fill Handle](docs/features/fill-handle.md)** — Drag the range corner to fill a series or copy; Ctrl+D / Ctrl+R
526
533
  - **[AI Toolkit](docs/features/ai-toolkit.md)** — Schema and validator for driving the grid from your own model
527
534
  - **[Keyboard & Accessibility](docs/features/keyboard-navigation.md)** — Keys, focus and ARIA
528
535
  - **[Data Source](docs/features/data-source.md)** — Server-side integration
package/dist/ai.cjs CHANGED
@@ -1 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const z=/^(\d{4})-(\d{2})-(\d{2})$/;function T(e){if(e==null)return null;let t;if(e instanceof Date)t=e;else if(typeof e=="number")t=new Date(e);else if(typeof e=="string"){const n=e.trim();if(n==="")return null;const i=z.exec(n);if(i){const r=Number(i[1]),o=Number(i[2])-1,l=Number(i[3]);if(t=new Date(r,o,l),t.getMonth()!==o||t.getDate()!==l)return null}else t=new Date(n)}else return null;return Number.isNaN(t.getTime())?null:t}function Q(e){const t=T(e);if(!t)return"";const n=String(t.getFullYear()).padStart(4,"0"),i=String(t.getMonth()+1).padStart(2,"0"),r=String(t.getDate()).padStart(2,"0");return`${n}-${i}-${r}`}function Z(e){switch(e){case"number":return["=","!=",">",">=","<","<=","isEmpty","isNotEmpty"];case"date":return["is","not","after","onOrAfter","before","onOrBefore","isEmpty","isNotEmpty"];case"boolean":return["is"];case"singleSelect":return["isAnyOf","is","not"];default:return["contains","equals","startsWith","endsWith","isEmpty","isNotEmpty"]}}const x=["sum","avg","count","min","max","unique"],I=["sum","avg","count","min","max"],G=["isEmpty","isNotEmpty"],E=3,j=["filter","sort","grouping","aggregation","pivot","columnVisibility"],w={filter:"filterModel",sort:"sortModel",grouping:"rowGroupingModel",aggregation:"aggregationModel",pivot:"pivotModel",columnVisibility:"columnVisibilityModel"};function v(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function $(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function R(e){const t=Array.isArray(e?.include)?e.include:j,n=Array.isArray(e?.exclude)?e.exclude:[];return j.filter(i=>t.includes(i)&&!n.includes(i))}function q(e){if(!Array.isArray(e))return[];const t=new Set,n=[];for(const i of e){if(!$(i))continue;const r=i.field;typeof r!="string"||r===""||r.startsWith("__")||i.isSpacer===!0||t.has(r)||(t.add(r),n.push(i))}return n}function W(e){return Z(e.type)}const C=e=>e.filterable!==!1,U=e=>e.sortable!==!1,A=e=>e.groupable!==!1,B=e=>e.hideable!==!1;function b(e,t=x){if(e.aggregable===!1)return[];const n=e.type==="number"||e.aggregable===!0?t:t.filter(r=>r==="count"),i=e.availableAggregationFunctions;return Array.isArray(i)?n.filter(r=>i.includes(r)):[...n]}function ee(e){const t=[];if(!Array.isArray(e.valueOptions))return t;for(const n of e.valueOptions){const i=n!==null&&typeof n=="object"?n.value:n;(typeof i=="string"||typeof i=="number"&&Number.isFinite(i))&&!t.includes(i)&&t.push(i)}return t}const te="OpenGridX grid state",re=10;function O(e){const t=typeof e.headerName=="string"&&e.headerName!==""?e.headerName:e.field,n=typeof e.description=="string"&&e.description!==""?` (${e.description})`:"";return`${t}${n}`}function m(e,t,n){return n.descriptions?{...e,description:t}:e}function ne(e,t){return!t.examples||!Array.isArray(e.aiExamples)?[]:e.aiExamples.filter(n=>typeof n=="string"||typeof n=="boolean"||typeof n=="number"&&Number.isFinite(n)).slice(0,re)}function ie(e,t){let n;switch(e.type){case"number":n={type:"number"};break;case"date":n={type:"string",format:"date"};break;case"boolean":n={type:"boolean"};break;case"singleSelect":{const r=ee(e),o=r.length>0?{enum:r}:{type:"string"};n={anyOf:[o,{type:"array",items:o}]};break}default:n={type:"string"}}const i=ne(e,t);return i.length>0?{...n,examples:i}:n}function oe(e,t){const n=W(e),i=n.filter(l=>G.includes(l)),r=e.type==="singleSelect"?"; isAnyOf takes an array":"",o=i.length>0?`${i.join(" and ")} take no value${r}`:r.slice(2);return m({type:"object",properties:{field:{const:e.field},operator:{enum:n},value:ie(e,t)},required:["field","operator"],additionalProperties:!1},o?`${O(e)}: ${o}`:O(e),t)}function se(e,t,n){const i=e.filter(C),r={},o={enum:["and","or"]};if(i.length>0){n.filterItem={oneOf:i.map(l=>oe(l,t))};for(let l=E;l>=1;l--){const s=l===E?{$ref:"#/$defs/filterItem"}:{anyOf:[{$ref:"#/$defs/filterItem"},{$ref:`#/$defs/filterGroup${l+1}`}]};n[`filterGroup${l}`]={type:"object",properties:{logicOperator:o,items:{type:"array",items:s}},required:["logicOperator","items"],additionalProperties:!1}}r.items={type:"array",items:{anyOf:[{$ref:"#/$defs/filterItem"},{$ref:"#/$defs/filterGroup1"}]}},r.logicOperator=o}return r.quickFilterValues=m({type:"array",items:{type:"string"}},"Words a row must contain, in any column",t),{type:"object",properties:r,additionalProperties:!1}}function _(e){return{enum:e.map(t=>t.field)}}function le(e,t,n,i){switch(e){case"filter":return se(t,n,i);case"sort":{const r=t.filter(U);return r.length===0?null:m({type:"array",items:{type:"object",properties:{field:_(r),sort:{enum:["asc","desc"]}},required:["field","sort"],additionalProperties:!1}},"Most significant first",n)}case"grouping":{const r=t.filter(A);return r.length===0?null:m({type:"array",items:_(r),uniqueItems:!0},"Outermost first",n)}case"aggregation":{const r={};for(const o of t){const l=b(o);l.length>0&&(r[o.field]=m({enum:l},O(o),n))}return Object.keys(r).length===0?null:{type:"object",properties:r,additionalProperties:!1}}case"pivot":{const r=t.filter(A),o=t.map(s=>({col:s,fns:b(s,I)})).filter(s=>s.fns.length>0).map(({col:s,fns:u})=>m({type:"object",properties:{field:{const:s.field},aggFn:{enum:u}},required:["field","aggFn"],additionalProperties:!1},O(s),n));if(r.length===0||o.length===0)return null;const l={type:"array",items:_(r),uniqueItems:!0};return{type:"object",properties:{rowFields:l,columnFields:l,valueFields:{type:"array",items:{oneOf:o}}},required:["rowFields","columnFields","valueFields"],additionalProperties:!1}}case"columnVisibility":{const r={};for(const o of t.filter(B))r[o.field]=m({type:"boolean"},O(o),n);return Object.keys(r).length===0?null:m({type:"object",properties:r,additionalProperties:!1},"false hides a column",n)}}}function ue(e,t={}){const n={descriptions:t.descriptions!==!1,examples:t.examples!==!1},i=q(e),r={},o={};for(const s of R(t)){const u=le(s,i,n,r);u&&(o[w[s]]=u)}const l={$schema:"https://json-schema.org/draft/2020-12/schema",schemaVersion:1,title:te,type:"object",properties:o,additionalProperties:!1};return n.descriptions&&(l.description="Grid state to apply. Include only the parts the user asked to change."),Object.keys(r).length>0&&(l.$defs=r),l}const y={ok:!1},d=e=>({ok:!0,value:e}),fe=/^\d+(?:\.\d*)?(?:e[+-]?\d+)?$/i,ae=/^\.\d+(?:e[+-]?\d+)?$/i,ce=/^\d{1,3}(?:,\d{3})+(?:\.\d*)?$/,de=/[\p{Sc}\s]/gu;function pe(e){let t=e.replace(de,"");if(t==="")return e.trim()===""?d(null):y;let n=!1;t.startsWith("(")&&t.endsWith(")")&&(n=!0,t=t.slice(1,-1)),(t.startsWith("-")||t.startsWith("+")||t.startsWith("\u2212"))&&(t[0]!=="+"&&(n=!n),t=t.slice(1));let i=!1;if(t.endsWith("%")&&(i=!0,t=t.slice(0,-1)),ce.test(t))t=t.split(",").join("");else if(!fe.test(t)&&!ae.test(t))return y;let r=Number(t);return Number.isFinite(r)?(i&&(r/=100),n&&(r=-r),d(r)):y}function ge(e,t){const n=e.trim();if(n==="")return d(null);const i=T(n);return i?d(i):y}const me=new Set(["true","yes","1"]),ye=new Set(["false","no","0"]);function be(e,t){const n=e.trim().toLowerCase();return n===""?d(null):me.has(n)?d(!0):ye.has(n)?d(!1):y}function $e(e,t){const n=e.trim();if(n==="")return d(null);if(!t||t.length===0)return y;const i=n.toLowerCase();for(const r of t){const o=typeof r=="object"?r.value:r;if(String(o).toLowerCase()===i)return d(o)}for(const r of t)if(typeof r=="object"&&r.label.toLowerCase()===i)return d(r.value);return y}const a=e=>({ok:!0,value:e}),p={ok:!1,reason:"invalidValue"},N=e=>e.ok?a(e.value):p;function k(e){return e==null||typeof e=="string"&&e.trim()===""}function he(e){return e==null?a(""):typeof e=="string"?a(e):typeof e=="number"||typeof e=="boolean"?a(String(e)):p}function Se(e){return k(e)?a(null):typeof e=="number"?Number.isFinite(e)?a(e):p:typeof e!="string"?p:N(pe(e))}function Oe(e){return k(e)?a(null):e instanceof Date?Number.isNaN(e.getTime())?p:a(e):typeof e=="number"?Number.isFinite(e)?a(new Date(e)):p:typeof e!="string"?p:N(ge(e))}function X(e){return k(e)?a(null):typeof e=="boolean"?a(e):typeof e!="string"&&typeof e!="number"?p:N(be(String(e)))}function ve(e,t){if(k(e))return a(null);if(typeof e!="string"&&typeof e!="number"&&typeof e!="boolean")return p;const n=t.valueOptions;return!n||n.length===0?a(e):N($e(String(e),n))}function Ae(e,t){switch(t.type){case"number":return Se(e);case"date":return Oe(e);case"boolean":return X(e);case"singleSelect":return ve(e,t);default:return he(e)}}const P=200,Fe=100;function f(e,t,n){return e.errors.length<Fe&&e.errors.push({path:t,message:n}),null}function c(e){return typeof e=="string"?JSON.stringify(e.slice(0,60)):e===null||typeof e!="object"?String(e):Array.isArray(e)?"a list":"an object"}function H(e){return typeof e=="string"?e.trim().toLowerCase():null}function D(e){return Object.keys(e).filter(t=>t!=="__proto__")}function h(e,t,n,i,r){const o=typeof t=="string"?e.byField.get(t):void 0;return o?i(o)?o:f(e,n,`Field ${c(t)} cannot be ${r}`):f(e,n,`Unknown field ${c(t)}`)}function S(e,t,n){if(t==null)return[];const i=Array.isArray(t)?t:[t];return i.length>P&&f(e,n,`Only the first ${P} entries are read`),i.slice(0,P)}function F(e,t,n,i){const r=H(t);return i.find(o=>o.toLowerCase()===r)??f(e,n,`${c(t)} is not one of ${i.join(", ")}`)}function L(e,t,n,i){const r=Ae(t,n);if(!r.ok)return f(e,i,`${c(t)} is not a valid ${n.type??"string"} value`);const o=r.value instanceof Date?Q(r.value):r.value;return o==null||o===""?f(e,i,"Missing value"):{v:o}}function Ee(e,t,n){const i=h(e,t.field,`${n}.field`,C,"filtered");if(!i)return null;const r=F(e,t.operator,`${n}.operator`,W(i));if(r===null)return null;const o={field:i.field,operator:r};if(G.includes(o.operator))return o;const l=v(t,"value")?t.value:void 0;if(o.operator==="isAnyOf"){const u=[];return S(e,l,`${n}.value`).forEach((g,K)=>{const M=L(e,g,i,`${n}.value[${K}]`);M&&!u.includes(M.v)&&u.push(M.v)}),u.length===0?f(e,`${n}.value`,"No valid values"):(o.value=u,o)}if(Array.isArray(l))return f(e,`${n}.value`,"Expected one value, not a list");const s=L(e,l,i,`${n}.value`);return s?(o.value=s.v,o):null}function J(e,t,n){return t===void 0?null:F(e,t,n,["and","or"])}function Y(e,t,n,i){const r=[];return S(e,t,n).forEach((o,l)=>{const s=`${n}[${l}]`;if(!$(o))f(e,s,`Expected a filter, got ${c(o)}`);else if(v(o,"items")||v(o,"logicOperator")){if(i>=E){f(e,s,`Groups nest at most ${E} levels`);return}const u=Y(e,o.items,`${s}.items`,i+1);u.length===0?f(e,s,"Empty group removed"):r.push({logicOperator:J(e,o.logicOperator,`${s}.logicOperator`)??"and",items:u})}else{const u=Ee(e,o,s);u&&r.push(u)}}),r}function Ne(e,t,n){const i={};v(t,"items")&&(i.items=Y(e,t.items,`${n}.items`,0));const r=J(e,t.logicOperator,`${n}.logicOperator`);return r&&(i.logicOperator=r),v(t,"quickFilterValues")&&(i.quickFilterValues=[],S(e,t.quickFilterValues,`${n}.quickFilterValues`).forEach((o,l)=>{const s=typeof o=="string"||typeof o=="number"?String(o).trim():null;s===null?f(e,`${n}.quickFilterValues[${l}]`,`Expected text, got ${c(o)}`):s!==""&&i.quickFilterValues?.push(s)})),i}function ke(e,t,n){const i=[];return S(e,t,n).forEach((r,o)=>{const l=`${n}[${o}]`;if(!$(r))return f(e,l,`Expected { field, sort }, got ${c(r)}`);const s=h(e,r.field,`${l}.field`,U,"sorted"),u=s&&F(e,r.sort,`${l}.sort`,["asc","desc"]);!s||!u||(i.some(g=>g.field===s.field)?f(e,l,`${c(s.field)} is sorted twice`):i.push({field:s.field,sort:u}))}),i}function V(e,t,n,i,r){const o=[];return S(e,t,n).forEach((l,s)=>{const u=h(e,l,`${n}[${s}]`,i,r);u&&!o.includes(u.field)&&o.push(u.field)}),o}const Me=e=>b(e).length>0,_e=e=>b(e,I).length>0;function Pe(e,t,n){const i={};for(const r of D(t)){if(H(t[r])==="none")continue;const o=h(e,r,`${n}.${r}`,Me,"aggregated"),l=o&&F(e,t[r],`${n}.${r}`,b(o));o&&l&&(i[o.field]=l)}return i}function je(e,t,n){const i=[];return S(e,t.valueFields,`${n}.valueFields`).forEach((r,o)=>{const l=`${n}.valueFields[${o}]`;if(!$(r))return f(e,l,`Expected { field, aggFn }, got ${c(r)}`);const s=h(e,r.field,`${l}.field`,_e,"a pivot value"),u=s&&F(e,r.aggFn,`${l}.aggFn`,b(s,I));s&&u&&!i.some(g=>g.field===s.field&&g.aggFn===u)&&i.push({field:s.field,aggFn:u})}),{rowFields:V(e,t.rowFields,`${n}.rowFields`,A,"a pivot label"),columnFields:V(e,t.columnFields,`${n}.columnFields`,A,"a pivot label"),valueFields:i}}function Ve(e,t,n){const i={};for(const r of D(t)){const o=h(e,r,`${n}.${r}`,B,"hidden"),l=X(t[r]);o&&(l.ok&&l.value!==null?i[o.field]=l.value:f(e,`${n}.${r}`,`Expected true or false, got ${c(t[r])}`))}return i}function Ie(e,t,n,i,r){t==="sort"?r.sortModel=ke(e,n,i):t==="grouping"?r.rowGroupingModel=V(e,n,i,A,"grouped"):$(n)?t==="filter"?r.filterModel=Ne(e,n,i):t==="aggregation"?r.aggregationModel=Pe(e,n,i):t==="pivot"?r.pivotModel=je(e,n,i):r.columnVisibilityModel=Ve(e,n,i):f(e,i,`Expected an object, got ${c(n)}`)}function De(e,t,n={}){const i={errors:[],byField:new Map},r={};try{for(const s of q(t))i.byField.set(s.field,s);let o=e;if(typeof e=="string")try{o=JSON.parse(e)}catch{return f(i,"","Not valid JSON"),{state:r,errors:i.errors}}if(!$(o))return f(i,"",`Expected an object, got ${c(o)}`),{state:r,errors:i.errors};const l=R(n);for(const s of D(o)){const u=j.find(g=>w[g]===s);s==="schemaVersion"?o[s]!==1&&f(i,s,`Unsupported schemaVersion ${c(o[s])}`):u?l.includes(u)?Ie(i,u,o[s],s,r):f(i,s,"Part not allowed"):f(i,s,"Unknown part")}}catch{return f(i,"","Could not read the input"),{state:{},errors:i.errors}}return{state:r,errors:i.errors}}exports.getGridAiSchema=ue,exports.validateGridAiState=De;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const Q=new Set;function de(e,t,r){process.env.NODE_ENV==="production"||Q.has(e)||(Q.add(e),console.warn(`[OpenGridX] ${t}.`,r))}function pe(e,t,r){const o=e[t];if(!r?.valueGetter)return{value:o};try{return{value:r.valueGetter({row:e,field:t,value:o})}}catch(n){return{value:void 0,error:n??new Error("valueGetter failed")}}}function ge(e,t,r){const{value:o,error:n}=pe(e,t,r);return n!==void 0&&de(`valueGetter:${t}`,`valueGetter for column "${t}" threw; the value is read as undefined`,n),o}const me=/^(\d{4})-(\d{2})-(\d{2})$/;function ee(e){if(e==null)return null;let t;if(e instanceof Date)t=e;else if(typeof e=="number")t=new Date(e);else if(typeof e=="string"){const r=e.trim();if(r==="")return null;const o=me.exec(r);if(o){const n=Number(o[1]),i=Number(o[2])-1,s=Number(o[3]);if(t=new Date(n,i,s),t.getMonth()!==i||t.getDate()!==s)return null}else t=new Date(r)}else return null;return Number.isNaN(t.getTime())?null:t}function ye(e){const t=ee(e);if(!t)return"";const r=String(t.getFullYear()).padStart(4,"0"),o=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0");return`${r}-${o}-${n}`}function be(e){switch(e){case"number":return["=","!=",">",">=","<","<=","isEmpty","isNotEmpty"];case"date":return["is","not","after","onOrAfter","before","onOrBefore","isEmpty","isNotEmpty"];case"boolean":return["is"];case"singleSelect":return["isAnyOf","is","not"];default:return["contains","equals","startsWith","endsWith","isEmpty","isNotEmpty"]}}const he=["sum","avg","count","min","max","unique"],B=["sum","avg","count","min","max"],te=["isEmpty","isNotEmpty"],D=3,U=["filter","sort","grouping","aggregation","pivot","columnVisibility"],H={filter:"filterModel",sort:"sortModel",grouping:"rowGroupingModel",aggregation:"aggregationModel",pivot:"pivotModel",columnVisibility:"columnVisibilityModel"};function S(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function $(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function re(e){const t=Array.isArray(e?.include)?e.include:U,r=Array.isArray(e?.exclude)?e.exclude:[];return U.filter(o=>t.includes(o)&&!r.includes(o))}function J(e){if(!Array.isArray(e))return[];const t=new Set,r=[];for(const o of e){if(!$(o))continue;const n=o.field;typeof n!="string"||n===""||n.startsWith("__")||o.isSpacer===!0||t.has(n)||(t.add(n),r.push(o))}return r}function ne(e){return be(e.type)}const ie=e=>e.filterable!==!1,oe=e=>e.sortable!==!1,T=e=>e.groupable!==!1,se=e=>e.hideable!==!1;function F(e,t=he){if(e.aggregable===!1)return[];const r=e.type==="number"||e.aggregable===!0?t:t.filter(n=>n==="count"),o=e.availableAggregationFunctions;return Array.isArray(o)?r.filter(n=>o.includes(n)):[...r]}function $e(e){const t=[];if(!Array.isArray(e.valueOptions))return t;for(const r of e.valueOptions){const o=r!==null&&typeof r=="object"?r.value:r;(typeof o=="string"||typeof o=="number"&&Number.isFinite(o))&&!t.includes(o)&&t.push(o)}return t}const Ae="OpenGridX grid state",Se=10;function j(e){const t=typeof e.headerName=="string"&&e.headerName!==""?e.headerName:e.field,r=typeof e.description=="string"&&e.description!==""?` (${e.description})`:"";return`${t}${r}`}function O(e,t,r){return r.descriptions?{...e,description:t}:e}function Oe(e,t){return!t.examples||!Array.isArray(e.aiExamples)?[]:e.aiExamples.filter(r=>typeof r=="string"||typeof r=="boolean"||typeof r=="number"&&Number.isFinite(r)).slice(0,Se)}function ve(e,t){let r;switch(e.type){case"number":r={type:"number"};break;case"date":r={type:"string",format:"date"};break;case"boolean":r={type:"boolean"};break;case"singleSelect":{const n=$e(e),i=n.length>0?{enum:n}:{type:"string"};r={anyOf:[i,{type:"array",items:i}]};break}default:r={type:"string"}}const o=Oe(e,t);return o.length>0?{...r,examples:o}:r}function Me(e,t){const r=ne(e),o=r.filter(s=>te.includes(s)),n=e.type==="singleSelect"?"; isAnyOf takes an array":"",i=o.length>0?`${o.join(" and ")} take no value${n}`:n.slice(2);return O({type:"object",properties:{field:{const:e.field},operator:{enum:r},value:ve(e,t)},required:["field","operator"],additionalProperties:!1},i?`${j(e)}: ${i}`:j(e),t)}function Ee(e,t,r){const o=e.filter(ie),n={},i={enum:["and","or"]};if(o.length>0){r.filterItem={oneOf:o.map(s=>Me(s,t))};for(let s=D;s>=1;s--){const l=s===D?{$ref:"#/$defs/filterItem"}:{anyOf:[{$ref:"#/$defs/filterItem"},{$ref:`#/$defs/filterGroup${s+1}`}]};r[`filterGroup${s}`]={type:"object",properties:{logicOperator:i,items:{type:"array",items:l}},required:["logicOperator","items"],additionalProperties:!1}}n.items={type:"array",items:{anyOf:[{$ref:"#/$defs/filterItem"},{$ref:"#/$defs/filterGroup1"}]}},n.logicOperator=i}return n.quickFilterValues=O({type:"array",items:{type:"string"}},"Words a row must contain, in any column",t),{type:"object",properties:n,additionalProperties:!1}}function q(e){return{enum:e.map(t=>t.field)}}function we(e,t,r,o){switch(e){case"filter":return Ee(t,r,o);case"sort":{const n=t.filter(oe);return n.length===0?null:O({type:"array",items:{type:"object",properties:{field:q(n),sort:{enum:["asc","desc"]}},required:["field","sort"],additionalProperties:!1}},"Most significant first",r)}case"grouping":{const n=t.filter(T);return n.length===0?null:O({type:"array",items:q(n),uniqueItems:!0},"Outermost first",r)}case"aggregation":{const n={};for(const i of t){const s=F(i);s.length>0&&(n[i.field]=O({enum:s},j(i),r))}return Object.keys(n).length===0?null:{type:"object",properties:n,additionalProperties:!1}}case"pivot":{const n=t.filter(T),i=t.map(l=>({col:l,fns:F(l,B)})).filter(l=>l.fns.length>0).map(({col:l,fns:a})=>O({type:"object",properties:{field:{const:l.field},aggFn:{enum:a}},required:["field","aggFn"],additionalProperties:!1},j(l),r));if(n.length===0||i.length===0)return null;const s={type:"array",items:q(n),uniqueItems:!0};return{type:"object",properties:{rowFields:s,columnFields:s,valueFields:{type:"array",items:{oneOf:i}}},required:["rowFields","columnFields","valueFields"],additionalProperties:!1}}case"columnVisibility":{const n={};for(const i of t.filter(se))n[i.field]=O({type:"boolean"},j(i),r);return Object.keys(n).length===0?null:O({type:"object",properties:n,additionalProperties:!1},"false hides a column",r)}}}function Y(e,t={}){const r={descriptions:t.descriptions!==!1,examples:t.examples!==!1},o=J(e),n={},i={};for(const l of re(t)){const a=we(l,o,r,n);a&&(i[H[l]]=a)}const s={$schema:"https://json-schema.org/draft/2020-12/schema",schemaVersion:1,title:Ae,type:"object",properties:i,additionalProperties:!1};return r.descriptions&&(s.description="Grid state to apply. Include only the parts the user asked to change."),Object.keys(n).length>0&&(s.$defs=n),s}const Fe=Symbol.for("opengridx.ai.validated");function I(e){return Object.defineProperty(e,Fe,{value:!0,enumerable:!1}),e}const M={ok:!1},h=e=>({ok:!0,value:e}),Ne=/^\d+(?:\.\d*)?(?:e[+-]?\d+)?$/i,_e=/^\.\d+(?:e[+-]?\d+)?$/i,ke=/^\d{1,3}(?:,\d{3})+(?:\.\d*)?$/,je=/[\p{Sc}\s]/gu;function Te(e){let t=e.replace(je,"");if(t==="")return e.trim()===""?h(null):M;let r=!1;t.startsWith("(")&&t.endsWith(")")&&(r=!0,t=t.slice(1,-1)),(t.startsWith("-")||t.startsWith("+")||t.startsWith("\u2212"))&&(t[0]!=="+"&&(r=!r),t=t.slice(1));let o=!1;if(t.endsWith("%")&&(o=!0,t=t.slice(0,-1)),ke.test(t))t=t.split(",").join("");else if(!Ne.test(t)&&!_e.test(t))return M;let n=Number(t);return Number.isFinite(n)?(o&&(n/=100),r&&(n=-n),h(n)):M}function Ge(e,t){const r=e.trim();if(r==="")return h(null);const o=ee(r);return o?h(o):M}const Ve=new Set(["true","yes","1"]),Pe=new Set(["false","no","0"]);function Re(e,t){const r=e.trim().toLowerCase();return r===""?h(null):Ve.has(r)?h(!0):Pe.has(r)?h(!1):M}function De(e,t){const r=e.trim();if(r==="")return h(null);if(!t||t.length===0)return M;const o=r.toLowerCase();for(const n of t){const i=typeof n=="object"?n.value:n;if(String(i).toLowerCase()===o)return h(i)}for(const n of t)if(typeof n=="object"&&n.label.toLowerCase()===o)return h(n.value);return M}const d=e=>({ok:!0,value:e}),A={ok:!1,reason:"invalidValue"},L=e=>e.ok?d(e.value):A;function C(e){return e==null||typeof e=="string"&&e.trim()===""}function Ie(e){return e==null?d(""):typeof e=="string"?d(e):typeof e=="number"||typeof e=="boolean"?d(String(e)):A}function Le(e){return C(e)?d(null):typeof e=="number"?Number.isFinite(e)?d(e):A:typeof e!="string"?A:L(Te(e))}function Ce(e){return C(e)?d(null):e instanceof Date?Number.isNaN(e.getTime())?A:d(e):typeof e=="number"?Number.isFinite(e)?d(new Date(e)):A:typeof e!="string"?A:L(Ge(e))}function le(e){return C(e)?d(null):typeof e=="boolean"?d(e):typeof e!="string"&&typeof e!="number"?A:L(Re(String(e)))}function qe(e,t){if(C(e))return d(null);if(typeof e!="string"&&typeof e!="number"&&typeof e!="boolean")return A;const r=t.valueOptions;return!r||r.length===0?d(e):L(De(String(e),r))}function We(e,t){switch(t.type){case"number":return Le(e);case"date":return Ce(e);case"boolean":return le(e);case"singleSelect":return qe(e,t);default:return Ie(e)}}const W=200,Ue=100;function u(e,t,r){return e.errors.length<Ue&&e.errors.push({path:t,message:r}),null}function p(e){return typeof e=="string"?JSON.stringify(e.slice(0,60)):e===null||typeof e!="object"?String(e):Array.isArray(e)?"a list":"an object"}function ae(e){return typeof e=="string"?e.trim().toLowerCase():null}function K(e){return Object.keys(e).filter(t=>t!=="__proto__")}function N(e,t,r,o,n){const i=typeof t=="string"?e.byField.get(t):void 0;return i?o(i)?i:u(e,r,`Field ${p(t)} cannot be ${n}`):u(e,r,`Unknown field ${p(t)}`)}function _(e,t,r){if(t==null)return[];const o=Array.isArray(t)?t:[t];return o.length>W&&u(e,r,`Only the first ${W} entries are read`),o.slice(0,W)}function G(e,t,r,o){const n=ae(t);return o.find(i=>i.toLowerCase()===n)??u(e,r,`${p(t)} is not one of ${o.join(", ")}`)}function Z(e,t,r,o){const n=We(t,r);if(!n.ok)return u(e,o,`${p(t)} is not a valid ${r.type??"string"} value`);const i=n.value instanceof Date?ye(n.value):n.value;return i==null||i===""?u(e,o,"Missing value"):{v:i}}function Xe(e,t,r){const o=N(e,t.field,`${r}.field`,ie,"filtered");if(!o)return null;const n=G(e,t.operator,`${r}.operator`,ne(o));if(n===null)return null;const i={field:o.field,operator:n};if(te.includes(i.operator))return i;const s=S(t,"value")?t.value:void 0;if(i.operator==="isAnyOf"){const a=[];return _(e,s,`${r}.value`).forEach((c,v)=>{const g=Z(e,c,o,`${r}.value[${v}]`);g&&!a.includes(g.v)&&a.push(g.v)}),a.length===0?u(e,`${r}.value`,"No valid values"):(i.value=a,i)}if(Array.isArray(s))return u(e,`${r}.value`,"Expected one value, not a list");const l=Z(e,s,o,`${r}.value`);return l?(i.value=l.v,i):null}function ue(e,t,r){return t===void 0?null:G(e,t,r,["and","or"])}function fe(e,t,r,o){const n=[];return _(e,t,r).forEach((i,s)=>{const l=`${r}[${s}]`;if(!$(i))u(e,l,`Expected a filter, got ${p(i)}`);else if(S(i,"items")||S(i,"logicOperator")){if(o>=D){u(e,l,`Groups nest at most ${D} levels`);return}const a=fe(e,i.items,`${l}.items`,o+1);a.length===0?u(e,l,"Empty group removed"):n.push({logicOperator:ue(e,i.logicOperator,`${l}.logicOperator`)??"and",items:a})}else{const a=Xe(e,i,l);a&&n.push(a)}}),n}function Be(e,t,r){const o={};S(t,"items")&&(o.items=fe(e,t.items,`${r}.items`,0));const n=ue(e,t.logicOperator,`${r}.logicOperator`);return n&&(o.logicOperator=n),S(t,"quickFilterValues")&&(o.quickFilterValues=[],_(e,t.quickFilterValues,`${r}.quickFilterValues`).forEach((i,s)=>{const l=typeof i=="string"||typeof i=="number"?String(i).trim():null;l===null?u(e,`${r}.quickFilterValues[${s}]`,`Expected text, got ${p(i)}`):l!==""&&o.quickFilterValues?.push(l)})),o}function He(e,t,r){const o=[];return _(e,t,r).forEach((n,i)=>{const s=`${r}[${i}]`;if(!$(n))return u(e,s,`Expected { field, sort }, got ${p(n)}`);const l=N(e,n.field,`${s}.field`,oe,"sorted"),a=l&&G(e,n.sort,`${s}.sort`,["asc","desc"]);!l||!a||(o.some(c=>c.field===l.field)?u(e,s,`${p(l.field)} is sorted twice`):o.push({field:l.field,sort:a}))}),o}function X(e,t,r,o,n){const i=[];return _(e,t,r).forEach((s,l)=>{const a=N(e,s,`${r}[${l}]`,o,n);a&&!i.includes(a.field)&&i.push(a.field)}),i}const Je=e=>F(e).length>0,Ye=e=>F(e,B).length>0;function Ke(e,t,r){const o={};for(const n of K(t)){if(ae(t[n])==="none")continue;const i=N(e,n,`${r}.${n}`,Je,"aggregated"),s=i&&G(e,t[n],`${r}.${n}`,F(i));i&&s&&(o[i.field]=s)}return o}function ze(e,t,r){const o=[];return _(e,t.valueFields,`${r}.valueFields`).forEach((n,i)=>{const s=`${r}.valueFields[${i}]`;if(!$(n))return u(e,s,`Expected { field, aggFn }, got ${p(n)}`);const l=N(e,n.field,`${s}.field`,Ye,"a pivot value"),a=l&&G(e,n.aggFn,`${s}.aggFn`,F(l,B));l&&a&&!o.some(c=>c.field===l.field&&c.aggFn===a)&&o.push({field:l.field,aggFn:a})}),{rowFields:X(e,t.rowFields,`${r}.rowFields`,T,"a pivot label"),columnFields:X(e,t.columnFields,`${r}.columnFields`,T,"a pivot label"),valueFields:o}}function Qe(e,t,r){const o={};for(const n of K(t)){const i=N(e,n,`${r}.${n}`,se,"hidden"),s=le(t[n]);i&&(s.ok&&s.value!==null?o[i.field]=s.value:u(e,`${r}.${n}`,`Expected true or false, got ${p(t[n])}`))}return o}function Ze(e,t,r,o,n){t==="sort"?n.sortModel=He(e,r,o):t==="grouping"?n.rowGroupingModel=X(e,r,o,T,"grouped"):$(r)?t==="filter"?n.filterModel=Be(e,r,o):t==="aggregation"?n.aggregationModel=Ke(e,r,o):t==="pivot"?n.pivotModel=ze(e,r,o):n.columnVisibilityModel=Qe(e,r,o):u(e,o,`Expected an object, got ${p(r)}`)}function z(e,t,r={}){const o=xe(e,t,r);return I(o.state),I(o)}function xe(e,t,r){const o={errors:[],byField:new Map},n={};try{for(const l of J(t))o.byField.set(l.field,l);let i=e;if(typeof e=="string")try{i=JSON.parse(e)}catch{return u(o,"","Not valid JSON"),{state:n,errors:o.errors}}if(!$(i))return u(o,"",`Expected an object, got ${p(i)}`),{state:n,errors:o.errors};const s=re(r);for(const l of K(i)){const a=U.find(c=>H[c]===l);l==="schemaVersion"?i[l]!==1&&u(o,l,`Unsupported schemaVersion ${p(i[l])}`):a?s.includes(a)?Ze(o,a,i[l],l,n):u(o,l,"Part not allowed"):u(o,l,"Unknown part")}}catch{return u(o,"","Could not read the input"),{state:{},errors:o.errors}}return{state:n,errors:o.errors}}const k="```";function V(){return typeof DOMException=="function"?new DOMException("The operation was aborted.","AbortError"):Object.assign(new Error("The operation was aborted."),{name:"AbortError"})}function et(e){const t=e.trim();if(t.startsWith("{"))return{json:t,message:""};const r=e.indexOf(k),o=r<0?-1:e.indexOf(`
2
+ `,r+k.length),n=o<0?-1:e.indexOf(k,o+1),i=n<0?"":e.slice(r+k.length,o).trim().toLowerCase();return n<0||i!==""&&i!=="json"?{json:null,message:t}:{json:e.slice(o+1,n),message:`${e.slice(0,r)}${e.slice(n+k.length)}`.trim()}}function tt(e){return e instanceof Error&&e.message?e.message:typeof e=="string"&&e?e:"Unknown error"}function ce(e,t){const r={state:I({}),errors:e};return t&&(r.message=t),I(r)}function rt(e,t,r){let o=e,n="";if(typeof e=="string"){const s=et(e);if(s.json===null)return ce([],s.message||void 0);o=s.json,n=s.message}else if($(e)&&S(e,"message")&&typeof e.message=="string"){const{message:s,...l}=e;o=l,n=s}const i=z(o,t,r);return n&&(i.message=n),i}function nt(e){const{columns:t,callModel:r,parts:o,schemaOptions:n={}}=e,i={include:o??n.include,exclude:n.exclude},s=Y(t,{...n,...i});return async(l,a)=>{const c=a?.signal??new AbortController().signal;if(c.aborted)throw V();const v=new Promise((m,y)=>{c.addEventListener("abort",()=>y(V()),{once:!0})});v.catch(()=>{});let g;try{const m={prompt:String(l),schema:s,currentState:a?.currentState??{},history:Array.isArray(a?.history)?a.history:[]};g=await Promise.race([r(m,{signal:c}),v])}catch(m){if(c.aborted)throw V();return ce([{path:"",message:`The model call failed: ${tt(m)}`}])}if(c.aborted)throw V();return rt(g,t,i)}}const it=20,ot=1e3,st=[{name:"set_filter",part:"filter",description:"Replace the filter model of the grid (filters and quick-filter words)."},{name:"set_sort",part:"sort",description:"Replace the sort model of the grid, most significant field first. An empty list clears sorting."},{name:"set_grouping",part:"grouping",description:"Group the rows by these fields, outermost first. An empty list removes grouping."},{name:"set_aggregation",part:"aggregation",description:"Replace the summaries: field to aggregation function. An empty object removes them."},{name:"set_column_visibility",part:"columnVisibility",description:"Show (true) or hide (false) columns. Columns not listed keep their visibility."}];function w(e,t=""){return{ok:!1,errors:[{path:t,message:e}]}}async function P(e){try{return e()}catch(t){return w(t instanceof Error&&t.message?t.message:"The tool failed")}}function R(e){const t=e?.current;return t&&typeof t.getGridAiState=="function"?t:null}function lt(e){return e instanceof Date?Number.isNaN(e.getTime())?null:e.toISOString():typeof e=="number"?Number.isFinite(e)?e:null:typeof e=="string"||typeof e=="boolean"?e:null}function at(e,t,r,o,n){const i=H[o],s=Y(t,{include:[o]});if(!S(s.properties,i))return null;const{$schema:l,schemaVersion:a,title:c,description:v,...g}=s;return{name:r,description:n,inputSchema:{...g,required:[i]},execute:m=>P(()=>{const y=R(e);if(!y)return w("The grid is not ready");if(!$(m)||!S(m,i))return w(`Expected an object with "${i}"`,i);const{state:f,errors:E}=z({[i]:m[i]},t,{include:[o]});if(E.length>0)return{ok:!1,errors:E};const b={};return f.filterModel&&y.setFilterModel(b.filterModel=f.filterModel),f.sortModel&&y.setSortModel(b.sortModel=f.sortModel),f.rowGroupingModel&&y.setRowGroupingModel(b.rowGroupingModel=f.rowGroupingModel),f.aggregationModel&&y.setAggregationModel(b.aggregationModel=f.aggregationModel),f.columnVisibilityModel&&(b.columnVisibilityModel={...y.getGridAiState().columnVisibilityModel,...f.columnVisibilityModel},y.setColumnVisibilityModel(b.columnVisibilityModel)),{ok:!0,applied:b}})}}const x={type:"object",properties:{},additionalProperties:!1};function ut(e,t,r={}){const o=J(t),n=[{name:"get_grid_state",description:"Read the current filter, sort, grouping, aggregation, pivot and column visibility of the grid, and its columns.",inputSchema:x,execute:()=>P(()=>{const i=R(e);return i?{ok:!0,state:i.getGridAiState(),columns:o.map(s=>({field:s.field,headerName:typeof s.headerName=="string"?s.headerName:s.field,type:typeof s.type=="string"?s.type:"string"}))}:w("The grid is not ready")})}];for(const{name:i,part:s,description:l}of st){const a=at(e,o,i,s,l);a&&n.push(a)}if(n.push({name:"clear_filters",description:"Remove every filter and the quick-filter words.",inputSchema:x,execute:()=>P(()=>{const i=R(e);if(!i)return w("The grid is not ready");const s={items:[]};return i.setFilterModel(s),{ok:!0,applied:{filterModel:s}}})}),r.allowRowAccess===!0){const i=typeof r.maxRows=="number"&&r.maxRows>=1?Math.min(Math.floor(r.maxRows),ot):it,s=Array.isArray(r.fields)?o.filter(l=>r.fields?.includes(l.field)):o;n.push({name:"get_row_summary",description:`Count the rows that pass the current filter and read the first ones (at most ${i}, fields: ${s.map(l=>l.field).join(", ")}).`,inputSchema:{type:"object",properties:{limit:{type:"integer",minimum:1,maximum:i}},additionalProperties:!1},execute:l=>P(()=>{const a=R(e);if(!a)return w("The grid is not ready");const c=$(l)&&S(l,"limit")?l.limit:i,v=typeof c=="number"&&c>=1?Math.min(Math.floor(c),i):i,g=a.getAllFilteredRows(),m=Array.isArray(g)?g:[],y=m.slice(0,v).map(f=>{const E={};if(!$(f))return E;for(const b of s)E[b.field]=lt(ge(f,b.field,b));return E});return{ok:!0,rowCount:m.length,rows:y}})})}return n}exports.createGridAgentTools=ut,exports.createGridAiPromptHandler=nt,exports.getGridAiSchema=Y,exports.validateGridAiState=z;