@opencorestack/opengridx 3.5.1 → 3.7.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.
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/dist/ai.cjs +4 -2
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.es.js +4 -2
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +62 -0
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +9643 -9024
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +17 -3
- package/docs/README.md +2 -0
- package/docs/architecture/datagrid-orchestration.md +46 -29
- package/docs/architecture/grid-row-meta.md +1 -1
- package/docs/customization/slots-api.md +1 -1
- package/docs/customization/theming.md +43 -0
- package/docs/features/ai-toolkit.md +2 -2
- package/docs/features/export-guide.md +2 -0
- package/docs/features/header-filters.md +34 -0
- package/docs/features/keyboard-navigation.md +2 -0
- package/docs/features/nextjs.md +241 -0
- package/docs/features/pdf-export.md +2 -0
- package/docs/features/pinning.md +2 -0
- package/docs/features/rtl.md +58 -0
- package/docs/features/virtualization.md +16 -2
- package/docs/performance.md +89 -119
- package/docs/roadmap.md +4 -2
- package/lib/ai/promptHandler.ts +12 -8
- package/lib/ai/validate.ts +5 -2
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +4 -2
- package/lib/components/Cell/Cell.css +31 -9
- package/lib/components/Cell/Cell.tsx +2 -2
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +4 -4
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +3 -1
- package/lib/components/DataGrid/DataGrid.css +19 -13
- package/lib/components/DataGrid/DataGrid.tsx +163 -670
- package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
- package/lib/components/DataGrid/GridListView.tsx +3 -1
- package/lib/components/DataGrid/GridRoot.tsx +239 -0
- package/lib/components/DataGrid/GridViewport.tsx +314 -0
- package/lib/components/FilterPanel/FilterPanel.css +9 -8
- package/lib/components/Header/ColumnMenu.css +5 -4
- package/lib/components/Header/ColumnMenu.tsx +14 -6
- package/lib/components/Header/Header.css +23 -14
- package/lib/components/Header/Header.tsx +16 -9
- package/lib/components/Header/headerColumnStyle.ts +3 -2
- package/lib/components/HeaderFilter/HeaderFilter.css +9 -3
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
- package/lib/components/ListView/ListViewRow.tsx +1 -1
- package/lib/components/Pagination/Pagination.css +5 -1
- package/lib/components/Pagination/Pagination.tsx +4 -4
- package/lib/components/Row/Row.css +2 -2
- package/lib/components/Row/Row.tsx +3 -3
- package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
- package/lib/components/Toolbar/GridToolbar.css +8 -7
- package/lib/components/Toolbar/GridToolbar.tsx +8 -28
- package/lib/components/Toolbar/PivotPanel.css +3 -3
- package/lib/components/Toolbar/PivotPanel.tsx +4 -7
- package/lib/components/ui/ExpandIcon.css +13 -0
- package/lib/components/ui/ExpandIcon.tsx +2 -4
- package/lib/hooks/core/useGridCellSelection.ts +20 -5
- package/lib/hooks/core/useGridColumns.tsx +2 -2
- package/lib/hooks/core/useGridDevWarnings.ts +3 -1
- package/lib/hooks/core/useGridEditPipeline.ts +55 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
- package/lib/hooks/core/useGridRangeEditing.ts +129 -0
- package/lib/hooks/core/useGridRenderWindow.ts +66 -0
- package/lib/hooks/core/useGridScrollSync.ts +28 -5
- package/lib/hooks/core/useGridScrollViewport.ts +42 -0
- package/lib/hooks/core/useGridSelectionAndStateApi.ts +61 -0
- package/lib/hooks/core/useGridToolbarStack.ts +49 -0
- package/lib/hooks/core/useGridViewApis.ts +46 -0
- package/lib/hooks/core/useLayout.ts +13 -6
- package/lib/hooks/useRowGrouping.ts +9 -9
- package/lib/styles/base/variables.css +12 -0
- package/lib/theme/DataGridThemeProvider.tsx +15 -0
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1503 -0
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1503 -0
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1503 -0
- package/lib/theme/types.ts +31 -0
- package/lib/types/ai.ts +109 -0
- package/lib/types/api.ts +169 -0
- package/lib/types/columns.ts +341 -0
- package/lib/types/dataSource.ts +40 -0
- package/lib/types/editing.ts +77 -0
- package/lib/types/export.ts +164 -0
- package/lib/types/filtering.ts +52 -0
- package/lib/types/grouping.ts +27 -0
- package/lib/types/index.ts +18 -1672
- package/lib/types/locale.ts +51 -0
- package/lib/types/props.ts +405 -0
- package/lib/types/rows.ts +132 -0
- package/lib/types/selection.ts +66 -0
- package/lib/types/slots.ts +56 -0
- package/lib/types/sorting.ts +25 -0
- package/lib/types/state.ts +35 -0
- package/lib/utils/aggregation/index.ts +56 -1
- package/lib/utils/direction.ts +52 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
- package/lib/utils/export/exportToPdf.ts +2 -1
- package/lib/utils/filtering/index.ts +58 -6
- package/lib/utils/headerFilterLocale.ts +37 -0
- package/lib/utils/scroll/index.ts +5 -3
- package/lib/utils/sorting/index.ts +141 -26
- package/lib/utils/values.ts +10 -6
- package/llms.txt +4 -2
- package/package.json +4 -2
|
@@ -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)
|
|
@@ -33,7 +33,7 @@ The grid calculates which rows are visible based on:
|
|
|
33
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
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
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
|
|
36
|
+
- ⚠️ Not millions of rows at the default height: browsers cap an element's height. See [The browser height limit](#the-browser-height-limit) for how many rows fit and what to use beyond that.
|
|
37
37
|
|
|
38
38
|
### Column Virtualization
|
|
39
39
|
|
|
@@ -89,6 +89,20 @@ In development builds the grid logs a `console.warn` when it is rendering every
|
|
|
89
89
|
|
|
90
90
|
Pagination can hide this problem, because a paginated grid only has one page of rows to render. Row grouping switches pagination off (see below), so an unbounded container that looked fine with pagination can freeze the tab once grouping is enabled.
|
|
91
91
|
|
|
92
|
+
### The browser height limit
|
|
93
|
+
|
|
94
|
+
**How many rows can one scrolling grid hold?**
|
|
95
|
+
|
|
96
|
+
| Rows (at the default 52 px) | Use |
|
|
97
|
+
| :--- | :--- |
|
|
98
|
+
| up to about 300,000 | client-side rows: they fit in every browser |
|
|
99
|
+
| up to about 600,000 | client-side rows in Chromium and WebKit only; Firefox users cannot scroll past about 344,000 |
|
|
100
|
+
| more | a [`dataSource`](data-source.md) (server-side pages or infinite scroll), or `pagination` |
|
|
101
|
+
|
|
102
|
+
`density="compact"` (32 px rows) stretches each limit by about 1.6×: about 559,000 rows in Firefox and 1,048,000 in Chromium and WebKit.
|
|
103
|
+
|
|
104
|
+
Why: the grid lays every row out at its real height (`rowCount × rowHeight` pixels) and does no scroll scaling, and browsers cap an element's 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 52 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). In development the grid warns when rows are cut off, names how many can be scrolled to and points to `dataSource`. Measured reach per browser: [Performance → Reach](../performance.md#reach-the-browser-height-ceiling).
|
|
105
|
+
|
|
92
106
|
## Configuration
|
|
93
107
|
|
|
94
108
|
### Row Height
|
|
@@ -243,7 +257,7 @@ You should see console logs only for visible rows + overscan buffer.
|
|
|
243
257
|
## Known Limitations
|
|
244
258
|
|
|
245
259
|
1. **Unbounded containers** - Virtualization needs a bounded viewport height (see above)
|
|
246
|
-
2. **Browser height cap** - Rows past roughly `33,554,
|
|
260
|
+
2. **Browser height cap** - Rows past roughly `33,554,428 / rowHeight` (Chromium and WebKit; less in Firefox) cannot be scrolled to ([see above](#the-browser-height-limit))
|
|
247
261
|
3. **Uniform row heights** - Only detail panels vary the height; cumulative heights are recomputed over all rows when panels change
|
|
248
262
|
4. **Horizontal scrolling** - May show brief flicker with very wide grids
|
|
249
263
|
|
package/docs/performance.md
CHANGED
|
@@ -87,12 +87,14 @@ import '@opencorestack/opengridx/styles';
|
|
|
87
87
|
|
|
88
88
|
## Know the browser's height limit
|
|
89
89
|
|
|
90
|
+
In short: up to about 300,000 rows at 52 px fit in every browser, up to about 600,000 in Chromium and WebKit; beyond that load rows with a [`dataSource`](features/data-source.md) or use pagination. The decision guide is in [Virtualization → The browser height limit](features/virtualization.md#the-browser-height-limit).
|
|
91
|
+
|
|
90
92
|
The grid sizes its scroll content to `rowCount × rowHeight` pixels and does not scale scroll positions. Browsers cap an element's height, and the [benchmark](#reach-the-browser-height-ceiling) measures what happens past the cap:
|
|
91
93
|
|
|
92
94
|
- **Chromium and WebKit** clamp the content at 33,554,428 px. Rows below that cannot be scrolled to: at the default 52 px row height that is every row past 645,277.
|
|
93
95
|
- **Firefox** has a lower limit, about 17.9M px, and collapses a taller element instead of clamping it. Up to 3.5.0 that made a grid with more content unscrollable in Firefox (only the first screen of rows was reachable). **Since 3.5.1 the grid measures the browser's limit once and caps its scroll content to it**, so Firefox scrolls normally and reaches the rows above the cap: about 344,000 rows at 52 px, 559,000 at 32 px.
|
|
94
96
|
|
|
95
|
-
In development the grid warns when rows are cut off, naming how many can be scrolled to in that browser
|
|
97
|
+
In development the grid warns when rows are cut off, naming how many can be scrolled to in that browser and pointing to `dataSource`. For more rows, use a [`dataSource`](features/data-source.md) (`paginationMode="server"` or infinite scroll), `pagination`, or a smaller `rowHeight` (`density="compact"`, 32 px, keeps 1M rows under the Chromium and WebKit cap).
|
|
96
98
|
|
|
97
99
|
## Use server-side operations for very large datasets
|
|
98
100
|
|
|
@@ -125,17 +127,39 @@ In server modes the grid shows `rows` as given: it does not re-filter, re-sort o
|
|
|
125
127
|
|
|
126
128
|
Measured with `npm run bench` (`bench/`), which installs the packed npm package into a production Vite + React 19 app and drives it with Playwright in Chromium, Firefox and WebKit. The numbers below are from one machine (Apple M1, 8 GB RAM): compare them with each other and with your own runs, not with other machines.
|
|
127
129
|
|
|
128
|
-
**In short** (Chromium, median):
|
|
130
|
+
**In short** (Chromium, median; 100k and 1M rows from the run below, 10k and 500k from the 3.5.0 run of 2026-10-09, before the faster sort, quick filter and grouping):
|
|
129
131
|
|
|
130
132
|
| Rows | Mount | Sort (number / string) | Filter | Quick filter | Group + sum | Scroll frames over 16.7 ms | JS heap |
|
|
131
133
|
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
132
|
-
| 10,000 | 79 ms | 34 / 42 ms | 19 ms | 52 ms | 30 ms | 0 % | 2.9 MB |
|
|
133
|
-
| 100,000 |
|
|
134
|
-
| 500,000 | 295 ms | 493 / 641 ms | 59 ms | 958 ms | 331 ms | 0 % | 39.4 MB |
|
|
135
|
-
| 1,000,000 |
|
|
134
|
+
| 10,000 (3.5.0) | 79 ms | 34 / 42 ms | 19 ms | 52 ms | 30 ms | 0 % | 2.9 MB |
|
|
135
|
+
| 100,000 | 75 ms | 49 / 66 ms | 20 ms | 165 ms | 74 ms | 0 % | 10.4 MB |
|
|
136
|
+
| 500,000 (3.5.0) | 295 ms | 493 / 641 ms | 59 ms | 958 ms | 331 ms | 0 % | 39.4 MB |
|
|
137
|
+
| 1,000,000 | 621 ms | 467 / 467 ms | 109 ms | 1,813 ms | 693 ms | 0 % | 76.9 MB |
|
|
138
|
+
|
|
139
|
+
### Before and after the faster sort, quick filter and grouping (unreleased)
|
|
140
|
+
|
|
141
|
+
Medians of 5 runs, the same machine, the 3.5.1 code (`122d431`) and the changed code (`e1df2f1`) measured back to back on 2026-10-12 (`bench/results/2026-10-12-apple-m1-darwin-before.json` and `bench/results/2026-10-12-apple-m1-darwin.json`). Load average at the start of each run: 2.9 (before) and 4.6 (after); an IDE, a browser and other tools were open. Row counts after every filter and grouping were identical in both runs.
|
|
142
|
+
|
|
143
|
+
| 1M rows, 52 px | Chromium before → after | Firefox before → after | WebKit before → after |
|
|
144
|
+
| :--- | ---: | ---: | ---: |
|
|
145
|
+
| Sort, number column | 1,716 → 467 ms | 1,935 → 468 ms | 1,126 → 340 ms |
|
|
146
|
+
| Sort, string column | 1,510 → 467 ms | 2,338 → 560 ms | 2,939 → 562 ms |
|
|
147
|
+
| Quick filter, one term (first keystroke) | 2,127 → 1,813 ms | 3,283 → 2,789 ms | 3,646 → 3,166 ms |
|
|
148
|
+
| Group by region + `sum` | 924 → 693 ms | 1,879 → 1,776 ms | 818 → 675 ms |
|
|
149
|
+
| Filter, number `>` | 144 → 97 ms | 203 → 125 ms | 142 → 89 ms |
|
|
150
|
+
| Mount | 804 → 621 ms | 1,216 → 925 ms | 620 → 547 ms |
|
|
151
|
+
|
|
152
|
+
| 100k rows, 52 px | Chromium before → after | Firefox before → after | WebKit before → after |
|
|
153
|
+
| :--- | ---: | ---: | ---: |
|
|
154
|
+
| Sort, number column | 99 → 49 ms | 153 → 74 ms | 120 → 87 ms |
|
|
155
|
+
| Sort, string column | 154 → 66 ms | 193 → 75 ms | 202 → 91 ms |
|
|
156
|
+
| Quick filter, one term | 207 → 165 ms | 372 → 287 ms | 294 → 282 ms |
|
|
157
|
+
| Group by region + `sum` | 79 → 74 ms | 108 → 78 ms | 103 → 107 ms |
|
|
158
|
+
|
|
159
|
+
What changed: sort keys are read once per row into typed arrays and strings are ranked by sorting only their distinct values with the collator (the collator no longer runs in every comparison, which was most of WebKit's string sort); the row heights after a sort or filter are computed in one pass without id lookups when no detail panel is open; cells of plain columns are read without allocating; the quick filter keeps its texts by row position for the rows array it searched and matches with plain loops; aggregation reads cells row by row. The quick filter's first keystroke is still dominated by reading, formatting (`valueFormatter`) and lower-casing every cell, and Firefox's grouping by allocation of per-row tree nodes and metadata; the Firefox runs at 1M rows varied widely (up to several seconds) in both runs.
|
|
136
160
|
|
|
137
161
|
- **Scrolling keeps up with the display at every size.** Chromium missed no frames; Firefox missed 0–2.4 %. The DOM holds about 19 row elements at rest (28 at 32 px) and at most 86 while scrolling fast, whatever the row count. WebKit scrolls steadily without missed frames too, but misses one or two frames each time the scroll position jumps far (the *during the jumps* rows below). Firefox's scroll numbers at 500k and 1M rows are not comparable: the grid cannot scroll there (see Reach).
|
|
138
|
-
- **Sorting, the quick filter and grouping run on the main thread in one piece.** Up to 100k rows they finish in a few frames; at 1M a sort blocks the page for about
|
|
162
|
+
- **Sorting, the quick filter and grouping run on the main thread in one piece.** Up to 100k rows they finish in a few frames; at 1M a sort blocks the page for about 0.35–0.6 s in every engine, grouping for 0.7 s (1.8 s in Firefox), and the first quick-filter keystroke for about 1.8–3 s. For sorting or searching a million rows interactively, use server-side sorting and filtering.
|
|
139
163
|
- **Reach:** past the browser's height cap rows cannot be scrolled to, and in Firefox the grid stops scrolling altogether. See [Reach](#reach-the-browser-height-ceiling).
|
|
140
164
|
|
|
141
165
|
### Method
|
|
@@ -154,155 +178,101 @@ Measured with `npm run bench` (`bench/`), which installs the packed npm package
|
|
|
154
178
|
### Results
|
|
155
179
|
|
|
156
180
|
<!-- bench:start -->
|
|
157
|
-
<!-- Generated by `node bench/report.mjs --write docs/performance.md` from bench/results/2026-10-
|
|
181
|
+
<!-- Generated by `node bench/report.mjs --write docs/performance.md` from bench/results/2026-10-12-apple-m1-darwin.json. Do not edit by hand. -->
|
|
158
182
|
|
|
159
183
|
- **Machine:** Apple M1, 8 cores, 8 GB RAM, macOS 26.5.1 (arm64)
|
|
160
184
|
- **Browsers:** Chromium 145.0.7632.6, Firefox 146.0.1, WebKit 26.0 (Playwright, headless, 1280×800 page)
|
|
161
|
-
- **Package:** @opencorestack/opengridx 3.5.
|
|
162
|
-
- **Date:** 2026-10-
|
|
185
|
+
- **Package:** @opencorestack/opengridx 3.5.1 (commit `e1df2f1`), production build
|
|
186
|
+
- **Date:** 2026-10-12; load average at start 4.61 / 5.66 / 5.46, at end 2.31 / 6.16 / 7.41
|
|
163
187
|
- **Runs:** median of 5 runs after one discarded warm-up run, with the min–max range in brackets
|
|
164
188
|
|
|
165
189
|
¹ Chromium only: Firefox and WebKit have no long-task entries, and the heap is read through the Chrome DevTools Protocol.
|
|
166
190
|
|
|
167
|
-
#### 10,000 rows, 52 px rows
|
|
168
|
-
|
|
169
|
-
| Metric | Chromium | Firefox | WebKit |
|
|
170
|
-
| :--- | ---: | ---: | ---: |
|
|
171
|
-
| Mount | 79 ms (59–88) | 84 ms (82–85) | 116 ms (110–128) |
|
|
172
|
-
| Sort, number column | 34 ms (28–38) | 44 ms (43–46) | 81 ms (72–85) |
|
|
173
|
-
| Sort, string column | 42 ms (22–45) | 56 ms (36–58) | 83 ms (76–108) |
|
|
174
|
-
| Filter, string `contains` | 19 ms (14–31) | 31 ms (26–40) | 51 ms (47–57) |
|
|
175
|
-
| Filter, number `>` | 18 ms (13–19) | 25 ms (24–28) | 37 ms (34–39) |
|
|
176
|
-
| Quick filter, one term | 52 ms (33–60) | 76 ms (68–83) | 96 ms (84–110) |
|
|
177
|
-
| Group by region + `sum` | 30 ms (22–37) | 41 ms (31–47) | 77 ms (72–126) |
|
|
178
|
-
| Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
|
|
179
|
-
| Scroll frame time p95 | 17.6 ms (17.5–17.6) | 17.6 ms (17.6–18.6) | 45.0 ms (43.0–46.0) |
|
|
180
|
-
| Scroll frame time p99 | 17.6 ms (17.6–17.7) | 33.3 ms (33.3–34.1) | 49.0 ms (47.0–58.0) |
|
|
181
|
-
| Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 1.7 % (1.4–4.2) | 8.6 % (7.6–9.7) |
|
|
182
|
-
| Scroll frames over 33 ms | 0.0 % | 0.0 % (0.0–0.7) | 6.5 % (5.4–7.1) |
|
|
183
|
-
| … over 16.7 ms while scrolling steadily | 0.0 % | 0.6 % (0.0–4.7) | 0.0 % |
|
|
184
|
-
| … over 16.7 ms during the jumps | 0.0 % (0.0–0.8) | 3.5 % (2.6–4.3) | 26.1 % (21.9–29.9) |
|
|
185
|
-
| Long tasks during scroll ¹ | 0 | – | – |
|
|
186
|
-
| Longest task during a sort ¹ | none | – | – |
|
|
187
|
-
| JS heap for the grid ¹ | 2.9 MB | – | – |
|
|
188
|
-
| Row elements in the DOM, end of scroll | 76 | 76 | 76 |
|
|
189
|
-
| Cells in the DOM, end of scroll | 912 | 912 | 912 |
|
|
190
|
-
| Row elements in the DOM, at rest | 19 | 19 | 19 |
|
|
191
|
-
| Reach: last row on screen at the bottom | all rows | all rows | all rows |
|
|
192
|
-
|
|
193
191
|
#### 100,000 rows, 52 px rows
|
|
194
192
|
|
|
195
193
|
| Metric | Chromium | Firefox | WebKit |
|
|
196
194
|
| :--- | ---: | ---: | ---: |
|
|
197
|
-
| Mount |
|
|
198
|
-
| Sort, number column |
|
|
199
|
-
| Sort, string column |
|
|
200
|
-
| Filter, string `contains` |
|
|
201
|
-
| Filter, number `>` |
|
|
202
|
-
| Quick filter, one term |
|
|
203
|
-
| Group by region + `sum` |
|
|
195
|
+
| Mount | 75 ms (68–131) | 142 ms (113–144) | 138 ms (126–194) |
|
|
196
|
+
| Sort, number column | 49 ms (42–64) | 74 ms (64–85) | 87 ms (70–92) |
|
|
197
|
+
| Sort, string column | 66 ms (61–87) | 75 ms (59–89) | 91 ms (81–126) |
|
|
198
|
+
| Filter, string `contains` | 20 ms (18–37) | 34 ms (21–56) | 47 ms (43–47) |
|
|
199
|
+
| Filter, number `>` | 16 ms (14–28) | 32 ms (24–37) | 30 ms (28–37) |
|
|
200
|
+
| Quick filter, one term | 165 ms (156–190) | 287 ms (266–346) | 282 ms (257–339) |
|
|
201
|
+
| Group by region + `sum` | 74 ms (70–77) | 78 ms (71–97) | 107 ms (94–148) |
|
|
204
202
|
| Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
|
|
205
|
-
| Scroll frame time p95 |
|
|
206
|
-
| Scroll frame time p99 |
|
|
207
|
-
| Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 2.
|
|
208
|
-
| Scroll frames over 33 ms | 0.0 % | 0.0 % (0.0–
|
|
209
|
-
| … over 16.7 ms while scrolling steadily | 0.0 % |
|
|
210
|
-
| … over 16.7 ms during the jumps | 0.0 % (0.0–0.8) |
|
|
203
|
+
| Scroll frame time p95 | 18.4 ms (18.2–18.6) | 17.6 ms (17.3–33.3) | 49.0 ms (44.0–62.0) |
|
|
204
|
+
| Scroll frame time p99 | 18.6 ms (18.6–18.7) | 33.3 ms (17.7–50.0) | 60.0 ms (53.0–92.0) |
|
|
205
|
+
| Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 2.7 % (0.7–7.6) | 11.8 % (8.1–20.8) |
|
|
206
|
+
| Scroll frames over 33 ms | 0.0 % | 0.0 % (0.0–1.5) | 7.4 % (6.6–10.1) |
|
|
207
|
+
| … over 16.7 ms while scrolling steadily | 0.0 % | 1.1 % (0.0–5.4) | 0.6 % (0.0–8.5) |
|
|
208
|
+
| … over 16.7 ms during the jumps | 0.0 % (0.0–0.8) | 5.3 % (1.7–15.1) | 38.3 % (24.4–65.3) |
|
|
211
209
|
| Long tasks during scroll ¹ | 0 | – | – |
|
|
212
|
-
| Longest task during a sort ¹ |
|
|
210
|
+
| Longest task during a sort ¹ | 59 ms (55–81) | – | – |
|
|
213
211
|
| JS heap for the grid ¹ | 10.4 MB | – | – |
|
|
214
212
|
| Row elements in the DOM, end of scroll | 76 | 76 | 76 |
|
|
215
213
|
| Cells in the DOM, end of scroll | 912 | 912 | 912 |
|
|
216
214
|
| Row elements in the DOM, at rest | 19 | 19 | 19 |
|
|
217
215
|
| Reach: last row on screen at the bottom | all rows | all rows | all rows |
|
|
218
216
|
|
|
219
|
-
#### 500,000 rows, 52 px rows
|
|
220
|
-
|
|
221
|
-
| Metric | Chromium | Firefox | WebKit |
|
|
222
|
-
| :--- | ---: | ---: | ---: |
|
|
223
|
-
| Mount | 295 ms (287–307) | 482 ms (473–505) | 402 ms (338–463) |
|
|
224
|
-
| Sort, number column | 493 ms (485–567) | 660 ms (629–936) | 483 ms (413–564) |
|
|
225
|
-
| Sort, string column | 641 ms (600–807) | 1,105 ms (1,045–1,188) | 1,407 ms (1,203–1,568) |
|
|
226
|
-
| Filter, string `contains` | 59 ms (51–67) | 63 ms (56–68) | 75 ms (72–87) |
|
|
227
|
-
| Filter, number `>` | 80 ms (70–111) | 100 ms (92–111) | 89 ms (77–101) |
|
|
228
|
-
| Quick filter, one term | 958 ms (836–976) | 1,565 ms (1,503–1,828) | 1,442 ms (1,275–1,619) |
|
|
229
|
-
| Group by region + `sum` | 331 ms (307–338) | 641 ms (419–675) | 386 ms (351–1,244) |
|
|
230
|
-
| Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
|
|
231
|
-
| Scroll frame time p95 | 17.6 ms (17.5–17.6) | 17.5 ms (17.5–17.6) | 45.0 ms (44.0–46.0) |
|
|
232
|
-
| Scroll frame time p99 | 17.7 ms (17.6–17.7) | 17.6 ms (17.6–17.7) | 49.0 ms (46.0–50.0) |
|
|
233
|
-
| Scroll frames over 16.7 ms | 0.0 % (0.0–0.7) | 0.0 % | 7.7 % (7.6–10.1) |
|
|
234
|
-
| Scroll frames over 33 ms | 0.0 % (0.0–0.3) | 0.0 % | 7.3 % (5.8–7.8) |
|
|
235
|
-
| … over 16.7 ms while scrolling steadily | 0.0 % | 0.0 % | 0.0 % (0.0–1.1) |
|
|
236
|
-
| … over 16.7 ms during the jumps | 0.0 % (0.0–1.8) | 0.0 % | 23.1 % (20.6–28.4) |
|
|
237
|
-
| Long tasks during scroll ¹ | 0 | – | – |
|
|
238
|
-
| Longest task during a sort ¹ | 635 ms (595–799) | – | – |
|
|
239
|
-
| JS heap for the grid ¹ | 39.4 MB | – | – |
|
|
240
|
-
| Row elements in the DOM, end of scroll | 76 | 22 | 76 |
|
|
241
|
-
| Cells in the DOM, end of scroll | 912 | 264 | 912 |
|
|
242
|
-
| Row elements in the DOM, at rest | 19 | 19 | 19 |
|
|
243
|
-
| Reach: last row on screen at the bottom | all rows | 24 (<0.1 %) | all rows |
|
|
244
|
-
|
|
245
217
|
#### 1,000,000 rows, 52 px rows
|
|
246
218
|
|
|
247
219
|
| Metric | Chromium | Firefox | WebKit |
|
|
248
220
|
| :--- | ---: | ---: | ---: |
|
|
249
|
-
| Mount |
|
|
250
|
-
| Sort, number column |
|
|
251
|
-
| Sort, string column |
|
|
252
|
-
| Filter, string `contains` |
|
|
253
|
-
| Filter, number `>` |
|
|
254
|
-
| Quick filter, one term | 1,
|
|
255
|
-
| Group by region + `sum` |
|
|
256
|
-
| Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
|
|
257
|
-
| Scroll frame time p95 |
|
|
258
|
-
| Scroll frame time p99 |
|
|
259
|
-
| Scroll frames over 16.7 ms | 0.
|
|
260
|
-
| Scroll frames over 33 ms | 0.0 % | 0.
|
|
261
|
-
| … over 16.7 ms while scrolling steadily | 0.0 % |
|
|
262
|
-
| … over 16.7 ms during the jumps | 0.
|
|
221
|
+
| Mount | 621 ms (522–1,024) | 925 ms (861–2,775) | 547 ms (490–616) |
|
|
222
|
+
| Sort, number column | 467 ms (376–546) | 468 ms (430–1,287) | 340 ms (304–690) |
|
|
223
|
+
| Sort, string column | 467 ms (415–530) | 560 ms (433–1,257) | 562 ms (461–639) |
|
|
224
|
+
| Filter, string `contains` | 109 ms (92–113) | 89 ms (80–185) | 95 ms (89–194) |
|
|
225
|
+
| Filter, number `>` | 97 ms (90–123) | 125 ms (102–434) | 89 ms (81–121) |
|
|
226
|
+
| Quick filter, one term | 1,813 ms (1,430–2,183) | 2,789 ms (2,644–7,252) | 3,166 ms (2,877–3,463) |
|
|
227
|
+
| Group by region + `sum` | 693 ms (599–818) | 1,776 ms (1,176–3,715) | 675 ms (614–854) |
|
|
228
|
+
| Scroll frame time p50 | 16.7 ms | 16.7 ms (16.7–50.0) | 17.0 ms |
|
|
229
|
+
| Scroll frame time p95 | 18.4 ms (18.4–18.5) | 33.3 ms (17.4–166.6) | 44.0 ms (44.0–46.0) |
|
|
230
|
+
| Scroll frame time p99 | 18.6 ms (18.6–33.3) | 34.1 ms (33.3–283.4) | 47.0 ms (46.0–52.0) |
|
|
231
|
+
| Scroll frames over 16.7 ms | 0.3 % (0.0–1.0) | 9.5 % (1.7–79.5) | 7.7 % (7.2–9.0) |
|
|
232
|
+
| Scroll frames over 33 ms | 0.0 % (0.0–0.3) | 0.4 % (0.0–54.2) | 7.2 % (6.8–7.9) |
|
|
233
|
+
| … over 16.7 ms while scrolling steadily | 0.0 % | 6.5 % (0.0–75.0) | 0.0 % (0.0–0.6) |
|
|
234
|
+
| … over 16.7 ms during the jumps | 0.8 % (0.0–2.8) | 14.3 % (4.3–94.7) | 22.6 % (20.4–26.4) |
|
|
263
235
|
| Long tasks during scroll ¹ | 0 | – | – |
|
|
264
|
-
| Longest task during a sort ¹ |
|
|
265
|
-
| JS heap for the grid ¹ |
|
|
266
|
-
| Row elements in the DOM, end of scroll | 77 |
|
|
267
|
-
| Cells in the DOM, end of scroll | 924 |
|
|
236
|
+
| Longest task during a sort ¹ | 462 ms (410–523) | – | – |
|
|
237
|
+
| JS heap for the grid ¹ | 76.9 MB | – | – |
|
|
238
|
+
| Row elements in the DOM, end of scroll | 77 | 76 | 77 |
|
|
239
|
+
| Cells in the DOM, end of scroll | 924 | 912 | 924 |
|
|
268
240
|
| Row elements in the DOM, at rest | 19 | 19 | 19 |
|
|
269
|
-
| Reach: last row on screen at the bottom | 645,277 (64.5 %) |
|
|
241
|
+
| Reach: last row on screen at the bottom | 645,277 (64.5 %) | 344,139 (34.4 %) | 645,277 (64.5 %) |
|
|
270
242
|
|
|
271
243
|
#### 1,000,000 rows, 32 px rows
|
|
272
244
|
|
|
273
245
|
| Metric | Chromium | Firefox | WebKit |
|
|
274
246
|
| :--- | ---: | ---: | ---: |
|
|
275
|
-
| Mount |
|
|
276
|
-
| Sort, number column |
|
|
277
|
-
| Sort, string column |
|
|
278
|
-
| Filter, string `contains` |
|
|
279
|
-
| Filter, number `>` |
|
|
280
|
-
| Quick filter, one term | 1,
|
|
281
|
-
| Group by region + `sum` |
|
|
282
|
-
| Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
|
|
283
|
-
| Scroll frame time p95 | 17.
|
|
284
|
-
| Scroll frame time p99 | 17.
|
|
285
|
-
| Scroll frames over 16.7 ms | 0.
|
|
286
|
-
| Scroll frames over 33 ms | 0.0 % | 0.0
|
|
287
|
-
| … over 16.7 ms while scrolling steadily | 0.0 % (0.0–0.6) |
|
|
288
|
-
| … over 16.7 ms during the jumps | 0.
|
|
289
|
-
| Long tasks during scroll ¹ | 0 | – | – |
|
|
290
|
-
| Longest task during a sort ¹ |
|
|
291
|
-
| JS heap for the grid ¹ |
|
|
292
|
-
| Row elements in the DOM, end of scroll | 86 |
|
|
293
|
-
| Cells in the DOM, end of scroll | 1,032 |
|
|
247
|
+
| Mount | 741 ms (581–1,283) | 1,356 ms (968–5,966) | 475 ms (472–553) |
|
|
248
|
+
| Sort, number column | 453 ms (403–523) | 627 ms (441–917) | 327 ms (321–338) |
|
|
249
|
+
| Sort, string column | 497 ms (437–673) | 693 ms (489–6,628) | 504 ms (379–566) |
|
|
250
|
+
| Filter, string `contains` | 106 ms (96–162) | 192 ms (147–268) | 100 ms (97–108) |
|
|
251
|
+
| Filter, number `>` | 103 ms (86–136) | 179 ms (154–635) | 91 ms (87–190) |
|
|
252
|
+
| Quick filter, one term | 1,621 ms (1,617–2,569) | 4,265 ms (3,490–10,444) | 3,082 ms (2,684–3,608) |
|
|
253
|
+
| Group by region + `sum` | 880 ms (641–1,137) | 2,096 ms (957–3,309) | 640 ms (579–723) |
|
|
254
|
+
| Scroll frame time p50 | 16.7 ms | 16.7 ms (16.7–49.4) | 17.0 ms |
|
|
255
|
+
| Scroll frame time p95 | 17.6 ms (17.4–17.6) | 66.7 ms (33.2–199.3) | 49.0 ms (48.0–51.0) |
|
|
256
|
+
| Scroll frame time p99 | 17.7 ms (17.6–33.3) | 100.0 ms (33.7–333.3) | 52.0 ms (50.0–55.0) |
|
|
257
|
+
| Scroll frames over 16.7 ms | 0.7 % (0.0–1.0) | 38.4 % (6.7–82.7) | 15.5 % (15.4–15.8) |
|
|
258
|
+
| Scroll frames over 33 ms | 0.0 % (0.0–0.7) | 18.6 % (0.0–50.6) | 7.8 % (7.7–8.8) |
|
|
259
|
+
| … over 16.7 ms while scrolling steadily | 0.0 % (0.0–0.6) | 28.0 % (1.1–80.3) | 0.0 % (0.0–0.6) |
|
|
260
|
+
| … over 16.7 ms during the jumps | 0.8 % (0.0–2.6) | 63.5 % (16.5–96.4) | 52.0 % (50.6–57.4) |
|
|
261
|
+
| Long tasks during scroll ¹ | 0 (0–1) | – | – |
|
|
262
|
+
| Longest task during a sort ¹ | 490 ms (429–655) | – | – |
|
|
263
|
+
| JS heap for the grid ¹ | 77.3 MB | – | – |
|
|
264
|
+
| Row elements in the DOM, end of scroll | 86 | 86 | 86 |
|
|
265
|
+
| Cells in the DOM, end of scroll | 1,032 | 1,032 | 1,032 |
|
|
294
266
|
| Row elements in the DOM, at rest | 28 | 28 | 28 |
|
|
295
|
-
| Reach: last row on screen at the bottom | all rows |
|
|
267
|
+
| Reach: last row on screen at the bottom | all rows | 559,224 (55.9 %) | all rows |
|
|
296
268
|
|
|
297
269
|
#### Reach: rows that can be scrolled into view
|
|
298
270
|
|
|
299
271
|
| Rows × row height | Content height | Chromium | Firefox | WebKit |
|
|
300
272
|
| :--- | ---: | ---: | ---: | ---: |
|
|
301
|
-
| 10,000 × 52 px | 520,000 px | all | all | all |
|
|
302
273
|
| 100,000 × 52 px | 5,200,000 px | all | all | all |
|
|
303
|
-
|
|
|
304
|
-
| 1,000,000 ×
|
|
305
|
-
| 1,000,000 × 32 px | 32,000,000 px | all | 33 (<0.1 %) | all |
|
|
274
|
+
| 1,000,000 × 52 px | 52,000,000 px | 645,277 (64.5 %) | 344,139 (34.4 %) | 645,277 (64.5 %) |
|
|
275
|
+
| 1,000,000 × 32 px | 32,000,000 px | all | 559,224 (55.9 %) | all |
|
|
306
276
|
<!-- bench:end -->
|
|
307
277
|
|
|
308
278
|
### Reach: the browser height ceiling
|
package/docs/roadmap.md
CHANGED
|
@@ -90,8 +90,8 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
90
90
|
### Across the quarter
|
|
91
91
|
|
|
92
92
|
* ✅ **Published benchmark** (done 9 Oct): reproducible 10k / 100k / 500k / 1M-row numbers (scroll frame times, sort, filter and grouping time, memory, the reachable row) in [`docs/performance.md`](performance.md#benchmarks); rerun with `npm run bench`.
|
|
93
|
-
* **Next.js App Router guide
|
|
94
|
-
* **RTL check
|
|
93
|
+
* ✅ **Next.js App Router guide** (done 12 Oct): [`docs/features/nextjs.md`](features/nextjs.md), verified with `next build` / `next start` on Next.js 15.5 and 16.4. The ES bundle now starts with `'use client'`.
|
|
94
|
+
* ✅ **RTL check** (done 12 Oct): [`docs/features/rtl.md`](features/rtl.md). Fixed: text alignment, horizontal scrolling and column virtualization, arrow keys, the resize and fill handles, range outlines and auto-scroll, toolbar panel placement. Pinned columns remain a limitation (Q1 2027).
|
|
95
95
|
* **Demo site**: source viewer on all examples (`DocsLayout` with Vite `?raw`) and syntax highlighting, done alongside each release's demo page.
|
|
96
96
|
|
|
97
97
|
### Definition of done for each feature
|
|
@@ -117,6 +117,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
117
117
|
|
|
118
118
|
## 🔭 Next (Q1 2027)
|
|
119
119
|
|
|
120
|
+
|
|
120
121
|
* **Row reordering inside tree data and row groups**, touch-friendly (MUI #4821, 405 👍, not in MUI X).
|
|
121
122
|
* **Find & highlight** (Ctrl+F through cells), **right-click context menu**, **Excel-style value (set) filter**.
|
|
122
123
|
* **Calculated columns** (`[price] * [qty]` defined by users).
|
|
@@ -124,6 +125,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
124
125
|
* **Sparklines** and a charts adapter that passes grid state to any chart library.
|
|
125
126
|
* **AI toolkit, part 3**: smart paste (unstructured text → pending rows) and `apiRef.getAiContext()` for "summarise selection"; an `opengridx-mcp` docs server for coding assistants.
|
|
126
127
|
* **Tailwind / shadcn theme preset**, sticky header against page scroll.
|
|
128
|
+
* ✅ **RTL, part 2** (done 12 Oct, [`rtl.md`](features/rtl.md)): pinned columns and pinned system columns with logical sticky offsets and mirrored shadows, column menu pinning by screen side, mirrored pagination arrows and tree chevrons, a direction change on a mounted grid, column reorder by drag checked. Remaining: hard-coded English menu labels (no `localeText` yet), and the bidi-mirrored header filter operator symbols.
|
|
127
129
|
|
|
128
130
|
### Not planned
|
|
129
131
|
|
package/lib/ai/promptHandler.ts
CHANGED
|
@@ -50,18 +50,22 @@ function emptyResult(errors: GridAiValidationError[], message?: string): GridAiP
|
|
|
50
50
|
/** Validates a model reply into a branded result. Never throws. */
|
|
51
51
|
export function readModelReply(reply: unknown, columns: readonly GridAiColumn[], parts: GridAiPartOptions): GridAiPromptResult {
|
|
52
52
|
let json: unknown = reply;
|
|
53
|
-
let
|
|
53
|
+
let around = '';
|
|
54
54
|
if (typeof reply === 'string') {
|
|
55
55
|
const text = readModelText(reply);
|
|
56
56
|
if (text.json === null) return emptyResult([], text.message || undefined);
|
|
57
|
-
|
|
58
|
-
message
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
around = text.message;
|
|
58
|
+
// Parsed here so a `message` key inside the JSON is read as for an object reply. Bad JSON is
|
|
59
|
+
// passed on as text: validateGridAiState reports it.
|
|
60
|
+
try {
|
|
61
|
+
json = JSON.parse(text.json) as unknown;
|
|
62
|
+
} catch {
|
|
63
|
+
json = text.json;
|
|
64
|
+
}
|
|
64
65
|
}
|
|
66
|
+
// A reply such as { filterModel, message: 'Showing paid orders' }: the validator skips the message.
|
|
67
|
+
const inner = isPlainObject(json) && hasOwn(json, 'message') && typeof json.message === 'string' ? json.message.trim() : '';
|
|
68
|
+
const message = [around, inner].filter(Boolean).join('\n\n');
|
|
65
69
|
const result: GridAiPromptResult = validateGridAiState(json, columns, parts);
|
|
66
70
|
if (message) result.message = message;
|
|
67
71
|
return result;
|
package/lib/ai/validate.ts
CHANGED
|
@@ -241,8 +241,8 @@ function readPart(ctx: Ctx, part: GridAiPart, raw: unknown, path: string, state:
|
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
243
|
* Checks a model's reply against the columns and returns the state that is safe to apply.
|
|
244
|
-
* `json` is an object or a JSON string (bad JSON gives one error and an empty state).
|
|
245
|
-
* parts, fields, operators and enum values are dropped, each with an error; values are coerced to
|
|
244
|
+
* `json` is an object or a JSON string (bad JSON gives one error and an empty state). A top-level
|
|
245
|
+
* string `message` (the model's reply text) is skipped, not reported. Unknown parts, fields, operators and enum values are dropped, each with an error; values are coerced to
|
|
246
246
|
* the column type (`"1,200"` → 1200, a date → `"YYYY-MM-DD"`); filter groups left empty are
|
|
247
247
|
* removed. Never throws, and never returns a field the columns do not allow.
|
|
248
248
|
*
|
|
@@ -283,6 +283,9 @@ function validate(json: unknown, columns: readonly GridAiColumn[], options: Grid
|
|
|
283
283
|
const part = ALL_PARTS.find((p) => PART_KEYS[p] === key);
|
|
284
284
|
if (key === 'schemaVersion') {
|
|
285
285
|
if (input[key] !== 1) fail(ctx, key, `Unsupported schemaVersion ${show(input[key])}`);
|
|
286
|
+
} else if (key === 'message') {
|
|
287
|
+
// The model's reply text next to the state (createGridAiPromptHandler shows it): not state.
|
|
288
|
+
if (typeof input[key] !== 'string') fail(ctx, key, `Expected text, got ${show(input[key])}`);
|
|
286
289
|
} else if (!part) fail(ctx, key, 'Unknown part');
|
|
287
290
|
else if (!allowed.includes(part)) fail(ctx, key, 'Part not allowed');
|
|
288
291
|
else readPart(ctx, part, input[key], key, state);
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
border: 1px solid var(--ogx-overlay-border, var(--ogx-grid-border-color));
|
|
22
22
|
border-radius: var(--ogx-border-radius-lg, 8px);
|
|
23
23
|
box-shadow: var(--ogx-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
|
|
24
|
-
font-
|
|
24
|
+
font-family: var(--ogx-overlay-font-family, inherit);
|
|
25
|
+
font-size: var(--ogx-overlay-font-size, var(--ogx-font-size-sm));
|
|
26
|
+
font-weight: var(--ogx-overlay-font-weight, inherit);
|
|
25
27
|
}
|
|
26
28
|
.ogx-ai-panel__header {
|
|
27
29
|
display: flex;
|
|
@@ -157,7 +159,7 @@
|
|
|
157
159
|
background: transparent;
|
|
158
160
|
color: var(--ogx-color-primary);
|
|
159
161
|
font: inherit;
|
|
160
|
-
text-align:
|
|
162
|
+
text-align: start;
|
|
161
163
|
cursor: pointer;
|
|
162
164
|
}
|
|
163
165
|
.ogx-toolbar__ai-btn {
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
display: flex;
|
|
4
4
|
align-items: center;
|
|
5
5
|
padding: var(--ogx-grid-cell-padding-y) var(--ogx-grid-cell-padding-x);
|
|
6
|
-
border-
|
|
6
|
+
border-inline-end: var(--ogx-grid-column-divider-width) solid var(--ogx-grid-column-divider-color, var(--ogx-grid-border-color));
|
|
7
|
+
font-family: var(--ogx-grid-cell-font-family, inherit);
|
|
7
8
|
font-size: var(--ogx-grid-cell-font-size);
|
|
9
|
+
font-weight: var(--ogx-grid-cell-font-weight, inherit);
|
|
8
10
|
color: var(--ogx-grid-row-text);
|
|
9
11
|
line-height: var(--ogx-grid-line-height);
|
|
10
12
|
white-space: nowrap;
|
|
@@ -52,6 +54,19 @@
|
|
|
52
54
|
.ogx__cell--range-right {
|
|
53
55
|
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
54
56
|
}
|
|
57
|
+
/* Right to left: `--range-left` marks the range's first column, which is on the right. */
|
|
58
|
+
.ogx__cell--range-left:dir(rtl) {
|
|
59
|
+
--ogx-range-edge-left: 0 0 0 0 transparent;
|
|
60
|
+
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
61
|
+
}
|
|
62
|
+
.ogx__cell--range-right:dir(rtl) {
|
|
63
|
+
--ogx-range-edge-right: 0 0 0 0 transparent;
|
|
64
|
+
--ogx-range-edge-left: inset 2px 0 0 0 var(--ogx-range-border);
|
|
65
|
+
}
|
|
66
|
+
.ogx__cell--range-left.ogx__cell--range-right:dir(rtl) {
|
|
67
|
+
--ogx-range-edge-left: inset 2px 0 0 0 var(--ogx-range-border);
|
|
68
|
+
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
69
|
+
}
|
|
55
70
|
/*
|
|
56
71
|
* Fill handle (v3.5): the square on the bottom-right cell of the range. The cell clips its overflow,
|
|
57
72
|
* so the 24px hit area lies inside the corner and the visible square sits in its corner.
|
|
@@ -59,7 +74,7 @@
|
|
|
59
74
|
*/
|
|
60
75
|
.ogx__cell-fill-handle {
|
|
61
76
|
position: absolute;
|
|
62
|
-
|
|
77
|
+
inset-inline-end: 0;
|
|
63
78
|
bottom: 0;
|
|
64
79
|
width: 24px;
|
|
65
80
|
height: 24px;
|
|
@@ -72,7 +87,7 @@
|
|
|
72
87
|
.ogx__cell-fill-handle::after {
|
|
73
88
|
content: '';
|
|
74
89
|
position: absolute;
|
|
75
|
-
|
|
90
|
+
inset-inline-end: 1px;
|
|
76
91
|
bottom: 1px;
|
|
77
92
|
width: 7px;
|
|
78
93
|
height: 7px;
|
|
@@ -101,7 +116,7 @@
|
|
|
101
116
|
}
|
|
102
117
|
.ogx__cell--align-left {
|
|
103
118
|
justify-content: flex-start;
|
|
104
|
-
text-align:
|
|
119
|
+
text-align: start;
|
|
105
120
|
}
|
|
106
121
|
.ogx__cell--align-center {
|
|
107
122
|
justify-content: center;
|
|
@@ -109,7 +124,7 @@
|
|
|
109
124
|
}
|
|
110
125
|
.ogx__cell--align-right {
|
|
111
126
|
justify-content: flex-end;
|
|
112
|
-
text-align:
|
|
127
|
+
text-align: end;
|
|
113
128
|
}
|
|
114
129
|
.ogx__cell--pinned-left,
|
|
115
130
|
.ogx__cell--pinned-right {
|
|
@@ -119,11 +134,11 @@
|
|
|
119
134
|
background-color: var(--ogx-grid-background);
|
|
120
135
|
}
|
|
121
136
|
.ogx__cell--pinned-right {
|
|
122
|
-
border-
|
|
137
|
+
border-inline-start: var(--ogx-grid-column-divider-width) solid var(--ogx-grid-column-divider-color, var(--ogx-grid-border-color));
|
|
123
138
|
}
|
|
124
139
|
/* When the columns are narrower than the grid, the free space goes before the right-pinned section. */
|
|
125
140
|
.ogx__cell--pinned-right-first {
|
|
126
|
-
margin-
|
|
141
|
+
margin-inline-start: auto;
|
|
127
142
|
}
|
|
128
143
|
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--pinned-left,
|
|
129
144
|
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--pinned-right {
|
|
@@ -146,7 +161,7 @@
|
|
|
146
161
|
content: '';
|
|
147
162
|
position: absolute;
|
|
148
163
|
top: 0;
|
|
149
|
-
|
|
164
|
+
inset-inline-end: -10px;
|
|
150
165
|
bottom: 0;
|
|
151
166
|
width: 10px;
|
|
152
167
|
pointer-events: none;
|
|
@@ -156,12 +171,19 @@
|
|
|
156
171
|
content: '';
|
|
157
172
|
position: absolute;
|
|
158
173
|
top: 0;
|
|
159
|
-
|
|
174
|
+
inset-inline-start: -10px;
|
|
160
175
|
bottom: 0;
|
|
161
176
|
width: 10px;
|
|
162
177
|
pointer-events: none;
|
|
163
178
|
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
164
179
|
}
|
|
180
|
+
/* Right to left: the start-pinned section is on the right, so its shadow falls on its left side. */
|
|
181
|
+
.ogx__cell--pinned-left-last:dir(rtl)::after {
|
|
182
|
+
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
183
|
+
}
|
|
184
|
+
.ogx__cell--pinned-right-first:dir(rtl)::before {
|
|
185
|
+
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
186
|
+
}
|
|
165
187
|
.ogx__cell--hidden {
|
|
166
188
|
visibility: hidden;
|
|
167
189
|
border: none;
|