Skip to content

Latest commit

 

History

History
383 lines (278 loc) · 46.3 KB

CLAUDE.md

File metadata and controls

383 lines (278 loc) · 46.3 KB

OpenGridX — AI Context

This file is auto-loaded by Claude Code and other AI coding assistants. It provides the orientation needed to work in this codebase without exploring files first.


What this is

@opencorestack/opengridx — a high-performance React DataGrid component library.

  • Language: TypeScript 5.9 strict mode. Zero any, zero eslint-disable.
  • Framework: React 19
  • Tests: Vitest 4 + @testing-library/react. Two projects: unit (jsdom, npm test) and browser (real Chromium, Firefox and WebKit via Playwright, npm run test:browser)
  • 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.10.1

Architecture — data flow

Props enter DataGrid.tsx (the orchestration layer — no logic lives here, only hook calls and one props object for GridRoot). Put new logic in a hook or pure util and call it from there. Hook call order is load-bearing (effects run in call order; several hooks install apiRef methods): the full ordered list is in docs/architecture/datagrid-orchestration.md. Contiguous runs of hooks are wrapped in composite hooks (useGridEditPipeline, useGridSelectionAndStateApi, useGridViewApis, useGridScrollViewport, useGridRangeEditing, useGridRenderWindow, useGridToolbarStack) that call their members in the same order; a composite may only wrap consecutive steps.

DataGridProps
  │
  ├─ useGridControlledState     — normalises controlled/uncontrolled prop pairs
  │    (sort, pagination, selection, column visibility, pivot, aggregation)
  ├─ useGridPivot → resolveGridModes (utils/gridModes) — which features are in effect
  ├─ useGridGroupingColumn      — the synthetic __group__ column + its auto-pin
  ├─ useDataGrid                — row store, dimensions, live apiRef
  │    useGridRowIdOf / useGridApiRefBinding
  ├─ useGridHierarchy           — useTreeData + useRowGrouping → active handlers + rowMetaMap
  ├─ useGridEditPipeline        — edit commit channel → batch edit → useGridEditing
  ├─ useGridLiveRowSelection, useGridDataSource, useGridDetailPanel
  │
  ├─ useGridColumns             — inject hierarchy renderers; resolve order/visibility
  │    reads rowMetaMap instead of row._* fields
  ├─ useGridRowPipeline         — filter → sort → paginate → split pinned rows
  ├─ useGridSelectionAndStateApi — useGridRowInteractions (selection + row click / checkbox handlers)
  │                               → useGridApiMethods (imperative state API)
  ├─ useAggregation, useGridAggregationApi, useLayout, useGridSpanning, useGridCellSelection
  ├─ useGridViewApis            — useGridClipboardApi, useGridScrollToIndexesApi, useGridColumnAutosize
  │
  ├─ useGridScrollViewport      — useGridViewport (scroll sync + measurement) → useGridMaxContentHeight
  │                               → useGridVirtualization (render window)
  ├─ useGridKeyboardNavigation  — focus + keys; useGridSortHandlers / useGridPointerFocusHandlers /
  │                               useGridColumnMenuHandlers for header and cell events
  ├─ useGridRangeEditing        — cell selection API → paste / range clear → undo/redo → fill handle
  ├─ useGridRenderWindow        — useGridAriaRows → span row window → useGridVisibleRows → span column window
  ├─ useGridToolbarStack        — columns panel, stylesheet warning, useGridAiAssistant, useGridToolbarProps
  │
  └─ renders <GridRoot>: root element, toolbar, AI panel, columns panel, GridListView (list view) or
       <GridViewport> (Header, GridPinnedRows, GridVirtualRows, GridAggregationFooter),
       cell selection stats, footer slot or GridPaginationArea, GridLiveRegion, overlays

Hierarchy (tree data, row grouping) is handled by useTreeData / useRowGrouping. They produce flat renderable row arrays of the consumer's own row objects (unchanged — nothing is injected since v3.0) and a rowMetaMap: Map<GridRowId, GridRowMeta> that carries all hierarchy information.


Key files

