@opencorestack/opengridx 3.5.0 → 3.6.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 (79) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +6 -3
  3. package/dist/ai.cjs +4 -2
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +2 -2
  6. package/dist/ai.es.js +4 -2
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +32 -0
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6924 -6691
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +17 -17
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +14 -1
  15. package/docs/README.md +2 -0
  16. package/docs/architecture/datagrid-orchestration.md +2 -1
  17. package/docs/features/ai-toolkit.md +2 -2
  18. package/docs/features/export-guide.md +2 -0
  19. package/docs/features/header-filters.md +34 -0
  20. package/docs/features/keyboard-navigation.md +2 -0
  21. package/docs/features/nextjs.md +241 -0
  22. package/docs/features/pdf-export.md +2 -0
  23. package/docs/features/pinning.md +2 -0
  24. package/docs/features/rtl.md +58 -0
  25. package/docs/features/virtualization.md +5 -5
  26. package/docs/performance.md +180 -1
  27. package/docs/roadmap.md +5 -3
  28. package/lib/ai/promptHandler.ts +12 -8
  29. package/lib/ai/validate.ts +5 -2
  30. package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
  31. package/lib/components/Cell/Cell.css +29 -9
  32. package/lib/components/Cell/Cell.tsx +2 -2
  33. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
  34. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
  35. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
  36. package/lib/components/DataGrid/DataGrid.css +12 -6
  37. package/lib/components/DataGrid/DataGrid.tsx +9 -1
  38. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
  39. package/lib/components/DataGrid/GridListView.tsx +3 -1
  40. package/lib/components/Header/ColumnMenu.css +1 -1
  41. package/lib/components/Header/ColumnMenu.tsx +14 -6
  42. package/lib/components/Header/Header.css +18 -10
  43. package/lib/components/Header/Header.tsx +16 -9
  44. package/lib/components/Header/headerColumnStyle.ts +3 -2
  45. package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
  46. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
  47. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
  48. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
  49. package/lib/components/ListView/ListViewRow.tsx +1 -1
  50. package/lib/components/Pagination/Pagination.css +4 -0
  51. package/lib/components/Pagination/Pagination.tsx +4 -4
  52. package/lib/components/Row/Row.tsx +3 -3
  53. package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
  54. package/lib/components/Toolbar/GridToolbar.tsx +8 -28
  55. package/lib/components/Toolbar/PivotPanel.tsx +4 -7
  56. package/lib/components/ui/ExpandIcon.css +13 -0
  57. package/lib/components/ui/ExpandIcon.tsx +2 -4
  58. package/lib/hooks/core/useGridCellSelection.ts +20 -5
  59. package/lib/hooks/core/useGridColumns.tsx +2 -2
  60. package/lib/hooks/core/useGridDevWarnings.ts +21 -6
  61. package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
  62. package/lib/hooks/core/useGridMaxContentHeight.ts +16 -0
  63. package/lib/hooks/core/useGridScrollSync.ts +28 -5
  64. package/lib/hooks/core/useGridVirtualization.ts +19 -0
  65. package/lib/hooks/core/useLayout.ts +13 -6
  66. package/lib/hooks/useRowGrouping.ts +9 -9
  67. package/lib/types/index.ts +30 -0
  68. package/lib/utils/aggregation/index.ts +56 -1
  69. package/lib/utils/direction.ts +52 -0
  70. package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
  71. package/lib/utils/export/exportToPdf.ts +2 -1
  72. package/lib/utils/filtering/index.ts +58 -6
  73. package/lib/utils/headerFilterLocale.ts +37 -0
  74. package/lib/utils/maxElementHeight.ts +53 -0
  75. package/lib/utils/scroll/index.ts +5 -3
  76. package/lib/utils/sorting/index.ts +141 -26
  77. package/lib/utils/values.ts +10 -6
  78. package/llms.txt +4 -2
  79. package/package.json +3 -2
