diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7412406f..44f2d994 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,12 @@ jobs: - name: Lint run: npm run lint + - name: Versions agree (llms.txt, CLAUDE.md, CHANGELOG vs package.json) + run: npm run check:versions + + - name: React Compiler baseline (no new CompileError or skip) + run: npm run check:compiler + - name: Type-check (lib, tests, demo, e2e) run: npm run typecheck diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ce9713e9..d62e5572 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -50,6 +50,9 @@ jobs: - name: Package contents and size budget run: npm run check:package + - name: Versions agree (llms.txt, CLAUDE.md, CHANGELOG vs package.json) + run: npm run check:versions + - 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 fe74c95a..787982cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ --- +## [3.10.0] — 2026-10-13 + +### Added + +- **The AI assistant's change chips are translatable**: 11 `localeText` keys (`aiAssistantChipSearch`, `ChipNoFilters`, `ChipSort(field, direction)`, `ChipNoSorting`, `ChipGroup`, `ChipNoGrouping`, `ChipSummary(fn, field)`, `ChipNoSummaries`, `ChipPivot`, `ChipNoPivot`, `ChipVisibility(field, visible)`), English by default; filter operators in the chips use `filterPanelOperators`. `buildAiChips` takes the texts as an optional third argument. + +### Performance + +- **Select all at 1,000,000 rows takes ~130 ms instead of ~260 ms** (deselect ~95 instead of ~120), with no API change. The selection set built for the new model is reused when that array comes back as `rowSelectionModel` (instead of `new Set(model)` again), and a set built by selecting all rows is known to contain them, so the header checkbox's "all selected" count is skipped. The shared selection set is typed `ReadonlySet` internally, so nothing can change it in place. + +### Internal + +- **Guards that fail CI instead of relying on review:** `check:versions` (llms.txt, CLAUDE.md and CHANGELOG must name the `package.json` version), `check:compiler` (fails on any React Compiler result outside a short allowlist, so a refactor that stops it from optimizing a hook is caught), ESLint `no-explicit-any` turned on (it was off; the library had none, the demo's 9 are now typed) and a rule against new `as unknown as` casts in the library outside the documented boundary files. + +- **Refactors (no behaviour change):** `GridToolbar` split into its panels with a shared `useToolbarPanel` (861 → 492 lines); the exporters one file per format; the xlsx cell helpers in `excelCells`; the cell range drag in `useGridCellRangeDrag`; keydown handling in a plain, unit-tested `handleGridKeyDown` function. Type casts `as unknown as` 43 → 15: the row pipeline, data source and grouping are called with the row type, the row store is typed with it, and column widenings go through one documented helper. DOM, styles and behaviour unchanged (theme baseline and the full browser suite in three engines). + +- Demo: the source viewer on every example page is syntax-highlighted (`CodeBlock`, prism-react-renderer), and the integration tips under it are corrected (inline columns and handlers are fine since 3.0 / 3.9; replace rows instead of mutating them). + +--- + ## [3.9.1] — 2026-10-13 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 41fec5cd..d0bcd1c0 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.1 +- **Current version:** 3.10.0 --- @@ -157,6 +157,13 @@ The GitHub wiki is generated from `docs/` + `wiki/` by `scripts/build-wiki.mjs` --- +## v3.10.0 — significant changes + +- **CI guards** (each fails the build): `check:versions` (`llms.txt` + its demo copy, CLAUDE.md "Current version" and a CHANGELOG entry must match `package.json`; bump them together), `check:compiler` (React Compiler: only the allowlisted `EXPECTED` results in `scripts/check-react-compiler.mjs`), `check:package` (no test artifacts, DataGrid-only size budget), ESLint `no-explicit-any` (now `error`, also in the demo) and `no-restricted-syntax` against `as unknown as` in `lib/` outside the boundary files listed in `eslint.config.js` +- **Types (R4)**: a `GridColDef` passed to an untyped public surface (callback params, toolbar, store) goes through `untypedColumn` / `untypedColumns` (`lib/utils/untypedColumns.ts`), the one place that widening is written; narrowing untyped → `GridColDef` / `R[]` is a plain `as`. `useDataGrid` types `setRows` / `replaceRows` / `rowsLookup` with `R`; call generic hooks with `` explicitly when an untyped argument would otherwise infer `GridRowModel`. Remaining `as unknown as` (15) are synthetic rows, third-party module typings, `window` / CSS custom properties: do not add new ones elsewhere +- R5 splits (no behaviour change): `GridToolbar` panels in `AggregationPanel` / `ColumnsPanelWrapper` / `FilterPanelWrapper` with shared `useToolbarPanel` (portal, position, Escape / outside press; `onClose` through a ref); exporters one file per format (`exportCsv`, `exportExcelHtml`, `exportJson`, `printGrid`, shared `exportRows`; `utils/export/index.ts` only re-exports); xlsx helpers in `excelCells.ts`; the range drag in `useGridCellRangeDrag`, called from inside `useGridCellSelection` (hook order unchanged); keydown handling in `handleGridKeyDown(event, context)` (`hooks/core/gridKeyDown.ts`, a plain function over a typed `GridKeyDownContext`, unit-testable without rendering; `useGridKeyboardNavigation` calls it from a `useCallback` with the same dependencies). A hook that takes refs as parameters must list them in its dependency arrays, or React Compiler skips it (`PreserveManualMemo`) +- Demo code samples go through `demo/components/CodeBlock.tsx` (prism-react-renderer, demo-only devDependency); live output panels (JSON state, logs) stay plain `
`
+
 ## 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
@@ -193,7 +200,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 (backlog) until its overhead is within ~20 %
+- The time-sliced pipeline (Option A of `docs/superpowers/specs/2026-10-12-nonblocking-pipeline-proposal.md`) is closed: 1.5–2× slower in total; branch `archive/timesliced-pipeline-2026-10-12` kept for reference
 
 ## v3.5.1 — significant changes
 
diff --git a/demo/components/CodeBlock.css b/demo/components/CodeBlock.css
new file mode 100644
index 00000000..cfe74634
--- /dev/null
+++ b/demo/components/CodeBlock.css
@@ -0,0 +1,10 @@
+.docs-code-block--highlighted {
+  padding: 16px;
+  border-radius: 8px;
+  overflow-x: auto;
+  white-space: pre;
+  font-family: ui-monospace, 'JetBrains Mono', Menlo, monospace;
+  font-size: 13px;
+  line-height: 1.55;
+  tab-size: 4;
+}
diff --git a/demo/components/CodeBlock.tsx b/demo/components/CodeBlock.tsx
new file mode 100644
index 00000000..1859435c
--- /dev/null
+++ b/demo/components/CodeBlock.tsx
@@ -0,0 +1,27 @@
+import { Highlight, themes } from 'prism-react-renderer';
+import './CodeBlock.css';
+
+interface CodeBlockProps {
+    code: string;
+    language?: 'tsx' | 'typescript' | 'css' | 'json' | 'bash';
+}
+
+/** Syntax-highlighted source (prism-react-renderer, VS Code dark theme) for the demo's code samples. */
+export function CodeBlock({ code, language = 'tsx' }: CodeBlockProps) {
+    return (
+        
+            {({ className, style, tokens, getLineProps, getTokenProps }) => (
+                // Prism hands out its theme colours as style objects per line and token.
+                
+                    
+                        {tokens.map((line, i) => (
+                            
+ {line.map((token, key) => )} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/demo/components/DocsLayout.tsx b/demo/components/DocsLayout.tsx index 8a14bbf3..24e98fa2 100644 --- a/demo/components/DocsLayout.tsx +++ b/demo/components/DocsLayout.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import './DocsLayout.css'; +import { CodeBlock } from './CodeBlock'; interface DocsLayoutProps { title: string; @@ -58,9 +59,7 @@ export function DocsLayout({ title, description, children, sourceCode }: DocsLay {showSource && (
-
-                                {sourceCode}
-                            
+
)} @@ -75,23 +74,20 @@ export function DocsLayout({ title, description, children, sourceCode }: DocsLay

Dependencies & Imports

-
-                        {`// Core imports should exclusively target the published package
+                    
-                    
+import '@opencorestack/opengridx/styles'; // Don't forget the base CSS styles`} />

