diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ffb72de4..84273837 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -34,7 +34,7 @@ body:
id: version
attributes:
label: OpenGridX version
- placeholder: "3.7.0"
+ placeholder: "e.g. 3.9.0 (npm ls @opencorestack/opengridx)"
validations:
required: true
- type: dropdown
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dadc168e..7412406f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -69,6 +69,9 @@ jobs:
- name: Build library (fails if React is bundled)
run: npm run build:lib
+ - name: Package contents and size budget (no test artifacts; DataGrid alone within budget)
+ run: npm run check:package
+
- name: Package smoke suite (packed tarball in React 18 and 19 apps)
run: npm run test:smoke -- --skip-lib
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index 6f4c82a0..ce9713e9 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -47,6 +47,9 @@ jobs:
- name: Build library (fails if React is bundled)
run: npm run build:lib
+ - name: Package contents and size budget
+ run: npm run check:package
+
- name: Package smoke suite (packed tarball in React 18 and 19 apps)
run: npm run test:smoke -- --skip-lib
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ed0e7a3..fe74c95a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,23 @@
---
+## [3.9.1] — 2026-10-13
+
+### Fixed
+
+- **The npm package no longer ships the theme test baselines.** `lib/theme/__baseline__/` (three JSON files, 1.6 MB, added in 3.7.0) was published because `files` excluded tests, screenshots and snapshots but not that folder. Unpacked size 9.4 → 7.8 MB.
+- The demo's home page badge showed v3.7.0; it and the header now read the version from `package.json`.
+
+### Internal
+
+- **`npm run check:package`**, run in CI and before publishing: fails when `npm pack` would ship a test artifact or when `DataGrid` alone, minified and gzipped, grows past its budget (92 KB; 85.5 KB now).
+
+### Documentation
+
+- Roadmap brought up to date (3.5.1 – 3.9.1 shipped; new features on hold until the backlog of fixes, refactors and performance work is closed; the 4.0 feature-modules proposal). `docs/performance.md`: edit, paste, fill, group toggle and select-all timings at 1M rows and how to keep rows memoised. README: spreadsheet editing, Ask AI panel, `localeText`, RTL and Theme Builder in the feature list; bundle sizes re-measured. `rtl.md` and `components/pagination.md` no longer say their labels cannot be translated. Architecture notes on the column-window memo and the locale context. The wiki home links the newer feature pages.
+
+---
+
## [3.9.0] — 2026-10-13
### Added
diff --git a/CLAUDE.md b/CLAUDE.md
index d5330e85..41fec5cd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,7 +14,7 @@ This file is auto-loaded by Claude Code and other AI coding assistants. It provi
- **Build:** `npm run build:lib` produces the published `dist/opengridx.es.js` / `dist/opengridx.umd.js`. `npm run build` type-checks the lib and builds the **demo site**.
- **Published:** yes — on npm as `@opencorestack/opengridx` with external consumers. Treat public types and behaviour changes as semver-relevant.
- **Lint:** `npm run lint` (ESLint). Must pass before every commit.
-- **Current version:** 3.9.0
+- **Current version:** 3.9.1
---
@@ -157,6 +157,10 @@ The GitHub wiki is generated from `docs/` + `wiki/` by `scripts/build-wiki.mjs`
---
+## v3.9.1 — significant changes
+
+- **`npm run check:package`** (after `build:lib`; in `ci.yml` and `npm-publish.yml`): fails if `npm pack` would ship a test artifact (`*.test.*`, `__baseline__`, `__screenshots__`, `__snapshots__`, `__fixtures__`) and if `DataGrid` alone, minified with esbuild and gzipped, exceeds `DATAGRID_GZIP_BUDGET` (92 KB; 85.5 KB at 3.9.0). `files` in package.json works by exclusion: add a negation for any new kind of test folder under `lib/`. Raise the budget only in the commit that needs it
+
## v3.9.0 — significant changes
- **Rows stay memoised.** Every prop passed to `Row` must keep its identity unless that row changed: handlers go through the latest-ref pattern (`useGridRowInteractions`: stable `handleRowClick` / `handleSelectionChange`), per-row state is passed only to the row it concerns (`editingCell`, `focusedCellField`, `cellRange`), and `useGridVirtualization` builds `virtualColumns` in its own memo over column inputs only. `DataGrid.rowMemo.test.tsx` counts `Row` renders through `isCellEditable` (`Cell` is memoised, so `renderCell` would not show them)
@@ -189,7 +193,7 @@ The GitHub wiki is generated from `docs/` + `wiki/` by `scripts/build-wiki.mjs`
- **RTL**: read/write horizontal scroll only through `getScrollStart` / `setScrollStart` (`lib/utils/direction.ts`; scroll state is the offset from the inline start, never negative); map arrows with `logicalArrowKey(key, isRtl(el))`; panels with `getPanelPosition`. `pinnedColumns.left` / `right` are logical (inline start / end): sticky offsets use `insetInlineStart` / `insetInlineEnd` (jsdom: `style.getPropertyValue('inset-inline-start')`), borders `border-inline-*`, box-shadow x-offsets need `:dir(rtl)` mirrors. The column menu pins by screen side. `useGridScrollSync` observes `dir` mutations
- Testing RTL: Playwright's headless Chromium runs with `--hide-scrollbars`; with classic scrollbars and `scrollbar-gutter: stable`, RTL cannot reach `scrollLeft` 0 — compare against the scroll origin. Reproduce on macOS with `defaults write chrome-headless-shell AppleShowScrollBars Always`. The grid's `scrollbar-width` / `scrollbar-color` disable `::-webkit-scrollbar` in Chromium
- Header filter strings are in `localeText` (`headerFilter*`, resolved in `lib/utils/headerFilterLocale.ts`)
-- Not in this release: the time-sliced pipeline (Option A of `docs/superpowers/specs/2026-10-12-nonblocking-pipeline-proposal.md`) is held for 3.7.0 until its overhead is within ~20 %
+- Not in this release: the time-sliced pipeline (Option A of `docs/superpowers/specs/2026-10-12-nonblocking-pipeline-proposal.md`) is held (backlog) until its overhead is within ~20 %
## v3.5.1 — significant changes
diff --git a/README.md b/README.md
index bca3d17f..a1b5e388 100644
--- a/README.md
+++ b/README.md
@@ -76,11 +76,13 @@ Without a `height` prop the grid fills its container (`height: 100%`, or the fre
- **Fully Customizable**: Slots system for replacing any component (pagination, overlays, toolbar).
- **Export Functionality**: Built-in CSV, Excel (HTML `.xls`, or real `.xlsx` with the optional ExcelJS peer), JSON, PDF (optional jsPDF peer) and Print export.
- **Clipboard**: `Ctrl+C` / `Cmd+C` copies the selected rows' visible columns as TSV for Excel/Sheets.
+- **Spreadsheet editing** (v3.4–3.5): paste from Excel / Google Sheets into a cell range (parsed per column type, through `processRowUpdate`), Delete clears a range, **undo / redo** (`undoRedo`, Ctrl+Z / Ctrl+Shift+Z), and a **fill handle** that extends series, patterns or copies (Ctrl/Cmd+D / R).
- **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.
- **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`).
-- **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.
+- **AI Toolkit** (v3.4–3.5): `@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`); `aiAssistant` adds an **Ask AI panel** whose changes show as removable chips with one-click undo, and `createGridAgentTools` gives agents typed tools. No AI SDK, no row data sent by default.
+- **Internationalisation** (v3.9): every built-in string (menus, toolbar, panels, AI panel, pagination, screen-reader announcements) is a `localeText` key, and **right-to-left** layouts work with `dir="rtl"` (v3.6), including pinned columns.
- **Accessibility**: WCAG 2.1 AA — full ARIA roles and keyboard navigation.
-- **Theming**: CSS variable API, `DataGridThemeProvider` with 5 built-in themes (`darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compactTheme`) and custom themes.
+- **Theming**: CSS variable API, `DataGridThemeProvider` with 5 built-in themes (`darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compactTheme`) and custom themes; the demo's **Theme Builder** edits every theme key live and exports the result as code.
- **Column & Row Reordering**: Drag-and-drop column reordering and row reordering.
- **Inline Cell Editing**: Full inline editing with `processRowUpdate` validation.
- **State Persistence**: Save and restore grid state (sort, filter, pagination, columns, density) via `useGridStateStorage`.
@@ -120,7 +122,7 @@ This means that **Cursor**, **GitHub Copilot**, **Windsurf**, and other AI agent
| `rowHeight` | `number` | `52` | Row height in pixels. |
| `headerHeight` | `number` | `56` | Header height in pixels. |
| `noRowsLabel` | `string` | `'No Data'` | Custom empty-state message. |
-| `localeText` | `GridLocaleText` | — | Overrides for the pager and empty-state strings (i18n). |
+| `localeText` | `GridLocaleText` | — | Overrides for every built-in UI string (v3.9; pagination and the empty state before). |
| `ariaLabel` | `string` | — | Accessible name for the grid. |
| `initialState` | `GridInitialState` | — | Starting sort, filter, pagination, column and density state (uncontrolled). |
| `onStateChange` | `(state: GridState) => void` | — | Fires when sort, filter, pagination, columns or density change. |
@@ -490,12 +492,14 @@ Built-in themes: `darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compac
| Artifact | Size | Gzipped | Notes |
| :--- | :--- | :--- | :--- |
-| **Core ES Module** (`opengridx.es.js`) | 331 KB | **86 KB** | Use this — tree-shakeable |
-| **Core UMD** (`opengridx.umd.js`) | 233 KB | **73 KB** | CommonJS / CDN compat |
-| **Styles** (`opengridx.css`) | 65 KB | **10 KB** | `import '@opencorestack/opengridx/styles'` |
+| **What an app ships for `DataGrid`** (your bundler minifies) | 271 KB | **87 KB** | Measured v3.9.0: esbuild, minified, React external, `DataGrid` only |
+| **Core ES Module** (`opengridx.es.js`, as published) | 493 KB | 130 KB | Unminified with readable names; use this — tree-shakeable |
+| **Core UMD** (`opengridx.umd.js`) | 357 KB | 112 KB | CommonJS / CDN compat |
+| **Styles** (`opengridx.css`) | 81 KB | **12 KB** | `import '@opencorestack/opengridx/styles'` |
+| **AI toolkit** (`/ai`, optional import) | 20 KB | **7 KB** | Adds nothing unless imported |
| **ExcelJS** (optional peer dep) | — | — | `npm install exceljs` |
| **jsPDF** (optional peer dep) | — | — | `npm install jspdf jspdf-autotable` |
-| **npm package download** | — | **~1.1 MB** | Compressed tarball (includes `lib/` source, docs and source maps) |
+| **npm package download** | — | **~2.5 MB** | Compressed tarball (includes `lib/` source, docs and source maps; 7.8 MB unpacked) |
- **Tree-shaking Ready**: ES Module build — bundlers (Vite, Webpack) only include what you use.
- **Zero UI Dependencies**: No MUI, Ant Design, or Radix. Pure React + vanilla CSS.
diff --git a/demo/Home.tsx b/demo/Home.tsx
index d550f910..24376c2f 100644
--- a/demo/Home.tsx
+++ b/demo/Home.tsx
@@ -1,4 +1,5 @@
import './Home.css';
+import { version as packageVersion } from '../package.json';
interface HomeProps {
onNavigate?: (id: string) => void;
@@ -63,7 +64,7 @@ export default function Home(_props: HomeProps) {
- OpenGridX v3.7.0
+ OpenGridX v{packageVersion}
The elite, high-performance DataGrid for modern React.
Built to handle massive data with a premium developer experience.
diff --git a/demo/public/llms.txt b/demo/public/llms.txt
index 22d57684..5ed8822e 100644
--- a/demo/public/llms.txt
+++ b/demo/public/llms.txt
@@ -1,6 +1,6 @@
# OpenGridX — AI Agent Context
# Package: @opencorestack/opengridx
-# Version: 3.9.0
+# Version: 3.9.1
# Docs: https://opencorestack.github.io/OpenGridX/
# This file is machine-readable. AI developers: start here.
diff --git a/docs/architecture/datagrid-orchestration.md b/docs/architecture/datagrid-orchestration.md
index 56166cd4..330bb4f0 100644
--- a/docs/architecture/datagrid-orchestration.md
+++ b/docs/architecture/datagrid-orchestration.md
@@ -113,10 +113,12 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
`GridToolbarSlot`, `GridAiAssistantArea` (the `aiAssistant` panel or `slots.aiAssistantPanel` while open, anchored under the toolbar), `GridStandaloneColumnPanel`, `GridListView` (list view) or `GridViewport` (`components/DataGrid/GridViewport.tsx`, the `role="grid"` viewport:
`Header`, `GridPinnedRows` top, `GridEmptyState`, `GridVirtualRows`, `GridPinnedRows` bottom,
`GridAggregationFooter`), `GridCellSelectionStatsArea` (`showCellSelectionStats` with `cellSelection`), then `slots.footer` or `GridPaginationArea`, `GridLiveRegion`,
-`GridLoadingOverlay` and `GridErrorOverlay`. `GridRoot` builds the overlay slot elements and passes the
+`GridLoadingOverlay` and `GridErrorOverlay`. `GridRoot` also provides `GridLocaleContext` (v3.9) around all of them: `localeText`, kept stable while shallowly equal (`useShallowStableValue`), resolved once by `resolveGridLocale` (`lib/utils/gridLocale.ts`); components and `Row` read it with `useGridLocale()` instead of a prop, so `Row` gets no prop that changes per render. `GridRoot` builds the overlay slot elements and passes the
rest of its props on to `GridViewport`. The props all three row renderers take alike are built once in
`GridViewport` (`rowRenderProps`) and spread into each. `rowRenderProps.cellRange` is the cell range's
render rectangle; each row renderer turns it into a per-row `cellRange` (`getRowCellRange`), `null` for rows
outside it, and `Row` is memoised with a value comparison of that prop, so a drag only re-renders the rows the
-range enters or leaves. `rowRenderProps.fillHandle` (v3.5) adds `fillHandleCol` to the one row that draws the fill
+range enters or leaves. The same rule holds for every row prop (v3.9): `editingCell` goes only to the row being
+edited, `focusedCellField` only to the focused row, and the row click and checkbox handlers keep one identity
+(`useGridRowInteractions` reads the latest handlers when called). `DataGrid.rowMemo.test.tsx` guards it. `rowRenderProps.fillHandle` (v3.5) adds `fillHandleCol` to the one row that draws the fill
handle (a single selected cell gets an empty column interval, so it draws the handle but no range).
diff --git a/docs/architecture/use-grid-virtualization.md b/docs/architecture/use-grid-virtualization.md
index 17cc32be..30728bf2 100644
--- a/docs/architecture/use-grid-virtualization.md
+++ b/docs/architecture/use-grid-virtualization.md
@@ -90,6 +90,8 @@ Row heights include an expanded detail panel's height, so the prefix sums accoun
Columns use the same search on `unpinnedAccWidths` (also inclusive), relative to the left edge of the unpinned area (`scrollLeft - leftPinnedWidth`), widened by 6 columns on each side. The skipped widths become `__spacer_left__` / `__spacer_right__` entries (`isSpacer: true`) in `virtualColumns`.
+The column window is computed in its own `useMemo` (v3.9), over the column parts of the layout (`unpinnedAccWidths`, `unpinnedColsWithWidth`, the pinned column lists and widths), `scrollLeft` and the viewport width only. A row change (an edit), a vertical scroll or a row-height change therefore keeps the same `virtualColumns` array, and `Row`, which is memoised on its props, does not re-render for it. `useLayout` returns the row and column parts from separate memos (v3.7.1), so the column fields keep their identity while rows change.
+
When a column declares `colSpan` / `rowSpan`, `DataGrid` widens this window afterwards (`useGridSpanRowWindow` / `useGridSpanColumnWindow`) so a span whose origin is outside the window is not cut.
---
diff --git a/docs/components/pagination.md b/docs/components/pagination.md
index 4d892cee..93da7359 100644
--- a/docs/components/pagination.md
+++ b/docs/components/pagination.md
@@ -41,7 +41,7 @@ You can replace the internal pagination UI using the `pagination` slot:
## 🌍 Internationalisation
-Pass `localeText` on `` to override the text labels in the Pagination component (rows-per-page, range, and page-counter labels). The rows-per-page select takes its accessible name from the `paginationRowsPerPage` label. The navigation landmark label (`aria-label="Pagination"`) and the button labels and titles (`Go to next page`, `Next page`, …) are hardcoded and not overrideable via `localeText`.
+Pass `localeText` on `` to override the text labels in the Pagination component (rows-per-page, range, and page-counter labels). The rows-per-page select takes its accessible name from the `paginationRowsPerPage` label. Since v3.9 the navigation landmark label (`paginationLabel`, default `"Pagination"`) and each button's label and tooltip (`paginationNextPageLabel` / `paginationNextPageTooltip` → `"Go to next page"` / `"Next page"`, and the same for First, Previous and Last) are `localeText` keys as well.
| Key | Type | Default |
| :--- | :--- | :--- |
diff --git a/docs/features/rtl.md b/docs/features/rtl.md
index 42f6b1cd..a1edd0c2 100644
--- a/docs/features/rtl.md
+++ b/docs/features/rtl.md
@@ -42,7 +42,7 @@ The column menu is labelled by screen side: in RTL *Pin to Left* adds the column
## Known limitations
-- **Menu and panel labels are hard-coded English** ("Pin to Left", "Sort Ascending"); there is no `localeText` key for the column menu yet.
+- ~~Menu and panel labels are hard-coded English~~: since v3.9 every built-in string, including the column menu (`columnMenuPinLeft`, `columnMenuSortAscending`, …), is a `localeText` key. See *Locale text for every UI string* in [`API_REFERENCE.md`](../API_REFERENCE.md).
- **Header filter operator symbols** are mirrored by the browser's bidi rules (`∋` shows as `∈`), as in any RTL text.
- **`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`.
- **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`).
diff --git a/docs/performance.md b/docs/performance.md
index d6ac0c3c..86089b4d 100644
--- a/docs/performance.md
+++ b/docs/performance.md
@@ -136,7 +136,7 @@ Measured with `npm run bench` (`bench/`), which installs the packed npm package
| 500,000 (3.5.0) | 295 ms | 493 / 641 ms | 59 ms | 958 ms | 331 ms | 0 % | 39.4 MB |
| 1,000,000 | 621 ms | 467 / 467 ms | 109 ms | 1,813 ms | 693 ms | 0 % | 76.9 MB |
-### Before and after the faster sort, quick filter and grouping (unreleased)
+### Before and after the faster sort, quick filter and grouping (3.6.0)
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.
@@ -162,6 +162,24 @@ What changed: sort keys are read once per row into typed arrays and strings are
- **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.
- **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).
+### Editing, selection and grouping at 1M rows (3.8 – 3.9)
+
+Measured in Chromium on an Apple M1, 1,000,000 rows, from the event to the end of its long task (best of three; not part of `npm run bench`, which covers mount, scroll, sort and filter).
+
+| Interaction | 3.7.0 | 3.8.0 | |
+| :--- | ---: | ---: | :--- |
+| Edit one cell | 561 ms | ~35 ms | the row store is updated in place; id lists over the visible rows are reused |
+| Paste 1,000 × 10 cells | ~790 ms | ~45 ms | |
+| Undo / redo of that paste | ~790 ms | ~45 ms | |
+| Fill 1,000 cells | 809 ms | ~50 ms | |
+| Expand a group | 279 ms | ~135 ms | leaf row metadata built once per tree |
+| Collapse a group | ~120 ms | ~1 ms | |
+| Select all | ~270 ms | ~270 ms | bounded by building 1M-id sets for the array `rowSelectionModel` |
+
+3.9.0 keeps rows memoised: selecting a row, starting or committing an edit, and vertical scrolling re-render only the rows they change (a click re-rendered every visible row, now one). React's own work per interaction is about 3–5 ms at 100k rows.
+
+To keep this, change rows by passing new row objects (never mutate them), keep `columns` and `localeText` shallowly stable (inline objects of plain values are fine; define function entries outside the render). Handlers such as `onRowClick` may be inline: the grid passes rows its own stable handlers.
+
### Method
- **The app is the built package.** `bench/app` installs the tarball from `npm pack`, like a consumer, and is a production build (no React development checks).
diff --git a/docs/roadmap.md b/docs/roadmap.md
index ca9257d8..35665380 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -85,14 +85,32 @@ This document tracks the current status of features in `OpenGridX` and outlines
| **3.3.0** ✅ | shipped 7 Oct | **Cell range selection** (opt-in `cellSelection` prop): mouse drag, Shift+click, Shift+arrows; Ctrl+C / Cmd+C copies the block as TSV | Selection status bar (count, sum, average of selected cells); wrapped header text; React Compiler compatibility check |
| **3.4.0** ✅ | shipped 8 Oct | **Paste from Excel / Google Sheets** into the selection, through `processRowUpdate` (values parsed per column type or `valueParser`, non-editable cells skipped, `onClipboardPaste` summary); **Delete clears a range**; built-in inputs and editors use the shared `Input` / `Checkbox` (`Input variant="cell"`). **Undo / redo** for cell edits and pastes (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y, `apiRef.undo()` / `redo()`) | **AI toolkit, part 1** in a new `@opencorestack/opengridx/ai` entry point: `getGridAiSchema(columns)` (JSON Schema of the filter, sort, grouping, aggregation, pivot and visibility models the columns allow) and `validateGridAiState(json, columns)` (drops unknown fields and operators, coerces values, returns errors) |
| **3.5.0** ✅ | shipped 9 Oct | **AI toolkit, part 2**: `aiAssistant={{ onPrompt }}` Ask AI panel (chips, Undo, voice) that calls the app's own model through `createGridAiPromptHandler`; only validated replies apply; `createGridAgentTools(apiRef, columns)` (plain tool objects for CopilotKit, the Vercel AI SDK, provider tool calling or WebMCP) | **Header filter row** (`headerFilters`); **fill handle** (series, Alt copy, Ctrl/Cmd+D / R); `rowGroupingModel` controlled/uncontrolled pair |
+| **3.5.1** ✅ | shipped 10 Oct | **Content height capped to the browser's limit** (Firefox collapsed grids taller than ~17.9M px) | Dev warning names the reachable row count |
+| **3.6.0** ✅ | shipped 12 Oct | **Sorting 3–5× faster** (typed-array keys) | Next.js `'use client'` banner; optional peers safe in webpack / Turbopack; **RTL parts 1 and 2** (pinned columns); header filter `localeText` |
+| **3.7.0** ✅ | shipped 13 Oct | **Theme Builder** demo page + 16 theme keys | `DataGrid.tsx` split (`GridRoot` / `GridViewport`, composite hooks); types split per domain; demo smoke test |
+| **3.7.1** ✅ | shipped 13 Oct | Audit fixes: exports survive a throwing `valueGetter`; **formula escaping of `valueFormatter` text** (security) | Quick filter matches shown text; date paste fixes; list view theme; CSS reset scoped to the grid |
+| **3.8.0** ✅ | shipped 13 Oct | **Edits at 1M rows 10–20× faster** (in-place row store; edit ~35 ms, paste ~45 ms) | Group expand ~2×, collapse ~1 ms |
+| **3.8.1** ✅ | shipped 13 Oct | **Grid buttons keep their own styles** (AI panel, toolbar active states) | Exact `bigint` sort; stricter number text; dead-code cleanup; every export documented |
+| **3.9.0** ✅ | shipped 13 Oct | **`localeText` for every UI string** (~200 keys) | 9 theme keys + Theme Builder controls; rows stay memoised through selection and editing; demo sidebar by priority |
+| **3.9.1** ✅ | shipped 13 Oct | **npm package no longer ships 1.6 MB of test baselines** | `check:package` guard in CI; version badges read `package.json`; docs brought up to date |
+| 3.10.0 | open | AI change chips translatable | Backlog work below |
| Freeze | 19 Dec – 5 Jan | Bug fixes only | — |
+### Now: backlog first (feature hold since 13 Oct)
+
+New features (the *Next* list below) wait until the open fixes, refactors and performance work are done. Tracked in [`superpowers/plans/2026-10-13-audit-tracker.md`](superpowers/plans/2026-10-13-audit-tracker.md):
+
+* **Refactors** (no behaviour change): about 40 `as unknown as` casts (R4), files over 600 lines such as `GridToolbar.tsx` (R5), hooks with very wide parameter lists such as keyboard navigation (R6).
+* **Performance**: the time-sliced filter / sort pipeline (held: 1.5–2× slower in total, needs to be within ~20 %); select-all at 1M rows (~270 ms, bounded by the array `rowSelectionModel`).
+* **Demo**: syntax highlighting in the source viewer.
+* **Bundle size (4.0)**: an app using only `DataGrid` ships 87 KB gzipped; opt-in [feature modules](superpowers/specs/2026-10-13-feature-modules-proposal.md) would save ~20 KB. Breaking, so planned for 4.0.
+
### Across the quarter
* ✅ **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`.
* ✅ **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'`.
-* ✅ **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).
-* **Demo site**: source viewer on all examples (`DocsLayout` with Vite `?raw`) and syntax highlighting, done alongside each release's demo page.
+* ✅ **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 followed in RTL part 2.
+* **Demo site**: ✅ source viewer on every example (`DocsLayout` with Vite `?raw`); syntax highlighting still open.
### Definition of done for each feature
@@ -115,7 +133,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
* **Undo must cover rejected updates**: a `processRowUpdate` that throws or a paste that partly fails must leave the history consistent.
* **Wrong AI filters** that look plausible: the validator, visible chips and undo are required, not optional.
-## 🔭 Next (Q1 2027)
+## 🔭 Next (Q1 2027) — on hold until the backlog above is closed
* **Row reordering inside tree data and row groups**, touch-friendly (MUI #4821, 405 👍, not in MUI X).
@@ -125,7 +143,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
* **Sparklines** and a charts adapter that passes grid state to any chart library.
* **AI toolkit, part 3**: smart paste (unstructured text → pending rows) and `apiRef.getAiContext()` for "summarise selection"; an `opengridx-mcp` docs server for coding assistants.
* **Tailwind / shadcn theme preset**, sticky header against page scroll.
-* ✅ **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.
+* ✅ **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: the bidi-mirrored header filter operator symbols (the menu labels became `localeText` keys in 3.9).
### Not planned
@@ -136,6 +154,9 @@ This document tracks the current status of features in `OpenGridX` and outlines
## ✅ Implemented Features (Recent)
+### 3.3 – 3.9 *(2026-10-07 → 2026-10-13)*
+- Spreadsheet editing (range selection, paste, undo / redo, fill handle), the AI toolkit (schema, validator, Ask AI panel, agent tools), header filters, the 1M-row benchmark and height cap, 3–5× faster sorting, RTL, the Theme Builder, a full audit with security and correctness fixes, 10–20× faster edits at 1M rows and `localeText` for every string. See the release table above and [CHANGELOG.md](../CHANGELOG.md).
+
### 3.x releases *(2026-09-29 → 2026-10-01)*
- **3.0.0** correctness release (rows never modified, typed generics, React 18 support), **3.0.1** fill-height layout fix, **3.1.0** `sortComparator` and column auto-size, **3.2.0** non-blocking PDF export, **3.2.1** Safari boolean editor fix, **3.2.2** linear-time export regexes. Details in [CHANGELOG.md](../CHANGELOG.md); upgrade guide in [migration/v2-to-v3.md](migration/v2-to-v3.md).
- Package smoke suite against the packed tarball in React 18 and 19 apps, pull-request CI, community files and a wiki generated from these docs.
@@ -158,5 +179,5 @@ This document tracks the current status of features in `OpenGridX` and outlines
- **Core hook tests**: `useGridRowPipeline`, `useGridControlledState`, `useGridKeyboardNavigation` now have full test coverage (25 new tests).
- **`CLAUDE.md`**: Root-level AI context file auto-loaded by Claude Code and other AI coding assistants.
-* **Demo app (partial)**: 32 interactive examples across 6 categories (Main features, Advanced features, Components, Customization, Tutorials, Resources) with categorized sidebar navigation and per-page Table of Contents. Source code viewer present on 6 of 32 examples. Syntax highlighting and public deployment are in the upcoming Demo Site section above.
+* **Demo app**: 56 pages in 13 categories ordered by what developers look for first (3.9), a per-page table of contents, the source viewer on every example, and public deployment on GitHub Pages with a smoke test of every route. Syntax highlighting is still open.
* **Quickstart & Installation Guides**: Comprehensive onboarding documentation for developers.
diff --git a/docs/superpowers/plans/2026-10-13-audit-tracker.md b/docs/superpowers/plans/2026-10-13-audit-tracker.md
index 15295c7d..c482a22b 100644
--- a/docs/superpowers/plans/2026-10-13-audit-tracker.md
+++ b/docs/superpowers/plans/2026-10-13-audit-tracker.md
@@ -70,3 +70,4 @@ Baselines (Chromium, M1, 1M rows): edit 508 ms · paste/undo/redo ~790 ms · fil
- 2026-10-13 — P8 done (row memoisation), regression test proven to fail without the fixes.
- 2026-10-13 — R7 done (agent, reviewed): 196 localeText keys via GridLocaleContext; added useShallowStableValue so an inline localeText of strings keeps rows memoised. Unit 1972, browser 585/585 in 3 engines.
- 2026-10-13 — Released 3.9.0 (CI run 38381619839).
+- 2026-10-13 — 3.9.1 patch (PR flow): baseline exclusion, check:package, version badges, docs. Cherry-picked from release/3.10.0.
diff --git a/docs/superpowers/specs/2026-10-13-feature-modules-proposal.md b/docs/superpowers/specs/2026-10-13-feature-modules-proposal.md
new file mode 100644
index 00000000..2d97b31a
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-13-feature-modules-proposal.md
@@ -0,0 +1,50 @@
+# Proposal: opt-in feature modules (4.0)
+
+Status: proposal, not scheduled. Replaces audit item P9 ("lazy or modular opt-in features").
+
+## Why
+
+An app that imports only `DataGrid` ships 87–88 KB gzipped (271 KB minified, esbuild, React external; measured 2026-10-13 on 3.9.0). Much of it is features the app may never turn on.
+
+Measured share of the minified bundle (esbuild metafile, `DataGrid`-only entry):
+
+| Part | Minified | Wired how |
+| :--- | :--- | :--- |
+| Cell selection | 10.0 KB | hook in the core hook order |
+| Header filter row (+ utils) | 11.3 KB | component, visible at mount |
+| AI assistant panel + chip logic + hook | ~13 KB | hook + panel |
+| Fill handle | 4.6 KB | hook |
+| Paste | 4.3 KB | hook |
+| Pivot logic | 4.5 KB | hook + utils |
+| Spanning | 4.2 KB | hook |
+| Undo / redo | 2.9 KB | hook |
+| Clipboard | 2.7 KB | hook |
+| Columns panel | 3.4 KB | component, on demand |
+
+Roughly 60 KB minified (~20 KB gzipped, ~23 %) is opt-in functionality.
+
+## Options considered
+
+1. **Lazy-load the on-demand UI** (`React.lazy` for the AI panel, columns panel, pivot): ~6 KB gzipped once the header filter row is excluded (it renders at mount, so lazy loading would flicker). Costs: a loading moment on first open, extra chunk files, no gain for the UMD build, SSR edge cases. **Rejected for now**: too little gain for the risk.
+2. **Feature modules (recommended for 4.0).** Features register through an explicit list, so a bundler drops what is not imported:
+
+ ```tsx
+ import { DataGrid } from '@opencorestack/opengridx';
+ import { cellSelection, fillHandle, undoRedo } from '@opencorestack/opengridx/features';
+
+
+ ```
+
+ Each module brings its hook (called at its fixed slot in the hook order) and its components. A grid without the module ignores the related props, with a development warning naming the missing module.
+3. **Separate entry points per feature without registration**: still needs the core to reach the hooks, so it reduces to option 2.
+
+## Constraints for option 2
+
+- **Hook order stays fixed.** `DataGrid` keeps one call site per feature slot and calls either the module's hook or a no-op of the same shape. A module may never add or remove hook calls between renders: the `features` list must be fixed for the grid's lifetime (development error if it changes).
+- `apiRef` methods of a missing feature exist and warn (they are part of `GridApi`'s type).
+- Breaking: apps using these features must add the module. A 3.x compatibility entry (`@opencorestack/opengridx/all`) registers every module, so migration is one import.
+- The package smoke suite needs a "minimal" fixture that asserts the bundle size, so a later change cannot pull a module back into the core.
+
+## Expected result
+
+~20 KB gzipped less for an app that uses none of the modules (87 → ~67 KB), measured the same way.
diff --git a/llms.txt b/llms.txt
index 22d57684..5ed8822e 100644
--- a/llms.txt
+++ b/llms.txt
@@ -1,6 +1,6 @@
# OpenGridX — AI Agent Context
# Package: @opencorestack/opengridx
-# Version: 3.9.0
+# Version: 3.9.1
# Docs: https://opencorestack.github.io/OpenGridX/
# This file is machine-readable. AI developers: start here.
diff --git a/package-lock.json b/package-lock.json
index 3e4c5b09..4c8b03c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@opencorestack/opengridx",
- "version": "3.9.0",
+ "version": "3.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@opencorestack/opengridx",
- "version": "3.9.0",
+ "version": "3.9.1",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.29.7",
diff --git a/package.json b/package.json
index 27b6b80f..1d904955 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencorestack/opengridx",
- "version": "3.9.0",
+ "version": "3.9.1",
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
"type": "module",
"main": "./dist/opengridx.umd.js",
@@ -29,6 +29,7 @@
"!lib/**/*.test.tsx",
"!lib/**/__screenshots__",
"!lib/**/__snapshots__",
+ "!lib/**/__baseline__",
"docs/architecture",
"docs/assets",
"docs/components",
@@ -65,7 +66,8 @@
"coverage": "vitest run --coverage",
"prepublishOnly": "npm run build:lib",
"wiki:build": "node scripts/build-wiki.mjs",
- "check:compiler": "node scripts/check-react-compiler.mjs"
+ "check:compiler": "node scripts/check-react-compiler.mjs",
+ "check:package": "node scripts/check-package.mjs"
},
"keywords": [
"react",
diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs
new file mode 100644
index 00000000..4c4aab25
--- /dev/null
+++ b/scripts/check-package.mjs
@@ -0,0 +1,44 @@
+// Checks what `npm publish` would ship, after `npm run build:lib`:
+// 1. no test artifact is in the package (tests, baselines, screenshots, snapshots, fixtures);
+// `files` in package.json works by exclusion, so a new kind of artifact slips in unnoticed;
+// 2. what an app ships for `DataGrid` alone (minified with esbuild, React and the optional peers
+// external, gzipped) stays within its budget. Raise the budget deliberately, in the same commit
+// as the change that needs it.
+import { execFileSync } from 'node:child_process';
+import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join, resolve } from 'node:path';
+import { gzipSync } from 'node:zlib';
+import { build } from 'esbuild';
+
+const ROOT = resolve(import.meta.dirname, '..');
+const DATAGRID_GZIP_BUDGET = 92 * 1024; // 3.9.0: 87.2 KB
+const FORBIDDEN = [/\.test\.[cm]?[jt]sx?$/, /\.browser\.test\./, /__baseline__\//, /__screenshots__\//, /__snapshots__\//, /__fixtures__\//];
+
+let failed = false;
+const fail = (message) => { console.error(`check-package: ${message}`); failed = true; };
+
+const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
+const [pack] = JSON.parse(execFileSync(npm, ['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: ROOT, encoding: 'utf8' }));
+const leaked = pack.files.map(f => f.path).filter(path => FORBIDDEN.some(re => re.test(path)));
+if (leaked.length > 0) fail(`${leaked.length} test file(s) would be published: ${leaked.slice(0, 5).join(', ')}${leaked.length > 5 ? ', …' : ''}. Exclude them in package.json "files".`);
+else console.log(`check-package: ${pack.entryCount} files, no test artifacts (unpacked ${(pack.unpackedSize / 1048576).toFixed(1)} MB)`);
+
+const dir = mkdtempSync(join(tmpdir(), 'ogx-size-'));
+try {
+ const entry = join(dir, 'entry.mjs');
+ writeFileSync(entry, `export { DataGrid } from ${JSON.stringify(join(ROOT, 'dist/opengridx.es.js'))};\n`);
+ const outfile = join(dir, 'out.js');
+ await build({
+ entryPoints: [entry], bundle: true, minify: true, format: 'esm', outfile, logLevel: 'error',
+ external: ['react', 'react-dom', 'react/jsx-runtime', 'exceljs', 'jspdf', 'jspdf-autotable'],
+ });
+ const gzip = gzipSync(readFileSync(outfile)).length;
+ const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
+ if (gzip > DATAGRID_GZIP_BUDGET) fail(`DataGrid alone is ${kb(gzip)} gzipped, over the ${kb(DATAGRID_GZIP_BUDGET)} budget.`);
+ else console.log(`check-package: DataGrid alone ${kb(gzip)} gzipped (budget ${kb(DATAGRID_GZIP_BUDGET)})`);
+} finally {
+ rmSync(dir, { recursive: true, force: true });
+}
+
+if (failed) process.exit(1);
diff --git a/wiki/Home.md b/wiki/Home.md
index 80c60d58..07ff1a29 100644
--- a/wiki/Home.md
+++ b/wiki/Home.md
@@ -28,6 +28,8 @@ npm install @opencorestack/opengridx
- **Data:** [Virtualization](Virtualization) · [Filtering & Search](Filtering) · [Sorting & Pagination](Sorting-and-Pagination) · [Infinite Scroll](Infinite-Scroll) · [Data Source](Data-Source)
- **Interaction:** [Editing & Reordering](Editing-and-Reordering) · [Row Selection](Row-Selection) · [Clipboard](Clipboard) · [Keyboard & Accessibility](Keyboard-and-Accessibility)
+- **Spreadsheet:** [Cell Range Selection](Cell-Range-Selection) · [Undo & Redo](Undo-Redo) · [Fill Handle](Fill-Handle) · [Header Filters](Header-Filters)
+- **AI & integration:** [AI Toolkit](AI-Toolkit) · [Next.js](Nextjs) · [Right-to-Left](Right-to-Left) · [Theming](Theming) · [Performance](Performance)
- **Layout:** [Pinning](Pinning) · [Column Grouping](Column-Grouping) · [Cell Spanning](Cell-Spanning) · [Master-Detail](Master-Detail) · [List View](List-View)
- **Analysis:** [Tree Data & Grouping](Tree-Data-and-Grouping) · [Aggregation & Pivot](Aggregation-and-Pivot)
- **Output:** [Export Guide](Export-Guide) · [PDF Export](PDF-Export) · [State Persistence](State-Persistence)