File Purpose
lib/types/ All public types, one file per domain: rows.ts (GridRowId, GridRowModel, GridRowMeta, pinned rows, row params), columns.ts (GridColDef, cell/value/render params, column pinning/order/visibility), sorting.ts, filtering.ts, selection.ts (row + cell selection), editing.ts (batch edit, paste, undo/redo, fill), grouping.ts (grouping, aggregation, pivot models), export.ts (PdfExportOptions, GridGroupedExportRow), dataSource.ts (data source, pagination model), state.ts, locale.ts (GridLocaleText), props.ts (DataGridProps), slots.ts, api.ts (GridApi), ai.ts (GridAi*). index.ts only re-exports them (export type *), so import from types, never from a domain file outside lib/types/
lib/components/DataGrid/DataGrid.tsx Orchestration only — hooks + one props object for GridRoot, no business logic. Hook order: docs/architecture/datagrid-orchestration.md
lib/components/DataGrid/GridRoot.tsx The grid's root element and its children (toolbar, panels, list view or viewport, footer, live region, overlays); passes the viewport's props on to GridViewport
lib/components/DataGrid/GridViewport.tsx The role="grid" viewport: header, pinned rows, empty state, virtual rows, aggregation footer; builds rowRenderProps
lib/hooks/core/ Core pipeline hooks (always active): useGridControlledState, useGridRowPipeline, useGridColumns, useGridVirtualization, useGridVisibleRows, useGridScrollSync, useGridKeyboardNavigation, useLayout; DataGrid wiring: useGridHierarchy, useGridGroupingColumn, useGridRowIdOf, useGridDetailPanel, useGridRowInteractions, useGridViewport, useGridAriaRows, useGridToolbarProps, useGridHeaderHandlers, useGridFocusHandlers, and the apiRef installers useGridApiRefBinding / useGridApiMethods / useGridAggregationApi / useGridClipboardApi / useGridScrollToIndexesApi; composite hooks that wrap consecutive steps of that order: useGridEditPipeline, useGridSelectionAndStateApi, useGridViewApis, useGridScrollViewport, useGridRangeEditing, useGridRenderWindow, useGridToolbarStack
lib/utils/gridModes.ts resolveGridModes: the one place features override each other (pivot vs tree/grouping/reorder, grouping vs pagination, infinite vs pages, list view needs listViewColumn)
lib/hooks/features/ Opt-in feature hooks: useAggregation, usePivot, useGridEditing, useGridSpanning, useGridDataSource, useGridClipboard
lib/hooks/useTreeData.ts Tree-data hierarchy — flat row array + rowMetaMap
lib/hooks/useRowGrouping.ts Row-grouping hierarchy — flat row array + rowMetaMap
lib/components/Cell/Cell.tsx Cell render — invokes renderCell, wraps with CellErrorBoundary
lib/components/Cell/CellErrorBoundary.tsx Class error boundary wrapping renderCell output
lib/utils/filtering/ Client-side filter operators
lib/utils/sorting/ Client-side sort comparators
lib/utils/aggregation/ The single set of aggregation functions (sum/avg/count/min/max/unique) used by footer, row grouping and exports — do not re-implement them elsewhere
lib/hooks/core/useGridDevWarnings.ts Dev-only console.warns (unbounded container, pagination + grouping)

Where to add things

Task Where
New filter operator for a column type lib/utils/filtering/filtering.ts + GridFilterOperator in lib/types/filtering.ts
New feature (aggregation, pivot, etc.) New hook in lib/hooks/features/, consumed by DataGrid.tsx
New core pipeline stage New hook in lib/hooks/core/, added to DataGrid.tsx
New component (slot, overlay, etc.) lib/components/<Name>/
New public prop on DataGrid Add to DataGridProps in lib/types/props.ts, destructure in DataGrid.tsx
New column type the type union on GridColDef in lib/types/columns.ts, operators in getOperatorsForType (lib/utils/filtering/index.ts), paste parsing in lib/utils/parsing.ts
New slot Add to the slots / slotProps of DataGridProps in lib/types/props.ts (slot prop types in lib/types/slots.ts), wire in DataGrid.tsx via slots?.mySlot

Naming conventions

  • CSS classes: ogx__ BEM prefix (e.g. ogx__cell, ogx__cell--focused)
  • CSS variables: --ogx-* (e.g. --ogx-row-height, --ogx-color-primary)
  • Hook names: use-grid-* for core hooks, use* for feature hooks
  • Exported types: Grid* prefix (e.g. GridColDef, GridRowMeta, GridLocaleText)
  • Never inject fields onto consumer row objects. Hierarchy info goes in rowMetaMap / params.rowMeta; the old _hasChildren-style runtime fields were removed in v3.0.

GridRowMeta pattern

Hierarchy metadata (hasChildren, treeDepth, groupingField, etc.) lives in a Map<GridRowId, GridRowMeta> returned by useTreeData/useRowGrouping, not on the row object. Access it in renderCell via params.rowMeta.

The underscore fields (_hasChildren, _treeDepth, …) were injected onto copied rows from v1.1 to v2.x as a backward-compat shim and removed in v3.0 — rows now pass through unchanged, so params.row is the consumer's own object. Migration: docs/migration/v2-to-v3.md.

Full doc: docs/architecture/grid-row-meta.md


