apexgrid-skill 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.cursorrules CHANGED
@@ -4,7 +4,8 @@ You are working with `apex-grid`, a Lit-based web-component data grid by ApexCha
4
4
  Documentation: https://github.com/apexcharts/apexgrid
5
5
  NPM: apex-grid (with a hyphen — no `apexgrid` package exists)
6
6
  GitHub: https://github.com/apexcharts/apexgrid
7
- Confirmed versions: apex-grid@3.6.0, lit@^3.0.0
7
+ Confirmed versions: apex-grid@4.0.0, lit@^3.3.0 (apex-grid-enterprise@0.9.0 needs apex-grid 4.0).
8
+ Upgrading from 3.x: see "Changed in 4.0" at the end.
8
9
 
9
10
  ## Quickstart — register + size the host
10
11
 
@@ -17,7 +18,10 @@ height or it collapses and renders nothing. `setup()` handles both.
17
18
  npm install apex-grid lit
18
19
  ```
19
20
  That is the whole install: `apex-grid` brings `lit`, `@lit/context` and `@lit-labs/virtualizer`.
20
- There is no theme CSS to import from anywhere (3.5.0 dropped the old Ignite UI dependencies).
21
+ There is no theme CSS to import for a styled grid (3.5.0 dropped the old `igniteui-webcomponents`
22
+ dependency). An Ignite UI theme never fully themed the grid: 3.x only took its brand and font. From
23
+ 4.0 the only way to follow an Ignite UI theme is the free `apex-grid/ignite-ui` add-on (not in 3.x;
24
+ see "Ignite UI theming" below).
21
25
 
22
26
  ### 2. Register + size — `setup()` does both
23
27
  ```ts
@@ -40,6 +44,25 @@ and IGNORES the light ones (`--ag-surface`, ...), even when set on the dark grid
40
44
  Brand, sizing and typography tokens are shared. In your own CSS, `var(--ag-brand)` has no
41
45
  root default: write `var(--ag-brand, #0d6fd3)`.
42
46
 
47
+ ### Ignite UI theming (new in 4.0)
48
+ `import 'apex-grid/ignite-ui';` once (free, no licence key; ES modules only, no script-tag file).
49
+ Every grid on the page, shadow roots included, then follows the Ignite UI theme: brand, text,
50
+ gridlines, pinned edge, header (`--ig-surface-raised`, 7.5+), state colours, font, enterprise popups.
51
+ - Ranking: `--ag-*` you set > family `--apx-*` tokens > Ignite UI > built-in values
52
+ (family-wide: explicit option > product CSS variable > `--apx-*` > named theme/palette > `--ig-*` > defaults).
53
+ - Mode: no `theme` attribute follows `--ig-theme-variant` live; `theme="light"`/`"dark"` pins the mode
54
+ (a pinned `light` blocks the dark variant).
55
+ - Per grid: `design-system="off"` (`designSystem = false`) drops the Ignite UI colours and font;
56
+ `tokens="off"` (`tokens = false`) drops `--apx-*` and Ignite UI, variant included.
57
+ - Live: no `refreshTokens()` needed. With the add-on, the Ignite-following tokens leave the root
58
+ defaults: page CSS reading them needs a fallback, `var(--ag-text, #111827)`.
59
+ - New tokens `--ag-overlay`, `--ag-overlay-text` (dark: `--ag-dark-overlay`, `--ag-dark-overlay-text`)
60
+ colour the enterprise popups (apex-grid-enterprise 0.9.0); no default.
61
+ - `--ag-font` you set now wins over family and Ignite UI fonts and has no root default: `var(--ag-font, Inter, sans-serif)`.
62
+ - BREAKING (4.0): the grid no longer reads `--ig-*` by itself (3.x took the brand and font with no
63
+ import). Without the add-on an Ignite UI stylesheet changes nothing. The 4.0 grid also follows
64
+ `--apx-*` set on `:root`.
65
+
43
66
  ### Minimal example
44
67
 
45
68
  ```ts
@@ -95,7 +118,7 @@ render(
95
118
  1. **Apex Grid is a Lit web component, not a JS class.** Don't `new ApexGrid(...)`. Render `<apex-grid>` and set `.data` / `.columns` as properties.
96
119
  2. **Two setup steps:** register the element and give the host a bounded height — `setup()` does both. **No theme CSS import is needed** (the grid self-styles via `--ag-*`).
97
120
  3. **`columns` and `data` are properties, not attributes.** Use property binding.
98
- 4. **`column.key` must be a real `keyof T`.** No aliases. Compute derived values in `cellTemplate` (its `row` is the row element; the record is `row.data`).
121
+ 4. **`column.key` must be a real `keyof T`.** No aliases. Compute derived values in `cellTemplate` from `ctx.data`, the record (4.0); `ctx.row` is the row element, which recycling re-binds to other records.
99
122
  5. **Set `type` per column.** Primitive `'string'`/`'number'`/`'boolean'` drive sort/filter/editing (default `'string'`). Presentation types also exist: `'select'`, `'rating'`, `'date'`, `'image'`, `'currency'`, `'avatar'`, `'badge'`, `'progress'`, `'sparkline'`, `'status'`.
100
123
  6. **`cellTemplate` and `headerTemplate` must return Lit `TemplateResult`s** (`html\`...\``). Strings render as escaped text.
101
124
  7. **Sort/filter events fire only for UI-initiated operations.** Programmatic `grid.sort(...)` / `grid.filter(...)` are silent.
@@ -135,9 +158,13 @@ Applied defaults: `{ type: 'string', resizable: false, hidden: false, sort: fals
135
158
  ```ts
136
159
  import { html } from 'lit';
137
160
 
138
- cellTemplate: ({ value, row, parent, column }) =>
139
- html`<strong>${value}</strong>`,
161
+ cellTemplate: ({ value, data, column, row, parent }) => // data = the record (4.0)
162
+ html`<strong>${value}</strong> <small>#${data.id}</small>`,
140
163
  ```
164
+ Body rows are recycled by default (4.0, `rowRecycling`): a row element can show another record after a
165
+ scroll, sort or filter. Read `ctx.data`, never hold `ctx.row` / `ctx.parent` / `grid.rows` across an
166
+ `await`, keep no state in template elements (or wrap in `keyed(ctx.data, ...)` from
167
+ `lit/directives/keyed.js`), or opt out with `grid.rowRecycling = false` / `row-recycling="false"`.
141
168
 
142
169
  ### Auto-generation — 3-line demo
143
170
 
@@ -176,14 +203,20 @@ grid.filter([
176
203
  | `columns` | `ColumnConfiguration<T>[]` | |
177
204
  | `autoGenerate` | `boolean` | Ignored if `columns` non-empty |
178
205
  | `sortConfiguration` | `{ multiple, triState }` | Multi-column + tri-state toggles |
179
- | `dataPipelineConfiguration` | `{ sort?, filter?, pagination?, quickFilter? }` | Server-side hooks |
206
+ | `dataPipelineConfiguration` | `{ sort?, filter?, quickFilter? }` | Server-side hooks (4.0 removed `pagination`: use `pagination.mode: 'remote'`) |
180
207
  | `selection` / `editing` / `pagination` / `tree` / `expansion` | config objects | Opt-in: `{ enabled, ... }` (see SKILL.md §5) |
208
+ | `rowRecycling` (attr `row-recycling`) | `boolean` | Default `true` (4.0); `false` keeps record-keyed rows |
209
+ | `showHeader` (attr `show-header`) | `boolean` | Default `true`; `false` (4.0) hides the header rows and filter panel |
181
210
  | `quickFilter` / `columnReordering` | `string` / `boolean` | Global search / drag-reorder headers |
182
211
  | `sortExpressions` / `filterExpressions` | array (get/set) | Assigning REPLACES (3.6); `[]` clears. `sort()` / `filter()` add |
183
212
  | `selectedRows` / `expandedRows` | `T[]` (get/set) | Snapshots; set to replace |
184
213
  | `rows` (getter) | `ApexGridRow<T>[]` | Currently rendered only (virtualized) |
185
214
  | `dataView` / `pageItems` (getters) | `readonly T[]` | Full post-filter/sort / current page |
186
- | `totalItems` / `pageCount` (getters) | `number` | |
215
+ | `totalItems` / `pageCount` (getters) | `number` | With pagination, pinned rows not counted (4.0) |
216
+
217
+ Feature config additions (4.0): `selection.isRowSelectable: (row) => boolean` (rejected rows get no
218
+ checkbox and every selection path skips them, e.g. a pinned total row);
219
+ `pagination.paginator: 'full' | 'compact'` (`'compact'`: summary, previous, page / count, next).
187
220
 
188
221
  | Method | Description |
189
222
  |---|---|
@@ -234,7 +267,8 @@ Built-in pipeline is skipped for whichever operations have a hook. Omit one to k
234
267
 
235
268
  ### 3. Importing a theme CSS that no longer exists
236
269
  ❌ `import 'igniteui-webcomponents/themes/.../bootstrap.css'` + `configureTheme(...)` — obsolete; no effect.
237
- ✅ No import needed — grid self-styles; retheme via `--ag-*`. The only required CSS is host height.
270
+ ✅ No import needed for a styled grid; retheme via `--ag-*`. The only required CSS is host height.
271
+ To follow an Ignite UI theme, `import 'apex-grid/ignite-ui'` (4.0), not a theme import.
238
272
 
239
273
  ### 4. Stringified attribute
240
274
  ❌ `<apex-grid columns="[...]">`
@@ -242,7 +276,7 @@ Built-in pipeline is skipped for whichever operations have a hook. Omit one to k
242
276
 
243
277
  ### 5. `column.key` not a `keyof T`
244
278
  ❌ `{ key: 'fullName' }` when not in `T`
245
- ✅ `cellTemplate: ({ row }) => html\`${row.data.firstName} ${row.data.lastName}\`` (`row` is the element)
279
+ ✅ `cellTemplate: ({ data }) => html\`${data.firstName} ${data.lastName}\`` (`data` is the record; `row` is the element)
246
280
 
247
281
  ### 6. String from `cellTemplate`
248
282
  ❌ `cellTemplate: ({ value }) => \`<b>\${value}</b>\``
@@ -268,6 +302,21 @@ Built-in pipeline is skipped for whichever operations have a hook. Omit one to k
268
302
  ❌ `import ... from 'apexgrid'` — no such package.
269
303
  ✅ `import ... from 'apex-grid'`.
270
304
 