Integration Best Practices

    -
  • Memorization: Wrap complex data processing or extensive column arrays in useMemo hooks so React does not recreate large references constantly.
  • -
  • Stable Methods: Supply stable functional references to event handlers like callbacks (e.g., useCallback) to prevent re-renders when rendering heavily customized cells.
  • -
  • Performance: Try to declare columns completely outside the component if it lacks interactive state.
  • +
  • Rows: change a row by passing a new object (rows.map(r => r.id === id ? {'{ ...r, ...changes }'} : r)), never by mutating it: the grid caches per row object.
  • +
  • Columns and handlers: inline columns and handlers such as onRowClick are fine. Columns are compared shallowly (a new inline valueGetter function counts as a change), and rows receive the grid's own stable handlers.
  • +
  • Height: give the grid a bounded height (a height prop or a sized parent) so it virtualizes. See Performance in the docs.
-
-                        {`// Minimal implementation boilerplate
+                    
     );
-}`}
-                    
+}`} />
diff --git a/demo/examples/AdvancedFilteringDemo/AdvancedFilteringDemo.tsx b/demo/examples/AdvancedFilteringDemo/AdvancedFilteringDemo.tsx index db93f10d..6edcf256 100644 --- a/demo/examples/AdvancedFilteringDemo/AdvancedFilteringDemo.tsx +++ b/demo/examples/AdvancedFilteringDemo/AdvancedFilteringDemo.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { DataGrid, GridToolbar, GridColDef, GridRowModel, GridFilterModel, GridTooltip, exportToCsv, exportToExcel, exportToJson, printGrid } from '@opencorestack/opengridx'; +import type { GridApi, GridToolbarSlotProps } from '@opencorestack/opengridx'; import { DocsLayout } from '../../components/DocsLayout'; import './AdvancedFilteringDemo.css'; @@ -17,7 +18,7 @@ interface Employee extends GridRowModel { } interface ExportToolbarProps { - apiRef?: any; + apiRef?: React.RefObject; fallbackRows: Employee[]; fallbackColumns: GridColDef[]; } @@ -161,7 +162,7 @@ const columns: GridColDef[] = [ { field: 'active', headerName: 'Active', width: 90, type: 'boolean' }, ]; -function ToolbarWithExport(props: any) { +function ToolbarWithExport(props: GridToolbarSlotProps) { return ( diff --git a/demo/examples/ThemingDemo/ThemingDemo.tsx b/demo/examples/ThemingDemo/ThemingDemo.tsx index 5265208f..7b30acfe 100644 --- a/demo/examples/ThemingDemo/ThemingDemo.tsx +++ b/demo/examples/ThemingDemo/ThemingDemo.tsx @@ -25,6 +25,7 @@ import { import type { GridTheme } from '@opencorestack/opengridx'; import { generateEmployees } from '../../data/mockData'; import { allColumns } from '../../data/columns'; +import type { GridApi, GridRowModel, GridToolbarSlotProps } from '@opencorestack/opengridx'; import { DocsLayout } from '../../components/DocsLayout'; import './ThemingDemo.css'; @@ -84,9 +85,9 @@ const Icons = { }; interface ExportToolbarProps { - apiRef?: any; - fallbackRows: any[]; - fallbackColumns: GridColDef[]; + apiRef?: React.RefObject; + fallbackRows: GridRowModel[]; + fallbackColumns: GridColDef[]; } function ExportToolbar({ apiRef, fallbackRows, fallbackColumns }: ExportToolbarProps) { @@ -105,7 +106,7 @@ function ExportToolbar({ apiRef, fallbackRows, fallbackColumns }: ExportToolbarP const selectedIds: (string | number)[] = apiRef?.current?.getSelectedRows?.() || []; if (selectedIds.length > 0) { const allRows = getAllRows(); - return allRows.filter((r: any) => selectedIds.includes(r.id)); + return allRows.filter((r: GridRowModel) => selectedIds.includes(r.id)); } return getAllRows(); }; @@ -192,7 +193,10 @@ function ExportToolbar({ apiRef, fallbackRows, fallbackColumns }: ExportToolbarP // If this were defined inside the component body, every render would create a // new function reference, causing React to unmount/remount the entire toolbar // and losing all state (search expansion, filter panel open, etc.). -function ToolbarWithExport(props: any) { +/** The toolbar slot plus the demo's own fallback data, passed through `slotProps.toolbar`. */ +type ExportToolbarSlotProps = GridToolbarSlotProps & { _fallbackRows?: GridRowModel[]; _fallbackColumns?: GridColDef[] }; + +function ToolbarWithExport(props: ExportToolbarSlotProps) { return (