Testing conventions

  • Test files are co-located with the file they test: useGridRowPipeline.test.ts lives in lib/hooks/core/
  • Import pattern: import { describe, it, expect, vi } from 'vitest' + import { renderHook, act } from '@testing-library/react'
  • Hook tests use renderHook(() => useMyHook(params)) — no DOM needed for pure hooks
  • Component tests use render from @testing-library/react
  • Run unit tests: npm test (jsdom). Real-browser tests: npm run test:browser. Both: npm run test:all
  • Run a single file: npx vitest run --project unit lib/hooks/core/useGridRowPipeline.test.ts
  • Anything that depends on layout, ResizeObserver, or scrolling (virtualization, container height, drag) must be a *.browser.test.tsx — jsdom has no layout engine and reports a 0px viewport (the grid falls back to 600px), so such bugs are invisible there
  • Browser tests run in Chromium, Firefox and WebKit: npx playwright install chromium firefox webkit once. One engine only: npx vitest run --project browser --browser.name=firefox
  • Browser test files run one at a time (fileParallelism: false): several test pages open at once in one Firefox steal focus and pointer input from each other
  • Vitest vs Playwright Test: render <DataGrid> with props and assert → Vitest (*.test.tsx, or *.browser.test.tsx for layout). Needs the built package, a real app, a dev/preview server or a file download → the e2e package smoke suite (e2e/, npm run test:smoke). Vitest aliases the package to lib/, so only the smoke suite catches packaging bugs (bundled react/jsx-runtime, missing CSS in dist/, .d.ts that fails skipLibCheck: false)
  • Smoke suite: scripts/smoke.mjs builds + packs to .pack/, npm cis each fixture in e2e/fixtures/{react19,react18,nopeers,nextjs} and unpacks the tarball there, builds them (strict tsc + vite), then runs e2e/smoke.spec.ts in Chromium/Firefox/WebKit × both fixtures against vite preview. Scenarios via ?scenario= (basic, flex, grouping, editing; React 19 also pinned, dark). Any console error/warning fails a test. nopeers is a webpack 5 app without exceljs/jspdf (nopeers.spec.ts, port 4321): every optional-peer import() must stay inside a try block, which webpack and Turbopack treat as optional (Vite tolerates a missing peer anyway, so the Vite fixtures cannot catch this). nextjs is a Next.js 16 App Router app (Turbopack, next build + next start, port 4323, e2e/nextjs.spec.ts): Server Component → 'use client' grid, stylesheet in the layout, no optional peers, no aliases; its next.config.ts only pins turbopack.root to the fixture so the repo's own exceljs is not found. A specifier hidden in a variable (import(name)) builds but breaks apps that do install the peers: browsers cannot resolve bare specifiers, and Vite's lib build strips webpackIgnore / @vite-ignore comments. Fixtures are outside root lint/typecheck/vitest; their lockfiles must not carry integrity for the tarball entry. Guide: docs/contributing/testing.md
  • Keep browser tests engine-neutral: no CDP (cdp() is Chromium-only; use a Playwright-backed command such as commands.setColorScheme from vitest.config.ts), Tab-order anchors are text inputs (WebKit on macOS skips buttons on Tab), compare scroll positions with what the browser actually took (Firefox snaps them to device pixels), and wait for CSS animations to finish before measuring. Skip an engine only with it.skipIf(server.browser === '...') (server from vitest/browser) and a comment saying why

Docs conventions

Content Location
Component public API (props, slots, events) docs/components/<name>.md
Internal hook architecture docs/architecture/<hook-name>.md
Feature guides (usage patterns, code examples) docs/features/<feature>.md
Full public API reference docs/API_REFERENCE.md
Roadmap and feature status docs/roadmap.md
Wiki-only pages (Home, Getting Started, FAQ) wiki/

The GitHub wiki is generated from docs/ + wiki/ by scripts/build-wiki.mjs (.github/workflows/wiki.yml, on push to main). Never edit the wiki directly. A new doc page needs an entry in SECTIONS there; npm run wiki:build warns about unlisted docs


Build and lint rules

  • npm run lint && npm run build must pass after every change
  • Zero eslint-disable or @ts-ignore — fix the root cause
  • Zero any — use unknown or explicit interfaces
  • Never add Co-Authored-By AI attribution to commit messages

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<R> 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> / R[] is a plain as. useDataGrid types setRows / replaceRows / rowsLookup with R; call generic hooks with <R> 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 <pre>

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)
  • 9 theme keys (checkbox checked fill / check mark, expand chevron, menu indicator / selected icon / icons / section labels / dividers) read var(--x, <old value>), so the default look is unchanged

v3.8.0 — significant changes

  • Row store edits are in place. REPLACE_ROW(S) (replaceStoredRows in useDataGrid.ts) mutate state.rows.idRowsLookup / idByRow / indexById and only copy state.rows.rows (the stored rows in order; effectiveRows is that array). Never detect an edit by those maps' identity; depend on rows. The writes must stay idempotent (StrictMode re-runs reducers; DataGrid.storeInPlace.test.tsx). SET_ROWS still builds new maps, which gives getRowIdOf a new identity
  • Id lists over a row view go through getRowIndex(rows, getRowId, view) (lib/utils/rowIndex.ts): it returns the previous object when every position keeps its id, so memos on it survive edits. Don't write rows.map(getRowId) over the renderable or data rows
  • Row grouping records leaves as one id → row index map (GridLeafIndex); leaf tree nodes and leaf GridRowMeta are LazyLeafTreeNodes / LazyLeafMetaMap (lib/utils/grouping/lazyLeafMaps.ts), made on first read and cached (one object per row). Rows are partitioned by index and leaf ids come from the store's allRows (rowIds param), so rows passed to useRowGrouping must stay aligned with rowIds. Iterating these maps materializes every leaf: keep it off hot paths
  • Row grouping's rowMetaMap is a LayeredMap (leaf metas built once per tree + group metas per toggle); internal rowMetaMap parameters are ReadonlyMap. Nothing may rely on its iteration order

