@svgrid/mcp 3.0.18 → 3.0.19

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/dist/data.js CHANGED
@@ -5630,7 +5630,7 @@ export const docs = [
5630
5630
  "path": "docs/help/icons.md",
5631
5631
  "title": "Icons",
5632
5632
  "section": "Help",
5633
- "markdown": "\n# Icons\n\nThe grid draws about fifty small marks for its own chrome - sort\narrows, the filter funnel, the column menu, expanders, pager arrows,\nthe tool panel's buttons. Every one of them has a name, and the\n`icons` prop replaces any of them with a snippet of your markup.\n\n<div data-docs-demo=\"431-custom-icons\" data-height=\"560\"></div>\n\nColours and sizes are [design tokens](./tokens.md); the glyphs\nthemselves are this prop. That split is deliberate - a token cannot\nchange the shape of a mark, and an icon set should not have to know\nyour palette.\n\n## The smallest version\n\nWrite a snippet, name it after the icon you want to replace:\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid } from '@svgrid/grid'\n</script>\n\n{#snippet funnel()}\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M4 4h16l-6.5 8v7l-3-1.5V12z\" />\n </svg>\n{/snippet}\n\n<SvGrid {data} {columns} icons={{ filter: funnel }} />\n```\n\nThat is the whole API. Everything else on this page is the catalogue\nand the two or three things worth knowing about how the markup is\ntreated.\n\n## A partial map is a complete answer\n\n`icons` is a **map**, not a single snippet that receives a name. Names\nyou leave out keep their built-in glyph, so you never have to be\nexhaustive and you can never blank an icon by forgetting it exists:\n\n```svelte\n<!-- Sort arrows are yours. The other ~50 marks are still ours. -->\n<SvGrid {data} {columns} icons={{ 'sort-asc': up, 'sort-desc': down }} />\n```\n\nThis matters more than it looks. The grid asks for icons you may not\nhave thought about - `op-regex` only appears once someone picks the\nregex operator, `page-last` only when pagination is on. A catch-all\nsnippet makes you responsible for all of them forever; a map does not.\n\nIt is the same shape as\n[`localization.text`](./i18n-rtl.md): a flat map of overrides with\nbuilt-in defaults. Strings there, glyphs here.\n\n## What your markup gets\n\nThe grid wraps your snippet in its own icon box, which means:\n\n- **Size comes from the call site.** The same `chevron-right` is 12px\n in a tree expander and 1em in a menu row. Your override inherits\n whichever box it lands in, so one snippet works everywhere the name\n is used - don't set a width.\n- **Colour follows `currentColor`.** Use it (`fill=\"currentColor\"` or\n `stroke=\"currentColor\"`) and your icon picks up hover, disabled,\n muted and accent states from the chrome around it for free. Hard-code\n a colour and it will look wrong in half of them.\n- **Rotation still applies.** Expanders draw one chevron and rotate it\n 90 degrees when open rather than swapping glyphs. Your override\n rotates with them, so supply the *closed* (pointing right) form.\n\nA snippet is a snippet, so an `<Icon>` component from your design\nsystem, an `<img>`, or a plain character all work:\n\n```svelte\n{#snippet fromDesignSystem()}<Icon name=\"table/sort-ascending\" />{/snippet}\n{#snippet justText()}<span aria-hidden=\"true\">+</span>{/snippet}\n```\n\n## Setting icons at runtime\n\n`icons` is an ordinary prop, so it is reactive, and\n[`api.setOption`](../reference/SvGridApi.md) works on it too - useful\nwhen the icon set follows a theme the user picks:\n\n```svelte\n<SvGrid {data} {columns} icons={dense ? compactIcons : {}} />\n```\n\n## The catalogue\n\nImport `GridIconName` for autocomplete, or `GRID_ICON_NAMES` for the\nlist at runtime.\n\n### Header and column menu\n\n| Name | Where it appears |\n| --- | --- |\n| `sort` | Hover hint on a sortable header |\n| `sort-asc`, `sort-desc` | Sort indicator, and the menu's sort rows |\n| `filter` | Header funnel button, and the menu's filter heading |\n| `menu` | The header's column-menu button |\n| `group` | \"Grouped\" header badge, and Group by in the menu |\n| `columns` | Choose columns |\n| `autosize` | Autosize this / all columns |\n| `reset` | Reset columns |\n| `x` | Remove sort, Remove grouping |\n| `chevron-down` | Filter-row operator caret, submenu arrow |\n| `chevron-right` | Tree, group and auto-group expanders |\n\n### Filter operators\n\n`op-contains`, `op-notContains`, `op-equals`, `op-notEquals`,\n`op-startsWith`, `op-endsWith`, `op-regex`, `op-in`, `op-notIn`,\n`op-greaterThan`, `op-lessThan`, `op-between`, `op-isBlank`,\n`op-isNotBlank`.\n\nThese appear in the operator menu and on the filter row's operator\nbutton.\n\n### Pinning\n\n`pin-left`, `pin-right`, `unpin`.\n\nTheir built-in glyphs are borrowed from `op-startsWith`,\n`op-greaterThan` and `x`, but they are separate names on purpose:\nrestyling a filter operator should not silently repaint the pin menu.\n\n### Toolbar and overlays\n\n| Name | Where it appears |\n| --- | --- |\n| `search` | Find-in-grid, the filter menu's value search, and the board / scheduler / chart search boxes |\n| `tool-panel` | The \"Columns & Filters\" button |\n| `chart` | The chart panel toggle |\n| `advanced-filter` | The \"advanced filter active\" chip |\n| `column-group-caret` | Collapse caret on a column-group header |\n\n### Glyph icons\n\nThese default to a character rather than a path. `GRID_ICON_GLYPHS`\nholds the defaults.\n\n| Name | Default | Where it appears |\n| --- | --- | --- |\n| `close` | `✕` | Find bar, tool panel |\n| `clear` | `✕` | Clear a column filter, clear the advanced filter |\n| `remove` | `×` | Remove a filter chip or a second condition |\n| `row-number` | `#` | Row-number column header |\n| `pinned-row-top`, `pinned-row-bottom` | `↑` `↓` | Pinned-row marker in the gutter |\n| `find-prev`, `find-next` | `↑` `↓` | Find-in-grid navigation |\n| `move-up`, `move-down` | `↑` `↓` | Reorder a column in the tool panel |\n| `group-add` | `⊞` | Group by a column from the tool panel |\n| `page-first`, `page-prev`, `page-next`, `page-last` | `⇤` `‹` `›` `⇥` | Pager |\n| `page-size-caret` | `▾` | Page-size selector |\n| `drag-handle` | `⠿` | Row-group panel chip grip |\n| `breadcrumb-separator` | `›` | Row-group panel |\n\n## Components you mount yourself\n\n`SvGroupCell` and `SvRowGroupPanel` (from `@svgrid/enterprise`) are exported\nso you can place them in your own markup, which means they never see the\ngrid's `icons`. Pass the same object to them:\n\n```svelte\n<SvRowGroupPanel {columns} {groupBy} {onChange} {icons} />\n\n<!-- and in a column definition -->\ncell: (ctx) => renderComponent(SvGroupCell, { row: ctx.row.original, onToggle, icons })\n```\n\n## What this does not cover\n\nTwo marks are deliberately outside the prop.\n\n**The scrollbar's stepper arrows.** `<sv-grid-scrollbar>` is a custom\nelement that builds its arrow with `createElementNS` inside a shadow\nroot, specifically so it stays usable under a strict Content Security\nPolicy and Trusted Types. A Svelte snippet cannot render into another\nelement's shadow root, and injecting arbitrary markup there would give\nup the CSP guarantee. Colour it with the `--sg-scrollbar-arrow*`\n[tokens](./tokens.md).\n\n**The checkbox tick and its indeterminate dash.** Both are `::after`\npseudo-elements drawn from borders rather than glyphs, so there is\nnothing to swap. They follow `--sg-accent` and `--sg-on-accent`.\n\nConditional formatting's `iconSet` is a different feature that happens\nto share the word - those are value-driven data marks you already\nchoose per rule. See\n[Conditional formatting](./cells/conditional-formatting.md).\n\n## Web components\n\n`<sv-grid>` does not expose `icons`. Snippets are a compile-time\nSvelte construct, and a host page has no way to author one, so there\nis nothing an attribute could carry. Restyle the element's icons with\nCSS and the `--sg-*` tokens instead. See\n[Web components](./web-components/sv-grid.md).\n\n## See also\n\n- [Design tokens](./tokens.md) - the colours and sizes the icons inherit\n- [Internationalization](./i18n-rtl.md) - `localization.text`, the same\n idea for the grid's strings\n- [Custom icon set](https://svgrid.com/demos/431-custom-icons/) demo -\n a partial override and a full one, side by side\n"
5633
+ "markdown": "\n# Icons\n\nThe grid draws about fifty small marks for its own chrome - sort\narrows, the filter funnel, the column menu, expanders, pager arrows,\nthe tool panel's buttons. Every one of them has a name, and the\n`icons` prop replaces any of them with a snippet of your markup.\n\n<div data-docs-demo=\"431-custom-icons\" data-height=\"560\"></div>\n\nColours and sizes are [design tokens](./tokens.md); the glyphs\nthemselves are this prop. That split is deliberate - a token cannot\nchange the shape of a mark, and an icon set should not have to know\nyour palette.\n\n## The smallest version\n\nWrite a snippet, name it after the icon you want to replace:\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid } from '@svgrid/grid'\n</script>\n\n{#snippet funnel()}\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M4 4h16l-6.5 8v7l-3-1.5V12z\" />\n </svg>\n{/snippet}\n\n<SvGrid {data} {columns} icons={{ filter: funnel }} />\n```\n\nThat is the whole API. Everything else on this page is the catalogue\nand the two or three things worth knowing about how the markup is\ntreated.\n\n## A partial map is a complete answer\n\n`icons` is a **map**, not a single snippet that receives a name. Names\nyou leave out keep their built-in glyph, so you never have to be\nexhaustive and you can never blank an icon by forgetting it exists:\n\n```svelte\n<!-- Sort arrows are yours. The other ~50 marks are still ours. -->\n<SvGrid {data} {columns} icons={{ 'sort-asc': up, 'sort-desc': down }} />\n```\n\nThis matters more than it looks. The grid asks for icons you may not\nhave thought about - `op-regex` only appears once someone picks the\nregex operator, `page-last` only when pagination is on. A catch-all\nsnippet makes you responsible for all of them forever; a map does not.\n\nIt is the same shape as\n[`localization.text`](./i18n-rtl.md): a flat map of overrides with\nbuilt-in defaults. Strings there, glyphs here.\n\n## What your markup gets\n\nThe grid wraps your snippet in its own icon box, which means:\n\n- **Size comes from the call site.** The same `chevron-right` is 12px\n in a tree expander and 1em in a menu row. Your override inherits\n whichever box it lands in, so one snippet works everywhere the name\n is used - don't set a width.\n- **Colour follows `currentColor`.** Use it (`fill=\"currentColor\"` or\n `stroke=\"currentColor\"`) and your icon picks up hover, disabled,\n muted and accent states from the chrome around it for free. Hard-code\n a colour and it will look wrong in half of them.\n- **Rotation still applies.** Expanders draw one chevron and rotate it\n 90 degrees when open rather than swapping glyphs. Your override\n rotates with them, so supply the *closed* (pointing right) form.\n\nA snippet is a snippet, so an `<Icon>` component from your design\nsystem, an `<img>`, or a plain character all work:\n\n```svelte\n{#snippet fromDesignSystem()}<Icon name=\"table/sort-ascending\" />{/snippet}\n{#snippet justText()}<span aria-hidden=\"true\">+</span>{/snippet}\n```\n\n## Setting icons at runtime\n\n`icons` is an ordinary prop, so it is reactive, and\n[`api.setOption`](../reference/SvGridApi.md) works on it too - useful\nwhen the icon set follows a theme the user picks:\n\n```svelte\n<SvGrid {data} {columns} icons={dense ? compactIcons : {}} />\n```\n\n## The catalogue\n\nImport `GridIconName` for autocomplete, or `GRID_ICON_NAMES` for the\nlist at runtime.\n\n### Header and column menu\n\n| Name | Where it appears |\n| --- | --- |\n| `sort` | Hover hint on a sortable header |\n| `sort-asc`, `sort-desc` | Sort indicator, and the menu's sort rows |\n| `filter` | Header funnel button, and the menu's filter heading |\n| `menu` | The header's column-menu button |\n| `group` | \"Grouped\" header badge, and Group by in the menu |\n| `columns` | Choose columns |\n| `autosize` | Autosize this / all columns |\n| `reset` | Reset columns |\n| `x` | Remove sort, Remove grouping |\n| `chevron-down` | Filter-row operator caret, submenu arrow |\n| `chevron-right` | Tree, group and auto-group expanders |\n\n### Filter operators\n\n`op-contains`, `op-notContains`, `op-equals`, `op-notEquals`,\n`op-startsWith`, `op-endsWith`, `op-regex`, `op-in`, `op-notIn`,\n`op-greaterThan`, `op-lessThan`, `op-between`, `op-isBlank`,\n`op-isNotBlank`.\n\nThese appear in the operator menu and on the filter row's operator\nbutton.\n\n### Pinning\n\n`pin-left`, `pin-right`, `unpin`.\n\nTheir built-in glyphs are borrowed from `op-startsWith`,\n`op-greaterThan` and `x`, but they are separate names on purpose:\nrestyling a filter operator should not silently repaint the pin menu.\n\n### Toolbar and overlays\n\n| Name | Where it appears |\n| --- | --- |\n| `search` | Find-in-grid, the filter menu's value search, and the board / scheduler / chart search boxes |\n| `tool-panel` | The \"Columns & Filters\" button |\n| `chart` | The chart panel toggle |\n| `advanced-filter` | The \"advanced filter active\" chip |\n| `column-group-caret` | Collapse caret on a column-group header |\n| `no-rows` | The empty-grid message when there is no data |\n| `no-rows-filtered` | The empty-grid message when the filters match no rows |\n\n### Glyph icons\n\nThese default to a character rather than a path. `GRID_ICON_GLYPHS`\nholds the defaults.\n\n| Name | Default | Where it appears |\n| --- | --- | --- |\n| `close` | `✕` | Find bar, tool panel |\n| `clear` | `✕` | Clear a column filter, clear the advanced filter |\n| `remove` | `×` | Remove a filter chip or a second condition |\n| `row-number` | `#` | Row-number column header |\n| `pinned-row-top`, `pinned-row-bottom` | `↑` `↓` | Pinned-row marker in the gutter |\n| `find-prev`, `find-next` | `↑` `↓` | Find-in-grid navigation |\n| `move-up`, `move-down` | `↑` `↓` | Reorder a column in the tool panel |\n| `group-add` | `⊞` | Group by a column from the tool panel |\n| `page-first`, `page-prev`, `page-next`, `page-last` | `⇤` `‹` `›` `⇥` | Pager |\n| `page-size-caret` | `▾` | Page-size selector |\n| `drag-handle` | `⠿` | Row-group panel chip grip |\n| `breadcrumb-separator` | `›` | Row-group panel |\n\n## Components you mount yourself\n\n`SvGroupCell` and `SvRowGroupPanel` (from `@svgrid/enterprise`) are exported\nso you can place them in your own markup, which means they never see the\ngrid's `icons`. Pass the same object to them:\n\n```svelte\n<SvRowGroupPanel {columns} {groupBy} {onChange} {icons} />\n\n<!-- and in a column definition -->\ncell: (ctx) => renderComponent(SvGroupCell, { row: ctx.row.original, onToggle, icons })\n```\n\n## What this does not cover\n\nTwo marks are deliberately outside the prop.\n\n**The scrollbar's stepper arrows.** `<sv-grid-scrollbar>` is a custom\nelement that builds its arrow with `createElementNS` inside a shadow\nroot, specifically so it stays usable under a strict Content Security\nPolicy and Trusted Types. A Svelte snippet cannot render into another\nelement's shadow root, and injecting arbitrary markup there would give\nup the CSP guarantee. Colour it with the `--sg-scrollbar-arrow*`\n[tokens](./tokens.md).\n\n**The checkbox tick and its indeterminate dash.** Both are `::after`\npseudo-elements drawn from borders rather than glyphs, so there is\nnothing to swap. They follow `--sg-accent` and `--sg-on-accent`.\n\nConditional formatting's `iconSet` is a different feature that happens\nto share the word - those are value-driven data marks you already\nchoose per rule. See\n[Conditional formatting](./cells/conditional-formatting.md).\n\n## Web components\n\n`<sv-grid>` does not expose `icons`. Snippets are a compile-time\nSvelte construct, and a host page has no way to author one, so there\nis nothing an attribute could carry. Restyle the element's icons with\nCSS and the `--sg-*` tokens instead. See\n[Web components](./web-components/sv-grid.md).\n\n## See also\n\n- [Design tokens](./tokens.md) - the colours and sizes the icons inherit\n- [Internationalization](./i18n-rtl.md) - `localization.text`, the same\n idea for the grid's strings\n- [Custom icon set](https://svgrid.com/demos/431-custom-icons/) demo -\n a partial override and a full one, side by side\n"
5634
5634
  },
5635
5635
  {
5636
5636
  "slug": "help/import",
@@ -7625,7 +7625,7 @@ export const docs = [
7625
7625
  "path": "docs/reference/auto/index.md",
7626
7626
  "title": "Auto-generated reference",
7627
7627
  "section": "API reference",
7628
- "markdown": "# Auto-generated reference\n\n_Generated from the published TypeScript sources. Do not edit by hand - rerun `node tools/build-reference.mjs`._\n\nFor the hand-curated reference (with defaults + behaviour tables),\nsee [the main reference index](../index.md).\n\n## @svgrid/grid\n\n- [`@svgrid/grid/core.ts`](./svgrid-grid-core.md) - 68 exports\n- [`@svgrid/grid/svgrid-wrapper.types.ts`](./svgrid-grid-svgrid-wrapper.types.md) - 10 exports\n- [`@svgrid/grid/ai.ts`](./svgrid-grid-ai.md) - 33 exports\n- [`@svgrid/grid/scheduler-model.ts`](./svgrid-grid-scheduler-model.md) - 30 exports\n- [`@svgrid/grid/dock-model.ts`](./svgrid-grid-dock-model.md) - 25 exports\n- [`@svgrid/grid/dock-manager-model.ts`](./svgrid-grid-dock-manager-model.md) - 32 exports\n- [`@svgrid/grid/chart.ts`](./svgrid-grid-chart.md) - 19 exports\n- [`@svgrid/grid/chart-types.ts`](./svgrid-grid-chart-types.md) - 61 exports\n- [`@svgrid/grid/chart-scale.ts`](./svgrid-grid-chart-scale.md) - 7 exports\n- [`@svgrid/grid/chart-format.ts`](./svgrid-grid-chart-format.md) - 1 exports\n- [`@svgrid/grid/chart-stats.ts`](./svgrid-grid-chart-stats.md) - 17 exports\n- [`@svgrid/grid/chart-trend.ts`](./svgrid-grid-chart-trend.md) - 2 exports\n- [`@svgrid/grid/chart-axes.ts`](./svgrid-grid-chart-axes.md) - 1 exports\n- [`@svgrid/grid/chart-cartesian.ts`](./svgrid-grid-chart-cartesian.md) - 5 exports\n- [`@svgrid/grid/chart-polar.ts`](./svgrid-grid-chart-polar.md) - 1 exports\n- [`@svgrid/grid/chart-hierarchy.ts`](./svgrid-grid-chart-hierarchy.md) - 2 exports\n- [`@svgrid/grid/chart-decimate.ts`](./svgrid-grid-chart-decimate.md) - 6 exports\n- [`@svgrid/grid/chart-financial.ts`](./svgrid-grid-chart-financial.md) - 8 exports\n- [`@svgrid/grid/chart-zoom.ts`](./svgrid-grid-chart-zoom.md) - 6 exports\n- [`@svgrid/grid/chart-motion.ts`](./svgrid-grid-chart-motion.md) - 1 exports\n- [`@svgrid/grid/chart-sync.svelte.ts`](./svgrid-grid-chart-sync.svelte.md) - 4 exports\n- [`@svgrid/grid/chart-indicators.ts`](./svgrid-grid-chart-indicators.md) - 14 exports\n- [`@svgrid/grid/chart-export-pdf.ts`](./svgrid-grid-chart-export-pdf.md) - 3 exports\n- [`@svgrid/grid/chart-samples.ts`](./svgrid-grid-chart-samples.md) - 3 exports\n- [`@svgrid/grid/SvGridChart.types.ts`](./svgrid-grid-SvGridChart.types.md) - 10 exports\n- [`@svgrid/grid/createTree.svelte.ts`](./svgrid-grid-createTree.svelte.md) - 12 exports\n- [`@svgrid/grid/editor-contract.ts`](./svgrid-grid-editor-contract.md) - 11 exports\n- [`@svgrid/grid/editor-registry.ts`](./svgrid-grid-editor-registry.md) - 9 exports\n- [`@svgrid/grid/list-option.ts`](./svgrid-grid-list-option.md) - 14 exports\n- [`@svgrid/grid/builtin-editors.ts`](./svgrid-grid-builtin-editors.md) - 1 exports\n- [`@svgrid/grid/advanced-filter.svelte.ts`](./svgrid-grid-advanced-filter.svelte.md) - 6 exports\n- [`@svgrid/grid/filtering/filter-operator-catalogue.ts`](./svgrid-grid-filter-operator-catalogue.md) - 8 exports\n- [`@svgrid/grid/filtering/row-predicate.ts`](./svgrid-grid-row-predicate.md) - 1 exports\n- [`@svgrid/grid/grid-messages.ts`](./svgrid-grid-grid-messages.md) - 4 exports\n- [`@svgrid/grid/chart-messages.ts`](./svgrid-grid-chart-messages.md) - 4 exports\n- [`@svgrid/grid/chart-panel-messages.ts`](./svgrid-grid-chart-panel-messages.md) - 4 exports\n- [`@svgrid/grid/chart-validate.ts`](./svgrid-grid-chart-validate.md) - 7 exports\n- [`@svgrid/grid/chart-summary.ts`](./svgrid-grid-chart-summary.md) - 2 exports\n- [`@svgrid/grid/chart-stream.ts`](./svgrid-grid-chart-stream.md) - 3 exports\n- [`@svgrid/grid/chart-pivot.ts`](./svgrid-grid-chart-pivot.md) - 6 exports\n- [`@svgrid/grid/chart-table.ts`](./svgrid-grid-chart-table.md) - 3 exports\n- [`@svgrid/grid/grid-icons.ts`](./svgrid-grid-grid-icons.md) - 4 exports\n- [`@svgrid/grid/cell-formatting.ts`](./svgrid-grid-cell-formatting.md) - 5 exports\n- [`@svgrid/grid/positioning.ts`](./svgrid-grid-positioning.md) - 10 exports\n- [`@svgrid/grid/toast-store.svelte.ts`](./svgrid-grid-toast-store.svelte.md) - 14 exports\n- [`@svgrid/grid/spreadsheet.ts`](./svgrid-grid-spreadsheet.md) - 7 exports\n- [`@svgrid/grid/chart-export.ts`](./svgrid-grid-chart-export.md) - 7 exports\n- [`@svgrid/grid/scheduler-ical.ts`](./svgrid-grid-scheduler-ical.md) - 5 exports\n- [`@svgrid/grid/duration.ts`](./svgrid-grid-duration.md) - 2 exports\n- [`@svgrid/grid/datetime/date-core.ts`](./svgrid-grid-date-core.md) - 22 exports\n- [`@svgrid/grid/datetime/timezone.ts`](./svgrid-grid-timezone.md) - 8 exports\n- [`@svgrid/grid/datetime/mask.ts`](./svgrid-grid-mask.md) - 3 exports\n- [`@svgrid/grid/datetime/date-format.ts`](./svgrid-grid-date-format.md) - 3 exports\n- [`@svgrid/grid/datetime/date-restrict.ts`](./svgrid-grid-date-restrict.md) - 5 exports\n- [`@svgrid/grid/server-data-source.ts`](./svgrid-grid-server-data-source.md) - 21 exports\n- [`@svgrid/grid/server-block-cache.ts`](./svgrid-grid-server-block-cache.md) - 8 exports\n- [`@svgrid/grid/row-model.ts`](./svgrid-grid-row-model.md) - 4 exports\n\n## @svgrid/enterprise\n\n- [`@svgrid/enterprise/export.ts`](./svgrid-enterprise-export.md) - 17 exports\n- [`@svgrid/enterprise/pivot.ts`](./svgrid-enterprise-pivot.md) - 10 exports\n- [`@svgrid/enterprise/pivot-chart.ts`](./svgrid-enterprise-pivot-chart.md) - 2 exports\n- [`@svgrid/enterprise/import.ts`](./svgrid-enterprise-import.md) - 13 exports\n- [`@svgrid/enterprise/install.ts`](./svgrid-enterprise-install.md) - 4 exports\n- [`@svgrid/enterprise/server/server-row-model.ts`](./svgrid-enterprise-server-row-model.md) - 19 exports\n- [`@svgrid/enterprise/server/server-selection.ts`](./svgrid-enterprise-server-selection.md) - 6 exports\n- [`@svgrid/enterprise/server/server-pivot.ts`](./svgrid-enterprise-server-pivot.md) - 2 exports\n- [`@svgrid/enterprise/server/svgrid-adapter.ts`](./svgrid-enterprise-svgrid-adapter.md) - 16 exports\n"
7628
+ "markdown": "# Auto-generated reference\n\n_Generated from the published TypeScript sources. Do not edit by hand - rerun `node tools/build-reference.mjs`._\n\nFor the hand-curated reference (with defaults + behaviour tables),\nsee [the main reference index](../index.md).\n\n## @svgrid/grid\n\n- [`@svgrid/grid/core.ts`](./svgrid-grid-core.md) - 68 exports\n- [`@svgrid/grid/svgrid-wrapper.types.ts`](./svgrid-grid-svgrid-wrapper.types.md) - 10 exports\n- [`@svgrid/grid/ai.ts`](./svgrid-grid-ai.md) - 34 exports\n- [`@svgrid/grid/scheduler-model.ts`](./svgrid-grid-scheduler-model.md) - 30 exports\n- [`@svgrid/grid/dock-model.ts`](./svgrid-grid-dock-model.md) - 25 exports\n- [`@svgrid/grid/dock-manager-model.ts`](./svgrid-grid-dock-manager-model.md) - 32 exports\n- [`@svgrid/grid/chart.ts`](./svgrid-grid-chart.md) - 19 exports\n- [`@svgrid/grid/chart-types.ts`](./svgrid-grid-chart-types.md) - 61 exports\n- [`@svgrid/grid/chart-scale.ts`](./svgrid-grid-chart-scale.md) - 7 exports\n- [`@svgrid/grid/chart-format.ts`](./svgrid-grid-chart-format.md) - 1 exports\n- [`@svgrid/grid/chart-stats.ts`](./svgrid-grid-chart-stats.md) - 17 exports\n- [`@svgrid/grid/chart-trend.ts`](./svgrid-grid-chart-trend.md) - 2 exports\n- [`@svgrid/grid/chart-axes.ts`](./svgrid-grid-chart-axes.md) - 1 exports\n- [`@svgrid/grid/chart-cartesian.ts`](./svgrid-grid-chart-cartesian.md) - 5 exports\n- [`@svgrid/grid/chart-polar.ts`](./svgrid-grid-chart-polar.md) - 1 exports\n- [`@svgrid/grid/chart-hierarchy.ts`](./svgrid-grid-chart-hierarchy.md) - 2 exports\n- [`@svgrid/grid/chart-decimate.ts`](./svgrid-grid-chart-decimate.md) - 6 exports\n- [`@svgrid/grid/chart-financial.ts`](./svgrid-grid-chart-financial.md) - 8 exports\n- [`@svgrid/grid/chart-zoom.ts`](./svgrid-grid-chart-zoom.md) - 6 exports\n- [`@svgrid/grid/chart-motion.ts`](./svgrid-grid-chart-motion.md) - 1 exports\n- [`@svgrid/grid/chart-sync.svelte.ts`](./svgrid-grid-chart-sync.svelte.md) - 4 exports\n- [`@svgrid/grid/chart-indicators.ts`](./svgrid-grid-chart-indicators.md) - 14 exports\n- [`@svgrid/grid/chart-export-pdf.ts`](./svgrid-grid-chart-export-pdf.md) - 3 exports\n- [`@svgrid/grid/chart-samples.ts`](./svgrid-grid-chart-samples.md) - 3 exports\n- [`@svgrid/grid/SvGridChart.types.ts`](./svgrid-grid-SvGridChart.types.md) - 10 exports\n- [`@svgrid/grid/createTree.svelte.ts`](./svgrid-grid-createTree.svelte.md) - 12 exports\n- [`@svgrid/grid/editor-contract.ts`](./svgrid-grid-editor-contract.md) - 11 exports\n- [`@svgrid/grid/editor-registry.ts`](./svgrid-grid-editor-registry.md) - 9 exports\n- [`@svgrid/grid/list-option.ts`](./svgrid-grid-list-option.md) - 14 exports\n- [`@svgrid/grid/builtin-editors.ts`](./svgrid-grid-builtin-editors.md) - 1 exports\n- [`@svgrid/grid/advanced-filter.svelte.ts`](./svgrid-grid-advanced-filter.svelte.md) - 6 exports\n- [`@svgrid/grid/filtering/filter-operator-catalogue.ts`](./svgrid-grid-filter-operator-catalogue.md) - 8 exports\n- [`@svgrid/grid/filtering/row-predicate.ts`](./svgrid-grid-row-predicate.md) - 1 exports\n- [`@svgrid/grid/grid-messages.ts`](./svgrid-grid-grid-messages.md) - 4 exports\n- [`@svgrid/grid/chart-messages.ts`](./svgrid-grid-chart-messages.md) - 4 exports\n- [`@svgrid/grid/chart-panel-messages.ts`](./svgrid-grid-chart-panel-messages.md) - 4 exports\n- [`@svgrid/grid/chart-validate.ts`](./svgrid-grid-chart-validate.md) - 7 exports\n- [`@svgrid/grid/chart-summary.ts`](./svgrid-grid-chart-summary.md) - 2 exports\n- [`@svgrid/grid/chart-stream.ts`](./svgrid-grid-chart-stream.md) - 3 exports\n- [`@svgrid/grid/chart-pivot.ts`](./svgrid-grid-chart-pivot.md) - 6 exports\n- [`@svgrid/grid/chart-table.ts`](./svgrid-grid-chart-table.md) - 3 exports\n- [`@svgrid/grid/grid-icons.ts`](./svgrid-grid-grid-icons.md) - 4 exports\n- [`@svgrid/grid/cell-formatting.ts`](./svgrid-grid-cell-formatting.md) - 5 exports\n- [`@svgrid/grid/positioning.ts`](./svgrid-grid-positioning.md) - 10 exports\n- [`@svgrid/grid/toast-store.svelte.ts`](./svgrid-grid-toast-store.svelte.md) - 14 exports\n- [`@svgrid/grid/spreadsheet.ts`](./svgrid-grid-spreadsheet.md) - 7 exports\n- [`@svgrid/grid/chart-export.ts`](./svgrid-grid-chart-export.md) - 7 exports\n- [`@svgrid/grid/scheduler-ical.ts`](./svgrid-grid-scheduler-ical.md) - 5 exports\n- [`@svgrid/grid/duration.ts`](./svgrid-grid-duration.md) - 2 exports\n- [`@svgrid/grid/datetime/date-core.ts`](./svgrid-grid-date-core.md) - 22 exports\n- [`@svgrid/grid/datetime/timezone.ts`](./svgrid-grid-timezone.md) - 8 exports\n- [`@svgrid/grid/datetime/mask.ts`](./svgrid-grid-mask.md) - 3 exports\n- [`@svgrid/grid/datetime/date-format.ts`](./svgrid-grid-date-format.md) - 3 exports\n- [`@svgrid/grid/datetime/date-restrict.ts`](./svgrid-grid-date-restrict.md) - 5 exports\n- [`@svgrid/grid/server-data-source.ts`](./svgrid-grid-server-data-source.md) - 21 exports\n- [`@svgrid/grid/server-block-cache.ts`](./svgrid-grid-server-block-cache.md) - 8 exports\n- [`@svgrid/grid/row-model.ts`](./svgrid-grid-row-model.md) - 4 exports\n\n## @svgrid/enterprise\n\n- [`@svgrid/enterprise/export.ts`](./svgrid-enterprise-export.md) - 17 exports\n- [`@svgrid/enterprise/pivot.ts`](./svgrid-enterprise-pivot.md) - 10 exports\n- [`@svgrid/enterprise/pivot-chart.ts`](./svgrid-enterprise-pivot-chart.md) - 2 exports\n- [`@svgrid/enterprise/import.ts`](./svgrid-enterprise-import.md) - 13 exports\n- [`@svgrid/enterprise/install.ts`](./svgrid-enterprise-install.md) - 4 exports\n- [`@svgrid/enterprise/server/server-row-model.ts`](./svgrid-enterprise-server-row-model.md) - 19 exports\n- [`@svgrid/enterprise/server/server-selection.ts`](./svgrid-enterprise-server-selection.md) - 6 exports\n- [`@svgrid/enterprise/server/server-pivot.ts`](./svgrid-enterprise-server-pivot.md) - 2 exports\n- [`@svgrid/enterprise/server/svgrid-adapter.ts`](./svgrid-enterprise-svgrid-adapter.md) - 16 exports\n"
7629
7629
  },
7630
7630
  {
7631
7631
  "slug": "reference/auto/svgrid-enterprise-export",
@@ -7646,7 +7646,7 @@ export const docs = [
7646
7646
  "path": "docs/reference/auto/svgrid-enterprise-install.md",
7647
7647
  "title": "@svgrid/enterprise · install.ts",
7648
7648
  "section": "API reference",
7649
- "markdown": "# `@svgrid/enterprise` · `install.ts`\n\nAuto-generated. Source: `packages\\enterprise\\src\\install.ts`.\n\n### `type EnterpriseAIApi`\n\nThe `api.ai.*` namespace added by the enterprise install.\n\n```ts\nexport type EnterpriseAIApi<TData extends RowData> = {\n /** Natural-language -> filter + sort plan (and optionally apply it). */\n filter(query: string, opts?: AIFilterOptions): Promise<AIFilterResult>\n /** Propose values for empty cells based on a few worked examples. */\n smartFill<TValue = unknown>(opts: AISmartFillOptions): Promise<AISmartFillResult<TValue>>\n /** One-paragraph + bullets summary of a row, selection, group, or the whole view. */\n summarize(opts: AISummarizeOptions): Promise<AISummary>\n /** Classify free-text cells into one of a known set of labels. */\n classify(opts: AIClassifyOptions): Promise<AIClassifyResult>\n /** Natural-language export: \"export EU orders from Q2 as a grouped PDF\". */\n export(query: string, opts?: AIExportOptions): Promise<AIExportPlan>\n /** Scan a slice for anomalies / outliers, returning a structured list. */\n findAnomalies(opts?: AIAnomalyOptions): Promise<AIAnomalyResult>\n /** Natural-language chart: \"revenue by region, stacked by product\". */\n chart(query: string, opts?: AIChartOptions): Promise<AIChartPlan>\n // TData is referenced so the type stays bound to the row shape even\n // though the helpers all read through `api.getData()`. Lets callers\n // get correct inference downstream without explicit generics.\n readonly _rowType?: TData\n}\n```\n\n### `type EnterprisePivotApi`\n\nThe `api.pivot.*` namespace added by the enterprise install.\n\n```ts\nexport type EnterprisePivotApi<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = {\n /**\n * Build a pivot row/column model from the grid's current data. Returns\n * `{ rows, columns }` you can feed to a separate `<SvGrid>` instance\n * (the typical pattern - the source grid stays unmodified). Pure: no\n * mutation, no DOM access.\n */\n build(config: PivotConfig<TData>): PivotResult<TFeatures>\n /**\n * Build a pivot from an arbitrary array (not necessarily the grid's\n * data). Useful for previewing a designer's config against a sample\n * before committing.\n */\n buildFrom<TIn extends RowData>(\n data: ReadonlyArray<TIn>,\n config: PivotConfig<TIn>,\n ): PivotResult<TFeatures>\n}\n```\n\n### `type EnterpriseGridApi`\n\nThe grid API plus everything enterprise adds - what `installEnterprise` hands back.\n\n```ts\nexport type EnterpriseGridApi<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = SvGridApi<TFeatures, TData> & {\n /** Export the current visible rows to the chosen format. Returns the built\n * file ({@link ExportResult}) for the dependency-free paths (undefined for\n * the vendored-writer xlsx paths, which download directly). */\n exportData(opts: ExportOptions<TData>): Promise<ExportResult | undefined>\n /** Copy the grid (view / selection / all) to the clipboard in a native text\n * format (default `tsv`, pastes into Excel / Sheets as columns). */\n copyExport(opts?: ClipboardExportOptions<TData>): Promise<void>\n /** Open a printable view of the current visible rows in a new window. */\n print(opts?: PrintOptions<TData>): Promise<void>\n /** Read an Excel / CSV / TSV / JSON file (or inline text) into typed\n * rows. Caller decides whether to commit into the grid or preview\n * the parsed result first via the returned `ImportResult`. */\n importData(opts: ImportOptions<TData>): Promise<ImportResult<TData>>\n /** AI helpers (Pro). All calls route through a consumer-registered\n * AIProvider via setAIProvider(); no model client is bundled. */\n ai: EnterpriseAIApi<TData>\n /** Pivot table builder (Pro). Pure - returns `{ rows, columns }` you\n * hand to a separate `<SvGrid>` instance. */\n pivot: EnterprisePivotApi<TFeatures, TData>\n}\n```\n\n### `function installEnterprise`\n\nAugment a SvGridApi instance with Pro methods (`exportData`, `print`,\n`ai.*`). Mutates and returns the same object so existing references\nkeep working.\n\nIf no license key has been set, the Pro methods still function but the\ngrid shows a small \"unlicensed\" watermark linking to jqwidgets.com and\nthe console emits a one-time nudge. Revoked or malformed keys throw on\nfirst call.\n\n```ts\nexport function installEnterprise<\n TFeatures extends TableFeatures,\n TData extends RowData,\n>(api: SvGridApi<TFeatures, TData>): EnterpriseGridApi<TFeatures, TData> {\n const pro = api as EnterpriseGridApi<TFeatures, TData>\n pro.exportData = (opts) => exportGrid(pro, opts)\n pro.copyExport = (opts) => copyExportToClipboard(pro, opts)\n pro.print = (opts) => printGrid(pro, opts)\n pro.importData = (opts) => importData(pro, opts)\n pro.ai = {\n filter: (query, opts) => aiFilter(pro, query, opts),\n smartFill: (opts) => aiSmartFill(pro, opts),\n summarize: (opts) => aiSummarize(pro, opts),\n classify: (opts) => aiClassify(pro, opts),\n export: (query, opts) => aiExport(pro, query, opts),\n findAnomalies: (opts) => aiFindAnomalies(pro, opts),\n chart: (query, opts) => aiChart(pro, query, opts),\n }\n // Wire the built-in chart panel's AI button (no-op without the `charting` prop).\n enableAiCharting(pro)\n // Register the scheduler / calendar view (no-op without the `scheduler` prop).\n enableSchedulerView()\n // Register the Kanban board view (no-op without the `board` prop).\n enableBoardView()\n // Register the Gantt view (no-op without the `gantt` prop).\n enableGanttView()\n enableSelectionBar()\n // Register the export engine so built-in AI exports (aiExport) can write the\n // enterprise formats (xlsx / pdf / ...); free grids without it get the plan only.\n registerExportProvider(exportGrid as never)\n enablePivot()\n enableAdvancedFilter()\n // Register the Excel keymap (Ctrl+Arrow, Ctrl+D, Ctrl+A and the rest). The\n // grid interprets a key itself only when nothing claims it, so a free grid\n // is unaffected.\n enableSheet()\n // Raise the unlicensed nudge for the server-side row model. No registry to fill -\n // the model hands the grid ordinary rows - so this only nudges when unlicensed.\n enableServerRowModel()\n pro.pivot = {\n build: (config) =>\n createPivotModel<TFeatures, TData>(pro.getData(), config),\n buildFrom: <TIn extends RowData>(data: ReadonlyArray<TIn>, config: PivotConfig<TIn>) =>\n // PivotConfig is variance-strict in TIn; the runtime call only needs\n // the row shape to match config.rows / config.cols field names, which\n // the caller-side generic enforces.\n createPivotModel<TFeatures, TIn>(data, config),\n }\n // Surface the unlicensed state at install time so the watermark appears\n // alongside the grid, not just after the user clicks Export.\n if (!hasValidLicense()) emitUnlicensedNudge()\n return pro\n}\n```\n"
7649
+ "markdown": "# `@svgrid/enterprise` · `install.ts`\n\nAuto-generated. Source: `packages\\enterprise\\src\\install.ts`.\n\n### `type EnterpriseAIApi`\n\nThe `api.ai.*` namespace added by the enterprise install.\n\n```ts\nexport type EnterpriseAIApi<TData extends RowData> = {\n /** Natural-language -> filter + sort plan (and optionally apply it). */\n filter(query: string, opts?: AIFilterOptions): Promise<AIFilterResult>\n /** Propose values for empty cells based on a few worked examples. */\n smartFill<TValue = unknown>(opts: AISmartFillOptions): Promise<AISmartFillResult<TValue>>\n /** One-paragraph + bullets summary of a row, selection, group, or the whole view. */\n summarize(opts: AISummarizeOptions): Promise<AISummary>\n /** Classify free-text cells into one of a known set of labels. */\n classify(opts: AIClassifyOptions): Promise<AIClassifyResult>\n /** Natural-language export: \"export EU orders from Q2 as a grouped PDF\". */\n export(query: string, opts?: AIExportOptions): Promise<AIExportPlan>\n /** Scan a slice for anomalies / outliers, returning a structured list. */\n findAnomalies(opts?: AIAnomalyOptions): Promise<AIAnomalyResult>\n /** Natural-language chart: \"revenue by region, stacked by product\". */\n chart(query: string, opts?: AIChartOptions): Promise<AIChartPlan>\n // TData is referenced so the type stays bound to the row shape even\n // though the helpers all read through `api.getData()`. Lets callers\n // get correct inference downstream without explicit generics.\n readonly _rowType?: TData\n}\n```\n\n### `type EnterprisePivotApi`\n\nThe `api.pivot.*` namespace added by the enterprise install.\n\n```ts\nexport type EnterprisePivotApi<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = {\n /**\n * Build a pivot row/column model from the grid's current data. Returns\n * `{ rows, columns }` you can feed to a separate `<SvGrid>` instance\n * (the typical pattern - the source grid stays unmodified). Pure: no\n * mutation, no DOM access.\n */\n build(config: PivotConfig<TData>): PivotResult<TFeatures>\n /**\n * Build a pivot from an arbitrary array (not necessarily the grid's\n * data). Useful for previewing a designer's config against a sample\n * before committing.\n */\n buildFrom<TIn extends RowData>(\n data: ReadonlyArray<TIn>,\n config: PivotConfig<TIn>,\n ): PivotResult<TFeatures>\n}\n```\n\n### `type EnterpriseGridApi`\n\nThe grid API plus everything enterprise adds - what `installEnterprise` hands back.\n\n```ts\nexport type EnterpriseGridApi<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = SvGridApi<TFeatures, TData> & {\n /** Export the current visible rows to the chosen format. Returns the built\n * file ({@link ExportResult}) for the dependency-free paths (undefined for\n * the vendored-writer xlsx paths, which download directly). */\n exportData(opts: ExportOptions<TData>): Promise<ExportResult | undefined>\n /** Copy the grid (view / selection / all) to the clipboard in a native text\n * format (default `tsv`, pastes into Excel / Sheets as columns). */\n copyExport(opts?: ClipboardExportOptions<TData>): Promise<void>\n /** Open a printable view of the current visible rows in a new window. */\n print(opts?: PrintOptions<TData>): Promise<void>\n /** Read an Excel / CSV / TSV / JSON file (or inline text) into typed\n * rows. Caller decides whether to commit into the grid or preview\n * the parsed result first via the returned `ImportResult`. */\n importData(opts: ImportOptions<TData>): Promise<ImportResult<TData>>\n /** AI helpers (Pro). All calls route through a consumer-registered\n * AIProvider via setAIProvider(); no model client is bundled. */\n ai: EnterpriseAIApi<TData>\n /** Pivot table builder (Pro). Pure - returns `{ rows, columns }` you\n * hand to a separate `<SvGrid>` instance. */\n pivot: EnterprisePivotApi<TFeatures, TData>\n}\n```\n\n### `function installEnterprise`\n\nAugment a SvGridApi instance with Pro methods (`exportData`, `print`,\n`ai.*`). Mutates and returns the same object so existing references\nkeep working.\n\nIf no license key has been set, the Pro methods still function but the\ngrid shows a small \"unlicensed\" watermark linking to jqwidgets.com and\nthe console emits a one-time nudge. Revoked or malformed keys throw on\nfirst call.\n\n```ts\nexport function installEnterprise<\n TFeatures extends TableFeatures,\n TData extends RowData,\n>(api: SvGridApi<TFeatures, TData>): EnterpriseGridApi<TFeatures, TData> {\n const pro = api as EnterpriseGridApi<TFeatures, TData>\n pro.exportData = (opts) => exportGrid(pro, opts)\n pro.copyExport = (opts) => copyExportToClipboard(pro, opts)\n pro.print = (opts) => printGrid(pro, opts)\n pro.importData = (opts) => importData(pro, opts)\n pro.ai = {\n filter: (query, opts) => aiFilter(pro, query, opts),\n smartFill: (opts) => aiSmartFill(pro, opts),\n summarize: (opts) => aiSummarize(pro, opts),\n classify: (opts) => aiClassify(pro, opts),\n export: (query, opts) => aiExport(pro, query, opts),\n findAnomalies: (opts) => aiFindAnomalies(pro, opts),\n chart: (query, opts) => aiChart(pro, query, opts),\n }\n // Wire the built-in chart panel's AI button (no-op without the `charting` prop).\n enableAiCharting(pro)\n // Register the scheduler / calendar view (no-op without the `scheduler` prop).\n enableSchedulerView()\n // Register the Kanban board view (no-op without the `board` prop).\n enableBoardView()\n // Register the gallery view (no-op without the `gallery` prop).\n enableGalleryView()\n // Register the Gantt view (no-op without the `gantt` prop).\n enableGanttView()\n enableSelectionBar()\n // Register the export engine so built-in AI exports (aiExport) can write the\n // enterprise formats (xlsx / pdf / ...); free grids without it get the plan only.\n registerExportProvider(exportGrid as never)\n enablePivot()\n enableAdvancedFilter()\n // Register the Excel keymap (Ctrl+Arrow, Ctrl+D, Ctrl+A and the rest). The\n // grid interprets a key itself only when nothing claims it, so a free grid\n // is unaffected.\n enableSheet()\n // Raise the unlicensed nudge for the server-side row model. No registry to fill -\n // the model hands the grid ordinary rows - so this only nudges when unlicensed.\n enableServerRowModel()\n pro.pivot = {\n build: (config) =>\n createPivotModel<TFeatures, TData>(pro.getData(), config),\n buildFrom: <TIn extends RowData>(data: ReadonlyArray<TIn>, config: PivotConfig<TIn>) =>\n // PivotConfig is variance-strict in TIn; the runtime call only needs\n // the row shape to match config.rows / config.cols field names, which\n // the caller-side generic enforces.\n createPivotModel<TFeatures, TIn>(data, config),\n }\n // Surface the unlicensed state at install time so the watermark appears\n // alongside the grid, not just after the user clicks Export.\n if (!hasValidLicense()) emitUnlicensedNudge()\n return pro\n}\n```\n"
7650
7650
  },
7651
7651
  {
7652
7652
  "slug": "reference/auto/svgrid-enterprise-pivot-chart",
@@ -7709,7 +7709,7 @@ export const docs = [
7709
7709
  "path": "docs/reference/auto/svgrid-grid-ai.md",
7710
7710
  "title": "@svgrid/grid · ai.ts",
7711
7711
  "section": "API reference",
7712
- "markdown": "# `@svgrid/grid` · `ai.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\ai.ts`.\n\n### `type AIProvider`\n\nShape every consumer-provided model adapter must implement. Keeping this\ntiny (one async call, two response formats) lets the same adapter drive\nOpenAI's `chat.completions`, Anthropic's `messages`, a self-hosted\nllama.cpp endpoint, or a server-side proxy. The grid only cares that the\nprovider eventually returns a string we can parse.\n\n```ts\nexport type AIProvider = (request: AIRequest) => Promise<string>\n```\n\n### `type AIRequest`\n\nOne call out to the model, as the grid builds it. Providers receive this.\n\n```ts\nexport type AIRequest = {\n /** Full prompt the grid built for the model. Already includes column\n * schema and any sampled rows where applicable. */\n prompt: string\n /** When 'json', the provider should ask the model to return strict\n * JSON only - no prose. We parse the response with JSON.parse and\n * throw a typed error on failure. */\n responseFormat?: 'text' | 'json'\n /** Honored if the underlying transport supports cancellation. */\n signal?: AbortSignal\n /** Free-form tag for telemetry / logging. One of: 'filter',\n * 'smart-fill', 'summarize', 'classify'. */\n task: AITask\n /** Soft hint to the provider about how many tokens we expect back.\n * Useful for routing small jobs to a cheaper model. */\n maxOutputTokens?: number\n}\n```\n\n### `type AITask`\n\nWhich helper produced a request - carried on {@link AIRequest} for routing and telemetry.\n\n```ts\nexport type AITask = 'filter' | 'smart-fill' | 'summarize' | 'classify' | 'export' | 'anomaly' | 'chart' | 'explain-chart'\n```\n\n### `function setAIProvider`\n\nRegister the model adapter every AI call will route through. Call once\nat app boot. Passing `null` clears the provider and AI calls revert to\nthrowing \"no provider\" errors.\n\n```ts\nexport function setAIProvider(p: AIProvider | null): void {\n provider = p\n}\n```\n\n### `function getAIProvider`\n\nThe provider registered with `setAIProvider`, or null when none is.\n\n```ts\nexport function getAIProvider(): AIProvider | null {\n return provider\n}\n```\n\n### `function hasAIProvider`\n\nWhether an AI provider is registered. Gate AI affordances on this so the UI stays honest.\n\n```ts\nexport function hasAIProvider(): boolean {\n return provider != null\n}\n```\n\n### `type AIFilterClause`\n\nOne condition in a filter plan: a column, a comparison, and the value to match.\n\n```ts\nexport type AIFilterClause = {\n field: string\n operator: 'contains' | 'equals' | 'startsWith' | 'greaterThan' | 'lessThan' | 'isBlank'\n value?: string\n}\n```\n\n### `type AISortClause`\n\nOne ordering clause in a filter plan.\n\n```ts\nexport type AISortClause = { field: string; desc: boolean }\n```\n\n### `type AIFilterResult`\n\nA natural-language query turned into filters and sorting, plus the model's reasoning.\n\n```ts\nexport type AIFilterResult = {\n filters: AIFilterClause[]\n sort: AISortClause[]\n /** Plain-English explanation of how the model interpreted the query.\n * Surface this in the UI so the user can confirm or undo. */\n rationale: string\n}\n```\n\n### `type AIFilterOptions`\n\nOptions for `aiFilter` - preview the plan, or apply it straight to the grid.\n\n```ts\nexport type AIFilterOptions = {\n /**\n * When true, the helper not only RETURNS the plan but also applies it\n * to the grid via `api.setFilter` / `api.setSort`. Defaults to false\n * so callers can show a preview before committing.\n */\n apply?: boolean\n signal?: AbortSignal\n}\n```\n\n### `type AISmartFillExample`\n\nOne worked example teaching smart-fill what to produce for a row.\n\n```ts\nexport type AISmartFillExample = { input: Record<string, unknown>; output: unknown }\n```\n\n### `type AISmartFillResult`\n\nProposed values for the blank cells of one column, each with a confidence score.\n\n```ts\nexport type AISmartFillResult<TValue = unknown> = {\n field: string\n predictions: Array<{ rowIndex: number; value: TValue; confidence: number }>\n rationale: string\n}\n```\n\n### `type AISmartFillOptions`\n\nOptions for `aiSmartFill` - which column to fill, which rows, and the examples to learn from.\n\n```ts\nexport type AISmartFillOptions = {\n /** Target column - the one whose values we want filled. */\n field: string\n /** Index of rows the model should propose values for. If omitted, every\n * row whose current `field` value is `null`, `undefined` or `''` is\n * selected automatically. */\n targetRowIndices?: number[]\n /**\n * Worked examples the user has already filled in. Required - the\n * model needs at least one to know the pattern, two or more to lock\n * the schema. We don't pull these from the grid automatically because\n * \"edited\" vs \"untouched\" isn't a state SvGrid exposes today.\n */\n examples: AISmartFillExample[]\n signal?: AbortSignal\n}\n```\n\n### `type AISummarizeTarget`\n\nWhat to summarise: one row, the selection, a group, or the whole set.\n\n```ts\nexport type AISummarizeTarget =\n | { kind: 'row'; rowIndex: number }\n | { kind: 'all' }\n | { kind: 'selection'; rowIndices: number[] }\n | { kind: 'group'; field: string; value: unknown }\n```\n\n### `type AISummary`\n\nA generated summary: prose, bullets, and the columns the model leaned on.\n\n```ts\nexport type AISummary = {\n text: string\n bullets: string[]\n /** Field names the model thinks are the most load-bearing for the\n * story it just told. UI can highlight those columns. */\n highlightedFields: string[]\n}\n```\n\n### `type AISummarizeOptions`\n\nOptions for `aiSummarize` - the target, and optionally the question to answer.\n\n```ts\nexport type AISummarizeOptions = {\n target: AISummarizeTarget\n /** Optional question the user is trying to answer. Helps the model\n * bias the summary toward the relevant columns. */\n question?: string\n signal?: AbortSignal\n}\n```\n\n### `type AIClassifyOptions`\n\nOptions for `aiClassify` - the column to label and the categories to choose from.\n\n```ts\nexport type AIClassifyOptions = {\n /** Column whose free-text we're classifying. */\n inputField: string\n /** Target column the model should write to. */\n outputField: string\n /** Allowed values. The model is constrained to pick one. */\n classes: string[]\n /** Optional one-line description of each class (acts as a labeling rubric). */\n classDescriptions?: Record<string, string>\n /** Rows to classify. Defaults to all. */\n targetRowIndices?: number[]\n signal?: AbortSignal\n}\n```\n\n### `type AIClassifyResult`\n\nProposed category labels per row, with the model's reasoning.\n\n```ts\nexport type AIClassifyResult = {\n inputField: string\n outputField: string\n predictions: Array<{ rowIndex: number; value: string; confidence: number }>\n}\n```\n\n### `type AIExportPlan`\n\nAn export the model derived from a request: format, columns, and scope.\n\n```ts\nexport type AIExportPlan = {\n format: ExportFormat\n filters: AIFilterClause[]\n sort: AISortClause[]\n groupBy: string[]\n /** Plain-English explanation of how the model read the request. */\n rationale: string\n}\n```\n\n### `type AIExportOptions`\n\nOptions for `aiExport` - preview the plan, or run the export it describes.\n\n```ts\nexport type AIExportOptions = {\n /**\n * Also apply the filter / sort / grouping to the grid (mutating the view) so\n * it mirrors the export. Default FALSE - the export is self-contained (it\n * computes its own rows), so the grid is left untouched unless you opt in.\n */\n apply?: boolean\n /** Actually download the file. Default true; set false to preview the plan. */\n run?: boolean\n /** Base filename (no extension). Default 'export'. */\n filename?: string\n signal?: AbortSignal\n}\n```\n\n### `type AIAnomaly`\n\nOne flagged value, with why it stands out and how strongly.\n\n```ts\nexport type AIAnomaly = {\n /** Index into the SCANNED rows (target order), when the model pins one row. */\n rowIndex?: number\n field?: string\n value?: unknown\n reason: string\n severity: 'low' | 'medium' | 'high'\n}\n```\n\n### `type AIAnomalyResult`\n\nEverything an anomaly scan flagged across the rows it looked at.\n\n```ts\nexport type AIAnomalyResult = {\n anomalies: AIAnomaly[]\n summary: string\n}\n```\n\n### `type AIAnomalyOptions`\n\nOptions for `aiFindAnomalies` - which rows and columns to scan.\n\n```ts\nexport type AIAnomalyOptions = {\n /** Which rows to scan. Defaults to the whole dataset. */\n target?: AISummarizeTarget\n /** Optional focus, e.g. \"look at pricing and margins\". */\n question?: string\n signal?: AbortSignal\n}\n```\n\n### `type AIChartType`\n\nChart shapes the model may choose from when planning a visualisation.\n\nThese are exactly the types the built-in panel can build from a dimension, a\nmeasure and an optional split - which is all a chart plan carries. Scatter\nis deliberately absent, because a plan cannot express its SECOND measure.\nA candlestick can be asked for: the panel finds the open / high / low /\nclose / volume columns by name, so the plan only needs a date dimension.\n\n```ts\nexport type AIChartType =\n | 'bar' | 'line' | 'area' | 'pie'\n | 'treemap' | 'funnel' | 'waterfall' | 'radar'\n | 'heatmap' | 'boxplot' | 'gauge' | 'calendar' | 'sankey'\n | 'histogram' | 'lollipop' | 'pareto' | 'sunburst' | 'radial-bar' | 'radial-column' | 'nightingale' | 'bullet' | 'chord' | 'stream'\n | 'candlestick'\n```\n\n### `type AIChartIndicator`\n\nThe indicators a chart plan may ask for on a candlestick.\n\n```ts\nexport type AIChartIndicator = 'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'\n```\n\n### `type AIChartPlan`\n\nA chart the model proposed: its type, and the fields to plot.\n\n```ts\nexport type AIChartPlan = {\n type: AIChartType\n /** Group-by (category-axis) column field, or null. */\n dimension: string | null\n /** Split-by column field: one series per distinct value, or null. */\n series: string | null\n /** Measure (value-axis) column field, or null. */\n measure: string | null\n reduce: AIChartReduce\n stacked: boolean\n logScale: boolean\n timeAxis: boolean\n valueFormat: 'number' | 'currency' | 'percent'\n /** Calendar unit to group a date dimension by, or null for exact values. */\n bucket: 'day' | 'week' | 'month' | 'quarter' | 'year' | null\n /** Candlestick only: indicator panes and overlays to add. */\n indicators?: AIChartIndicator[]\n rationale: string\n /** With `apply`: what `validateChartSpec` flagged as an error on the spec\n * the panel built from the plan, empty when it drew cleanly. */\n diagnostics?: ChartDiagnostic[]\n}\n```\n\n### `type AIChartReduce`\n\nThe reducers a chart plan may name: the panel's full list.\n\n```ts\nexport type AIChartReduce =\n | 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'p90' | 'first' | 'last' | 'countDistinct'\n```\n\n### `type AIChartOptions`\n\nOptions for `aiChart` - preview the plan, or render it into the grid.\n\n```ts\nexport type AIChartOptions = {\n /** Apply the plan to the grid's chart panel (open + configure). Default false. */\n apply?: boolean\n signal?: AbortSignal\n}\n```\n\n### `type AIChartExplanation`\n\nWhat `aiExplainChart` returns: the grounded summary and the model's insights.\n\n```ts\nexport type AIChartExplanation = {\n /** The plain-language reading of the chart (`chartSummary`), as the model saw it. */\n summary: string\n /** Two to three observations the model added: comparisons, outliers, what to look at next. */\n insights: string[]\n}\n```\n\n### `type AIExplainChartOptions`\n\nOptions for `aiExplainChart`.\n\n```ts\nexport type AIExplainChartOptions = {\n /** The spec to explain. Default: the grid panel's active chart. */\n spec?: ChartSpec\n signal?: AbortSignal\n}\n```\n\n### `function enableAiCharting`\n\nWire the grid's chart-panel AI button to the model: registers a handler (via\n`api.setChartAiHandler`) that runs `aiChart` and returns the plan for the\npanel to apply + explain. Call once after `onApiReady`. `installEnterprise`\ncalls this for you.\n\n```ts\nexport function enableAiCharting<\n TFeatures extends TableFeatures,\n TData extends RowData,\n>(api: SvGridApi<TFeatures, TData>): void {\n const hook = api as unknown as {\n setChartAiHandler?: (\n fn: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,\n ) => void\n }\n hook.setChartAiHandler?.(async (prompt: string) => {\n const plan = await aiChart(api, prompt)\n return {\n type: plan.type,\n dimension: plan.dimension,\n series: plan.series,\n measure: plan.measure,\n reduce: plan.reduce,\n stacked: plan.stacked,\n logScale: plan.logScale,\n timeAxis: plan.timeAxis,\n valueFormat: plan.valueFormat,\n bucket: plan.bucket,\n rationale: plan.rationale,\n }\n })\n // And the Explain button beside it.\n const explain = api as unknown as { setChartExplainHandler?: (fn: (() => Promise<AIChartExplanation | null>) | null) => void }\n explain.setChartExplainHandler?.(() => aiExplainChart(api))\n}\n```\n\n### `function disableAiCharting`\n\nRemove the natural-language chart handler, hiding the AI button in the chart\npanel. The inverse of `enableAiCharting`; safe to call when none was set.\n\n```ts\nexport function disableAiCharting<\n TFeatures extends TableFeatures,\n TData extends RowData,\n>(api: SvGridApi<TFeatures, TData>): void {\n const hook = api as unknown as { setChartAiHandler?: (fn: null) => void; setChartExplainHandler?: (fn: null) => void }\n hook.setChartAiHandler?.(null)\n hook.setChartExplainHandler?.(null)\n}\n```\n\n### `const mockAIProvider`\n\nA deterministic provider that returns canned, schema-shaped responses\nfor each task. Wire it in via `setAIProvider(mockAIProvider)` to make\nthe AI demo work end-to-end without a real model key. Not for production.\n\n```ts\nexport const mockAIProvider: AIProvider = async (req) => {\n // Tiny artificial delay so the UI's \"thinking\" state is visible.\n await new Promise((r) => setTimeout(r, 350 + Math.random() * 400))\n\n if (req.task === 'filter') {\n const q = extractUserQuery(req.prompt)\n return JSON.stringify(buildMockFilter(req.prompt, q))\n }\n if (req.task === 'smart-fill') {\n return JSON.stringify(buildMockSmartFill(req.prompt))\n }\n if (req.task === 'summarize') {\n return JSON.stringify(buildMockSummary(req.prompt))\n }\n if (req.task === 'classify') {\n return JSON.stringify(buildMockClassify(req.prompt))\n }\n if (req.task === 'export') {\n return JSON.stringify(buildMockExport(req.prompt))\n }\n if (req.task === 'anomaly') {\n return JSON.stringify(buildMockAnomaly(req.prompt))\n }\n if (req.task === 'chart') {\n return JSON.stringify(buildMockChart(req.prompt))\n }\n if (req.task === 'explain-chart') {\n // Two canned observations round the reading the prompt was grounded on.\n const reading = /Reading of the chart: ([^\\n]+)/.exec(req.prompt)?.[1] ?? ''\n const rises = /rises/.test(reading)\n return JSON.stringify({\n insights: [\n rises ? 'The growth is front-loaded: the early categories carry most of the change.' : 'The movement is gradual with no single category driving it.',\n 'Compare the peak against the same category a period earlier before reading it as a trend.',\n ],\n })\n }\n return '{}'\n}\n```\n"
7712
+ "markdown": "# `@svgrid/grid` · `ai.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\ai.ts`.\n\n### `type AIProvider`\n\nShape every consumer-provided model adapter must implement. Keeping this\ntiny (one async call, two response formats) lets the same adapter drive\nOpenAI's `chat.completions`, Anthropic's `messages`, a self-hosted\nllama.cpp endpoint, or a server-side proxy. The grid only cares that the\nprovider eventually returns a string we can parse.\n\n```ts\nexport type AIProvider = (request: AIRequest) => Promise<string>\n```\n\n### `type AIRequest`\n\nOne call out to the model, as the grid builds it. Providers receive this.\n\n```ts\nexport type AIRequest = {\n /** Full prompt the grid built for the model. Already includes column\n * schema and any sampled rows where applicable. */\n prompt: string\n /** When 'json', the provider should ask the model to return strict\n * JSON only - no prose. We parse the response with JSON.parse and\n * throw a typed error on failure. */\n responseFormat?: 'text' | 'json'\n /** Honored if the underlying transport supports cancellation. */\n signal?: AbortSignal\n /** Free-form tag for telemetry / logging. One of: 'filter',\n * 'smart-fill', 'summarize', 'classify'. */\n task: AITask\n /** Soft hint to the provider about how many tokens we expect back.\n * Useful for routing small jobs to a cheaper model. */\n maxOutputTokens?: number\n}\n```\n\n### `type AITask`\n\nWhich helper produced a request - carried on {@link AIRequest} for routing and telemetry.\n\n```ts\nexport type AITask = 'filter' | 'smart-fill' | 'summarize' | 'classify' | 'export' | 'anomaly' | 'chart' | 'explain-chart'\n```\n\n### `function setAIProvider`\n\nRegister the model adapter every AI call will route through. Call once\nat app boot. Passing `null` clears the provider and AI calls revert to\nthrowing \"no provider\" errors.\n\n```ts\nexport function setAIProvider(p: AIProvider | null): void {\n provider = p\n}\n```\n\n### `function getAIProvider`\n\nThe provider registered with `setAIProvider`, or null when none is.\n\n```ts\nexport function getAIProvider(): AIProvider | null {\n return provider\n}\n```\n\n### `function hasAIProvider`\n\nWhether an AI provider is registered. Gate AI affordances on this so the UI stays honest.\n\n```ts\nexport function hasAIProvider(): boolean {\n return provider != null\n}\n```\n\n### `type AIRowSource`\n\nWhat the read-only helpers need from a grid: its rows. A full `SvGridApi`\nsatisfies it, and so does `{ getData: () => rows }`, which lets a chat or a\nreport summarize data that is not on screen in a grid. `aiFilter` with\n`apply: true` and `aiExport` still need the grid itself.\n\n```ts\nexport type AIRowSource<\n TFeatures extends TableFeatures = TableFeatures,\n TData extends RowData = RowData,\n> = Pick<SvGridApi<TFeatures, TData>, 'getData'>\n```\n\n### `type AIFilterClause`\n\nOne condition in a filter plan: a column, a comparison, and the value to match.\n\n```ts\nexport type AIFilterClause = {\n field: string\n operator: 'contains' | 'equals' | 'startsWith' | 'greaterThan' | 'lessThan' | 'isBlank'\n value?: string\n}\n```\n\n### `type AISortClause`\n\nOne ordering clause in a filter plan.\n\n```ts\nexport type AISortClause = { field: string; desc: boolean }\n```\n\n### `type AIFilterResult`\n\nA natural-language query turned into filters and sorting, plus the model's reasoning.\n\n```ts\nexport type AIFilterResult = {\n filters: AIFilterClause[]\n sort: AISortClause[]\n /** Plain-English explanation of how the model interpreted the query.\n * Surface this in the UI so the user can confirm or undo. */\n rationale: string\n}\n```\n\n### `type AIFilterOptions`\n\nOptions for `aiFilter` - preview the plan, or apply it straight to the grid.\n\n```ts\nexport type AIFilterOptions = {\n /**\n * When true, the helper not only RETURNS the plan but also applies it\n * to the grid via `api.setFilter` / `api.setSort`. Defaults to false\n * so callers can show a preview before committing.\n */\n apply?: boolean\n signal?: AbortSignal\n}\n```\n\n### `type AISmartFillExample`\n\nOne worked example teaching smart-fill what to produce for a row.\n\n```ts\nexport type AISmartFillExample = { input: Record<string, unknown>; output: unknown }\n```\n\n### `type AISmartFillResult`\n\nProposed values for the blank cells of one column, each with a confidence score.\n\n```ts\nexport type AISmartFillResult<TValue = unknown> = {\n field: string\n predictions: Array<{ rowIndex: number; value: TValue; confidence: number }>\n rationale: string\n}\n```\n\n### `type AISmartFillOptions`\n\nOptions for `aiSmartFill` - which column to fill, which rows, and the examples to learn from.\n\n```ts\nexport type AISmartFillOptions = {\n /** Target column - the one whose values we want filled. */\n field: string\n /** Index of rows the model should propose values for. If omitted, every\n * row whose current `field` value is `null`, `undefined` or `''` is\n * selected automatically. */\n targetRowIndices?: number[]\n /**\n * Worked examples the user has already filled in. Required - the\n * model needs at least one to know the pattern, two or more to lock\n * the schema. We don't pull these from the grid automatically because\n * \"edited\" vs \"untouched\" isn't a state SvGrid exposes today.\n */\n examples: AISmartFillExample[]\n signal?: AbortSignal\n}\n```\n\n### `type AISummarizeTarget`\n\nWhat to summarise: one row, the selection, a group, or the whole set.\n\n```ts\nexport type AISummarizeTarget =\n | { kind: 'row'; rowIndex: number }\n | { kind: 'all' }\n | { kind: 'selection'; rowIndices: number[] }\n | { kind: 'group'; field: string; value: unknown }\n```\n\n### `type AISummary`\n\nA generated summary: prose, bullets, and the columns the model leaned on.\n\n```ts\nexport type AISummary = {\n text: string\n bullets: string[]\n /** Field names the model thinks are the most load-bearing for the\n * story it just told. UI can highlight those columns. */\n highlightedFields: string[]\n}\n```\n\n### `type AISummarizeOptions`\n\nOptions for `aiSummarize` - the target, and optionally the question to answer.\n\n```ts\nexport type AISummarizeOptions = {\n target: AISummarizeTarget\n /** Optional question the user is trying to answer. Helps the model\n * bias the summary toward the relevant columns. */\n question?: string\n signal?: AbortSignal\n}\n```\n\n### `type AIClassifyOptions`\n\nOptions for `aiClassify` - the column to label and the categories to choose from.\n\n```ts\nexport type AIClassifyOptions = {\n /** Column whose free-text we're classifying. */\n inputField: string\n /** Target column the model should write to. */\n outputField: string\n /** Allowed values. The model is constrained to pick one. */\n classes: string[]\n /** Optional one-line description of each class (acts as a labeling rubric). */\n classDescriptions?: Record<string, string>\n /** Rows to classify. Defaults to all. */\n targetRowIndices?: number[]\n signal?: AbortSignal\n}\n```\n\n### `type AIClassifyResult`\n\nProposed category labels per row, with the model's reasoning.\n\n```ts\nexport type AIClassifyResult = {\n inputField: string\n outputField: string\n predictions: Array<{ rowIndex: number; value: string; confidence: number }>\n}\n```\n\n### `type AIExportPlan`\n\nAn export the model derived from a request: format, columns, and scope.\n\n```ts\nexport type AIExportPlan = {\n format: ExportFormat\n filters: AIFilterClause[]\n sort: AISortClause[]\n groupBy: string[]\n /** Plain-English explanation of how the model read the request. */\n rationale: string\n}\n```\n\n### `type AIExportOptions`\n\nOptions for `aiExport` - preview the plan, or run the export it describes.\n\n```ts\nexport type AIExportOptions = {\n /**\n * Also apply the filter / sort / grouping to the grid (mutating the view) so\n * it mirrors the export. Default FALSE - the export is self-contained (it\n * computes its own rows), so the grid is left untouched unless you opt in.\n */\n apply?: boolean\n /** Actually download the file. Default true; set false to preview the plan. */\n run?: boolean\n /** Base filename (no extension). Default 'export'. */\n filename?: string\n signal?: AbortSignal\n}\n```\n\n### `type AIAnomaly`\n\nOne flagged value, with why it stands out and how strongly.\n\n```ts\nexport type AIAnomaly = {\n /** Index into the SCANNED rows (target order), when the model pins one row. */\n rowIndex?: number\n field?: string\n value?: unknown\n reason: string\n severity: 'low' | 'medium' | 'high'\n}\n```\n\n### `type AIAnomalyResult`\n\nEverything an anomaly scan flagged across the rows it looked at.\n\n```ts\nexport type AIAnomalyResult = {\n anomalies: AIAnomaly[]\n summary: string\n}\n```\n\n### `type AIAnomalyOptions`\n\nOptions for `aiFindAnomalies` - which rows and columns to scan.\n\n```ts\nexport type AIAnomalyOptions = {\n /** Which rows to scan. Defaults to the whole dataset. */\n target?: AISummarizeTarget\n /** Optional focus, e.g. \"look at pricing and margins\". */\n question?: string\n signal?: AbortSignal\n}\n```\n\n### `type AIChartType`\n\nChart shapes the model may choose from when planning a visualisation.\n\nThese are exactly the types the built-in panel can build from a dimension, a\nmeasure and an optional split - which is all a chart plan carries. Scatter\nis deliberately absent, because a plan cannot express its SECOND measure.\nA candlestick can be asked for: the panel finds the open / high / low /\nclose / volume columns by name, so the plan only needs a date dimension.\n\n```ts\nexport type AIChartType =\n | 'bar' | 'line' | 'area' | 'pie'\n | 'treemap' | 'funnel' | 'waterfall' | 'radar'\n | 'heatmap' | 'boxplot' | 'gauge' | 'calendar' | 'sankey'\n | 'histogram' | 'lollipop' | 'pareto' | 'sunburst' | 'radial-bar' | 'radial-column' | 'nightingale' | 'bullet' | 'chord' | 'stream'\n | 'candlestick'\n```\n\n### `type AIChartIndicator`\n\nThe indicators a chart plan may ask for on a candlestick.\n\n```ts\nexport type AIChartIndicator = 'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'\n```\n\n### `type AIChartPlan`\n\nA chart the model proposed: its type, and the fields to plot.\n\n```ts\nexport type AIChartPlan = {\n type: AIChartType\n /** Group-by (category-axis) column field, or null. */\n dimension: string | null\n /** Split-by column field: one series per distinct value, or null. */\n series: string | null\n /** Measure (value-axis) column field, or null. */\n measure: string | null\n reduce: AIChartReduce\n stacked: boolean\n logScale: boolean\n timeAxis: boolean\n valueFormat: 'number' | 'currency' | 'percent'\n /** Calendar unit to group a date dimension by, or null for exact values. */\n bucket: 'day' | 'week' | 'month' | 'quarter' | 'year' | null\n /** Candlestick only: indicator panes and overlays to add. */\n indicators?: AIChartIndicator[]\n rationale: string\n /** With `apply`: what `validateChartSpec` flagged as an error on the spec\n * the panel built from the plan, empty when it drew cleanly. */\n diagnostics?: ChartDiagnostic[]\n}\n```\n\n### `type AIChartReduce`\n\nThe reducers a chart plan may name: the panel's full list.\n\n```ts\nexport type AIChartReduce =\n | 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'p90' | 'first' | 'last' | 'countDistinct'\n```\n\n### `type AIChartOptions`\n\nOptions for `aiChart` - preview the plan, or render it into the grid.\n\n```ts\nexport type AIChartOptions = {\n /** Apply the plan to the grid's chart panel (open + configure). Default false. */\n apply?: boolean\n signal?: AbortSignal\n}\n```\n\n### `type AIChartExplanation`\n\nWhat `aiExplainChart` returns: the grounded summary and the model's insights.\n\n```ts\nexport type AIChartExplanation = {\n /** The plain-language reading of the chart (`chartSummary`), as the model saw it. */\n summary: string\n /** Two to three observations the model added: comparisons, outliers, what to look at next. */\n insights: string[]\n}\n```\n\n### `type AIExplainChartOptions`\n\nOptions for `aiExplainChart`.\n\n```ts\nexport type AIExplainChartOptions = {\n /** The spec to explain. Default: the grid panel's active chart. */\n spec?: ChartSpec\n signal?: AbortSignal\n}\n```\n\n### `function enableAiCharting`\n\nWire the grid's chart-panel AI button to the model: registers a handler (via\n`api.setChartAiHandler`) that runs `aiChart` and returns the plan for the\npanel to apply + explain. Call once after `onApiReady`. `installEnterprise`\ncalls this for you.\n\n```ts\nexport function enableAiCharting<\n TFeatures extends TableFeatures,\n TData extends RowData,\n>(api: SvGridApi<TFeatures, TData>): void {\n const hook = api as unknown as {\n setChartAiHandler?: (\n fn: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,\n ) => void\n }\n hook.setChartAiHandler?.(async (prompt: string) => {\n const plan = await aiChart(api, prompt)\n return {\n type: plan.type,\n dimension: plan.dimension,\n series: plan.series,\n measure: plan.measure,\n reduce: plan.reduce,\n stacked: plan.stacked,\n logScale: plan.logScale,\n timeAxis: plan.timeAxis,\n valueFormat: plan.valueFormat,\n bucket: plan.bucket,\n rationale: plan.rationale,\n }\n })\n // And the Explain button beside it.\n const explain = api as unknown as { setChartExplainHandler?: (fn: (() => Promise<AIChartExplanation | null>) | null) => void }\n explain.setChartExplainHandler?.(() => aiExplainChart(api))\n}\n```\n\n### `function disableAiCharting`\n\nRemove the natural-language chart handler, hiding the AI button in the chart\npanel. The inverse of `enableAiCharting`; safe to call when none was set.\n\n```ts\nexport function disableAiCharting<\n TFeatures extends TableFeatures,\n TData extends RowData,\n>(api: SvGridApi<TFeatures, TData>): void {\n const hook = api as unknown as { setChartAiHandler?: (fn: null) => void; setChartExplainHandler?: (fn: null) => void }\n hook.setChartAiHandler?.(null)\n hook.setChartExplainHandler?.(null)\n}\n```\n\n### `const mockAIProvider`\n\nA deterministic provider that returns canned, schema-shaped responses\nfor each task. Wire it in via `setAIProvider(mockAIProvider)` to make\nthe AI demo work end-to-end without a real model key. Not for production.\n\n```ts\nexport const mockAIProvider: AIProvider = async (req) => {\n // Tiny artificial delay so the UI's \"thinking\" state is visible.\n await new Promise((r) => setTimeout(r, 350 + Math.random() * 400))\n\n if (req.task === 'filter') {\n const q = extractUserQuery(req.prompt)\n return JSON.stringify(buildMockFilter(req.prompt, q))\n }\n if (req.task === 'smart-fill') {\n return JSON.stringify(buildMockSmartFill(req.prompt))\n }\n if (req.task === 'summarize') {\n return JSON.stringify(buildMockSummary(req.prompt))\n }\n if (req.task === 'classify') {\n return JSON.stringify(buildMockClassify(req.prompt))\n }\n if (req.task === 'export') {\n return JSON.stringify(buildMockExport(req.prompt))\n }\n if (req.task === 'anomaly') {\n return JSON.stringify(buildMockAnomaly(req.prompt))\n }\n if (req.task === 'chart') {\n return JSON.stringify(buildMockChart(req.prompt))\n }\n if (req.task === 'explain-chart') {\n // Two canned observations round the reading the prompt was grounded on.\n const reading = /Reading of the chart: ([^\\n]+)/.exec(req.prompt)?.[1] ?? ''\n const rises = /rises/.test(reading)\n return JSON.stringify({\n insights: [\n rises ? 'The growth is front-loaded: the early categories carry most of the change.' : 'The movement is gradual with no single category driving it.',\n 'Compare the peak against the same category a period earlier before reading it as a trend.',\n ],\n })\n }\n return '{}'\n}\n```\n"
7713
7713
  },
7714
7714
  {
7715
7715
  "slug": "reference/auto/svgrid-grid-builtin-editors",
@@ -7723,7 +7723,7 @@ export const docs = [
7723
7723
  "path": "docs/reference/auto/svgrid-grid-cell-formatting.md",
7724
7724
  "title": "@svgrid/grid · cell-formatting.ts",
7725
7725
  "section": "API reference",
7726
- "markdown": "# `@svgrid/grid` · `cell-formatting.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\cell-formatting.ts`.\n\n### `function resolveDatePattern`\n\nMap shortcut date patterns to Intl options (merged with caller `options`).\n\n```ts\nexport function resolveDatePattern(\n pattern: string | undefined,\n kind: 'date' | 'datetime',\n): Intl.DateTimeFormatOptions | undefined {\n if (!pattern) return undefined\n\n const p = pattern.trim()\n\n switch (p) {\n /** Short numeric local date (no weekday). */\n case 'd':\n return kind === 'datetime'\n ? {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n }\n : { year: 'numeric', month: 'numeric', day: 'numeric' }\n /** Long spelled-out date (+ time if datetime). */\n case 'D':\n return kind === 'datetime'\n ? {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n }\n : { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }\n /** ISO-friendly yyyy-mm-dd (with time when datetime). */\n case 'y-m-d':\n return kind === 'datetime'\n ? {\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n }\n : { year: 'numeric', month: '2-digit', day: '2-digit' }\n case 'medium':\n return { dateStyle: 'medium', timeStyle: kind === 'datetime' ? 'short' : undefined }\n case 'short':\n return { dateStyle: 'short', timeStyle: kind === 'datetime' ? 'short' : undefined }\n case 'long':\n return { dateStyle: 'long', timeStyle: kind === 'datetime' ? 'short' : undefined }\n default:\n return undefined\n }\n}\n```\n\n### `function getNumberFormatter`\n\nA cached `Intl.NumberFormat` for the locale and options given. Cached because\nconstructing one is among the most expensive calls in the platform, and both\ncallers do it per rendered value: once per cell while scrolling a number\ncolumn, and once per axis tick, data label and tooltip while drawing a chart.\n\n```ts\nexport function getNumberFormatter(\n locales: string | readonly string[] | undefined,\n options: Intl.NumberFormatOptions,\n): Intl.NumberFormat {\n // Stable key - for normal column configs the options object is reused\n // (declared in module scope), so this stringifies a small static shape.\n const key =\n (Array.isArray(locales) ? locales.join(',') : locales ?? '') +\n '|' +\n (options.style ?? '') +\n '|' +\n (options.currency ?? '') +\n '|' +\n (options.minimumFractionDigits ?? '') +\n '|' +\n (options.maximumFractionDigits ?? '') +\n '|' +\n (options.useGrouping ?? '') +\n '|' +\n (options.currencyDisplay ?? '') +\n '|' +\n (options.notation ?? '') +\n // Every option that changes the output belongs in the key. `signDisplay`\n // was missing, so a percent column asking for \"+3.27\" got the formatter\n // a currency column with the same digits had already cached, and no sign.\n '|' +\n (options.signDisplay ?? '') +\n '|' +\n (options.minimumIntegerDigits ?? '') +\n '|' +\n (options.compactDisplay ?? '') +\n '|' +\n (options.unit ?? '') +\n '|' +\n (options.unitDisplay ?? '') +\n '|' +\n (options.minimumSignificantDigits ?? '') +\n '|' +\n (options.maximumSignificantDigits ?? '')\n let fmt = numberFormatterCache.get(key)\n if (!fmt) {\n fmt = new Intl.NumberFormat(locales as string | string[] | undefined, options)\n numberFormatterCache.set(key, fmt)\n }\n return fmt\n}\n```\n\n### `function parseDateValue`\n\nA cell value as a Date, or null when it is not one. Accepts a Date, epoch ms,\nor a parseable string.\n\nA bare calendar date ('2026-06-27') is read as LOCAL midnight of that day.\n`new Date('2026-06-27')` reads it as UTC midnight, which every zone west of\nUTC formats as the 26th. The day in the string is the day the user meant.\n\n```ts\nexport function parseDateValue(value: unknown): Date | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) return null\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? null : d\n }\n if (typeof value !== 'string') return null\n const s = value.trim()\n if (s === '') return null\n const ymd = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(s)\n if (ymd) {\n const [y, m, day] = [Number(ymd[1]), Number(ymd[2]) - 1, Number(ymd[3])]\n const d = new Date(0)\n d.setFullYear(y, m, day)\n d.setHours(0, 0, 0, 0)\n // Reject a rollover ('2026-02-30' is not March 2nd), as `new Date` does.\n return d.getMonth() === m && d.getDate() === day ? d : null\n }\n const d = new Date(s)\n return Number.isNaN(d.getTime()) ? null : d\n}\n```\n\n### `function getDateFormatter`\n\nA cached `Intl.DateTimeFormat` for the locale and options given. Cached because\n constructing one per cell is the dominant cost when formatting a date column.\n\n```ts\nexport function getDateFormatter(\n locales: string | readonly string[] | undefined,\n options: Intl.DateTimeFormatOptions,\n): Intl.DateTimeFormat {\n const key =\n (Array.isArray(locales) ? locales.join(',') : locales ?? '') +\n '|' +\n (options.dateStyle ?? '') +\n '|' +\n (options.timeStyle ?? '') +\n '|' +\n (options.year ?? '') +\n '|' +\n (options.month ?? '') +\n '|' +\n (options.day ?? '') +\n '|' +\n (options.hour ?? '') +\n '|' +\n (options.minute ?? '') +\n '|' +\n (options.second ?? '') +\n '|' +\n (options.weekday ?? '') +\n '|' +\n (options.hour12 ?? '')\n let fmt = dateFormatterCache.get(key)\n if (!fmt) {\n fmt = new Intl.DateTimeFormat(locales as string | string[] | undefined, options)\n dateFormatterCache.set(key, fmt)\n }\n return fmt\n}\n```\n\n### `function formatNumericWithConfig`\n\nFormat a numeric value for number / currency / percent column formats.\n\n```ts\nexport function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string {\n const n = typeof value === 'number' ? value : Number(value)\n if (!Number.isFinite(n)) return String(value ?? '')\n\n const locales = config.locales\n\n if (config.type === 'currency') {\n return getNumberFormatter(locales, {\n style: 'currency',\n currency: config.currency ?? 'USD',\n ...config.options,\n }).format(n)\n }\n\n if (config.type === 'percent') {\n const frac = config.valueIsPercentPoints === true ? n / 100 : n\n return getNumberFormatter(locales, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n ...config.options,\n }).format(frac)\n }\n\n return getNumberFormatter(locales, config.options ?? {}).format(n)\n}\n```\n"
7726
+ "markdown": "# `@svgrid/grid` · `cell-formatting.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\cell-formatting.ts`.\n\n### `function resolveDatePattern`\n\nMap shortcut date patterns to Intl options (merged with caller `options`).\n\n```ts\nexport function resolveDatePattern(\n pattern: string | undefined,\n kind: 'date' | 'datetime',\n): Intl.DateTimeFormatOptions | undefined {\n if (!pattern) return undefined\n\n const p = pattern.trim()\n\n switch (p) {\n /** Short numeric local date (no weekday). */\n case 'd':\n return kind === 'datetime'\n ? {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n }\n : { year: 'numeric', month: 'numeric', day: 'numeric' }\n /** Long spelled-out date (+ time if datetime). */\n case 'D':\n return kind === 'datetime'\n ? {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n }\n : { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }\n /** ISO-friendly yyyy-mm-dd (with time when datetime). */\n case 'y-m-d':\n return kind === 'datetime'\n ? {\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n }\n : { year: 'numeric', month: '2-digit', day: '2-digit' }\n case 'medium':\n return { dateStyle: 'medium', timeStyle: kind === 'datetime' ? 'short' : undefined }\n case 'short':\n return { dateStyle: 'short', timeStyle: kind === 'datetime' ? 'short' : undefined }\n case 'long':\n return { dateStyle: 'long', timeStyle: kind === 'datetime' ? 'short' : undefined }\n default:\n return undefined\n }\n}\n```\n\n### `function getNumberFormatter`\n\nA cached `Intl.NumberFormat` for the locale and options given. Cached because\nconstructing one is among the most expensive calls in the platform, and both\ncallers do it per rendered value: once per cell while scrolling a number\ncolumn, and once per axis tick, data label and tooltip while drawing a chart.\n\n```ts\nexport function getNumberFormatter(\n locales: string | readonly string[] | undefined,\n options: Intl.NumberFormatOptions,\n): Intl.NumberFormat {\n // Stable key - for normal column configs the options object is reused\n // (declared in module scope), so this stringifies a small static shape.\n const key =\n (Array.isArray(locales) ? locales.join(',') : locales ?? '') +\n '|' +\n (options.style ?? '') +\n '|' +\n (options.currency ?? '') +\n '|' +\n (options.minimumFractionDigits ?? '') +\n '|' +\n (options.maximumFractionDigits ?? '') +\n '|' +\n (options.useGrouping ?? '') +\n '|' +\n (options.currencyDisplay ?? '') +\n '|' +\n (options.notation ?? '') +\n // Every option that changes the output belongs in the key. `signDisplay`\n // was missing, so a percent column asking for \"+3.27\" got the formatter\n // a currency column with the same digits had already cached, and no sign.\n '|' +\n (options.signDisplay ?? '') +\n '|' +\n (options.minimumIntegerDigits ?? '') +\n '|' +\n (options.compactDisplay ?? '') +\n '|' +\n (options.unit ?? '') +\n '|' +\n (options.unitDisplay ?? '') +\n '|' +\n (options.minimumSignificantDigits ?? '') +\n '|' +\n (options.maximumSignificantDigits ?? '')\n let fmt = numberFormatterCache.get(key)\n if (!fmt) {\n fmt = new Intl.NumberFormat(locales as string | string[] | undefined, options)\n numberFormatterCache.set(key, fmt)\n }\n return fmt\n}\n```\n\n### `function parseDateValue`\n\nA cell value as a Date, or null when it is not one. Accepts a Date, epoch ms,\nor a parseable string.\n\nA bare calendar date ('2026-06-27') is read as LOCAL midnight of that day.\n`new Date('2026-06-27')` reads it as UTC midnight, which every zone west of\nUTC formats as the 26th. The day in the string is the day the user meant.\n\n```ts\nexport function parseDateValue(value: unknown): Date | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) return null\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? null : d\n }\n if (typeof value !== 'string') return null\n const s = value.trim()\n if (s === '') return null\n const ymd = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(s)\n if (ymd) {\n const [y, m, day] = [Number(ymd[1]), Number(ymd[2]) - 1, Number(ymd[3])]\n const d = new Date(0)\n d.setFullYear(y, m, day)\n d.setHours(0, 0, 0, 0)\n // Reject a rollover ('2026-02-30' is not March 2nd), as `new Date` does.\n return d.getMonth() === m && d.getDate() === day ? d : null\n }\n const d = new Date(s)\n return Number.isNaN(d.getTime()) ? null : d\n}\n```\n\n### `function getDateFormatter`\n\nA cached `Intl.DateTimeFormat` for the locale and options given. Cached because\n constructing one per cell is the dominant cost when formatting a date column.\n\n```ts\nexport function getDateFormatter(\n locales: string | readonly string[] | undefined,\n options: Intl.DateTimeFormatOptions,\n): Intl.DateTimeFormat {\n const key =\n (Array.isArray(locales) ? locales.join(',') : locales ?? '') +\n '|' +\n (options.dateStyle ?? '') +\n '|' +\n (options.timeStyle ?? '') +\n '|' +\n (options.year ?? '') +\n '|' +\n (options.month ?? '') +\n '|' +\n (options.day ?? '') +\n '|' +\n (options.hour ?? '') +\n '|' +\n (options.minute ?? '') +\n '|' +\n (options.second ?? '') +\n '|' +\n (options.weekday ?? '') +\n '|' +\n (options.hour12 ?? '')\n let fmt = dateFormatterCache.get(key)\n if (!fmt) {\n fmt = new Intl.DateTimeFormat(locales as string | string[] | undefined, options)\n dateFormatterCache.set(key, fmt)\n }\n return fmt\n}\n```\n\n### `function formatNumericWithConfig`\n\nFormat a numeric value for number / currency / percent column formats.\n\n```ts\nexport function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string {\n // An empty cell stays empty. Number(null) and Number('') are 0, so a cleared\n // value in a currency column used to render as \"$0\", a number nobody entered.\n if (value == null || (typeof value === 'string' && value.trim() === '')) return ''\n const n = typeof value === 'number' ? value : Number(value)\n if (!Number.isFinite(n)) return String(value ?? '')\n\n const locales = config.locales\n\n if (config.type === 'currency') {\n return getNumberFormatter(locales, {\n style: 'currency',\n currency: config.currency ?? 'USD',\n ...config.options,\n }).format(n)\n }\n\n if (config.type === 'percent') {\n const frac = config.valueIsPercentPoints === true ? n / 100 : n\n return getNumberFormatter(locales, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n ...config.options,\n }).format(frac)\n }\n\n return getNumberFormatter(locales, config.options ?? {}).format(n)\n}\n```\n"
7727
7727
  },
7728
7728
  {
7729
7729
  "slug": "reference/auto/svgrid-grid-chart-axes",
@@ -7912,7 +7912,7 @@ export const docs = [
7912
7912
  "path": "docs/reference/auto/svgrid-grid-core.md",
7913
7913
  "title": "@svgrid/grid · core.ts",
7914
7914
  "section": "API reference",
7915
- "markdown": "# `@svgrid/grid` · `core.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\core.ts`.\n\n### `type RowData`\n\nThe constraint every row type satisfies: an object keyed by string. Your own\nrow type (`type Person = { name: string }`) is what flows through the generics\nbelow; this is only the lower bound they are declared against.\n\n```ts\nexport type RowData = Record<string, unknown>\n```\n\n### `type Updater`\n\nA new value, or a function that derives it from the previous one - the shape\nevery `set*` on the grid accepts, so callers can update state without first\nreading it.\n\n api.setSorting([{ id: 'name', desc: false }])\n api.setSorting((prev) => [...prev, { id: 'age', desc: true }])\n\n```ts\nexport type Updater<T> = T | ((prev: T) => T)\n```\n\n### `type SortingState`\n\nActive sort clauses, outermost first. `desc: false` is ascending.\n\n```ts\nexport type SortingState = Array<{ id: string; desc: boolean }>\n```\n\n### `type ColumnFilter`\n\nOne column's filter: the column `id`, the `value` being matched, and\noptionally which comparison to use. `fn` defaults to the column's own type -\nsee {@link filterFns} for the available names.\n\n```ts\nexport type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }\n```\n\n### `type ColumnFiltersState`\n\nEvery active column filter. A column with no entry here is unfiltered.\n\n```ts\nexport type ColumnFiltersState = Array<ColumnFilter>\n```\n\n### `type PaginationState`\n\nCurrent page position. `pageIndex` is 0-based, so page 1 is index 0.\n\n```ts\nexport type PaginationState = { pageIndex: number; pageSize: number }\n```\n\n### `type GroupingState`\n\nColumn ids the rows are grouped by, outermost first.\n\n```ts\nexport type GroupingState = Array<string>\n```\n\n### `type ExpandedState`\n\nWhich rows are expanded, keyed by row id. Absent means collapsed.\n\n```ts\nexport type ExpandedState = Record<string, boolean>\n```\n\n### `type RowSelectionState`\n\nWhich rows are selected, keyed by row id. Absent means unselected.\n\n```ts\nexport type RowSelectionState = Record<string, boolean>\n```\n\n### `type ActiveCellState`\n\nWhere keyboard focus sits. The indices address the *displayed* grid (after\nsorting, filtering and paging), not the source data.\n\n```ts\nexport type ActiveCellState = {\n rowIndex: number\n colIndex: number\n cellId: string | null\n}\n```\n\n### `type TableFeatures`\n\nThe set of features a grid has registered, as built by {@link tableFeatures}.\nDeliberately open: a feature is identified by its key, so the type carries\nwhich ones are on without enumerating them.\n\n```ts\nexport type TableFeatures = Record<string, unknown>\n```\n\n### `type CellData`\n\nA cell's value. Unconstrained - a column can hold anything.\n\n```ts\nexport type CellData = unknown\n```\n\n### `type HeaderContext`\n\nWhat a column's `header` render function receives.\n\n```ts\nexport type HeaderContext<TData extends RowData> = {\n header: Header<TData>\n column: Column<TData>\n table: SvGrid<TData>\n}\n```\n\n### `type CellContext`\n\nWhat a column's `cell` render function receives. `getValue()` applies the\ncolumn's accessor (`field` or `fieldFn`); `row.original` is the raw object.\n\n```ts\nexport type CellContext<TData extends RowData> = {\n cell: Cell<TData>\n row: Row<TData>\n column: Column<TData>\n table: SvGrid<TData>\n getValue: () => unknown\n}\n```\n\n### `type CellSpanParams`\n\nParams passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks.\n\n```ts\nexport type CellSpanParams<TData extends RowData = RowData> = {\n /** The row's underlying data object. */\n data: TData\n /** Display-row index in the current (filtered/sorted) row set. */\n rowIndex: number\n /** The column's id. */\n columnId: string\n /** The cell's base value for this column. */\n value: unknown\n}\n```\n\n### `type EditorOptionSource`\n\nThe raw option list a column's `editorOptions` can supply.\n\n```ts\nexport type EditorOptionSource = ReadonlyArray<\n string | number | { value: string | number; label?: string; color?: string }\n```\n\n### `type ValueParserParams`\n\nParams passed to a column's `valueParser(...)` on edit commit.\n\n```ts\nexport type ValueParserParams<TData extends RowData = RowData> = {\n /** The value after built-in per-`editorType` coercion. */\n newValue: unknown\n /** The cell's previous value. */\n oldValue: unknown\n /** The raw string the editor produced (pre-coercion). */\n rawInput: string\n /** The row's underlying data object. */\n data: TData\n /** The column's id. */\n columnId: string\n}\n```\n\n### `type EditorContext`\n\nContext passed to a custom `cellEditor` snippet/component. Three write\nhelpers cover the lifecycle:\n\n - `update(next)` - stage `next` as the draft, keep the editor open.\n Use this for live-preview controls (sliders,\n color pickers) so the user can keep adjusting.\n - `commit(next?)` - write the value AND close the editor. The\n argument is optional; when omitted, the most\n recently `update()`d value is saved. Use this\n for \"done\" gestures (Enter, picking an option).\n - `cancel()` - discard the draft and close the editor.\n\n```ts\nexport type EditorContext<TData extends RowData> = CellContext<TData> & {\n value: unknown\n update: (next: unknown) => void\n commit: (next?: unknown) => void\n cancel: () => void\n}\n```\n\n### `type CellFormatConfig`\n\nDeclarative cell formatting, applied through `Intl` - number, currency,\npercent, date and datetime. Prefer this over a `formatter` function: it is\nlocale-aware, and export and the clipboard reuse the same configuration.\n\n```ts\nexport type CellFormatConfig =\n | {\n type: 'number'\n locales?: string | Array<string>\n options?: Intl.NumberFormatOptions\n }\n | {\n type: 'currency'\n /** ISO 4217 (default USD) */\n currency?: string\n locales?: string | Array<string>\n options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>\n }\n | {\n type: 'percent'\n locales?: string | Array<string>\n options?: Omit<Intl.NumberFormatOptions, 'style'>\n /**\n * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).\n * Default false.\n */\n valueIsPercentPoints?: boolean\n }\n | {\n type: 'date' | 'datetime'\n locales?: string | Array<string>\n /**\n * Shortcut patterns merged with `options`:\n * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,\n * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.\n */\n pattern?: string\n options?: Intl.DateTimeFormatOptions\n }\n```\n\n### `type CellFormatter`\n\nA column's custom display function, for anything {@link CellFormatConfig}\ncannot express. Returns a string - to render markup, use `cell` instead.\n\n```ts\nexport type CellFormatter<TData extends RowData> = (context: {\n value: unknown\n row: Row<TData>\n column: Column<TData>\n table: SvGrid<TData>\n}) => string\n```\n\n### `type ColumnDefTemplate`\n\nA header or cell slot: a literal string, or a function returning renderable content.\n\n```ts\nexport type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)\n```\n\n### `type GroupAggregator`\n\nHow a column's value is aggregated for a group row when `columnGrouping`\nis active. Built-in reducers cover the common cases; pass a function for\nanything custom (weighted average, median, percentile, distinct count).\nThe function receives the finite numeric values AND the raw leaf rows.\n\n```ts\nexport type GroupAggregator<TData = any> =\n | 'sum'\n | 'avg'\n | 'min'\n | 'max'\n | 'count'\n | 'countDistinct'\n | 'extent'\n | 'first'\n | ((values: number[], rows: Array<TData>) => unknown)\n```\n\n### `function applyGroupAggregate`\n\nApply a group aggregator over a bucket's leaf rows for one column.\n\n```ts\nexport function applyGroupAggregate<TData extends RowData>(\n agg: GroupAggregator<TData>,\n columnId: string,\n rows: ReadonlyArray<Row<TData>>,\n): unknown {\n // One pass, no intermediate arrays.\n //\n // This used to build a `raw` array, then a coerced one, then a filtered one -\n // three allocations per aggregated column PER GROUP - before reducing. On a\n // 100k-row grid grouped two levels deep, aggregation was about two thirds of\n // the total grouping cost (213ms with three aggregators against 81ms with\n // none), and each additional aggregated column added roughly 80ms.\n //\n // `count` first: it never needs to look at a value at all.\n if (agg === 'count') return rows.length\n\n if (agg === 'first') {\n return rows.length ? rows[0]!.getCellValueByColumnId(columnId) : undefined\n }\n\n if (agg === 'countDistinct') {\n const seen = new Set<string>()\n for (const row of rows) seen.add(String(row.getCellValueByColumnId(columnId) ?? ''))\n return seen.size\n }\n\n if (typeof agg === 'function') {\n // Custom aggregators keep their contract: the finite numbers, then the\n // original row objects. Note `Number(null)` is 0 and therefore finite, so\n // nulls DO reach the callback as zeros - long-standing behaviour.\n const nums: number[] = []\n for (const row of rows) {\n const n = Number(row.getCellValueByColumnId(columnId))\n if (Number.isFinite(n)) nums.push(n)\n }\n return agg(nums, rows.map((r) => r.original))\n }\n\n // sum / avg / min / max / extent share one accumulation pass.\n let count = 0\n let sum = 0\n let min = Infinity\n let max = -Infinity\n for (const row of rows) {\n const n = Number(row.getCellValueByColumnId(columnId))\n if (!Number.isFinite(n)) continue\n count++\n // Left-to-right, matching the previous `reduce`, so float rounding is\n // bit-identical rather than merely close.\n sum += n\n // Math.min/max on scalars rather than `<`, which differs on -0, and rather\n // than the old `Math.min(...nums)` - spreading a whole group throws\n // RangeError once the bucket is big enough to exhaust the argument stack.\n min = Math.min(min, n)\n max = Math.max(max, n)\n }\n if (!count) return undefined\n switch (agg) {\n case 'sum':\n return sum\n case 'avg':\n return sum / count\n case 'min':\n return min\n case 'max':\n return max\n case 'extent':\n return `${min} – ${max}`\n default:\n return undefined\n }\n}\n```\n\n### `type ColumnDef`\n\nA column definition.\n\n`TFeatures` is a phantom parameter - it is threaded through nested\n`columns` groups but no member depends on it, so `{}`, `TableFeatures` and\n`typeof features` are all interchangeable here. It is deliberately left\nWITHOUT a default: `ColumnDef<Row>` would otherwise bind `Row` to this slot\nand silently type your data as `RowData`, losing every field-name check.\nPrefer {@link GridColumns} / {@link GridColumnDef} for the common case.\n\n```ts\nexport type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {\n id?: string\n field?: keyof TData & string\n fieldFn?: (row: TData) => unknown\n header?: ColumnDefTemplate<HeaderContext<TData>>\n footer?: ColumnDefTemplate<HeaderContext<TData>>\n cell?: ColumnDefTemplate<CellContext<TData>>\n columns?: Array<ColumnDef<TFeatures, TData>>\n /**\n * Declarative cell spanning (merged cells). Return how many COLUMNS this\n * cell spans to the right (1 = no span). Value-driven. Feed\n * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses\n * the same real `colspan`/`rowspan` merge engine (no separate code path).\n */\n colSpan?: (params: CellSpanParams<TData>) => number\n /**\n * Declarative cell spanning (merged cells). Return how many ROWS this cell\n * spans downward (1 = no span). See `colSpan` for how to apply.\n */\n rowSpan?: (params: CellSpanParams<TData>) => number\n /**\n * High-level data type for the column. A convenience that resolves to the\n * right `editorType`, alignment, date `format`, and filter operators without\n * setting each by hand:\n * 'text' → text editor, left-aligned\n * 'number' → number editor, right-aligned, numeric filter operators\n * 'boolean' → checkbox editor, centered\n * 'date' → date editor (Date values), right-aligned, `{ type: 'date' }` format\n * 'dateString' → date editor for ISO date STRINGS (e.g. '2026-06-27')\n * Anything you set explicitly (`editorType`, `align`, `format`) still wins -\n * `cellDataType` only fills the gaps. Grid-level `inferColumnTypes` infers\n * this from the first data row for columns that declare neither.\n */\n cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'\n /**\n * Hide this column when the grid's `responsive` mode is on and the grid is\n * narrower than this many pixels - drop low-priority columns on small\n * screens. No effect unless the grid has `responsive` set.\n */\n hideBelow?: number\n /**\n * For a column INSIDE a collapsible column group: `'open'` shows this column\n * only while the group is expanded, `'closed'` only while collapsed. Omit to\n * always show it. Setting it on any direct child gives the parent group a\n * collapse toggle. Pair with `openByDefault` on the group.\n */\n columnGroupShow?: 'open' | 'closed'\n /**\n * For a GROUP column (one with `columns: [...]`): start the group expanded.\n * Defaults to `false` (collapsed), the conventional default - so only the always-on\n * and `columnGroupShow: 'closed'` children show until the user expands it.\n */\n openByDefault?: boolean\n editorType?:\n | 'text'\n | 'number'\n | 'date' // rich SvCalendar popover (opt out with 'date-native')\n | 'datetime' // rich SvDateTimePicker (opt out with 'datetime-native')\n | 'time' // rich SvTimePicker dial (opt out with 'time-native')\n | 'date-native' // plain <input type=\"date\">\n | 'datetime-native' // plain <input type=\"datetime-local\">\n | 'time-native' // plain <input type=\"time\"> - HH:MM or HH:MM:SS\n | 'password' // native <input type=\"password\"> with masked rendering\n | 'checkbox'\n | 'list'\n | 'chips'\n | 'select' // custom dropdown - single value, no typeahead\n | 'rich-select' // custom dropdown with a typeahead search input\n | 'autocomplete' // free-text input with a live-filtered suggestion list (accepts any value)\n | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline\n | 'color' // native <input type=\"color\"> swatch\n | 'rating' // 5-star rating control\n // Any other string names a CUSTOM editor registered via `registerCellEditor`\n // (or `registerBuiltinEditors`). `(string & {})` keeps the literals above\n // autocompleting while allowing arbitrary custom type names.\n | (string & {})\n /**\n * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`\n * and `cancel()` helper. Use when none of the built-in `editorType`s fit;\n * the snippet's outer element is mounted inside the editing cell and\n * inherits keyboard handling (Esc cancels, Enter commits unless your\n * snippet preventDefaults it).\n *\n * Coexists with `editorType`: when both are set, `cellEditor` wins and\n * `editorType` is treated as a hint for parsing the saved value.\n */\n cellEditor?: ColumnDefTemplate<EditorContext<TData>>\n /**\n * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`\n * runs per cell so the tooltip can reflect the value. Returning an empty\n * string skips the tooltip.\n */\n tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)\n /**\n * Declarative per-cell validation. Runs for EVERY\n * rendered cell - including values already present in `data` on load, not\n * just on edit - so bad data is flagged immediately. Invalid cells get the\n * `sv-grid-cell-invalid` class (red highlight) and the returned message as\n * their tooltip.\n *\n * Return value:\n * - `null` / `undefined` / `true` → valid (no highlight)\n * - `false` → invalid, no message\n * - a non-empty `string` → invalid, string is the tooltip\n *\n * On its own `validate` flags: the value keeps rendering as-is and an\n * edit that fails it still lands. Set `rejectInvalid` to refuse such an\n * edit instead.\n */\n validate?: (params: {\n value: unknown\n row: TData\n rowIndex: number\n column: Column<TData>\n }) => string | boolean | null | undefined\n /**\n * Refuse an edit that `validate` flags. When true, a committed value that\n * makes `validate` return `false` or a message is not written: the cell\n * keeps its old value, no history step is recorded and\n * `onCellValueChange` does not fire. The value is checked after\n * `valueParser` has run, so the rule sees what would be stored. Applies to\n * the inline editor, a commit to a selection and a full-row edit; paste\n * does not go through the editor and is not checked.\n */\n rejectInvalid?: boolean\n /**\n * Gate editing per column or per cell.\n *\n * - `true` (or omitted): the column is fully editable.\n * - `false`: the column is read-only - double-click, type-to-edit,\n * fill-handle drag, Delete, and clipboard paste all skip it.\n * - `(ctx) => boolean`: evaluated for each cell, so you can lock\n * individual rows (e.g. by role, status, ownership). Returning\n * `false` opts the cell out of every editing path, identical to\n * setting `editable: false` on the whole column for that row.\n *\n * The grid-wide `enableInlineEditing` prop still wins when set to\n * `false`.\n */\n editable?: boolean | ((context: CellContext<TData>) => boolean)\n /**\n * Transform the committed edit value before it is written to the row.\n * Runs after the built-in per-`editorType` coercion, so `newValue` is\n * already type-parsed; return the final value to store (e.g. round a\n * number, uppercase a code, look up an id). A `valueParser` hook.\n */\n valueParser?: (params: ValueParserParams<TData>) => unknown\n /**\n * Briefly flash / highlight this column's cell when its value changes\n * (streaming feeds, edits, server pushes). `true` uses the default flash;\n * pass `{ className }` to apply your own animation class instead.\n */\n cellFlash?: boolean | { className?: string }\n /**\n * When `false`, this column never shows a sort indicator and clicking\n * its header is a no-op - `api.setSort(thisColumn, ...)` is also\n * ignored. Defaults to `true` (the column participates in sorting as\n * long as `rowSortingFeature` is registered).\n */\n sortable?: boolean\n /**\n * When `false`, this column never shows a filter funnel / menu and\n * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the\n * column is filterable as long as `columnFilteringFeature` is\n * registered).\n */\n filterable?: boolean\n /**\n * Options for `editorType: 'list' | 'chips'`. Either bare values (the\n * string is both value and label) or `{ value, label }` objects.\n * For `chips` this is optional - when omitted, the chips editor becomes\n * free-form (user types and presses Enter to commit a chip).\n *\n * Pass a function `(row) => options` for row-dependent (cascading)\n * options - e.g. City options that depend on Country in the same row.\n *\n * Either form may return a **Promise**, for options that come from the\n * server. While it resolves, the editor shows a loading state and the cell\n * renders its raw value.\n *\n * Results are cached so reopening an editor does not refetch: a static source\n * per column, a per-row source per row AND per that row's data - so a cascade\n * reloads by itself when the cell it depends on is edited. Call\n * `api.refreshEditorOptions(columnId?)` when the list changes server-side.\n */\n editorOptions?:\n | EditorOptionSource\n | Promise<EditorOptionSource>\n | ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>)\n /** When true, list/chips allow multiple selections. Cell value becomes an array. */\n editorMultiple?: boolean\n /**\n * The text editor accepts line breaks: Alt+Enter inserts one and the\n * editor grows a line per break, while Enter still commits. What a\n * spreadsheet cell with wrapped text wants; `editorType: 'textarea'` is\n * the other shape, where Enter itself is the line break.\n */\n editorMultiline?: boolean\n /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */\n editorSeparator?: string\n format?: CellFormatConfig\n formatter?: CellFormatter<TData>\n /**\n * Aggregate this column's values into the group row when grouping is\n * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |\n * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result\n * is formatted with this column's `format` and shown in the group header.\n */\n aggregate?: GroupAggregator<TData>\n /**\n * What this column contributes to the grid's footer summary row (the one\n * turned on with `summary` / `enableRowSummaries`). Takes the same\n * aggregators as {@link aggregate}, and the result is formatted with this\n * column's `format`.\n *\n * Without it the footer falls back to its default: the sum of a numeric\n * column, `Count: N` otherwise. Set `false` to leave the cell blank, which is\n * usually what an actions or checkbox column wants.\n *\n * { field: 'amount', summary: 'avg' }\n * { id: 'actions', summary: false }\n */\n summary?: GroupAggregator<TData> | false\n /**\n * Render the cell as an in-cell sparkline chart. The cell value should be\n * an array of numbers (or a comma/space separated string). Mutually\n * exclusive with a custom `cell` renderer (a `cell` wins if both are set).\n *\n * { sparkline: { type: 'line' } } // default line\n * { sparkline: { type: 'bar', color: '#16a34a' } }\n * { sparkline: { type: 'winloss' } } // sign-only up/down\n *\n * See `SparklineConfig` for the full option set (type, color,\n * negativeColor, width, height, fixed min/max).\n */\n sparkline?: SparklineConfig\n /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */\n width?: number\n /**\n * Whether the user may resize this column. Only consulted when the grid has\n * `columnResize` on - it narrows that, it does not enable anything.\n *\n * `false` removes the column's drag handle entirely, so pointer drag, the\n * keyboard arrows and double-click-to-autosize are all gone with it, and the\n * column menu drops its Autosize item. Use it for the columns whose width is\n * part of the layout rather than a preference: a row-number gutter, a\n * checkbox column, a fixed icon column.\n *\n * Programmatic sizing is unaffected - `api.autosizeColumn()`,\n * `api.setColumnWidth()` and `fitColumns` all still apply, the same way they\n * do when `columnResize` is off. This governs the user affordance only.\n */\n resizable?: boolean\n /**\n * Initial visibility. Set `false` to start the column hidden while still\n * listing it in the Choose Columns UI for the user to re-enable. Applied\n * once at mount; after that `api.setColumnVisible` / user toggles win.\n * On a group column, `false` hides the whole group's leaf columns.\n */\n visible?: boolean\n /**\n * Horizontal alignment for header and body cells. When omitted, the\n * default is inferred from `editorType`:\n * - `'number' | 'date' | 'datetime'` → `'right'`\n * - `'checkbox'` → `'center'`\n * - everything else → `'left'`\n */\n align?: 'left' | 'center' | 'right'\n /**\n * Per-cell conditional CSS. Two shapes:\n *\n * - **String** (or array of strings): class name(s) added to the\n * cell's `<td>` for every row in this column.\n * - **Function**: invoked per cell with the same `CellContext` shape\n * the `cell` renderer receives. Return a string, an array of\n * strings, or an object mapping class names to booleans.\n *\n * Use it for status tinting, conditional bold, \"negative number\"\n * coloring - anything that's a function of the row's value. Cells\n * still receive their format / cell renderer; the class just\n * augments the rendered `<td>`.\n */\n cellClass?:\n | string\n | ReadonlyArray<string>\n | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)\n}\n```\n\n### `type GridColumnDef`\n\nA column definition keyed only by your row type - the ergonomic form of\n{@link ColumnDef}, whose first parameter is a phantom feature bag that is\nalmost always `{}`.\n\n```ts\nconst columns: GridColumns<Person> = [{ field: 'firstName', header: 'Name' }]\n```\n\nInterchangeable with `ColumnDef<{}, TData>` and `ColumnDef<typeof features,\nTData>` in both directions, so it mixes freely with existing code.\n\n```ts\nexport type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeatures, TData>\n```\n\n### `type GridColumns`\n\nAn array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`.\n\n```ts\nexport type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>\n```\n\n### `type Column`\n\nA resolved column: your {@link ColumnDef} plus everything the grid computed\nfrom it - its id, its depth under any group header, and the sort handlers a\nheader needs. This is what you receive in render contexts; the `ColumnDef`\nis what you wrote.\n\n```ts\nexport type Column<TData extends RowData> = {\n id: string\n columnDef: ColumnDef<any, TData>\n depth: number\n parentId?: string\n getCanSort: () => boolean\n getCanFilter: () => boolean\n getIsSorted: () => false | 'asc' | 'desc'\n getToggleSortingHandler: () => () => void\n}\n```\n\n### `type Header`\n\nOne header cell. `colSpan` is how many leaf columns it covers, and\n`isPlaceholder` marks the empty cells that pad a group-header row so the\nlevels line up.\n\n```ts\nexport type Header<TData extends RowData> = {\n id: string\n isPlaceholder: boolean\n colSpan: number\n column: Column<TData>\n getContext: () => HeaderContext<TData>\n}\n```\n\n### `type HeaderGroup`\n\nOne row of header cells. A grid with grouped columns has several, outermost first.\n\n```ts\nexport type HeaderGroup<TData extends RowData> = {\n id: string\n headers: Array<Header<TData>>\n}\n```\n\n### `type Cell`\n\nOne cell: the intersection of a {@link Row} and a {@link Column}.\n\n```ts\nexport type Cell<TData extends RowData> = {\n id: string\n row: Row<TData>\n column: Column<TData>\n getValue: () => unknown\n getContext: () => CellContext<TData>\n}\n```\n\n### `type Row`\n\nA row in the display model. `original` is your untouched data object;\neverything else is grid-computed. `index` is the position in the displayed\nset, so it shifts as sorting and filtering change - key on `id`, not index.\n\nGroup rows and tree parents carry `subRows`; a plain data row does not.\n\n```ts\nexport type Row<TData extends RowData> = {\n id: string\n index: number\n original: TData\n depth: number\n subRows?: Array<Row<TData>>\n /** Total leaf (data) rows under this group row. Undefined for data rows. */\n leafCount?: number\n getCanExpand: () => boolean\n getIsExpanded: () => boolean\n toggleExpanded: () => void\n getIsSelected: () => boolean\n toggleSelected: () => void\n getAllCells: () => Array<Cell<TData>>\n getCellValueByColumnId: (columnId: string) => unknown\n}\n```\n\n### `type RowModel`\n\nThe output of the row pipeline: the rows to display, in order.\n\n```ts\nexport type RowModel<TData extends RowData> = {\n rows: Array<Row<TData>>\n}\n```\n\n### `type Store`\n\nThe minimal reactive store behind the headless core - read `state`, write\nthrough `setState`, and `subscribe` for changes. Deliberately framework\nfree, which is what lets the core run under plain Node.\n\nIn Svelte you rarely touch this: `subscribeGrid` wraps it with fine-grained\nselectors so a component only re-runs for the slice it read.\n\n```ts\nexport type Store<T> = {\n readonly state: T\n setState: (updater: (prev: T) => T) => void\n subscribe: (listener: () => void) => () => void\n}\n```\n\n### `const rowSortingFeature`\n\nClick-to-sort. Injected by the `sortable` shortcut.\n\nThis and the five features below are opaque markers: pass the ones you want\nto {@link tableFeatures} and the grid wires up the matching row model. With\n`<SvGrid>` you rarely name them - the boolean shortcuts (`sortable`,\n`filterable`, `pageable`, `groupable`) inject them for you. Reach for them\ndirectly when driving the headless core, or when you want a feature on\nwithout its UI.\n\nThe names match TanStack Table v9, so a features object written for it works\nhere unchanged.\n\n```ts\nexport const rowSortingFeature = { key: 'rowSortingFeature' }\n```\n\n### `const columnFilteringFeature`\n\nPer-column filtering. Injected by the `filterable` shortcut.\n\n```ts\nexport const columnFilteringFeature = { key: 'columnFilteringFeature' }\n```\n\n### `const rowPaginationFeature`\n\nPaging of the row model. Injected by the `pageable` shortcut.\n\n```ts\nexport const rowPaginationFeature = { key: 'rowPaginationFeature' }\n```\n\n### `const columnGroupingFeature`\n\nRow grouping with aggregation. Injected by the `groupable` shortcut.\n\n```ts\nexport const columnGroupingFeature = { key: 'columnGroupingFeature' }\n```\n\n### `const rowSelectionFeature`\n\nRow selection state (the checkbox column reads it).\n\n```ts\nexport const rowSelectionFeature = { key: 'rowSelectionFeature' }\n```\n\n### `const rowExpandingFeature`\n\nExpand / collapse, for tree rows and master-detail.\n\n```ts\nexport const rowExpandingFeature = { key: 'rowExpandingFeature' }\n```\n\n### `function tableFeatures`\n\nDeclare which features a grid uses. Identity at runtime - its whole job is to\ncapture the exact set in the type, so `ColumnDef<typeof features, Row>` knows\nwhat is registered and anything you did not register is tree-shaken out.\n\n```ts\nconst features = tableFeatures({ rowSortingFeature, columnFilteringFeature })\n```\n\nSame call signature as TanStack Table v9, so a features object written for it\ntransfers unchanged.\n\n```ts\nexport function tableFeatures<T extends TableFeatures>(features: T): T {\n return features\n}\n```\n\n### `const sortFns`\n\nBuilt-in comparators, chosen per column by its data type. `auto` compares as\ntext; set a column's type or supply your own comparator to override.\n\n```ts\nexport const sortFns = {\n auto: (a: unknown, b: unknown) => compareAutoValues(a, b),\n number: (a: unknown, b: unknown) => compareNumericKeys(numberSortKey(a), numberSortKey(b)),\n date: (a: unknown, b: unknown) => compareNumericKeys(dateSortKey(a), dateSortKey(b)),\n}\n```\n\n### `const filterFns`\n\nBuilt-in match functions, named by {@link ColumnFilter}'s `fn`.\n`includesString` is case-insensitive substring; `equals` is strict identity.\n\n```ts\nexport const filterFns = {\n includesString: (value: unknown, query: string) =>\n String(value).toLowerCase().includes(query.toLowerCase()),\n equals: (value: unknown, query: unknown) => value === query,\n}\n```\n\n### `type BaseRowCtx`\n\nEverything a base row needs that is the same for every row in the table.\n\nOne object per table, referenced by every row, instead of one closure scope\nper row. See {@link BASE_ROW_METHODS}.\n\n```ts\nexport type BaseRowCtx<TData extends RowData> = {\n grid: SvGrid<TData>\n store: { state: Record<string, any> }\n columns: Array<Column<TData>>\n columnCount: number\n columnIndexById: Map<string, number>\n}\n```\n\n### `const ROW_VALUES`\n\nA base row's cached cell values. @internal\n\n```ts\nexport const ROW_VALUES = Symbol('svgrid.row.values')\n```\n\n### `const ROW_CELLS`\n\nA base row's cached cells. @internal\n\n```ts\nexport const ROW_CELLS = Symbol('svgrid.row.cells')\n```\n\n### `const ROW_FILTER_POS`\n\nThe row's index in the last full filter's output (see ROW_FILTER_GEN). @internal\n\n```ts\nexport const ROW_FILTER_POS = Symbol('svgrid.row.filterPos')\n```\n\n### `function denseRows`\n\nThe rows as a plain dense array. For a lazyRows() list, makes the rows not\nmade yet and returns its backing array - the same row objects, without the\nProxy in front. Anything that reads every row (a sort, a filter) should go\nthrough this first: a trap per read, over several passes of a million rows,\ncost more than building the rows did.\n\n```ts\nexport function denseRows<T>(rows: T[]): T[] {\n const fill = (rows as unknown as Record<symbol, (() => T[]) | undefined>)[LAZY_FILL]\n return fill ? fill() : rows\n}\n```\n\n### `type WindowedRowModel`\n\nThe row model for windowed data (windowed-data.ts), or null when `data` is\nan ordinary array. It lives in windowed-row-model.ts and is installed by the\nfirst createWindowedData() call, so a grid that is never handed windowed\ndata does not bundle it: this file imports nothing from either module.\n\n```ts\nexport type WindowedRowModel<TData extends RowData> = (data: unknown, columns: Array<Column<TData>>) => RowModel<TData> | null\n```\n\n### `type WindowedRowModelInstall`\n\nHow windowed-row-model.ts builds its row model from the base row helpers. @internal\n\n```ts\nexport type WindowedRowModelInstall =<TData extends RowData>(ctx: {\n rowCtxFor: (columns: Array<Column<TData>>) => BaseRowCtx<TData>\n getRowId: () => ((row: TData, index: number) => string) | undefined\n}) => WindowedRowModel<TData>\n```\n\n### `function registerWindowedRowModel`\n\nInternal: windowed-row-model.ts installs itself here.\n\n```ts\nexport function registerWindowedRowModel(install: WindowedRowModelInstall): void {\n installWindowedRowModel = install\n}\n```\n\n### `type BaseRowState`\n\nA base row's private fields, on top of the public {@link Row} surface.\n\n```ts\nexport type BaseRowState<TData extends RowData> = Row<TData> & {\n [ROW_CTX]: BaseRowCtx<TData>\n [ROW_VALUES]: Array<unknown> | null\n [ROW_CELLS]: Array<Cell<TData>> | null\n [ROW_FILTER_GEN]: number\n [ROW_FILTER_POS]: number\n [ROW_SORT_DROPPED]: number\n [ROW_SORT_KEY]: unknown\n}\n```\n\n### `const BASE_ROW_METHODS`\n\nThe methods every base row carries, defined ONCE and assigned by reference.\n\nRows used to be built as object literals whose methods were closures, which\nmeant a 100k-row grid allocated 700k closures and a closure scope per row\nbefore painting anything. Measured at 100k x 9: 13.8 ms and 56.5 MB to build,\nagainst 2.0 ms and 14.5 MB for this shape - the single largest cost in\nmounting a large grid.\n\nThey read their row through `this` rather than a captured variable, which is\nwhy they can be shared. Note they are assigned as OWN properties rather than\nput on a prototype: `Row` is public, several row models legitimately do\n`{ ...row, depth }`, and a spread copies own properties but not a prototype.\nA class here would silently strip every method off a cloned row.\n\n```ts\nexport const BASE_ROW_METHODS = {\n getCanExpand(this: BaseRowState<RowData>) {\n return false\n },\n getIsExpanded(this: BaseRowState<RowData>) {\n return Boolean((this[ROW_CTX].store.state.expanded ?? {})[this.id])\n },\n toggleExpanded(this: BaseRowState<RowData>) {\n const id = this.id\n this[ROW_CTX].grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getIsSelected(this: BaseRowState<RowData>) {\n return Boolean((this[ROW_CTX].store.state.rowSelection ?? {})[this.id])\n },\n toggleSelected(this: BaseRowState<RowData>) {\n const id = this.id\n this[ROW_CTX].grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getAllCells(this: BaseRowState<RowData>) {\n return this[ROW_CELLS] ?? buildBaseRowCells(this)\n },\n getCellValueByColumnId(this: BaseRowState<RowData>, columnId: string) {\n const idx = this[ROW_CTX].columnIndexById.get(columnId)\n if (idx === undefined) return undefined\n if (!this[ROW_VALUES]) this[ROW_VALUES] = baseRowValues(this)\n return this[ROW_VALUES][idx]\n },\n}\n```\n\n### `type RowModelFactory`\n\nOne stage of the row pipeline: takes the rows produced so far and returns the\nnext set. Stages compose in the order given to `_rowModels`, so filtering\nbefore sorting sorts only what survived the filter.\n\n```ts\nexport type RowModelFactory<TData extends RowData> = (args: {\n table: SvGrid<TData>\n rows: Array<Row<TData>>\n}) => Array<Row<TData>>\n```\n\n### `function createCoreRowModel`\n\nThe identity stage that starts every pipeline. Always required, even when no\nother stage is: it is what turns your data into rows.\n\n```ts\nexport function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ rows }) => rows\n}\n```\n\n### `function createFilteredRowModel`\n\nDrops rows that fail the active {@link ColumnFiltersState}. Pairs with\n`columnFilteringFeature`; without it there are no filters to apply.\n\n```ts\nexport function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {\n /**\n * The last full filter, so a tick can repair it. When `getRowModel`\n * reports which rows were replaced (`replacedRowsOf`) and the filter state\n * and columns are the same objects as last time, each replacement is tested\n * on its own: a row that stays in keeps its slot, a row that stays out\n * stays out, and a row whose membership flips sends the whole thing back\n * through the full pass, because an insertion has to land in data order.\n * Positions come from stamps on the rows (ROW_FILTER_GEN / ROW_FILTER_POS)\n * written by the full pass; no map is built.\n */\n let cache: { input: Array<Row<TData>>; output: Array<Row<TData>>; filters: ColumnFiltersState; columns: Array<Column<TData>>; gen: number } | null = null\n let generation = 0\n\n return ({ table, rows }) => {\n const filters: ColumnFiltersState = table.getState().columnFilters ?? []\n if (!filters.length) {\n cache = null\n return rows\n }\n const columns = table.getAllColumns()\n\n // Resolve each filter's match function once, outside the row loop.\n const compiled = filters.map((filter) => ({\n id: filter.id,\n value: filter.value,\n fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,\n }))\n const passes = (row: Row<TData>) => {\n for (let i = 0; i < compiled.length; i++) {\n const filter = compiled[i]!\n // `getCellValueByColumnId` rather than `getAllCells().find(...)`.\n // Both read the same lazily-built `cachedValues` array, but the latter\n // also builds and caches the row's whole `Cell[]` - one object per\n // column - purely to reach one field. On a 100k-row grid that is\n // 100,000 cell arrays the filter never looks at again, and it defeats\n // the laziness the row factory exists to provide.\n if (!filter.fn(row.getCellValueByColumnId(filter.id), filter.value as any)) return false\n }\n return true\n }\n\n if (cache && cache.filters === filters && cache.columns === columns) {\n if (cache.input === rows) return cache.output\n const replaced = replacedRowsOf(table)\n if (replaced && rows.length === cache.input.length) {\n let out: Array<Row<TData>> | null = null\n let ok = true\n for (let i = 0; i < rows.length && ok; i++) {\n const prev = cache.input[i]! as BaseRowState<TData>\n const next = rows[i]! as BaseRowState<TData>\n if (next === prev) continue\n if (replaced.get(prev) !== next) { ok = false; break }\n const wasIn = prev[ROW_FILTER_GEN] === cache.gen\n const isIn = passes(next)\n if (wasIn !== isIn) { ok = false; break }\n if (!isIn) continue\n out ??= cache.output.slice()\n const pos = prev[ROW_FILTER_POS]\n out[pos] = next\n next[ROW_FILTER_GEN] = cache.gen\n next[ROW_FILTER_POS] = pos\n }\n if (ok) {\n const output = out ?? cache.output\n cache = { ...cache, input: rows, output }\n return output\n }\n }\n }\n\n const gen = ++generation\n const output: Array<Row<TData>> = []\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i]! as BaseRowState<TData>\n if (!passes(row)) continue\n row[ROW_FILTER_GEN] = gen\n row[ROW_FILTER_POS] = output.length\n output.push(row)\n }\n cache = { input: rows, output, filters, columns, gen }\n return output\n }\n}\n```\n\n### `function createPaginatedRowModel`\n\nNarrows the rows to the current page. Put it LAST: anything after it would\nonly ever see one page of data.\n\n```ts\nexport function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ table, rows }) => {\n const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }\n const start = pagination.pageIndex * pagination.pageSize\n return rows.slice(start, start + pagination.pageSize)\n }\n}\n```\n\n### `function createGroupedRowModel`\n\nBuckets rows by the active {@link GroupingState} and inserts a group row\nahead of each bucket, carrying that bucket's aggregates.\n\n```ts\nexport function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ table, rows }) => {\n const grouping: GroupingState = table.getState().grouping ?? []\n if (!grouping.length) return rows\n const columns = table.getAllColumns()\n\n // Recursively bucket rows by each grouping column in turn. At every level a\n // group row is built that stands in for its children - a non-group column\n // resolves to the value shared by every leaf row, or to undefined when the\n // leaves disagree.\n function buildGroups(\n input: Array<Row<TData>>,\n levelIndex: number,\n depth: number,\n idPrefix: string,\n /** Grouping columns already fixed by an ancestor bucket, and their raw\n * values - `undefined` where that bucket mixed several. */\n fixedValues: ReadonlyMap<string, unknown>,\n ): Array<Row<TData>> {\n if (levelIndex >= grouping.length) {\n // Leaves: actual data rows, with their nesting depth recorded.\n return input.map((row) => ({ ...row, depth }))\n }\n const groupKey = grouping[levelIndex]\n if (!groupKey) return input\n\n // Buckets carry the RAW grouping value alongside the rows, plus whether\n // the bucket saw more than one distinct raw value. Both are needed to let\n // deeper levels skip re-scanning this column: buckets are keyed by\n // `String(value ?? '')`, so `null`, `undefined` and `''` collapse into one\n // bucket, and a scan of such a bucket would report disagreement. Tracking\n // it here costs one comparison per row and keeps the shortcut honest.\n type Bucket = { rows: Array<Row<TData>>; raw: unknown; mixed: boolean }\n const buckets = new Map<string, Bucket>()\n for (const row of input) {\n const value = row.getCellValueByColumnId(groupKey)\n const key = String(value ?? '')\n const bucket = buckets.get(key)\n if (bucket) {\n bucket.rows.push(row)\n if (!bucket.mixed && bucket.raw !== value) bucket.mixed = true\n } else {\n buckets.set(key, { rows: [row], raw: value, mixed: false })\n }\n }\n\n const groupRows: Array<Row<TData>> = []\n let index = 0\n buckets.forEach((bucket, key) => {\n const children = bucket.rows\n const id = `${idPrefix}_${groupKey}_${key}`\n // Record this column as fixed for deeper levels ONLY when the bucket is\n // homogeneous. If all rows here share a raw value, so does every subset\n // of them, which is what makes the shortcut sound.\n //\n // A MIXED bucket must not be recorded at all - not even as \"undefined\".\n // `null`, `undefined` and `''` share a bucket key, so a mixed bucket can\n // still split into homogeneous children one level down, and those\n // children have a real shared value that a scan would find. Marking the\n // column resolved here would hand them the parent's disagreement.\n const nextFixed = bucket.mixed ? fixedValues : new Map(fixedValues).set(groupKey, bucket.raw)\n const subRows = buildGroups(children, levelIndex + 1, depth + 1, id, nextFixed)\n const isDeepest = levelIndex + 1 >= grouping.length\n const leafCount = isDeepest\n ? subRows.length\n : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)\n\n // Every grouping column ABOVE this level is already resolved: bucketing\n // by it is what made it constant, so scanning the children to rediscover\n // it is pure waste. Only the current level's key short-circuited before,\n // so a second-level group walked all of its children to re-derive the\n // first level's value - about 100,000 reads on the 100k x 9 two-level\n // case, for an answer already in hand.\n //\n // The current level still returns the stringified bucket key rather than\n // the raw value, because that is what it has always returned and the\n // group row's display depends on it.\n const resolveColumnValue = (columnId: string): unknown => {\n if (columnId === groupKey) return key\n if (fixedValues.has(columnId)) return fixedValues.get(columnId)\n let resolved: unknown\n let hasResolved = false\n for (const child of children) {\n const childValue = child.getCellValueByColumnId(columnId)\n if (!hasResolved) {\n resolved = childValue\n hasResolved = true\n } else if (childValue !== resolved) {\n return undefined\n }\n }\n return resolved\n }\n\n const groupOriginal: Record<string, unknown> = {}\n columns.forEach((column) => {\n const field = column.columnDef.field\n if (!field) return\n const agg = column.columnDef.aggregate\n groupOriginal[field] = agg\n ? applyGroupAggregate(agg, column.id, children)\n : resolveColumnValue(column.id)\n })\n\n const groupRow: Row<TData> = {\n id,\n index: index++,\n original: groupOriginal as TData,\n depth,\n subRows,\n leafCount,\n getCanExpand: () => true,\n getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),\n toggleExpanded: () => {\n table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),\n toggleSelected: () => {\n table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getAllCells: () => [],\n // Prefer the precomputed group value (which carries aggregates)\n // and fall back to the shared-value resolver for columns without\n // a field.\n getCellValueByColumnId: (columnId: string) => {\n const col = columns.find((c) => c.id === columnId)\n const field = col?.columnDef.field\n if (field && field in groupOriginal) return groupOriginal[field]\n return resolveColumnValue(columnId)\n },\n }\n groupRows.push(groupRow)\n })\n return groupRows\n }\n\n return buildGroups(rows, 0, 0, 'group', new Map())\n }\n}\n```\n\n### `type TreeRowModelOptions`\n\nHow to read a hierarchy out of FLAT rows: each row names its parent, and the\ngrid reconstructs the tree. Rows whose parent id matches nothing become roots\nrather than disappearing.\n\nFor nested source data (`children: [...]`), flatten it first with\n{@link flattenTreeData}.\n\n```ts\nexport type TreeRowModelOptions = {\n /** Field holding each row's parent id. Rows with no parent are roots. */\n parentField: string\n /** Field holding the row's own id. Defaults to `'id'`. */\n idField?: string\n}\n```\n\n### `function createTreeRowModel`\n\nClient-side tree data: nest the grid's own flat rows into a parent/child\nhierarchy that `createExpandedRowModel` then walks.\n\nThis works on the rows the grid already built rather than on raw data, so\ntree rows keep their cells, editing, selection and formatting - they are real\ndata rows that happen to have children, not synthetic banners like grouping's.\nThat is also why the model is parent-id based: nested source arrays never\nbecome rows (the grid only builds rows for `data`), so nested input is\nflattened first with {@link flattenTreeData}. One code path, no duplicated\nrow construction.\n\nRows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable\ndata row for a full-width group banner.\n\n```ts\nexport function createTreeRowModel<TData extends RowData>(\n options: TreeRowModelOptions,\n): RowModelFactory<TData> {\n const { parentField, idField = 'id' } = options\n return ({ table, rows }) => {\n if (!rows.length) return rows\n const keyOf = (row: Row<TData>) => (row.original as any)?.[idField]\n const parentOf = (row: Row<TData>) => (row.original as any)?.[parentField]\n\n const present = new Set<unknown>()\n for (const row of rows) present.add(keyOf(row))\n\n const childrenByParent = new Map<unknown, Array<Row<TData>>>()\n const roots: Array<Row<TData>> = []\n for (const row of rows) {\n const parent = parentOf(row)\n // A row whose parent is absent (filtered out, or never existed) becomes a\n // root rather than disappearing - silently dropping rows is worse than a\n // shallower tree. Self-parenting is treated the same way.\n if (parent == null || parent === keyOf(row) || !present.has(parent)) {\n roots.push(row)\n continue\n }\n const list = childrenByParent.get(parent) ?? []\n list.push(row)\n childrenByParent.set(parent, list)\n }\n\n // Guards a cycle in the parent chain from recursing forever.\n const seen = new Set<unknown>()\n const build = (row: Row<TData>, depth: number): Row<TData> => {\n const key = keyOf(row)\n const id = row.id\n if (seen.has(key)) {\n return { ...row, depth, subRows: [], getCanExpand: () => false } as Row<TData>\n }\n seen.add(key)\n const subRows = (childrenByParent.get(key) ?? []).map((child) => build(child, depth + 1))\n return {\n ...row,\n depth,\n subRows,\n leafCount: subRows.reduce((n, sub) => n + 1 + (sub.leafCount ?? 0), 0),\n __treeRow: true,\n getCanExpand: () => subRows.length > 0,\n getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),\n toggleExpanded: () => {\n table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n } as Row<TData>\n }\n\n return roots.map((root) => build(root, 0))\n }\n}\n```\n\n### `type FlattenTreeOptions`\n\nHow to flatten NESTED source data into the parent-id shape tree rows need.\n`parentField` is written onto each row, so point `treeData.parentField` at\nthe same name afterwards.\n\n```ts\nexport type FlattenTreeOptions = {\n /** Field holding an array of child objects. */\n childrenField: string\n /** Field holding each object's id. Defaults to `'id'`. */\n idField?: string\n /** Field to WRITE the resolved parent id onto. Defaults to `'__parentId'`. */\n parentField?: string\n}\n```\n\n### `function flattenTreeData`\n\nFlatten nested tree data into the flat parent-id shape `createTreeRowModel`\nconsumes, stamping each child with its parent's id.\n\nChildren are emitted directly after their parent so the natural order already\nmatches the rendered tree. The `childrenField` array is left on the objects\n(harmless, and callers often still want it); only the parent link is added.\n\n```ts\nexport function flattenTreeData<T extends RowData>(\n data: ReadonlyArray<T>,\n options: FlattenTreeOptions,\n): T[] {\n const { childrenField, idField = 'id', parentField = '__parentId' } = options\n const out: T[] = []\n const walk = (nodes: ReadonlyArray<T>, parentId: unknown) => {\n for (const node of nodes) {\n const flat = { ...node, [parentField]: parentId } as T\n out.push(flat)\n const kids = (node as any)[childrenField]\n if (Array.isArray(kids) && kids.length) walk(kids as ReadonlyArray<T>, (node as any)[idField])\n }\n }\n walk(data, null)\n return out\n}\n```\n\n### `function createExpandedRowModel`\n\nHides the descendants of collapsed rows. Needed for grouping, tree data and\nmaster-detail alike - all three are the same expand/collapse mechanism.\n\n```ts\nexport function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ table, rows }) => {\n const expanded: ExpandedState = table.getState().expanded ?? {}\n // Nothing expanded means every collapsed row hides its children and the\n // output is the input, element for element. Returning it as is keeps the\n // array identity the stages downstream and the render window key on, and\n // skips a 100k-element copy per data change on a flat grid.\n let anyExpanded = false\n for (const id in expanded) {\n if (expanded[id]) { anyExpanded = true; break }\n }\n if (!anyExpanded) return rows\n const flattened: Array<Row<TData>> = []\n const visit = (row: Row<TData>) => {\n flattened.push(row)\n if (row.subRows?.length && expanded[row.id]) {\n for (const sub of row.subRows) visit(sub)\n }\n }\n for (const row of rows) visit(row)\n return flattened\n }\n}\n```\n\n### `function replacedRowsOf`\n\nThe rows `getRowModel` replaced in the rebuild this pipeline run follows, or null.\n\n```ts\nexport function replacedRowsOf(table: SvGrid<any>): ReadonlyMap<Row<any>, Row<any>> | null {\n return replacedRowsByGrid.get(table) ?? null\n}\n```\n\n### `function createSortedRowModel`\n\nThe sorting stage of the row pipeline: orders the rows by the grid's\n`sorting` state with the built-in comparators (or the `localSortFns` map\nhanded in), stable, equal keys keeping their data order. A tick that\nreplaces a few rows out of many is repaired incrementally rather than\nre-sorted (see the note inside). Registered by `rowSortingFeature`; an app\nonly calls this to install its own comparator set.\n\n```ts\nexport function createSortedRowModel<TData extends RowData>(\n localSortFns: typeof sortFns = sortFns,\n): RowModelFactory<TData> {\n /**\n * What the last full sort produced, kept so a tick can repair it.\n *\n * A live feed replaces a few hundred row objects out of a hundred thousand\n * and hands the grid a new array. Re-sorting all of it costs the same as\n * the first sort did (115-149 ms in the browser for 100k rows, see\n * docs/help/benchmarks.md); the order it would produce differs from the\n * previous one only where the replaced rows land. So the previous output\n * is kept, with its sort keys aligned to it, and when `getRowModel` says\n * which rows were replaced (see `replacedRowsOf`) the stage drops those,\n * sorts the replacements among themselves and merges them back in one\n * linear pass: O(n + k log k) instead of O(n log n) key builds and\n * comparisons.\n *\n * The repair is only attempted when everything else is the same object as\n * last time - the sorting state, the columns, and every input row except\n * the replaced ones - and it produces exactly what the full sort would:\n * equal keys fall back to the data index, which is the order a stable sort\n * over data-ordered input keeps.\n */\n let cache: {\n input: Array<Row<TData>>\n output: Array<Row<TData>>\n sorting: SortingState\n columns: Array<Column<TData>>\n clauses: Array<SortClause>\n } | null = null\n\n type SortClause = {\n desc: boolean\n compare: (a: any, b: any) => number\n /** The key for one row, or `MISSING_KEY` when it cannot be produced incrementally. */\n keyOf: (row: Row<TData>) => any\n }\n const MISSING_KEY = Symbol('svgrid.sort.missing-key')\n\n function fullSort(rows: Array<Row<TData>>, sorting: SortingState, allColumns: Array<Column<TData>>) {\n // Resolve every clause ONCE, before sorting.\n //\n // This used to live inside the comparator, so `getAllColumns().find(...)`\n // ran per comparison per clause: a single-clause sort of 100k rows made\n // 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;\n // `pnpm bench --case=sort-1col`). The comparator is called O(n log n)\n // times, so anything inside it that is not O(1) sets the cost of the sort.\n const clauses: Array<{\n keys: Array<any>\n desc: boolean\n compare: (a: any, b: any) => number\n keyOf: (row: Row<TData>) => any\n /** Set when every key is an integer rank in [0, rankCount): the text rank path. */\n rankCount?: number\n }> = []\n\n for (const clause of sorting) {\n const column = allColumns.find((col) => col.id === clause.id)\n if (!column) continue\n const editorType = column.columnDef.editorType\n const comparator =\n editorType === 'number'\n ? localSortFns.number\n : editorType === 'date' || editorType === 'datetime'\n ? localSortFns.date\n : localSortFns.auto\n\n // Precompute one sort key per row, so the comparator reads an array slot\n // instead of walking the row's column index on every comparison. For the\n // three built-in comparators the key is also cheaper to compare than the\n // raw value: a timestamp rather than two `new Date()` allocations, a\n // number rather than two `Number()` coercions, a collator rather than a\n // fresh one per `localeCompare` call.\n //\n // The identity checks against `sortFns` matter: `localSortFns` is a\n // public parameter, so a caller can substitute their own comparators.\n // When they have, we fall through to calling their function with the raw\n // values - still hoisted, just not specialised.\n const columnId = column.id\n const n = rows.length\n let keys: Array<any> = new Array(n)\n let compare: (a: any, b: any) => number\n let keyOf: (row: Row<TData>) => any\n let autoMode: 'number' | 'mixed' | 'text' | undefined\n let rankCount: number | undefined\n\n if (comparator === sortFns.number) {\n keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId))\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (comparator === sortFns.date) {\n keyOf = (row) => dateSortKey(row.getCellValueByColumnId(columnId))\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (comparator === sortFns.auto && (autoMode = autoSortMode(rows, columnId)) === 'number') {\n // A column with no declared type whose values are all numbers. Sorting\n // it as text put 10 before 2 (#104).\n keyOf = (row) => {\n const value = row.getCellValueByColumnId(columnId)\n // A text value arriving in a tick changes the mode; only a full sort\n // can place it.\n return typeof value === 'number' || isBlankSortValue(value)\n ? numberSortKey(value)\n : MISSING_KEY\n }\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (autoMode === 'mixed') {\n // Numbers and text in one column. Rare, so no key tricks: the raw\n // values through the pairwise comparator, which orders by kind first.\n keyOf = (row) => row.getCellValueByColumnId(columnId)\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareAutoValues\n } else if (comparator === sortFns.auto) {\n const strings: string[] = new Array(n)\n // Decide whether ranking is worth attempting BEFORE paying for it.\n //\n // Building the distinct set and then discarding it costs about 9 ms on\n // a 100k-row column where nearly every value is unique, and ranking\n // saves about 19 ms where they repeat - so guessing wrong in either\n // direction is measurable. A small stride sample answers it for well\n // under a millisecond.\n //\n // Strided rather than the first N rows: data arrives sorted or\n // clustered often enough that a prefix is a bad estimator of the whole\n // column. Reading every k-th row is no more expensive and does not care\n // how the rows are arranged.\n const rankLimit = n >> 1\n // Each distinct value's id (its insertion order), when ranking.\n let idOf: Map<string, number> | null = null\n if (n > 0) {\n // Ranking pays when the column has at most n/2 distinct values. A\n // small sample cannot see that by asking whether half of it repeats:\n // 1M rows of 80k values, each one 12 times over, gave 256 samples\n // with almost no repeats, so ranking was skipped and the sort made\n // 20M collator calls (1.75 s; ranked, it is a fraction of that).\n // Count the repeats in a larger sample instead. S samples from u\n // equally common values repeat about S^2 / 2u times, so u <= n/2\n // shows as at least S^2 / n repeats. Up to SAMPLE rows the whole\n // column is the sample and the test is exact.\n const SAMPLE = 2048\n const sampleTarget = Math.min(n, SAMPLE)\n const stride = Math.max(1, Math.floor(n / sampleTarget))\n const sample = new Set<string>()\n let sampled = 0\n for (let i = 0; i < n; i += stride) {\n sample.add(autoTextKey(rows[i]!.getCellValueByColumnId(columnId)))\n sampled++\n }\n const repeats = sampled - sample.size\n const worthRanking = stride === 1 ? sample.size <= rankLimit : repeats * n >= sampled * sampled\n if (worthRanking) idOf = new Map()\n }\n\n // One hash per row: the value's id is looked up (or assigned) as the\n // row is read, and the rank lookup further down is an array read by\n // id. It used to add every value to a Set and then look every value up\n // again in a rank Map, two hashes per row.\n const ids: Int32Array | null = idOf ? new Int32Array(n) : null\n for (let i = 0; i < n; i++) {\n const s = autoTextKey(rows[i]!.getCellValueByColumnId(columnId))\n strings[i] = s\n if (idOf) {\n let id = idOf.get(s)\n if (id === undefined) {\n id = idOf.size\n idOf.set(s, id)\n if (idOf.size > rankLimit) {\n idOf = null\n continue\n }\n }\n ids![i] = id\n }\n }\n\n // Collation is by far the most expensive comparison we do - a CPU\n // profile of a 100k text sort put 65% of the whole operation inside the\n // collator. But a column's DISTINCT values are usually far fewer than\n // its rows (statuses, regions, categories, owners), so rank the\n // distinct values once and sort by rank afterwards. That turns\n // O(n log n) collator calls into O(u log u), where u is the number of\n // distinct values, and the resulting order is identical because rank is\n // a monotone relabelling of the collated order - equal strings share a\n // rank, so ties still fall through to the stable sort exactly as before.\n //\n // Guarded on the uniqueness ratio: when nearly every value is distinct\n // the ranking pass cannot save any collator calls and would just add an\n // O(n) Map build, so that case keeps comparing directly.\n if (idOf) {\n // Map keys come back in insertion order, which is id order.\n const values = Array.from(idOf.keys())\n const byRank = values.map((_, id) => id).sort((a, b) => compareCollatedKeys(values[a]!, values[b]!))\n const rankById = new Int32Array(values.length)\n for (let r = 0; r < byRank.length; r++) rankById[byRank[r]!] = r\n for (let i = 0; i < n; i++) keys[i] = rankById[ids![i]!]!\n const rankIds = idOf\n rankCount = values.length\n compare = compareNumericKeys\n // A replacement whose text is not among the ranked values has no\n // rank; the repair path treats that as \"sort everything again\".\n keyOf = (row) => {\n const value = row.getCellValueByColumnId(columnId)\n // A number arriving in a tick makes the column mixed; resort fully.\n if (typeof value === 'number') return MISSING_KEY\n const id = rankIds.get(autoTextKey(value))\n return id === undefined ? MISSING_KEY : rankById[id]!\n }\n } else {\n keys = strings\n compare = compareCollatedKeys\n keyOf = (row) => {\n const value = row.getCellValueByColumnId(columnId)\n return typeof value === 'number' ? MISSING_KEY : autoTextKey(value)\n }\n }\n } else {\n keyOf = (row) => row.getCellValueByColumnId(columnId)\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = comparator\n }\n\n clauses.push({ keys, desc: clause.desc, compare, keyOf, rankCount })\n }\n\n if (!clauses.length) return null\n\n // Sort an index array, then materialise. `Array.prototype.sort` is stable,\n // so equal keys keep their original relative order exactly as the previous\n // `[...rows].sort(...)` did.\n // An index array the comparator sorts in place, or a typed array from\n // countingOrder / radixOrder for a large one-column numeric sort.\n let order: ArrayLike<number> = new Array<number>(rows.length)\n for (let i = 0; i < order.length; i++) (order as number[])[i] = i\n\n // Single-clause sorts get a specialised comparator.\n //\n // Most sorts are one column, and that path runs O(n log n) times - 1.66\n // million comparisons for 100k rows. The general loop pays a clause-array\n // index, three property loads and an indirect call on every one of them,\n // none of which vary once the clause list is fixed. Hoisting them into a\n // closure and, for the numeric comparator, inlining the subtraction removes\n // the call entirely.\n //\n // `keys[ib] - keys[ia]` for descending is exactly `-(keys[ia] - keys[ib])`\n // as far as sorting is concerned: both are NaN for unorderable values,\n // which the spec coerces to 0, and they differ only in producing 0 versus\n // -0 for equal keys, which sorts identically.\n if (clauses.length === 1) {\n const { keys, compare, desc, rankCount } = clauses[0]!\n if (rankCount !== undefined && rows.length >= STABLE_NUMERIC_SORT_MIN) {\n order = countingOrder(keys as number[], rankCount, desc)\n } else if (compare === compareNumericKeys && rows.length >= STABLE_NUMERIC_SORT_MIN) {\n order = radixOrder(keys as number[], desc)\n } else if (compare === compareNumericKeys) {\n ;(order as number[]).sort(\n desc\n ? function compareOneNumericDesc(ia, ib) { return keys[ib] - keys[ia] }\n : function compareOneNumericAsc(ia, ib) { return keys[ia] - keys[ib] },\n )\n } else {\n ;(order as number[]).sort(\n desc\n ? function compareOneDesc(ia, ib) { return -compare(keys[ia], keys[ib]) }\n : function compareOneAsc(ia, ib) { return compare(keys[ia], keys[ib]) },\n )\n }\n } else {\n ;(order as number[]).sort(function compareRowsByClauses(ia, ib) {\n for (let k = 0; k < clauses.length; k++) {\n const clause = clauses[k]!\n const result = clause.compare(clause.keys[ia], clause.keys[ib])\n if (result !== 0) return clause.desc ? -result : result\n }\n return 0\n })\n }\n\n const sorted = new Array<Row<TData>>(rows.length)\n for (let i = 0; i < order.length; i++) sorted[i] = rows[order[i]!]!\n // Each row keeps its key(s), so a later tick can place a replacement\n // among the kept rows without reading any of them again. One property\n // write per row; an array per row only when there are several clauses.\n if (clauses.length === 1) {\n const keys = clauses[0]!.keys\n for (let i = 0; i < rows.length; i++) (rows[i] as any)[ROW_SORT_KEY] = keys[i]\n } else {\n for (let i = 0; i < rows.length; i++) {\n const per = new Array(clauses.length)\n for (let k = 0; k < clauses.length; k++) per[k] = clauses[k]!.keys[i]\n ;(rows[i] as any)[ROW_SORT_KEY] = per\n }\n }\n return { sorted, clauses: clauses.map(({ desc, compare, keyOf }) => ({ desc, compare, keyOf })) }\n }\n\n /**\n * The previous output with the replaced rows swapped for their\n * replacements, in sorted position. Null when the input is not the cached\n * input plus replacements, or a replacement has no incremental key.\n *\n * Every row carries the key(s) the last full sort computed for it\n * (ROW_SORT_KEY: the key itself for one clause, an array for several), so\n * the merge moves row references and nothing else. At 100k rows the loop\n * that also copied an aligned key array cost 3.7 ms; this one is the\n * reference copy alone.\n */\n function repair(rows: Array<Row<TData>>, replaced: ReadonlyMap<Row<any>, Row<any>>) {\n const c = cache!\n if (rows.length !== c.input.length) return null\n // Which rows of THIS stage's input changed. The map may name rows a\n // stage upstream filtered out; only the ones that reach here matter.\n const dropGen = ++sortDropGeneration\n const added: Array<Row<TData>> = []\n for (let i = 0; i < rows.length; i++) {\n const prev = c.input[i]!\n const next = rows[i]!\n if (next === prev) continue\n if (replaced.get(prev) !== next) return null\n ;(prev as any)[ROW_SORT_DROPPED] = dropGen\n added.push(next)\n }\n if (!added.length) return c.output\n\n const clauses = c.clauses\n const single = clauses.length === 1\n for (let j = 0; j < added.length; j++) {\n const row = added[j]! as any\n if (single) {\n const key = clauses[0]!.keyOf(row)\n if (key === MISSING_KEY) return null\n row[ROW_SORT_KEY] = key\n } else {\n const keys = new Array(clauses.length)\n for (let k = 0; k < clauses.length; k++) {\n const key = clauses[k]!.keyOf(row)\n if (key === MISSING_KEY) return null\n keys[k] = key\n }\n row[ROW_SORT_KEY] = keys\n }\n }\n\n // Keys first, then the data index: the tie-break a stable sort over\n // data-ordered input applies, so the merge lands where a full sort would.\n const prevOut = c.output\n const compareRows = single\n ? (() => {\n const { compare, desc } = clauses[0]!\n return (a: Row<TData>, b: Row<TData>) => {\n const r = compare((a as any)[ROW_SORT_KEY], (b as any)[ROW_SORT_KEY])\n if (r !== 0) return desc ? -r : r\n return a.index - b.index\n }\n })()\n : (a: Row<TData>, b: Row<TData>) => {\n const ka = (a as any)[ROW_SORT_KEY]\n const kb = (b as any)[ROW_SORT_KEY]\n for (let k = 0; k < clauses.length; k++) {\n const clause = clauses[k]!\n const r = clause.compare(ka[k], kb[k])\n if (r !== 0) return clause.desc ? -r : r\n }\n return a.index - b.index\n }\n\n // Sort the replacements among themselves (k log k)...\n added.sort(compareRows)\n\n // ...find where each one goes in the previous output by binary search\n // (k log n comparisons, against the dropped rows too - they are skipped\n // below, which does not move an insertion point)...\n const n = prevOut.length\n const insertAt = new Array<number>(added.length)\n let lo = 0\n for (let j = 0; j < added.length; j++) {\n // Monotone: the replacements are sorted, so each search starts where\n // the previous one landed.\n const row = added[j]!\n let hi = n\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (compareRows(row, prevOut[mid]!) < 0) hi = mid\n else lo = mid + 1\n }\n insertAt[j] = lo\n }\n\n // ...then one linear pass that only moves references.\n const out = new Array<Row<TData>>(rows.length)\n let w = 0\n let j = 0\n let nextInsert = insertAt[0] ?? -1\n for (let i = 0; i < n; i++) {\n while (nextInsert === i) {\n out[w++] = added[j++]!\n nextInsert = j < added.length ? insertAt[j]! : -1\n }\n const prev = prevOut[i]!\n if ((prev as any)[ROW_SORT_DROPPED] === dropGen) continue\n out[w++] = prev\n }\n while (j < added.length) out[w++] = added[j++]!\n return out\n }\n\n return function sortedRowModelStage({ table, rows }) {\n const sorting = table.getState().sorting ?? []\n if (!sorting.length) {\n cache = null\n return rows\n }\n const columns = table.getAllColumns()\n\n if (cache && cache.sorting === sorting && cache.columns === columns) {\n if (cache.input === rows) return cache.output\n const replaced = replacedRowsOf(table)\n if (replaced) {\n const repaired = repair(rows, replaced)\n if (repaired) {\n cache = { ...cache, input: rows, output: repaired }\n return repaired\n }\n }\n }\n\n const full = fullSort(rows, sorting, columns)\n if (!full) {\n cache = null\n return rows\n }\n cache = { input: rows, output: full.sorted, sorting, columns, clauses: full.clauses }\n return full.sorted\n }\n}\n```\n\n### `type SvGridOptions`\n\nEverything {@link createSvGridCore} accepts: the data and columns, the\nfeatures and row models that make up the pipeline, and an `on*Change`\ncallback per piece of state for controlled use.\n\n`<SvGrid>` builds this for you from its props - you only construct it\ndirectly when driving the headless core.\n\n```ts\nexport type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {\n _features: TFeatures\n _rowModels?: {\n coreRowModel?: RowModelFactory<TData>\n filteredRowModel?: RowModelFactory<TData>\n sortedRowModel?: RowModelFactory<TData>\n paginatedRowModel?: RowModelFactory<TData>\n groupedRowModel?: RowModelFactory<TData>\n expandedRowModel?: RowModelFactory<TData>\n }\n columns: Array<ColumnDef<TFeatures, TData>>\n data: ReadonlyArray<TData>\n /**\n * Optional row-id resolver. When set, the value it returns becomes\n * `row.id` (and therefore the selection / expansion / edit key). When\n * omitted, ids fall back to the row's array index as a string. Use a\n * stable id (database PK, UUID, etc.) so selection survives reorders.\n */\n getRowId?: (row: TData, index: number) => string\n state?: Partial<Record<string, any>>\n onSortingChange?: (updater: Updater<SortingState>) => void\n onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void\n onPaginationChange?: (updater: Updater<PaginationState>) => void\n onGroupingChange?: (updater: Updater<GroupingState>) => void\n onExpandedChange?: (updater: Updater<ExpandedState>) => void\n onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void\n onActiveCellChange?: (updater: Updater<ActiveCellState>) => void\n}\n```\n\n### `type SvGrid`\n\nThe headless grid instance: the state stores plus the read methods a renderer\nneeds (`getHeaderGroups()`, `getRowModel()`, the `set*` writers).\n\nFramework free by design - `<SvGrid>` is one renderer over this, and you can\nwrite another. See the \"Why headless?\" guide.\n\n```ts\nexport type SvGrid<TData extends RowData> = {\n store: Store<Record<string, any>>\n optionsStore: Store<Record<string, any>>\n state: Record<string, any>\n getState: () => Record<string, any>\n setOptions: (updater: Updater<Record<string, any>>) => void\n setColumnFilters: (updater: Updater<ColumnFiltersState>) => void\n setPagination: (updater: Updater<PaginationState>) => void\n setGrouping: (updater: Updater<GroupingState>) => void\n setExpanded: (updater: Updater<ExpandedState>) => void\n setRowSelection: (updater: Updater<RowSelectionState>) => void\n setActiveCell: (updater: Updater<ActiveCellState>) => void\n moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void\n getAllColumns: () => Array<Column<TData>>\n getHeaderGroups: () => Array<HeaderGroup<TData>>\n getFooterGroups: () => Array<HeaderGroup<TData>>\n getRowModel: () => RowModel<TData>\n}\n```\n\n### `function createSvGridCore`\n\nBuild a headless grid: state, the row pipeline, and the read methods, with no\nDOM and no Svelte. This is the engine `<SvGrid>` renders.\n\nMost callers want `createSvGrid` (the runes-aware wrapper) or the component\nitself; reach for this when you are writing your own renderer or running the\npipeline outside a browser.\n\n```ts\nexport function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(\n options: SvGridOptions<TFeatures, TData>,\n): SvGrid<TData> {\n const internalState: Record<string, any> = {\n sorting: [],\n columnFilters: [],\n pagination: { pageIndex: 0, pageSize: options.data.length || 10 },\n grouping: [],\n expanded: {},\n rowSelection: {},\n activeCell: { rowIndex: 0, colIndex: 0, cellId: null },\n ...(options.state ?? {}),\n }\n const store = createStore(internalState)\n const optionsStore = createStore(options as Record<string, any>)\n let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null\n let cachedColumns: Array<Column<TData>> = []\n let cachedHeaderGroups: Array<HeaderGroup<TData>> = []\n let cachedBaseRowsInput: ReadonlyArray<TData> | null = null\n let cachedBaseRowsColumns: Array<Column<TData>> | null = null\n let cachedBaseRows: Array<Row<TData>> = []\n /** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */\n let replacedRows: Map<Row<TData>, Row<TData>> | null = null\n let cachedRowCtx: BaseRowCtx<TData> | null = null\n /** This grid's windowed-data row model, made on first use from the installed hook. */\n let windowedRowModel: WindowedRowModel<TData> | null = null\n\n /** The shared row context for `columns`, reused while they are the same. */\n function rowCtxFor(columns: Array<Column<TData>>): BaseRowCtx<TData> {\n if (cachedRowCtx && cachedRowCtx.columns === columns) return cachedRowCtx\n const columnIndexById = new Map<string, number>()\n for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)\n cachedRowCtx = {\n grid: grid as SvGrid<TData>,\n store,\n columns,\n columnCount: columns.length,\n columnIndexById,\n }\n return cachedRowCtx\n }\n\n let cachedRowModel: RowModel<TData> | null = null\n let cachedRowModelBaseRows: Array<Row<TData>> | null = null\n let cachedPipeline = options._rowModels\n let cachedSlices: {\n sorting: SortingState | undefined\n columnFilters: ColumnFiltersState | undefined\n pagination: PaginationState | undefined\n grouping: GroupingState | undefined\n expanded: ExpandedState | undefined\n } | null = null\n\n const grid = {\n store,\n optionsStore,\n get state() {\n return store.state\n },\n getState() {\n return store.state\n },\n setOptions(updater: Updater<Record<string, any>>) {\n optionsStore.setState((prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : updater,\n )\n },\n setColumnFilters(updater: Updater<ColumnFiltersState>) {\n store.setState((prev) => ({\n ...prev,\n columnFilters:\n typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,\n }))\n options.onColumnFiltersChange?.(updater)\n },\n setPagination(updater: Updater<PaginationState>) {\n store.setState((prev) => ({\n ...prev,\n pagination:\n typeof updater === 'function'\n ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })\n : updater,\n }))\n options.onPaginationChange?.(updater)\n },\n setGrouping(updater: Updater<GroupingState>) {\n store.setState((prev) => ({\n ...prev,\n grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,\n }))\n options.onGroupingChange?.(updater)\n },\n setExpanded(updater: Updater<ExpandedState>) {\n store.setState((prev) => ({\n ...prev,\n expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,\n }))\n options.onExpandedChange?.(updater)\n },\n setRowSelection(updater: Updater<RowSelectionState>) {\n store.setState((prev) => ({\n ...prev,\n rowSelection:\n typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,\n }))\n options.onRowSelectionChange?.(updater)\n },\n setActiveCell(updater: Updater<ActiveCellState>) {\n store.setState((prev) => {\n const previous: ActiveCellState = prev.activeCell ?? {\n rowIndex: 0,\n colIndex: 0,\n cellId: null,\n }\n const nextActive =\n typeof updater === 'function' ? updater(previous) : updater\n return {\n ...prev,\n activeCell: nextActive,\n }\n })\n options.onActiveCellChange?.(updater)\n },\n moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {\n const rows = grid.getRowModel().rows\n const columns = grid.getAllColumns()\n const maxRow = Math.max(rows.length - 1, 0)\n const maxCol = Math.max(columns.length - 1, 0)\n const current: ActiveCellState = grid.getState().activeCell ?? {\n rowIndex: 0,\n colIndex: 0,\n cellId: null,\n }\n\n const rowIndex = Math.min(\n Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),\n maxRow,\n )\n const colIndex = Math.min(\n Math.max(current.colIndex + (next.colDelta ?? 0), 0),\n maxCol,\n )\n const columnId = columns[colIndex]?.id ?? 'col_0'\n grid.setActiveCell({\n rowIndex,\n colIndex,\n cellId: `${rowIndex}_${columnId}`,\n })\n },\n getAllColumns() {\n // Cache hit: referentially identical columns array.\n if (cachedColumnsInput === options.columns && cachedColumns.length) {\n return cachedColumns\n }\n // Soft cache hit: consumers commonly recreate the columns array\n // inline on every render (e.g. `columns={[...]}`). If the new\n // array has the same length AND each entry has the same `field` /\n // `id` / `header` (the visibility-affecting structure of a\n // column), trust the previous build. Mutable inner fields like\n // `cell` and `editorOptions` are still picked up on the next real\n // render that bumps an actual data dep - they're read at cell-\n // render time, not at this top-level cache.\n if (\n cachedColumnsInput &&\n options.columns.length === cachedColumnsInput.length &&\n cachedColumns.length === options.columns.length &&\n options.columns.every((c, i) => {\n const prev = cachedColumnsInput![i]!\n return (\n c.field === prev.field &&\n c.id === prev.id &&\n c.header === prev.header &&\n c.editorType === prev.editorType\n )\n })\n ) {\n // Update the stored input reference so the strict check hits\n // next time, but reuse the built column model.\n cachedColumnsInput = options.columns\n return cachedColumns\n }\n\n cachedColumnsInput = options.columns\n cachedHeaderGroups = []\n const build = (\n defs: Array<ColumnDef<TFeatures, TData>>,\n depth: number,\n parentId?: string,\n ): Array<Column<TData>> => {\n const leaves: Array<Column<TData>> = []\n defs.forEach((columnDef, index) => {\n const id = resolveColumnId(columnDef, parentId, depth, index)\n if (columnDef.columns?.length) {\n leaves.push(...build(columnDef.columns, depth + 1, id))\n return\n }\n leaves.push({\n id,\n depth,\n parentId,\n columnDef,\n getCanSort: () =>\n Boolean((options._features as any).rowSortingFeature) &&\n columnDef.sortable !== false,\n getCanFilter: () =>\n Boolean((options._features as any).columnFilteringFeature) &&\n columnDef.filterable !== false,\n getIsSorted: () => {\n const entry = store.state.sorting?.find((s: any) => s.id === id)\n if (!entry) return false\n return entry.desc ? 'desc' : 'asc'\n },\n getToggleSortingHandler: () => () => {\n const clauses: SortingState = store.state.sorting ?? []\n const current = clauses.find((s: any) => s.id === id)\n const nextClause: SortingState = !current\n ? [...clauses, { id, desc: false }]\n : current.desc\n ? clauses.filter((s) => s.id !== id)\n : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))\n store.setState((prev) => ({ ...prev, sorting: nextClause }))\n options.onSortingChange?.(nextClause)\n },\n })\n })\n return leaves\n }\n cachedColumns = build(options.columns, 0)\n return cachedColumns\n },\n getHeaderGroups() {\n if (cachedHeaderGroups.length) return cachedHeaderGroups\n const headers = grid.getAllColumns().map((column) => {\n const header: Header<TData> = {\n id: column.id,\n isPlaceholder: false,\n colSpan: 1,\n column,\n getContext: () => ({ header, column, table: grid }),\n }\n return header\n })\n cachedHeaderGroups = [{ id: 'header_group_0', headers }]\n return cachedHeaderGroups\n },\n getFooterGroups() {\n return grid.getHeaderGroups()\n },\n getRowModel() {\n const columns = grid.getAllColumns()\n // Read once. `options.data` is a getter when <SvGrid> drives the engine\n // (a $state.raw signal), and the rebuild below touched it twice per row:\n // 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.\n const data = options.data\n if (installWindowedRowModel) {\n windowedRowModel ??= installWindowedRowModel<TData>({ rowCtxFor, getRowId: () => options.getRowId })\n const model = windowedRowModel(data, columns)\n if (model) return model\n }\n if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {\n // Same columns as last time: the shared context still describes them,\n // and a row whose data object sits at the same index can keep its\n // row object. A row model that streams blocks hands the grid a new\n // array of 60k entries per block where 100 changed; building 60k\n // fresh row objects each time was most of the cost of a block\n // landing. The reused row drops its memoised values and cells, since\n // an app may have changed the object in place before passing a new\n // array - that is the case the old rebuild covered by accident.\n // A lazy row list (lazyRows below) is read through its backing array,\n // so reusing rows never makes the ones nobody looked at.\n const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? (lazyRowsBacking(cachedBaseRows) ?? cachedBaseRows) : null\n const previousInput = cachedBaseRowsInput\n // Same length as last time: a replacement, not an add or remove, so\n // each new row stands in for the old row at its index and the stages\n // can repair rather than recompute. Anything else is structural.\n replacedRows = previous && previous.length === data.length ? new Map() : null\n cachedBaseRowsInput = data\n cachedBaseRowsColumns = columns\n // O(1) column-id → index lookup so getCellValueByColumnId doesn't do\n // a linear `findIndex` on every cell read (was O(rows × cells × cols)).\n let rowCtx: BaseRowCtx<TData>\n if (previous && cachedRowCtx) {\n rowCtx = cachedRowCtx\n } else {\n const columnIndexById = new Map<string, number>()\n for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)\n // One shared context for every row in this table, so a row carries a\n // pointer rather than a closure scope. See BASE_ROW_METHODS.\n rowCtx = {\n grid: grid as SvGrid<TData>,\n store,\n columns,\n columnCount: columns.length,\n columnIndexById,\n }\n cachedRowCtx = rowCtx\n }\n\n const getRowId = options.getRowId\n const m = BASE_ROW_METHODS as unknown as {\n getCanExpand: Row<TData>['getCanExpand']\n getIsExpanded: Row<TData>['getIsExpanded']\n toggleExpanded: Row<TData>['toggleExpanded']\n getIsSelected: Row<TData>['getIsSelected']\n toggleSelected: Row<TData>['toggleSelected']\n getAllCells: Row<TData>['getAllCells']\n getCellValueByColumnId: Row<TData>['getCellValueByColumnId']\n }\n // `_values` and `_cells` stay null until something reads them - a\n // 100k-row grid showing twenty rows must not materialise every row's\n // values or cell objects to paint.\n const makeRow = (index: number): BaseRowState<TData> => {\n const original = data[index]!\n return {\n id: getRowId ? getRowId(original, index) : String(index),\n index,\n original,\n depth: 0,\n [ROW_CTX]: rowCtx,\n [ROW_VALUES]: null,\n [ROW_CELLS]: null,\n [ROW_FILTER_GEN]: 0,\n [ROW_FILTER_POS]: -1,\n [ROW_SORT_DROPPED]: 0,\n [ROW_SORT_KEY]: undefined,\n getCanExpand: m.getCanExpand,\n getIsExpanded: m.getIsExpanded,\n toggleExpanded: m.toggleExpanded,\n getIsSelected: m.getIsSelected,\n toggleSelected: m.toggleSelected,\n getAllCells: m.getAllCells,\n getCellValueByColumnId: m.getCellValueByColumnId,\n }\n }\n if (!previous) {\n // A first build has nothing to reuse or repair: make each row the\n // first time something reads it. A grid that only shows the first\n // screen makes ~40 row objects, not 100k; a sort or a filter reads\n // every row and makes them then. Building them all up front was\n // most of a 100k-row mount (~24 ms, plus the garbage).\n cachedBaseRows = lazyRows(data.length, makeRow)\n } else cachedBaseRows = new Array(data.length)\n for (let index = 0; previous && index < data.length; index++) {\n const original = data[index]!\n const kept = previous[index] as BaseRowState<TData> | undefined\n if (kept && kept.original === original) {\n // Read before writing: a row nobody rendered (a million off-screen\n // placeholders under a row model) has nothing memoised, and a\n // store per row was a measurable share of each block landing.\n if (kept[ROW_VALUES] !== null) kept[ROW_VALUES] = null\n if (kept[ROW_CELLS] !== null) kept[ROW_CELLS] = null\n cachedBaseRows[index] = kept\n continue\n }\n // Past a quarter of the rows the merge stops being cheaper than a\n // sort with the ranking pass; let the stages do the full work.\n if (replacedRows && replacedRows.size * 4 >= data.length) replacedRows = null\n const row = makeRow(index)\n // The previous list was lazy and never made this row, but its data\n // object is the same one: hand the old list this row too, so a\n // caller still holding it gets the same object for the same data,\n // as when every row was built up front.\n if (kept === undefined && previousInput?.[index] === original) previous[index] = row\n cachedBaseRows[index] = row\n if (replacedRows && kept) replacedRows.set(kept, row)\n }\n // A new array with every object the same is the documented way to\n // say \"I changed rows in place, re-read them\" (docs/help/cells/\n // view-refresh.md). Nothing was replaced, so there is nothing to\n // repair around: the stages recompute from the current values. A\n // repair assumes the rows it keeps are unchanged in value as well as\n // identity, which is the immutable-update contract a tick follows.\n if (replacedRows && replacedRows.size === 0) replacedRows = null\n }\n\n // Only the slices a pipeline stage actually READS belong in this key.\n //\n // `rowSelection` used to be here, which meant ticking one checkbox on a\n // 100k-row grid re-filtered and re-sorted the entire dataset to rebuild a\n // row array that was identical by construction. Nothing reads it: the two\n // consumers are `getIsSelected` closures (on data rows and on group rows)\n // that read `store.state` when called, so they observe a selection change\n // without the model being rebuilt.\n //\n // `_rowModels` is a closed set of six named slots, so no consumer stage\n // can be inserted that might read selection. A caller CAN supply a custom\n // function for one of those slots; if one ever needs a slice that is not\n // listed here, add it here rather than reinstating all of them.\n const currentSlices = {\n sorting: store.state.sorting,\n columnFilters: store.state.columnFilters,\n pagination: store.state.pagination,\n grouping: store.state.grouping,\n expanded: store.state.expanded,\n }\n if (\n cachedRowModel &&\n cachedRowModelBaseRows === cachedBaseRows &&\n cachedPipeline === options._rowModels &&\n cachedSlices?.sorting === currentSlices.sorting &&\n cachedSlices?.columnFilters === currentSlices.columnFilters &&\n cachedSlices?.pagination === currentSlices.pagination &&\n cachedSlices?.grouping === currentSlices.grouping &&\n cachedSlices?.expanded === currentSlices.expanded\n ) {\n return cachedRowModel\n }\n\n // A stage with work to do reads every row; give it the dense array.\n const anyStageActive =\n (currentSlices.sorting?.length ?? 0) > 0 ||\n (currentSlices.columnFilters?.length ?? 0) > 0 ||\n (currentSlices.grouping?.length ?? 0) > 0 ||\n (currentSlices.expanded as unknown) === true ||\n Object.keys(currentSlices.expanded ?? {}).length > 0\n let rows: Array<Row<TData>> = anyStageActive ? denseRows(cachedBaseRows) : cachedBaseRows\n\n const pipeline = options._rowModels ?? {}\n const ordered: Array<RowModelFactory<TData> | undefined> = [\n pipeline.coreRowModel,\n pipeline.filteredRowModel,\n pipeline.sortedRowModel,\n pipeline.groupedRowModel,\n pipeline.expandedRowModel,\n pipeline.paginatedRowModel,\n ]\n // The stages read the replacements through `replacedRowsOf` while the\n // run lasts; a stage that runs outside one sees null.\n replacedRowsByGrid.set(grid, replacedRows)\n try {\n ordered.forEach((fn) => {\n if (fn) rows = fn({ table: grid, rows })\n })\n } finally {\n replacedRowsByGrid.set(grid, null)\n replacedRows = null\n }\n cachedPipeline = options._rowModels\n cachedSlices = currentSlices\n cachedRowModelBaseRows = cachedBaseRows\n cachedRowModel = { rows }\n return cachedRowModel\n },\n } as InternalGrid<TData>\n\n return grid\n}\n```\n\n### `function isFunction`\n\nNarrowing helper for the many options that accept a value or a function.\n\n```ts\nexport function isFunction(value: unknown): value is (...args: Array<any>) => any {\n return typeof value === 'function'\n}\n```\n"
7915
+ "markdown": "# `@svgrid/grid` · `core.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\core.ts`.\n\n### `type RowData`\n\nThe constraint every row type satisfies: an object keyed by string. Your own\nrow type (`type Person = { name: string }`) is what flows through the generics\nbelow; this is only the lower bound they are declared against.\n\n```ts\nexport type RowData = Record<string, unknown>\n```\n\n### `type Updater`\n\nA new value, or a function that derives it from the previous one - the shape\nevery `set*` on the grid accepts, so callers can update state without first\nreading it.\n\n api.setSorting([{ id: 'name', desc: false }])\n api.setSorting((prev) => [...prev, { id: 'age', desc: true }])\n\n```ts\nexport type Updater<T> = T | ((prev: T) => T)\n```\n\n### `type SortingState`\n\nActive sort clauses, outermost first. `desc: false` is ascending.\n\n```ts\nexport type SortingState = Array<{ id: string; desc: boolean }>\n```\n\n### `type ColumnFilter`\n\nOne column's filter: the column `id`, the `value` being matched, and\noptionally which comparison to use. `fn` defaults to the column's own type -\nsee {@link filterFns} for the available names.\n\n```ts\nexport type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }\n```\n\n### `type ColumnFiltersState`\n\nEvery active column filter. A column with no entry here is unfiltered.\n\n```ts\nexport type ColumnFiltersState = Array<ColumnFilter>\n```\n\n### `type PaginationState`\n\nCurrent page position. `pageIndex` is 0-based, so page 1 is index 0.\n\n```ts\nexport type PaginationState = { pageIndex: number; pageSize: number }\n```\n\n### `type GroupingState`\n\nColumn ids the rows are grouped by, outermost first.\n\n```ts\nexport type GroupingState = Array<string>\n```\n\n### `type ExpandedState`\n\nWhich rows are expanded, keyed by row id. Absent means collapsed.\n\n```ts\nexport type ExpandedState = Record<string, boolean>\n```\n\n### `type RowSelectionState`\n\nWhich rows are selected, keyed by row id. Absent means unselected.\n\n```ts\nexport type RowSelectionState = Record<string, boolean>\n```\n\n### `type ActiveCellState`\n\nWhere keyboard focus sits. The indices address the *displayed* grid (after\nsorting, filtering and paging), not the source data.\n\n```ts\nexport type ActiveCellState = {\n rowIndex: number\n colIndex: number\n cellId: string | null\n}\n```\n\n### `type TableFeatures`\n\nThe set of features a grid has registered, as built by {@link tableFeatures}.\nDeliberately open: a feature is identified by its key, so the type carries\nwhich ones are on without enumerating them.\n\n```ts\nexport type TableFeatures = Record<string, unknown>\n```\n\n### `type CellData`\n\nA cell's value. Unconstrained - a column can hold anything.\n\n```ts\nexport type CellData = unknown\n```\n\n### `type HeaderContext`\n\nWhat a column's `header` render function receives.\n\n```ts\nexport type HeaderContext<TData extends RowData> = {\n header: Header<TData>\n column: Column<TData>\n table: SvGrid<TData>\n}\n```\n\n### `type CellContext`\n\nWhat a column's `cell` render function receives. `getValue()` applies the\ncolumn's accessor (`field` or `fieldFn`); `row.original` is the raw object.\n\n```ts\nexport type CellContext<TData extends RowData> = {\n cell: Cell<TData>\n row: Row<TData>\n column: Column<TData>\n table: SvGrid<TData>\n getValue: () => unknown\n}\n```\n\n### `type CellSpanParams`\n\nParams passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks.\n\n```ts\nexport type CellSpanParams<TData extends RowData = RowData> = {\n /** The row's underlying data object. */\n data: TData\n /** Display-row index in the current (filtered/sorted) row set. */\n rowIndex: number\n /** The column's id. */\n columnId: string\n /** The cell's base value for this column. */\n value: unknown\n}\n```\n\n### `type EditorOptionSource`\n\nThe raw option list a column's `editorOptions` can supply.\n\n```ts\nexport type EditorOptionSource = ReadonlyArray<\n string | number | { value: string | number; label?: string; color?: string }\n```\n\n### `type ValueParserParams`\n\nParams passed to a column's `valueParser(...)` on edit commit.\n\n```ts\nexport type ValueParserParams<TData extends RowData = RowData> = {\n /** The value after built-in per-`editorType` coercion. */\n newValue: unknown\n /** The cell's previous value. */\n oldValue: unknown\n /** The raw string the editor produced (pre-coercion). */\n rawInput: string\n /** The row's underlying data object. */\n data: TData\n /** The column's id. */\n columnId: string\n}\n```\n\n### `type EditorContext`\n\nContext passed to a custom `cellEditor` snippet/component. Three write\nhelpers cover the lifecycle:\n\n - `update(next)` - stage `next` as the draft, keep the editor open.\n Use this for live-preview controls (sliders,\n color pickers) so the user can keep adjusting.\n - `commit(next?)` - write the value AND close the editor. The\n argument is optional; when omitted, the most\n recently `update()`d value is saved. Use this\n for \"done\" gestures (Enter, picking an option).\n - `cancel()` - discard the draft and close the editor.\n\n```ts\nexport type EditorContext<TData extends RowData> = CellContext<TData> & {\n value: unknown\n update: (next: unknown) => void\n commit: (next?: unknown) => void\n cancel: () => void\n}\n```\n\n### `type CellFormatConfig`\n\nDeclarative cell formatting, applied through `Intl` - number, currency,\npercent, date and datetime. Prefer this over a `formatter` function: it is\nlocale-aware, and export and the clipboard reuse the same configuration.\n\n```ts\nexport type CellFormatConfig =\n | {\n type: 'number'\n locales?: string | Array<string>\n options?: Intl.NumberFormatOptions\n }\n | {\n type: 'currency'\n /** ISO 4217 (default USD) */\n currency?: string\n locales?: string | Array<string>\n options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>\n }\n | {\n type: 'percent'\n locales?: string | Array<string>\n options?: Omit<Intl.NumberFormatOptions, 'style'>\n /**\n * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).\n * Default false.\n */\n valueIsPercentPoints?: boolean\n }\n | {\n type: 'date' | 'datetime'\n locales?: string | Array<string>\n /**\n * Shortcut patterns merged with `options`:\n * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,\n * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.\n */\n pattern?: string\n options?: Intl.DateTimeFormatOptions\n }\n```\n\n### `type CellFormatter`\n\nA column's custom display function, for anything {@link CellFormatConfig}\ncannot express. Returns a string - to render markup, use `cell` instead.\n\n```ts\nexport type CellFormatter<TData extends RowData> = (context: {\n value: unknown\n row: Row<TData>\n column: Column<TData>\n table: SvGrid<TData>\n}) => string\n```\n\n### `type ColumnDefTemplate`\n\nA header or cell slot: a literal string, or a function returning renderable content.\n\n```ts\nexport type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)\n```\n\n### `type GroupAggregator`\n\nHow a column's value is aggregated for a group row when `columnGrouping`\nis active. Built-in reducers cover the common cases; pass a function for\nanything custom (weighted average, median, percentile, distinct count).\nThe function receives the finite numeric values AND the raw leaf rows.\n\n```ts\nexport type GroupAggregator<TData = any> =\n | 'sum'\n | 'avg'\n | 'min'\n | 'max'\n | 'count'\n | 'countDistinct'\n | 'extent'\n | 'first'\n | ((values: number[], rows: Array<TData>) => unknown)\n```\n\n### `function applyGroupAggregate`\n\nApply a group aggregator over a bucket's leaf rows for one column.\n\n```ts\nexport function applyGroupAggregate<TData extends RowData>(\n agg: GroupAggregator<TData>,\n columnId: string,\n rows: ReadonlyArray<Row<TData>>,\n): unknown {\n // One pass, no intermediate arrays.\n //\n // This used to build a `raw` array, then a coerced one, then a filtered one -\n // three allocations per aggregated column PER GROUP - before reducing. On a\n // 100k-row grid grouped two levels deep, aggregation was about two thirds of\n // the total grouping cost (213ms with three aggregators against 81ms with\n // none), and each additional aggregated column added roughly 80ms.\n //\n // `count` first: it never needs to look at a value at all.\n if (agg === 'count') return rows.length\n\n if (agg === 'first') {\n return rows.length ? rows[0]!.getCellValueByColumnId(columnId) : undefined\n }\n\n if (agg === 'countDistinct') {\n const seen = new Set<string>()\n for (const row of rows) seen.add(String(row.getCellValueByColumnId(columnId) ?? ''))\n return seen.size\n }\n\n if (typeof agg === 'function') {\n // Custom aggregators keep their contract: the finite numbers, then the\n // original row objects. Note `Number(null)` is 0 and therefore finite, so\n // nulls DO reach the callback as zeros - long-standing behaviour.\n const nums: number[] = []\n for (const row of rows) {\n const n = Number(row.getCellValueByColumnId(columnId))\n if (Number.isFinite(n)) nums.push(n)\n }\n return agg(nums, rows.map((r) => r.original))\n }\n\n // sum / avg / min / max / extent share one accumulation pass.\n let count = 0\n let sum = 0\n let min = Infinity\n let max = -Infinity\n for (const row of rows) {\n const n = Number(row.getCellValueByColumnId(columnId))\n if (!Number.isFinite(n)) continue\n count++\n // Left-to-right, matching the previous `reduce`, so float rounding is\n // bit-identical rather than merely close.\n sum += n\n // Math.min/max on scalars rather than `<`, which differs on -0, and rather\n // than the old `Math.min(...nums)` - spreading a whole group throws\n // RangeError once the bucket is big enough to exhaust the argument stack.\n min = Math.min(min, n)\n max = Math.max(max, n)\n }\n if (!count) return undefined\n switch (agg) {\n case 'sum':\n return sum\n case 'avg':\n return sum / count\n case 'min':\n return min\n case 'max':\n return max\n case 'extent':\n return `${min} – ${max}`\n default:\n return undefined\n }\n}\n```\n\n### `type ColumnDef`\n\nA column definition.\n\n`TFeatures` is a phantom parameter - it is threaded through nested\n`columns` groups but no member depends on it, so `{}`, `TableFeatures` and\n`typeof features` are all interchangeable here. It is deliberately left\nWITHOUT a default: `ColumnDef<Row>` would otherwise bind `Row` to this slot\nand silently type your data as `RowData`, losing every field-name check.\nPrefer {@link GridColumns} / {@link GridColumnDef} for the common case.\n\n```ts\nexport type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {\n id?: string\n field?: keyof TData & string\n fieldFn?: (row: TData) => unknown\n header?: ColumnDefTemplate<HeaderContext<TData>>\n footer?: ColumnDefTemplate<HeaderContext<TData>>\n cell?: ColumnDefTemplate<CellContext<TData>>\n /**\n * A field type registered by `@svgrid/enterprise/fields` (`'attachment'`,\n * `'rating'`, `'url'`, ...). With the module enabled the cell is drawn by\n * that type; a column's own `cell` still wins. Without it the column draws\n * as an ordinary one.\n */\n fieldType?: string\n /** Properties of `fieldType` (options, precision, max files, ...). */\n fieldProps?: Record<string, unknown>\n columns?: Array<ColumnDef<TFeatures, TData>>\n /**\n * Declarative cell spanning (merged cells). Return how many COLUMNS this\n * cell spans to the right (1 = no span). Value-driven. Feed\n * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses\n * the same real `colspan`/`rowspan` merge engine (no separate code path).\n */\n colSpan?: (params: CellSpanParams<TData>) => number\n /**\n * Declarative cell spanning (merged cells). Return how many ROWS this cell\n * spans downward (1 = no span). See `colSpan` for how to apply.\n */\n rowSpan?: (params: CellSpanParams<TData>) => number\n /**\n * What the column's values are. It decides sorting, filter operators and\n * alignment, whatever edits the cell: `editorType` only picks the editor,\n * so a number column can be typed in a text box or a custom editor and\n * still sort, filter and align as numbers.\n * 'text' → left-aligned, text filters (text editor by default)\n * 'number' → right-aligned, numeric sort and filters (number editor)\n * 'boolean' → centered, true / false filter (checkbox editor)\n * 'date' → Date values, right-aligned, date sort and filters (date\n * editor, `{ type: 'date' }` format)\n * 'dateString' → ISO date STRINGS (e.g. '2026-06-27'), as a date\n * Anything you set explicitly (`editorType`, `align`, `format`) still wins.\n * Without it, a built-in `editorType` (`number`, `date`, `datetime`,\n * `checkbox`) implies the type, as before; grid-level `inferColumnTypes`\n * infers it from the first data row for columns with neither.\n */\n cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'\n /**\n * Hide this column when the grid's `responsive` mode is on and the grid is\n * narrower than this many pixels - drop low-priority columns on small\n * screens. No effect unless the grid has `responsive` set.\n */\n hideBelow?: number\n /**\n * For a column INSIDE a collapsible column group: `'open'` shows this column\n * only while the group is expanded, `'closed'` only while collapsed. Omit to\n * always show it. Setting it on any direct child gives the parent group a\n * collapse toggle. Pair with `openByDefault` on the group.\n */\n columnGroupShow?: 'open' | 'closed'\n /**\n * For a GROUP column (one with `columns: [...]`): start the group expanded.\n * Defaults to `false` (collapsed), the conventional default - so only the always-on\n * and `columnGroupShow: 'closed'` children show until the user expands it.\n */\n openByDefault?: boolean\n editorType?:\n | 'text'\n | 'number'\n | 'date' // rich SvCalendar popover (opt out with 'date-native')\n | 'datetime' // rich SvDateTimePicker (opt out with 'datetime-native')\n | 'time' // rich SvTimePicker dial (opt out with 'time-native')\n | 'date-native' // plain <input type=\"date\">\n | 'datetime-native' // plain <input type=\"datetime-local\">\n | 'time-native' // plain <input type=\"time\"> - HH:MM or HH:MM:SS\n | 'password' // native <input type=\"password\"> with masked rendering\n | 'checkbox'\n | 'list'\n | 'chips'\n | 'select' // custom dropdown - single value, no typeahead\n | 'rich-select' // custom dropdown with a typeahead search input\n | 'autocomplete' // free-text input with a live-filtered suggestion list (accepts any value)\n | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline\n | 'color' // native <input type=\"color\"> swatch\n | 'rating' // 5-star rating control\n // Any other string names a CUSTOM editor registered via `registerCellEditor`\n // (or `registerBuiltinEditors`). `(string & {})` keeps the literals above\n // autocompleting while allowing arbitrary custom type names.\n | (string & {})\n /**\n * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`\n * and `cancel()` helper. Use when none of the built-in `editorType`s fit;\n * the snippet's outer element is mounted inside the editing cell and\n * inherits keyboard handling (Esc cancels, Enter commits unless your\n * snippet preventDefaults it).\n *\n * Coexists with `editorType`: when both are set, `cellEditor` wins and\n * `editorType` is treated as a hint for parsing the saved value.\n */\n cellEditor?: ColumnDefTemplate<EditorContext<TData>>\n /**\n * The text format of the date / datetime editor's box, in date-format\n * tokens (`'MMMM d, yyyy'`, `'MM/dd/yyyy h:mm tt'`), so an edit shows the\n * date the way the cell does. Default `'yyyy-MM-dd'`, with time\n * `'yyyy-MM-dd HH:mm'`.\n */\n editorFormatString?: string\n /**\n * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`\n * runs per cell so the tooltip can reflect the value. Returning an empty\n * string skips the tooltip.\n */\n tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)\n /**\n * Declarative per-cell validation. Runs for EVERY\n * rendered cell - including values already present in `data` on load, not\n * just on edit - so bad data is flagged immediately. Invalid cells get the\n * `sv-grid-cell-invalid` class (red highlight) and the returned message as\n * their tooltip.\n *\n * Return value:\n * - `null` / `undefined` / `true` → valid (no highlight)\n * - `false` → invalid, no message\n * - a non-empty `string` → invalid, string is the tooltip\n *\n * On its own `validate` flags: the value keeps rendering as-is and an\n * edit that fails it still lands. Set `rejectInvalid` to refuse such an\n * edit instead.\n */\n validate?: (params: {\n value: unknown\n row: TData\n rowIndex: number\n column: Column<TData>\n }) => string | boolean | null | undefined\n /**\n * Refuse an edit that `validate` flags. When true, a committed value that\n * makes `validate` return `false` or a message is not written: the cell\n * keeps its old value, no history step is recorded and\n * `onCellValueChange` does not fire. The value is checked after\n * `valueParser` has run, so the rule sees what would be stored. Applies to\n * the inline editor, a commit to a selection and a full-row edit; paste\n * does not go through the editor and is not checked.\n */\n rejectInvalid?: boolean\n /**\n * Gate editing per column or per cell.\n *\n * - `true` (or omitted): the column is fully editable.\n * - `false`: the column is read-only - double-click, type-to-edit,\n * fill-handle drag, Delete, and clipboard paste all skip it.\n * - `(ctx) => boolean`: evaluated for each cell, so you can lock\n * individual rows (e.g. by role, status, ownership). Returning\n * `false` opts the cell out of every editing path, identical to\n * setting `editable: false` on the whole column for that row.\n *\n * The grid-wide `enableInlineEditing` prop still wins when set to\n * `false`.\n */\n editable?: boolean | ((context: CellContext<TData>) => boolean)\n /**\n * Transform the committed edit value before it is written to the row.\n * Runs after the built-in per-`editorType` coercion, so `newValue` is\n * already type-parsed; return the final value to store (e.g. round a\n * number, uppercase a code, look up an id). A `valueParser` hook.\n */\n valueParser?: (params: ValueParserParams<TData>) => unknown\n /**\n * Briefly flash / highlight this column's cell when its value changes\n * (streaming feeds, edits, server pushes). `true` uses the default flash;\n * pass `{ className }` to apply your own animation class instead.\n */\n cellFlash?: boolean | { className?: string }\n /**\n * When `false`, this column never shows a sort indicator and clicking\n * its header is a no-op - `api.setSort(thisColumn, ...)` is also\n * ignored. Defaults to `true` (the column participates in sorting as\n * long as `rowSortingFeature` is registered).\n */\n sortable?: boolean\n /**\n * When `false`, this column never shows a filter funnel / menu and\n * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the\n * column is filterable as long as `columnFilteringFeature` is\n * registered).\n */\n filterable?: boolean\n /**\n * Options for `editorType: 'list' | 'chips'`. Either bare values (the\n * string is both value and label) or `{ value, label }` objects.\n * For `chips` this is optional - when omitted, the chips editor becomes\n * free-form (user types and presses Enter to commit a chip).\n *\n * Pass a function `(row) => options` for row-dependent (cascading)\n * options - e.g. City options that depend on Country in the same row.\n *\n * Either form may return a **Promise**, for options that come from the\n * server. While it resolves, the editor shows a loading state and the cell\n * renders its raw value.\n *\n * Results are cached so reopening an editor does not refetch: a static source\n * per column, a per-row source per row AND per that row's data - so a cascade\n * reloads by itself when the cell it depends on is edited. Call\n * `api.refreshEditorOptions(columnId?)` when the list changes server-side.\n */\n editorOptions?:\n | EditorOptionSource\n | Promise<EditorOptionSource>\n | ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>)\n /** When true, list/chips allow multiple selections. Cell value becomes an array. */\n editorMultiple?: boolean\n /**\n * The text editor accepts line breaks: Alt+Enter inserts one and the\n * editor grows a line per break, while Enter still commits. What a\n * spreadsheet cell with wrapped text wants; `editorType: 'textarea'` is\n * the other shape, where Enter itself is the line break.\n */\n editorMultiline?: boolean\n /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */\n editorSeparator?: string\n format?: CellFormatConfig\n formatter?: CellFormatter<TData>\n /**\n * The number this column sorts a value by, in place of the value: a status\n * option's position in its list, say, so \"To do\" comes before \"Done\".\n * Blanks and anything not a number sort first, as in a number column.\n */\n sortValue?: (value: unknown) => number | null | undefined\n /**\n * Aggregate this column's values into the group row when grouping is\n * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |\n * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result\n * is formatted with this column's `format` and shown in the group header.\n */\n aggregate?: GroupAggregator<TData>\n /**\n * What this column contributes to the grid's footer summary row (the one\n * turned on with `summary` / `enableRowSummaries`). Takes the same\n * aggregators as {@link aggregate}, and the result is formatted with this\n * column's `format`.\n *\n * Without it the footer falls back to its default: the sum of a numeric\n * column, `Count: N` otherwise. Set `false` to leave the cell blank, which is\n * usually what an actions or checkbox column wants.\n *\n * { field: 'amount', summary: 'avg' }\n * { id: 'actions', summary: false }\n */\n summary?: GroupAggregator<TData> | false\n /**\n * Render the cell as an in-cell sparkline chart. The cell value should be\n * an array of numbers (or a comma/space separated string). Mutually\n * exclusive with a custom `cell` renderer (a `cell` wins if both are set).\n *\n * { sparkline: { type: 'line' } } // default line\n * { sparkline: { type: 'bar', color: '#16a34a' } }\n * { sparkline: { type: 'winloss' } } // sign-only up/down\n *\n * See `SparklineConfig` for the full option set (type, color,\n * negativeColor, width, height, fixed min/max).\n */\n sparkline?: SparklineConfig\n /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */\n width?: number\n /**\n * Whether the user may resize this column. Only consulted when the grid has\n * `columnResize` on - it narrows that, it does not enable anything.\n *\n * `false` removes the column's drag handle entirely, so pointer drag, the\n * keyboard arrows and double-click-to-autosize are all gone with it, and the\n * column menu drops its Autosize item. Use it for the columns whose width is\n * part of the layout rather than a preference: a row-number gutter, a\n * checkbox column, a fixed icon column.\n *\n * Programmatic sizing is unaffected - `api.autosizeColumn()`,\n * `api.setColumnWidth()` and `fitColumns` all still apply, the same way they\n * do when `columnResize` is off. This governs the user affordance only.\n */\n resizable?: boolean\n /**\n * Initial visibility. Set `false` to start the column hidden while still\n * listing it in the Choose Columns UI for the user to re-enable. Applied\n * once at mount; after that `api.setColumnVisible` / user toggles win.\n * On a group column, `false` hides the whole group's leaf columns.\n */\n visible?: boolean\n /**\n * Horizontal alignment for header and body cells. When omitted, the\n * default is inferred from `editorType`:\n * - `'number' | 'date' | 'datetime'` → `'right'`\n * - `'checkbox'` → `'center'`\n * - everything else → `'left'`\n */\n align?: 'left' | 'center' | 'right'\n /**\n * Per-cell conditional CSS. Two shapes:\n *\n * - **String** (or array of strings): class name(s) added to the\n * cell's `<td>` for every row in this column.\n * - **Function**: invoked per cell with the same `CellContext` shape\n * the `cell` renderer receives. Return a string, an array of\n * strings, or an object mapping class names to booleans.\n *\n * Use it for status tinting, conditional bold, \"negative number\"\n * coloring - anything that's a function of the row's value. Cells\n * still receive their format / cell renderer; the class just\n * augments the rendered `<td>`.\n */\n cellClass?:\n | string\n | ReadonlyArray<string>\n | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)\n}\n```\n\n### `type GridColumnDef`\n\nA column definition keyed only by your row type - the ergonomic form of\n{@link ColumnDef}, whose first parameter is a phantom feature bag that is\nalmost always `{}`.\n\n```ts\nconst columns: GridColumns<Person> = [{ field: 'firstName', header: 'Name' }]\n```\n\nInterchangeable with `ColumnDef<{}, TData>` and `ColumnDef<typeof features,\nTData>` in both directions, so it mixes freely with existing code.\n\n```ts\nexport type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeatures, TData>\n```\n\n### `type GridColumns`\n\nAn array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`.\n\n```ts\nexport type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>\n```\n\n### `type Column`\n\nA resolved column: your {@link ColumnDef} plus everything the grid computed\nfrom it - its id, its depth under any group header, and the sort handlers a\nheader needs. This is what you receive in render contexts; the `ColumnDef`\nis what you wrote.\n\n```ts\nexport type Column<TData extends RowData> = {\n id: string\n columnDef: ColumnDef<any, TData>\n depth: number\n parentId?: string\n getCanSort: () => boolean\n getCanFilter: () => boolean\n getIsSorted: () => false | 'asc' | 'desc'\n getToggleSortingHandler: () => () => void\n}\n```\n\n### `type Header`\n\nOne header cell. `colSpan` is how many leaf columns it covers, and\n`isPlaceholder` marks the empty cells that pad a group-header row so the\nlevels line up.\n\n```ts\nexport type Header<TData extends RowData> = {\n id: string\n isPlaceholder: boolean\n colSpan: number\n column: Column<TData>\n getContext: () => HeaderContext<TData>\n}\n```\n\n### `type HeaderGroup`\n\nOne row of header cells. A grid with grouped columns has several, outermost first.\n\n```ts\nexport type HeaderGroup<TData extends RowData> = {\n id: string\n headers: Array<Header<TData>>\n}\n```\n\n### `type Cell`\n\nOne cell: the intersection of a {@link Row} and a {@link Column}.\n\n```ts\nexport type Cell<TData extends RowData> = {\n id: string\n row: Row<TData>\n column: Column<TData>\n getValue: () => unknown\n getContext: () => CellContext<TData>\n}\n```\n\n### `type Row`\n\nA row in the display model. `original` is your untouched data object;\neverything else is grid-computed. `index` is the position in the displayed\nset, so it shifts as sorting and filtering change - key on `id`, not index.\n\nGroup rows and tree parents carry `subRows`; a plain data row does not.\n\n```ts\nexport type Row<TData extends RowData> = {\n id: string\n index: number\n original: TData\n depth: number\n subRows?: Array<Row<TData>>\n /** Total leaf (data) rows under this group row. Undefined for data rows. */\n leafCount?: number\n getCanExpand: () => boolean\n getIsExpanded: () => boolean\n toggleExpanded: () => void\n getIsSelected: () => boolean\n toggleSelected: () => void\n getAllCells: () => Array<Cell<TData>>\n getCellValueByColumnId: (columnId: string) => unknown\n}\n```\n\n### `type RowModel`\n\nThe output of the row pipeline: the rows to display, in order.\n\n```ts\nexport type RowModel<TData extends RowData> = {\n rows: Array<Row<TData>>\n}\n```\n\n### `type Store`\n\nThe minimal reactive store behind the headless core - read `state`, write\nthrough `setState`, and `subscribe` for changes. Deliberately framework\nfree, which is what lets the core run under plain Node.\n\nIn Svelte you rarely touch this: `subscribeGrid` wraps it with fine-grained\nselectors so a component only re-runs for the slice it read.\n\n```ts\nexport type Store<T> = {\n readonly state: T\n setState: (updater: (prev: T) => T) => void\n subscribe: (listener: () => void) => () => void\n}\n```\n\n### `const rowSortingFeature`\n\nClick-to-sort. Injected by the `sortable` shortcut.\n\nThis and the five features below are opaque markers: pass the ones you want\nto {@link tableFeatures} and the grid wires up the matching row model. With\n`<SvGrid>` you rarely name them - the boolean shortcuts (`sortable`,\n`filterable`, `pageable`, `groupable`) inject them for you. Reach for them\ndirectly when driving the headless core, or when you want a feature on\nwithout its UI.\n\nThe names match TanStack Table v9, so a features object written for it works\nhere unchanged.\n\n```ts\nexport const rowSortingFeature = { key: 'rowSortingFeature' }\n```\n\n### `const columnFilteringFeature`\n\nPer-column filtering. Injected by the `filterable` shortcut.\n\n```ts\nexport const columnFilteringFeature = { key: 'columnFilteringFeature' }\n```\n\n### `const rowPaginationFeature`\n\nPaging of the row model. Injected by the `pageable` shortcut.\n\n```ts\nexport const rowPaginationFeature = { key: 'rowPaginationFeature' }\n```\n\n### `const columnGroupingFeature`\n\nRow grouping with aggregation. Injected by the `groupable` shortcut.\n\n```ts\nexport const columnGroupingFeature = { key: 'columnGroupingFeature' }\n```\n\n### `const rowSelectionFeature`\n\nRow selection state (the checkbox column reads it).\n\n```ts\nexport const rowSelectionFeature = { key: 'rowSelectionFeature' }\n```\n\n### `const rowExpandingFeature`\n\nExpand / collapse, for tree rows and master-detail.\n\n```ts\nexport const rowExpandingFeature = { key: 'rowExpandingFeature' }\n```\n\n### `function tableFeatures`\n\nDeclare which features a grid uses. Identity at runtime - its whole job is to\ncapture the exact set in the type, so `ColumnDef<typeof features, Row>` knows\nwhat is registered and anything you did not register is tree-shaken out.\n\n```ts\nconst features = tableFeatures({ rowSortingFeature, columnFilteringFeature })\n```\n\nSame call signature as TanStack Table v9, so a features object written for it\ntransfers unchanged.\n\n```ts\nexport function tableFeatures<T extends TableFeatures>(features: T): T {\n return features\n}\n```\n\n### `const sortFns`\n\nBuilt-in comparators, chosen per column by its data type. `auto` compares as\ntext; set a column's type or supply your own comparator to override.\n\n```ts\nexport const sortFns = {\n auto: (a: unknown, b: unknown) => compareAutoValues(a, b),\n number: (a: unknown, b: unknown) => compareNumericKeys(numberSortKey(a), numberSortKey(b)),\n date: (a: unknown, b: unknown) => compareNumericKeys(dateSortKey(a), dateSortKey(b)),\n}\n```\n\n### `const filterFns`\n\nBuilt-in match functions, named by {@link ColumnFilter}'s `fn`.\n`includesString` is case-insensitive substring; `equals` is strict identity.\n\n```ts\nexport const filterFns = {\n includesString: (value: unknown, query: string) =>\n String(value).toLowerCase().includes(query.toLowerCase()),\n equals: (value: unknown, query: unknown) => value === query,\n}\n```\n\n### `type BaseRowCtx`\n\nEverything a base row needs that is the same for every row in the table.\n\nOne object per table, referenced by every row, instead of one closure scope\nper row. See {@link BASE_ROW_METHODS}.\n\n```ts\nexport type BaseRowCtx<TData extends RowData> = {\n grid: SvGrid<TData>\n store: { state: Record<string, any> }\n columns: Array<Column<TData>>\n columnCount: number\n columnIndexById: Map<string, number>\n}\n```\n\n### `const ROW_VALUES`\n\nA base row's cached cell values. @internal\n\n```ts\nexport const ROW_VALUES = Symbol('svgrid.row.values')\n```\n\n### `const ROW_CELLS`\n\nA base row's cached cells. @internal\n\n```ts\nexport const ROW_CELLS = Symbol('svgrid.row.cells')\n```\n\n### `const ROW_FILTER_POS`\n\nThe row's index in the last full filter's output (see ROW_FILTER_GEN). @internal\n\n```ts\nexport const ROW_FILTER_POS = Symbol('svgrid.row.filterPos')\n```\n\n### `function denseRows`\n\nThe rows as a plain dense array. For a lazyRows() list, makes the rows not\nmade yet and returns its backing array - the same row objects, without the\nProxy in front. Anything that reads every row (a sort, a filter) should go\nthrough this first: a trap per read, over several passes of a million rows,\ncost more than building the rows did.\n\n```ts\nexport function denseRows<T>(rows: T[]): T[] {\n const fill = (rows as unknown as Record<symbol, (() => T[]) | undefined>)[LAZY_FILL]\n return fill ? fill() : rows\n}\n```\n\n### `type WindowedRowModel`\n\nThe row model for windowed data (windowed-data.ts), or null when `data` is\nan ordinary array. It lives in windowed-row-model.ts and is installed by the\nfirst createWindowedData() call, so a grid that is never handed windowed\ndata does not bundle it: this file imports nothing from either module.\n\n```ts\nexport type WindowedRowModel<TData extends RowData> = (data: unknown, columns: Array<Column<TData>>) => RowModel<TData> | null\n```\n\n### `type WindowedRowModelInstall`\n\nHow windowed-row-model.ts builds its row model from the base row helpers. @internal\n\n```ts\nexport type WindowedRowModelInstall =<TData extends RowData>(ctx: {\n rowCtxFor: (columns: Array<Column<TData>>) => BaseRowCtx<TData>\n getRowId: () => ((row: TData, index: number) => string) | undefined\n}) => WindowedRowModel<TData>\n```\n\n### `function registerWindowedRowModel`\n\nInternal: windowed-row-model.ts installs itself here.\n\n```ts\nexport function registerWindowedRowModel(install: WindowedRowModelInstall): void {\n installWindowedRowModel = install\n}\n```\n\n### `type BaseRowState`\n\nA base row's private fields, on top of the public {@link Row} surface.\n\n```ts\nexport type BaseRowState<TData extends RowData> = Row<TData> & {\n [ROW_CTX]: BaseRowCtx<TData>\n [ROW_VALUES]: Array<unknown> | null\n [ROW_CELLS]: Array<Cell<TData>> | null\n [ROW_FILTER_GEN]: number\n [ROW_FILTER_POS]: number\n [ROW_SORT_DROPPED]: number\n [ROW_SORT_KEY]: unknown\n}\n```\n\n### `const BASE_ROW_METHODS`\n\nThe methods every base row carries, defined ONCE and assigned by reference.\n\nRows used to be built as object literals whose methods were closures, which\nmeant a 100k-row grid allocated 700k closures and a closure scope per row\nbefore painting anything. Measured at 100k x 9: 13.8 ms and 56.5 MB to build,\nagainst 2.0 ms and 14.5 MB for this shape - the single largest cost in\nmounting a large grid.\n\nThey read their row through `this` rather than a captured variable, which is\nwhy they can be shared. Note they are assigned as OWN properties rather than\nput on a prototype: `Row` is public, several row models legitimately do\n`{ ...row, depth }`, and a spread copies own properties but not a prototype.\nA class here would silently strip every method off a cloned row.\n\n```ts\nexport const BASE_ROW_METHODS = {\n getCanExpand(this: BaseRowState<RowData>) {\n return false\n },\n getIsExpanded(this: BaseRowState<RowData>) {\n return Boolean((this[ROW_CTX].store.state.expanded ?? {})[this.id])\n },\n toggleExpanded(this: BaseRowState<RowData>) {\n const id = this.id\n this[ROW_CTX].grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getIsSelected(this: BaseRowState<RowData>) {\n return Boolean((this[ROW_CTX].store.state.rowSelection ?? {})[this.id])\n },\n toggleSelected(this: BaseRowState<RowData>) {\n const id = this.id\n this[ROW_CTX].grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getAllCells(this: BaseRowState<RowData>) {\n return this[ROW_CELLS] ?? buildBaseRowCells(this)\n },\n getCellValueByColumnId(this: BaseRowState<RowData>, columnId: string) {\n const idx = this[ROW_CTX].columnIndexById.get(columnId)\n if (idx === undefined) return undefined\n if (!this[ROW_VALUES]) this[ROW_VALUES] = baseRowValues(this)\n return this[ROW_VALUES][idx]\n },\n}\n```\n\n### `type RowModelFactory`\n\nOne stage of the row pipeline: takes the rows produced so far and returns the\nnext set. Stages compose in the order given to `_rowModels`, so filtering\nbefore sorting sorts only what survived the filter.\n\n```ts\nexport type RowModelFactory<TData extends RowData> = (args: {\n table: SvGrid<TData>\n rows: Array<Row<TData>>\n}) => Array<Row<TData>>\n```\n\n### `function createCoreRowModel`\n\nThe identity stage that starts every pipeline. Always required, even when no\nother stage is: it is what turns your data into rows.\n\n```ts\nexport function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ rows }) => rows\n}\n```\n\n### `function createFilteredRowModel`\n\nDrops rows that fail the active {@link ColumnFiltersState}. Pairs with\n`columnFilteringFeature`; without it there are no filters to apply.\n\n```ts\nexport function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {\n /**\n * The last full filter, so a tick can repair it. When `getRowModel`\n * reports which rows were replaced (`replacedRowsOf`) and the filter state\n * and columns are the same objects as last time, each replacement is tested\n * on its own: a row that stays in keeps its slot, a row that stays out\n * stays out, and a row whose membership flips sends the whole thing back\n * through the full pass, because an insertion has to land in data order.\n * Positions come from stamps on the rows (ROW_FILTER_GEN / ROW_FILTER_POS)\n * written by the full pass; no map is built.\n */\n let cache: { input: Array<Row<TData>>; output: Array<Row<TData>>; filters: ColumnFiltersState; columns: Array<Column<TData>>; gen: number } | null = null\n let generation = 0\n\n return ({ table, rows }) => {\n const filters: ColumnFiltersState = table.getState().columnFilters ?? []\n if (!filters.length) {\n cache = null\n return rows\n }\n const columns = table.getAllColumns()\n\n // Resolve each filter's match function once, outside the row loop.\n const compiled = filters.map((filter) => ({\n id: filter.id,\n value: filter.value,\n fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,\n }))\n const passes = (row: Row<TData>) => {\n for (let i = 0; i < compiled.length; i++) {\n const filter = compiled[i]!\n // `getCellValueByColumnId` rather than `getAllCells().find(...)`.\n // Both read the same lazily-built `cachedValues` array, but the latter\n // also builds and caches the row's whole `Cell[]` - one object per\n // column - purely to reach one field. On a 100k-row grid that is\n // 100,000 cell arrays the filter never looks at again, and it defeats\n // the laziness the row factory exists to provide.\n if (!filter.fn(row.getCellValueByColumnId(filter.id), filter.value as any)) return false\n }\n return true\n }\n\n if (cache && cache.filters === filters && cache.columns === columns) {\n if (cache.input === rows) return cache.output\n const replaced = replacedRowsOf(table)\n if (replaced && rows.length === cache.input.length) {\n let out: Array<Row<TData>> | null = null\n let ok = true\n for (let i = 0; i < rows.length && ok; i++) {\n const prev = cache.input[i]! as BaseRowState<TData>\n const next = rows[i]! as BaseRowState<TData>\n if (next === prev) continue\n if (replaced.get(prev) !== next) { ok = false; break }\n const wasIn = prev[ROW_FILTER_GEN] === cache.gen\n const isIn = passes(next)\n if (wasIn !== isIn) { ok = false; break }\n if (!isIn) continue\n out ??= cache.output.slice()\n const pos = prev[ROW_FILTER_POS]\n out[pos] = next\n next[ROW_FILTER_GEN] = cache.gen\n next[ROW_FILTER_POS] = pos\n }\n if (ok) {\n const output = out ?? cache.output\n cache = { ...cache, input: rows, output }\n return output\n }\n }\n }\n\n const gen = ++generation\n const output: Array<Row<TData>> = []\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i]! as BaseRowState<TData>\n if (!passes(row)) continue\n row[ROW_FILTER_GEN] = gen\n row[ROW_FILTER_POS] = output.length\n output.push(row)\n }\n cache = { input: rows, output, filters, columns, gen }\n return output\n }\n}\n```\n\n### `function createPaginatedRowModel`\n\nNarrows the rows to the current page. Put it LAST: anything after it would\nonly ever see one page of data.\n\n```ts\nexport function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ table, rows }) => {\n const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }\n const start = pagination.pageIndex * pagination.pageSize\n return rows.slice(start, start + pagination.pageSize)\n }\n}\n```\n\n### `function createGroupedRowModel`\n\nBuckets rows by the active {@link GroupingState} and inserts a group row\nahead of each bucket, carrying that bucket's aggregates.\n\n```ts\nexport function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ table, rows }) => {\n const grouping: GroupingState = table.getState().grouping ?? []\n if (!grouping.length) return rows\n const columns = table.getAllColumns()\n\n // Recursively bucket rows by each grouping column in turn. At every level a\n // group row is built that stands in for its children - a non-group column\n // resolves to the value shared by every leaf row, or to undefined when the\n // leaves disagree.\n function buildGroups(\n input: Array<Row<TData>>,\n levelIndex: number,\n depth: number,\n idPrefix: string,\n /** Grouping columns already fixed by an ancestor bucket, and their raw\n * values - `undefined` where that bucket mixed several. */\n fixedValues: ReadonlyMap<string, unknown>,\n ): Array<Row<TData>> {\n if (levelIndex >= grouping.length) {\n // Leaves: actual data rows, with their nesting depth recorded.\n return input.map((row) => ({ ...row, depth }))\n }\n const groupKey = grouping[levelIndex]\n if (!groupKey) return input\n\n // Buckets carry the RAW grouping value alongside the rows, plus whether\n // the bucket saw more than one distinct raw value. Both are needed to let\n // deeper levels skip re-scanning this column: buckets are keyed by\n // `String(value ?? '')`, so `null`, `undefined` and `''` collapse into one\n // bucket, and a scan of such a bucket would report disagreement. Tracking\n // it here costs one comparison per row and keeps the shortcut honest.\n type Bucket = { rows: Array<Row<TData>>; raw: unknown; mixed: boolean }\n const buckets = new Map<string, Bucket>()\n for (const row of input) {\n const value = row.getCellValueByColumnId(groupKey)\n const key = String(value ?? '')\n const bucket = buckets.get(key)\n if (bucket) {\n bucket.rows.push(row)\n if (!bucket.mixed && bucket.raw !== value) bucket.mixed = true\n } else {\n buckets.set(key, { rows: [row], raw: value, mixed: false })\n }\n }\n\n const groupRows: Array<Row<TData>> = []\n let index = 0\n buckets.forEach((bucket, key) => {\n const children = bucket.rows\n const id = `${idPrefix}_${groupKey}_${key}`\n // Record this column as fixed for deeper levels ONLY when the bucket is\n // homogeneous. If all rows here share a raw value, so does every subset\n // of them, which is what makes the shortcut sound.\n //\n // A MIXED bucket must not be recorded at all - not even as \"undefined\".\n // `null`, `undefined` and `''` share a bucket key, so a mixed bucket can\n // still split into homogeneous children one level down, and those\n // children have a real shared value that a scan would find. Marking the\n // column resolved here would hand them the parent's disagreement.\n const nextFixed = bucket.mixed ? fixedValues : new Map(fixedValues).set(groupKey, bucket.raw)\n const subRows = buildGroups(children, levelIndex + 1, depth + 1, id, nextFixed)\n const isDeepest = levelIndex + 1 >= grouping.length\n const leafCount = isDeepest\n ? subRows.length\n : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)\n\n // Every grouping column ABOVE this level is already resolved: bucketing\n // by it is what made it constant, so scanning the children to rediscover\n // it is pure waste. Only the current level's key short-circuited before,\n // so a second-level group walked all of its children to re-derive the\n // first level's value - about 100,000 reads on the 100k x 9 two-level\n // case, for an answer already in hand.\n //\n // The current level still returns the stringified bucket key rather than\n // the raw value, because that is what it has always returned and the\n // group row's display depends on it.\n const resolveColumnValue = (columnId: string): unknown => {\n if (columnId === groupKey) return key\n if (fixedValues.has(columnId)) return fixedValues.get(columnId)\n let resolved: unknown\n let hasResolved = false\n for (const child of children) {\n const childValue = child.getCellValueByColumnId(columnId)\n if (!hasResolved) {\n resolved = childValue\n hasResolved = true\n } else if (childValue !== resolved) {\n return undefined\n }\n }\n return resolved\n }\n\n const groupOriginal: Record<string, unknown> = {}\n columns.forEach((column) => {\n const field = column.columnDef.field\n if (!field) return\n const agg = column.columnDef.aggregate\n groupOriginal[field] = agg\n ? applyGroupAggregate(agg, column.id, children)\n : resolveColumnValue(column.id)\n })\n\n const groupRow: Row<TData> = {\n id,\n index: index++,\n original: groupOriginal as TData,\n depth,\n subRows,\n leafCount,\n getCanExpand: () => true,\n getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),\n toggleExpanded: () => {\n table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),\n toggleSelected: () => {\n table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n getAllCells: () => [],\n // Prefer the precomputed group value (which carries aggregates)\n // and fall back to the shared-value resolver for columns without\n // a field.\n getCellValueByColumnId: (columnId: string) => {\n const col = columns.find((c) => c.id === columnId)\n const field = col?.columnDef.field\n if (field && field in groupOriginal) return groupOriginal[field]\n return resolveColumnValue(columnId)\n },\n }\n groupRows.push(groupRow)\n })\n return groupRows\n }\n\n return buildGroups(rows, 0, 0, 'group', new Map())\n }\n}\n```\n\n### `type TreeRowModelOptions`\n\nHow to read a hierarchy out of FLAT rows: each row names its parent, and the\ngrid reconstructs the tree. Rows whose parent id matches nothing become roots\nrather than disappearing.\n\nFor nested source data (`children: [...]`), flatten it first with\n{@link flattenTreeData}.\n\n```ts\nexport type TreeRowModelOptions = {\n /** Field holding each row's parent id. Rows with no parent are roots. */\n parentField: string\n /** Field holding the row's own id. Defaults to `'id'`. */\n idField?: string\n}\n```\n\n### `function createTreeRowModel`\n\nClient-side tree data: nest the grid's own flat rows into a parent/child\nhierarchy that `createExpandedRowModel` then walks.\n\nThis works on the rows the grid already built rather than on raw data, so\ntree rows keep their cells, editing, selection and formatting - they are real\ndata rows that happen to have children, not synthetic banners like grouping's.\nThat is also why the model is parent-id based: nested source arrays never\nbecome rows (the grid only builds rows for `data`), so nested input is\nflattened first with {@link flattenTreeData}. One code path, no duplicated\nrow construction.\n\nRows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable\ndata row for a full-width group banner.\n\n```ts\nexport function createTreeRowModel<TData extends RowData>(\n options: TreeRowModelOptions,\n): RowModelFactory<TData> {\n const { parentField, idField = 'id' } = options\n return ({ table, rows }) => {\n if (!rows.length) return rows\n const keyOf = (row: Row<TData>) => (row.original as any)?.[idField]\n const parentOf = (row: Row<TData>) => (row.original as any)?.[parentField]\n\n const present = new Set<unknown>()\n for (const row of rows) present.add(keyOf(row))\n\n const childrenByParent = new Map<unknown, Array<Row<TData>>>()\n const roots: Array<Row<TData>> = []\n for (const row of rows) {\n const parent = parentOf(row)\n // A row whose parent is absent (filtered out, or never existed) becomes a\n // root rather than disappearing - silently dropping rows is worse than a\n // shallower tree. Self-parenting is treated the same way.\n if (parent == null || parent === keyOf(row) || !present.has(parent)) {\n roots.push(row)\n continue\n }\n const list = childrenByParent.get(parent) ?? []\n list.push(row)\n childrenByParent.set(parent, list)\n }\n\n // Guards a cycle in the parent chain from recursing forever.\n const seen = new Set<unknown>()\n const build = (row: Row<TData>, depth: number): Row<TData> => {\n const key = keyOf(row)\n const id = row.id\n if (seen.has(key)) {\n return { ...row, depth, subRows: [], getCanExpand: () => false } as Row<TData>\n }\n seen.add(key)\n const subRows = (childrenByParent.get(key) ?? []).map((child) => build(child, depth + 1))\n return {\n ...row,\n depth,\n subRows,\n leafCount: subRows.reduce((n, sub) => n + 1 + (sub.leafCount ?? 0), 0),\n __treeRow: true,\n getCanExpand: () => subRows.length > 0,\n getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),\n toggleExpanded: () => {\n table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))\n },\n } as Row<TData>\n }\n\n return roots.map((root) => build(root, 0))\n }\n}\n```\n\n### `type FlattenTreeOptions`\n\nHow to flatten NESTED source data into the parent-id shape tree rows need.\n`parentField` is written onto each row, so point `treeData.parentField` at\nthe same name afterwards.\n\n```ts\nexport type FlattenTreeOptions = {\n /** Field holding an array of child objects. */\n childrenField: string\n /** Field holding each object's id. Defaults to `'id'`. */\n idField?: string\n /** Field to WRITE the resolved parent id onto. Defaults to `'__parentId'`. */\n parentField?: string\n}\n```\n\n### `function flattenTreeData`\n\nFlatten nested tree data into the flat parent-id shape `createTreeRowModel`\nconsumes, stamping each child with its parent's id.\n\nChildren are emitted directly after their parent so the natural order already\nmatches the rendered tree. The `childrenField` array is left on the objects\n(harmless, and callers often still want it); only the parent link is added.\n\n```ts\nexport function flattenTreeData<T extends RowData>(\n data: ReadonlyArray<T>,\n options: FlattenTreeOptions,\n): T[] {\n const { childrenField, idField = 'id', parentField = '__parentId' } = options\n const out: T[] = []\n const walk = (nodes: ReadonlyArray<T>, parentId: unknown) => {\n for (const node of nodes) {\n const flat = { ...node, [parentField]: parentId } as T\n out.push(flat)\n const kids = (node as any)[childrenField]\n if (Array.isArray(kids) && kids.length) walk(kids as ReadonlyArray<T>, (node as any)[idField])\n }\n }\n walk(data, null)\n return out\n}\n```\n\n### `function createExpandedRowModel`\n\nHides the descendants of collapsed rows. Needed for grouping, tree data and\nmaster-detail alike - all three are the same expand/collapse mechanism.\n\n```ts\nexport function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {\n return ({ table, rows }) => {\n const expanded: ExpandedState = table.getState().expanded ?? {}\n // Nothing expanded means every collapsed row hides its children and the\n // output is the input, element for element. Returning it as is keeps the\n // array identity the stages downstream and the render window key on, and\n // skips a 100k-element copy per data change on a flat grid.\n let anyExpanded = false\n for (const id in expanded) {\n if (expanded[id]) { anyExpanded = true; break }\n }\n if (!anyExpanded) return rows\n const flattened: Array<Row<TData>> = []\n const visit = (row: Row<TData>) => {\n flattened.push(row)\n if (row.subRows?.length && expanded[row.id]) {\n for (const sub of row.subRows) visit(sub)\n }\n }\n for (const row of rows) visit(row)\n return flattened\n }\n}\n```\n\n### `function replacedRowsOf`\n\nThe rows `getRowModel` replaced in the rebuild this pipeline run follows, or null.\n\n```ts\nexport function replacedRowsOf(table: SvGrid<any>): ReadonlyMap<Row<any>, Row<any>> | null {\n return replacedRowsByGrid.get(table) ?? null\n}\n```\n\n### `function createSortedRowModel`\n\nThe sorting stage of the row pipeline: orders the rows by the grid's\n`sorting` state with the built-in comparators (or the `localSortFns` map\nhanded in), stable, equal keys keeping their data order. A tick that\nreplaces a few rows out of many is repaired incrementally rather than\nre-sorted (see the note inside). Registered by `rowSortingFeature`; an app\nonly calls this to install its own comparator set.\n\n```ts\nexport function createSortedRowModel<TData extends RowData>(\n localSortFns: typeof sortFns = sortFns,\n): RowModelFactory<TData> {\n /**\n * What the last full sort produced, kept so a tick can repair it.\n *\n * A live feed replaces a few hundred row objects out of a hundred thousand\n * and hands the grid a new array. Re-sorting all of it costs the same as\n * the first sort did (115-149 ms in the browser for 100k rows, see\n * docs/help/benchmarks.md); the order it would produce differs from the\n * previous one only where the replaced rows land. So the previous output\n * is kept, with its sort keys aligned to it, and when `getRowModel` says\n * which rows were replaced (see `replacedRowsOf`) the stage drops those,\n * sorts the replacements among themselves and merges them back in one\n * linear pass: O(n + k log k) instead of O(n log n) key builds and\n * comparisons.\n *\n * The repair is only attempted when everything else is the same object as\n * last time - the sorting state, the columns, and every input row except\n * the replaced ones - and it produces exactly what the full sort would:\n * equal keys fall back to the data index, which is the order a stable sort\n * over data-ordered input keeps.\n */\n let cache: {\n input: Array<Row<TData>>\n output: Array<Row<TData>>\n sorting: SortingState\n columns: Array<Column<TData>>\n clauses: Array<SortClause>\n } | null = null\n\n type SortClause = {\n desc: boolean\n compare: (a: any, b: any) => number\n /** The key for one row, or `MISSING_KEY` when it cannot be produced incrementally. */\n keyOf: (row: Row<TData>) => any\n }\n const MISSING_KEY = Symbol('svgrid.sort.missing-key')\n\n function fullSort(rows: Array<Row<TData>>, sorting: SortingState, allColumns: Array<Column<TData>>) {\n // Resolve every clause ONCE, before sorting.\n //\n // This used to live inside the comparator, so `getAllColumns().find(...)`\n // ran per comparison per clause: a single-clause sort of 100k rows made\n // 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;\n // `pnpm bench --case=sort-1col`). The comparator is called O(n log n)\n // times, so anything inside it that is not O(1) sets the cost of the sort.\n const clauses: Array<{\n keys: Array<any>\n desc: boolean\n compare: (a: any, b: any) => number\n keyOf: (row: Row<TData>) => any\n /** Set when every key is an integer rank in [0, rankCount): the text rank path. */\n rankCount?: number\n }> = []\n\n for (const clause of sorting) {\n const column = allColumns.find((col) => col.id === clause.id)\n if (!column) continue\n // By the column's data type, not by what edits it: a number column\n // edited in a text box still sorts as numbers.\n const dataType = columnDataType(column.columnDef)\n const comparator =\n dataType === 'number'\n ? localSortFns.number\n : isDateDataType(dataType)\n ? localSortFns.date\n : localSortFns.auto\n\n // Precompute one sort key per row, so the comparator reads an array slot\n // instead of walking the row's column index on every comparison. For the\n // three built-in comparators the key is also cheaper to compare than the\n // raw value: a timestamp rather than two `new Date()` allocations, a\n // number rather than two `Number()` coercions, a collator rather than a\n // fresh one per `localeCompare` call.\n //\n // The identity checks against `sortFns` matter: `localSortFns` is a\n // public parameter, so a caller can substitute their own comparators.\n // When they have, we fall through to calling their function with the raw\n // values - still hoisted, just not specialised.\n const columnId = column.id\n const n = rows.length\n let keys: Array<any> = new Array(n)\n let compare: (a: any, b: any) => number\n let keyOf: (row: Row<TData>) => any\n let autoMode: 'number' | 'mixed' | 'text' | undefined\n let rankCount: number | undefined\n\n const sortValue = column.columnDef.sortValue\n if (typeof sortValue === 'function') {\n keyOf = (row) => numberSortKey(sortValue(row.getCellValueByColumnId(columnId)))\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (comparator === sortFns.number) {\n keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId))\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (comparator === sortFns.date) {\n keyOf = (row) => dateSortKey(row.getCellValueByColumnId(columnId))\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (comparator === sortFns.auto && (autoMode = autoSortMode(rows, columnId)) === 'number') {\n // A column with no declared type whose values are all numbers. Sorting\n // it as text put 10 before 2 (#104).\n keyOf = (row) => {\n const value = row.getCellValueByColumnId(columnId)\n // A text value arriving in a tick changes the mode; only a full sort\n // can place it.\n return typeof value === 'number' || isBlankSortValue(value)\n ? numberSortKey(value)\n : MISSING_KEY\n }\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareNumericKeys\n } else if (autoMode === 'mixed') {\n // Numbers and text in one column. Rare, so no key tricks: the raw\n // values through the pairwise comparator, which orders by kind first.\n keyOf = (row) => row.getCellValueByColumnId(columnId)\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = compareAutoValues\n } else if (comparator === sortFns.auto) {\n const strings: string[] = new Array(n)\n // Decide whether ranking is worth attempting BEFORE paying for it.\n //\n // Building the distinct set and then discarding it costs about 9 ms on\n // a 100k-row column where nearly every value is unique, and ranking\n // saves about 19 ms where they repeat - so guessing wrong in either\n // direction is measurable. A small stride sample answers it for well\n // under a millisecond.\n //\n // Strided rather than the first N rows: data arrives sorted or\n // clustered often enough that a prefix is a bad estimator of the whole\n // column. Reading every k-th row is no more expensive and does not care\n // how the rows are arranged.\n const rankLimit = n >> 1\n // Each distinct value's id (its insertion order), when ranking.\n let idOf: Map<string, number> | null = null\n if (n > 0) {\n // Ranking pays when the column has at most n/2 distinct values. A\n // small sample cannot see that by asking whether half of it repeats:\n // 1M rows of 80k values, each one 12 times over, gave 256 samples\n // with almost no repeats, so ranking was skipped and the sort made\n // 20M collator calls (1.75 s; ranked, it is a fraction of that).\n // Count the repeats in a larger sample instead. S samples from u\n // equally common values repeat about S^2 / 2u times, so u <= n/2\n // shows as at least S^2 / n repeats. Up to SAMPLE rows the whole\n // column is the sample and the test is exact.\n const SAMPLE = 2048\n const sampleTarget = Math.min(n, SAMPLE)\n const stride = Math.max(1, Math.floor(n / sampleTarget))\n const sample = new Set<string>()\n let sampled = 0\n for (let i = 0; i < n; i += stride) {\n sample.add(autoTextKey(rows[i]!.getCellValueByColumnId(columnId)))\n sampled++\n }\n const repeats = sampled - sample.size\n const worthRanking = stride === 1 ? sample.size <= rankLimit : repeats * n >= sampled * sampled\n if (worthRanking) idOf = new Map()\n }\n\n // One hash per row: the value's id is looked up (or assigned) as the\n // row is read, and the rank lookup further down is an array read by\n // id. It used to add every value to a Set and then look every value up\n // again in a rank Map, two hashes per row.\n const ids: Int32Array | null = idOf ? new Int32Array(n) : null\n for (let i = 0; i < n; i++) {\n const s = autoTextKey(rows[i]!.getCellValueByColumnId(columnId))\n strings[i] = s\n if (idOf) {\n let id = idOf.get(s)\n if (id === undefined) {\n id = idOf.size\n idOf.set(s, id)\n if (idOf.size > rankLimit) {\n idOf = null\n continue\n }\n }\n ids![i] = id\n }\n }\n\n // Collation is by far the most expensive comparison we do - a CPU\n // profile of a 100k text sort put 65% of the whole operation inside the\n // collator. But a column's DISTINCT values are usually far fewer than\n // its rows (statuses, regions, categories, owners), so rank the\n // distinct values once and sort by rank afterwards. That turns\n // O(n log n) collator calls into O(u log u), where u is the number of\n // distinct values, and the resulting order is identical because rank is\n // a monotone relabelling of the collated order - equal strings share a\n // rank, so ties still fall through to the stable sort exactly as before.\n //\n // Guarded on the uniqueness ratio: when nearly every value is distinct\n // the ranking pass cannot save any collator calls and would just add an\n // O(n) Map build, so that case keeps comparing directly.\n if (idOf) {\n // Map keys come back in insertion order, which is id order.\n const values = Array.from(idOf.keys())\n const byRank = values.map((_, id) => id).sort((a, b) => compareCollatedKeys(values[a]!, values[b]!))\n const rankById = new Int32Array(values.length)\n for (let r = 0; r < byRank.length; r++) rankById[byRank[r]!] = r\n for (let i = 0; i < n; i++) keys[i] = rankById[ids![i]!]!\n const rankIds = idOf\n rankCount = values.length\n compare = compareNumericKeys\n // A replacement whose text is not among the ranked values has no\n // rank; the repair path treats that as \"sort everything again\".\n keyOf = (row) => {\n const value = row.getCellValueByColumnId(columnId)\n // A number arriving in a tick makes the column mixed; resort fully.\n if (typeof value === 'number') return MISSING_KEY\n const id = rankIds.get(autoTextKey(value))\n return id === undefined ? MISSING_KEY : rankById[id]!\n }\n } else {\n keys = strings\n compare = compareCollatedKeys\n keyOf = (row) => {\n const value = row.getCellValueByColumnId(columnId)\n return typeof value === 'number' ? MISSING_KEY : autoTextKey(value)\n }\n }\n } else {\n keyOf = (row) => row.getCellValueByColumnId(columnId)\n for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)\n compare = comparator\n }\n\n clauses.push({ keys, desc: clause.desc, compare, keyOf, rankCount })\n }\n\n if (!clauses.length) return null\n\n // Sort an index array, then materialise. `Array.prototype.sort` is stable,\n // so equal keys keep their original relative order exactly as the previous\n // `[...rows].sort(...)` did.\n // An index array the comparator sorts in place, or a typed array from\n // countingOrder / radixOrder for a large one-column numeric sort.\n let order: ArrayLike<number> = new Array<number>(rows.length)\n for (let i = 0; i < order.length; i++) (order as number[])[i] = i\n\n // Single-clause sorts get a specialised comparator.\n //\n // Most sorts are one column, and that path runs O(n log n) times - 1.66\n // million comparisons for 100k rows. The general loop pays a clause-array\n // index, three property loads and an indirect call on every one of them,\n // none of which vary once the clause list is fixed. Hoisting them into a\n // closure and, for the numeric comparator, inlining the subtraction removes\n // the call entirely.\n //\n // `keys[ib] - keys[ia]` for descending is exactly `-(keys[ia] - keys[ib])`\n // as far as sorting is concerned: both are NaN for unorderable values,\n // which the spec coerces to 0, and they differ only in producing 0 versus\n // -0 for equal keys, which sorts identically.\n if (clauses.length === 1) {\n const { keys, compare, desc, rankCount } = clauses[0]!\n if (rankCount !== undefined && rows.length >= STABLE_NUMERIC_SORT_MIN) {\n order = countingOrder(keys as number[], rankCount, desc)\n } else if (compare === compareNumericKeys && rows.length >= STABLE_NUMERIC_SORT_MIN) {\n order = radixOrder(keys as number[], desc)\n } else if (compare === compareNumericKeys) {\n ;(order as number[]).sort(\n desc\n ? function compareOneNumericDesc(ia, ib) { return keys[ib] - keys[ia] }\n : function compareOneNumericAsc(ia, ib) { return keys[ia] - keys[ib] },\n )\n } else {\n ;(order as number[]).sort(\n desc\n ? function compareOneDesc(ia, ib) { return -compare(keys[ia], keys[ib]) }\n : function compareOneAsc(ia, ib) { return compare(keys[ia], keys[ib]) },\n )\n }\n } else {\n ;(order as number[]).sort(function compareRowsByClauses(ia, ib) {\n for (let k = 0; k < clauses.length; k++) {\n const clause = clauses[k]!\n const result = clause.compare(clause.keys[ia], clause.keys[ib])\n if (result !== 0) return clause.desc ? -result : result\n }\n return 0\n })\n }\n\n const sorted = new Array<Row<TData>>(rows.length)\n for (let i = 0; i < order.length; i++) sorted[i] = rows[order[i]!]!\n // Each row keeps its key(s), so a later tick can place a replacement\n // among the kept rows without reading any of them again. One property\n // write per row; an array per row only when there are several clauses.\n if (clauses.length === 1) {\n const keys = clauses[0]!.keys\n for (let i = 0; i < rows.length; i++) (rows[i] as any)[ROW_SORT_KEY] = keys[i]\n } else {\n for (let i = 0; i < rows.length; i++) {\n const per = new Array(clauses.length)\n for (let k = 0; k < clauses.length; k++) per[k] = clauses[k]!.keys[i]\n ;(rows[i] as any)[ROW_SORT_KEY] = per\n }\n }\n return { sorted, clauses: clauses.map(({ desc, compare, keyOf }) => ({ desc, compare, keyOf })) }\n }\n\n /**\n * The previous output with the replaced rows swapped for their\n * replacements, in sorted position. Null when the input is not the cached\n * input plus replacements, or a replacement has no incremental key.\n *\n * Every row carries the key(s) the last full sort computed for it\n * (ROW_SORT_KEY: the key itself for one clause, an array for several), so\n * the merge moves row references and nothing else. At 100k rows the loop\n * that also copied an aligned key array cost 3.7 ms; this one is the\n * reference copy alone.\n */\n function repair(rows: Array<Row<TData>>, replaced: ReadonlyMap<Row<any>, Row<any>>) {\n const c = cache!\n if (rows.length !== c.input.length) return null\n // Which rows of THIS stage's input changed. The map may name rows a\n // stage upstream filtered out; only the ones that reach here matter.\n const dropGen = ++sortDropGeneration\n const added: Array<Row<TData>> = []\n for (let i = 0; i < rows.length; i++) {\n const prev = c.input[i]!\n const next = rows[i]!\n if (next === prev) continue\n if (replaced.get(prev) !== next) return null\n ;(prev as any)[ROW_SORT_DROPPED] = dropGen\n added.push(next)\n }\n if (!added.length) return c.output\n\n const clauses = c.clauses\n const single = clauses.length === 1\n for (let j = 0; j < added.length; j++) {\n const row = added[j]! as any\n if (single) {\n const key = clauses[0]!.keyOf(row)\n if (key === MISSING_KEY) return null\n row[ROW_SORT_KEY] = key\n } else {\n const keys = new Array(clauses.length)\n for (let k = 0; k < clauses.length; k++) {\n const key = clauses[k]!.keyOf(row)\n if (key === MISSING_KEY) return null\n keys[k] = key\n }\n row[ROW_SORT_KEY] = keys\n }\n }\n\n // Keys first, then the data index: the tie-break a stable sort over\n // data-ordered input applies, so the merge lands where a full sort would.\n const prevOut = c.output\n const compareRows = single\n ? (() => {\n const { compare, desc } = clauses[0]!\n return (a: Row<TData>, b: Row<TData>) => {\n const r = compare((a as any)[ROW_SORT_KEY], (b as any)[ROW_SORT_KEY])\n if (r !== 0) return desc ? -r : r\n return a.index - b.index\n }\n })()\n : (a: Row<TData>, b: Row<TData>) => {\n const ka = (a as any)[ROW_SORT_KEY]\n const kb = (b as any)[ROW_SORT_KEY]\n for (let k = 0; k < clauses.length; k++) {\n const clause = clauses[k]!\n const r = clause.compare(ka[k], kb[k])\n if (r !== 0) return clause.desc ? -r : r\n }\n return a.index - b.index\n }\n\n // Sort the replacements among themselves (k log k)...\n added.sort(compareRows)\n\n // ...find where each one goes in the previous output by binary search\n // (k log n comparisons, against the dropped rows too - they are skipped\n // below, which does not move an insertion point)...\n const n = prevOut.length\n const insertAt = new Array<number>(added.length)\n let lo = 0\n for (let j = 0; j < added.length; j++) {\n // Monotone: the replacements are sorted, so each search starts where\n // the previous one landed.\n const row = added[j]!\n let hi = n\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (compareRows(row, prevOut[mid]!) < 0) hi = mid\n else lo = mid + 1\n }\n insertAt[j] = lo\n }\n\n // ...then one linear pass that only moves references.\n const out = new Array<Row<TData>>(rows.length)\n let w = 0\n let j = 0\n let nextInsert = insertAt[0] ?? -1\n for (let i = 0; i < n; i++) {\n while (nextInsert === i) {\n out[w++] = added[j++]!\n nextInsert = j < added.length ? insertAt[j]! : -1\n }\n const prev = prevOut[i]!\n if ((prev as any)[ROW_SORT_DROPPED] === dropGen) continue\n out[w++] = prev\n }\n while (j < added.length) out[w++] = added[j++]!\n return out\n }\n\n return function sortedRowModelStage({ table, rows }) {\n const sorting = table.getState().sorting ?? []\n if (!sorting.length) {\n cache = null\n return rows\n }\n const columns = table.getAllColumns()\n\n if (cache && cache.sorting === sorting && cache.columns === columns) {\n if (cache.input === rows) return cache.output\n const replaced = replacedRowsOf(table)\n if (replaced) {\n const repaired = repair(rows, replaced)\n if (repaired) {\n cache = { ...cache, input: rows, output: repaired }\n return repaired\n }\n }\n }\n\n const full = fullSort(rows, sorting, columns)\n if (!full) {\n cache = null\n return rows\n }\n cache = { input: rows, output: full.sorted, sorting, columns, clauses: full.clauses }\n return full.sorted\n }\n}\n```\n\n### `type SvGridOptions`\n\nEverything {@link createSvGridCore} accepts: the data and columns, the\nfeatures and row models that make up the pipeline, and an `on*Change`\ncallback per piece of state for controlled use.\n\n`<SvGrid>` builds this for you from its props - you only construct it\ndirectly when driving the headless core.\n\n```ts\nexport type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {\n _features: TFeatures\n _rowModels?: {\n coreRowModel?: RowModelFactory<TData>\n filteredRowModel?: RowModelFactory<TData>\n sortedRowModel?: RowModelFactory<TData>\n paginatedRowModel?: RowModelFactory<TData>\n groupedRowModel?: RowModelFactory<TData>\n expandedRowModel?: RowModelFactory<TData>\n }\n columns: Array<ColumnDef<TFeatures, TData>>\n data: ReadonlyArray<TData>\n /**\n * Optional row-id resolver. When set, the value it returns becomes\n * `row.id` (and therefore the selection / expansion / edit key). When\n * omitted, ids fall back to the row's array index as a string. Use a\n * stable id (database PK, UUID, etc.) so selection survives reorders.\n */\n getRowId?: (row: TData, index: number) => string\n state?: Partial<Record<string, any>>\n onSortingChange?: (updater: Updater<SortingState>) => void\n onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void\n onPaginationChange?: (updater: Updater<PaginationState>) => void\n onGroupingChange?: (updater: Updater<GroupingState>) => void\n onExpandedChange?: (updater: Updater<ExpandedState>) => void\n onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void\n onActiveCellChange?: (updater: Updater<ActiveCellState>) => void\n}\n```\n\n### `type SvGrid`\n\nThe headless grid instance: the state stores plus the read methods a renderer\nneeds (`getHeaderGroups()`, `getRowModel()`, the `set*` writers).\n\nFramework free by design - `<SvGrid>` is one renderer over this, and you can\nwrite another. See the \"Why headless?\" guide.\n\n```ts\nexport type SvGrid<TData extends RowData> = {\n store: Store<Record<string, any>>\n optionsStore: Store<Record<string, any>>\n state: Record<string, any>\n getState: () => Record<string, any>\n setOptions: (updater: Updater<Record<string, any>>) => void\n setColumnFilters: (updater: Updater<ColumnFiltersState>) => void\n setPagination: (updater: Updater<PaginationState>) => void\n setGrouping: (updater: Updater<GroupingState>) => void\n setExpanded: (updater: Updater<ExpandedState>) => void\n setRowSelection: (updater: Updater<RowSelectionState>) => void\n setActiveCell: (updater: Updater<ActiveCellState>) => void\n moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void\n getAllColumns: () => Array<Column<TData>>\n getHeaderGroups: () => Array<HeaderGroup<TData>>\n getFooterGroups: () => Array<HeaderGroup<TData>>\n getRowModel: () => RowModel<TData>\n}\n```\n\n### `function createSvGridCore`\n\nBuild a headless grid: state, the row pipeline, and the read methods, with no\nDOM and no Svelte. This is the engine `<SvGrid>` renders.\n\nMost callers want `createSvGrid` (the runes-aware wrapper) or the component\nitself; reach for this when you are writing your own renderer or running the\npipeline outside a browser.\n\n```ts\nexport function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(\n options: SvGridOptions<TFeatures, TData>,\n): SvGrid<TData> {\n const internalState: Record<string, any> = {\n sorting: [],\n columnFilters: [],\n pagination: { pageIndex: 0, pageSize: options.data.length || 10 },\n grouping: [],\n expanded: {},\n rowSelection: {},\n activeCell: { rowIndex: 0, colIndex: 0, cellId: null },\n ...(options.state ?? {}),\n }\n const store = createStore(internalState)\n const optionsStore = createStore(options as Record<string, any>)\n let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null\n let cachedColumns: Array<Column<TData>> = []\n let cachedHeaderGroups: Array<HeaderGroup<TData>> = []\n let cachedBaseRowsInput: ReadonlyArray<TData> | null = null\n let cachedBaseRowsColumns: Array<Column<TData>> | null = null\n let cachedBaseRows: Array<Row<TData>> = []\n /** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */\n let replacedRows: Map<Row<TData>, Row<TData>> | null = null\n let cachedRowCtx: BaseRowCtx<TData> | null = null\n /** This grid's windowed-data row model, made on first use from the installed hook. */\n let windowedRowModel: WindowedRowModel<TData> | null = null\n\n /** The shared row context for `columns`, reused while they are the same. */\n function rowCtxFor(columns: Array<Column<TData>>): BaseRowCtx<TData> {\n if (cachedRowCtx && cachedRowCtx.columns === columns) return cachedRowCtx\n const columnIndexById = new Map<string, number>()\n for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)\n cachedRowCtx = {\n grid: grid as SvGrid<TData>,\n store,\n columns,\n columnCount: columns.length,\n columnIndexById,\n }\n return cachedRowCtx\n }\n\n let cachedRowModel: RowModel<TData> | null = null\n let cachedRowModelBaseRows: Array<Row<TData>> | null = null\n let cachedPipeline = options._rowModels\n let cachedSlices: {\n sorting: SortingState | undefined\n columnFilters: ColumnFiltersState | undefined\n pagination: PaginationState | undefined\n grouping: GroupingState | undefined\n expanded: ExpandedState | undefined\n } | null = null\n\n const grid = {\n store,\n optionsStore,\n get state() {\n return store.state\n },\n getState() {\n return store.state\n },\n setOptions(updater: Updater<Record<string, any>>) {\n optionsStore.setState((prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : updater,\n )\n },\n setColumnFilters(updater: Updater<ColumnFiltersState>) {\n store.setState((prev) => ({\n ...prev,\n columnFilters:\n typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,\n }))\n options.onColumnFiltersChange?.(updater)\n },\n setPagination(updater: Updater<PaginationState>) {\n store.setState((prev) => ({\n ...prev,\n pagination:\n typeof updater === 'function'\n ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })\n : updater,\n }))\n options.onPaginationChange?.(updater)\n },\n setGrouping(updater: Updater<GroupingState>) {\n store.setState((prev) => ({\n ...prev,\n grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,\n }))\n options.onGroupingChange?.(updater)\n },\n setExpanded(updater: Updater<ExpandedState>) {\n store.setState((prev) => ({\n ...prev,\n expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,\n }))\n options.onExpandedChange?.(updater)\n },\n setRowSelection(updater: Updater<RowSelectionState>) {\n store.setState((prev) => ({\n ...prev,\n rowSelection:\n typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,\n }))\n options.onRowSelectionChange?.(updater)\n },\n setActiveCell(updater: Updater<ActiveCellState>) {\n store.setState((prev) => {\n const previous: ActiveCellState = prev.activeCell ?? {\n rowIndex: 0,\n colIndex: 0,\n cellId: null,\n }\n const nextActive =\n typeof updater === 'function' ? updater(previous) : updater\n return {\n ...prev,\n activeCell: nextActive,\n }\n })\n options.onActiveCellChange?.(updater)\n },\n moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {\n const rows = grid.getRowModel().rows\n const columns = grid.getAllColumns()\n const maxRow = Math.max(rows.length - 1, 0)\n const maxCol = Math.max(columns.length - 1, 0)\n const current: ActiveCellState = grid.getState().activeCell ?? {\n rowIndex: 0,\n colIndex: 0,\n cellId: null,\n }\n\n const rowIndex = Math.min(\n Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),\n maxRow,\n )\n const colIndex = Math.min(\n Math.max(current.colIndex + (next.colDelta ?? 0), 0),\n maxCol,\n )\n const columnId = columns[colIndex]?.id ?? 'col_0'\n grid.setActiveCell({\n rowIndex,\n colIndex,\n cellId: `${rowIndex}_${columnId}`,\n })\n },\n getAllColumns() {\n // Cache hit: referentially identical columns array.\n if (cachedColumnsInput === options.columns && cachedColumns.length) {\n return cachedColumns\n }\n // Soft cache hit: consumers commonly recreate the columns array\n // inline on every render (e.g. `columns={[...]}`). If the new\n // array has the same length AND each entry has the same `field` /\n // `id` / `header` (the visibility-affecting structure of a\n // column), trust the previous build. Mutable inner fields like\n // `cell` and `editorOptions` are still picked up on the next real\n // render that bumps an actual data dep - they're read at cell-\n // render time, not at this top-level cache.\n if (\n cachedColumnsInput &&\n options.columns.length === cachedColumnsInput.length &&\n cachedColumns.length === options.columns.length &&\n options.columns.every((c, i) => {\n const prev = cachedColumnsInput![i]!\n return (\n c.field === prev.field &&\n c.id === prev.id &&\n c.header === prev.header &&\n c.editorType === prev.editorType &&\n // A field's options and format live in fieldProps; a reused\n // column would keep drawing the old ones.\n c.fieldType === prev.fieldType &&\n c.fieldProps === prev.fieldProps\n )\n })\n ) {\n // Update the stored input reference so the strict check hits\n // next time, but reuse the built column model.\n cachedColumnsInput = options.columns\n return cachedColumns\n }\n\n cachedColumnsInput = options.columns\n cachedHeaderGroups = []\n const build = (\n defs: Array<ColumnDef<TFeatures, TData>>,\n depth: number,\n parentId?: string,\n ): Array<Column<TData>> => {\n const leaves: Array<Column<TData>> = []\n defs.forEach((columnDef, index) => {\n const id = resolveColumnId(columnDef, parentId, depth, index)\n if (columnDef.columns?.length) {\n leaves.push(...build(columnDef.columns, depth + 1, id))\n return\n }\n leaves.push({\n id,\n depth,\n parentId,\n columnDef,\n getCanSort: () =>\n Boolean((options._features as any).rowSortingFeature) &&\n columnDef.sortable !== false,\n getCanFilter: () =>\n Boolean((options._features as any).columnFilteringFeature) &&\n columnDef.filterable !== false,\n getIsSorted: () => {\n const entry = store.state.sorting?.find((s: any) => s.id === id)\n if (!entry) return false\n return entry.desc ? 'desc' : 'asc'\n },\n getToggleSortingHandler: () => () => {\n const clauses: SortingState = store.state.sorting ?? []\n const current = clauses.find((s: any) => s.id === id)\n const nextClause: SortingState = !current\n ? [...clauses, { id, desc: false }]\n : current.desc\n ? clauses.filter((s) => s.id !== id)\n : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))\n store.setState((prev) => ({ ...prev, sorting: nextClause }))\n options.onSortingChange?.(nextClause)\n },\n })\n })\n return leaves\n }\n cachedColumns = build(options.columns, 0)\n return cachedColumns\n },\n getHeaderGroups() {\n if (cachedHeaderGroups.length) return cachedHeaderGroups\n const headers = grid.getAllColumns().map((column) => {\n const header: Header<TData> = {\n id: column.id,\n isPlaceholder: false,\n colSpan: 1,\n column,\n getContext: () => ({ header, column, table: grid }),\n }\n return header\n })\n cachedHeaderGroups = [{ id: 'header_group_0', headers }]\n return cachedHeaderGroups\n },\n getFooterGroups() {\n return grid.getHeaderGroups()\n },\n getRowModel() {\n const columns = grid.getAllColumns()\n // Read once. `options.data` is a getter when <SvGrid> drives the engine\n // (a $state.raw signal), and the rebuild below touched it twice per row:\n // 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.\n const data = options.data\n if (installWindowedRowModel) {\n windowedRowModel ??= installWindowedRowModel<TData>({ rowCtxFor, getRowId: () => options.getRowId })\n const model = windowedRowModel(data, columns)\n if (model) return model\n }\n if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {\n // Same columns as last time: the shared context still describes them,\n // and a row whose data object sits at the same index can keep its\n // row object. A row model that streams blocks hands the grid a new\n // array of 60k entries per block where 100 changed; building 60k\n // fresh row objects each time was most of the cost of a block\n // landing. The reused row drops its memoised values and cells, since\n // an app may have changed the object in place before passing a new\n // array - that is the case the old rebuild covered by accident.\n // A lazy row list (lazyRows below) is read through its backing array,\n // so reusing rows never makes the ones nobody looked at.\n const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? (lazyRowsBacking(cachedBaseRows) ?? cachedBaseRows) : null\n const previousInput = cachedBaseRowsInput\n // Same length as last time: a replacement, not an add or remove, so\n // each new row stands in for the old row at its index and the stages\n // can repair rather than recompute. Anything else is structural.\n replacedRows = previous && previous.length === data.length ? new Map() : null\n cachedBaseRowsInput = data\n cachedBaseRowsColumns = columns\n // O(1) column-id → index lookup so getCellValueByColumnId doesn't do\n // a linear `findIndex` on every cell read (was O(rows × cells × cols)).\n let rowCtx: BaseRowCtx<TData>\n if (previous && cachedRowCtx) {\n rowCtx = cachedRowCtx\n } else {\n const columnIndexById = new Map<string, number>()\n for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)\n // One shared context for every row in this table, so a row carries a\n // pointer rather than a closure scope. See BASE_ROW_METHODS.\n rowCtx = {\n grid: grid as SvGrid<TData>,\n store,\n columns,\n columnCount: columns.length,\n columnIndexById,\n }\n cachedRowCtx = rowCtx\n }\n\n const getRowId = options.getRowId\n const m = BASE_ROW_METHODS as unknown as {\n getCanExpand: Row<TData>['getCanExpand']\n getIsExpanded: Row<TData>['getIsExpanded']\n toggleExpanded: Row<TData>['toggleExpanded']\n getIsSelected: Row<TData>['getIsSelected']\n toggleSelected: Row<TData>['toggleSelected']\n getAllCells: Row<TData>['getAllCells']\n getCellValueByColumnId: Row<TData>['getCellValueByColumnId']\n }\n // `_values` and `_cells` stay null until something reads them - a\n // 100k-row grid showing twenty rows must not materialise every row's\n // values or cell objects to paint.\n const makeRow = (index: number): BaseRowState<TData> => {\n const original = data[index]!\n return {\n id: getRowId ? getRowId(original, index) : String(index),\n index,\n original,\n depth: 0,\n [ROW_CTX]: rowCtx,\n [ROW_VALUES]: null,\n [ROW_CELLS]: null,\n [ROW_FILTER_GEN]: 0,\n [ROW_FILTER_POS]: -1,\n [ROW_SORT_DROPPED]: 0,\n [ROW_SORT_KEY]: undefined,\n getCanExpand: m.getCanExpand,\n getIsExpanded: m.getIsExpanded,\n toggleExpanded: m.toggleExpanded,\n getIsSelected: m.getIsSelected,\n toggleSelected: m.toggleSelected,\n getAllCells: m.getAllCells,\n getCellValueByColumnId: m.getCellValueByColumnId,\n }\n }\n if (!previous) {\n // A first build has nothing to reuse or repair: make each row the\n // first time something reads it. A grid that only shows the first\n // screen makes ~40 row objects, not 100k; a sort or a filter reads\n // every row and makes them then. Building them all up front was\n // most of a 100k-row mount (~24 ms, plus the garbage).\n cachedBaseRows = lazyRows(data.length, makeRow)\n } else cachedBaseRows = new Array(data.length)\n for (let index = 0; previous && index < data.length; index++) {\n const original = data[index]!\n const kept = previous[index] as BaseRowState<TData> | undefined\n if (kept && kept.original === original) {\n // Read before writing: a row nobody rendered (a million off-screen\n // placeholders under a row model) has nothing memoised, and a\n // store per row was a measurable share of each block landing.\n if (kept[ROW_VALUES] !== null) kept[ROW_VALUES] = null\n if (kept[ROW_CELLS] !== null) kept[ROW_CELLS] = null\n cachedBaseRows[index] = kept\n continue\n }\n // Past a quarter of the rows the merge stops being cheaper than a\n // sort with the ranking pass; let the stages do the full work.\n if (replacedRows && replacedRows.size * 4 >= data.length) replacedRows = null\n const row = makeRow(index)\n // The previous list was lazy and never made this row, but its data\n // object is the same one: hand the old list this row too, so a\n // caller still holding it gets the same object for the same data,\n // as when every row was built up front.\n if (kept === undefined && previousInput?.[index] === original) previous[index] = row\n cachedBaseRows[index] = row\n if (replacedRows && kept) replacedRows.set(kept, row)\n }\n // A new array with every object the same is the documented way to\n // say \"I changed rows in place, re-read them\" (docs/help/cells/\n // view-refresh.md). Nothing was replaced, so there is nothing to\n // repair around: the stages recompute from the current values. A\n // repair assumes the rows it keeps are unchanged in value as well as\n // identity, which is the immutable-update contract a tick follows.\n if (replacedRows && replacedRows.size === 0) replacedRows = null\n }\n\n // Only the slices a pipeline stage actually READS belong in this key.\n //\n // `rowSelection` used to be here, which meant ticking one checkbox on a\n // 100k-row grid re-filtered and re-sorted the entire dataset to rebuild a\n // row array that was identical by construction. Nothing reads it: the two\n // consumers are `getIsSelected` closures (on data rows and on group rows)\n // that read `store.state` when called, so they observe a selection change\n // without the model being rebuilt.\n //\n // `_rowModels` is a closed set of six named slots, so no consumer stage\n // can be inserted that might read selection. A caller CAN supply a custom\n // function for one of those slots; if one ever needs a slice that is not\n // listed here, add it here rather than reinstating all of them.\n const currentSlices = {\n sorting: store.state.sorting,\n columnFilters: store.state.columnFilters,\n pagination: store.state.pagination,\n grouping: store.state.grouping,\n expanded: store.state.expanded,\n }\n if (\n cachedRowModel &&\n cachedRowModelBaseRows === cachedBaseRows &&\n cachedPipeline === options._rowModels &&\n cachedSlices?.sorting === currentSlices.sorting &&\n cachedSlices?.columnFilters === currentSlices.columnFilters &&\n cachedSlices?.pagination === currentSlices.pagination &&\n cachedSlices?.grouping === currentSlices.grouping &&\n cachedSlices?.expanded === currentSlices.expanded\n ) {\n return cachedRowModel\n }\n\n // A stage with work to do reads every row; give it the dense array.\n const anyStageActive =\n (currentSlices.sorting?.length ?? 0) > 0 ||\n (currentSlices.columnFilters?.length ?? 0) > 0 ||\n (currentSlices.grouping?.length ?? 0) > 0 ||\n (currentSlices.expanded as unknown) === true ||\n Object.keys(currentSlices.expanded ?? {}).length > 0\n let rows: Array<Row<TData>> = anyStageActive ? denseRows(cachedBaseRows) : cachedBaseRows\n\n const pipeline = options._rowModels ?? {}\n const ordered: Array<RowModelFactory<TData> | undefined> = [\n pipeline.coreRowModel,\n pipeline.filteredRowModel,\n pipeline.sortedRowModel,\n pipeline.groupedRowModel,\n pipeline.expandedRowModel,\n pipeline.paginatedRowModel,\n ]\n // The stages read the replacements through `replacedRowsOf` while the\n // run lasts; a stage that runs outside one sees null.\n replacedRowsByGrid.set(grid, replacedRows)\n try {\n ordered.forEach((fn) => {\n if (fn) rows = fn({ table: grid, rows })\n })\n } finally {\n replacedRowsByGrid.set(grid, null)\n replacedRows = null\n }\n cachedPipeline = options._rowModels\n cachedSlices = currentSlices\n cachedRowModelBaseRows = cachedBaseRows\n cachedRowModel = { rows }\n return cachedRowModel\n },\n } as InternalGrid<TData>\n\n return grid\n}\n```\n\n### `function isFunction`\n\nNarrowing helper for the many options that accept a value or a function.\n\n```ts\nexport function isFunction(value: unknown): value is (...args: Array<any>) => any {\n return typeof value === 'function'\n}\n```\n"
7916
7916
  },
7917
7917
  {
7918
7918
  "slug": "reference/auto/svgrid-grid-createTree.svelte",
@@ -7975,7 +7975,7 @@ export const docs = [
7975
7975
  "path": "docs/reference/auto/svgrid-grid-editor-registry.md",
7976
7976
  "title": "@svgrid/grid · editor-registry.ts",
7977
7977
  "section": "API reference",
7978
- "markdown": "# `@svgrid/grid` · `editor-registry.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\editor-registry.ts`.\n\n### `type CellEditorContext`\n\nContext the grid hands a cell editor when it mounts one for an edit.\n\nExtends {@link EditorInteraction} - the shared commit / cancel / move contract\nthe `Sv*` editors are written against - and narrows the parts the grid always\nsupplies, so a registered component and a built-in editor speak the same\nlanguage. `onCommitAndMove`, `onRequestClose` and `inCell` come from the\ncontract and are always populated by the grid.\n\n```ts\nexport type CellEditorContext<T = unknown> = Required<\n Pick<EditorInteraction<T>, 'onCommit' | 'onCancel' | 'onCommitAndMove' | 'onRequestClose' | 'inCell'>\n```\n\n### `type CellEditorRegistration`\n\nHow a registered component is mounted and wired for a cell edit.\n\n```ts\nexport type CellEditorRegistration = {\n /** The Svelte component to mount in the editing cell. */\n component: Component<any>\n /**\n * Map the editing context to the component's props. Defaults to\n * {@link defaultEditorProps} (value + onChange/onCommit/onCancel).\n */\n props?: (ctx: CellEditorContext) => Record<string, unknown>\n /** Hint that the editor should open its popover immediately on mount. */\n autoOpen?: boolean\n}\n```\n\n### `function registerCellEditor`\n\nRegister a cell editor under `type`. Pass a component (uses the default prop\nmapping) or a full {@link CellEditorRegistration} for custom prop mapping.\nRe-registering the same type replaces it.\n\n```ts\nexport function registerCellEditor(\n type: string,\n registration: CellEditorRegistration | Component<any>,\n): void {\n registry.set(\n type,\n typeof registration === 'function' ? { component: registration } : registration,\n )\n}\n```\n\n### `function getCellEditor`\n\nLook up a registered cell editor by type.\n\n```ts\nexport function getCellEditor(type: string): CellEditorRegistration | undefined {\n return registry.get(type)\n}\n```\n\n### `function hasCellEditor`\n\nWhether a custom editor is registered for `type`.\n\n```ts\nexport function hasCellEditor(type: string): boolean {\n return registry.has(type)\n}\n```\n\n### `function unregisterCellEditor`\n\nRemove a registered cell editor.\n\n```ts\nexport function unregisterCellEditor(type: string): void {\n registry.delete(type)\n}\n```\n\n### `function registeredCellEditorTypes`\n\nAll registered custom editor type names.\n\n```ts\nexport function registeredCellEditorTypes(): string[] {\n return [...registry.keys()]\n}\n```\n\n### `function defaultEditorProps`\n\nThe default context → props mapping used when a registration has no `props`.\nPasses the whole {@link EditorInteraction} surface, so an editor written\nagainst the shared contract works when registered with no mapping at all.\nSvelte ignores props a component doesn't declare, so the extra keys are inert\nfor a simple editor that only wants `value` + `onCommit`.\n\n```ts\nexport function defaultEditorProps(ctx: CellEditorContext): Record<string, unknown> {\n return {\n value: ctx.value,\n onChange: ctx.onChange,\n onCommit: ctx.onCommit,\n onCancel: ctx.onCancel,\n onCommitAndMove: ctx.onCommitAndMove,\n onRequestClose: ctx.onRequestClose,\n inCell: ctx.inCell,\n }\n}\n```\n\n### `function resolveEditorProps`\n\nResolve the props to spread onto a registered editor for a given context.\n\n```ts\nexport function resolveEditorProps(\n registration: CellEditorRegistration,\n ctx: CellEditorContext,\n): Record<string, unknown> {\n return registration.props ? registration.props(ctx) : defaultEditorProps(ctx)\n}\n```\n"
7978
+ "markdown": "# `@svgrid/grid` · `editor-registry.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\editor-registry.ts`.\n\n### `type CellEditorContext`\n\nContext the grid hands a cell editor when it mounts one for an edit.\n\nExtends {@link EditorInteraction} - the shared commit / cancel / move contract\nthe `Sv*` editors are written against - and narrows the parts the grid always\nsupplies, so a registered component and a built-in editor speak the same\nlanguage. `onCommitAndMove`, `onRequestClose` and `inCell` come from the\ncontract and are always populated by the grid.\n\n```ts\nexport type CellEditorContext<T = unknown> = Required<\n Pick<EditorInteraction<T>, 'onCommit' | 'onCancel' | 'onCommitAndMove' | 'onRequestClose' | 'inCell'>\n```\n\n### `type CellEditorRegistration`\n\nHow a registered component is mounted and wired for a cell edit.\n\n```ts\nexport type CellEditorRegistration = {\n /** The Svelte component to mount in the editing cell. */\n component: Component<any>\n /**\n * Map the editing context to the component's props. Defaults to\n * {@link defaultEditorProps} (value + onChange/onCommit/onCancel).\n */\n props?: (ctx: CellEditorContext) => Record<string, unknown>\n /** Hint that the editor should open its popover immediately on mount. */\n autoOpen?: boolean\n /**\n * Whether typing on a selected cell starts this editor with the typed\n * character (`typed: true` on the context). A function decides per key,\n * e.g. digits only for a number editor. Default: every printable key.\n */\n typeToEdit?: boolean | ((char: string) => boolean)\n}\n```\n\n### `function registerCellEditor`\n\nRegister a cell editor under `type`. Pass a component (uses the default prop\nmapping) or a full {@link CellEditorRegistration} for custom prop mapping.\nRe-registering the same type replaces it.\n\n```ts\nexport function registerCellEditor(\n type: string,\n registration: CellEditorRegistration | Component<any>,\n): void {\n registry.set(\n type,\n typeof registration === 'function' ? { component: registration } : registration,\n )\n}\n```\n\n### `function getCellEditor`\n\nLook up a registered cell editor by type.\n\n```ts\nexport function getCellEditor(type: string): CellEditorRegistration | undefined {\n return registry.get(type)\n}\n```\n\n### `function hasCellEditor`\n\nWhether a custom editor is registered for `type`.\n\n```ts\nexport function hasCellEditor(type: string): boolean {\n return registry.has(type)\n}\n```\n\n### `function unregisterCellEditor`\n\nRemove a registered cell editor.\n\n```ts\nexport function unregisterCellEditor(type: string): void {\n registry.delete(type)\n}\n```\n\n### `function registeredCellEditorTypes`\n\nAll registered custom editor type names.\n\n```ts\nexport function registeredCellEditorTypes(): string[] {\n return [...registry.keys()]\n}\n```\n\n### `function defaultEditorProps`\n\nThe default context → props mapping used when a registration has no `props`.\nPasses the whole {@link EditorInteraction} surface, so an editor written\nagainst the shared contract works when registered with no mapping at all.\nSvelte ignores props a component doesn't declare, so the extra keys are inert\nfor a simple editor that only wants `value` + `onCommit`.\n\n```ts\nexport function defaultEditorProps(ctx: CellEditorContext): Record<string, unknown> {\n return {\n value: ctx.value,\n onChange: ctx.onChange,\n onCommit: ctx.onCommit,\n onCancel: ctx.onCancel,\n onCommitAndMove: ctx.onCommitAndMove,\n onRequestClose: ctx.onRequestClose,\n inCell: ctx.inCell,\n }\n}\n```\n\n### `function resolveEditorProps`\n\nResolve the props to spread onto a registered editor for a given context.\n\n```ts\nexport function resolveEditorProps(\n registration: CellEditorRegistration,\n ctx: CellEditorContext,\n): Record<string, unknown> {\n return registration.props ? registration.props(ctx) : defaultEditorProps(ctx)\n}\n```\n"
7979
7979
  },
7980
7980
  {
7981
7981
  "slug": "reference/auto/svgrid-grid-filter-operator-catalogue",
@@ -7989,14 +7989,14 @@ export const docs = [
7989
7989
  "path": "docs/reference/auto/svgrid-grid-grid-icons.md",
7990
7990
  "title": "@svgrid/grid · grid-icons.ts",
7991
7991
  "section": "API reference",
7992
- "markdown": "# `@svgrid/grid` · `grid-icons.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\grid-icons.ts`.\n\n### `type GridIconName`\n\nEvery icon `<SvGrid>` draws for its own chrome. Pass any subset of these to\nthe `icons` prop to swap the built-in glyph for your own markup.\n\n```ts\nexport type GridIconName =\n // ---- Header + column menu ----\n | 'sort'\n | 'sort-asc'\n | 'sort-desc'\n | 'filter'\n | 'menu'\n | 'group'\n | 'x'\n | 'chevron-down'\n | 'chevron-right'\n | 'autosize'\n | 'columns'\n | 'reset'\n```\n\n### `type GridIcons`\n\nConsumer icon overrides, keyed by name. Any subset; names you leave out keep\ntheir built-in glyph, so `icons` never has to be exhaustive.\n\n```ts\nexport type GridIcons = Partial<Record<GridIconName, Snippet>>\n```\n\n### `const GRID_ICON_GLYPHS`\n\nIcons whose built-in form is a character rather than an SVG path. Kept as\ndata so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise\nrender the same defaults instead of three copies drifting apart.\n\nEach is the exact character that shipped inline before, so a grid that sets\nno `icons` renders byte-for-byte what it always did.\n\n```ts\nexport const GRID_ICON_GLYPHS: Partial<Record<GridIconName, string>> = {\n close: '✕', // multiplication X\n clear: '✕',\n remove: '×', // multiplication sign\n 'row-number': '#',\n 'pinned-row-top': '↑', // upwards arrow\n 'pinned-row-bottom': '↓', // downwards arrow\n 'find-prev': '↑',\n 'find-next': '↓',\n 'move-up': '↑',\n 'move-down': '↓',\n 'group-add': '⊞', // squared plus\n 'page-first': '⇤', // leftwards arrow to bar\n 'page-prev': '‹', // single left-pointing angle quote\n 'page-next': '›', // single right-pointing angle quote\n 'page-last': '⇥', // rightwards arrow to bar\n 'page-size-caret': '▾', // black down-pointing small triangle\n 'drag-handle': '⠿', // braille pattern dots-123456, the grip\n 'breadcrumb-separator': '›',\n}\n```\n\n### `const GRID_ICON_NAMES`\n\nEvery icon name, in catalogue order. Exported for tests and docs generation;\nthe grid itself never reads it, so it costs a consumer's bundle nothing.\n\n```ts\nexport const GRID_ICON_NAMES: ReadonlyArray<GridIconName> = [\n 'sort',\n 'sort-asc',\n 'sort-desc',\n 'filter',\n 'menu',\n 'group',\n 'x',\n 'chevron-down',\n 'chevron-right',\n 'autosize',\n 'columns',\n 'reset',\n 'op-contains',\n 'op-notContains',\n 'op-equals',\n 'op-notEquals',\n 'op-startsWith',\n 'op-endsWith',\n 'op-regex',\n 'op-in',\n 'op-notIn',\n 'op-greaterThan',\n 'op-lessThan',\n 'op-between',\n 'op-isBlank',\n 'op-isNotBlank',\n 'pin-left',\n 'pin-right',\n 'unpin',\n 'search',\n 'tool-panel',\n 'chart',\n 'advanced-filter',\n 'column-group-caret',\n 'close',\n 'clear',\n 'remove',\n 'row-number',\n 'pinned-row-top',\n 'pinned-row-bottom',\n 'find-prev',\n 'find-next',\n 'move-up',\n 'move-down',\n 'group-add',\n 'page-first',\n 'page-prev',\n 'page-next',\n 'page-last',\n 'page-size-caret',\n 'drag-handle',\n 'breadcrumb-separator',\n]\n```\n"
7992
+ "markdown": "# `@svgrid/grid` · `grid-icons.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\grid-icons.ts`.\n\n### `type GridIconName`\n\nEvery icon `<SvGrid>` draws for its own chrome. Pass any subset of these to\nthe `icons` prop to swap the built-in glyph for your own markup.\n\n```ts\nexport type GridIconName =\n // ---- Header + column menu ----\n | 'sort'\n | 'sort-asc'\n | 'sort-desc'\n | 'filter'\n | 'menu'\n | 'group'\n | 'x'\n | 'chevron-down'\n | 'chevron-right'\n | 'autosize'\n | 'columns'\n | 'reset'\n```\n\n### `type GridIcons`\n\nConsumer icon overrides, keyed by name. Any subset; names you leave out keep\ntheir built-in glyph, so `icons` never has to be exhaustive.\n\n```ts\nexport type GridIcons = Partial<Record<GridIconName, Snippet>>\n```\n\n### `const GRID_ICON_GLYPHS`\n\nIcons whose built-in form is a character rather than an SVG path. Kept as\ndata so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise\nrender the same defaults instead of three copies drifting apart.\n\nEach is the exact character that shipped inline before, so a grid that sets\nno `icons` renders byte-for-byte what it always did.\n\n```ts\nexport const GRID_ICON_GLYPHS: Partial<Record<GridIconName, string>> = {\n close: '✕', // multiplication X\n clear: '✕',\n remove: '×', // multiplication sign\n 'row-number': '#',\n 'pinned-row-top': '↑', // upwards arrow\n 'pinned-row-bottom': '↓', // downwards arrow\n 'find-prev': '↑',\n 'find-next': '↓',\n 'move-up': '↑',\n 'move-down': '↓',\n 'group-add': '⊞', // squared plus\n 'page-first': '⇤', // leftwards arrow to bar\n 'page-prev': '‹', // single left-pointing angle quote\n 'page-next': '›', // single right-pointing angle quote\n 'page-last': '⇥', // rightwards arrow to bar\n 'page-size-caret': '▾', // black down-pointing small triangle\n 'drag-handle': '⠿', // braille pattern dots-123456, the grip\n 'breadcrumb-separator': '›',\n}\n```\n\n### `const GRID_ICON_NAMES`\n\nEvery icon name, in catalogue order. Exported for tests and docs generation;\nthe grid itself never reads it, so it costs a consumer's bundle nothing.\n\n```ts\nexport const GRID_ICON_NAMES: ReadonlyArray<GridIconName> = [\n 'sort',\n 'sort-asc',\n 'sort-desc',\n 'filter',\n 'menu',\n 'group',\n 'x',\n 'chevron-down',\n 'chevron-right',\n 'autosize',\n 'columns',\n 'reset',\n 'op-contains',\n 'op-notContains',\n 'op-equals',\n 'op-notEquals',\n 'op-startsWith',\n 'op-endsWith',\n 'op-regex',\n 'op-in',\n 'op-notIn',\n 'op-greaterThan',\n 'op-lessThan',\n 'op-between',\n 'op-isBlank',\n 'op-isNotBlank',\n 'pin-left',\n 'pin-right',\n 'unpin',\n 'search',\n 'tool-panel',\n 'chart',\n 'advanced-filter',\n 'column-group-caret',\n 'no-rows',\n 'no-rows-filtered',\n 'close',\n 'clear',\n 'remove',\n 'row-number',\n 'pinned-row-top',\n 'pinned-row-bottom',\n 'find-prev',\n 'find-next',\n 'move-up',\n 'move-down',\n 'group-add',\n 'page-first',\n 'page-prev',\n 'page-next',\n 'page-last',\n 'page-size-caret',\n 'drag-handle',\n 'breadcrumb-separator',\n]\n```\n"
7993
7993
  },
7994
7994
  {
7995
7995
  "slug": "reference/auto/svgrid-grid-grid-messages",
7996
7996
  "path": "docs/reference/auto/svgrid-grid-grid-messages.md",
7997
7997
  "title": "@svgrid/grid · grid-messages.ts",
7998
7998
  "section": "API reference",
7999
- "markdown": "# `@svgrid/grid` · `grid-messages.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\grid-messages.ts`.\n\n### `type GridMessages`\n\nEvery string `localization.text` accepts: the grid chrome's own\n({@link GridChromeMessages}) and the chart panel's\n({@link GridChartPanelMessages}).\n\n```ts\nexport type GridMessages = GridChromeMessages & GridChartPanelMessages\n```\n\n### `type GridChromeMessages`\n\nEvery localizable chrome string, grouped by area in the comments.\n\n```ts\nexport type GridChromeMessages = {\n // Empty / loading state\n noRows: string\n loading: string\n // Tool panel\n columns: string\n filters: string\n clearFilter: string\n // Pagination\n pageSize: string\n page: string\n of: string\n firstPage: string\n prevPage: string\n nextPage: string\n lastPage: string\n // Status bar aggregate labels\n statCount: string\n statSum: string\n statAvg: string\n statMin: string\n statMax: string\n // Grouping chrome\n group: string\n grandTotal: string\n total: string\n rowsSuffix: string\n rowSuffix: string\n // Screen-reader announcements. Unlike the labels above these are whole\n // sentences, so they take `{placeholders}` - a translator needs to control\n // word order, which assembling atoms in English order does not allow.\n announceFilterResults: string\n announceNoMatches: string\n announceFiltersCleared: string\n announceRowsSelected: string\n announceSelectionCleared: string\n // Advanced-filter toolbar chip\n advancedFilterActive: string\n advancedFilterClear: string\n // Filter operator labels (mirror filterOperatorOptions ids)\n opContains: string\n opNotContains: string\n opEquals: string\n opNotEquals: string\n opStartsWith: string\n opEndsWith: string\n opRegex: string\n opIn: string\n opNotIn: string\n opGreaterThan: string\n opLessThan: string\n opBetween: string\n opIsBlank: string\n opIsNotBlank: string\n // Date-column relabels for less/greater than\n opBefore: string\n opAfter: string\n // Context menu\n menuCopy: string\n menuCut: string\n menuPaste: string\n menuClear: string\n menuInsertRowAbove: string\n menuInsertRowBelow: string\n menuRemoveRow: string\n menuRemoveColumn: string\n menuEditComment: string\n // Selection bar\n selectionBarLabel: string\n selectionBarCount: string\n selectionBarClear: string\n selectionBarMore: string\n selectionBarSelectAll: string\n selectionBarEditFields: string\n // Bulk edit dialog\n bulkEditTitle: string\n bulkEditLead: string\n bulkEditMixed: string\n bulkEditApply: string\n bulkEditCancel: string\n bulkEditNoFields: string\n chartRange: string\n // Rows whose data has not arrived (server-side / infinite loading)\n rowLoadFailed: string\n rowLoadRetry: string\n // The expander on a client-side group row\n expandGroup: string\n collapseGroup: string\n // The chevron of the detail-toggle column (`showDetailToggle`)\n openDetail: string\n closeDetail: string\n // Upsell notes (enterprise views not installed)\n pivotUpsellTitle: string\n pivotUpsellBody: string\n ganttUpsellTitle: string\n ganttUpsellBody: string\n /** Placeholder in the Gantt view's search box. */\n ganttSearchPlaceholder: string\n /** Shared licensing line under every Enterprise upsell note. */\n upsellLicense: string\n upsellLicenseLink: string\n}\n```\n\n### `const defaultGridMessages`\n\nEnglish defaults - the literal strings the grid shipped before localization.\n\n```ts\nexport const defaultGridMessages: GridChromeMessages = {\n noRows: 'No rows to display.',\n loading: 'Loading grid data...',\n columns: 'Columns',\n filters: 'Filters',\n clearFilter: 'Clear filter',\n pageSize: 'Page Size:',\n page: 'Page',\n of: 'of',\n firstPage: 'First page',\n prevPage: 'Previous page',\n nextPage: 'Next page',\n lastPage: 'Last page',\n statCount: 'Count',\n statSum: 'Sum',\n statAvg: 'Avg',\n statMin: 'Min',\n statMax: 'Max',\n group: 'Group',\n grandTotal: 'Grand total',\n total: 'Total',\n rowsSuffix: 'rows',\n rowSuffix: 'row',\n announceFilterResults: '{visible} of {total} rows match the current filters',\n announceNoMatches: 'No rows match the current filters',\n announceFiltersCleared: 'Filters cleared, showing all {total} rows',\n announceRowsSelected: '{count} rows selected',\n announceSelectionCleared: 'Selection cleared',\n advancedFilterActive: 'Advanced filter',\n advancedFilterClear: 'Clear advanced filter',\n menuCopy: 'Copy',\n menuCut: 'Cut',\n menuPaste: 'Paste',\n menuClear: 'Clear',\n menuInsertRowAbove: 'Insert row above',\n menuInsertRowBelow: 'Insert row below',\n menuRemoveRow: 'Remove row',\n menuRemoveColumn: 'Remove column',\n menuEditComment: 'Edit comment',\n selectionBarLabel: 'Selection actions',\n /** Label beside the count CHIP, which carries the number itself. */\n selectionBarCount: 'selected',\n selectionBarClear: 'Clear selection',\n selectionBarMore: 'More actions',\n selectionBarSelectAll: 'Select all',\n selectionBarEditFields: 'Edit fields',\n bulkEditTitle: 'Edit fields',\n bulkEditLead:\n 'Changes apply to all {count} selected items. Fields you leave alone keep their existing values.',\n bulkEditMixed: 'Multiple values',\n bulkEditApply: 'Apply to {count} items',\n bulkEditCancel: 'Cancel',\n bulkEditNoFields: 'No editable fields on this grid.',\n opContains: 'Contains',\n opNotContains: 'Not contains',\n opEquals: 'Equals',\n opNotEquals: 'Not equals',\n opStartsWith: 'Starts with',\n opEndsWith: 'Ends with',\n opRegex: 'Regex',\n opIn: 'In',\n opNotIn: 'Not in',\n opGreaterThan: 'Greater than',\n opLessThan: 'Less than',\n opBetween: 'Between',\n opIsBlank: 'Blank',\n opIsNotBlank: 'Not blank',\n opBefore: 'Before',\n opAfter: 'After',\n chartRange: 'Chart selected range',\n rowLoadFailed: 'Could not load these rows.',\n rowLoadRetry: 'Retry',\n expandGroup: 'Expand group',\n collapseGroup: 'Collapse group',\n openDetail: 'Open details',\n closeDetail: 'Close details',\n pivotUpsellTitle: 'Pivot mode',\n pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',\n ganttUpsellTitle: 'Gantt view',\n ganttUpsellBody: 'The Gantt view is an Enterprise feature. Install @svgrid/enterprise and call enableGanttView() to render it.',\n ganttSearchPlaceholder: 'Search tasks...',\n upsellLicense:\n 'A license key is required for production use. Without one the feature still works, but the grid renders an unlicensed watermark.',\n upsellLicenseLink: 'View licensing',\n}\n```\n\n### `function resolveGridMessages`\n\nMerge a consumer's `localeText` over the English defaults. Undefined/empty\nvalues fall back to the default (via `resolveMessages`), so a partial map only\nreplaces the keys it sets.\n\n```ts\nexport function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {\n return resolveMessages(defaultGridMessages, overrides)\n}\n```\n"
7999
+ "markdown": "# `@svgrid/grid` · `grid-messages.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\grid-messages.ts`.\n\n### `type GridMessages`\n\nEvery string `localization.text` accepts: the grid chrome's own\n({@link GridChromeMessages}) and the chart panel's\n({@link GridChartPanelMessages}).\n\n```ts\nexport type GridMessages = GridChromeMessages & GridChartPanelMessages\n```\n\n### `type GridChromeMessages`\n\nEvery localizable chrome string, grouped by area in the comments.\n\n```ts\nexport type GridChromeMessages = {\n // Empty / loading state\n noRows: string\n /** The empty state when a filter is what emptied the grid. */\n noRowsFiltered: string\n /** The button in that empty state that clears every filter. */\n clearFilters: string\n loading: string\n // Tool panel\n columns: string\n filters: string\n clearFilter: string\n // Pagination\n pageSize: string\n page: string\n of: string\n firstPage: string\n prevPage: string\n nextPage: string\n lastPage: string\n // Status bar aggregate labels\n statCount: string\n statSum: string\n statAvg: string\n statMin: string\n statMax: string\n // Grouping chrome\n group: string\n grandTotal: string\n total: string\n rowsSuffix: string\n rowSuffix: string\n // Screen-reader announcements. Unlike the labels above these are whole\n // sentences, so they take `{placeholders}` - a translator needs to control\n // word order, which assembling atoms in English order does not allow.\n announceFilterResults: string\n announceNoMatches: string\n announceFiltersCleared: string\n announceRowsSelected: string\n announceSelectionCleared: string\n // Advanced-filter toolbar chip\n advancedFilterActive: string\n advancedFilterClear: string\n // Filter operator labels (mirror filterOperatorOptions ids)\n opContains: string\n opNotContains: string\n opEquals: string\n opNotEquals: string\n opStartsWith: string\n opEndsWith: string\n opRegex: string\n opIn: string\n opNotIn: string\n opGreaterThan: string\n opLessThan: string\n opBetween: string\n opIsBlank: string\n opIsNotBlank: string\n // Date-column relabels for less/greater than\n opBefore: string\n opAfter: string\n // Context menu\n menuCopy: string\n menuCut: string\n menuPaste: string\n menuClear: string\n menuInsertRowAbove: string\n menuInsertRowBelow: string\n menuRemoveRow: string\n menuRemoveColumn: string\n menuEditComment: string\n // Selection bar\n selectionBarLabel: string\n selectionBarCount: string\n selectionBarClear: string\n selectionBarMore: string\n selectionBarSelectAll: string\n selectionBarEditFields: string\n selectionBarDelete: string\n /** `{count}` is the number of selected rows. */\n selectionBarDeleteConfirm: string\n selectionBarCancel: string\n // Pin splitter (`pinSplitter`)\n pinSplitterLabel: string\n pinSplitterHint: string\n pinSplitterNone: string\n pinSplitterOne: string\n /** `{count}` is the number of pinned columns. */\n pinSplitterMany: string\n // Bulk edit dialog\n bulkEditTitle: string\n bulkEditLead: string\n bulkEditMixed: string\n bulkEditApply: string\n bulkEditCancel: string\n bulkEditNoFields: string\n chartRange: string\n // Rows whose data has not arrived (server-side / infinite loading)\n rowLoadFailed: string\n rowLoadRetry: string\n // The expander on a client-side group row\n expandGroup: string\n collapseGroup: string\n // The chevron of the detail-toggle column (`showDetailToggle`)\n openDetail: string\n closeDetail: string\n // Upsell notes (enterprise views not installed)\n pivotUpsellTitle: string\n pivotUpsellBody: string\n ganttUpsellTitle: string\n ganttUpsellBody: string\n /** Placeholder in the Gantt view's search box. */\n ganttSearchPlaceholder: string\n /** Shared licensing line under every Enterprise upsell note. */\n upsellLicense: string\n upsellLicenseLink: string\n}\n```\n\n### `const defaultGridMessages`\n\nEnglish defaults - the literal strings the grid shipped before localization.\n\n```ts\nexport const defaultGridMessages: GridChromeMessages = {\n noRows: 'No rows to display.',\n noRowsFiltered: 'No rows match the current filters.',\n clearFilters: 'Clear filters',\n loading: 'Loading grid data...',\n columns: 'Columns',\n filters: 'Filters',\n clearFilter: 'Clear filter',\n pageSize: 'Page Size:',\n page: 'Page',\n of: 'of',\n firstPage: 'First page',\n prevPage: 'Previous page',\n nextPage: 'Next page',\n lastPage: 'Last page',\n statCount: 'Count',\n statSum: 'Sum',\n statAvg: 'Avg',\n statMin: 'Min',\n statMax: 'Max',\n group: 'Group',\n grandTotal: 'Grand total',\n total: 'Total',\n rowsSuffix: 'rows',\n rowSuffix: 'row',\n announceFilterResults: '{visible} of {total} rows match the current filters',\n announceNoMatches: 'No rows match the current filters',\n announceFiltersCleared: 'Filters cleared, showing all {total} rows',\n announceRowsSelected: '{count} rows selected',\n announceSelectionCleared: 'Selection cleared',\n advancedFilterActive: 'Advanced filter',\n advancedFilterClear: 'Clear advanced filter',\n menuCopy: 'Copy',\n menuCut: 'Cut',\n menuPaste: 'Paste',\n menuClear: 'Clear',\n menuInsertRowAbove: 'Insert row above',\n menuInsertRowBelow: 'Insert row below',\n menuRemoveRow: 'Remove row',\n menuRemoveColumn: 'Remove column',\n menuEditComment: 'Edit comment',\n selectionBarLabel: 'Selection actions',\n /** Label beside the count CHIP, which carries the number itself. */\n selectionBarCount: 'selected',\n selectionBarClear: 'Clear selection',\n selectionBarMore: 'More actions',\n selectionBarSelectAll: 'Select all',\n selectionBarEditFields: 'Edit fields',\n selectionBarDelete: 'Delete',\n selectionBarDeleteConfirm: 'Delete {count} rows?',\n selectionBarCancel: 'Cancel',\n pinSplitterLabel: 'Pinned columns',\n pinSplitterHint: 'Drag to pin or unpin columns',\n pinSplitterNone: 'No pinned columns',\n pinSplitterOne: '1 pinned column',\n pinSplitterMany: '{count} pinned columns',\n bulkEditTitle: 'Edit fields',\n bulkEditLead:\n 'Changes apply to all {count} selected items. Fields you leave alone keep their existing values.',\n bulkEditMixed: 'Multiple values',\n bulkEditApply: 'Apply to {count} items',\n bulkEditCancel: 'Cancel',\n bulkEditNoFields: 'No editable fields on this grid.',\n opContains: 'Contains',\n opNotContains: 'Not contains',\n opEquals: 'Equals',\n opNotEquals: 'Not equals',\n opStartsWith: 'Starts with',\n opEndsWith: 'Ends with',\n opRegex: 'Regex',\n opIn: 'In',\n opNotIn: 'Not in',\n opGreaterThan: 'Greater than',\n opLessThan: 'Less than',\n opBetween: 'Between',\n opIsBlank: 'Blank',\n opIsNotBlank: 'Not blank',\n opBefore: 'Before',\n opAfter: 'After',\n chartRange: 'Chart selected range',\n rowLoadFailed: 'Could not load these rows.',\n rowLoadRetry: 'Retry',\n expandGroup: 'Expand group',\n collapseGroup: 'Collapse group',\n openDetail: 'Open details',\n closeDetail: 'Close details',\n pivotUpsellTitle: 'Pivot mode',\n pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',\n ganttUpsellTitle: 'Gantt view',\n ganttUpsellBody: 'The Gantt view is an Enterprise feature. Install @svgrid/enterprise and call enableGanttView() to render it.',\n ganttSearchPlaceholder: 'Search tasks...',\n upsellLicense:\n 'A license key is required for production use. Without one the feature still works, but the grid renders an unlicensed watermark.',\n upsellLicenseLink: 'View licensing',\n}\n```\n\n### `function resolveGridMessages`\n\nMerge a consumer's `localeText` over the English defaults. Undefined/empty\nvalues fall back to the default (via `resolveMessages`), so a partial map only\nreplaces the keys it sets.\n\n```ts\nexport function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {\n return resolveMessages(defaultGridMessages, overrides)\n}\n```\n"
8000
8000
  },
8001
8001
  {
8002
8002
  "slug": "reference/auto/svgrid-grid-list-option",
@@ -8031,7 +8031,7 @@ export const docs = [
8031
8031
  "path": "docs/reference/auto/svgrid-grid-row-predicate.md",
8032
8032
  "title": "@svgrid/grid · filtering/row-predicate.ts",
8033
8033
  "section": "API reference",
8034
- "markdown": "# `@svgrid/grid` · `filtering/row-predicate.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\filtering\\row-predicate.ts`.\n\n### `function applyRowPredicate`\n\nApply `keep` to leaf rows, dropping banners left with no surviving child.\n\n```ts\nexport function applyRowPredicate<TRow>(\n rows: ReadonlyArray<TRow>,\n keep: (row: TRow) => boolean,\n isGroup: (row: TRow) => boolean,\n): TRow[] {\n // Fast path: no grouping in play, so every row is a leaf.\n let hasGroup = false\n for (const row of rows) {\n if (isGroup(row)) {\n hasGroup = true\n break\n }\n }\n if (!hasGroup) return rows.filter(keep)\n\n // One reverse pass. `survivorsBelow` counts leaves kept since we last closed\n // out a banner, so a banner is kept when anything after it survived.\n const keepFlags = new Array<boolean>(rows.length).fill(false)\n let survivorsSinceBanner = 0\n for (let i = rows.length - 1; i >= 0; i -= 1) {\n const row = rows[i]!\n if (isGroup(row)) {\n if (survivorsSinceBanner > 0) {\n keepFlags[i] = true\n // The banner itself counts as surviving content for any banner above\n // it, so nested groups keep their ancestors.\n survivorsSinceBanner = 1\n } else {\n keepFlags[i] = false\n }\n } else if (keep(row)) {\n keepFlags[i] = true\n survivorsSinceBanner += 1\n }\n }\n\n const out: TRow[] = []\n for (let i = 0; i < rows.length; i += 1) if (keepFlags[i]) out.push(rows[i]!)\n return out\n}\n```\n"
8034
+ "markdown": "# `@svgrid/grid` · `filtering/row-predicate.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\filtering\\row-predicate.ts`.\n\n### `function applyRowPredicate`\n\nApply `keep` to leaf rows, dropping banners left with no surviving child.\n\n```ts\nexport function applyRowPredicate<TRow>(\n rows: ReadonlyArray<TRow>,\n keep: (row: TRow) => boolean,\n isGroup: (row: TRow) => boolean,\n): TRow[] {\n // Fast path: no grouping in play, so every row is a leaf.\n let hasGroup = false\n for (const row of rows) {\n if (isGroup(row)) {\n hasGroup = true\n break\n }\n }\n if (!hasGroup) return rows.filter(keep)\n\n // Any leaf under a banner, through its subRows (the banner is collapsed).\n const anyLeaf = (row: TRow): boolean =>\n ((row as Tree<TRow>).subRows ?? []).some((sub) => (isGroup(sub) ? anyLeaf(sub) : keep(sub)))\n\n // One forward pass. `open` holds the banners above the current row; a kept\n // row keeps every one of them, and only them.\n const keepFlags = new Array<boolean>(rows.length).fill(false)\n const open: number[] = []\n const keepOpen = () => {\n for (let k = open.length - 1; k >= 0 && !keepFlags[open[k]!]; k -= 1) keepFlags[open[k]!] = true\n }\n for (let i = 0; i < rows.length; i += 1) {\n const row = rows[i]!\n const depth = depthOf(row)\n while (open.length && depthOf(rows[open[open.length - 1]!]) >= depth) open.pop()\n if (isGroup(row)) {\n open.push(i)\n const next = rows[i + 1]\n const collapsed = next === undefined || depthOf(next) <= depth\n if (collapsed && anyLeaf(row)) keepOpen()\n } else if (keep(row)) {\n keepFlags[i] = true\n keepOpen()\n }\n }\n\n const out: TRow[] = []\n for (let i = 0; i < rows.length; i += 1) if (keepFlags[i]) out.push(rows[i]!)\n return out\n}\n```\n"
8035
8035
  },
8036
8036
  {
8037
8037
  "slug": "reference/auto/svgrid-grid-scheduler-ical",
@@ -8073,7 +8073,7 @@ export const docs = [
8073
8073
  "path": "docs/reference/auto/svgrid-grid-svgrid-wrapper.types.md",
8074
8074
  "title": "@svgrid/grid · svgrid-wrapper.types.ts",
8075
8075
  "section": "API reference",
8076
- "markdown": "# `@svgrid/grid` · `svgrid-wrapper.types.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\svgrid-wrapper.types.ts`.\n\n### `type SvGridFilterOperator`\n\nThe comparisons a column filter can use, as shown in the filter menu.\n\n```ts\nexport type SvGridFilterOperator = FilterOperator\n```\n\n### `type SvGridViewState`\n\nA serializable snapshot of everything that makes up the current \"view\":\nsort, grouping, pagination, column layout (width / pinning / order /\nvisibility), and all filter surfaces. Round-trippable through\n`api.getState()` / `api.setState()` - persist it to a URL, localStorage, or\na server to implement \"save view\" / \"named views\".\n\n```ts\nexport type SvGridViewState = {\n sorting: Array<{ id: string; desc: boolean }>\n grouping: string[]\n pagination: { pageIndex: number; pageSize: number }\n columnWidths: Record<string, number>\n columnPinning: { left: string[]; right: string[] }\n columnOrder: string[]\n /** Ids of columns currently hidden via setColumnVisible. */\n hiddenColumns: string[]\n globalFilter: string\n columnFilters: Record<\n string,\n { operator: SvGridFilterOperator; value: string; valueTo?: string }\n >\n /** Facet (Excel-style value checklist) selections, keyed by column id. */\n facetFilters: Record<string, string[]>\n /**\n * Advanced-filter expression (Pro). OPTIONAL, and omitted entirely when no\n * advanced filter is set - so views saved before this feature existed, and\n * views from grids that never use it, round-trip byte-identical. An explicit\n * `null` clears the filter on `setState`.\n */\n advancedFilter?: GridPredicateExpr | null\n /** Built-in chart panel state (present only when `charting` is on): the\n * ACTIVE chart, for back-compat + spread-and-tweak. */\n chart?: {\n open: boolean\n type: ChartType\n dimension: string | null\n series: string | null\n measure: string | null\n reduce: 'sum' | 'avg' | 'count'\n stacked: boolean\n }\n /** All charts in the tab strip (multi-chart), plus the active index.\n * Every field the panel can change is here so a saved view restores the\n * chart exactly; `null` inherits the `charting` config. */\n charts?: ChartTabSnapshot[]\n chartActive?: number\n /** The named charts kept with `api.saveChart`; present only when there is one. */\n savedCharts?: SavedChart[]\n}\n```\n\n### `type ChartTabSnapshot`\n\nOne chart tab as the saved view carries it (`GridState.charts[i]`).\n\n```ts\nexport type ChartTabSnapshot = {\n title: string\n type: ChartType\n dimension: string | null\n series: string | null\n measure: string | null\n /** Scatter's y measure, a range's high, a bullet's target. */\n measure2?: string | null\n reduce: ChartReducer\n stacked: boolean | null\n stacked100?: boolean | null\n orientation?: 'vertical' | 'horizontal' | null\n donut?: boolean | null\n palette?: string[] | null\n dataLabels: boolean | null\n logScale: boolean | null\n timeAxis: boolean | null\n valueFormat: 'number' | 'currency' | 'percent' | 'compact' | null\n bucket?: ChartTimeBucket | null\n bins?: number | null\n funnelShape?: 'trapezoid' | 'pyramid' | 'cone' | null\n candleStyle?: 'classic' | 'hollow' | 'heikin-ashi' | null\n /** The zoom window as category indices, or null for the whole chart. */\n zoom?: ChartZoomWindow | null\n ohlc?: { open: string | null; high: string | null; low: string | null; close: string | null; volume: string | null } | null\n indicators?: string[]\n format?: ChartFormatState | null\n /** An unlinked chart carries the spec it was frozen with. */\n frozen?: { spec: ChartSpec; at: string } | null\n}\n```\n\n### `type SavedChart`\n\nA chart configuration kept under a name (`api.saveChart`).\n\n```ts\nexport type SavedChart = {\n name: string\n /** ISO timestamp of the save. */\n savedAt: string\n /** The tab's state at the time; `applySavedChart` puts it on the active tab. */\n tab: ChartTabSnapshot\n}\n```\n\n### `type SvGridCellCoords`\n\nOne cell, in the 0-based display coordinates every selection method uses.\n\n```ts\nexport type SvGridCellCoords = { row: number; col: number }\n```\n\n### `type SvGridSelectedRange`\n\nA selected rectangle with its orientation kept: `from` is where the\nselection was started (the anchor) and `to` where it ended (the focus),\nso a range dragged upwards has `from.row > to.row`. `highlight` is the\nactive cell inside it. The same three fields as Handsontable's CellRange,\nso code written against `getSelectedRange()` there reads the same here.\n\n```ts\nexport type SvGridSelectedRange = {\n from: SvGridCellCoords\n to: SvGridCellCoords\n highlight: SvGridCellCoords\n}\n```\n\n### `type SvGridTransaction`\n\nA batch of row mutations for `api.applyTransaction`. `update` / `remove`\n(by id) match on `getRowId`; `remove` also accepts row object references.\n\n```ts\nexport type SvGridTransaction<TData> = {\n add?: ReadonlyArray<TData>\n update?: ReadonlyArray<TData>\n remove?: ReadonlyArray<TData | string>\n}\n```\n\n### `type SvGridTransactionResult`\n\nWhat a batched `applyTransaction` did: how many rows were added, updated and removed.\n\n```ts\nexport type SvGridTransactionResult = {\n added: number\n updated: number\n removed: number\n}\n```\n\n### `type SvGridApi`\n\nImperative API exposed via the `<SvGrid onApiReady>` callback. Use it for\ndata, column, filter, sort, group, and visibility operations from outside\nthe component.\n\n```ts\nexport type SvGridApi<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = {\n // ----- DOM -----\n /**\n * The grid's root element (`.sv-grid-root`), or null before mount and\n * after unmount. For measuring, focusing, or reading the theme the grid\n * is drawn with - the PDF export takes its colours from here.\n */\n getElement(): HTMLElement | null\n\n // ----- Cells -----\n /** Read a cell value from the underlying data at `rowIndex`. */\n getCellValue(rowIndex: number, columnId: string): unknown\n /** Write a cell value through the column's field. */\n setCellValue(rowIndex: number, columnId: string, value: unknown): void\n /**\n * Programmatically begin editing a cell (as a double-click would). Returns\n * `true` if editing started (cell exists, editable, editing enabled).\n */\n startEditing(rowIndex: number, columnId: string): boolean\n /**\n * Commit (default) or, with `cancel: true`, discard the active edit.\n * Returns `true` if there was an edit in progress.\n */\n stopEditing(cancel?: boolean): boolean\n\n // ----- Cell selection -----\n /**\n * Programmatically select one or more rectangular cell ranges. Each\n * range is `[rowStart, colStart, rowEnd, colEnd]` in 0-indexed grid\n * coordinates. Pass an empty array to clear the selection.\n *\n * Every range is honoured: the last one becomes the active range and\n * the others stay highlighted beside it, as after a Ctrl+drag. The\n * active cell goes to the last range's start corner.\n */\n selectCells(ranges: ReadonlyArray<readonly [number, number, number, number]>): void\n /**\n * The current cell-selection rectangles in the same shape `selectCells`\n * accepts, each normalised to `[minRow, minCol, maxRow, maxCol]`, in the\n * order they were selected with the active one last. Empty array when no\n * range is active.\n *\n * Handsontable's method of the same name. Two differences, both on\n * purpose: an empty array rather than `undefined`, so `getSelected()[0]`\n * never throws; and normalised corners, so a loop from the first row to\n * the last cannot run zero times because the range was dragged upwards.\n * For the orientation, and the active cell inside a range, use\n * `getSelectedRange()`.\n *\n * With cell selection off (`selectable={false}`) no rectangle is ever\n * recorded and this stays `[]`; the focused cell is `getActiveCell()`.\n */\n getSelected(): Array<[number, number, number, number]>\n /**\n * The most recently selected rectangle as `[startRow, startCol, endRow,\n * endCol]`, start being where the selection began, or `undefined` when\n * nothing is selected. Handsontable's `getSelectedLast()`, orientation\n * included: a range dragged upwards has `startRow > endRow`.\n */\n getSelectedLast(): [number, number, number, number] | undefined\n /**\n * Every selected rectangle with its orientation and active cell, oldest\n * first, or `undefined` when nothing is selected. Handsontable's\n * `getSelectedRange()`.\n */\n getSelectedRange(): SvGridSelectedRange[] | undefined\n /** The most recently selected rectangle, or `undefined`. Handsontable's\n * `getSelectedRangeLast()`. */\n getSelectedRangeLast(): SvGridSelectedRange | undefined\n\n // ----- Integrated charting (requires the `charting` prop) -----\n /** Open the built-in chart panel. */\n openChart(): void\n /** Close the built-in chart panel. */\n closeChart(): void\n /** The live chart spec the panel is rendering, or `null`. */\n getChartSpec(): ChartSpec | null\n /** Select cell rectangles + open the chart panel (scopes to the range). */\n chartRange(ranges?: ReadonlyArray<readonly [number, number, number, number]>): void\n /**\n * Configure the built-in chart panel's ACTIVE chart. Column references\n * accept a column id OR field name. Opens the panel unless `open === false`.\n */\n configureChart(config: {\n open?: boolean\n type?: ChartType\n dimension?: string | null\n series?: string | null\n measure?: string | null\n reduce?: ChartReducer\n stacked?: boolean\n dataLabels?: boolean\n logScale?: boolean\n timeAxis?: boolean\n valueFormat?: 'number' | 'currency' | 'percent' | 'compact'\n /** Group a date dimension by calendar unit; `null` returns to exact values. */\n bucket?: ChartTimeBucket | null\n /** Second measure: a scatter's Y, a range's high end, a bullet's target. */\n measure2?: string | null\n /** Histogram bin count; `null` for Sturges' rule. */\n bins?: number | null\n funnelShape?: 'trapezoid' | 'pyramid' | 'cone' | null\n candleStyle?: 'classic' | 'hollow' | 'heikin-ashi' | null\n /** Candlestick / OHLC: the price columns (ids or fields); `null` goes\n * back to guessing them from the column names. */\n ohlc?: { open?: string | null; high?: string | null; low?: string | null; close?: string | null; volume?: string | null } | null\n /** Indicator panes under a price chart (`volume`, `rsi`, `macd`,\n * `stochastic`, `atr`, `obv`) and overlays on it (`sma`, `ema`,\n * `bb`, `vwap`). */\n indicators?: Array<'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'>\n /** The builder's format state (titles, axes, legend, per-series style);\n * `null` resets it. */\n format?: ChartFormatState | null\n /** `true` unlinks the chart from the grid (it keeps the spec it has),\n * `false` links it back. */\n frozen?: boolean\n /** Apply a saved chart (by name) to the active tab before the other keys. */\n saved?: string\n }): void\n /**\n * Keep the active chart's whole configuration (type, columns, aggregate,\n * switches, indicators, format, zoom) under a name. A same-named entry is\n * replaced. Saved charts travel with `getState()` / `setState()` and show\n * in the panel's Saved charts popover.\n */\n saveChart(name: string): void\n /** Apply a saved chart to the active tab, which keeps its title. `false` when no chart has that name. */\n applySavedChart(name: string): boolean\n removeSavedChart(name: string): void\n getSavedCharts(): SavedChart[]\n /**\n * Register a natural-language \"chart this\" handler. When set, the chart\n * panel shows an AI button. `@svgrid/enterprise`'s `enableAiCharting(api)`\n * fills this. Pass `null` to remove it.\n */\n setChartAiHandler(\n handler: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,\n ): void\n /**\n * Register an \"explain this chart\" handler. When set, the chart panel's AI\n * row shows an Explain button that reads the active chart and shows the\n * summary and insights it returns. `enableAiCharting(api)` fills this with\n * `aiExplainChart`. Pass `null` to remove it.\n */\n setChartExplainHandler(handler: (() => Promise<{ summary: string; insights: string[] } | null>) | null): void\n\n // ----- Rows -----\n /** Add one row. `position` defaults to `'bottom'`. */\n addRow(row: TData, position?: 'top' | 'bottom' | number): void\n addRows(rows: ReadonlyArray<TData>, position?: 'top' | 'bottom' | number): void\n /** Remove a row at the given data-array index. */\n removeRow(rowIndex: number): void\n removeRows(rowIndices: ReadonlyArray<number>): void\n /**\n * Apply a batch of add / update / remove mutations in a SINGLE data update\n * (one re-render, not one per row) - the high-frequency / streaming path.\n * `update` and `remove`-by-id match rows via `getRowId`, so set that prop\n * for those to work; `remove` also accepts row object references. Returns\n * the counts actually applied.\n */\n applyTransaction(tx: SvGridTransaction<TData>): SvGridTransactionResult\n\n // ----- Columns -----\n /** Add one column. `position` defaults to `'right'`. */\n addColumn(\n column: ColumnDef<TFeatures, TData>,\n position?: 'left' | 'right' | number,\n ): void\n addColumns(\n columns: ReadonlyArray<ColumnDef<TFeatures, TData>>,\n position?: 'left' | 'right' | number,\n ): void\n /** Remove a column by id (or field when no id was provided). */\n removeColumn(columnId: string): void\n\n // ----- Visibility -----\n setColumnVisible(columnId: string, visible: boolean): void\n isColumnVisible(columnId: string): boolean\n\n // ----- Sort / group / filter -----\n /** Sort by one column (replaces any existing sort). Pass `null` to clear. */\n setSort(columnId: string, direction: 'asc' | 'desc' | null): void\n clearSort(): void\n setGroupBy(columnIds: ReadonlyArray<string>): void\n\n // ----- Options (runtime prop overrides) -----\n /**\n * Change a grid prop at runtime, e.g. `api.setOption('sortable', true)`. The\n * override is merged over the incoming prop and the grid re-renders reactively -\n * identical to the parent passing a new prop value. Pass `undefined` to clear the\n * override and fall back to the prop.\n *\n * Note: seed-once props are NOT retroactive - `initialSorting`,\n * `initialColumnPinning`, `initialHiddenColumns`, `columnOrder`, the initial\n * `pageSize`, and the one-shot `externalSort` / `externalFilter` are read at mount,\n * so overriding them later has no effect. Prefer the dedicated data path for\n * `data` / `columns` (this works, but the sync effects are the idiomatic route).\n */\n setOption<K extends keyof Props<TFeatures, TData>>(key: K, value: Props<TFeatures, TData>[K] | undefined): void\n /** Read a prop's effective value: the runtime override if set, else the incoming prop. */\n getOption<K extends keyof Props<TFeatures, TData>>(key: K): Props<TFeatures, TData>[K]\n /** Clear every runtime override set via `setOption`, reverting to the incoming props. */\n resetOptions(): void\n\n /** Set the operator filter for a column. Pass `null` to clear. */\n setFilter(\n columnId: string,\n filter:\n | {\n operator: SvGridFilterOperator\n value?: string\n /** Upper bound for the `between` operator. Required when `operator === 'between'`. */\n valueTo?: string\n /**\n * Optional SECOND condition on the same column, joined by `join`\n * (multi-condition filtering, e.g. \"> 100 AND < 500\").\n */\n operator2?: SvGridFilterOperator\n value2?: string\n valueTo2?: string\n /** How to combine the two conditions. Defaults to `'AND'`. */\n join?: 'AND' | 'OR'\n }\n | null,\n ): void\n /**\n * Set the facet (set-list, Excel-style multi-select) filter for a column.\n * Pass an empty array or `null` to clear it. The values restore the\n * checked state of the column-menu's value list - the engine then filters\n * the data to rows whose cell value is in the set. Used to restore\n * snapshots captured via `onFiltersChange`'s `selectedValues`.\n */\n setFacetFilter(columnId: string, values: ReadonlyArray<string> | null): void\n clearFilter(columnId: string): void\n /**\n * Drop cached async `editorOptions` so the next render refetches them.\n * Omit `columnId` to clear every column. Only affects columns whose\n * `editorOptions` returns a Promise - static lists are never refetched.\n */\n refreshEditorOptions(columnId?: string): void\n /**\n * Open the `contextMenu` at the pointer, for the cell at (rowIndex,\n * colIndex), from an element of your own: a spreadsheet's column letters\n * and row numbers offer the cell menu for the column or row they select.\n * Does nothing without a `contextMenu` prop.\n */\n openContextMenu(event: MouseEvent, rowIndex: number, colIndex: number): void\n /**\n * Clear every active column filter (menu, filter-row, set-list, global, and\n * the advanced filter). Resets the grid to \"no filtering\" in a single call.\n */\n clearAllFilters(): void\n /**\n * Set the advanced-filter expression (Pro). `null` clears it. Composed with\n * AND after the global, column and facet filters.\n *\n * Rows are only removed once `@svgrid/enterprise`'s `enableAdvancedFilter()`\n * has registered a compiler. Without it the expression is stored but nothing\n * is filtered - use `isAdvancedFilterActive()` to tell the two apart.\n */\n setAdvancedFilter(expr: GridPredicateExpr | null): void\n /** The current advanced-filter expression, or `null`. */\n getAdvancedFilter(): GridPredicateExpr | null\n /** Clear the advanced filter, leaving other filter surfaces untouched. */\n clearAdvancedFilter(): void\n /** Whether an expression is set AND an engine is registered to run it. */\n isAdvancedFilterActive(): boolean\n /**\n * Read the active column-menu filters as a snapshot. Keyed by column id.\n * Returns an empty object when nothing is filtered. `valueTo` is only\n * present when `operator === 'between'`.\n */\n getFilters(): Record<\n string,\n { operator: SvGridFilterOperator; value: string; valueTo?: string }\n >\n\n /**\n * Snapshot of the DATA rows the grid is displaying right now - after\n * filtering, sorting, grouping and pagination. Use this when you need the\n * visible result set (e.g. to export it as CSV).\n *\n * Group banner rows are not included: the return type is `TData`, and a\n * banner is not one of your rows. That matters while grouping is on, because\n * with every group collapsed this returns an EMPTY array even though the\n * grid visibly shows a banner per group. Count banners from `getState()`\n * rather than from the length of this.\n */\n getDisplayedRows(): ReadonlyArray<TData>\n\n /** Snapshot of the current data array (pre-pipeline). */\n getData(): ReadonlyArray<TData>\n\n /**\n * The stable id the grid keys a row by: what the `getRowId` prop\n * computed, or the row's position when no `getRowId` is set. Pass a\n * display index (the space `getActiveCell`, `scrollToRow` and the cell\n * methods use) or a row object; returns null for an unknown index or a\n * row that is not in the data. Pair with `selectRows`, `toggleRowSelected`\n * or an `applyTransaction` remove, all of which take these ids.\n */\n getRowId(row: number | TData): string | null\n\n /**\n * Snapshot of every column the grid currently knows about, in visual\n * order, with the human-readable header label. Use this when exporting\n * or building a column-picker UI - the data is read once, no\n * subscription. Hidden columns are included; check `visible` to filter.\n */\n getColumns(): ReadonlyArray<{\n id: string\n field?: string\n header: string\n visible: boolean\n /** The column's `format` config, when set. Lets an exporter reproduce\n * the on-screen display value (currency, date pattern, etc.). */\n format?: CellFormatConfig\n /** Effective horizontal alignment ('left' | 'center' | 'right'). */\n align?: 'left' | 'center' | 'right'\n /** The column's declared `editorType`, when set. Lets a filter or\n * expression UI offer type-appropriate operators. */\n editorType?: string\n }>\n\n // ----- Free data export (CSV / TSV / JSON + clipboard) -----\n /**\n * Export the grid to a **CSV** file. Free in the community grid; the\n * richer Excel / PDF / styled formats live in @svgrid/enterprise. Values\n * are formatted as shown on screen (pass `rawValues: true` for raw). Rows\n * default to the current view (`rows: 'selected' | 'all'` to change).\n * Resolves with the serialized text; pass `download: false` to skip the\n * browser download and just get the string.\n */\n exportCsv(options?: GridExportOptions): Promise<string>\n /** Export the grid to a **TSV** file (tab-separated). See `exportCsv`. */\n exportTsv(options?: GridExportOptions): Promise<string>\n /** Export the grid to a **JSON** file (array of `{ field: value }`). */\n exportJson(options?: GridExportOptions): Promise<string>\n /**\n * Copy the grid to the system clipboard. `format: 'tsv'` (default) pastes\n * straight into Excel / Sheets; `'csv'` / `'markdown'` also supported.\n * Resolves with the copied text.\n */\n copyToClipboard(options?: GridClipboardOptions): Promise<string>\n\n /** Clear every checked row. Emits `onRowSelectionChange({}, [])`. */\n clearRowSelection(): void\n\n // ----- Column layout (width + pinning) -----\n /**\n * Set the width of one column in pixels. Identical to dragging the\n * column's resize handle. Width is clamped to `MIN_COLUMN_WIDTH`.\n */\n setColumnWidth(columnId: string, width: number): void\n /**\n * Snapshot of every column's current width (in pixels), keyed by\n * column id. Columns the user has never resized AND that have no\n * explicit `width` on their ColumnDef are reported at the grid-wide\n * default. Useful for \"save view\" + URL persistence.\n */\n getColumnWidths(): Record<string, number>\n /**\n * Snap one column's width to its widest visible cell (header text +\n * any rendered body cell). Equivalent to double-clicking the column's\n * resize handle. The grid also exposes this through the column menu's\n * \"Autosize\" item.\n */\n autosizeColumn(columnId: string): void\n /**\n * The height of a row in px: the one it was dragged or set to, else the\n * declared `rowHeight` (30 when none). `rowIndex` is a display index.\n */\n getRowHeight(rowIndex: number): number\n /**\n * Give a row its own height, the way dragging its grip does with\n * `rowResize`; `null` takes it back to the declared height. The height\n * belongs to the row (its id), so it follows the row through a sort and\n * survives the data being replaced. Ignored under `autoRowHeight`, where\n * the content decides.\n */\n setRowHeight(rowIndex: number, height: number | null): void\n /**\n * Fold a column to nothing, the way a spreadsheet hides one: it keeps\n * its index, its cells and its width, and takes no room until it is\n * unfolded. Unlike `setColumnVisible(id, false)`, which takes the\n * column out of the model and shifts every index after it, references\n * by index stay valid, so a sheet's formulas and formats are untouched.\n * Arrow keys, Tab and Enter step over a collapsed column.\n */\n setColumnCollapsed(columnId: string, collapsed: boolean): void\n isColumnCollapsed(columnId: string): boolean\n /**\n * Fold a row to nothing, the row-side twin of `setColumnCollapsed`.\n * `rowIndex` is a display index; the fold belongs to the row (its id)\n * and follows it through a sort.\n */\n setRowCollapsed(rowIndex: number, collapsed: boolean): void\n isRowCollapsed(rowIndex: number): boolean\n /** The `mergedCells` in force, copied: origins with their spans. */\n getMergedCells(): Array<{ rowIndex: number; colIndex: number; rowSpan: number; colSpan: number }>\n /** Run `autosizeColumn` on every column. */\n autosizeAllColumns(): void\n /**\n * Replace the column-pinning state in one call. Each entry is a\n * column id; the order in the array becomes the visible order along\n * the pinned edge.\n */\n setColumnPinning(pinning: {\n left?: ReadonlyArray<string>\n right?: ReadonlyArray<string>\n }): void\n /** Snapshot of the current column-pinning state. */\n getColumnPinning(): { left: string[]; right: string[] }\n\n // ----- Column reorder -----\n /**\n * Replace the column order. Pass an array of column ids in the\n * desired visual order. Unknown ids are skipped; columns not in the\n * array keep their existing relative position after the listed ones.\n * Fires `onColumnOrderChange` once the new order is applied. Pin\n * groups (`columnPinning.left` / `right`) are still applied on top.\n */\n setColumnOrder(order: ReadonlyArray<string>): void\n /**\n * Snapshot of the current visual column order - the same shape the\n * `onColumnOrderChange` callback receives. Useful for saving and\n * restoring a view layout.\n */\n getColumnOrder(): string[]\n\n // ----- Row expansion -----\n /**\n * Set whether a row (group node or expandable leaf) is expanded.\n * The `id` is the engine's row id - for grouped rows that's the\n * synthetic group key, `group_<field>_<value>` (e.g.\n * `\"group_department_Engineering\"`; a second level appends\n * `_<field>_<value>`). An id that matches no row is ignored silently.\n */\n setRowExpanded(id: string, expanded: boolean): void\n /** Expand every group node in the current grouped row model. */\n expandAllGroups(): void\n /** Collapse every expansion - resets expanded state to {}. */\n collapseAllGroups(): void\n\n // ----- Undo / redo -----\n /** Undo the most recent inline-edit. Returns false when the history is empty. */\n undo(): boolean\n /** Redo the most recently undone edit. Returns false when the redo stack is empty. */\n redo(): boolean\n /** True when there's at least one step on the undo stack. */\n canUndo(): boolean\n /** True when there's at least one step on the redo stack. */\n canRedo(): boolean\n /** Wipe both stacks (e.g. after a server save commits the buffer). */\n clearHistory(): void\n /**\n * Mark every step recorded from now on with `tag`, until the next call;\n * `undefined` stops the marking. The grid never reads the tag. It is for a\n * consumer that shows one of several data sets through the grid (a\n * workbook's sheets, say) and has to know which one the next Ctrl+Z would\n * change before it lets the grid apply it: it tags the steps with the data\n * set they were made on and asks `peekUndo` first.\n */\n setHistoryTag(tag: unknown): void\n /** The step the next `undo()` would apply, or null when there is none. A\n * multi-cell action carries one tag, so it reports as one. */\n peekUndo(): { tag: unknown } | null\n /** The step the next `redo()` would apply, or null when there is none. */\n peekRedo(): { tag: unknown } | null\n\n // ----- Keyboard commands -----\n /**\n * The same `GridCommandContext` a registered shortcut handler receives.\n *\n * Keyboard commands are handed one on every keystroke, but a button in a\n * ribbon or a toolbar has no keystroke to ride in on, and re-implementing\n * \"bold the selection\" against the api would give the button and the key\n * two code paths that drift. Both go through this instead.\n *\n * Live, not a snapshot: the object reads through to the grid, so one built\n * once at mount still reports the current selection.\n */\n getCommandContext(): GridCommandContext\n\n // ----- Find in grid -----\n /** Open the built-in find overlay (Ctrl+F also opens it). */\n openFind(): void\n /** Close the find overlay and clear the query. */\n closeFind(): void\n /** Update the find query programmatically (useful for app-wide command palettes). */\n setFindQuery(q: string): void\n /** Snapshot of the current find hits (rowIndex / colIndex / columnId). */\n getFindHits(): Array<{ rowIndex: number; colIndex: number; columnId: string }>\n\n // ----- Row selection (read + write) -----\n /**\n * The currently selected data rows (group-header rows excluded), in row-model\n * order. Read once - no subscription. The push-based equivalent is\n * `onRowSelectionChange`.\n */\n getSelectedRows(): TData[]\n /** The engine row ids of the selected rows. Keys into the selection record. */\n getSelectedRowIds(): string[]\n /**\n * Select rows by engine row id. By default this REPLACES the selection;\n * pass `additive: true` to add to the existing selection instead.\n */\n selectRows(ids: ReadonlyArray<string>, additive?: boolean): void\n /** Select every selectable (non-group) row in the current row model. */\n selectAllRows(): void\n /** Flip one row's selected state by id. */\n toggleRowSelected(id: string): void\n\n // ----- Pagination -----\n /**\n * Current pagination snapshot. `total` is the post-filter row count;\n * `pageCount` is derived from it and `pageSize` (always >= 1).\n */\n getPageInfo(): {\n pageIndex: number\n pageSize: number\n pageCount: number\n total: number\n }\n /** Jump to a 0-based page. Clamped to [0, pageCount - 1]. */\n setPage(pageIndex: number): void\n /** Advance one page (no-op past the last page). */\n nextPage(): void\n /** Go back one page (no-op before the first page). */\n prevPage(): void\n /** Jump to the first page. */\n firstPage(): void\n /** Jump to the last page. */\n lastPage(): void\n /** Change the page size, keeping the first visible row in view. */\n setPageSize(pageSize: number): void\n\n // ----- Navigation / scrolling -----\n /**\n * Scroll the body so the given row index is at the top of the viewport.\n * Works with virtualization on. Index is clamped to the row count.\n */\n scrollToRow(rowIndex: number): void\n /** The active (focused) cell, or null when nothing is focused. */\n getActiveCell(): { rowIndex: number; colIndex: number; columnId: string } | null\n /** Move the active cell. Both coordinates are clamped to the grid bounds. */\n setActiveCell(rowIndex: number, colIndex: number): void\n\n // ----- View state (save / restore) -----\n /**\n * Serializable snapshot of the whole view - sort, grouping, pagination,\n * column layout, and every filter surface. Pair with `setState` for\n * \"save view\" / URL persistence / named views.\n */\n getState(): SvGridViewState\n /**\n * Restore a view from a (partial) snapshot produced by `getState`. Only the\n * keys present are applied, so you can restore just the columns, just the\n * filters, etc.\n */\n setState(state: Partial<SvGridViewState>): void\n /** Force a recompute of the row pipeline + a re-render. */\n refresh(): void\n}\n```\n\n### `type SvGridWrapperProps`\n\nThe props `<SvGrid>` accepts. See the SvGrid reference for the full list with defaults.\n\n```ts\nexport type SvGridWrapperProps<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = {\n data: ReadonlyArray<TData>\n columns: Array<ColumnDef<TFeatures, TData>>\n /**\n * Feature set from `tableFeatures({ ... })`. Optional - the `sortable` /\n * `filterable` / `groupable` shortcuts inject the matching feature, so a\n * grid can be configured from the boolean shortcuts alone.\n */\n features?: TFeatures\n /**\n * Capability shortcuts. Every capability is OFF by default (a bare grid is\n * a plain read-only table); set a shortcut `true` to opt in.\n *\n * `sortable` - column sorting (injects `rowSortingFeature`)\n * `filterable` - column filtering (injects `columnFilteringFeature`)\n * `editable` - inline editing (alias of `enableInlineEditing`)\n * `groupable` - grouping controls (alias of `showGroupingControls`)\n * `pageable` - pagination footer (alias of `showPagination`)\n */\n sortable?: boolean\n filterable?: boolean\n editable?: boolean\n groupable?: boolean\n pageable?: boolean\n options?: Partial<SvGridOptions<TFeatures, TData>>\n loading?: boolean\n error?: string | null\n emptyMessage?: string\n showGlobalFilter?: boolean\n showColumnFilters?: boolean\n showGroupingControls?: boolean\n showRowSelection?: boolean\n showPagination?: boolean\n virtualization?: boolean\n /** Row height in pixels. Pass a function `(rowIndex) => px` for per-row\n * variable heights (e.g. when wiring up an interactive row-resize). */\n rowHeight?: number | ((rowIndex: number) => number)\n overscan?: number\n containerHeight?: number\n columnVirtualization?: boolean\n columnOverscan?: number\n columnWidth?: number\n showFilterMenu?: boolean\n showFilterRow?: boolean\n enableCellSelection?: boolean\n enableInlineEditing?: boolean\n enableRowSummaries?: boolean\n /** Shortcut alias for `enableRowSummaries`; wins when both are set. */\n summary?: boolean\n /** Receives the imperative grid API when the component is ready. */\n onApiReady?: (api: SvGridApi<TFeatures, TData>) => void\n}\n```\n"
8076
+ "markdown": "# `@svgrid/grid` · `svgrid-wrapper.types.ts`\n\nAuto-generated. Source: `packages\\grid\\src\\svgrid-wrapper.types.ts`.\n\n### `type SvGridFilterOperator`\n\nThe comparisons a column filter can use, as shown in the filter menu.\n\n```ts\nexport type SvGridFilterOperator = FilterOperator\n```\n\n### `type SvGridViewState`\n\nA serializable snapshot of everything that makes up the current \"view\":\nsort, grouping, pagination, column layout (width / pinning / order /\nvisibility), and all filter surfaces. Round-trippable through\n`api.getState()` / `api.setState()` - persist it to a URL, localStorage, or\na server to implement \"save view\" / \"named views\".\n\n```ts\nexport type SvGridViewState = {\n sorting: Array<{ id: string; desc: boolean }>\n grouping: string[]\n pagination: { pageIndex: number; pageSize: number }\n columnWidths: Record<string, number>\n columnPinning: { left: string[]; right: string[] }\n columnOrder: string[]\n /** Ids of columns currently hidden via setColumnVisible. */\n hiddenColumns: string[]\n globalFilter: string\n columnFilters: Record<\n string,\n { operator: SvGridFilterOperator; value: string; valueTo?: string }\n >\n /** Facet (Excel-style value checklist) selections, keyed by column id. */\n facetFilters: Record<string, string[]>\n /**\n * Advanced-filter expression (Pro). OPTIONAL, and omitted entirely when no\n * advanced filter is set - so views saved before this feature existed, and\n * views from grids that never use it, round-trip byte-identical. An explicit\n * `null` clears the filter on `setState`.\n */\n advancedFilter?: GridPredicateExpr | null\n /** Built-in chart panel state (present only when `charting` is on): the\n * ACTIVE chart, for back-compat + spread-and-tweak. */\n chart?: {\n open: boolean\n type: ChartType\n dimension: string | null\n series: string | null\n measure: string | null\n reduce: 'sum' | 'avg' | 'count'\n stacked: boolean\n }\n /** All charts in the tab strip (multi-chart), plus the active index.\n * Every field the panel can change is here so a saved view restores the\n * chart exactly; `null` inherits the `charting` config. */\n charts?: ChartTabSnapshot[]\n chartActive?: number\n /** The named charts kept with `api.saveChart`; present only when there is one. */\n savedCharts?: SavedChart[]\n}\n```\n\n### `type ChartTabSnapshot`\n\nOne chart tab as the saved view carries it (`GridState.charts[i]`).\n\n```ts\nexport type ChartTabSnapshot = {\n title: string\n type: ChartType\n dimension: string | null\n series: string | null\n measure: string | null\n /** Scatter's y measure, a range's high, a bullet's target. */\n measure2?: string | null\n reduce: ChartReducer\n stacked: boolean | null\n stacked100?: boolean | null\n orientation?: 'vertical' | 'horizontal' | null\n donut?: boolean | null\n palette?: string[] | null\n dataLabels: boolean | null\n logScale: boolean | null\n timeAxis: boolean | null\n valueFormat: 'number' | 'currency' | 'percent' | 'compact' | null\n bucket?: ChartTimeBucket | null\n bins?: number | null\n funnelShape?: 'trapezoid' | 'pyramid' | 'cone' | null\n candleStyle?: 'classic' | 'hollow' | 'heikin-ashi' | null\n /** The zoom window as category indices, or null for the whole chart. */\n zoom?: ChartZoomWindow | null\n ohlc?: { open: string | null; high: string | null; low: string | null; close: string | null; volume: string | null } | null\n indicators?: string[]\n format?: ChartFormatState | null\n /** An unlinked chart carries the spec it was frozen with. */\n frozen?: { spec: ChartSpec; at: string } | null\n}\n```\n\n### `type SavedChart`\n\nA chart configuration kept under a name (`api.saveChart`).\n\n```ts\nexport type SavedChart = {\n name: string\n /** ISO timestamp of the save. */\n savedAt: string\n /** The tab's state at the time; `applySavedChart` puts it on the active tab. */\n tab: ChartTabSnapshot\n}\n```\n\n### `type SvGridCellCoords`\n\nOne cell, in the 0-based display coordinates every selection method uses.\n\n```ts\nexport type SvGridCellCoords = { row: number; col: number }\n```\n\n### `type SvGridSelectedRange`\n\nA selected rectangle with its orientation kept: `from` is where the\nselection was started (the anchor) and `to` where it ended (the focus),\nso a range dragged upwards has `from.row > to.row`. `highlight` is the\nactive cell inside it. The same three fields as Handsontable's CellRange,\nso code written against `getSelectedRange()` there reads the same here.\n\n```ts\nexport type SvGridSelectedRange = {\n from: SvGridCellCoords\n to: SvGridCellCoords\n highlight: SvGridCellCoords\n}\n```\n\n### `type SvGridTransaction`\n\nA batch of row mutations for `api.applyTransaction`. `update` / `remove`\n(by id) match on `getRowId`; `remove` also accepts row object references.\n\n```ts\nexport type SvGridTransaction<TData> = {\n add?: ReadonlyArray<TData>\n update?: ReadonlyArray<TData>\n remove?: ReadonlyArray<TData | string>\n}\n```\n\n### `type SvGridTransactionResult`\n\nWhat a batched `applyTransaction` did: how many rows were added, updated and removed.\n\n```ts\nexport type SvGridTransactionResult = {\n added: number\n updated: number\n removed: number\n}\n```\n\n### `type SvGridApi`\n\nImperative API exposed via the `<SvGrid onApiReady>` callback. Use it for\ndata, column, filter, sort, group, and visibility operations from outside\nthe component.\n\n```ts\nexport type SvGridApi<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = {\n // ----- DOM -----\n /**\n * The grid's root element (`.sv-grid-root`), or null before mount and\n * after unmount. For measuring, focusing, or reading the theme the grid\n * is drawn with - the PDF export takes its colours from here.\n */\n getElement(): HTMLElement | null\n\n // ----- Cells -----\n /** Read a cell value from the underlying data at `rowIndex`. */\n getCellValue(rowIndex: number, columnId: string): unknown\n /** Write a cell value through the column's field. */\n setCellValue(rowIndex: number, columnId: string, value: unknown): void\n /**\n * Programmatically begin editing a cell (as a double-click would). Returns\n * `true` if editing started (cell exists, editable, editing enabled).\n */\n startEditing(rowIndex: number, columnId: string): boolean\n /**\n * Commit (default) or, with `cancel: true`, discard the active edit.\n * Returns `true` if there was an edit in progress.\n */\n stopEditing(cancel?: boolean): boolean\n\n // ----- Cell selection -----\n /**\n * Programmatically select one or more rectangular cell ranges. Each\n * range is `[rowStart, colStart, rowEnd, colEnd]` in 0-indexed grid\n * coordinates. Pass an empty array to clear the selection.\n *\n * Every range is honoured: the last one becomes the active range and\n * the others stay highlighted beside it, as after a Ctrl+drag. The\n * active cell goes to the last range's start corner.\n */\n selectCells(ranges: ReadonlyArray<readonly [number, number, number, number]>): void\n /**\n * The current cell-selection rectangles in the same shape `selectCells`\n * accepts, each normalised to `[minRow, minCol, maxRow, maxCol]`, in the\n * order they were selected with the active one last. Empty array when no\n * range is active.\n *\n * Handsontable's method of the same name. Two differences, both on\n * purpose: an empty array rather than `undefined`, so `getSelected()[0]`\n * never throws; and normalised corners, so a loop from the first row to\n * the last cannot run zero times because the range was dragged upwards.\n * For the orientation, and the active cell inside a range, use\n * `getSelectedRange()`.\n *\n * With cell selection off (`selectable={false}`) no rectangle is ever\n * recorded and this stays `[]`; the focused cell is `getActiveCell()`.\n */\n getSelected(): Array<[number, number, number, number]>\n /**\n * The most recently selected rectangle as `[startRow, startCol, endRow,\n * endCol]`, start being where the selection began, or `undefined` when\n * nothing is selected. Handsontable's `getSelectedLast()`, orientation\n * included: a range dragged upwards has `startRow > endRow`.\n */\n getSelectedLast(): [number, number, number, number] | undefined\n /**\n * Every selected rectangle with its orientation and active cell, oldest\n * first, or `undefined` when nothing is selected. Handsontable's\n * `getSelectedRange()`.\n */\n getSelectedRange(): SvGridSelectedRange[] | undefined\n /** The most recently selected rectangle, or `undefined`. Handsontable's\n * `getSelectedRangeLast()`. */\n getSelectedRangeLast(): SvGridSelectedRange | undefined\n\n // ----- Integrated charting (requires the `charting` prop) -----\n /** Open the built-in chart panel. */\n openChart(): void\n /** Close the built-in chart panel. */\n closeChart(): void\n /** The live chart spec the panel is rendering, or `null`. */\n getChartSpec(): ChartSpec | null\n /** Select cell rectangles + open the chart panel (scopes to the range). */\n chartRange(ranges?: ReadonlyArray<readonly [number, number, number, number]>): void\n /**\n * Configure the built-in chart panel's ACTIVE chart. Column references\n * accept a column id OR field name. Opens the panel unless `open === false`.\n */\n configureChart(config: {\n open?: boolean\n type?: ChartType\n dimension?: string | null\n series?: string | null\n measure?: string | null\n reduce?: ChartReducer\n stacked?: boolean\n dataLabels?: boolean\n logScale?: boolean\n timeAxis?: boolean\n valueFormat?: 'number' | 'currency' | 'percent' | 'compact'\n /** Group a date dimension by calendar unit; `null` returns to exact values. */\n bucket?: ChartTimeBucket | null\n /** Second measure: a scatter's Y, a range's high end, a bullet's target. */\n measure2?: string | null\n /** Histogram bin count; `null` for Sturges' rule. */\n bins?: number | null\n funnelShape?: 'trapezoid' | 'pyramid' | 'cone' | null\n candleStyle?: 'classic' | 'hollow' | 'heikin-ashi' | null\n /** Candlestick / OHLC: the price columns (ids or fields); `null` goes\n * back to guessing them from the column names. */\n ohlc?: { open?: string | null; high?: string | null; low?: string | null; close?: string | null; volume?: string | null } | null\n /** Indicator panes under a price chart (`volume`, `rsi`, `macd`,\n * `stochastic`, `atr`, `obv`) and overlays on it (`sma`, `ema`,\n * `bb`, `vwap`). */\n indicators?: Array<'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'>\n /** The builder's format state (titles, axes, legend, per-series style);\n * `null` resets it. */\n format?: ChartFormatState | null\n /** `true` unlinks the chart from the grid (it keeps the spec it has),\n * `false` links it back. */\n frozen?: boolean\n /** Apply a saved chart (by name) to the active tab before the other keys. */\n saved?: string\n }): void\n /**\n * Keep the active chart's whole configuration (type, columns, aggregate,\n * switches, indicators, format, zoom) under a name. A same-named entry is\n * replaced. Saved charts travel with `getState()` / `setState()` and show\n * in the panel's Saved charts popover.\n */\n saveChart(name: string): void\n /** Apply a saved chart to the active tab, which keeps its title. `false` when no chart has that name. */\n applySavedChart(name: string): boolean\n removeSavedChart(name: string): void\n getSavedCharts(): SavedChart[]\n /**\n * Register a natural-language \"chart this\" handler. When set, the chart\n * panel shows an AI button. `@svgrid/enterprise`'s `enableAiCharting(api)`\n * fills this. Pass `null` to remove it.\n */\n setChartAiHandler(\n handler: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,\n ): void\n /**\n * Register an \"explain this chart\" handler. When set, the chart panel's AI\n * row shows an Explain button that reads the active chart and shows the\n * summary and insights it returns. `enableAiCharting(api)` fills this with\n * `aiExplainChart`. Pass `null` to remove it.\n */\n setChartExplainHandler(handler: (() => Promise<{ summary: string; insights: string[] } | null>) | null): void\n\n // ----- Rows -----\n /** Add one row. `position` defaults to `'bottom'`. */\n addRow(row: TData, position?: 'top' | 'bottom' | number): void\n addRows(rows: ReadonlyArray<TData>, position?: 'top' | 'bottom' | number): void\n /** Remove a row at the given data-array index. */\n removeRow(rowIndex: number): void\n removeRows(rowIndices: ReadonlyArray<number>): void\n /**\n * Apply a batch of add / update / remove mutations in a SINGLE data update\n * (one re-render, not one per row) - the high-frequency / streaming path.\n * `update` and `remove`-by-id match rows via `getRowId`, so set that prop\n * for those to work; `remove` also accepts row object references. Returns\n * the counts actually applied.\n */\n applyTransaction(tx: SvGridTransaction<TData>): SvGridTransactionResult\n\n // ----- Columns -----\n /** Add one column. `position` defaults to `'right'`. */\n addColumn(\n column: ColumnDef<TFeatures, TData>,\n position?: 'left' | 'right' | number,\n ): void\n addColumns(\n columns: ReadonlyArray<ColumnDef<TFeatures, TData>>,\n position?: 'left' | 'right' | number,\n ): void\n /** Remove a column by id (or field when no id was provided). */\n removeColumn(columnId: string): void\n\n // ----- Visibility -----\n setColumnVisible(columnId: string, visible: boolean): void\n isColumnVisible(columnId: string): boolean\n\n // ----- Sort / group / filter -----\n /** Sort by one column (replaces any existing sort). Pass `null` to clear. */\n setSort(columnId: string, direction: 'asc' | 'desc' | null): void\n clearSort(): void\n setGroupBy(columnIds: ReadonlyArray<string>): void\n\n // ----- Options (runtime prop overrides) -----\n /**\n * Change a grid prop at runtime, e.g. `api.setOption('sortable', true)`. The\n * override is merged over the incoming prop and the grid re-renders reactively -\n * identical to the parent passing a new prop value. Pass `undefined` to clear the\n * override and fall back to the prop.\n *\n * Note: seed-once props are NOT retroactive - `initialSorting`,\n * `initialColumnPinning`, `initialHiddenColumns`, `columnOrder`, the initial\n * `pageSize`, and the one-shot `externalSort` / `externalFilter` are read at mount,\n * so overriding them later has no effect. Prefer the dedicated data path for\n * `data` / `columns` (this works, but the sync effects are the idiomatic route).\n */\n setOption<K extends keyof Props<TFeatures, TData>>(key: K, value: Props<TFeatures, TData>[K] | undefined): void\n /** Read a prop's effective value: the runtime override if set, else the incoming prop. */\n getOption<K extends keyof Props<TFeatures, TData>>(key: K): Props<TFeatures, TData>[K]\n /** Clear every runtime override set via `setOption`, reverting to the incoming props. */\n resetOptions(): void\n\n /** Set the operator filter for a column. Pass `null` to clear. */\n setFilter(\n columnId: string,\n filter:\n | {\n operator: SvGridFilterOperator\n value?: string\n /** Upper bound for the `between` operator. Required when `operator === 'between'`. */\n valueTo?: string\n /**\n * Optional SECOND condition on the same column, joined by `join`\n * (multi-condition filtering, e.g. \"> 100 AND < 500\").\n */\n operator2?: SvGridFilterOperator\n value2?: string\n valueTo2?: string\n /** How to combine the two conditions. Defaults to `'AND'`. */\n join?: 'AND' | 'OR'\n }\n | null,\n ): void\n /**\n * Set the facet (set-list, Excel-style multi-select) filter for a column.\n * Pass an empty array or `null` to clear it. The values restore the\n * checked state of the column-menu's value list - the engine then filters\n * the data to rows whose cell value is in the set. Used to restore\n * snapshots captured via `onFiltersChange`'s `selectedValues`.\n */\n setFacetFilter(columnId: string, values: ReadonlyArray<string> | null): void\n clearFilter(columnId: string): void\n /**\n * Drop cached async `editorOptions` so the next render refetches them.\n * Omit `columnId` to clear every column. Only affects columns whose\n * `editorOptions` returns a Promise - static lists are never refetched.\n */\n refreshEditorOptions(columnId?: string): void\n /**\n * Open the `contextMenu` at the pointer, for the cell at (rowIndex,\n * colIndex), from an element of your own: a spreadsheet's column letters\n * and row numbers offer the cell menu for the column or row they select.\n * Does nothing without a `contextMenu` prop.\n */\n openContextMenu(event: MouseEvent, rowIndex: number, colIndex: number): void\n /**\n * Clear every active column filter (menu, filter-row, set-list, global, and\n * the advanced filter). Resets the grid to \"no filtering\" in a single call.\n */\n clearAllFilters(): void\n /**\n * Set the advanced-filter expression (Pro). `null` clears it. Composed with\n * AND after the global, column and facet filters.\n *\n * Rows are only removed once `@svgrid/enterprise`'s `enableAdvancedFilter()`\n * has registered a compiler. Without it the expression is stored but nothing\n * is filtered - use `isAdvancedFilterActive()` to tell the two apart.\n */\n setAdvancedFilter(expr: GridPredicateExpr | null): void\n /** The current advanced-filter expression, or `null`. */\n getAdvancedFilter(): GridPredicateExpr | null\n /** Clear the advanced filter, leaving other filter surfaces untouched. */\n clearAdvancedFilter(): void\n /** Whether an expression is set AND an engine is registered to run it. */\n isAdvancedFilterActive(): boolean\n /**\n * Read the active column-menu filters as a snapshot. Keyed by column id.\n * Returns an empty object when nothing is filtered. `valueTo` is only\n * present when `operator === 'between'`.\n */\n getFilters(): Record<\n string,\n { operator: SvGridFilterOperator; value: string; valueTo?: string }\n >\n\n /**\n * Snapshot of the DATA rows the grid is displaying right now - after\n * filtering, sorting, grouping and pagination. Use this when you need the\n * visible result set (e.g. to export it as CSV).\n *\n * Group banner rows are not included: the return type is `TData`, and a\n * banner is not one of your rows. That matters while grouping is on, because\n * with every group collapsed this returns an EMPTY array even though the\n * grid visibly shows a banner per group. Count banners from `getState()`\n * rather than from the length of this.\n */\n getDisplayedRows(): ReadonlyArray<TData>\n\n /** Snapshot of the current data array (pre-pipeline). */\n getData(): ReadonlyArray<TData>\n\n /**\n * The stable id the grid keys a row by: what the `getRowId` prop\n * computed, or the row's position when no `getRowId` is set. Pass a\n * display index (the space `getActiveCell`, `scrollToRow` and the cell\n * methods use) or a row object; returns null for an unknown index or a\n * row that is not in the data. Pair with `selectRows`, `toggleRowSelected`\n * or an `applyTransaction` remove, all of which take these ids.\n */\n getRowId(row: number | TData): string | null\n\n /**\n * Snapshot of every column the grid currently knows about, in visual\n * order, with the human-readable header label. Use this when exporting\n * or building a column-picker UI - the data is read once, no\n * subscription. Hidden columns are included; check `visible` to filter.\n */\n getColumns(): ReadonlyArray<{\n id: string\n field?: string\n header: string\n visible: boolean\n /** The column's `format` config, when set. Lets an exporter reproduce\n * the on-screen display value (currency, date pattern, etc.). */\n format?: CellFormatConfig\n /** Effective horizontal alignment ('left' | 'center' | 'right'). */\n align?: 'left' | 'center' | 'right'\n /** The column's data type: `cellDataType`, or the one a built-in\n * `editorType` implies. Lets a filter or expression UI offer\n * type-appropriate operators whatever edits the cell. */\n cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'\n /** The column's `editorType`, when set: what edits it. */\n editorType?: string\n }>\n\n // ----- Free data export (CSV / TSV / JSON + clipboard) -----\n /**\n * Export the grid to a **CSV** file. Free in the community grid; the\n * richer Excel / PDF / styled formats live in @svgrid/enterprise. Values\n * are formatted as shown on screen (pass `rawValues: true` for raw). Rows\n * default to the current view (`rows: 'selected' | 'all'` to change).\n * Resolves with the serialized text; pass `download: false` to skip the\n * browser download and just get the string.\n */\n exportCsv(options?: GridExportOptions): Promise<string>\n /** Export the grid to a **TSV** file (tab-separated). See `exportCsv`. */\n exportTsv(options?: GridExportOptions): Promise<string>\n /** Export the grid to a **JSON** file (array of `{ field: value }`). */\n exportJson(options?: GridExportOptions): Promise<string>\n /**\n * Copy the grid to the system clipboard. `format: 'tsv'` (default) pastes\n * straight into Excel / Sheets; `'csv'` / `'markdown'` also supported.\n * Resolves with the copied text.\n */\n copyToClipboard(options?: GridClipboardOptions): Promise<string>\n\n /** Clear every checked row. Emits `onRowSelectionChange({}, [])`. */\n clearRowSelection(): void\n\n // ----- Column layout (width + pinning) -----\n /**\n * Set the width of one column in pixels. Identical to dragging the\n * column's resize handle. Width is clamped to `MIN_COLUMN_WIDTH`.\n */\n setColumnWidth(columnId: string, width: number): void\n /**\n * Snapshot of every column's current width (in pixels), keyed by\n * column id. Columns the user has never resized AND that have no\n * explicit `width` on their ColumnDef are reported at the grid-wide\n * default. Useful for \"save view\" + URL persistence.\n */\n getColumnWidths(): Record<string, number>\n /**\n * Snap one column's width to its widest visible cell (header text +\n * any rendered body cell). Equivalent to double-clicking the column's\n * resize handle. The grid also exposes this through the column menu's\n * \"Autosize\" item.\n */\n autosizeColumn(columnId: string): void\n /**\n * The height of a row in px: the one it was dragged or set to, else the\n * declared `rowHeight` (30 when none). `rowIndex` is a display index.\n */\n getRowHeight(rowIndex: number): number\n /**\n * Give a row its own height, the way dragging its grip does with\n * `rowResize`; `null` takes it back to the declared height. The height\n * belongs to the row (its id), so it follows the row through a sort and\n * survives the data being replaced. Ignored under `autoRowHeight`, where\n * the content decides.\n */\n setRowHeight(rowIndex: number, height: number | null): void\n /**\n * Fold a column to nothing, the way a spreadsheet hides one: it keeps\n * its index, its cells and its width, and takes no room until it is\n * unfolded. Unlike `setColumnVisible(id, false)`, which takes the\n * column out of the model and shifts every index after it, references\n * by index stay valid, so a sheet's formulas and formats are untouched.\n * Arrow keys, Tab and Enter step over a collapsed column.\n */\n setColumnCollapsed(columnId: string, collapsed: boolean): void\n isColumnCollapsed(columnId: string): boolean\n /**\n * Fold a row to nothing, the row-side twin of `setColumnCollapsed`.\n * `rowIndex` is a display index; the fold belongs to the row (its id)\n * and follows it through a sort.\n */\n setRowCollapsed(rowIndex: number, collapsed: boolean): void\n isRowCollapsed(rowIndex: number): boolean\n /** The `mergedCells` in force, copied: origins with their spans. */\n getMergedCells(): Array<{ rowIndex: number; colIndex: number; rowSpan: number; colSpan: number }>\n /** Run `autosizeColumn` on every column. */\n autosizeAllColumns(): void\n /**\n * Replace the column-pinning state in one call. Each entry is a\n * column id; the order in the array becomes the visible order along\n * the pinned edge.\n */\n setColumnPinning(pinning: {\n left?: ReadonlyArray<string>\n right?: ReadonlyArray<string>\n }): void\n /** Snapshot of the current column-pinning state. */\n getColumnPinning(): { left: string[]; right: string[] }\n\n // ----- Column reorder -----\n /**\n * Replace the column order. Pass an array of column ids in the\n * desired visual order. Unknown ids are skipped; columns not in the\n * array keep their existing relative position after the listed ones.\n * Fires `onColumnOrderChange` once the new order is applied. Pin\n * groups (`columnPinning.left` / `right`) are still applied on top.\n */\n setColumnOrder(order: ReadonlyArray<string>): void\n /**\n * Snapshot of the current visual column order - the same shape the\n * `onColumnOrderChange` callback receives. Useful for saving and\n * restoring a view layout.\n */\n getColumnOrder(): string[]\n\n // ----- Row expansion -----\n /**\n * Set whether a row (group node or expandable leaf) is expanded.\n * The `id` is the engine's row id - for grouped rows that's the\n * synthetic group key, `group_<field>_<value>` (e.g.\n * `\"group_department_Engineering\"`; a second level appends\n * `_<field>_<value>`). An id that matches no row is ignored silently.\n */\n setRowExpanded(id: string, expanded: boolean): void\n /** Expand every group node in the current grouped row model. */\n expandAllGroups(): void\n /** Collapse every expansion - resets expanded state to {}. */\n collapseAllGroups(): void\n\n // ----- Undo / redo -----\n /** Undo the most recent inline-edit. Returns false when the history is empty. */\n undo(): boolean\n /** Redo the most recently undone edit. Returns false when the redo stack is empty. */\n redo(): boolean\n /** True when there's at least one step on the undo stack. */\n canUndo(): boolean\n /** True when there's at least one step on the redo stack. */\n canRedo(): boolean\n /** Wipe both stacks (e.g. after a server save commits the buffer). */\n clearHistory(): void\n /**\n * Mark every step recorded from now on with `tag`, until the next call;\n * `undefined` stops the marking. The grid never reads the tag. It is for a\n * consumer that shows one of several data sets through the grid (a\n * workbook's sheets, say) and has to know which one the next Ctrl+Z would\n * change before it lets the grid apply it: it tags the steps with the data\n * set they were made on and asks `peekUndo` first.\n */\n setHistoryTag(tag: unknown): void\n /** The step the next `undo()` would apply, or null when there is none. A\n * multi-cell action carries one tag, so it reports as one. */\n peekUndo(): { tag: unknown } | null\n /** The step the next `redo()` would apply, or null when there is none. */\n peekRedo(): { tag: unknown } | null\n\n // ----- Keyboard commands -----\n /**\n * The same `GridCommandContext` a registered shortcut handler receives.\n *\n * Keyboard commands are handed one on every keystroke, but a button in a\n * ribbon or a toolbar has no keystroke to ride in on, and re-implementing\n * \"bold the selection\" against the api would give the button and the key\n * two code paths that drift. Both go through this instead.\n *\n * Live, not a snapshot: the object reads through to the grid, so one built\n * once at mount still reports the current selection.\n */\n getCommandContext(): GridCommandContext\n\n // ----- Find in grid -----\n /** Open the built-in find overlay (Ctrl+F also opens it). */\n openFind(): void\n /** Close the find overlay and clear the query. */\n closeFind(): void\n /** Update the find query programmatically (useful for app-wide command palettes). */\n setFindQuery(q: string): void\n /** Snapshot of the current find hits (rowIndex / colIndex / columnId). */\n getFindHits(): Array<{ rowIndex: number; colIndex: number; columnId: string }>\n\n // ----- Row selection (read + write) -----\n /**\n * The currently selected data rows (group-header rows excluded), in row-model\n * order. Read once - no subscription. The push-based equivalent is\n * `onRowSelectionChange`.\n */\n getSelectedRows(): TData[]\n /** The engine row ids of the selected rows. Keys into the selection record. */\n getSelectedRowIds(): string[]\n /**\n * Select rows by engine row id. By default this REPLACES the selection;\n * pass `additive: true` to add to the existing selection instead.\n */\n selectRows(ids: ReadonlyArray<string>, additive?: boolean): void\n /** Select every selectable (non-group) row in the current row model. */\n selectAllRows(): void\n /** Flip one row's selected state by id. */\n toggleRowSelected(id: string): void\n\n // ----- Pagination -----\n /**\n * Current pagination snapshot. `total` is the post-filter row count;\n * `pageCount` is derived from it and `pageSize` (always >= 1).\n */\n getPageInfo(): {\n pageIndex: number\n pageSize: number\n pageCount: number\n total: number\n }\n /** Jump to a 0-based page. Clamped to [0, pageCount - 1]. */\n setPage(pageIndex: number): void\n /** Advance one page (no-op past the last page). */\n nextPage(): void\n /** Go back one page (no-op before the first page). */\n prevPage(): void\n /** Jump to the first page. */\n firstPage(): void\n /** Jump to the last page. */\n lastPage(): void\n /** Change the page size, keeping the first visible row in view. */\n setPageSize(pageSize: number): void\n\n // ----- Navigation / scrolling -----\n /**\n * Scroll the body so the given row index is at the top of the viewport.\n * Works with virtualization on. Index is clamped to the row count.\n */\n scrollToRow(rowIndex: number): void\n /** The active (focused) cell, or null when nothing is focused. */\n getActiveCell(): { rowIndex: number; colIndex: number; columnId: string } | null\n /** Move the active cell. Both coordinates are clamped to the grid bounds. */\n setActiveCell(rowIndex: number, colIndex: number): void\n\n // ----- View state (save / restore) -----\n /**\n * Serializable snapshot of the whole view - sort, grouping, pagination,\n * column layout, and every filter surface. Pair with `setState` for\n * \"save view\" / URL persistence / named views.\n */\n getState(): SvGridViewState\n /**\n * Restore a view from a (partial) snapshot produced by `getState`. Only the\n * keys present are applied, so you can restore just the columns, just the\n * filters, etc.\n */\n setState(state: Partial<SvGridViewState>): void\n /** Force a recompute of the row pipeline + a re-render. */\n refresh(): void\n}\n```\n\n### `type SvGridWrapperProps`\n\nThe props `<SvGrid>` accepts. See the SvGrid reference for the full list with defaults.\n\n```ts\nexport type SvGridWrapperProps<\n TFeatures extends TableFeatures,\n TData extends RowData,\n> = {\n data: ReadonlyArray<TData>\n columns: Array<ColumnDef<TFeatures, TData>>\n /**\n * Feature set from `tableFeatures({ ... })`. Optional - the `sortable` /\n * `filterable` / `groupable` shortcuts inject the matching feature, so a\n * grid can be configured from the boolean shortcuts alone.\n */\n features?: TFeatures\n /**\n * Capability shortcuts. Every capability is OFF by default (a bare grid is\n * a plain read-only table); set a shortcut `true` to opt in.\n *\n * `sortable` - column sorting (injects `rowSortingFeature`)\n * `filterable` - column filtering (injects `columnFilteringFeature`)\n * `editable` - inline editing (alias of `enableInlineEditing`)\n * `groupable` - grouping controls (alias of `showGroupingControls`)\n * `pageable` - pagination footer (alias of `showPagination`)\n */\n sortable?: boolean\n filterable?: boolean\n editable?: boolean\n groupable?: boolean\n pageable?: boolean\n options?: Partial<SvGridOptions<TFeatures, TData>>\n loading?: boolean\n error?: string | null\n emptyMessage?: string\n showGlobalFilter?: boolean\n showColumnFilters?: boolean\n showGroupingControls?: boolean\n showRowSelection?: boolean\n showPagination?: boolean\n virtualization?: boolean\n /** Row height in pixels. Pass a function `(rowIndex) => px` for per-row\n * variable heights (e.g. when wiring up an interactive row-resize). */\n rowHeight?: number | ((rowIndex: number) => number)\n overscan?: number\n containerHeight?: number\n columnVirtualization?: boolean\n columnOverscan?: number\n columnWidth?: number\n showFilterMenu?: boolean\n showFilterRow?: boolean\n enableCellSelection?: boolean\n enableInlineEditing?: boolean\n enableRowSummaries?: boolean\n /** Shortcut alias for `enableRowSummaries`; wins when both are set. */\n summary?: boolean\n /** Receives the imperative grid API when the component is ready. */\n onApiReady?: (api: SvGridApi<TFeatures, TData>) => void\n}\n```\n"
8077
8077
  },
8078
8078
  {
8079
8079
  "slug": "reference/auto/svgrid-grid-timezone",
@@ -8346,8 +8346,8 @@ export const apiReference = {
8346
8346
  ]
8347
8347
  };
8348
8348
  export const apiSurface = {
8349
- "gridVersion": "3.0.14",
8350
- "enterpriseVersion": "3.1.11",
8349
+ "gridVersion": "3.0.16",
8350
+ "enterpriseVersion": "3.1.13",
8351
8351
  "grid": {
8352
8352
  "values": [
8353
8353
  "ALL_FILTER_OPERATORS",
package/dist/preview.d.ts CHANGED
@@ -8,7 +8,7 @@ export declare const UI_RESOURCE_URI = "ui://svgrid/preview.html";
8
8
  * start failing in a way nobody is watching for. `tools/mcp-tools.test.ts` checks these
9
9
  * against the workspace so they cannot quietly rot either.
10
10
  */
11
- export declare const PREVIEW_GRID_WC_VERSION = "3.0.16";
11
+ export declare const PREVIEW_GRID_WC_VERSION = "3.0.18";
12
12
  export declare function previewResource(): {
13
13
  uri: string;
14
14
  name: string;
package/dist/preview.js CHANGED
@@ -27,7 +27,7 @@ const UI_RESOURCE_META_KEY = 'ui/resourceUri';
27
27
  * start failing in a way nobody is watching for. `tools/mcp-tools.test.ts` checks these
28
28
  * against the workspace so they cannot quietly rot either.
29
29
  */
30
- export const PREVIEW_GRID_WC_VERSION = '3.0.16';
30
+ export const PREVIEW_GRID_WC_VERSION = '3.0.18';
31
31
  const EXT_APPS_VERSION = '1.7.5';
32
32
  const CDN = 'https://cdn.jsdelivr.net/npm';
33
33
  /**
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "commercial",
6
6
  "url": "https://svgrid.com/pricing"
7
7
  },
8
- "version": "3.0.18",
8
+ "version": "3.0.19",
9
9
  "description": "Model Context Protocol server for SvGrid, the Svelte 5 data grid: checks the code your AI writes against the real API surface, plus version-pinned docs, API reference and 373 demo sources.",
10
10
  "license": "MIT",
11
11
  "author": "jQWidgets <sales@jqwidgets.com>",
@@ -33,7 +33,7 @@
33
33
  "@modelcontextprotocol/sdk": "^1.0.4",
34
34
  "svelte": "^5.55.5",
35
35
  "zod": "^3.23.8",
36
- "@svgrid/enterprise": "^3.1.11"
36
+ "@svgrid/enterprise": "^3.1.13"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.7",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "com.svgrid/svgrid",
4
4
  "title": "SvGrid",
5
5
  "description": "Checks AI-written SvGrid code against the real API, plus version-pinned Svelte 5 grid docs.",
6
- "version": "3.0.18",
6
+ "version": "3.0.19",
7
7
  "websiteUrl": "https://svgrid.com/docs/help/mcp-server/",
8
8
  "repository": {
9
9
  "url": "https://github.com/sv-grid/sv-grid",
@@ -15,7 +15,7 @@
15
15
  "registryType": "npm",
16
16
  "registryBaseUrl": "https://registry.npmjs.org",
17
17
  "identifier": "@svgrid/mcp",
18
- "version": "3.0.18",
18
+ "version": "3.0.19",
19
19
  "runtimeHint": "npx",
20
20
  "transport": {
21
21
  "type": "stdio"