305
+ ### 12. Ignite UI theme without the add-on (4.0)
306
+ ❌ Ignite UI stylesheet on the page, no `apex-grid/ignite-ui`: `--ig-*` is silently ignored in 4.0.
307
+ ❌ `theme="light"` on the grid while the page switches to a dark Ignite UI theme: blocks the dark variant.
308
+ ❌ `import 'apex-grid/ignite-ui'` on apex-grid 3.x: no such module.
309
+ ✅ apex-grid 4.0, `import 'apex-grid/ignite-ui'` once and leave `theme` unset.
310
+
311
+ ### 13. Treating a row element as a record (4.0)
312
+ ❌ Reading `ctx.row.data` after an `await`, keeping `grid.rows[i]` as a handle, or an `<input>` / widget in a
313
+ `cellTemplate` holding its own state: with recycling the element shows another record after a scroll or sort.
314
+ ✅ Read `ctx.data`; `keyed(ctx.data, html\`...\`)` for stateful templates; or `grid.rowRecycling = false`.
315
+
316
+ ### 14. Reading the grid role from the host (4.0)
317
+ ❌ `grid.getAttribute('role')` / `aria-rowcount` on `<apex-grid>`: they moved.
318
+ ✅ `grid.shadowRoot.querySelector('[part~="table"]')`. An `aria-label` on the host still names the grid.
319
+
271
320
  ---
272
321
 
273
322
  ## New in 3.1–3.3
@@ -304,6 +353,37 @@ Built-in pipeline is skipped for whichever operations have a hook. Omit one to k
304
353
 
305
354
  ---
306
355
 
356
+ ## Changed in 4.0 (upgrading from 3.x)
357
+
358
+ Breaking:
359
+ - Body rows are recycled by default (`rowRecycling`): read `ctx.data` in templates, or set
360
+ `rowRecycling = false` / `row-recycling="false"` for record-keyed rows.
361
+ - `role` (`grid` / `treegrid`), `aria-rowcount`, `aria-colcount`, `aria-multiselectable` and the
362
+ accessible name moved from the host to the inner `[part~="table"]` element.
363
+ - An Ignite UI theme does nothing until `import 'apex-grid/ignite-ui'`; `--ag-font` has no root default.
364
+ - Pagination: pinned rows no longer count toward `totalItems` / pages; a local page change no longer runs
365
+ custom sort / filter hooks; `DataPipelineConfiguration.pagination` and `DataPipelineParams.type`
366
+ `'pagination'` are removed (types only; the hook was never called).
367
+ - `min()` / `max()` fail a value that is not a number (3.x passed `'abc'`).
368
+ - Types / DOM: `ctx.data` is required in the cell context; `ApexGridPaginator.pageSizeOptions` is
369
+ `readonly number[]`; the paginator's previous button id is `prev`; protected
370
+ `ApexGrid.headerRowCount` and `ApexGridHeaderRow.renderDragGhost` removed.
371
+
372
+ New:
373
+ - `showHeader` (`show-header="false"`), `selection.isRowSelectable`, `pagination.paginator: 'compact'`.
374
+ - `apex-grid::part()` for every paginator part (`paginator`, `paginator-size`, `paginator-info`,
375
+ `paginator-controls`, `paginator-page`, `paginator-button`, `paginator-first` / `-prev` / `-next` /
376
+ `-last`) and toolbar part (`toolbar`, `toolbar-search`, `search-field`, `search-icon`, `search-input`,
377
+ `toolbar-actions`, `toolbar-action`, `export`, `export-trigger`, `export-menu`, `export-menu-item`, ...).
378
+ - `ctx.data` in cell and editor templates; a cell template's `ctx.commit` writes to it.
379
+ - IME input in the cell editor, filter panel and quick filter; the filter panel and `min()` / `max()` read
380
+ full-width digits and the column `locale`'s grouping (`1.234,5` under `de-DE`) and currency marks.
381
+ - Locale keys: `toolbar.exportCsv`, `filter.and` / `filter.or`, `cell.rating`, `cell.true` / `cell.false`,
382
+ `validation.required` / `.min` / `.max` / `.pattern`; `ValidatorContext.localize(key, params?)`.
383
+ - Ignite UI add-on (`apex-grid/ignite-ui`) and the family `--apx-*` tokens.
384
+
385
+ ---
386
+
307
387
  ## Dev-server reminder
308
388
 
309
389
  ES module imports require an HTTP server — opening with `file://` fails CORS. Use Vite (`npm create vite@latest`) or any static server.
package/README.md CHANGED
@@ -20,13 +20,14 @@ AI models routinely get web-component grid code wrong: they treat `<apex-grid>`
20
20
 
21
21
  ### Coverage
22
22
 
23
- - **Quickstart** — register + size the host (`setup()` does both); no theme import — the grid self-styles via `--ag-*` CSS variables
23
+ - **Quickstart**: register + size the host (`setup()` does both); no theme import, since the grid self-styles via `--ag-*` CSS variables; the free `apex-grid/ignite-ui` add-on (apex-grid 4.0) follows an Ignite UI theme
24
+ - **Upgrading from 3.x to 4.0**: row recycling and `ctx.data`, the grid role on `[part~="table"]`, pagination and validator changes
24
25
  - **Custom-element registration** — `setup()`, `import 'apex-grid/define'`, or `ApexGrid.register()`
25
26
  - **Generic `ColumnConfiguration<T>`** — `key`, the 13 column `type`s (incl. `date`, `select`, `currency`, …), templates, `sort`, `filter`, `pinned`, explicit widths
26
27
  - **Lit cell, header & editor templates** with the right context shapes
27
28
  - **Sort, filter & quick-filter** — programmatic API, UI events, operands, multi-column
28
29
  - **Editing, selection, pagination, tree & master-detail** — the opt-in feature config objects
29
- - **Server-side data** — `dataPipelineConfiguration` async hooks (sort/filter/pagination/quickFilter)
30
+ - **Server-side data**: `dataPipelineConfiguration` async hooks (sort/filter/quickFilter) and remote pagination
30
31
  - **CSV export & column reordering/pinning**
31
32
  - **Virtualization** — `grid.rows` vs `grid.dataView` vs `grid.totalItems`
32
33
  - **Vanilla JS** — full setup without using Lit's `render()` in app code
package/SKILL.md CHANGED
@@ -5,17 +5,20 @@ description: >
5
5
  data grid by ApexCharts). Use whenever the user asks to create, configure, or
6
6
  troubleshoot a sortable / filterable / paginated / editable / virtualized data
7
7
  table with `<apex-grid>`. Covers the register-and-size setup (no theme import —
8
- the grid styles itself via `--ag-*` CSS variables), the generic
8
+ the grid styles itself via `--ag-*` CSS variables), the optional free
9
+ `apex-grid/ignite-ui` add-on that follows an Ignite UI theme, the generic
9
10
  `ColumnConfiguration<T>` shape with its 13 column types, Lit `cellTemplate`
10
- requirements, theming and dark mode (`--ag-*` / `--ag-dark-*` tokens,
11
- `theme="dark"`), sorting / filtering / quick-filter, pagination, inline
12
- editing, row selection, master-detail and tree rows, CSV export, batch writes
13
- via `applyEdits`, right-to-left layout, the UI-only event model, and the
14
- `dataPipelineConfiguration` hooks for server-side data.
11
+ requirements (read the record from `ctx.data`; body rows are recycled),
12
+ theming and dark mode (`--ag-*` / `--ag-dark-*` tokens, `theme="dark"`),
13
+ sorting / filtering / quick-filter, pagination (full or compact paginator),
14
+ inline editing, row selection, master-detail and tree rows, CSV export, batch
15
+ writes via `applyEdits`, right-to-left layout, the UI-only event model, the
16
+ `dataPipelineConfiguration` hooks for server-side data, and upgrading from
17
+ apex-grid 3.x to 4.0.
15
18
  metadata:
16
19
  author: ApexCharts
17
- version: "2.4.0"
18
- library_version: "3.6.0"
20
+ version: "2.6.0"
21
+ library_version: "4.0.0"
19
22
  category: data-visualization
20
23
  tags: [grid, data-grid, table, web-component, lit, apex-grid]
21
24
  docs: https://github.com/apexcharts/apexgrid
@@ -37,9 +40,9 @@ npm install apex-grid lit
37
40
 
38
41
  That is the whole install. `apex-grid` brings its own runtime dependencies (`lit`, `@lit/context`, `@lit-labs/virtualizer`) and there is **no theme CSS to import** from anywhere.
39
42
 
40
- > **Changed in 3.5.0:** earlier versions also pulled in `igniteui-webcomponents` and `igniteui-theming` transitively. Both are gone. The grid never registered an Ignite UI element, so nothing you wrote against the grid is affected, but code that relied on `igc-*` elements being installed as a side effect must now depend on that package directly.
43
+ > **Changed in 3.5.0:** earlier versions also pulled in `igniteui-webcomponents` and `igniteui-theming` transitively. Both are gone. The grid never registered an Ignite UI element, so nothing you wrote against the grid is affected, but code that relied on `igc-*` elements being installed as a side effect must now depend on that package directly. An Ignite UI theme on the page never fully themed the grid: through 3.x it only tinted the brand and replaced the font. From 4.0 the only way to make the grid follow an Ignite UI theme is the `apex-grid/ignite-ui` add-on (§1.3).
41
44
 
42
- > Confirmed working versions: `apex-grid@3.6.0`, `lit@^3.0.0`.
45
+ > Confirmed working versions: `apex-grid@4.0.0`, `lit@^3.3.0` (and `apex-grid-enterprise@0.9.0`, which needs apex-grid 4.0). 4.0 is a major version: moving an app from 3.x, read §1.6 first.
43
46
 
44
47
  ### 1.2 Register the element + size the host — `setup()` does both
45
48
 