v3.7.0 — significant changes

  • Types live in domain files under lib/types/ (columns, rows, props, api, editing, selection, …); lib/types/index.ts only re-exports. Import from types, add a type to its domain file
  • DataGrid.tsx renders through GridRoot → GridViewport, and wraps contiguous hook runs in composite hooks (useGridEditPipeline, useGridSelectionAndStateApi, useGridViewApis, useGridScrollViewport, useGridRangeEditing, useGridRenderWindow, useGridToolbarStack). Never reorder hooks across a composite; the true numbered order is in docs/architecture/datagrid-orchestration.md. Each extracted component takes exactly one child position (keeps useId values stable in SSR)
  • 16 theme keys (fonts, header padding, border width/radius, divider widths/colours, overlay font). Variables without a stylesheet value are read as var(--x, fallback) so they follow a provider's base token; divider width 0 hides a divider. lib/theme/defaultThemeBaseline.browser.test.tsx compares the default theme's computed styles with lib/theme/__baseline__/default-theme.darwin.<engine>.json on macOS only (text metrics differ per OS); an intended default-look change must regenerate them
  • Demo smoke: npm run test:demo (e2e/demo.spec.ts, port 4322, base /OpenGridX/) loads every route of examplesConfig in demo/App.tsx; @heavy pages also in Firefox/WebKit. It runs in ci.yml and is the build step of deploy.yml. A route can set fullWidth: true
  • source-map-js is overridden to ^1.2.2 (root, fixtures, bench). The sprintf-js alert has no fix while vite-plugin-dts relies on api-extractor
  • Browser tests that drag must release the mouse over the page (and test files reset with commands.pointerUp() in afterEach): a release outside the frame leaves WebKit's button pressed and swallows the next file's pointerdown

v3.6.0 — significant changes

  • Sorting encodes each key into typed arrays (encodeSortColumn): rank 0–4 (number, date, boolean, string, other), empty = 5, plus a number per row; strings/other get their position among the distinct texts in collator order. Only keys where an item carries both comparator and params use the per-pair callSortComparator path. Never call the collator inside a comparator over rows
  • getCellValue returns row[field] without allocating when the column has no valueGetter — keep it allocation-free. Built-in aggregations go through aggregateRows (row by row, same addition order); AGGREGATION_FUNCTIONS stays the value-array API for pivot and cell selection
  • Quick filter: filterRows keeps texts by index for the last rows array per GridColumnLookup (reused when the same object is at the same index); createRowFilter keeps a per-row WeakMap. Joining texts into one string was measured and rejected
  • Row-grouping leaf GridRowMeta objects are one per row (sharing per depth was reverted: params.rowMeta is public). Equivalence tests with the old code as oracles: *.equivalence.test.ts
  • dist/opengridx.es.js starts with 'use client' (banner in vite.config.lib.js, checked by check-bundle); the UMD build has none
  • Every optional-peer import() (exceljs, jspdf, jspdf-autotable) must stay inside try — webpack and Turbopack only treat it as optional there; a hidden variable specifier breaks apps that install the peers. The nopeers smoke fixture (webpack 5, no peers, port 4321, e2e/nopeers.spec.ts) guards it; fixtures must keep webpack from resolving the root node_modules
  • validateGridAiState skips a top-level string message; the prompt handler surfaces it as result.message for object, JSON-string and fenced input
  • 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)
  • 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

  • Content height is capped to the browser's limit. Firefox lays out elements up to about 17.9M px and collapses a taller one (it does not clamp like Chromium/WebKit at about 33.5M px), which made large grids unscrollable there. useGridMaxContentHeight (hook 32a, before useGridVirtualization) measures the limit once per document with an invisible probe (lib/utils/maxElementHeight.ts: getMaxElementHeight, countReachableRows; undefined until measured, Infinity without a layout engine), and useGridVirtualization returns virtualContainerHeight capped to it — always render the rows' container with that, never totalHeight - pinned heights
  • The tall-content dev warning waits for the measurement and fires only when rows are cut off, naming the reachable count. DataGrid.heightCap.browser.test.tsx (500k rows) fails without the cap in Firefox
  • Benchmark: npm run bench (bench/, port 4320) measures the packed package at 10k–1M rows in 3 engines; results in bench/results/, tables in docs/performance.md (node bench/report.mjs --write docs/performance.md). Run it on an idle machine; the weekly CI pass is for trends only

v3.5.0 — significant changes