@@ -243,7 +243,7 @@ Public CSS: `ogx__cell-fill-handle` (24 × 24 px hit area in the corner cell; th
243
243
 
244
244
  | Prop | Type | Default | Description |
245
245
  | :--- | :--- | :--- | :--- |
246
- | `localeText` | `GridLocaleText` | — | Override user-visible strings: the Pagination labels, the empty-state label (`noRowsLabel`, which wins over the `noRowsLabel` prop), the cell-range announcement and the Ask AI button (`aiAssistantButton`). See [`GridLocaleText`](#gridlocaletext). |
246
+ | `localeText` | `GridLocaleText` | — | Override user-visible strings: the Pagination labels, the empty-state label (`noRowsLabel`, which wins over the `noRowsLabel` prop), the cell-range announcement, the Ask AI button (`aiAssistantButton`) and the header filter row (`headerFilter*`, v3.6). See [`GridLocaleText`](#gridlocaletext). |
247
247
 
248
248
  #### Imperative Ref
249
249
 
@@ -889,6 +889,19 @@ interface GridLocaleText {
889
889
  cellSelectionAnnouncement?: (cellCount: number, rowCount: number, columnCount: number) => string;
890
890
  /** v3.5: label of the toolbar's Ask AI button. Default "Ask AI". */
891
891
  aiAssistantButton?: string;
892
+ /** v3.6: header filter row strings (defaults in English). See features/header-filters.md#translating-the-row. */
893
+ headerFilterCustom?: string; // "Custom filter"
894
+ headerFilterCustomTooltip?: string; // "Set in the filter panel"
895
+ headerFilterCellLabel?: (header: string) => string; // "Filter Qty"
896
+ headerFilterValueLabel?: (header: string) => string; // "Filter value for Qty"
897
+ headerFilterOperatorButtonLabel?: (header: string, operatorLabel: string) => string; // "Filter operator for Qty: Equals"
898
+ headerFilterOperatorMenuLabel?: (header: string) => string; // "Filter operator for Qty"
899
+ headerFilterClearLabel?: (header: string) => string; // "Clear filter for Qty"
900
+ headerFilterClearTooltip?: string; // "Clear"
901
+ headerFilterAny?: string; // "Any"
902
+ headerFilterTrue?: string; // "Yes"
903
+ headerFilterFalse?: string; // "No"
904
+ headerFilterOperators?: Partial<Record<GridFilterOperator, string>>; // operator names
892
905
  }
893
906
  ```
894
907
 
package/docs/README.md CHANGED
@@ -48,9 +48,11 @@ In-depth guides on the grid's functional capabilities.
48
48
  - **[Cell Spanning](features/cell-spanning.md)** - Row and Col span spanning
49
49
  - **[Master-Detail](features/master-detail.md)** - Expandable Detail Panels
50
50
  - **[Keyboard & Accessibility](features/keyboard-navigation.md)** - Keys, focus behaviour and ARIA structure
51
+ - **[Right-to-Left (RTL)](features/rtl.md)** - What works in `dir="rtl"`, what was fixed, and the known limitations (pinned columns)
51
52
  - **[List View](features/list-view.md)** - Card-based responsive layout
52
53
  - **[Infinite Scroll](features/infinite-scroll.md)** - Advanced performance loading
53
54
  - **[Data Source](features/data-source.md)** - Server-side fetching and SSR
55
+ - **[Next.js (App Router)](features/nextjs.md)** - Client components, server-fetched rows, Route Handlers, the AI toolkit on server and client, common errors
54
56
  - **[Export Guide](features/export-guide.md)** - Excel, CSV, JSON, and Print
55
57
  - **[AI Toolkit](features/ai-toolkit.md)** - JSON Schema of the grid state for your own model, a validator for its replies, the **Ask AI** panel (`aiAssistant`) and tools for AI agents (`@opencorestack/opengridx/ai`)
56
58
 
@@ -71,7 +71,8 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
71
71
  | 31 | `useGridScrollToIndexesApi` | `hooks/core/useGridScrollToIndexesApi.ts` | `scrollToIndexes` |
72
72
  | 31a | `useGridColumnAutosize` | `hooks/core/useGridColumnAutosize.ts` | `autosizeColumn` / `autosizeColumns` on the API, and the resize handle's auto-size handler (v3.1) |
73
73
  | 32 | `useGridViewport` | `hooks/core/useGridViewport.ts` | Scroll sync, viewport measurement, viewport callback ref |
74
- | 33 | `useGridVirtualization` | `hooks/core/useGridVirtualization.ts` | Render window |
74
+ | 32a | `useGridMaxContentHeight` | `hooks/core/useGridMaxContentHeight.ts` | The tallest element this browser lays out (`utils/maxElementHeight.ts`, measured once per document after mount; `undefined` until then, `Infinity` without a layout engine) |
75
+ | 33 | `useGridVirtualization` | `hooks/core/useGridVirtualization.ts` | Render window; `virtualContainerHeight` caps the rows' container to `maxContentHeight` (Firefox collapses taller elements) |
75
76
  | 34 | `useGridSortHandlers` | `hooks/core/useGridHeaderHandlers.ts` | Header sort / shift-sort |
76
77
  | 35 | `useGridKeyboardNavigation` | `hooks/core/useGridKeyboardNavigation.ts` | Focus and keyboard handling; takes `cellSelection.keyboard` (Shift+navigation, Escape, Ctrl/Cmd+A) |
77
78
  | 36 | `useGridPointerFocusHandlers` | `hooks/core/useGridFocusHandlers.ts` | Cell and header click focus |
@@ -104,7 +104,7 @@ getGridAiSchema(
104
104
 
105
105
  ## `validateGridAiState(json, columns, options?)`
106
106
 
107
- Returns `{ state, errors }`, where `state` is a `GridAiState` (`filterModel`, `sortModel`, `rowGroupingModel`, `aggregationModel`, `pivotModel`, `columnVisibilityModel`, each present only when the reply contained it) and `errors` is a list of `{ path, message }`, such as `{ path: 'filterModel.items[2].field', message: 'Unknown field "revenue"' }`.
107
+ Returns `{ state, errors }`, where `state` is a `GridAiState` (`filterModel`, `sortModel`, `rowGroupingModel`, `aggregationModel`, `pivotModel`, `columnVisibilityModel`, each present only when the reply contained it) and `errors` is a list of `{ path, message }`, such as `{ path: 'filterModel.items[2].field', message: 'Unknown field "revenue"' }`. A top-level string `message` key (the model's reply text, which `createGridAiPromptHandler` shows) is skipped in both object and JSON string input, so you can validate a whole reply without removing it first; a `message` that is not a string is reported.
108
108
 
109
109
  - `json` may be an object or a JSON string. Bad JSON gives one error and an empty state.
110
110
  - Unknown parts, unknown fields, fields a column does not allow (`filterable: false` …), operators that do not fit the column type, and functions or enum values outside the allowed list are **dropped**, each with an error. Parts outside `options.include` / `options.exclude` are dropped too.
@@ -172,7 +172,7 @@ A copied state (`{ ...result.state }`) or a hand-built one is not branded.
172
172
  Returns the `onPrompt` function. It builds the schema once (from `columns`, limited to `parts` and `schemaOptions`), then for each prompt:
173
173
 
174
174
  1. calls `callModel({ prompt, schema, currentState, history }, { signal })`;
175
- 2. reads the reply: a state object, a JSON string, or text with a fenced `json` block (the text around the block becomes `result.message`). An object's string `message` key is the message, not state. Plain text with no JSON becomes the message with nothing to apply;
175
+ 2. reads the reply: a state object, a JSON string, or text with a fenced `json` block (the text around the block becomes `result.message`). A string `message` key in the state object (or in the JSON of a string reply) is the message, not state; when a fenced block has text around it and a `message` key inside, both are kept, joined by a blank line. Plain text with no JSON becomes the message with nothing to apply;
176
176
  3. validates it with `validateGridAiState` (same `parts`) and returns the branded `{ state, errors, message? }`.
177
177
 
178
178
  A `callModel` throw or rejection becomes `{ state: {}, errors: [{ path: '', message: 'The model call failed: …' }] }`. An abort (Stop, a new prompt, closing the panel) rejects with an `AbortError`, even when `callModel` ignores the signal.
@@ -2,6 +2,8 @@
2
2
 
3
3
  The DataGrid provides built-in export utilities for multiple formats. The standard exports (CSV, JSON, Print, basic Excel) have **zero dependencies**. The advanced Excel export uses [ExcelJS](https://github.com/exceljs/exceljs) loaded lazily.
4
4
 
5
+ `exceljs` is an optional peer dependency: install it (`npm install exceljs`) only if you call `exportToExcelAdvanced`. Apps without it still build in Vite, webpack 5 and Next.js (Turbopack and webpack; webpack prints a `Module not found` warning for the optional import), and `exportToExcelAdvanced` then rejects with `exportToExcelAdvanced requires 'exceljs'. Run: npm install exceljs`. Before v3.6, webpack and Next.js builds failed without it.
6
+
5
7
  ## Available Export Formats
6
8
 
7
9
  | Format | Function | Deps | Description |
@@ -107,6 +107,40 @@ The filter row is a header row (`role="row"`) between the column headers and the
107
107
 
108
108
  Inside a control, arrow keys edit the text (or move through a select) and do not move the grid's focus, as in a cell editor. **Escape** returns focus to the filter cell. **Tab** leaves the grid: the grid stays a single Tab stop and every control in the row has `tabIndex={-1}`.
109
109
 
110
+ ## Translating the row
111
+
112
+ Every string the row shows or announces comes from `localeText` (v3.6). Each key defaults to the English text below.
113
+
114
+ | `GridLocaleText` key | Type | Default |
115
+ | :--- | :--- | :--- |
116
+ | `headerFilterCustom` | `string` | `"Custom filter"` |
117
+ | `headerFilterCustomTooltip` | `string` | `"Set in the filter panel"` |
118
+ | `headerFilterCellLabel` | `(header) => string` | `` `Filter ${header}` `` (the cell's `aria-label`) |
119
+ | `headerFilterValueLabel` | `(header) => string` | `` `Filter value for ${header}` `` |
120
+ | `headerFilterOperatorButtonLabel` | `(header, operatorLabel) => string` | `` `Filter operator for ${header}: ${operatorLabel}` `` |
121
+ | `headerFilterOperatorMenuLabel` | `(header) => string` | `` `Filter operator for ${header}` `` |
122
+ | `headerFilterClearLabel` | `(header) => string` | `` `Clear filter for ${header}` `` |
123
+ | `headerFilterClearTooltip` | `string` | `"Clear"` |
124
+ | `headerFilterAny` | `string` | `"Any"` (boolean and `singleSelect` selects) |
125
+ | `headerFilterTrue` / `headerFilterFalse` | `string` | `"Yes"` / `"No"` |
126
+ | `headerFilterOperators` | `Partial<Record<GridFilterOperator, string>>` | English names ("Contains", "Is empty", ...); operators left out keep them |
127
+
128
+ ```tsx
129
+ <DataGrid
130
+ headerFilters
131
+ localeText={{
132
+ headerFilterAny: 'Tous',
133
+ headerFilterTrue: 'Oui',
134
+ headerFilterFalse: 'Non',
135
+ headerFilterCellLabel: (h) => `Filtrer ${h}`,
136
+ headerFilterOperators: { contains: 'Contient', isEmpty: 'Est vide' },
137
+ }}
138
+ /* ... */
139
+ />
140
+ ```
141
+
142
+ The operator symbols on the buttons (`=`, `≠`, `>`) are not translated. Demo: *Locale Text* (`/locale-text`).
143
+
110
144
  ## Layout
111
145
 
112
146
  - The row sits inside the sticky header block (`.ogx__header-wrap`), under any column group rows and the column header row, so it stays at the top while rows scroll.
@@ -108,6 +108,8 @@ In `listView`, focus moves between whole rows instead of cells:
108
108
 
109
109
  Keys pressed in content your `renderCell` puts in a row (inputs, buttons) are left to that content.
110
110
 
111
+ In a right-to-left grid (`dir="rtl"`, v3.6) ArrowLeft and ArrowRight follow the screen: ArrowLeft moves to the next column, and the Alt+Arrow shortcuts (expand / collapse, header resize) are mirrored the same way. See [Right-to-Left](rtl.md).
112
+
111
113
  ## ARIA structure
112
114
 
113
115
  | Element | Attributes |
@@ -0,0 +1,241 @@
1
+ # Next.js (App Router)
2
+
3
+ OpenGridX is a client component: it uses state, effects, context and the DOM. In the App Router you render it from a file marked `'use client'`, and fetch the data in Server Components or Route Handlers.
4
+
5
+ Everything on this page was built with `next build`, served with `next start` and loaded in Chromium, Firefox and WebKit without console errors, on Next.js 16.4 (Turbopack; `next build --webpack` also builds) and 15.5 (webpack), React 19.3.
6
+
7
+ ## 1. Install and import the stylesheet
8
+
9
+ ```bash
10
+ npm install @opencorestack/opengridx
11
+ ```
12
+
13
+ Import the stylesheet once, in the root layout:
14
+
15
+ ```tsx
16
+ // app/layout.tsx
17
+ import type { ReactNode } from 'react';
18
+ import '@opencorestack/opengridx/styles';
19
+
20
+ export default function RootLayout({ children }: { children: ReactNode }) {
21
+ return (
22
+ <html lang="en">
23
+ <body>{children}</body>
24
+ </html>
25
+ );
26
+ }
27
+ ```
28
+
29
+ ### The optional export peers
30
+
31
+ `exportToExcelAdvanced` needs `exceljs`, and `exportToPdf` needs `jspdf` + `jspdf-autotable`. The grid loads them with `import()` only when you call those functions. Install them if you use those exports:
32
+
33
+ ```bash
34
+ npm install exceljs jspdf jspdf-autotable
35
+ ```
36
+
37
+ If you do not install them, the app still builds (Turbopack and webpack treat these imports as optional; webpack prints a `Module not found` **warning**, not an error) and every other export works. Calling `exportToExcelAdvanced` or `exportToPdf` then rejects with an install hint such as `exportToExcelAdvanced requires 'exceljs'. Run: npm install exceljs`. No `next.config` alias is needed from v3.6. In 3.5.1 and earlier `next build` failed with `Module not found: Can't resolve 'exceljs'` without the peers.
38
+
39
+ ## 2. Render the grid from a client component
40
+
41
+ Column definitions usually hold functions (`valueFormatter`, `renderCell`, `valueGetter`). Functions cannot cross from a Server Component to a Client Component, so **define the columns in the client file**:
42
+
43
+ ```tsx
44
+ // app/OrdersGrid.tsx
45
+ 'use client';
46
+
47
+ import { DataGrid, GridToolbar, type GridColDef } from '@opencorestack/opengridx';
48
+ import type { Order } from '../lib/orders';
49
+
50
+ const columns: GridColDef<Order>[] = [
51
+ { field: 'id', headerName: 'ID', type: 'number', width: 80 },
52
+ { field: 'customer', headerName: 'Customer', flex: 1 },
53
+ {
54
+ field: 'amount',
55
+ headerName: 'Amount',
56
+ type: 'number',
57
+ width: 140,
58
+ valueFormatter: ({ value }) => `$${Number(value).toFixed(2)}`,
59
+ },
60
+ { field: 'shipped', headerName: 'Shipped', type: 'boolean', width: 110 },
61
+ ];
62
+
63
+ export function OrdersGrid({ rows }: { rows: Order[] }) {
64
+ return (
65
+ <DataGrid
66
+ rows={rows}
67
+ columns={columns}
68
+ height={480}
69
+ checkboxSelection
70
+ headerFilters
71
+ slots={{ toolbar: GridToolbar }}
72
+ />
73
+ );
74
+ }
75
+ ```
76
+
77
+ Give the grid a `height` (or a parent with a bounded height): without one it fills its container, and in an auto-height page that means every row renders. See [Virtualization](virtualization.md).
78
+
79
+ ## 3. Fetch rows in a Server Component
80
+
81
+ The page stays a Server Component. It loads the rows and passes them as props. Props must be serialisable: plain objects, arrays, strings, numbers, booleans, `null`. `Date` objects survive; class instances, `Map`s with functions and functions do not.
82
+
83
+ ```tsx
84
+ // app/page.tsx (Server Component, no 'use client')
85
+ import { getOrders } from '../lib/orders';
86
+ import { OrdersGrid } from './OrdersGrid';
87
+
88
+ export default async function Page() {
89
+ const rows = await getOrders(); // a database query, fetch() to your API, ...
90
+ return <OrdersGrid rows={rows} />;
91
+ }
92
+ ```
93
+
94
+ The grid renders on the server (the first screen of rows is in the HTML) and hydrates in the browser.
95
+
96
+ **Rendering `<DataGrid>` straight from a Server Component** works from v3.6, whose bundle starts with `'use client'`, as long as every prop is serialisable (`columns={[{ field: 'name' }]}`, no functions). In 3.5.1 and earlier it fails with `TypeError: createContext is not a function`. Wrapping the grid in your own client file, as above, works in every version and is where your column functions belong anyway.
97
+
98
+ ## 4. Server-side data with `dataSource` and a Route Handler
99
+
100
+ For large tables, let the server sort, filter and page. The `dataSource` lives in the client file and calls a Route Handler:
101
+
102
+ ```tsx
103
+ // app/orders/ServerOrdersGrid.tsx
104
+ 'use client';
105
+
106
+ import { DataGrid, GridToolbar, type GridColDef, type GridDataSource } from '@opencorestack/opengridx';
107
+ import type { Order } from '../../lib/orders';
108
+
109
+ const columns: GridColDef<Order>[] = [
110
+ { field: 'id', headerName: 'ID', type: 'number', width: 80 },
111
+ { field: 'customer', headerName: 'Customer', flex: 1 },
112
+ { field: 'amount', headerName: 'Amount', type: 'number', width: 140 },
113
+ ];
114
+
115
+ // Module scope (or useMemo): a new dataSource object on every render refetches.
116
+ const dataSource: GridDataSource<Order> = {
117
+ getRows: async (params) => {
118
+ const res = await fetch('/api/orders', {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify(params),
122
+ });
123
+ if (!res.ok) throw new Error(`Orders request failed: ${res.status}`);
124
+ return res.json();
125
+ },
126
+ };
127
+
128
+ export function ServerOrdersGrid() {
129
+ return (
130
+ <DataGrid
131
+ rows={[]}
132
+ columns={columns}
133
+ dataSource={dataSource}
134
+ sortingMode="server"
135
+ filterMode="server"
136
+ paginationMode="server"
137
+ pagination
138
+ initialState={{ pagination: { paginationModel: { page: 0, pageSize: 25 } } }}
139
+ height={480}
140
+ slots={{ toolbar: GridToolbar }}
141
+ />
142
+ );
143
+ }
144
+ ```
145
+
146
+ ```ts
147
+ // app/api/orders/route.ts
148
+ import type { GridGetRowsParams, GridGetRowsResponse } from '@opencorestack/opengridx';
149
+ import { queryOrders } from '../../../lib/orders';
150
+
151
+ export async function POST(request: Request) {
152
+ const params = (await request.json()) as GridGetRowsParams;
153
+ // params: startRow, endRow, sortModel, filterModel (items, quickFilterValues), groupKeys
154
+ const { rows, total } = await queryOrders(params);
155
+ const body: GridGetRowsResponse = { rows, rowCount: total };
156
+ return Response.json(body);
157
+ }
158
+ ```
159
+
160
+ Importing types from the package in a Route Handler is free: they are erased at build time. See [Data Source](data-source.md) for the request and response shapes.
161
+
162
+ ## 5. The AI toolkit (`@opencorestack/opengridx/ai`)
163
+
164
+ `@opencorestack/opengridx/ai` has no React code and no imports, so it runs anywhere: Server Components, Route Handlers, middleware and the browser.
165
+
166
+ - **On the server**: build the schema (`getGridAiSchema`) for your model call, and keep your API key there. You may also run `validateGridAiState` on the reply to log or reject bad answers.
167
+ - **In the browser**: the grid applies a reply only if `validateGridAiState` branded it, and the brand is a `Symbol` that does not survive JSON. So the reply must be validated again on the client. `createGridAiPromptHandler` does that for you:
168
+
169
+ ```tsx
170
+ // app/ai/AiOrdersGrid.tsx
171
+ 'use client';
172
+
173
+ import { DataGrid, GridToolbar } from '@opencorestack/opengridx';
174
+ import { createGridAiPromptHandler } from '@opencorestack/opengridx/ai';
175
+ import { aiColumns } from '../../lib/aiColumns'; // plain column data, shared with the route
176
+ import type { Order } from '../../lib/orders';
177
+
178
+ const onPrompt = createGridAiPromptHandler({
179
+ columns: aiColumns,
180
+ callModel: async (request, { signal }) => {
181
+ // request: { prompt, schema, currentState, history }
182
+ const res = await fetch('/api/grid-ai', { method: 'POST', body: JSON.stringify(request), signal });
183
+ return res.json(); // e.g. { filterModel, sortModel, message }
184
+ },
185
+ });
186
+
187
+ export function AiOrdersGrid({ rows }: { rows: Order[] }) {
188
+ return <DataGrid rows={rows} columns={aiColumns} height={480} slots={{ toolbar: GridToolbar }} aiAssistant={{ onPrompt }} />;
189
+ }
190
+ ```
191
+
192
+ ```ts
193
+ // app/api/grid-ai/route.ts
194
+ import { validateGridAiState } from '@opencorestack/opengridx/ai';
195
+ import { aiColumns } from '../../../lib/aiColumns';
196
+
197
+ export async function POST(request: Request) {
198
+ const { prompt, schema, currentState } = await request.json();
199
+ const reply = await callYourModel({ prompt, schema, currentState }); // your SDK, with `schema` as structured output
200
+ // Optional server-side check. A string `message` key is skipped, not reported as an error.
201
+ const { errors } = validateGridAiState(reply, aiColumns);
202
+ if (errors.length) console.info('Model reply had invalid parts:', errors);
203
+ return Response.json(reply);
204
+ }
205
+ ```
206
+
207
+ Columns shared between the route and the client must be plain data (no functions), or the server gets a client reference instead of the array. Keep a function-free list such as `aiColumns` in a module without `'use client'`, and spread extra function fields onto it in the client file if the grid needs them.
208
+
209
+ ## Pages Router
210
+
211
+ The Pages Router has no Server Components: every page is a client component that also renders on the server. Import the grid directly in the page, and the stylesheet in `pages/_app.tsx`:
212
+
213
+ ```tsx
214
+ // pages/_app.tsx
215
+ import type { AppProps } from 'next/app';
216
+ import '@opencorestack/opengridx/styles';
217
+
218
+ export default function App({ Component, pageProps }: AppProps) {
219
+ return <Component {...pageProps} />;
220
+ }
221
+ ```
222
+
223
+ Column functions can stay in the page. Rows from `getServerSideProps` / `getStaticProps` must be JSON-serialisable (no `Date`; send ISO strings and use `valueGetter`).
224
+
225
+ ## Common errors
226
+
227
+ | Error | Cause and fix |
228
+ | :--- | :--- |
229
+ | `You're importing a module that depends on useState into a React Server Component module` (webpack: `…needs useState. This React hook only works in a Client Component`) | A file of yours that calls hooks (or wraps the grid) has no `'use client'`. Add it at the top of the file that renders `<DataGrid>`. |
230
+ | `TypeError: createContext is not a function` (build: "Failed to collect page data") | 3.5.1 or earlier, imported from a Server Component. Render the grid from a `'use client'` file, or upgrade. |
231
+ | `Functions cannot be passed directly to Client Components` … `{field: "name", valueFormatter: function}` | Columns with functions built in a Server Component. Define them in the client file. |
232
+ | `Module not found: Can't resolve 'exceljs'` (or `jspdf`) | 3.5.1 or earlier without the optional export peers. Upgrade, or install them (section 1). From v3.6 webpack shows this as a warning only. |
233
+ | Unstyled grid, every row rendered, dev warning `[OpenGridX] The grid stylesheet is not loaded` | The stylesheet is not imported. Add `import '@opencorestack/opengridx/styles'` to `app/layout.tsx` (or `pages/_app.tsx`). |
234
+ | `Hydration failed because the server rendered HTML didn't match the client` | Usually rows or cell text that differ between server and client: `Math.random()`, `Date.now()`, `toLocaleString()` without a fixed locale and time zone in a `valueFormatter`. Fetch rows on the server and pass them as props, and format with an explicit locale (`toLocaleString('en-US', { timeZone: 'UTC' })`). Rows created in an effect or with `typeof window` branches render differently on the server; build them once on the server instead. |
235
+ | The grid refetches in a loop with `dataSource` | The `dataSource` object is recreated on every render. Define it at module scope or with `useMemo`. |
236
+
237
+ ## See also
238
+
239
+ - [Data Source](data-source.md)
240
+ - [AI Toolkit](ai-toolkit.md)
241
+ - [Export Guide](export-guide.md)
@@ -10,6 +10,8 @@ Generate a styled PDF report from grid data using `exportToPdf()`.
10
10
  npm install jspdf jspdf-autotable
11
11
  ```
12
12
 
13
+ They are optional: an app that never calls `exportToPdf` need not install them, and still builds in Vite, webpack 5 and Next.js (webpack prints a `Module not found` warning for the optional import, not an error). Without them `exportToPdf` rejects with the install hint below.
14
+
13
15
  > **Currency symbols such as `₹` print as `?` by default.** The built-in Helvetica font only covers Latin-1. Pass the [`font` option](#non-latin-text-and-unicode-fonts) with a Unicode `.ttf`:
14
16
  >
15
17
  > ```ts
@@ -35,6 +35,8 @@ Pinned column headers are not draggable and are not drop targets for header drag
35
35
 
36
36
  ---
37
37
 
38
+ > **Right-to-left:** `left` and `right` are the inline **start** and **end** sides. In a `dir="rtl"` grid `pinnedColumns.left` (and the pinned checkbox, expand and reorder columns) stick to the right edge, `pinnedColumns.right` to the left edge, and the shadows fall towards the scrolling columns. The column menu's *Pin to Left* / *Pin to Right* items pin by screen side, so in RTL *Pin to Left* adds the column to `pinnedColumns.right`. See [Right-to-Left](rtl.md#pinned-columns).
39
+
38
40
  ## ⬆️ Row Pinning
39
41
 
40
42
  Rows can be pinned to the top or bottom of the grid. Pinned rows remain visible while the user scrolls vertically through the grid body.
@@ -0,0 +1,58 @@
1
+ # Right-to-Left (RTL)
2
+
3
+ Put the grid inside an element with `dir="rtl"`, or set `dir="rtl"` on `<html>` (`document.dir = 'rtl'`). There is no prop: the grid follows the CSS `direction` it inherits.
4
+
5
+ ```tsx
6
+ <div dir="rtl" lang="ar">
7
+ <DataGrid rows={rows} columns={columns} height={480} />
8
+ </div>
9
+ ```
10
+
11
+ **Status (v3.6):** RTL grids work: column order, text, horizontal scrolling with column virtualization, keyboard, cell ranges, the fill handle, resizing, the toolbar panels, pinned columns (see [Pinned columns](#pinned-columns)), mirrored icons and switching `dir` at runtime. Remaining gaps are under [Known limitations](#known-limitations). Checked in Chromium, Firefox and WebKit; `lib/components/DataGrid/DataGrid.rtl.browser.test.tsx` covers what is listed as working or fixed.
12
+
13
+ Demo: *Right-to-Left* (`/rtl`), with Arabic and Hebrew sample data and an RTL toggle.
14
+
15
+ ## What was checked
16
+
17
+ | Area | Result | Notes |
18
+ | :--- | :--- | :--- |
19
+ | Column order | Works | The first column is on the right. |
20
+ | Header and cell text alignment | **Fixed in v3.6** | Cells used `text-align: left`; `align: 'left'` / `'right'` now mean start / end (`text-align: start` / `end`), so default text starts on the right and number columns end on the left, matching the header. |
21
+ | Horizontal scrolling and column virtualization | **Fixed in v3.6** | Browsers report `scrollLeft` from 0 down to a negative number in RTL. The grid counted it as 0, so after a horizontal scroll it rendered the first columns (now scrolled out of view) and the body was blank. It now measures the scroll offset from the start edge. |
22
+ | Keyboard ArrowLeft / ArrowRight | **Fixed in v3.6** | ArrowLeft moves to the column on the left (the next column), ArrowRight to the right; Home / End go to the first / last column and scroll it into view. Shift+Arrow extends a cell range the same way. Alt+ArrowLeft / Alt+ArrowRight on a tree or group row expand / collapse in the screen direction (ArrowLeft expands), as do Alt+Arrow column resizing on a header (ArrowLeft widens) and Arrow keys on a focused resize handle. |
23
+ | Column resize handle | **Fixed in v3.6** | It was on the column's right edge (its start edge in RTL) and dragging it right narrowed the column. It now sits on the left (end) edge and dragging left widens the column. |
24
+ | Cell range selection | **Fixed in v3.6** | The range outline was drawn on the inner edges in RTL (the `--range-left` / `--range-right` modifiers mark the first and last column); it now outlines the range's outer edges. Dragging past the left edge of the grid auto-scrolls towards the later columns (it did not scroll at all before). |
25
+ | Fill handle | **Fixed in v3.6** | The square was on the bottom-right corner of the range; it is now on the bottom-left, where the range ends. Dragging it fills towards the pointer as in LTR. |
26
+ | Toolbar panels (columns, filters, summaries, pivot) | **Fixed in v3.6** | They opened leftwards from the button, off the screen when the toolbar is on the left. In RTL they now open rightwards, lined up with the button's left edge. |
27
+ | Column menu, header filter operator menu | Works | Placed under their button. |
28
+ | Header filter row | Works | Filter cells line up with their columns. Operator symbols are mirrored by the browser's bidi rules (`∋` shows as `∈`), as in any RTL text. |
29
+ | Ask AI panel | Works | Opens over the grid. |
30
+ | Left / right pinned columns, pinned system columns | **Fixed in v3.6** | Sticky offsets were physical `left` / `right`, so pinned columns scrolled away. They are now logical (`inset-inline-start` / `-end`) in the header, column group rows, header filter row, body, skeleton rows and aggregation footer. See [Pinned columns](#pinned-columns). |
31
+ | Pinned shadows and section borders | **Fixed in v3.6** | Mirrored: they face the scrolling columns. |
32
+ | Range drag with pinned columns | **Fixed in v3.6** | Auto-scroll starts at the inner edge of the pinned section on each side. |
33
+ | Pagination arrows, tree / group chevrons | **Fixed in v3.6** | Mirrored with `scaleX(-1)` under `:dir(rtl)`; tree indentation is on the start side. Sort arrows, search, close and other non-directional icons are not mirrored. |
34
+ | Switching `dir` on a mounted grid | **Fixed in v3.6** | Firefox resets `scrollLeft` without a scroll event, which left the body blank; the grid re-reads the position when any `dir` attribute changes. Changing `direction` through a class or stylesheet is not observed: scroll the grid or remount it. |
35
+ | Column reordering by drag | Works | Drops use the target header, not pointer offsets; the drop marker is on the target's start edge. |
36
+
37
+ ## Pinned columns
38
+
39
+ `pinnedColumns.left` and `pinnedColumns.right` are **logical**: the inline start and end. In a right-to-left grid left-pinned columns, and the checkbox, expand and reorder columns pinned with `pinCheckboxColumn` / `pinExpandColumn`, stick to the **right** edge; right-pinned columns stick to the left edge. This is what MUI X and AG Grid do (AG Grid's `pinned: 'left'` and MUI's `pinnedColumns.left` move to the right in RTL), and it keeps one configuration for both directions: the identifier column a user reads first stays pinned where reading starts, and the system columns stay next to it.
40
+
41
+ The column menu is labelled by screen side: in RTL *Pin to Left* adds the column to `pinnedColumns.right`. `onPinnedColumnsChange` and `apiRef` report the logical lists. Shadow tokens are swapped in RTL (`--ogx-grid-pinned-right-shadow` is used for the start section, which casts its shadow leftwards).
42
+
43
+ ## Known limitations
44
+
45
+ - **Menu and panel labels are hard-coded English** ("Pin to Left", "Sort Ascending"); there is no `localeText` key for the column menu yet.
46
+ - **Header filter operator symbols** are mirrored by the browser's bidi rules (`∋` shows as `∈`), as in any RTL text.
47
+ - **`direction` set through CSS** (a class or stylesheet, not the `dir` attribute) and changed at runtime is not detected; scroll the grid or remount it with a `key`.
48
+ - **Headless Chromium tests:** Playwright launches Chromium with `--hide-scrollbars`. With classic scrollbars (Linux) `scrollbar-gutter: stable` still reserves the gutter, and in RTL Chromium then cannot scroll the last gutter-width of the start into view (`scrollLeft` stops at minus the gutter). A plain scroller without the grid does the same and real browsers with visible scrollbars do not. Tests must compare with the browser's scroll origin, as `DataGrid.rtl.browser.test.tsx` does. To reproduce on macOS: `defaults write chrome-headless-shell AppleShowScrollBars Always` (undo with `defaults delete chrome-headless-shell`).
49
+
50
+ ## Left-to-right grids
51
+
52
+ Every fix uses CSS logical properties (`text-align: start`, `inset-inline-end`, `margin-inline-start`) or applies only when the computed `direction` is `rtl`, so LTR grids are unchanged. A browser test measured 415 element positions (headers, cells, resize and fill handles, header filter controls, toolbar buttons and panels, before and after a horizontal scroll, with pinned columns) in all three engines before and after the change and found no difference; RTL part 2 repeated this with 2,211 boxes, shadows, borders and pseudo-element offsets (pinned and system columns, column groups, header filters, aggregation footer, before and after a scroll and with a range); `DataGrid.rtl.browser.test.tsx` keeps an LTR layout check.
53
+
54
+ ## See also
55
+
56
+ - [Keyboard & Accessibility](keyboard-navigation.md)
57
+ - [Pinning](pinning.md)
58
+ - [Cell Range Selection](cell-selection.md)
@@ -29,11 +29,11 @@ The grid calculates which rows are visible based on:
29
29
  />
30
30
  ```
31
31
 
32
- **Performance characteristics** (when the grid has a bounded height — see below):
33
- - ✅ Renders ~15-25 rows regardless of total dataset size
34
- - ✅ Constant memory usage
35
- - ✅ Smooth 60fps scrolling
36
- - ⚠️ Not millions of rows. Browsers cap an element's height (33,554,432px in Chromium, about 17.9M px in Firefox) and the grid does no scroll scaling: it lays every row out at its real height, so rows past the cap cannot be scrolled to. The limit is roughly `33,554,432 / rowHeight` rows in Chromium (about 645,000 at the default 52px), and about half that in Firefox (about 340,000). Beyond that, use pagination, or server-side paging / infinite scroll. In development the grid warns when the content passes 17.8M px.
32
+ **Performance characteristics** (when the grid has a bounded height — see below), measured by the [benchmark](../performance.md#benchmarks):
33
+ - ✅ About 19 row elements at rest in an 800 px page at 52 px rows, at most about 86 while scrolling fast, whatever the row count (10k to 1M)
34
+ - ✅ Scrolling keeps up with a 60 Hz display at 1M rows in Chromium (no missed frames) and Firefox; WebKit misses a frame or two on each long jump of the scroll position
35
+ - ✅ The grid itself adds about 10 MB of JS heap at 100k rows and 79 MB at 1M (Chromium), on top of your row data
36
+ - ⚠️ Not millions of rows at the default height. Browsers cap an element's height and the grid does no scroll scaling: it lays every row out at its real height. Chromium and WebKit stop at 33,554,428 px, so rows past about `33,554,428 / rowHeight` cannot be scrolled to (645,277 at the default 52 px; 1M rows fit at 32 px). Firefox's limit is about 17.9M px (about 344,000 rows at 52 px); since 3.5.1 the grid caps its content to the limit it measures, so Firefox scrolls normally up to there (on 3.5.0 and earlier, content past the limit collapsed and the grid did not scroll at all). Beyond that, use pagination, or server-side paging / infinite scroll. In development the grid warns when rows are cut off and names how many can be scrolled to. Measured reach per browser: [Performance → Reach](../performance.md#reach-the-browser-height-ceiling).
37
37
 
38
38
  ### Column Virtualization
39
39