@@ -100,7 +103,33 @@ apex-grid[theme~='dark'] { --ag-grid-shadow: var(--ag-dark-shadow-card); } /* th
100
103
 
101
104
  **CSS parts.** Since 3.6.0 every part a body row renders is reachable with `apex-grid::part(...)`: `cell`, `selection-cell`, `selection-checkbox`, `expansion-cell`, `expansion-toggle`, `expansion-chevron`, `detail-panel`, `row-number-cell`, `row-number`, `reorder-handle-cell`, `reorder-handle`, `reorder-grip`. Before, only `cell` was. Parts inside your own `detailTemplate` content are reached by listing them in `expansion.exportParts` (`references/state-and-features.md`).
102
105
 
103
- > The deprecated `setup({ theme })` option does **not** change the grid's appearance. It only ever forwarded to Ignite UI's `configureTheme()`, and since 3.5.0 it is inert and warns once. Omit it; use `--ag-*` variables (and the `theme="dark"` attribute) instead.
106
+ Since 4.0.0 the paginator and toolbar parts are reachable too. Paginator: `paginator-host` (the element), `paginator`, `paginator-size`, `paginator-info`, `paginator-controls`, `paginator-page`, `paginator-button` (every button) plus one part per button, `paginator-first`, `paginator-prev`, `paginator-next`, `paginator-last`. Toolbar: `toolbar-host`, `toolbar`, `toolbar-search`, `search-field`, `search-icon`, `search-input`, `toolbar-actions`, `toolbar-action`, `export`, `export-trigger`, `export-icon`, `export-caret`, `export-menu`, `export-menu-item`. Also new: `table` (the element with the grid role) and `drag-ghost` (the column drag ghost).
107
+
108
+ ```css
109
+ apex-grid::part(paginator-button) { border-radius: 999px; }
110
+ apex-grid::part(paginator-next) { font-weight: 600; }
111
+ apex-grid::part(search-input) { font-size: 14px; }
112
+ ```
113
+
114
+ > The deprecated `setup({ theme })` option does **not** change the grid's appearance. It only ever forwarded to Ignite UI's `configureTheme()`, and since 3.5.0 it is inert and warns once. Omit it; use `--ag-*` variables (and the `theme="dark"` attribute) instead, and to follow an Ignite UI theme, the `apex-grid/ignite-ui` add-on below.
115
+
116
+ **Ignite UI theming (new in 4.0.0).** The add-on ships with apex-grid 4.0; on 3.x `import 'apex-grid/ignite-ui'` does not resolve. To make grids follow an Ignite UI theme loaded on the page, import the add-on once. It is free and needs no licence key.
117
+
118
+ ```ts
119
+ import 'apex-grid/ignite-ui';
120
+ ```
121
+
122
+ Every grid on the page, in the document or any shadow root, then follows the theme, including grids that connected before the import: the brand (`--ig-primary-*`), text and muted text, gridlines and the pinned-column edge, the header (`--ig-surface-raised`, Ignite UI 7.5+), the state colours, the font, and the `apex-grid-enterprise` popups (`--ig-surface-overlay`, 7.5+). Card and row surfaces keep the grid's own light or dark background. The add-on registers on a shared `globalThis` slot, so it reaches grids from a separately built bundle too. The grid ships as ES modules only: load it from your bundle or a `<script type="module">`.
123
+
124
+ - **Ranking**, highest first: an `--ag-*` token you set > the family `--apx-*` tokens (`--apx-accent`, `--apx-fore`, `--apx-grid`, `--apx-surface`, `--apx-font-family`, `--apx-radius-factor`, also new in that release) > Ignite UI > built-in values. (Across the Apex family: explicit option > product CSS variable > `--apx-*` > named theme/palette > `--ig-*` > defaults.)
125
+ - **Mode:** a grid without a `theme` attribute follows the Ignite UI variant (`--ig-theme-variant`) declared on `:root` or an ancestor, live, on a theme swap too. `theme="light"` keeps a grid light and `theme="dark"` keeps it dark, so do not pin `theme="light"` if the grid should go dark with a dark Ignite UI theme.
126
+ - **Per grid:** `design-system="off"` (property `designSystem = false`) turns the Ignite UI colours and font off (the variant still picks the mode); `tokens="off"` (`tokens = false`) turns off the `--apx-*` tokens and Ignite UI together, the variant included, for the grid and the content it renders.
127
+ - **No `refreshTokens()` needed:** the grid follows the cascade live.
128
+ - **Your own CSS:** while the add-on is loaded, the tokens that follow Ignite UI (text, lines, surfaces, header, state colours, pinned edge) leave the root defaults, so page CSS that reads one needs its own fallback: `var(--ag-text, #111827)`.
129
+ - **Enterprise popups:** new tokens `--ag-overlay` and `--ag-overlay-text` (dark grids: `--ag-dark-overlay`, `--ag-dark-overlay-text`) colour the `apex-grid-enterprise` popups (apex-grid-enterprise 0.9.0); unset, each keeps its built-in colours. For the enterprise chart panel to read the theme itself, also import `apexcharts/features/ignite-ui` (ApexCharts 8.8.0 or later; the grid never loads it).
130
+ - **`--ag-font`:** in 4.0 an `--ag-font` you set wins over the family and Ignite UI fonts, and it no longer has a root default (the built-in Inter stack is unchanged inside the grid), so page CSS that reads it needs a fallback, as with `--ag-brand`.
131
+
132
+ > **Breaking in 4.0.0:** the grid no longer reads Ignite UI tokens by itself. Through 3.x an Ignite UI theme on the page re-tinted the brand (`--ig-primary-500`, `--ig-primary-700`) and replaced the font (`--ig-font-family`) with no import. From 4.0 an Ignite UI stylesheet changes nothing until you `import 'apex-grid/ignite-ui'`, which brings the brand and font back along with the rest of the theme. The 4.0 grid also follows the family `--apx-*` tokens: a page that sets them on `:root` for its charts sees the grid follow them too (set the matching `--ag-*` tokens to keep the grid as it was).
104
133
 
105
134
  ### 1.4 Minimal example — a styled, sortable, filterable table
106
135
 
@@ -146,6 +175,20 @@ If rows don't render at all, it's almost always the host height (§1.2). If the
146
175
 
147
176
  > **Dev-server reminder.** ES module imports need an HTTP server — opening via `file://` fails with CORS errors. Use Vite (`npm create vite@latest`), any static server, or your framework's dev server.
148
177
 
178
+ ### 1.6 Upgrading from 3.x (4.0.0)
179
+
180
+ 4.0 is a major version. Check these when moving an app from 3.6:
181
+
182
+ - **Body rows are recycled by default** (`rowRecycling`, default `true`). A scroll, sort or filter re-binds the rendered row elements to the records now in view, so a row element and its cells can show another record later. In `cellTemplate` / `editorTemplate`, read the record from `ctx.data` (not `ctx.row.data`), derive everything from the context on each render, and do not hold `ctx.row`, `ctx.parent` or `grid.rows` elements across an `await` or a scroll. A template that keeps element state (an input's typed value, a third-party widget) can wrap itself in `keyed(ctx.data, ...)` (`lit/directives/keyed.js`), or opt the grid out with `grid.rowRecycling = false` (attribute `row-recycling="false"`) for record-keyed rows. Detail panels follow their record and need no change.
183
+ - **The grid role moved off the host.** `role` (`grid`, or `treegrid` for tree, master-detail and grouped grids), `aria-rowcount`, `aria-colcount`, `aria-multiselectable` and the accessible name are on the inner `[part~="table"]` element. An `aria-label` / `aria-labelledby` on the host still names the grid and `aria-describedby` describes it. Tests that read these from the host query `grid.shadowRoot.querySelector('[part~="table"]')`.
184
+ - **An Ignite UI theme does nothing until you `import 'apex-grid/ignite-ui'`** (3.x took its brand and font on its own), and **`--ag-font` has no root default**: page CSS that reads it writes `var(--ag-font, Inter, sans-serif)` (§1.3).
185
+ - **Pagination.** Pinned rows no longer count toward `totalItems`, `pageCount` or the summary (they show on every page). A local page change no longer reruns the pipeline, so custom `sort` / `filter` hooks are not called on it (remote mode still reruns). `DataPipelineConfiguration.pagination` and the `'pagination'` value of `DataPipelineParams.type` are removed: the hook was never called, but TypeScript that sets it stops compiling. For server paging use `pagination.mode: 'remote'` (`references/data-pipeline.md`).
186
+ - **`min()` and `max()` fail a value that is not a number.** On 3.x text such as `'abc'` passed both. Empty values still pass.
187
+ - **Smaller type and DOM changes:** `ctx.data` is a required field of the cell context (code that builds one by hand supplies it), `ApexGridPaginator.pageSizeOptions` is `readonly number[]`, the paginator's previous button id is `prev` (was `previous`), and the protected `ApexGrid.headerRowCount` and `ApexGridHeaderRow.renderDragGhost` are gone (subclasses only).
188
+ - **Noticeable, not breaking:** `pageItems` keeps the same array while nothing that shapes it changes; the column header row is no longer a Tab stop; the column menu opens in the top layer (it can extend outside the grid's box); a page change is announced once.
189
+
190
+ New in 4.0: `showHeader`, `selection.isRowSelectable`, `pagination.paginator: 'compact'` (§5), `::part()` for every paginator and toolbar part (§1.3), `ctx.data` (§4), the Ignite UI add-on and the family `--apx-*` tokens (§1.3), IME input and numbers typed in full width or with the locale's grouping (§5), and locale keys for text that stayed English (`references/state-and-features.md`).
191
+
149
192
  ---
150
193
 
151
194
  ## 2. Naming — `apex-grid` vs `<apex-grid>` vs `ApexGrid`
@@ -168,7 +211,7 @@ AI models trained before the package was published may suggest `apexgrid` (no hy
168
211
 
169
212
  3. **`columns` and `data` are properties, not attributes.** Use property binding (`.columns=${...}` in Lit, `[columns]=` in Angular, `:columns.prop=` in Vue, `el.columns = ...` in vanilla JS). A stringified `columns="[...]"` HTML attribute becomes the literal string and the grid renders nothing.
170
213
 
171
- 4. **`column.key` must be a real `keyof T`.** No `field` / `dataIndex` / `accessor` aliases. To display a derived value, compute it in `cellTemplate`, whose `row` is the `<apex-grid-row>` element: the record is `row.data`.
214
+ 4. **`column.key` must be a real `keyof T`.** No `field` / `dataIndex` / `accessor` aliases. To display a derived value, compute it in `cellTemplate` from `ctx.data`, the record the cell renders (4.0). `ctx.row` is the `<apex-grid-row>` element, which recycling can re-bind to another record.
172
215
 
173
216
  5. **Set `type` per column.** The primitive types `'string'`, `'number'`, `'boolean'` drive the default sort comparer, filter operands, and editor. The default is `'string'` — leaving a numeric column unset sorts as strings ("10" before "9"). There are also presentation types — `'select'`, `'rating'`, `'date'`, `'image'`, `'currency'`, `'avatar'`, `'badge'`, `'progress'`, `'sparkline'`, `'status'` — that change rendering while sorting/filtering as their underlying value (§4).
174
217
 
@@ -210,7 +253,7 @@ interface ColumnConfiguration<T extends object, K extends keyof T = keyof T> {
210
253
  shape?: 'square' | 'circle'; // 'image' crop (default 'square')
211
254
  alt?: string; // 'image' alt text
212
255
  currency?: string; // 'currency' ISO 4217 code (default 'USD')
213
- locale?: string; // 'currency' BCP 47 locale
256
+ locale?: string; // 'currency' BCP 47 locale; 4.0: also how numbers typed into the filter panel, or checked by min() and max(), are read (enterprise: paste, advanced filter)
214
257
  badgeVariant?: 'gold' | 'brand' | 'neutral' | 'muted' | ((v) => …); // 'badge'
215
258
  statusVariant?: 'active' | 'trial' | 'churn' | ((v) => …); // 'status'
216
259
  showDelta?: boolean; // 'sparkline' trailing delta label (default true)
@@ -258,14 +301,16 @@ grid.filter({ key: 'price', condition: 'greaterThan', searchTerm: 100 }); // a
258
301
 
259
302
  ### Cell template
260
303
 
261
- Receives `ApexCellContext<T, K>` (`{ parent, row, column, value, commit? }`, where `row` is the `<apex-grid-row>` element and `row.data` the record). Must return a Lit `TemplateResult`:
304
+ Receives `ApexCellContext<T, K>` (`{ data, value, column, parent, row, commit? }`). `data` (4.0) is the record the cell was rendered for; `parent` and `row` are the cell and `<apex-grid-row>` elements. Must return a Lit `TemplateResult`:
262
305
 
263
306
  ```ts
264
307
  import { html } from 'lit';
265
308
 
266
- { key: 'name', cellTemplate: ({ value, row }) => html`<strong>${value}</strong>` }
309
+ { key: 'name', cellTemplate: ({ value, data }) => html`<strong>${value}</strong> <small>#${data.id}</small>` }
267
310
  ```
268
311
 
312
+ > **Changed in 4.0.0:** body rows are recycled by default (`rowRecycling`), so a row element and its cells can show another record after a scroll, sort or filter. Read the record from `ctx.data`, which stays that record even after an `await`; `ctx.row.data` is whatever the element shows now. Do not keep state in the elements a template creates, or wrap it in `keyed(ctx.data, ...)`, or set `grid.rowRecycling = false` (§1.6).
313
+
269
314
  Since 3.6.0 a `cellTemplate` runs again for every visible row after each pipeline run (sort, filter, edit, quick filter, tree toggle), so a template that reads other fields of its row stays current. Keep templates cheap.
270
315
 
271
316
  ### Auto-generated columns — the 3-line demo
@@ -309,7 +354,9 @@ grid.sort({ key: 'name', direction: 'ascending' }); // programmatic (silen
309
354
  | `columns` | `ColumnConfiguration<T>[]` | Each entry merged with column defaults. |
310
355
  | `autoGenerate` (attr `auto-generate`) | `boolean` | Ignored if `columns` is non-empty. |
311
356
  | `sortConfiguration` | `{ multiple, triState }` | Multi-column + tri-state sorting toggles. |
312
- | `dataPipelineConfiguration` | `{ sort?, filter?, pagination?, quickFilter? }` | Server-side / async hooks (§ data-pipeline). |
357
+ | `dataPipelineConfiguration` | `{ sort?, filter?, quickFilter? }` | Server-side / async hooks (§ data-pipeline). 4.0 removed the never-called `pagination` hook: page on the server with `pagination.mode: 'remote'`. |
358
+ | `rowRecycling` (attr `row-recycling`) | `boolean` | Default `true` (4.0): body row elements are re-bound to other records on scroll, sort and filter. `false` (`row-recycling="false"`) keeps record-keyed rows for templates that hold element state (§1.6). |
359
+ | `showHeader` (attr `show-header`) | `boolean` | Default `true`. `false` (4.0) leaves out the column header row, the column group row and the filter panel, so a short grid (a dashboard tile) gives that room to the rows. Screen readers still announce the column names; sort, filter, widths and the page are kept. While hidden, resizing, the column menu, drag reorder and the header filter button are unavailable. |
313
360
  | `quickFilter` (attr `quick-filter`) | `string` | Global substring search across visible columns. |
314
361
  | `showQuickFilter` / `showExport` | `boolean` | Toolbar UI toggles for quick-filter input / export menu. |
315
362
  | `columnReordering` (attr `column-reordering`) | `boolean` | Drag-reorder headers; per-column opt-out via `reorderable`. |
@@ -325,10 +372,10 @@ grid.sort({ key: 'name', direction: 'ascending' }); // programmatic (silen
325
372
  | `exportFormats` *(getter)* | `readonly { id, label }[]` | Export menu formats (CSV in the community grid). |
326
373
  | `sortExpressions` / `filterExpressions` | `…[]` (get/set) | Current state. Assigning **replaces** the whole sort / filter, and `[]` or `null` clears it (3.6; on 3.5 assignment merged and `[]` did nothing). `sort()` / `filter()` add instead. `clearSort()` / `clearFilter()` also clear. |
327
374
  | `selectedRows` / `expandedRows` | `T[]` (get/set) | Snapshots; set to replace (goes through cancellable events). |
328
- | `rows` *(getter)* | `ApexGridRow<T>[]` | **Currently rendered only** (virtualized). |
375
+ | `rows` *(getter)* | `ApexGridRow<T>[]` | **Currently rendered only** (virtualized). With recycling (4.0 default) an element can show another record later: read its `data` each time, never keep it as a handle to a record. |
329
376
  | `dataView` *(getter)* | `readonly T[]` | Full post-filter/sort data. |
330
- | `pageItems` *(getter)* | `readonly T[]` | Current page slice (= `dataView` if no pagination). |
331
- | `totalItems` / `pageCount` *(getters)* | `number` | Row / page totals. |
377
+ | `pageItems` *(getter)* | `readonly T[]` | Current page slice (= `dataView` if no pagination), without pinned rows. Since 4.0 the same array while nothing that shapes it changes. |
378
+ | `totalItems` / `pageCount` *(getters)* | `number` | Row / page totals. With pagination, pinned rows are not counted (4.0); in remote mode `totalItems` is the `pagination.totalItems` you supply. |
332
379
  | `page` / `pageSize` | `number` (get/set) | Current page (0-based) / page size. |
333
380
 
334
381
  ### Feature config properties (opt-in)
@@ -336,8 +383,8 @@ grid.sort({ key: 'name', direction: 'ascending' }); // programmatic (silen
336
383
  | Property | Shape |
337
384
  |---|---|
338
385
  | `editing` | `{ enabled, mode?: 'cell' \| 'row', trigger?: 'click' \| 'doubleClick', history?: { enabled, stackSize? } }` — plus per-column `editable: true` |
339
- | `selection` | `{ enabled, mode?: 'single' \| 'multiple', showCheckboxColumn? }` |
340
- | `pagination` | `{ enabled, mode?: 'local' \| 'remote', page?, pageSize?, pageSizeOptions?, totalItems? }` |
386
+ | `selection` | `{ enabled, mode?: 'single' \| 'multiple', showCheckboxColumn?, isRowSelectable? }`. `isRowSelectable(row)` (4.0) keeps chosen rows (a pinned total row) out of selection: no checkbox, and clicks, Shift/Ctrl clicks, Space, Select all, `selectRow`, `selectedRows = ...` and `setState` skip them; the header checkbox counts only selectable rows. Unset, every row is selectable, pinned rows included |
387
+ | `pagination` | `{ enabled, mode?: 'local' \| 'remote', page?, pageSize?, pageSizeOptions?, totalItems?, paginator?: 'full' \| 'compact' }`. `'full'` (default) drops the page size select, then first / last, when the grid is narrow; `'compact'` (4.0) always shows summary, previous, page / count, next |
341
388
  | `tree` | `{ enabled, getDataPath: (row) => string[], groupColumnKey?, defaultExpanded?, childIndent? }`: flat data, hierarchy derived. Under a filter, the ancestors of a match show as their own rows (3.6) |
342
389
  | `expansion` | `{ enabled, detailTemplate: (ctx) => TemplateResult, isExpandable?, showToggleColumn?, exportParts? }`: master-detail. `exportParts` (3.6) re-exports parts used in the detail content |
343
390
  | `rowPinning` | `{ enabled }` — sticky top/bottom bands via `pinRow()` / `unpinRow()` |
@@ -345,8 +392,10 @@ grid.sort({ key: 'name', direction: 'ascending' }); // programmatic (silen
345
392
 
346
393
  ```ts
347
394
  grid.editing = { enabled: true, mode: 'cell', trigger: 'doubleClick', history: { enabled: true } };
348
- grid.selection = { enabled: true, mode: 'multiple', showCheckboxColumn: true };
349
- grid.pagination = { enabled: true, pageSize: 25 };
395
+ grid.selection = { enabled: true, mode: 'multiple', showCheckboxColumn: true,
396
+ isRowSelectable: (row) => row !== totalRow }; // 4.0
397
+ grid.pagination = { enabled: true, pageSize: 25, paginator: 'compact' }; // 'compact': 4.0
398
+ grid.showHeader = false; // 4.0: no header rows (dashboard tile)
350
399
  grid.rowPinning = { enabled: true };
351
400
  grid.rowReordering = { enabled: true };
352
401
  ```
@@ -437,7 +486,11 @@ Keyboard-driven commit (Enter) or cancel (Escape) returns focus to the cell, so
437
486
 
438
487
  **Editing behavior:** clearing a `number` or `currency` editor now commits `null` (previously it wrote `NaN`). An empty or unparseable numeric input leaves the cell empty rather than poisoning the row. Since 3.6.0 an open editor stays on the record it was opened for when the data, sort, filter or page change under it, and the edit is cancelled if that record leaves the view; row mode edits a staged copy (`references/state-and-features.md`).
439
488
 
440
- **Announcements:** sorting, filtering, selection, paging, row pinning, row reorder, expansion, and undo / redo each announce through a localized live region, and the host applies a localized `aria-label` ("Data grid") plus `aria-multiselectable` when multi-row selection is on. The strings are locale keys (`announce.*`, `header.*`, `grid.label`, `editor.rating`); override them via `localeText` (see `references/state-and-features.md`).
489
+ **Announcements:** sorting, filtering, selection, paging, row pinning, row reorder, expansion, and undo / redo each announce through a localized live region (a page change once, "Page X of N"). The strings are locale keys (`announce.*`, `header.*`, `grid.label`, `editor.rating`, and since 4.0 `cell.rating`, `cell.true` / `cell.false`); override them via `localeText` (see `references/state-and-features.md`).
490
+
491
+ > **Changed in 4.0.0:** the `grid` / `treegrid` role, `aria-rowcount`, `aria-colcount`, `aria-multiselectable` (multi-row selection) and the localized accessible name ("Data grid", `grid.label`) are on the inner `[part~="table"]` element, not the `<apex-grid>` host. It holds only the header rows, the pinned row bands and the body, so the toolbar, paginator, filter panel and live region are no longer read as table content. An `aria-label` / `aria-labelledby` you put on the host still names the grid. The column header row is no longer a Tab stop.
492
+
493
+ **Input methods and typed numbers (4.0):** Japanese, Chinese or Korean input through an IME works in the cell editor, the filter panel and the quick filter: the Enter, Escape and Tab that confirm or cancel a conversion stay with the IME (they do not commit or cancel an edit, close the filter panel or clear the quick filter), and the filter panel and quick filter apply the text once the conversion ends. A number typed in the filter panel, or checked by `min()` / `max()`, is read as a person writes it: full-width digits and signs (`123`, `-5`), the grouping and decimal separator of the column's `locale` (else the runtime locale, so `1.234,5` is 1234.5 under `'de-DE'`), and a leading or trailing `¥`, `$`, `€`, `円` or the column currency's symbol. A plain ASCII decimal reads as before in every locale (`1.000` stays 1 even under `'de-DE'`), and other text splits: the filter panel falls back to its leading number as in 3.x (`12.5%` filters by 12.5, `10²` by 10), while `min()` / `max()` read `12.5%` or `10²` as not a number, so the value fails. Programmatic `filter()` search terms are not parsed this way: pass numbers.
441
494
 
442
495
  **New in 3.5, all automatic:** the grid honours `prefers-reduced-motion` (every transition routes through the `--ag-dur-*` tokens, which zero under the media query), it stays usable under forced colors / Windows High Contrast (the active-cell ring, invalid-cell ring, selected-row bar and focus rings are painted as outlines, which forced colors recolours rather than drops), and interactive targets meet the WCAG 2.2 AA 24x24 minimum. The only visible change is that the paginator bar is 2px taller.
443
496
 
@@ -467,7 +520,7 @@ Arrow-key navigation, column resize and column reorder all follow inline order r
467
520
 
468
521
  ### 3. Importing a theme CSS that no longer exists
469
522
  ❌ `import 'igniteui-webcomponents/themes/light/bootstrap.css'` + `setup({ theme: 'bootstrap' })` is obsolete; it never affected the grid, and as of 3.5.0 the option is inert and that package is not even installed.
470
- ✅ Nothing to import — the grid styles itself; retheme via `--ag-*` CSS variables. The only required CSS is a host **height**.
523
+ ✅ Nothing to import for a styled grid: it styles itself; retheme via `--ag-*` CSS variables. The only required CSS is a host **height**. To follow an Ignite UI theme that the page loads, the path is the `apex-grid/ignite-ui` add-on (apex-grid 4.0, §1.3), not a theme import into the grid.
471
524
 
472
525
  ### 4. Stringified `columns` attribute
473
526
  ❌ `<apex-grid columns="[...]">` — becomes the literal string `"[object Object],..."`.
@@ -475,7 +528,7 @@ Arrow-key navigation, column resize and column reorder all follow inline order r
475
528
 
476
529
  ### 5. `column.key` not a real key of `T`
477
530
  ❌ `{ key: 'fullName' }` when `User` has only `firstName` + `lastName`.
478
- ✅ Compute via template: `cellTemplate: ({ row }) => html\`${row.data.firstName} ${row.data.lastName}\``. The context's `row` is the row element; the record is `row.data`.
531
+ ✅ Compute via template: `cellTemplate: ({ data }) => html\`${data.firstName} ${data.lastName}\``. The context's `data` is the record (4.0); `row` is the row element, not the record.
479
532
 
480
533
  ### 6. String from `cellTemplate`
481
534
  ❌ `cellTemplate: ({ value }) => \`<strong>\${value}</strong>\`` — renders as escaped text.
@@ -505,15 +558,28 @@ Arrow-key navigation, column resize and column reorder all follow inline order r
505
558
  ❌ `apex-grid[theme='dark'] { --ag-surface: #111827; --ag-text: #f9fafb; }`: since 3.6 a dark grid ignores the light colour tokens, even on its own element.
506
559
  ✅ `apex-grid { --ag-dark-surface: #111827; --ag-dark-text: #f9fafb; }` (or on any ancestor). Brand, sizing and typography tokens are shared, so `--ag-brand` still applies to both.
507
560
 
561
+ ### 13. Expecting an Ignite UI theme to restyle the grid without the add-on (4.0)
562
+ ❌ Loading an Ignite UI theme stylesheet and expecting the 4.0 grid to follow it: without `apex-grid/ignite-ui` every `--ig-*` token is silently ignored (3.x picked up the brand and font on its own; 4.0 does not). Also ❌ `theme="light"` on a page that switches to a dark Ignite UI theme: the pinned mode blocks the dark variant. Also ❌ `import 'apex-grid/ignite-ui'` on apex-grid 3.x: the module does not exist there.
563
+ ✅ apex-grid 4.0, then `import 'apex-grid/ignite-ui'` once (free, no licence key) and leave the `theme` attribute off so the grid follows `--ig-theme-variant`. Use `design-system="off"` or `tokens="off"` to opt one grid out.
564
+
565
+ ### 14. Treating a row element as a record (4.0)
566
+ ❌ A click handler in a `cellTemplate` that reads `ctx.row.data` after an `await`, or code that keeps `grid.rows[0]` as a handle: with recycling (the 4.0 default) that element can show another record after a scroll, sort or filter, so the code acts on the wrong row. Also ❌ an `<input>` or third-party widget in a `cellTemplate` that keeps its own typed state: the state shows on another record after a scroll.
567
+ ✅ Read `ctx.data` (it stays the record the cell rendered, across an `await`), keep state in your data rather than in the elements, wrap a stateful template in `keyed(ctx.data, html\`...\`)` (`lit/directives/keyed.js`), or set `grid.rowRecycling = false` for record-keyed rows.
568
+
569
+ ### 15. Reading the grid's ARIA from the host (4.0)
570
+ ❌ `expect(grid.getAttribute('role')).toBe('grid')` or `grid.getAttribute('aria-rowcount')` in a test: the host no longer carries them.
571
+ ✅ `grid.shadowRoot!.querySelector('[part~="table"]')!.getAttribute('role')`. Name the grid with `aria-label` / `aria-labelledby` on the host as before.
572
+
508
573
  ---
509
574
 
510
575
  ## 7. Reference Routing Table
511
576
 
512
577
  | Topic | Reference File |
513
578
  |---|---|
514
- | Column types, templates, auto-generation, per-column styling, header/editor templates | `references/columns-and-templates.md` |
579
+ | Upgrading an app from 3.x to 4.0 | §1.6 |
580
+ | Column types, templates (`ctx.data`, row recycling), auto-generation, per-column styling, header/editor templates | `references/columns-and-templates.md` |
515
581
  | Sort & filter — operands, expressions, multi-column, quick-filter, events | `references/sort-and-filter.md` |
516
- | Server-side data hooks (sort/filter/pagination/quickFilter), virtualization, `dataView` | `references/data-pipeline.md` |
582
+ | Server-side data hooks (sort/filter/quickFilter), remote pagination, virtualization, `dataView` | `references/data-pipeline.md` |
517
583
  | Row pinning / reordering, undo-redo, batch edits (`applyEdits`), validators, row-mode editing, column groups, state & schema (`getState`/`setState`/`getSchema`), localization, export source and CSV escaping, master-detail parts and nested grids (`exportParts`, `ownsEvent`) | `references/state-and-features.md` |
518
584
  | End-to-end vanilla JS (no Lit `render`) | `references/vanilla-js.md` |
519
585
  | Lit, React, Vue, Angular integration | `references/framework-integration.md` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apexgrid-skill",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "AI coding skill for the apex-grid web component — structured reference docs that help AI assistants (Claude, Cursor, Copilot, etc.) generate correct apex-grid code.",
5
5
  "keywords": [
6
6
  "apex-grid",
@@ -36,7 +36,7 @@ interface BaseColumnConfiguration<T extends object, K extends keyof T = keyof T>
36
36
  shape?: 'square' | 'circle'; // 'image' (def 'square')
37
37
  alt?: string; // 'image' alt text
38
38
  currency?: string; // 'currency' ISO 4217 (def 'USD')
39
- locale?: string; // 'currency' BCP 47 locale
39
+ locale?: string; // 'currency' BCP 47 locale; 4.0: also the separators for numbers typed into the filter panel, or checked by min() and max() (enterprise: paste, advanced filter)
40
40
  badgeVariant?: 'gold' | 'brand' | 'neutral' | 'muted' | ((v) => …); // 'badge'
41
41
  statusVariant?: 'active' | 'trial' | 'churn' | ((v) => …); // 'status'
42
42
  showDelta?: boolean; // 'sparkline' (def true)
@@ -100,15 +100,36 @@ Receives `ApexCellContext<T, K>`:
100
100
 
101
101
  ```ts
102
102
  interface BaseApexCellContext<T, K> {
103
- parent: ApexGridCell<T>; // <apex-grid-cell> element
104
- row: ApexGridRow<T>; // <apex-grid-row> element; the record is row.data
103
+ readonly data: T; // 4.0: the record this cell was rendered for; stays it after an await
104
+ parent: ApexGridCell<T>; // <apex-grid-cell> element (may later show another record)
105
+ row: ApexGridRow<T>; // <apex-grid-row> element (may later show another record)
105
106
  column: ColumnConfiguration<T, K>;
106
107
  value: T[K]; // T[K] — fully typed
107
- commit?: (value: T[K]) => Promise<boolean>; // editable columns only: write without opening the editor
108
+ commit?: (value: T[K]) => Promise<boolean>; // editable columns only: write without opening the editor; writes to ctx.data
108
109
  }
109
110
  ```
110
111
 
111
- Since 3.6 a `cellTemplate` runs again for every visible row, body and pinned, after each pipeline run (sort, filter, quick filter, edit, tree toggle), so a template that reads other fields of `row.data` stays current after an edit to another cell. Keep templates cheap; plain and typed cells are not re-rendered this way. In row edit mode, `commit(value)` stages the value with the row edit instead of writing straight to the record (3.6), so `cancelEdit()` rolls it back.
112
+ `ApexEditorContext` extends it with `commit(value)` and `cancel()`, so an `editorTemplate` gets `ctx.data` too. Unlike the cell context's `commit`, the editor's `commit` commits whichever cell is in edit when it is called, so do not call it after an `await` that can outlive the edit.
113
+
114
+ ### Row recycling (Changed in 4.0.0)
115
+
116
+ Body rows are recycled by default (`grid.rowRecycling`, default `true`): a scroll, sort or filter re-binds the rendered row elements to the records now in view instead of building new rows, which makes long jumps and sorts cheaper on large grids. A row element and its cells can therefore show another record later. Templates must:
117
+
118
+ - **derive what they render from the context on each render**, and read the record from `ctx.data` (on 3.x the usual spelling was `ctx.row.data`, which now reads whatever record the element shows at that moment);
119
+ - **not hold `ctx.row` or `ctx.parent` across an `await`** (a `ctx.commit` after an `await` is safe: it writes to `ctx.data`);
120
+ - **not keep state in the elements they create** (an input's typed value, a third-party widget), or wrap the template in `keyed(ctx.data, ...)` so the state never shows on the wrong record.
121
+
122
+ ```ts
123
+ import { html } from 'lit';
124
+ import { keyed } from 'lit/directives/keyed.js';
125
+
126
+ { key: 'notes',
127
+ cellTemplate: (ctx) => keyed(ctx.data, html`<my-rich-note .value=${ctx.value}></my-rich-note>`) }
128
+ ```
129
+
130
+ To keep record-keyed rows (each element stays with its record), opt out: `grid.rowRecycling = false` or `<apex-grid row-recycling="false">`. Detail panel content follows its record and keeps its DOM state without any change, and a grid created inside a `detailTemplate` keeps its own setting. Pinned rows are not recycled, but a pinned band can re-bind its elements (when a row above is unpinned), which is why `ctx.data` is the safe read everywhere.
131
+
132
+ Since 3.6 a `cellTemplate` runs again for every visible row, body and pinned, after each pipeline run (sort, filter, quick filter, edit, tree toggle), so a template that reads other fields of `ctx.data` stays current after an edit to another cell. Keep templates cheap; plain and typed cells are not re-rendered this way. In row edit mode, `commit(value)` stages the value with the row edit instead of writing straight to the record (3.6), so `cancelEdit()` rolls it back.
112
133
 
113
134
  ```ts
114
135
  import { html } from 'lit';
@@ -121,11 +142,11 @@ const columns: ColumnConfiguration<User>[] = [
121
142
  { key: 'name',
122
143
  cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
123
144
 
124
- // Conditional render with row context (row is the element, row.data the record)
145
+ // Conditional render with the record (ctx.data, 4.0)
125
146
  { key: 'subscribed', type: 'boolean',
126
- cellTemplate: ({ value, row }) => html`
147
+ cellTemplate: ({ value, data }) => html`
127
148
  <input type="checkbox" .checked=${value} disabled />
128
- <small>${row.data.name}</small>` },
149
+ <small>${data.name}</small>` },
129
150
 
130
151
  // Prefer the built-in type over a template where one exists:
131
152
  // `avatar` and `rating` are column types, no template needed.
@@ -176,7 +197,7 @@ import { required, min, max, pattern, custom } from 'apex-grid';
176
197
  validators: [custom((value, ctx) => (value === ctx.data.id ? 'Cannot equal id' : null))] }
177
198
  ```
178
199
 
179
- Built-in factories: `required(message?)`, `min(limit, message?)`, `max(limit, message?)`, `pattern(regex, message?)`, `custom(fn)`. `min`/`max`/`pattern` pass on empty / non-numeric values, so compose with `required`. In row edit mode `ctx.data` is the staged row (the record with the row's pending values applied) since 3.6, so a cross-field rule such as `ctx.data.end >= ctx.data.start` sees the row being saved. See `references/state-and-features.md` for the `Validator` / `ValidatorContext` shapes and the `cellValidationFailed` payload.
200
+ Built-in factories: `required(message?)`, `min(limit, message?)`, `max(limit, message?)`, `pattern(regex, message?)`, `custom(fn)`. `min`/`max`/`pattern` pass on empty values, so compose with `required`. **Changed in 4.0.0:** `min` / `max` fail a value that is not a number (on 3.x `'abc'` passed both), and read text as typed: full-width digits (`123`), the column `locale`'s grouping and decimal separator, and a currency mark. Called without a `message`, the built-ins report the `validation.*` locale keys (4.0), so `localeText` translates them. In row edit mode `ctx.data` is the staged row (the record with the row's pending values applied) since 3.6, so a cross-field rule such as `ctx.data.end >= ctx.data.start` sees the row being saved. See `references/state-and-features.md` for the `Validator` / `ValidatorContext` shapes and the `cellValidationFailed` payload.
180
201
 
181
202
  **Numeric editing (3.4):** clearing a `number` or `currency` editor now commits `null`, not `NaN`. An empty or unparseable numeric input leaves the cell empty instead of writing `NaN` into the row. Editing also opens from the keyboard now (Enter or F2 on the active cell), not only by pointer; see SKILL.md §5 for the full keyboard model. Separately, a spreadsheet-style error value (an object carrying a `#...` code, such as an enterprise formula engine's `#VALUE!`) renders as its code in typed `number` / `currency` columns rather than coercing to `NaN` and showing an empty cell.
182
203
 
@@ -235,7 +256,7 @@ grid.updateColumns([
235
256
  { key: 'internalId', hidden: true }
236
257
  ```
237
258
 
238
- Column data is still accessible via `grid.dataView` and the cell template's `row` context, but no cell or header is rendered for it. Toggle at runtime via `updateColumns`.
259
+ Column data is still accessible via `grid.dataView` and the cell template's `data` context, but no cell or header is rendered for it. Toggle at runtime via `updateColumns`.
239
260
 
240
261
  ## Resizable columns
241
262
 
@@ -249,8 +270,9 @@ The grid renders a drag handle on the right edge of the column header. Minimum r
249
270
 
250
271
  | ❌ | ✅ |
251
272
  |---|---|
252
- | `{ key: 'fullName' }` when `fullName` isn't in `T` | Add to `T`, or compute via `cellTemplate: ({ row }) => html\`${row.data.firstName} ${row.data.lastName}\`` |
253
- | `cellTemplate: ({ row }) => row.name` | `row` is the `<apex-grid-row>` element: read the record as `row.data.name` |
273
+ | `{ key: 'fullName' }` when `fullName` isn't in `T` | Add to `T`, or compute via `cellTemplate: ({ data }) => html\`${data.firstName} ${data.lastName}\`` |
274
+ | `cellTemplate: ({ row }) => row.name` | `row` is the `<apex-grid-row>` element, not the record: read `data.name` (4.0) |
275
+ | Reading `ctx.row.data` after an `await`, or keeping state in a template's elements | With row recycling (4.0 default) the element can show another record by then: read `ctx.data`, use `keyed(ctx.data, ...)`, or set `grid.rowRecycling = false` |
254
276
  | `cellTemplate: ({ value }) => \`<b>\${value}</b>\`` (string) | `cellTemplate: ({ value }) => html\`<b>\${value}</b>\`` |
255
277
  | `{ key: 'age', sort: true }` (default `type: 'string'`) | Set `type: 'number'` |
256
278
  | `{ key: 'created', type: 'string', sort: { comparer } }` to fake dates | `{ key: 'created', type: 'date', format: 'medium' }` — a real type (accepts `Date`/ISO/timestamp) |
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## How the pipeline runs
4
4
 
5
- When `data`, `columns`, sort state, filter state, the quick-filter value, or the page changes, Apex Grid runs an async pipeline:
5
+ When `data`, `columns`, sort state, filter state, the quick-filter value, or (in remote pagination mode) the page changes, Apex Grid runs an async pipeline. Since 4.0 a local page change only re-slices the view and does not rerun it. The full pipeline:
6
6
 
7
7
  ```
8
8
  data ─► (quickFilter) ─► (filter) ─► (sort) ─► (pagination) ─► virtualizer ─► rendered rows
@@ -13,18 +13,19 @@ By default every stage runs in-memory. Override any stage with `dataPipelineConf
13
13
  ## `dataPipelineConfiguration`
14
14
 
15
15
  ```ts
16
- type DataPipelineParams<T> = { data: T[]; grid: ApexGrid<T>; type: 'sort' | 'filter' | 'quickFilter' | 'pagination' };
16
+ type DataPipelineParams<T> = { data: T[]; grid: ApexGrid<T>; type: 'sort' | 'filter' | 'quickFilter' };
17
17
  type DataPipelineHook<T> = (state: DataPipelineParams<T>) => T[] | Promise<T[]>;
18
18
 
19
19
  interface DataPipelineConfiguration<T extends object> {
20
20
  sort?: DataPipelineHook<T>;
21
21
  filter?: DataPipelineHook<T>; // column filters
22
22
  quickFilter?: DataPipelineHook<T>; // global search (runs before column filter)
23
- pagination?: DataPipelineHook<T>; // return the page slice; remote: set pagination.totalItems
24
23
  }
25
24
  ```
26
25
 
27
- The grid invokes the matching hook in place of its built-in pipeline stage. Whatever the hook returns becomes the input for the next stage (or the final view). All four hooks are optional — omit a stage to keep it in-memory.
26
+ The grid invokes the matching hook in place of its built-in pipeline stage. Whatever the hook returns becomes the input for the next stage (or the final view). All three hooks are optional: omit a stage to keep it in-memory.
27
+
28
+ > **Changed in 4.0.0 (types only):** `DataPipelineConfiguration.pagination` and the `'pagination'` value of `DataPipelineParams.type` are removed. The hook was documented but never called, so nothing changes at runtime, but TypeScript that sets it stops compiling. Page on the server with remote pagination (below).
28
29
 
29
30
  ```ts
30
31
  grid.dataPipelineConfiguration = {
@@ -62,7 +63,27 @@ grid.dataPipelineConfiguration = {
62
63
 
63
64
  ### Order of operations
64
65
 
65
- The grid runs the stages in a fixed order: **quick-filter → column filter → sort → pagination**. If your server endpoint does several of these together, do the work in the earliest relevant hook (returning the fully processed array) and omit the later hooks. For server-driven paging, return the current page from the `pagination` hook and set `grid.pagination = { ..., mode: 'remote', totalItems }` so the paginator can compute the page count.
66
+ The grid runs the stages in a fixed order: **quick-filter → column filter → sort → pagination**. If your server endpoint does several of these together, do the work in the earliest relevant hook (returning the fully processed array) and omit the later hooks.
67
+
68
+ ### Server-driven paging: `pagination.mode: 'remote'`
69
+
70
+ In remote mode the grid does not slice `data`: it renders whatever `data` holds as the current page. Fetch that page on `pageChanged`, assign it to `data`, and keep `pagination.totalItems` at the number of rows the server holds for the current filter, so the paginator can count the pages.
71
+
72
+ ```ts
73
+ grid.pagination = { enabled: true, mode: 'remote', pageSize: 25, totalItems: 0 };
74
+
75
+ async function load(page: number, pageSize: number) {
76
+ const res = await fetch(`/api/users?page=${page}&size=${pageSize}`);
77
+ const { rows, total } = await res.json();
78
+ grid.pagination = { ...grid.pagination!, page, pageSize, totalItems: total }; // a `page` in the config is applied on every assignment
79
+ grid.data = rows;
80
+ }
81
+
82
+ grid.addEventListener('pageChanged', (e) => load(e.detail.page, e.detail.pageSize));
83
+ load(0, 25);
84
+ ```
85
+
86
+ In remote mode a page or page size change reruns the pipeline (so your `sort` / `filter` hooks run again). **Changed in 4.0.0:** in local mode it no longer does, so custom `sort` and `filter` hooks are not called on a local page change, and with pagination on, pinned rows no longer count toward `totalItems`, `pageCount` or the summary (they show on every page).
66
87
 
67
88
  ### Hooks fire on every state change
68
89
 
@@ -100,18 +121,20 @@ grid.dataPipelineConfiguration = {
100
121
 
101
122
  ## Virtualization
102
123
 
103
- Apex Grid uses [`@lit-labs/virtualizer`](https://www.npmjs.com/package/@lit-labs/virtualizer) (`<apex-virtualizer>`) for row recycling. **Only currently visible rows + a small overscan are present in the DOM.**
124
+ Apex Grid uses [`@lit-labs/virtualizer`](https://www.npmjs.com/package/@lit-labs/virtualizer) (`<apex-virtualizer>`, pinned to exactly 2.1.1 in 4.0) to render rows. **Only currently visible rows + a small overscan are present in the DOM.**
104
125
 
105
126
  | API | What it returns |
106
127
  |---|---|
107
128
  | `grid.rows` | `ApexGridRow<T>[]` — only the rendered chunk |
108
129
  | `grid.dataView` | `readonly T[]` — full post-pipeline data (all rows) |
109
- | `grid.totalItems` | `number` — `dataView.length` |
130
+ | `grid.totalItems` | `number`: `dataView.length`; with pagination, without pinned rows (4.0); in remote mode, the `pagination.totalItems` you supply |
131
+
132
+ **Row recycling (Changed in 4.0.0).** `grid.rowRecycling` defaults to `true`: a scroll, sort or filter re-binds the rendered row elements to the records now in view (and only the rows that scroll in render). So a row element can show a different record after any of those, not only after a scroll. `grid.rowRecycling = false` (attribute `row-recycling="false"`) keeps record-keyed rows. Templates read the record from `ctx.data` (`references/columns-and-templates.md`).
110
133
 
111
134
  Implications:
112
135
 
113
136
  - **Don't size the grid based on `grid.rows.length`.** Use `grid.totalItems`.
114
- - **Don't hold references to `<apex-grid-row>` elements** across scrolls — they get recycled.
137
+ - **Don't hold references to `<apex-grid-row>` elements** across a scroll, sort or filter: read `row.data` each time, never keep the element as a handle to a record.
115
138
  - **DOM-driven row queries** (`document.querySelectorAll('apex-grid-row')`) only see visible rows.
116
139
 
117
140
  ## When the pipeline runs
@@ -152,7 +175,8 @@ Assigning `grid.data = null` or `undefined` (a framework prop still loading) sho
152
175
  | ❌ | ✅ |
153
176
  |---|---|
154
177
  | Reading `grid.rows.length` for total count | `grid.totalItems` (or `grid.dataView.length`) |
155
- | Caching `<apex-grid-row>` references across scrolls | Look them up fresh from `grid.rows` (or operate on `grid.dataView` data) |
178
+ | Caching `<apex-grid-row>` references across scrolls, sorts or filters | Look them up fresh from `grid.rows` (or operate on `grid.dataView` data) |
179
+ | `dataPipelineConfiguration.pagination` to page on the server | Removed in 4.0 (it was never called): `pagination.mode: 'remote'` + `pageChanged` + `pagination.totalItems` |
156
180
  | Re-fetching from the server only on UI events | Hooks fire for both UI and programmatic — debounce / abort inside the hook |
157
181
  | Sorting in the `filter` hook expecting it to override the built-in sort | The grid still runs the built-in sort after your filter hook unless you also supply a `sort` hook |
158
182
  | Returning a Promise that rejects | Errors propagate; the grid keeps the previous `dataView`. Catch + log inside the hook to surface failures |
@@ -239,7 +239,10 @@ export class UsersGridComponent implements OnInit {
239
239
  | ❌ | ✅ |
240
240
  |---|---|
241
241
  | Forgetting to register the element | Top-level `import 'apex-grid/define'` once in the app entry |
242
- | Importing an Ignite UI theme + `configureTheme(name)` | Obsolete — the grid self-styles; retheme via `--ag-*` CSS variables. No import does anything for appearance |
242
+ | Importing an Ignite UI theme + `configureTheme(name)` | Obsolete: the grid self-styles; retheme via `--ag-*` CSS variables. To follow an Ignite UI theme on the page, `import 'apex-grid/ignite-ui'` once in the app entry (apex-grid 4.0; free). Without it the 4.0 grid ignores `--ig-*` |
243
+ | A stateful widget (a framework component mounted into a cell, a third-party picker) inside `cellTemplate` showing the wrong row's state after a scroll or sort | Body rows are recycled by default in 4.0: read the record from `ctx.data`, keep the state in your data, wrap the template in `keyed(ctx.data, ...)`, or set `grid.rowRecycling = false` (`row-recycling="false"`) |
244
+ | `show-header={false}` / `:show-header="false"` expecting a boolean | Boolean attributes such as `show-header` and `row-recycling` turn off only on the string `"false"`; bind the property instead (`.showHeader=${false}`, `:showHeader.prop="false"`, `[showHeader]="false"`, or `el.showHeader = false`) |
245
+ | Pinning `theme="light"` on a page with a dark Ignite UI theme | Leave the `theme` attribute off so the grid follows `--ig-theme-variant`; `design-system="off"` / `tokens="off"` opt one grid out |
243
246
  | Forgetting `setup()` / no host-height rule for `apex-grid` | `setup()` registers + sizes the host. Or `import 'apex-grid/define'` + `apex-grid { height: ... }` (never set `display` — it overrides the component's `:host { display: grid }`) |
244
247
  | Vue: `:data="data"` (attribute, becomes `"[object Object]"`) | `:data.prop="data"`, or set via `ref` in `onMounted` |
245
248
  | React: `<apex-grid columns={[...]}>` before React 19 | Use `ref` + `useEffect` to set as property |
@@ -109,6 +109,14 @@ Unary operands (no `searchTerm` needed): `empty`, `notEmpty`, `all`, `true`, `fa
109
109
 
110
110
  Since 3.6 the numeric presentation types `currency`, `rating` and `progress` take the number operands (on 3.5 they took the text ones). A condition name that does not belong to the column's set, such as `contains` on a `currency` column, is dropped with a console warning, from `filter()` and from a saved state alike. Text conditions read `null` / `undefined` as empty text and match other values (numbers, dates) on their text instead of throwing, and the range conditions (`greaterThan`, `lessThan`, ...) no longer match an empty cell as if it were 0.
111
111
 
112
+ **Typed numbers in the filter panel (Changed in 4.0.0).** On a numeric column (`number`, `currency`, `rating`, `progress`) the filter panel reads a number as a person writes it: full-width digits and signs (`123`, `-5`), the grouping and decimal separator of the column's `locale` (or the runtime locale), and a leading or trailing `¥`, `$`, `€`, `円` or the column currency's symbol. On 3.6 `123` did not filter and `1,000` filtered by 1. A plain ASCII decimal reads as before in every locale (`1.000` filters as 1 even under `'de-DE'`), the input keeps the text as typed, and text the parser does not read falls back to its leading number as in 3.x (`12.5%` filters by 12.5, `10²` by 10). The `min()` / `max()` validators have no such fallback: `12.5%` or `10²` there is not a number, so the value fails. Programmatic `filter()` / `filterExpressions` terms are not parsed: pass a number as `searchTerm`.
113
+
114
+ ```ts
115
+ { key: 'price', type: 'currency', currency: 'EUR', locale: 'de-DE', filter: true } // typing 1.234,5 filters by 1234.5
116
+ ```
117
+
118
+ **IME input (4.0).** Japanese, Chinese or Korean typed through an input method works in the filter panel and the quick filter: the Enter, Escape and Tab that confirm or cancel a conversion stay with the IME (they do not close the panel or clear the quick filter), the text applies once the conversion ends rather than on every unconverted keystroke, and a quick filter search still waiting out its debounce is dropped when a conversion starts. Typing without an IME filters as before.
119
+
112
120
  ### Programmatic filter
113
121
 
114
122
  ```ts
@@ -1,6 +1,6 @@
1
1
  # State, Localization & Interaction Features (3.1+)
2
2
 
3
- > Everything here landed in **apex-grid 3.1 and later** on top of the 3.0 API; version tags mark what is newer. The setup rules from `SKILL.md` (register + bounded host height, no theme import) still apply. Row pinning / reordering, undo-redo, and validators are all **opt-in** via a config object, off by default.
3
+ > Everything here landed in **apex-grid 3.1 and later** on top of the 3.0 API; version tags mark what is newer (up to 4.0). The setup rules from `SKILL.md` (register + bounded host height, no theme import) still apply. Row pinning / reordering, undo-redo, and validators are all **opt-in** via a config object, off by default.
4
4
 
5
5
  ## Row pinning — sticky top / bottom bands
6
6
 
@@ -23,6 +23,19 @@ grid.pinnedRows; // getter → { top: T[]; bottom
23
23
 
24
24
  Events: `rowPinning` (cancellable) → `rowPinned`, both `CustomEvent<{ row: T; position: 'top' | 'bottom' | null }>` (`null` when unpinning). Pins survive sort / filter / pagination as long as row identity is preserved. Since 3.6, replacing `grid.data` carries the pins onto the new rows: matched by `rowId` when it is set, otherwise a pin whose object left the data is dropped (re-pin those). While `rowPinning.enabled` is `false`, `pinnedRows` reports none and the rows return to the body, but the pins are kept (`getState` still saves them) and come back when pinning is re-enabled.
25
25
 
26
+ **With pagination (Changed in 4.0.0):** pinned rows show on every page and no longer count toward the pages: `totalItems`, `pageCount`, the paginator summary and the `pageChanged` detail count only the rows the pager pages through, and pinning a row on the last page can move the grid to the new last page. Without pagination `totalItems` still counts every row.
27
+
28
+ **A pinned summary row that cannot be selected (4.0):** pinned rows are selectable like any other unless `selection.isRowSelectable` says otherwise.
29
+
30
+ ```ts
31
+ const totalRow = { id: -1, name: 'Total', amount: sum };
32
+ grid.data = [...rows, totalRow];
33
+ grid.rowPinning = { enabled: true };
34
+ grid.pinRow(totalRow, 'bottom');
35
+ grid.selection = { enabled: true, mode: 'multiple', showCheckboxColumn: true,
36
+ isRowSelectable: (row) => row !== totalRow }; // no checkbox, skipped by Select all
37
+ ```
38
+
26
39
  ## Row reordering — drag / keyboard / API
27
40
 
28
41
  ```ts
@@ -128,17 +141,32 @@ const columns: ColumnConfiguration<User>[] = [
128
141
  | Factory | Signature | Fails when |
129
142
  |---|---|---|
130
143
  | `required(message?)` | `(msg?) => Validator` | value is `null` / `undefined` / empty-or-whitespace string |
131
- | `min(limit, message?)` | `(number, msg?) => Validator` | numeric value `< limit` (non-numeric / empty pass — compose with `required`) |
132
- | `max(limit, message?)` | `(number, msg?) => Validator` | numeric value `> limit` (non-numeric / empty pass) |
144
+ | `min(limit, message?)` | `(number, msg?) => Validator` | numeric value `< limit`, or (4.0) a value that is not a number (empty passes; compose with `required`) |
145
+ | `max(limit, message?)` | `(number, msg?) => Validator` | numeric value `> limit`, or (4.0) a value that is not a number (empty passes) |
133
146
  | `pattern(regex, message?)` | `(RegExp, msg?) => Validator` | `String(value)` doesn't match `regex` (empty passes) |
134
147
  | `custom(fn)` | `(Validator) => Validator` | wraps a predicate; an inline arrow works identically |
135
148
 
149
+ > **Changed in 4.0.0:** `min()` and `max()` fail a value that is not a number; on 3.x `'abc'` (any text `Number()` could not read) passed both. Text is read as a person types it: full-width digits and signs (`123`, `-5`), the grouping and decimal separator of the column's `locale` (or the runtime locale), and a leading or trailing `¥`, `$`, `€`, `円` or the column currency's symbol.
150
+
136
151
  ```ts
137
152
  // Validator signature
138
153
  type Validator<T, K> = (value: unknown, context: ValidatorContext<T, K>) => string | null;
139
- interface ValidatorContext<T, K> { column: ColumnConfiguration<T, K>; data: T; rowIndex: number; }
154
+ interface ValidatorContext<T, K> {
155
+ column: ColumnConfiguration<T, K>;
156
+ data: T;
157
+ rowIndex: number;
158
+ localize?: (key: GridLocaleKey, params?: LocaleParams) => string; // 4.0; absent outside a grid
159
+ }
160
+
161
+ // 4.0: a custom validator can reuse a translated message through the grid's localeText
162
+ const endRequiredWithStart = custom<Booking>((value, ctx) =>
163
+ ctx.data.start && value == null
164
+ ? (ctx.localize?.('validation.required') ?? 'This field is required')
165
+ : null);
140
166
  ```
141
167
 
168
+ Called without a `message`, the built-ins report the `validation.required`, `validation.min`, `validation.max` and `validation.pattern` locale keys (4.0) through `ctx.localize`, so `localeText` (or `esLocale`) translates them. On 3.x those messages were English whatever `localeText` said.
169
+
142
170
  `rowIndex` is view-relative, or -1 for a pinned row or a record on another page (3.6). In row edit mode `ctx.data` is the **staged row** (3.6): a copy of the record (same prototype) with the row's staged values applied, so a cross-field rule judges the row being saved rather than the stored record. A row commit also re-validates the row's other editable cells against it.
143
171
 
144
172
  Event: `cellValidationFailed` — `CustomEvent<{ key; rowIndex; data: T; value: unknown; errors: readonly string[] }>`.
@@ -219,6 +247,19 @@ await grid.unpinColumn('actions'); // = pinColumn(key, null)
219
247
 
220
248
  `gotoPage` / `setPageSize` / `nextPage` / `previousPage` / `firstPage` / `lastPage` emit `pageChanging` → `pageChanged`. `setQuickFilter` emits `quickFilterChanging` → `quickFilterChanged`, plus (3.6) `pageChanged` when it moves the grid back to the first page. Since 3.6 the `*-ed` events fire after the pipeline run they caused, so awaiting the returned promise, or reading `grid.pageItems` in the listener, sees the new rows even with an async pipeline hook. `pinColumn` / `unpinColumn` emit `columnPinning` → `columnPinned` and only change the visual render order (the `columns` array isn't reordered).
221
249
 
250
+ Since 4.0 `pageChanged` reports the change that was applied, so two quick `nextPage()` calls report page 2 and then page 3, and a local page change no longer reruns sort / filter (custom hooks are not called; remote mode still reruns). A page size or quick filter cancelled through `pageChanging` / `quickFilterChanging` no longer leaves the selector or search box showing the refused value.
251
+
252
+ **Paginator layout (4.0).** `pagination.paginator: 'full'` (default) shows the page size select, the "1-25 of 100" summary, first, previous, "page / count", next and last, and drops the select, then first / last, when the grid is too narrow, so previous and next stay reachable. `'compact'` always uses that narrow layout. Every paginator and toolbar part is styleable from outside (`paginator-button`, `paginator-prev`, `search-input`, ...; full list in SKILL.md §1.3). A `pageSize` missing from `pageSizeOptions` now appears in the selector.
253
+
254
+ ```ts
255
+ grid.pagination = { enabled: true, pageSize: 10, paginator: 'compact' };
256
+ ```
257
+
258
+ ```css
259
+ apex-grid::part(paginator-info) { font-variant-numeric: tabular-nums; }
260
+ apex-grid::part(paginator-button) { border-radius: 999px; }
261
+ ```
262
+
222
263
  ## Localization — `localeText`
223
264
 
224
265
  Every built-in UI string (pagination, filter operators, toolbar, row/selection labels) is localizable through a `GridLocaleText` map keyed by dot-namespaced `GridLocaleKey`s. Partial maps are valid — any omitted key falls back to the English default.
@@ -234,10 +275,20 @@ grid.localize('pagination.summary', { start: 1, end: 25, total: 100 }); // res
234
275
 
235
276
  Exports: `EN_LOCALE` (the default dictionary / source of truth for keys), `esLocale` (ready-made Spanish), `interpolate` (`{token}` substitution), `localize`, and the types `GridLocaleKey` / `GridLocaleText` / `LocaleParams`. `grid.localize(key, params?, fallback?)` applies any `localeText` override over the English default and interpolates `{placeholder}` tokens; `fallback` covers keys outside the built-in set (e.g. a custom operand label).
236
277
 
237
- **3.4 key changes** (both English and Spanish): the accessibility work added live-region announcement keys (`announce.*`, e.g. `announce.sortedAscending`, `announce.page`, `announce.rowSelected`, `announce.undoOne`), header control / sort-state labels (`header.*`, e.g. `header.sortedAscending`, `header.filterColumn`, `header.columnMenu`), the host label `grid.label` ("Data grid"), and `editor.rating`. The `ai.noAdapter` key was **removed** (drop it from any `localeText` override you kept). `EN_LOCALE` remains the source of truth for the full, current key list.
278
+ **3.4 key changes** (both English and Spanish): the accessibility work added live-region announcement keys (`announce.*`, e.g. `announce.sortedAscending`, `announce.page`, `announce.rowSelected`, `announce.undoOne`), header control / sort-state labels (`header.*`, e.g. `header.sortedAscending`, `header.filterColumn`, `header.columnMenu`), the grid label `grid.label` ("Data grid"; since 4.0 on the inner `[part~="table"]` element, not the host), and `editor.rating`. The `ai.noAdapter` key was **removed** (drop it from any `localeText` override you kept). `EN_LOCALE` remains the source of truth for the full, current key list.
238
279
 
239
280
  **3.6 key additions** (English and Spanish) are used only by enterprise panels: `toolPanel.valueColumn`, `toolPanel.columnLabelColumn`, `chart.mapCategoryNone`, `setFilter.more`, `pivot.columnsTruncated`. A partial `localeText` map needs none of them; a full translation typed as `Record<GridLocaleKey, string>` must add them to compile.
240
281
 
282
+ **4.0 key additions** (English and Spanish; each English default is the text shown before, and `esLocale` translates them all). Text that stayed English whatever `localeText` said now has keys: `toolbar.exportCsv` (the CSV export menu item), `filter.and` / `filter.or` (the filter panel's join toggles), `cell.rating` (a rating's accessible name, `{value} of {max}`), `cell.true` / `cell.false` (a boolean cell's accessible name), and `validation.required`, `validation.min`, `validation.max` (`{limit}`), `validation.pattern` (the built-in validators' default messages). Keys read only by `apex-grid-enterprise`: `filterBuilder.true` / `filterBuilder.false`, `contextMenu.groupBy`, `contextMenu.ungroupAll`, `contextMenu.expandGroups`, `contextMenu.collapseGroups`, `ai.cancelled`, `chart.rangeResized`. As with 3.6, a full `Record<GridLocaleKey, string>` translation must add them to compile. A filter chip for a condition with no value now shows its `filter.operator.*` label ("Not empty") instead of the operator id.
283
+
284
+ ```ts
285
+ grid.localeText = {
286
+ 'toolbar.exportCsv': 'CSV herunterladen',
287
+ 'validation.min': 'Mindestens {limit}',
288
+ 'cell.true': 'ja', 'cell.false': 'nein',
289
+ };
290
+ ```
291
+
241
292
  ## Column menu & column separator
242
293
 
243
294
  | Property | Type | Default | Notes |