Design: docs/superpowers/specs/2026-10-09-ai-assistant-header-filters-fill-design.md.

  • AI assistant (aiAssistant): core holds the panel (useGridAiAssistant, hook 45a, before useGridToolbarProps; UI in lib/components/AiAssistant/), /ai holds model logic (createGridAiPromptHandler, createGridAgentTools). The core bundle must contain no AI code and /ai no React (check-bundle; /ai budget 8 kB, ≈ 6.7 kB now). /ai strings must never contain the word "react" or a lowercase import/require( (check-bundle regexes)
  • Brand rule: validateGridAiState brands result and state with the non-enumerable Symbol.for('opengridx.ai.validated') (lib/utils/aiBrand.ts); the grid applies only isGridAiValidated(result.state). Parts the validator emptied are skipped; column visibility is merged; Undo restores a snapshot of touched parts (view undo, separate from undoRedo)
  • GridAiState / GridAiPart / GridAiValidationError and assistant types live in lib/types/ai.ts; all GridAi* / GridAgent* types are exported from /ai only (publicApi rule /^Grid(?:Ai|Agent)[A-Z]/)
  • Agent tools are all-or-nothing on validation errors and never throw; get_row_summary exists only with allowRowAccess. They read apiRef.current at call time — create them in a handler/effect, not during render (react-hooks/refs)
  • rowGroupingModel is a controlled/uncontrolled pair (onRowGroupingModelChange) in useGridControlledState. New GridApi: setSortModel, setRowGroupingModel, setAggregationModel, setColumnVisibilityModel, setPivotModel, getGridAiState, openAiAssistant, closeAiAssistant
  • Two buttons swapped by a ternary inside a <form> need distinct keys, or the click on the outgoing one submits the form (the Stop button re-sent the prompt)
  • Header filters (headerFilters): each cell owns only the root filterModel item id: 'header:<field>' (lib/utils/headerFilters.ts: getHeaderFilterState, setHeaderFilterItem, which returns the same object for a model it can't show). Custom = 'or' root with items, field inside a group, or an untagged/second root item on the field. Off in pivot mode
  • The filter row renders inside .ogx__header-wrap after .ogx__header; column cells of both rows must use getHeaderColumnStyle (lib/components/Header/headerColumnStyle.ts). Focus: FocusedCell.headerFilter; column header is row -2 when header filters are on, filter row -1. useGridAriaRows({ headerFilterRow }). "Custom filter" opens the panel via GridToolbarHost.registerFiltersPanel + forceFiltersOpen / onFiltersPanelClose
  • Fill handle: useGridFillHandle (hook 36d, after useGridUndoRedo, inside useGridRangeEditing) → rowRenderProps.fillHandle (built in GridViewport) → fillHandleCol on the handle's row only; RANGE_FILL_HANDLE (32) flag; .ogx__cell-fill-handle (24 px hit area inside the corner cell). Drags go through cellSelection.startTrackedDrag (pointer events, touch, auto-scroll, Escape cancels). Pure fill logic in lib/utils/editing/fill.ts (no regexes); commits via applyEdits(edits, 'fill'); Ctrl/Cmd+D / R always copy
  • Smoke suites of parallel agents share ports 4318/4319 — never run two at once

v3.4.0 — significant changes

  • Batch edits: paste, range clear, undo and redo commit through useGridBatchEdit.applyEdits — one processRowUpdate per row (rows in parallel, a failing row reported through onProcessRowUpdateError without blocking others) and one REPLACE_ROWS store update for all successful rows. getRow sees rows stored since the last render. Single edits store through storeRow. Shared helpers in lib/utils/editing/commit.ts; edit targets (rows × range columns, same indices as cellSelection.getCurrentRange().resolved) in lib/utils/editing/targets.ts
  • Hook order (see docs/architecture/datagrid-orchestration.md): useGridEditCommitChannel (12a) and useGridBatchEdit (12b) before useGridEditing; useGridClipboardPaste (36b) after useGridCellSelectionApi, then useGridUndoRedo (36c). Commits reach the history through the channel's emit(changes, source); history ignores 'undo' / 'redo' sources
  • Paste, Delete/Backspace and Ctrl/Cmd+Z / Shift+Z / Ctrl+Y are native listeners on the grid root, gated by classifyKeyTarget ('grid' / 'control') and no open editor, so an editor's own paste and undo keep working
  • History: an action is recorded only after processRowUpdate succeeds, as { id, field, before, after } with after read from the stored row via getCellValue. Undo writes before expecting after (redo the reverse); stale cells are skipped 'changed', removed rows 'missing'; values are compared, not row identity
  • Parsing: lib/utils/parsing.ts (parseTsv, per-type parse*Text, parseValueByType, parseCellText with valueParser) is pure and the single parser for paste and /ai (lib/ai/coerce.ts only adds typed-JSON handling on top). Keep its regexes linear
  • /ai entry point (lib/ai/, no React, no dependencies): ships as dist/ai.es.js, dist/ai.cjs (.cjs because the package is "type": "module") and dist/ai.d.ts, built by a second vite.config.lib.js run (--mode ai). check-bundle enforces no imports, no react, ≤ 5 KB gzipped (≈ 4.6 KB now) and that the core bundles don't contain the marker 'OpenGridX grid state'. Demo, Vitest and tsconfig alias @opencorestack/opengridx/ai before the root alias. lib/ai/columns.ts is the single source of which columns each part allows; the validator reads own keys only, never __proto__, caps lists at 200 and errors at 100, and never throws (fuzz test)
  • Shared inputs: Input is forwardRef with variant: 'field' | 'cell' and inputClassName (className stays on the wrapper); error sets aria-invalid. Built-in search boxes, the filter value box and text/number/date editors render through Input; the boolean editor and list-view checkbox through Checkbox (hidden <input>: tests click .ogx-checkbox__box). The boolean editor needs preventDefault on mousedown on the whole .ogx__edit-boolean div. Browser tests that click styled controls must import lib/styles/opengridx.css
  • Browser paste tests copy from a textarea and press the paste shortcut (ControlOrMeta+V); Firefox drops clipboardData on a synthetic ClipboardEvent

v3.3.0 — significant changes

  • Cell range selection (cellSelection, design: docs/superpowers/specs/2026-10-01-cell-range-selection-design.md). useGridCellSelection runs after useGridSpanning and before useGridKeyboardNavigation (feeds it cellSelection.keyboard); useGridCellSelectionApi runs after useGridPointerFocusHandlers, keeps the anchor in sync with focusedCell and installs the six apiRef methods. Pure helpers in lib/utils/cellSelection.ts
  • The model stores corner row ids + fields and is resolved each render against allRenderableRows and the data columns of navigationColumns, so a range follows sorting; a missing corner clears it with reason 'dataChange'. Only the first range is used. isRangeColumnField excludes system fields and __group__; synthetic rows are highlighted but skipped by copy and getSelectedCells
  • Row is React.memo (areRowPropsEqual, cellRange compared by value); row renderers pass getRowCellRange(...), null outside the range. Handlers passed to rows must keep a stable identity, or every row re-renders
  • With cellSelection on, the Ctrl/Cmd+C shortcut copies the range (or the focused cell) through useGridClipboard's getShortcutText, no header line; copySelectedRows is unchanged. A drag clears the page text selection (WebKit otherwise extends it outside the grid and takes over Ctrl+C)
  • Browser tests drive held-mouse drags with the Playwright-backed commands.pointerMoveTo / pointerDown / pointerUp from vitest.config.ts
  • Wrapped header text: wrapHeaderText (grid) and GridColDef.wrapHeaderText (column wins); lib/utils/headerWrap.ts (getHeaderLineClamp, HEADER_WRAP_LINE_HEIGHT 16 must match Header.css). The header never grows; class ogx__header-cell--wrap
  • React Compiler: ESLint runs react-hooks recommended-latest (only set-state-in-effect off). npm run check:compiler should report only useGridDataSource (try/finally) plus the two 'use no memo' wrappers (InlineToolbar, CellRenderTarget — they call consumer functions that may use hooks). REACT_COMPILER=1 npx vitest run … tests lib/ compiled. In components and hooks: no ?./??/||/ternaries inside try (use attempt() from lib/utils/attempt.ts), no try/finally, no callback naming itself (go through a ref), default array/object props as module-level constants
  • Vitest's unit project excludes .claude/** (agent worktrees)

v3.2.2 — significant changes

  • Regexes that run on cell text or consumer input must be linear (CodeQL scans every push): no adjacent overlapping quantifiers such as \s*(px)?\s* or [\d\s]*\d[\d\s]*; use a lookahead, trim first, or a plain loop. Tests in lib/utils/export/exportShared.test.ts time 50k-char worst cases
  • exceljs is a devDependency as well as an optional peer (like jspdf); uuid is overridden to ^11.1.1 in the root and both e2e fixtures

v3.2.1 — significant changes

  • Boolean edit checkbox calls preventDefault() on mousedown (GridEditInputCell.tsx): Safari does not focus a checkbox on click, so the cell blur committed the old value first
  • Package smoke suite npm run test:smoke (e2e/, scripts/smoke.mjs) runs in ci.yml (PRs, branches, manual) and before npm publish; weekly nightly.yml. Fixture lockfiles must not carry an integrity field for the tarball

v3.2.0 — significant changes

  • exportToPdf is time-sliced (runPdfExport + PdfExportTuning in lib/utils/export/exportToPdf.ts; internal, not exported from the package). Yields use setTimeout(0), not scheduler.yield() (which resumes ahead of queued tasks and starves React's scheduler). The body is drawn as several autoTable calls: continuation chunks start at startY: MARGIN and a willDrawPage hook moves data.cursor.y to the previous finalY and sets settings.showHead = 'never' for that page only (bypasses autoTable's fit-below-startY page break, so breaks match a single call). Chunks are even-sized (alternate shading parity); foot only on the last chunk. exportToPdf.browser.test.ts checks drawing ops are identical to a single call
  • New options onProgress, signal (DOMException 'AbortError', nothing saved), maxRows (default PDF_MAX_ROWS_DEFAULT 20000, dev warning only)

v3.1.0 — significant changes

  • GridColDef.sortComparator(v1, v2, params1, params2): ascending comparator; sees null/undefined (no built-in nulls-last for that column); negated for desc. Sorting utils (lib/utils/sorting) take it through GridSortValue.comparator + params — two items use it only when both carry the same comparator, otherwise the built-in key comparison applies (tree auto-parents, group rows sorted by a non-grouping field). Always call it through callSortComparator (throw / NaN → 0, warn once). Row grouping passes the grouping column's comparator for group rows; pivot uses the source column's for row-label fields
  • Column auto-size: useGridColumnAutosize (hooks/core, after useGridScrollToIndexesApi) measures via lib/utils/columnAutosize (.ogx__cell-content / .ogx__header-cell-content at max-content + cell padding/borders, scoped to the grid's own .ogx root so nested grids are skipped) and writes through handleColumnResize. Installs apiRef.autosizeColumn / autosizeColumns; ColumnResizeHandle calls it on double-click and Enter. Only rendered rows are measured

v3.0.1 — significant changes

  • Without a height prop the root gets ogx--fill (height: 100%; flex: 1 1 auto; min-height: 0) and fills its container; in an auto-height container it still grows to fit. The unbounded warning in useGridDevWarnings also requires the viewport to be at least as tall as the rows
  • The list view does not virtualize; useGridDevWarnings warns above LIST_VIEW_WARN_THRESHOLD (2000) items
  • The column resize handle lies inside its header cell (the cell clips overflow); right-pinned columns take margin-left: auto on their first cell and .ogx__content is max(100%, totalWidth) when right-pinned columns exist
  • GridApi<R> / useGridApiRef<R>() type the row getters; apiRef prop accepts MutableRefObject<GridApi<R>> | MutableRefObject<GridApi>
  • .ogx-list-view__rows has tabIndex={-1} (Firefox makes scroll containers Tab stops). CI (deploy + publish workflows) installs and runs Chromium, Firefox and WebKit
  • Demo pages: a <DataGrid> without height now stretches to its parent; give demo grids an explicit height (or a sized wrapper) unless filling is intended

v3.0.0 — significant changes (breaking)

Correctness release; full list in CHANGELOG.md, consumer guide in docs/migration/v2-to-v3.md. Facts future work needs:

  • Rows are never modified. No id or underscore fields are written; the row store maps rows to ids via getRowId and idByRow (useGridRowIdOf). Hierarchy info lives only in rowMetaMap
  • Generics use GridValidRowModel (object), not GridRowModel, so consumer interfaces work; DataGrid and exports have typed and untyped-columns overloads
  • Read cell values through getCellValue (lib/utils/values.ts): it applies valueGetter and contains throws (value undefined, dev warning once per column). Don't call valueGetter directly
  • Consumer callbacks must be contained (throws fall back to defaults); synthetic rows (group, subtotal, auto-parent, pivot Grand Total) are never selectable and never start or join row spans
  • DataGrid.tsx hook layout is documented in docs/architecture/datagrid-orchestration.md
  • npm run typecheck (lib, tests, demo) is a gate alongside lint and build

v2.1.0 — significant changes

  • Row-grouping aggregation now shares lib/utils/aggregation (nulls ignored, unique, availableAggregationFunctions). Expansion in both useRowGrouping and useTreeData is derived state (depth default plus user overrides keyed by config); no effect resets it on row changes. Never call callbacks such as onRowExpansionChange inside a setState updater
  • Hierarchy renderers injected by useGridColumns must render params.formattedValue, not params.value, or valueFormatter is lost; Cell passes formattedValue into renderCell params
  • slots.footer / noRowsOverlay / loadingOverlay are rendered (they were typed-only). footer replaces the pagination area and receives grid state
  • New: onRowDoubleClick, exported ColumnVisibilityPanel, ExcelAdvancedExportOptions.groupedRows
  • Row indices: renderContext and layout.cumulativeHeights cover center (unpinned) rows only; allRenderableRows indices include top-pinned rows. Convert with pinnedTopRows.length. Vertical scroll-into-view goes through lib/utils/scroll (scrollRowIntoView), which accounts for the sticky header and pinned rows
  • Virtualization needs a bounded container height. In an unbounded container (e.g. flex child without min-height: 0) the viewport grows to content and every row renders; pagination masks it and grouping disables pagination. useGridDevWarnings warns about both

v2.0.0 — significant changes (breaking)

Breaking: removed dead public API surface

The following props existed in v1.x but had no runtime implementation. They have been removed from the public TypeScript types. (GridColDef.description was not removed — it renders as the header title tooltip.)

Removed Type Reason
onPinnedRowsChange DataGridProps No pin/unpin-row UI exists; callback was never called
onRowGroupingModelChange DataGridProps No drag-to-group UI; callback was never called

New: density prop wired

density?: 'compact' | 'standard' | 'comfortable' now controls effectiveRowHeight (compact=32, standard=rowHeight, comfortable=72) via CSS --ogx-row-height. Previously the prop was accepted but silently discarded.

New: disableRowSelectionOnClick and disableMultipleRowSelection

Both props are now wired in handleRowClick. disableRowSelectionOnClick suppresses click-to-select; disableMultipleRowSelection caps selection to a single row.

New: groupingColDef implemented

When rowGroupingModel is active, passing groupingColDef now creates a dedicated synthetic __group__ column at position 0, auto-pinned left. Previously the prop was accepted but had no runtime effect.

New: groupable: false honored

Columns with groupable: false are now skipped when building the grouping tree in useRowGrouping. Previously this flag was silently ignored.

New: groupingValueFormatter on GridColDef

groupingValueFormatter?: (params: { field: string; value: unknown }) => string

Customizes the label for group-header rows at that level. Falls back to "${field}: ${value}" when omitted. The formatted string flows through GridRowMeta.groupLabel so useGridColumns picks it up for rendering.

New: availableAggregationFunctions honored

Per-column availableAggregationFunctions?: string[] now gates which aggregation functions are computed in useAggregation. If the current aggregationModel entry for a field uses a function not in that list, the aggregation is skipped for that field.

New: Multi-sort shift-click

Shift-clicking a column header now appends to (or removes from) sortModel rather than replacing it. The sort priority badge (1, 2, 3…) appears next to the sort arrow when more than one sort key is active. Plain click still replaces with a single-key sort.


v1.3.0 — significant changes

  • GridApi.getGroupedExportRows() — new method returning a flat GridGroupedExportRow[] ordered list by traversing the active row-grouping tree (group-header → children → subtotal → grand-total). Returns null when row grouping is not active.
  • GridGroupedExportRow type — public interface for each entry: type (group-header | leaf | group-subtotal | grand-total), depth, optional groupField, groupValue, aggregatedValues, row.
  • Grouped export across all formats — exportToCsv, exportToExcel, exportToJson, printGrid, exportToPdf all accept groupedRows?: GridGroupedExportRow[]; flat behavior unchanged when absent.

v1.2.3 — significant changes

  • hideable: false columns excluded from Columns panel — ColumnVisibilityPanel now filters out columns with hideable: false by default, removing the permanently-disabled rows that added visual noise. Columns still render in the grid body and exports unchanged.
  • GridToolbarProps.showNonHideableColumns — new optional boolean prop. Pass true to restore the old behavior (show non-hideable columns as disabled rows in the panel). Default: false.

v1.2.2 — significant changes

  • CellErrorBoundary CellRenderTarget pattern — renderFn is now called inside a module-scope CellRenderTarget child component so the throw happens in a descendant; boundaries cannot catch errors in their own render()
  • resetKey={row} — error boundary resets when the row object reference changes (not just the cell value), so "Restore" correctly clears error state after fresh row objects are provided
  • getAllFilteredRows() API — new GridApi method returning all filtered+sorted rows regardless of pagination; use for full-dataset exports
  • PDF aggregation footer — PdfExportDemo now computes the aggregation directly from exported rows (bypasses useEffect-based getAggregationResult); footer now reliably appears with correct values
  • PDF footer text color — aggregation footer row text is now explicitly black ([0,0,0]) instead of inheriting the default (which could render invisible against the light gray fill)
  • exportToPdf standalone API — uses autoTable(doc, opts) standalone function instead of doc.autoTable(opts) to avoid unreliable ESM prototype patching

v1.2.1 — significant changes

  • exportToPdf — native PDF export via optional peer deps jspdf + jspdf-autotable; lazy-loaded, zero bundle impact for non-PDF consumers
  • PdfExportOptions — 12-field interface: fileName, title, logoUrl, orientation, selectedRows, aggregationResult, aggregationModel, filterModel, alternateRowColor, headerBackgroundColor, headerTextColor, fontSize
  • CellErrorBoundary fix — changed to renderFn prop so the boundary calls renderCell inside its own render(); previously the throw occurred in the parent before the boundary could catch it
  • GitHub Pages SPA routing — deploy workflow now copies 404.html so direct URL access to demo routes works

v1.1.0 — significant changes

  • GridRowMeta — hierarchy metadata moved off GridRowModel into Map<GridRowId, GridRowMeta>; exposed via params.rowMeta in renderCell
  • CellErrorBoundary — renderCell errors now caught per-cell; grid continues rendering
  • GridLocaleText / localeText prop — all pagination strings are now overrideable for i18n
  • Duplicate DOM ID fix — Row checkbox and FilterPanel inputs now use useId() (was ogx-select-row-${row.id}, caused duplicates with two grids on one page)