# Technical Documentation: angular-ui/ui-grid

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/angular-ui/ui-grid)

### Overview (`README.md`)

[](https://github.com/orneryd/ui-grid/actions/workflows/ci.yml)
[](https://coveralls.io/github/orneryd/ui-grid?branch=main)
[](https://www.npmjs.com/package/@ornery/ui-grid)
[](./LICENSE.md)

#### [Discord Community](https://discord.gg/Baz4w8ZWN)

# UI Grid — 5.x

**The modern web data grid for Angular, React, and Web Components. Every feature is free and open source.**

Rust, WASM implementation, egui, and native adapter source and documentation live in [orneryd/uiGrid](https://github.com/orneryd/uiGrid).

v5.x + is in use by my other project, [NornicDB](https://github.com/orneryd/NornicDB), in the web ui components (It's a high-performance golang graph+vector database if you want to take a look)

A from-scratch rewrite of the original by the original author. Same `gridOptions` / `columnDefs` / `onRegisterApi` api surface, modern Angular signals internals, and zero legacy baggage.

Just like the original grid, it will **NEVER** be monetized. This is purely my contribution to the greater community.

It proves that the datagrid cabal wants you to think it's hard to write a data grid. Well, it's not, and nobody should be paying to group data.

**[Live Demo & Docs](https://orneryd.github.io/ui-grid/)** | **[npm](https://www.npmjs.com/package/@ornery/ui-grid)** | **[Rust Project](https://github.com/orneryd/uiGrid)** | **[Used by NornicDB](https://orneryd.github.io/NornicDB/)**

---

## Why Remastered?

- **Original authorship** — built by the same engineer who created AngularJS ui-grid, with a decade of hindsight on what worked and what didn't
- **Familiar API** — if you used the original ui-grid, you already know this one: `gridOptions`, `columnDefs`, `onRegisterApi`, `gridApi.core.*`
- **Modern internals** — shared vanilla web component core with Shadow DOM encapsulation; Angular and React wrappers are thin bridges that project framework templates into the vanilla element via slot-based portals
- **No legacy** — no `$scope`, no Bower, no Grunt, no jQuery, no module system from 2013

---

## Feature Comparison

Everything below ships free and MIT-licensed. No enterprise tier, no license keys, no per-developer fees.

| Feature                   | UI Grid  | ag-Grid Community | ag-Grid Enterprise |    Vaadin Grid    |   Kendo UI   |   Syncfusion    |
| ------------------------- | :------: | :---------------: | :----------------: | :---------------: | :----------: | :-------------: |
| Sorting                   | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| Filtering                 | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| **Row Grouping**          | **Free** |         —         |    ~$999/dev/yr    |         —         |     Paid     |   Community\*   |
| **Tree Data**             | **Free** |         —         |    ~$999/dev/yr    |       Free        |     Paid     |   Community\*   |
| **Master/Detail Rows**    | **Free** |         —         |    ~$999/dev/yr    |         —         |     Paid     |   Community\*   |
| **Inline Cell Editing**   | **Free** |       Free        |         —          | Pro ~$159/dev/mo  |     Paid     |   Community\*   |
| Row Selection             | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| Column Resizing           | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| CSV Export                | **Free** |       Free        |         —          |         —         |     Paid     |   Community\*   |
| **Excel Export**          | **Free** |         —         |    ~$999/dev/yr    |         —         |     Paid     |   Community\*   |
| **PDF Export**            | **Free** |         —         |    ~$999/dev/yr    |         —         |     Paid     |   Community\*   |
| Virtual Scrolling         | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| Pagination                | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| Column Pinning            | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| Column Reordering         | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| **Save/Restore State**    | **Free** |       Free        |         —          |         —         |     Paid     |        —        |
| Infinite Scroll           | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| **Keyboard Cell Nav**     | **Free** |       Free        |         —          |         —         |     Paid     |   Community\*   |
| **Row Edit (dirty/save)** | **Free** |         —         |         —          |         —         |      —       |        —        |
| **Cell Validation**       | **Free** |         —         |         —          |         —         |      —       |        —        |
| **CSV/JSON Import**       | **Free** |         —         |         —          |         —         |      —       |        —        |
| **Shadow DOM**            | **Free** |         —         |         —          |         —         |      —       |        —        |
| **Web Component Build**   | **Free** |         —         |         —          |      Native       |      —       |        —        |
| **Feature Tree-Shaking**  | **Free** |         —         |         —          |         —         |      —       |        —        |
| **SSR Support**           | **Free** |         —         |    ~$999/dev/yr    |         —         |      —       |        —        |
| i18n (6 locales built-in) | **Free** |       Free        |         —          |       Free        |     Paid     |   Community\*   |
| React                     | **Yes**  |      Wrapper      |      Wrapper       |        No         |   Wrapper    |     Wrapper     |
| Angular                   | **Yes**  |      Wrapper      |      Wrapper       |        No         |   Wrapper    |     Wrapper     |
| **License**               | **MIT**  |        MIT        |     Commercial     |   Apache/Comm.    |  Commercial  | Comm./Community |
| **Price**                 |  **$0**  |        $0         |    ~$999/dev/yr    | $159/dev/mo (Pro) | ~$799/dev/yr |    See below    |

> **Bold** = features where UI Grid gives you for free what competitors charge for or don't offer at all.
>
> _Syncfusion Community license is free for companies with <$1M annual revenue, ≤5 developers, and ≤$3M outside capital. Their paid tier requires a custom quote. Kendo UI ranges $799–$1,299/dev/yr depending on support level. Prices are approximate and subject to change._

---

## Quick Start

```bash
npm install @ornery/ui-grid
```

The WASM runtime is installed transitively and loaded through your application
bundler. No `angular.json` asset entry or `dist/ui-grid-wasm-web` copy is needed,
and applications should not register the TypeScript pipeline as a WASM engine.

For an unbundled deployment that hosts the web-target WASM files separately,
configure their asset root before mounting a grid:

```typescript
import { setUiGridWasmAssetBase } from '@ornery/ui-grid';

setUiGridWasmAssetBase('https://cdn.example.com/ui-grid-assets/');
```

The configured root must contain
`dist/ui-grid-wasm-web/ui_grid_wasm.js` and its adjacent
`ui_grid_wasm_bg.wasm`. If WASM initialization is unavailable, the grid remains
functional through its TypeScript fallback.

### Angular Component

```typescript
import { Component } from '@angular/core';
import { GridOptions, UiGridComponent } from '@ornery/ui-grid';

@Component({
  selector: 'app-my-grid',
  imports: [UiGridComponent],
  template: `<app-ui-grid [options]="gridOptions" />`,
})
export class MyGridComponent {
  gridOptions: GridOptions = {
    id: 'my-grid',
    data: [
      { name: 'Alice', role: 'Engineer', salary: 120000 },
      { name: 'Bob', role: 'Designer', salary: 95000 },
    ],
    columnDefs: [
      { name: 'name' },
      { name: 'role' },
      { name: 'salary', type: 'number', align: 'end' },
    ],
    onRegisterApi: (api) => {
      this.gridApi = api;
    },
  };
}
```

### React

```bash
npm install @ornery/ui-grid-react @ornery/ui-grid-core @ornery/ui-grid-vanilla
```

```tsx
import { UiGrid } from '@ornery/ui-grid-react';
import type { GridOptions } from '@ornery/ui-grid-core';

function MyGrid() {
  const options: GridOptions = {
    id: 'my-grid',
    data: [
      { name: 'Alice', role: 'Engineer', salary: 120000 },
      { name: 'Bob', role: 'Designer', salary: 95000 },
    ],
    columnDefs: [
      { name: 'name' },
      { name: 'role' },
      { name: 'salary', type: 'number', align: 'end' },
    ],
  };

  return <UiGrid options={options} />;
}
```

### Web Components (Vanilla)

The grid's rendering engine is a framework-free custom element (`<ui-grid-element>`) built on `@ornery/ui-grid-core` with pure DOM rendering and Shadow DOM encapsulation. Both the Angular and React wrappers are thin bridges around this same element — they mount `<ui-grid-element>`, pass options, and project framework-specific templates into it via a slot-based portal system.

```bash
npm install @ornery/ui-grid-vanilla @ornery/ui-grid-core
```

No-bundler browser usage is supported by the browser bundle. It includes the core runtime and registers `<ui-grid-element>` when loaded:

```html
<script type="module" src="./ui-grid-element.js"></script>

<ui-grid-element
  grid-id="static-grid"
  enable-sorting
  enable-filtering
  column-defs='[{"name":"name"},{"name":"role"}]'
  data='[{"name":"Alice","role":"Engineer"}]'
>
</ui-grid-element>
```

When building from this repo, `npm run build:vanilla` writes that file to `projects/ui-grid-vanilla/dist/browser/ui-grid-element.js`. Use that browser bundle for static script-tag consumption; `projects/ui-grid-vanilla/dist/index.js` is CommonJS and `projects/ui-grid-vanilla/dist/index.mjs` is the package ESM entry for bundlers or import-map setups.

Declarative HTML usage:

```html
<ui-grid-element
  grid-id="vanilla-demo"
  title="Team Roster"
  enable-sorting
  enable-filtering
  column-defs='[
    { "name": "name" },
    { "name": "role" },
    { "name": "salary", "type": "number", "align": "end" }
  ]'
  data='[
    { "name": "Alice", "role": "Engineer", "salary": 120000 },
    { "name": "Bob", "role": "Designer", "salary": 95000 }
  ]'
>
</ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement(); // registers <ui-grid-element>
</script>
```

Supported declarative inputs include boolean flags (`enable-sorting`, `enable-filtering`), scalar attributes (`grid-id`, `title`, `row-height`), and JSON attributes (`column-defs`, `data`).

For callbacks, function-valued column definitions, or high-frequency updates, use the `options` property:

```typescript
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';

await mountVanillaUiGrid(document.getElementById('app'), {
  id: 'mounted-grid',
  data: [{ name: 'Alice', role: 'Engineer' }],
  columnDefs: [{ name: 'name' }, { name: 'role' }],
});
```

## Features

- **Sorting** — click column headers to cycle asc/desc/none, custom comparators, programmatic API
- **Filtering** — per-column inputs with conditions: contains, exact, startsWith, endsWith, greaterThan, regex, custom predicates
- **Row Grouping** — nested multi-column grouping with collapsible group headers
- **Tree View** — hierarchical data with expand/collapse per node, arbitrary nesting depth
- **Expandable Rows** — master/detail pattern with custom templates (Angular `ng-template`, React render prop, or vanilla `<template>` slot)
- **Cell Editing** — inline spreadsheet-style editing with full keyboard navigation (Tab, Enter, Escape), edit-on-focus, Enter/Tab commit+move across editable columns
- **Keyboard Cell Navigation** — Arrow/Tab/Home/End navigation with wrap/clamp modes, focus persistence across re-renders, `keyDownOverrides` for custom key handling, full `gridApi.cellNav` surface
- **Row Selection** — click/shift/ctrl/drag-paint mouse selection, keyboard (Space, Ctrl+A), row-header checkbox column, select-all header, `isRowSelectable` hook, 13 options, 18 API methods, 3 events
- **Column Resizing** — drag column borders to resize, programmatic API, persisted via save/restore state
- **Row Edit** — dirty/saving/error row lifecycle with `rowEditWaitInterval` debounce, `setSavePromise` pattern, auto-retry on error, visual row state indicators
- **Cell Validation** — declarative per-column `validators` (built-in `required`, `minLength`, `maxLength` + custom), async validator support, invalid cell markers with error messages, `gridApi.validate` surface
- **Pagination** — client-side or external pagination with configurable page sizes
- **Infinite Scroll** — bi-directional infinite scrolling with loading state management, `needLoadMoreData`/`needLoadMoreDataTop` events, full public API
- **Column Pinning** — freeze columns left or right with CSS `position: sticky`, programmatic API, save/restore state
- **Column Moving** — HTML5 native drag-and-drop column reordering
- **CSV Export** — download visible/selected/all rows with formula-injection protection, full option matrix (separator, header filter, field callbacks, BOM compatibility)
- **Excel Export** — ExcelBuilder-compatible sheet data with native numeric/boolean types preserved, configurable filename/sheet/header/custom formatters
- **PDF Export** — pdfMake-ready document definition with orientation/page size/styles/header/footer/custom formatter, auto-download when pdfMake is available
- **CSV/JSON Import** — file picker or programmatic import, full CSV parser (quoted values, escaped quotes, CRLF), header-to-column mapping, integrates with row-edit for dirty marking
- **Export/Import Menu** — `buildGridExporterMenuItems()` with per-format and per-scope flags, i18n-driven menu labels
- **Virtual Scrolling** — virtual scroll viewport, auto-enabled at 40+ rows
- **Save/Restore State** — serialize and restore sort, filters, grouping, collapsed groups, pinning, column order, column widths, pagination, selection, focused cell, tree/expandable expansion, and scroll position (per-field opt-in flags)
- **Auto Resize** — ResizeObserver-driven viewport height recalculation
- **Custom Cell Templates** — Angular `ng-template`, React `cellRenderers` map (per-column render functions), vanilla `<template>` slots with slot-based portal projection
- **Shadow DOM** — encapsulated styles with CSS custom property and `::part()` hooks
- **Web Component** — ships as `<ui-grid-element>`, a framework-free vanilla custom element (`@ornery/ui-grid-vanilla`); Angular and React wrappers mount this same element and project templates into it
- **Feature-Flag Builds** — compile-time tree-shaking of unused features
- **i18n** — 6 locales built-in (English, Spanish, French, German, Japanese, Simplified Chinese), runtime `gridApi.i18n` for language switching, fallback chain (`options.labels` → current locale → en-US), register additional locales via `gridApi.i18n.add()`
- **SSR Support** — server-side rendering with platform-safe guards

---

## Theming

The grid renders inside Shadow DOM. Customize it via the public `--ui-grid-*` CSS custom properties:

```css
.my-app {
  --ui-grid-surface: #1e1b2e;
  --ui-grid-accent: #8b5cf6;
  --ui-grid-header-background: #2d2640;
  --ui-grid-cell-color: #e2e0f0;
  --ui-grid-border-color: rgba(139, 92, 246, 0.2);
  --ui-grid-row-hover: #322e4a;
}
```

Legacy `--app-ui-grid-*` aliases remain supported as a fallback for older app themes, but new consumer theming should target only `--ui-grid-*`.

Target structural elements with `::part()`:

```css
app-ui-grid::part(header) {
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
```

See [docs/theming.md](./docs/theming.md) for the full CSS variable reference grouped by grid section, `::part()` hooks, and the demo app's 4-mode theme system.

---

## Custom Builds

Ship only the features you use:

```bash
# Only sorting and filtering — everything else is tree-shaken
node scripts/build-grid.mjs --features sorting,filtering

# Bake in a locale at build time
node scripts/build-grid.mjs --features sorting,filtering,pagination --locale i18n/fr-FR.json

# See all available flags
node scripts/build-grid.mjs --list
```

See [docs/custom-builds.md](./docs/custom-builds.md) for the full feature flag table and build presets.

---

## Documentation

| Guide                                             | Description                                            |
| ------------------------------------------------- | ------------------------------------------------------ |
| [Getting Started](./docs/getting-started.md)      | Install, minimal setup, run the demo                   |
| [Features](./docs/features.md)                    | Overview of all features with code examples            |
| [Theming](./docs/theming.md)                      | CSS custom properties, `::part()` hooks, sample themes |
| [API Reference](./docs/api-reference.md)          | GridOptions, GridColumnDef, UiGridApi                  |
| [Cell Editing](./docs/cell-editing.md)            | Keyboard navigation, conditional editing, API          |
| [Tree View](./docs/tree-view.md)                  | Hierarchical data, options, API                        |
| [Expandable Rows](./docs/expandable-rows.md)      | Master/detail, template context, API                   |
| [Custom Builds](./docs/custom-builds.md)          | Feature flags, build presets, locale baking            |
| [Web Component](./docs/web-component.md)          | Vanilla `<ui-grid-element>` custom element usage       |
| [Internationalization](./docs/i18n.md)            | Runtime overrides, build-time locales                  |
| [Accessibility](./docs/accessibility.md)          | ARIA roles, keyboard navigation, screen reader support |
| [Rust Project](https://github.com/orneryd/uiGrid) | Rust/WASM implementation, egui, and native adapters    |

Interactive versions of all web documentation are also available in the [live demo](https://orneryd.github.io/ui-grid/).

---

## Development

```bash
npm start          # Dev server at localhost:4200
npm test           # Run tests (Vitest)
npm run build      # Production build
npm run build:library   # Build the library (ng-packagr)
npm run start:vanilla   # Run the framework-neutral browser demo at 127.0.0.1:4174
```

---

## Compatibility

| Dependency | Version |
| ---------- | ------- |
| Angular    | 21.2    |
| TypeScript | 5.9     |
| RxJS       | 7.8     |
| Node       | 22.20   |
| npm        | 11.11   |

---

## Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/my-feature`)
3. Run `npm test` to ensure all tests pass
4. Submit a pull request

---

## License

[MIT](./LICENSE.md)

---

### Overview (`projects/ui-grid-solid/README.md`)

# @ornery/ui-grid-solid

SolidJS adapter for the existing uiGrid web component. Grid behavior, state,
rendering and event subscriptions stay in the vanilla renderer and core.

## Install

```sh
npm install @ornery/ui-grid-solid @ornery/ui-grid-core @ornery/ui-grid-vanilla solid-js@2.0.0-rc.4 @solidjs/web@2.0.0-rc.4
```

This package is developed against `solid-js@2.0.0-rc.4`. SolidJS 1 is not supported.
Use a SolidJS JSX compiler/plugin with `jsxImportSource: "@solidjs/web"`.

```tsx
import { createSignal } from 'solid-js';
import { UiGrid, type UiGridApi } from '@ornery/ui-grid-solid';

export function App() {
  const [rows, setRows] = createSignal([{ id: 1, name: 'Alice' }]);
  let api: UiGridApi | undefined;
  return <div style={{ height: '400px' }}>
    <button onClick={() => setRows([{ id: 1, name: 'Bob' }])}>Update</button>
    <UiGrid
      options={{
        id: 'users', data: rows(), columnDefs: [{ name: 'name' }],
        rowIdentity: (row) => String(row['id']), enableSorting: true,
      }}
      onRegisterApi={(value) => { api = value; }}
      onError={(error) => console.error(error)}
    />
  </div>;
}
```

`options` uses the existing `GridOptions` type. Replace options/data through
signals; mutating a plain array in place does not notify Solid. Top-level
reactive option getters are tracked. Updates assign options to the same element,
preserving the controller's interactive state. Give the parent a definite height.

`onRegisterApi` receives a compatible `UiGridApi` whose mutating operations also
refresh the Solid renderer bridges; `options.onRegisterApi` also runs. As in
vanilla, registration callbacks may run again on options updates.
Use `api.core.on` and other existing API event groups for subscriptions, and
dispose application-owned subscriptions with Solid's `onCleanup`.

## Lifecycle and SSR

The server export renders only a host div. The vanilla module is dynamically
imported inside a client effect. Disposing the component cancels pending mount
work and removes the element, invoking vanilla's `disconnectedCallback` cleanup.
Initialization failures call `onError` when supplied, otherwise surface globally.

**Correction to the contribution plan:** the published `solid-js@2.0.0-rc.4`
package does not export `onMount`. This adapter uses SolidJS's two-argument
`createEffect` with a constant client guard for mounting, and a separate tracked
effect for options. It does not use `onSettled` as a mount hook.

Source exports use the `solid` condition so SolidStart can compile for its target.
Precompiled browser and server ESM exports are also included.

The [`examples/solid-start`](examples/solid-start) route example covers server
rendering, a reactive data update after hydration, and route navigation that
unmounts and remounts the grid.

## Local development

```sh
npm ci --prefix projects/ui-grid-solid
npm run build:solid
npm run test:solid
npm run start:solid
```

`build:solid` builds core, vanilla, then Solid using a cross-platform Node script.
In PowerShell environments that block npm.ps1, use `npm.cmd` for these commands.

## Solid JSX cells

Pass `cellRenderers`, keyed by column name:

```tsx
const cellRenderers: UiGridCellRenderers = {
  name: (context) => <strong>{String(context.value)}</strong>,
};
// Import UiGridCellRenderers from @ornery/ui-grid-solid.
<UiGrid options={options()} cellRenderers={cellRenderers} />;
```

Each renderer runs under the grid's Solid context and receives reactive getters
for `value`, `$implicit`, `row`, `column`, and `rowIndex`. Read these getters
inside JSX or tracked computations; do not destructure them into plain values.
Cell-local signals survive value updates while the slot remains mounted.

The bridge follows vanilla slot events and API row updates. Removed/virtualized
cells dispose their Solid roots. Replacing a renderer function remounts its
cells; removing it restores vanilla rendering. Keep renderer functions stable
when local state should survive. Grid teardown removes all renderer roots and
the bridge's API subscription.

Renderer failures are reported through `onError`. The failed cell stays isolated
from the grid and remounts on the next options update, allowing it to recover.
The API returned by `onRegisterApi` wraps `core.benchmark()` so its result and
`benchmarkComplete` event include completion of the Solid renderer bridge.

The demo loads 100,000 rows across three columns by default. Click **Move Name column** to exercise
`api.core.moveColumn()`.

## Solid JSX headers

Pass `headerRenderers`, also keyed by column name:

```tsx
const headerRenderers: UiGridHeaderRenderers = {
  name: (context) => <strong>{context.value} (Solid)</strong>,
};
// Import UiGridHeaderRenderers from @ornery/ui-grid-solid.
<UiGrid options={options()} headerRenderers={headerRenderers} />;
```

Header context exposes reactive `value`, `$implicit`, and `column` getters.
Local signals survive title and column option updates while the header remains
mounted. Hiding or removing a column disposes its root; showing it mounts a new
root. Renderer failures call `onError` and retry on the next options update.

Clicking a projected header cycles sorting through ascending, descending, and
none. An interactive child can call `event.preventDefault()` to suppress that
sort for its click. Programmatic column movement through
`api.core.moveColumn()` keeps projected headers aligned with the grid. A custom
header replaces the vanilla header contents, so it should render any additional
controls the application needs.

## Solid JSX expandable details

Pass one `detailRenderer` and enable expandable rows:

```tsx
const detailRenderer: UiGridDetailRenderer = (context) => (
  <section>
    <strong>{String(context.row['name'])}</strong>
    <span>Row {context.rowIndex}</span>
  </section>
);

<UiGrid
  options={{
    ...options(),
    enableExpandable: true,
    expandableRowHeight: 120,
  }}
  detailRenderer={detailRenderer}
/>;
```

Import `UiGridDetailRenderer` from `@ornery/ui-grid-solid`. The renderer receives
reactive `row`, `$implicit`, `rowIndex`, and `expanded` getters. Its Solid root
stays mounted while the same expanded detail remains visible, so local state
survives data updates, sorting, and index changes.

Use the existing `api.expandable.toggleRowExpansion()`, `expandAllRows()`, and
`collapseAllRows()` operations to control details. Collapsing, filtering out,
virtualizing out, or removing a row disposes its detail root. Errors are isolated
through `onError` and retry on the next options update.

## Solid JSX cell editors

Pass `cellEditors`, keyed by column name, and enable editing for the grid and
column:

```tsx
const cellEditors: UiGridCellEditors = {
  role: (context) => <select
    value={context.value}
    onChange={(event) => context.setValue(event.currentTarget.value)}
  >
    <option value="Engineer">Engineer</option>
    <option value="Designer">Designer</option>
  </select>,
};

<UiGrid
  options={{
    ...options(),
    enableCellEdit: true,
    columnDefs: [{ name: 'role', enableCellEdit: true }],
  }}
  cellEditors={cellEditors}
/>;
```

Import `UiGridCellEditors` from `@ornery/ui-grid-solid`. The reactive context
contains `value`, `$implicit`, `row`, `column`, `rowIndex`, `setValue`, `commit`,
and `cancel`. The first input, select, textarea, or focusable element receives
focus when editing starts.

Enter commits, Escape cancels, and moving focus outside the editor commits.
The Solid editor bridge follows the public editing events and uses vanilla's
internal editor input to synchronize values. The existing Grid API remains
responsible for parsing typed values, updating nested fields, raising edit
events, validation, and restoring cell rendering.
Buttons can invoke `context.commit()` or `context.cancel()` directly. The Solid
root is disposed whenever the edit session ends or the grid unmounts. Renderer
failures call `onError` and retry on the next options update.

---

### Overview (`projects/ui-grid-vanilla/README.md`)

# @ornery/ui-grid-vanilla

A vanilla Web Components wrapper for `@ornery/ui-grid-core`. Renders a high-performance data grid as a custom element with no framework dependency.

## Architecture

The grid is built as a composition of autonomous custom elements, each following the `@ornery/web-components` pattern:

```
<ui-grid-element>                  ← main grid element (shadow DOM)
  ├── ui-grid-shell.html           ← declarative grid layout template
  ├── ui-grid-empty.html           ← empty/no-options state template
  │
  ├── <ui-grid-header-cell>        ← column header (sort, group, pin, resize)
  ├── <ui-grid-filter-cell>        ← column filter input (shadow DOM + .html template)
  ├── <ui-grid-body-cell>          ← data cell (class-managed, perf-optimized)
  ├── <ui-grid-group-row>          ← grouping disclosure row (shadow DOM + .html template)
  ├── <ui-grid-pagination>         ← pagination controls (shadow DOM + .html template)
  └── <ui-grid-template>           ← declarative reusable template factory
```

### Component Pattern

Components follow the standard `@ornery/web-components` lifecycle:

```js
import template from './my-component.html';

class MyComponent extends HTMLElement {
  // 1. Attach shadow DOM in constructor
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  // 2. Declare instance properties as template context
  myProp = 'default';

  // 3. Render in connectedCallback
  connectedCallback() {
    this.myProp = this.getAttribute('data-my-prop') ?? 'default';
    template(this).connect(); // auto-renders into shadowRoot
  }
}
```

The `.html` template uses ES6 template literal syntax:

```html
<div class="my-class">${this.myProp}</div>
```

Templates are compiled at build time by `@ornery/web-components/vite` (or `/webpack`, `/esbuild`, `/rollup`).

### Performance Strategy

- **Hot-path components** (`ui-grid-body-cell`, `ui-grid-header-cell`) skip `template().connect()` entirely — the parent pre-renders innerHTML as a string, and the component only manages CSS classes via `attributeChangedCallback`. This avoids DOM construction overhead per cell.
- **Low-frequency components** (`ui-grid-filter-cell`, `ui-grid-group-row`, `ui-grid-pagination`) use full `template(this).connect()` with shadow DOM for encapsulation and declarative rendering.

### Template Bindings

Cell and row slot templates support dual binding syntax:

| Syntax  | Example         | Description                                           |
| ------- | --------------- | ----------------------------------------------------- |
| `{{ }}` | `{{value}}`     | Mustache-style, original grid syntax                  |
| `${ }`  | `${this.value}` | ES6 template literal, `@ornery/web-components` native |

Both resolve against the same context: `{ $implicit, value, valueText, row, column, rowIndex }`.

## Installation

```bash
npm install @ornery/ui-grid-vanilla @ornery/ui-grid-core
```

## Usage

### No-Bundler Browser Usage

Use the browser bundle when a static HTML host needs to consume the grid without Vite, Webpack, tsup, or another package-aware build step. The bundle includes `@ornery/ui-grid-core` and registers `<ui-grid-element>` when the module loads.

After `npm run build:vanilla` from the repo root, the browser-ready file is written to `projects/ui-grid-vanilla/dist/browser/ui-grid-element.js`.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>UI Grid Browser Bundle</title>
    <script type="module" src="./ui-grid-element.js"></script>
  </head>
  <body>
    <ui-grid-element
      grid-id="static-grid"
      enable-sorting
      enable-filtering
      column-defs='[{"name":"name"},{"name":"role"},{"name":"salary","type":"number","align":"end"}]'
      data='[{"name":"Alice","role":"Engineer","salary":120000},{"name":"Bob","role":"Designer","salary":95000}]'
    >
    </ui-grid-element>
  </body>
</html>
```

For imperative setup in the same no-bundler environment, import from that same file:

```html
<div id="grid"></div>

<script type="module">
  import { mountVanillaUiGrid } from './ui-grid-element.js';

  await mountVanillaUiGrid(document.querySelector('#grid'), {
    id: 'static-mounted-grid',
    columnDefs: [{ name: 'name' }, { name: 'role' }],
    data: [{ name: 'Alice', role: 'Engineer' }],
    enableSorting: true,
    enableFiltering: true,
  });
</script>
```

The package `dist/index.js` file is CommonJS for Node/package consumers, and `dist/index.mjs` is package ESM for bundlers or import-map setups. Use `dist/browser/ui-grid-element.js` for direct browser loading.

### Imperative (JavaScript)

```js
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';

const grid = await mountVanillaUiGrid(document.getElementById('grid'), {
  id: 'my-grid',
  columnDefs: [
    { name: 'name', field: 'name', displayName: 'Name' },
    { name: 'age', field: 'age', displayName: 'Age', align: 'end' },
  ],
  data: [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 },
  ],
  enableSorting: true,
  enableFiltering: true,
});
```

### Declarative (HTML attributes)

```html
<ui-grid-element
  grid-id="my-grid"
  enable-sorting
  enable-filtering
  column-defs='[{"name":"name","field":"name"},{"name":"age","field":"age","align":"end"}]'
  data='[{"name":"Alice","age":30},{"name":"Bob","age":25}]'
>
</ui-grid-element>
```

### Custom Cell Templates (slots)

```html
<ui-grid-element id="grid">
  
  <template slot="cell-name">
    <strong>{{value}}</strong>
  </template>

  
  <template slot="cell-age">
    <span style="color: ${this.value > 30 ? 'red' : 'green'}">${this.value}</span>
  </template>
</ui-grid-element>
```

### Declarative Reusable Templates

Define reusable components from HTML alone with `<template is="ui-grid-template">`:

```html

<template is="ui-grid-template" name="ui-status-badge" status="unknown">
  <style>
    :host {
      display: inline-block;
    }
    .badge {
      padding: 2px 8px;
      border-radius: 4px;
    }
    .badge-active {
      background: #d1fae5;
      color: #065f46;
    }
    .badge-inactive {
      background: #fee2e2;
      color: #991b1b;
    }
  </style>
  <span class="badge badge-${this.status}">${this.status}</span>
</template>


<ui-grid-element>
  <template slot="cell-status">
    <ui-status-badge status="{{value}}"></ui-status-badge>
  </template>
</ui-grid-element>
```

## Bundler Setup

### Vite

```js
import { defineConfig } from 'vite';
import webComponents from '@ornery/web-components/vite';

export default defineConfig({
  plugins: [webComponents()],
});
```

### Webpack 5

```js
import webComponents from '@ornery/web-components/webpack';

export default {
  plugins: [webComponents()],
};
```

### tsup (esbuild)

```ts
import { defineConfig } from 'tsup';
import webComponents from '@ornery/web-components/esbuild';

export default defineConfig({
  esbuildPlugins: [webComponents()],
});
```

## API

### `mountVanillaUiGrid(target, options, wasmModule?, tagName?)`

Mount a grid into a target element. Returns the grid element.

### `defineStandaloneUiGridElement(tagName?)`

Register the `<ui-grid-element>` custom element (and all sub-components).

### `UIGridTemplate`

Exported class for the `<template is="ui-grid-template">` element. Auto-registered when the grid is defined.

### Grid Element Properties

See `VanillaUiGridElement` type — all `GridOptions` fields are available as both HTML attributes and JS properties.

---

### Overview (`openspec/changes/fix-wasm-runtime-loading/README.md`)

# fix-wasm-runtime-loading

Bundle and initialize the UI Grid WASM runtime through published package dependencies instead of application-relative demo assets.

---

### Overview (`projects/ui-grid-solid/examples/solid-start/README.md`)

# SolidStart v2 example

Create a SolidStart v2 application, install the four packages shown in the
adapter README, then copy `src/routes/index.tsx` and `src/routes/about.tsx` into
the application's routes directory. SolidStart renders the grid host on the
server; the adapter imports and mounts the vanilla custom element after
hydration in the browser.

The links exercise route navigation. Leaving `/` unmounts the grid, including
its renderer roots and subscriptions. Returning to `/` creates a fresh grid.

SolidStart v2 uses `vite.config.ts` with `solidStart()` and a deployment plugin.
Keep the generated configuration from the SolidStart scaffold; this example
only replaces the route files.

---

### Getting Started (`docs/getting-started.md`)

# Getting Started

Get up and running with `@ornery/ui-grid` in under five minutes.

## Install

```bash
npm install @ornery/ui-grid
```

**Peer dependencies:** `@angular/core`, `@angular/common`, `rxjs`, `tslib`.

## Minimal Angular Setup

The grid ships as a standalone component — no module imports required:

```typescript
import { Component } from '@angular/core';
import { GridOptions, UiGridComponent } from '@ornery/ui-grid';

@Component({
  selector: 'app-my-grid',
  imports: [UiGridComponent],
  template: `<app-ui-grid [options]="gridOptions" />`,
})
export class MyGridComponent {
  gridOptions: GridOptions = {
    id: 'my-grid',
    data: [
      { name: 'Alice', role: 'Engineer', salary: 120000 },
      { name: 'Bob', role: 'Designer', salary: 95000 },
    ],
    columnDefs: [
      { name: 'name' },
      { name: 'role' },
      { name: 'salary', type: 'number', align: 'end' },
    ],
  };
}
```

## Required GridOptions Fields

| Field        | Type                       | Description                                                 |
| ------------ | -------------------------- | ----------------------------------------------------------- |
| `id`         | `string`                   | Unique grid identifier (used for CSV filenames and row IDs) |
| `data`       | `readonly GridRecord[]`    | Array of row objects                                        |
| `columnDefs` | `readonly GridColumnDef[]` | Column definitions — each needs at minimum a `name`         |

## React

```bash
npm install @ornery/ui-grid-react @ornery/ui-grid-core
```

```tsx
import { UiGrid } from '@ornery/ui-grid-react';
import type { GridOptions } from '@ornery/ui-grid-core';

const options: GridOptions = {
  id: 'react-grid',
  data: [{ name: 'Alice', role: 'Engineer' }],
  columnDefs: [{ name: 'name' }, { name: 'role' }],
};

function App() {
  return <UiGrid options={options} />;
}
```

## Web Components

### Vanilla (`@ornery/ui-grid-vanilla`)

Framework-free, pure DOM with Shadow DOM. zero dependencies:

```bash
npm install @ornery/ui-grid-vanilla @ornery/ui-grid-core
```

Declarative setup now works directly in HTML:

```html
<ui-grid-element
  grid-id="vanilla-demo"
  enable-sorting
  enable-filtering
  column-defs='[{"name":"name"},{"name":"role"}]'
  data='[{"name":"Alice","role":"Engineer"}]'
>
</ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement();
</script>
```

Or bind the same fields as individual JS properties:

For static HTML hosts with no package-aware build step, use the browser bundle emitted by `npm run build:vanilla`:

```html
<script type="module" src="./ui-grid-element.js"></script>

<ui-grid-element
  grid-id="static-grid"
  enable-sorting
  enable-filtering
  column-defs='[{"name":"name"},{"name":"role"}]'
  data='[{"name":"Alice","role":"Engineer"}]'
>
</ui-grid-element>
```

The repo build writes this file to `projects/ui-grid-vanilla/dist/browser/ui-grid-element.js`. It includes `@ornery/ui-grid-core` and auto-registers the element.

```html
<ui-grid-element id="my-grid"></ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement();

  const grid = document.querySelector('#my-grid');
  grid.gridId = 'vanilla-props';
  grid.enableSorting = true;
  grid.enableFiltering = true;
  grid.columnDefs = [{ name: 'name' }, { name: 'role' }];
  grid.data = [{ name: 'Alice', role: 'Engineer' }];
</script>
```

The original bulk `options` property remains available when you need callbacks or function-valued configuration:

```html
<ui-grid-element id="my-grid"></ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement();

  document.querySelector('#my-grid').options = {
    id: 'vanilla-demo',
    data: [{ name: 'Alice', role: 'Engineer' }],
    columnDefs: [{ name: 'name' }, { name: 'role' }],
  };
</script>
```

## Run the Demo Locally

```bash
git clone https://github.com/orneryd/ui-grid.git
cd ui-grid
npm ci
npm start
```

Open `http://localhost:4200` to see the full demo with 100,000 rows, theming, and all features active. The live demo includes dedicated pages for Angular, React, and Web Components.

## Next Steps

- [Features](./features.md) — see everything the grid can do
- [Theming](./theming.md) — customize colors and layout via CSS custom properties
- [API Reference](./api-reference.md) — full GridOptions, GridColumnDef, and UiGridApi documentation
- [Web Component](./web-component.md) — framework-neutral custom element usage
- [Rust Project](https://github.com/orneryd/uiGrid) — Rust/WASM implementation, egui, and native adapters

---

### Accessibility (`docs/accessibility.md`)

# Accessibility

UI Grid is built with accessibility as a core concern. The grid applies WAI-ARIA grid semantics, supports full keyboard navigation, manages focus across editing and scrolling, and routes all visible text through the i18n system so screen reader announcements localize automatically.

## ARIA Roles

The grid maps its structure to the [WAI-ARIA grid pattern](https://www.w3.org/WAI/ARIA/apd/pattern/grid/):

| Element | Role | Notes |
|---------|------|-------|
| Grid container | `grid` | Top-level landmark; `aria-label` set from `options.title` (defaults to `"Data grid"`) |
| Header row | `row` | Contains column headers |
| Header cell | `columnheader` | One per visible column |
| Body container | `rowgroup` | Groups all data rows |
| Data row | `row` | One per visible record |
| Data cell | `gridcell` | Contains cell content or editor |
| Group row | `row` | Collapsible group header with `aria-expanded` |
| Pagination | `navigation` | `aria-label` from `labels.paginationPage` |

When tree view or expandable rows are active, `aria-expanded` is set on the toggle buttons to communicate open/closed state to assistive technology.

## aria-* Attributes

| Attribute | Where | Value |
|-----------|-------|-------|
| `aria-label` | Grid container | `options.title` or `"Data grid"` |
| `aria-label` | Sort buttons | Dynamic: `"Sort"`, `"Sort ascending"`, `"Sort descending"` (from i18n labels) |
| `aria-label` | Group toggle buttons | `"Group by this column"` / `"Remove grouping"` |
| `aria-label` | Tree toggle buttons | `"Collapse"` / `"Expand"` |
| `aria-label` | Expand detail buttons | `"Expand row"` / `"Collapse row"` |
| `aria-label` | Filter inputs | `"Filter {column name}"` |
| `aria-label` | Cell editor input | Column display name |
| `aria-label` | Pagination nav | `"Page"` |
| `aria-label` | Previous/Next buttons | `"Previous"` / `"Next"` |
| `aria-sort` | Column headers | `"ascending"`, `"descending"`, or `"none"` |
| `aria-expanded` | Tree/expand/group toggles | `true` or `false` |
| `aria-hidden` | Decorative SVG icons | `true` (with `focusable="false"`) |

All `aria-label` values are sourced from the i18n `GridLabels` object, so they localize automatically when you provide translated labels.

## Keyboard Navigation

Every data cell receives `tabindex="0"`, making the grid fully keyboard-navigable. The following keys are handled:

### Grid Navigation

| Key | Action |
|-----|--------|
| `Arrow Left` | Move focus one cell left (wraps to previous row) |
| `Arrow Right` | Move focus one cell right (wraps to next row) |
| `Arrow Up` | Move focus one row up |
| `Arrow Down` | Move focus one row down |
| `Tab` | Move to next cell (same as Arrow Right) |
| `Shift + Tab` | Move to previous cell (same as Arrow Left) |
| `Enter` | Move down one row (Shift+Enter = up) |

### Cell Editing

| Key | Action |
|-----|--------|
| `F2` | Begin editing the focused cell (if editable) |
| `Backspace` / `Delete` | Clear cell value and enter edit mode |
| Any printable character | Begin editing with that character as initial input |
| `Escape` | Cancel edit, restore original value, return focus to cell |
| `Enter` | Commit edit, move focus down (Shift = up) |
| `Tab` | Commit edit, move focus to next cell (Shift = previous) |

A "printable character" is any single-character key pressed without Ctrl, Meta, or Alt modifiers.

## Focus Management

Focus management operates inside the grid's Shadow DOM:

- **Cell focus** — after keyboard navigation or virtual scroll repositioning, `focusGridRenderedCell()` queries the shadow root for the target cell by `data-row-id` and `data-col-name` attributes. A `requestAnimationFrame` retry handles cases where Angular hasn't rendered the target cell yet.
- **Editor focus** — when a cell enters edit mode, `focusGridEditor()` finds the editor input in the shadow root and calls `input.select()` to select all text.
- **Cancellation** — a focus token prevents stale focus calls from previous navigation actions.

## Screen Reader Support

Visually hidden text (`.sr-only` / `.ui-grid-sr-only`) provides labels where icon-only buttons or implicit context would otherwise leave screen reader users without information:

- Sort buttons include hidden text matching the `aria-label`
- Group toggle buttons include hidden text for the action
- Filter cells include a hidden `"Filter {column name}"` label
- Group disclosure rows include hidden `"Collapse group"` / `"Expand group"` text
- Pagination Previous/Next icon buttons include hidden text

The `.sr-only` class uses the standard clip technique (1px box, `overflow: hidden`, `clip: rect(0,0,0,0)`), available as both `.sr-only` and `.ui-grid-sr-only` in the grid's Shadow DOM styles.

## Localized Announcements

Every screen reader label flows through the `GridLabels` i18n system. When you provide translated labels (via `GridOptions.labels` at runtime or a baked-in locale at build time), all ARIA labels and screen reader text update automatically. See the [Internationalization](./i18n.md) guide for details.

## Theming and Contrast

The grid's CSS custom property system supports high-contrast themes:

- Override `--ui-grid-cell-color`, `--ui-grid-muted-color`, `--ui-grid-surface`, and `--ui-grid-accent` to meet WCAG contrast ratios
- The `data-color-mode` / `data-visual-mode` attribute pattern (used in the demo app) demonstrates how to provide accessible light and dark modes without JavaScript theme-switching logic
- Focus indicators use the `--ui-grid-accent` color for sort indicators and active states

## Custom Cell Templates

When providing custom cell templates via `cellTemplate` or `cellRenderer`, ensure your content is accessible:

- Use semantic HTML elements where possible
- Add `aria-label` or `aria-labelledby` to interactive elements
- Ensure custom controls are keyboard-reachable (`tabindex="0"` on interactive elements)
- Maintain sufficient color contrast in custom-rendered content

---

### Api Reference (`docs/api-reference.md`)

# API Reference

Complete reference for `GridOptions`, `GridColumnDef`, and the `UiGridApi` runtime surface.

All types are exported from `@ornery/ui-grid-core`. The Angular package (`@ornery/ui-grid`) re-exports them for convenience.

## Vanilla Custom Element Surface

The vanilla web component (`@ornery/ui-grid-vanilla`) supports three equivalent configuration styles for the subset of `GridOptions` that can be expressed without functions:

### Declarative attributes

```html
<ui-grid-element
  grid-id="accounts"
  title="Accounts"
  enable-sorting
  enable-filtering
  row-height="44"
  column-defs='[{"name":"name"},{"name":"status"}]'
  data='[{"name":"Acme","status":"Active"}]'
>
</ui-grid-element>
```

### Individual JS properties

```javascript
const grid = document.querySelector('ui-grid-element');
grid.gridId = 'accounts';
grid.title = 'Accounts';
grid.enableSorting = true;
grid.enableFiltering = true;
grid.rowHeight = 44;
grid.columnDefs = [{ name: 'name' }, { name: 'status' }];
grid.data = [{ name: 'Acme', status: 'Active' }];
```

### Full `options` object

```javascript
grid.options = {
  id: 'accounts',
  data: [{ name: 'Acme', status: 'Active' }],
  columnDefs: [{ name: 'name' }, { name: 'status' }],
  enableSorting: true,
  enableFiltering: true,
  onRegisterApi: (api) => {
    window.gridApi = api;
  },
};
```

Use attributes or individual properties for static configuration. Use `options` for callbacks like `onRegisterApi`, function-valued column definitions such as `valueGetter`, `sortingAlgorithm`, and `cellRenderer`, or when you prefer a single bulk assignment.

The vanilla merge order is:

```text
{ ...attributeOptions, ...jsPropertyOptions }
```

That means explicit JS property values win over declarative attributes when both are present.

## GridOptions

| Field                        | Type                                               | Default    | Description                                |
| ---------------------------- | -------------------------------------------------- | ---------- | ------------------------------------------ |
| `id`                         | `string`                                           | required   | Unique grid identifier                     |
| `data`                       | `readonly GridRecord[]`                            | required   | Row data array                             |
| `columnDefs`                 | `readonly GridColumnDef[]`                         | required   | Column definitions                         |
| `title`                      | `string`                                           | —          | Grid heading text                          |
| `rowHeight`                  | `number`                                           | 44         | Row height in pixels                       |
| `headerRowHeight`            | `number`                                           | —          | Header row height override                 |
| `emptyMessage`               | `string`                                           | —          | Message when no rows match                 |
| `labels`                     | `Partial<GridLabels>`                              | en-US      | i18n string overrides                      |
| `enableSorting`              | `boolean`                                          | true       | Enable column sorting                      |
| `enableFiltering`            | `boolean`                                          | true       | Enable filter row                          |
| `enableGrouping`             | `boolean`                                          | false      | Enable row grouping                        |
| `grouping`                   | `{ groupBy?: string[]; startCollapsed?: boolean }` | —          | Grouping configuration                     |
| `enableColumnMoving`         | `boolean`                                          | false      | Enable drag-and-drop column reorder        |
| `enablePinning`              | `boolean`                                          | —          | Enable column pinning (freeze left/right)  |
| `enableVirtualization`       | `boolean`                                          | auto       | virtual scroll (auto at 40+ rows)      |
| `virtualizationThreshold`    | `number`                                           | 40         | Row count that triggers virtualization     |
| `enablePagination`           | `boolean`                                          | false      | Enable pagination                          |
| `enablePaginationControls`   | `boolean`                                          | true       | Show pagination UI                         |
| `useExternalPagination`      | `boolean`                                          | false      | Grid skips local slicing                   |
| `paginationPageSize`         | `number`                                           | —          | Rows per page                              |
| `paginationPageSizes`        | `number[] \| null`                                 | —          | Page size selector options                 |
| `paginationCurrentPage`      | `number`                                           | —          | Initial page                               |
| `totalItems`                 | `number`                                           | —          | Total for external pagination              |
| `enableExpandable`           | `boolean`                                          | false      | Enable expandable detail rows              |
| `expandableRowHeight`        | `number`                                           | 150        | Detail row height                          |
| `expandableRowHeaderWidth`   | `number`                                           | —          | Width of the expand toggle column          |
| `expandableRowTemplate`      | `TemplateRef`                                      | —          | Angular template for detail content        |
| `expandableRowScope`         | `Record<string, unknown>`                          | —          | Extra template context variables           |
| `enableTreeView`             | `boolean`                                          | false      | Enable hierarchical tree display           |
| `treeChildrenField`          | `string`                                           | 'children' | Property name for child rows               |
| `treeIndent`                 | `number`                                           | 10         | Pixels per indent level                    |
| `showTreeExpandNoChildren`   | `boolean`                                          | true       | Show toggle for childless rows             |
| `treeRowHeaderAlwaysVisible` | `boolean`                                          | false      | Always show expand column                  |
| `enableCellEdit`             | `boolean`                                          | false      | Enable inline cell editing                 |
| `enableCellEditOnFocus`      | `boolean`                                          | false      | Enter edit mode on cell focus              |
| `cellEditableCondition`      | `boolean \| (ctx) => boolean`                      | —          | Grid-level edit guard                      |
| `enableAutoResize`           | `boolean`                                          | false      | ResizeObserver-driven viewport height      |
| `infiniteScrollRowsFromEnd`  | `number`                                           | —          | Threshold rows from end to trigger load    |
| `infiniteScrollUp`           | `boolean`                                          | —          | Enable upward infinite scroll              |
| `infiniteScrollDown`         | `boolean`                                          | —          | Enable downward infinite scroll            |
| `rowIdentity`                | `(row, i) => string`                               | —          | Custom row ID function                     |
| `benchmark`                  | `{ iterations?: number }`                          | —          | Render benchmark configuration             |
| `onRegisterApi`              | `(api) => void`                                    | —          | Callback to receive the UiGridApi instance |

## GridColumnDef

| Field                   | Type                                                                                                                                                              | Description                                                   |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `name`                  | `string`                                                                                                                                                          | Column identifier (also the default data key)                 |
| `displayName`           | `string`                                                                                                                                                          | Header label (auto-titleized from name if absent)             |
| `field`                 | `string`                                                                                                                                                          | Dot-path into data record (e.g. `'address.city'`)             |
| `type`                  | `'string' \| 'number' \| 'boolean' \| 'date' \| 'object'`                                                                                                         | Data type hint for sorting and editing                        |
| `width`                 | `string`                                                                                                                                                          | CSS grid track value (e.g. `'200px'`, `'minmax(10rem, 1fr)'`) |
| `align`                 | `'start' \| 'center' \| 'end'`                                                                                                                                    | Cell text alignment                                           |
| `visible`               | `boolean`                                                                                                                                                         | Hide column when false                                        |
| `sortable`              | `boolean`                                                                                                                                                         | Column-level sort override                                    |
| `enableSorting`         | `boolean`                                                                                                                                                         | Alias for sortable                                            |
| `filterable`            | `boolean`                                                                                                                                                         | Column-level filter override                                  |
| `enableFiltering`       | `boolean`                                                                                                                                                         | Alias for filterable                                          |
| `enableGrouping`        | `boolean`                                                                                                                                                         | Allow grouping by this column                                 |
| `enablePinning`         | `boolean`                                                                                                                                                         | Allow pinning this column                                     |
| `pinnedLeft`            | `boolean`                                                                                                                                                         | Pin this column to the left on init                           |
| `pinnedRight`           | `boolean`                                                                                                                                                         | Pin this column to the right on init                          |
| `enableCellEdit`        | `boolean`                                                                                                                                                         | Column-level edit override                                    |
| `enableCellEditOnFocus` | `boolean`                                                                                                                                                         | Edit starts on focus for this column                          |
| `cellEditableCondition` | `boolean \| (ctx) => boolean`                                                                                                                                     | Column-level edit guard                                       |
| `editModelField`        | `string`                                                                                                                                                          | Dot-path for writing edited values                            |
| `sort`                  | `{ direction?: SortDirection; priority?: number; ignoreSort?: boolean }`                                                                                          | Initial sort configuration                                    |
| `filter`                | `{ term?: unknown; condition?: FilterCondition \| RegExp \| Function; flags?: { caseSensitive?: boolean; date?: boolean }; rawTerm?: boolean; noTerm?: boolean }` | Filter configuration                                          |
| `sortingAlgorithm`      | `(a, b) => number`                                                                                                                                                | Custom sort comparator                                        |
| `formatter`             | `(value, row) => string`                                                                                                                                          | Display value formatter                                       |
| `valueGetter`           | `(row) => unknown`                                                                                                                                                | Custom value extractor                                        |
| `cellTemplate`          | `TemplateRef`                                                                                                                                                     | Angular template for cell content                             |
| `cellRenderer`          | `(ctx) => string`                                                                                                                                                 | HTML string cell renderer                                     |

## UiGridApi

Access the API via `onRegisterApi`. The API is organized into namespaces.

### core

| Method                            | Description                           |
| --------------------------------- | ------------------------------------- |
| `refresh()`                       | Force a full pipeline re-run          |
| `queueGridRefresh()`              | Alias for `refresh()`                 |
| `queueRefresh()`                  | Alias for `refresh()`                 |
| `refreshRows()`                   | Alias for `refresh()`                 |
| `getVisibleRows()`                | Get currently visible GridRow objects |
| `sortColumn(name, direction?)`    | Programmatic sort                     |
| `setFilter(name, value)`          | Set a column filter                   |
| `clearAllFilters()`               | Clear all active filters              |
| `groupByColumn(name)`             | Add/remove a column from grouping     |
| `clearGrouping()`                 | Remove all grouping                   |
| `moveColumn(from, to)`            | Reorder columns by index              |
| `exportCsv()`                     | Download visible rows as CSV          |
| `benchmark(iterations?)`          | Run render benchmark                  |
| `setRowInvisible(row, reason?)`   | Hide a row programmatically           |
| `clearRowInvisible(row, reason?)` | Un-hide a row                         |

### core.on (Events)

| Event                       | Callback Signature                 |
| --------------------------- | ---------------------------------- |
| `renderingComplete`         | `(api) => void`                    |
| `sortChanged`               | `(columnName, direction) => void`  |
| `filterChanged`             | `(filters) => void`                |
| `groupingChanged`           | `(groupBy) => void`                |
| `columnOrderChanged`        | `(order) => void`                  |
| `rowsVisibleChanged`        | `(rows) => void`                   |
| `rowsRendered`              | `(rows) => void`                   |
| `scrollBegin` / `scrollEnd` | `() => void`                       |
| `canvasHeightChanged`       | `(oldHeight, newHeight) => void`   |
| `gridDimensionChanged`      | `(oldH, oldW, newH, newW) => void` |
| `benchmarkComplete`         | `(result) => void`                 |

### pagination

| Method                                     | Description                |
| ------------------------------------------ | -------------------------- |
| `nextPage()` / `previousPage()`            | Navigate pages             |
| `seek(page)`                               | Jump to a specific page    |
| `setPageSize(n)`                           | Change rows per page       |
| `getPage()` / `getTotalPages()`            | Current page info          |
| `getFirstRowIndex()` / `getLastRowIndex()` | Row index range            |
| `on.paginationChanged`                     | `(page, pageSize) => void` |

### expandable

| Method                                  | Description               |
| --------------------------------------- | ------------------------- |
| `toggleRowExpansion(row)`               | Toggle a single row       |
| `expandAllRows()` / `collapseAllRows()` | Bulk expand/collapse      |
| `toggleAllRows()`                       | Toggle all rows           |
| `on.rowExpandedStateChanged`            | `(row, expanded) => void` |

### treeBase

| Method                                  | Description                |
| --------------------------------------- | -------------------------- |
| `toggleRowTreeState(row)`               | Toggle tree node expansion |
| `expandRow(row)` / `collapseRow(row)`   | Explicit expand/collapse   |
| `expandAllRows()` / `collapseAllRows()` | Bulk operations            |
| `getRowChildren(row)`                   | Get child GridRow objects  |
| `on.rowExpanded` / `on.rowCollapsed`    | `(row) => void`            |

### treeView

| Method               | Description                      |
| -------------------- | -------------------------------- |
| `getTreeView()`      | Get current tree expansion state |
| `setTreeView(state)` | Bulk set expansion state         |

### pinning

| Method                       | Description                                   |
| ---------------------------- | --------------------------------------------- |
| `pinColumn(name, direction)` | Pin a column left, right, or unpin (`'none'`) |
| `on.columnPinned`            | `(columnName, direction) => void`             |

### infiniteScroll

| Method                                      | Description                     |
| ------------------------------------------- | ------------------------------- |
| `dataLoaded(scrollUp?, scrollDown?)`        | Signal that new data is loaded  |
| `resetScroll(scrollUp?, scrollDown?)`       | Reset to fresh state            |
| `saveScrollPercentage()`                    | Persist scroll position ratio   |
| `setScrollDirections(up, down)`             | Toggle scroll directions        |
| `dataRemovedTop(scrollUp?, scrollDown?)`    | Signal rows removed from top    |
| `dataRemovedBottom(scrollUp?, scrollDown?)` | Signal rows removed from bottom |
| `on.needLoadMoreData`                       | `() => void`                    |
| `on.needLoadMoreDataTop`                    | `() => void`                    |

### edit

| Method / Event                    | Description                              |
| --------------------------------- | ---------------------------------------- |
| `beginCellEdit(row, col, event?)` | Start editing a cell                     |
| `endCellEdit()`                   | Commit the current edit                  |
| `cancelCellEdit()`                | Cancel without committing                |
| `getEditingCell()`                | Returns current editing position or null |
| `on.beginCellEdit`                | `(row, col, event) => void`              |
| `on.afterCellEdit`                | `(row, col, newVal, oldVal) => void`     |
| `on.cancelCellEdit`               | `(row, col) => void`                     |

### saveState

| Method           | Description                             |
| ---------------- | --------------------------------------- |
| `save()`         | Returns a serializable `GridSavedState` |
| `restore(state)` | Applies a previously saved state        |

---

### Cell Editing (`docs/cell-editing.md`)

# Cell Editing

Inline spreadsheet-style editing with full keyboard navigation.

## Enable Editing

Set `enableCellEdit` or `enableCellEditOnFocus` at the grid or column level:

```typescript
const gridOptions: GridOptions = {
  enableCellEditOnFocus: true, // grid-level: edit on cell focus
  columnDefs: [
    { name: 'name', enableCellEdit: true },    // this column is editable
    { name: 'revenue', enableCellEdit: false }, // this column is read-only
  ],
};
```

## Keyboard Navigation

| Key | Action |
|-----|--------|
| `F2` or `Enter` | Begin editing focused cell |
| Any printable key | Begin editing and replace value |
| `Backspace` / `Delete` | Clear value and begin editing |
| `Tab` | Commit and move to next editable cell |
| `Shift + Tab` | Commit and move to previous editable cell |
| `Enter` | Commit and move down |
| `Shift + Enter` | Commit and move up |
| `Escape` | Cancel edit, restore original value |

## Conditional Editing

Use `cellEditableCondition` to allow/deny editing per row:

```typescript
{
  name: 'status',
  enableCellEdit: true,
  cellEditableCondition: (ctx) => ctx.row['status'] !== 'Enterprise',
}
```

## Edit Model Field

When the column name differs from the data property you want to write to, set `editModelField`:

```typescript
{
  name: 'owner',
  field: 'account.owner',
  editModelField: 'account.owner', // writes edited value to this path
  enableCellEdit: true,
}
```

## API

| Method / Event | Description |
|----------------|-------------|
| `gridApi.edit.beginCellEdit(row, colName)` | Start editing programmatically |
| `gridApi.edit.endCellEdit()` | Commit the current edit |
| `gridApi.edit.cancelCellEdit()` | Cancel without committing |
| `gridApi.edit.getEditingCell()` | Returns the current editing position or null |
| `gridApi.edit.on.beginCellEdit` | `(row, col, event) => void` |
| `gridApi.edit.on.afterCellEdit` | `(row, col, newVal, oldVal) => void` |
| `gridApi.edit.on.cancelCellEdit` | `(row, col) => void` |

## Type-Aware Editing

The editor input type adapts to the column's `type`:
- `'number'` → `<input type="number">`
- `'date'` → `<input type="date">`
- default → `<input type="text">`

Value parsing is automatic: numeric strings become `Number`, `'true'`/`'false'` become booleans, and date strings remain as-is.

---

### Custom Builds (`docs/custom-builds.md`)

# Custom Builds

Ship only the features you use. The build script generates compile-time boolean constants that let the bundler tree-shake unused code paths entirely.

## Feature Flags

| CLI Name | TypeScript Constant | What It Guards |
|----------|-------------------|----------------|
| `sorting` | `FEATURE_SORTING` | Column sort UI and pipeline sort step |
| `filtering` | `FEATURE_FILTERING` | Filter row and pipeline filter step |
| `grouping` | `FEATURE_GROUPING` | Group headers and grouping pipeline |
| `pagination` | `FEATURE_PAGINATION` | Pagination footer and page slicing |
| `cell-edit` | `FEATURE_CELL_EDIT` | Inline editing UI and keyboard handlers |
| `expandable` | `FEATURE_EXPANDABLE` | Expandable detail rows |
| `tree-view` | `FEATURE_TREE_VIEW` | Tree row rendering and tree pipeline |
| `infinite-scroll` | `FEATURE_INFINITE_SCROLL` | Infinite scroll state machine |
| `column-moving` | `FEATURE_COLUMN_MOVING` | drag-and-drop on headers |
| `csv-export` | `FEATURE_CSV_EXPORT` | CSV export and toolbar button |
| `save-state` | `FEATURE_SAVE_STATE` | Save/restore state serialization |
| `auto-resize` | `FEATURE_AUTO_RESIZE` | ResizeObserver integration |

## Usage

```bash
# Full build (default — all features enabled)
node scripts/build-grid.mjs

# Minimal build — only sorting and filtering
node scripts/build-grid.mjs --features sorting,filtering

# Custom locale baked in at build time
node scripts/build-grid.mjs --locale projects/ui-grid/src/lib/grid/i18n/fr-FR.json

# Both features and locale
node scripts/build-grid.mjs --features sorting,filtering,pagination --locale i18n/fr-FR.json

# See available flags
node scripts/build-grid.mjs --list

# Dry run — writes generated file but skips ng build
node scripts/build-grid.mjs --features sorting --dry-run
```

## How It Works

1. The script backs up `grid.features.ts`
2. Generates a new version with only selected flags set to `true`
3. Optionally swaps `i18n/en-US.json` with the provided locale file
4. Runs `ng build uiGridPackage --configuration production`
5. Restores the original files (even on build failure)

The bundler sees `if (false) { ... }` and tree-shakes the dead code path entirely. The grid API uses noop fallbacks for all optional feature namespaces, so runtime behavior is always safe.

## Build Presets

Higher-level presets are available via the preset build script:

```bash
# Build the full preset (default)
npm run build:library:preset -- --preset=full

# Build the minimal/headless preset
npm run build:library:preset -- --preset=minimal
```

| Preset | Includes | Status |
|--------|----------|--------|
| `full` | All features + Angular component | Supported |
| `minimal` | Headless core: API, sorting, filtering, pagination | Supported |
| `data-heavy` | Sorting, filtering, viewport, export | Planned |
| `interactive` | Sorting, filtering, grouping, editing, tree, expandable | Planned |

---

### Expandable Rows (`docs/expandable-rows.md`)

# Expandable Rows

Master/detail pattern — expand any row to reveal a custom Angular template below it.

## Setup

```typescript
@Component({
  template: `
    <ng-template #detail let-row>
      <div class="detail-panel">
        <h3>{{ row.name }}</h3>
        <p>Revenue: {{ row.revenue | currency }}</p>
      </div>
    </ng-template>

    <app-ui-grid [options]="gridOptions" />
  `
})
export class MyComponent {
  @ViewChild('detail') detailTemplate!: TemplateRef<any>;

  gridOptions: GridOptions = {
    id: 'master-detail',
    data: this.data,
    enableExpandable: true,
    expandableRowHeight: 120,
    expandableRowTemplate: this.detailTemplate,
    columnDefs: [
      { name: 'name' },
      { name: 'revenue', type: 'number' },
    ],
  };
}
```

## Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enableExpandable` | `boolean` | `false` | Enable expandable rows |
| `expandableRowHeight` | `number` | `150` | Min height of the detail row (px) |
| `expandableRowTemplate` | `TemplateRef` | — | Angular template for the detail content |
| `expandableRowHeaderWidth` | `number` | — | Width of the expand toggle column |
| `expandableRowScope` | `Record<string, unknown>` | — | Extra variables injected into template context |

## Template Context

The template receives a `GridExpandableTemplateContext`:

| Property | Type | Description |
|----------|------|-------------|
| `$implicit` | `GridRecord` | The row data (use with `let-row`) |
| `row` | `GridRecord` | Same as `$implicit` |
| `rowIndex` | `number` | Index of the row in the current view |
| `expanded` | `boolean` | Always `true` when the template renders |

## API

| Method | Description |
|--------|-------------|
| `gridApi.expandable.toggleRowExpansion(row)` | Toggle a single row |
| `gridApi.expandable.expandAllRows()` | Expand all rows |
| `gridApi.expandable.collapseAllRows()` | Collapse all rows |
| `gridApi.expandable.toggleAllRows()` | Toggle all rows |
| `gridApi.expandable.on.rowExpandedStateChanged` | `(row, expanded) => void` |

## Expandable vs Tree View

**Expandable rows** show custom template content below a data row (master/detail). **Tree view** shows child data rows that share the same column structure as parents. You can use both simultaneously.

---

### Features (`docs/features.md`)

# Features

Every feature is included free and open source. No enterprise tier, no license keys. Features can be individually tree-shaken at build time via compile-time feature flags.

## Feature Overview

| Feature            | Option Flag                 | Default | Description                                                         |
| ------------------ | --------------------------- | ------- | ------------------------------------------------------------------- |
| Sorting            | `enableSorting`             | `true`  | Click column headers to cycle asc / desc / none                     |
| Filtering          | `enableFiltering`           | `true`  | Per-column filter inputs with configurable conditions               |
| Row Grouping       | `enableGrouping`            | `false` | Nested multi-column grouping with collapsible headers               |
| Tree View          | `enableTreeView`            | `false` | Hierarchical data with expand/collapse per node                     |
| Expandable Rows    | `enableExpandable`          | `false` | Master/detail pattern with custom Angular templates                 |
| Cell Editing       | `enableCellEdit`            | `false` | Inline spreadsheet-style editing with keyboard nav                  |
| Pagination         | `enablePagination`          | `false` | Client-side or external pagination                                  |
| Infinite Scroll    | `infiniteScrollRowsFromEnd` | —       | Bi-directional infinite scroll with loading state                   |
| Column Moving      | `enableColumnMoving`        | `false` | Drag-and-drop column reordering                                     |
| CSV Export         | `gridApi.core.exportCsv()`  | —       | Export visible rows with formula-injection protection               |
| Virtual Scrolling  | `enableVirtualization`      | auto    | virtual scroll, auto-enabled at 40+ rows                            |
| Save/Restore State | `gridApi.saveState`         | —       | Serialize sort, filter, grouping, pagination, expansion state       |
| Auto Resize        | `enableAutoResize`          | `false` | ResizeObserver-driven viewport height recalculation                 |
| Custom Templates   | `cellTemplate`              | —       | Angular ng-template for fully custom cell rendering                 |
| Shadow DOM         | always                      | —       | Encapsulated styles with CSS custom property and `::part()` hooks   |
| Web Component      | `npm run build:vanilla`     | —       | Ship as `<ui-grid-element>` for package or no-bundler browser usage |
| SSR Support        | automatic                   | —       | Server-side rendering with platform-safe guards                     |
| i18n               | `labels`                    | en-US   | Override any UI string at runtime or bake in a locale at build time |

## Sorting

- Enabled by default (`enableSorting: true`)
- Click a column header to cycle: none → ascending → descending → none
- Per-column disable: `column.sortable = false`
- Custom comparator: `column.sortingAlgorithm = (a, b) => number`
- Programmatic: `gridApi.core.sortColumn('name', 'asc')`

## Filtering

- Enabled by default (`enableFiltering: true`)
- Built-in conditions: `contains`, `startsWith`, `endsWith`, `exact`, `notEqual`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`
- Wildcard support: terms with `*` auto-compile to regex
- Custom predicates: `column.filter.condition = (term, value, row, column) => boolean`
- RegExp conditions: `column.filter.condition = /pattern/i`

## Row Grouping

- Requires `enableGrouping: true`
- Multiple columns: `grouping: { groupBy: ['status', 'company'] }`
- Start collapsed: `grouping: { startCollapsed: true }`
- Per-column disable: `column.enableGrouping = false`
- Mutually exclusive with Tree View

## Compile-Time Feature Flags

Build custom bundles with only the features you need:

```bash
node scripts/build-grid.mjs --features sorting,filtering
```

Disabled features compile to `false` constants — the bundler tree-shakes all guarded code paths. See [Custom Builds](./custom-builds.md) for details.

---

### I18n (`docs/i18n.md`)

# Internationalization

Override any UI string at runtime via the `labels` option, or bake in a complete locale file at build time.

## Default Labels (en-US)

| Key | Default Value |
|-----|---------------|
| `sortDefault` | Sort |
| `sortAsc` | Sort ascending |
| `sortDesc` | Sort descending |
| `groupColumn` | Group by this column |
| `ungroupColumn` | Ungroup this column |
| `groupCollapse` | Collapse group |
| `groupExpand` | Expand group |
| `treeCollapse` | Collapse |
| `treeExpand` | Expand |
| `expandDetail` | Expand row |
| `collapseDetail` | Collapse row |
| `filterPlaceholder` | Filter... |
| `filterDisabled` | Filter disabled |
| `filterColumn` | Filter this column |
| `paginationPrevious` | Previous |
| `paginationNext` | Next |
| `paginationPage` | Page |
| `paginationOf` | of |
| `paginationRows` | rows |
| `emptyHeading` | No data |
| `emptyDescription` | No rows to display |
| `toolbarOf` | of |
| `toolbarRows` | rows |
| `statsVisibleRows` | visible rows |
| `groupRowsSuffix` | rows |

## Runtime Override

Pass partial overrides via `GridOptions.labels`:

```typescript
const gridOptions: GridOptions = {
  id: 'my-grid',
  data: myData,
  columnDefs: myColumns,
  labels: {
    filterPlaceholder: 'Filtrer...',
    emptyHeading: 'Aucune donnée',
    emptyDescription: 'Aucune ligne à afficher',
    paginationPrevious: 'Précédent',
    paginationNext: 'Suivant',
  },
};
```

## Build-Time Locale

Bake in a complete locale file at build time. This replaces `en-US.json` during the build:

```bash
node scripts/build-grid.mjs --locale projects/ui-grid/src/lib/grid/i18n/fr-FR.json
```

## Creating a Locale File

1. Copy `projects/ui-grid/src/lib/grid/i18n/en-US.json`
2. Translate all 26 keys
3. Save as a new JSON file (e.g. `fr-FR.json`)
4. The file must use the same key names — only the values change

## ARIA Labels and Screen Readers

All interactive elements in the grid derive their `aria-label` values from the `GridLabels` object. When you provide translated labels, screen reader announcements localize automatically — no additional configuration is needed.

Labels that flow into ARIA attributes:

| Label Key | ARIA Usage |
|-----------|------------|
| `sortDefault` / `sortAsc` / `sortDesc` | `aria-label` on sort buttons |
| `groupColumn` / `ungroupColumn` | `aria-label` on group toggle buttons |
| `groupCollapse` / `groupExpand` | Screen reader text on group disclosure rows |
| `treeCollapse` / `treeExpand` | `aria-label` on tree toggle buttons |
| `expandDetail` / `collapseDetail` | `aria-label` on expand/collapse detail buttons |
| `filterColumn` | Screen reader text prefix for filter inputs (`"Filter {column}"`) |
| `paginationPrevious` / `paginationNext` | `aria-label` on pagination buttons |
| `paginationPage` | `aria-label` on the pagination `<nav>` element |

See the [Accessibility](./accessibility.md) guide for the full a11y documentation.

---

### Theming (`docs/theming.md`)

# Theming

UI Grid uses Shadow DOM encapsulation. All visual customization flows through CSS custom properties and `::part()` selectors.

The runtime styles now normalize theme resolution around the public `--ui-grid-*` token names again. Legacy `--app-ui-grid-*` aliases still work as a fallback, but consumer themes should target the public `--ui-grid-*` surface.

## How It Works

The grid component renders inside a Shadow DOM boundary. Parent styles cannot leak in. Instead, the grid exposes two theming surfaces:

1. **CSS custom properties** — set `--ui-grid-*` variables on any ancestor element. They pierce the shadow boundary. Legacy `--app-ui-grid-*` aliases remain supported as fallback input for older app themes.
2. **`::part()` selectors** — target specific structural elements by their part name for full CSS access.

All public styling tokens follow the `--ui-grid-*` naming pattern.

## Recommended Override Pattern

```css
.my-app {
  --ui-grid-surface: #1e1b2e;
  --ui-grid-accent: #8b5cf6;
  --ui-grid-header-background: #2d2640;
  --ui-grid-cell-color: #e2e0f0;
  --ui-grid-border-color: rgba(139, 92, 246, 0.2);
  --ui-grid-row-hover: #322e4a;
}
```

If you already ship older `--app-ui-grid-*` theme tokens, they still work as a compatibility fallback. New consumer themes should use only `--ui-grid-*`.

## CSS Custom Properties

### Foundation

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-surface` | `#ffffff` | Base surface for the frame, controls, menus, and inputs |
| `--ui-grid-border-color` | `#d4d4d8` | Shared border color across frame, cells, controls, and menus |
| `--ui-grid-header-background` | `#f3f4f6` | Header and filter row background |
| `--ui-grid-header-weight` | `700` | Header label font weight |
| `--ui-grid-cell-color` | `#111827` | Primary text color for rows and controls |
| `--ui-grid-muted-color` | `#6b7280` | Secondary text color for helper copy and inactive controls |
| `--ui-grid-row-odd` | `#fcfcfd` | Odd striped row background |
| `--ui-grid-row-even` | `#f7f7f8` | Even striped row background |
| `--ui-grid-row-hover` | `#eef4ff` | Row hover background |
| `--ui-grid-accent` | `#2563eb` | Accent color for active states, focus rings, and highlighted controls |
| `--ui-grid-group-background` | `#eceff3` | Group header row background |
| `--ui-grid-radius` | `4px` | Base corner radius reused by frame, cards, and inputs |
| `--ui-grid-shadow` | `0 10px 24px rgba(15, 23, 42, 0.08)` | Frame shadow |

### Status Pills

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-status-active-bg` | `rgba(22, 163, 74, 0.14)` | Active pill background |
| `--ui-grid-status-active-color` | `#166534` | Active pill text |
| `--ui-grid-status-expansion-bg` | `rgba(37, 99, 235, 0.14)` | Expansion pill background |
| `--ui-grid-status-expansion-color` | `#1d4ed8` | Expansion pill text |
| `--ui-grid-status-enterprise-bg` | `rgba(15, 118, 110, 0.14)` | Enterprise pill background |
| `--ui-grid-status-enterprise-color` | `#115e59` | Enterprise pill text |
| `--ui-grid-status-pilot-bg` | `rgba(234, 88, 12, 0.14)` | Pilot pill background |
| `--ui-grid-status-pilot-color` | `#c2410c` | Pilot pill text |
| `--ui-grid-status-pill-padding-block` | `0.2rem` | Vertical pill padding |
| `--ui-grid-status-pill-padding-inline` | `0.55rem` | Horizontal pill padding |
| `--ui-grid-status-pill-radius` | `999px` | Pill border radius |
| `--ui-grid-status-pill-font-size` | `0.85rem` | Pill font size |
| `--ui-grid-status-pill-default-bg` | `rgba(15, 23, 42, 0.07)` | Default pill background when no named state class is applied |

### Shell And Hero

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-shell-gap` | `1.5rem` | Gap between major shell sections |
| `--ui-grid-hero-gap` | `1.5rem` | Gap between hero content and actions |
| `--ui-grid-hero-padding-block` | `1rem` | Hero top and bottom padding |
| `--ui-grid-eyebrow-margin-block-end` | `0.5rem` | Space under the eyebrow label |
| `--ui-grid-eyebrow-letter-spacing` | `0.18em` | Eyebrow tracking |
| `--ui-grid-eyebrow-font-size` | `0.72rem` | Eyebrow size |
| `--ui-grid-hero-title-font-size` | `clamp(1.4rem, 2vw, 2rem)` | Hero title size |
| `--ui-grid-hero-title-line-height` | `1.1` | Hero title line height |
| `--ui-grid-hero-deck-max-width` | `56ch` | Max width for the intro paragraph |
| `--ui-grid-hero-deck-margin-block-start` | `0.75rem` | Space above the deck paragraph |
| `--ui-grid-hero-actions-gap` | `1rem` | Gap between hero actions |
| `--ui-grid-action-radius` | `999px` | CTA button radius |
| `--ui-grid-action-padding-block` | `0.8rem` | CTA button vertical padding |
| `--ui-grid-action-padding-inline` | `1.1rem` | CTA button horizontal padding |
| `--ui-grid-stats-card-min-width` | `8rem` | Minimum width for hero stats cards |
| `--ui-grid-stats-card-padding-block` | `0.85rem` | Stats card vertical padding |
| `--ui-grid-stats-card-padding-inline` | `1rem` | Stats card horizontal padding |
| `--ui-grid-stats-card-value-font-size` | `1.9rem` | Hero stats numeric size |

### Frame, Metrics, Toolbar, And Scrolling

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-metrics-gap` | `1rem` | Gap between metric cards |
| `--ui-grid-metric-card-padding-block` | `1rem` | Metric card vertical padding |
| `--ui-grid-metric-card-padding-inline` | `1.1rem` | Metric card horizontal padding |
| `--ui-grid-metric-value-font-size` | `1.3rem` | Metric value size |
| `--ui-grid-toolbar-gap` | `1rem` | Gap inside the toolbar |
| `--ui-grid-toolbar-padding-block` | `1rem` | Toolbar vertical padding |
| `--ui-grid-toolbar-padding-inline` | `1.25rem` | Toolbar horizontal padding |
| `--ui-grid-toolbar-inline-gap` | `0.4rem` | Inline gap in toolbar text blocks |
| `--ui-grid-overflow-x` | `auto` | Horizontal overflow behavior for the single grid scroll container |
| `--ui-grid-overflow-y` | `auto` | Vertical overflow behavior for the single grid scroll container |

### Header, Pinning, And Controls

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-header-z-index` | `3` | Sticky header and filter row stacking level |
| `--ui-grid-pinned-cell-z-index` | `2` | Sticky pinned cell stacking level |
| `--ui-grid-pin-menu-open-z-index` | `8` | Header cell stacking level when the inline pin chooser is open |
| `--ui-grid-pin-menu-z-index` | `20` | Pin chooser internal stacking level |
| `--ui-grid-header-cell-gap` | `0.75rem` | Gap between header label and action cluster |
| `--ui-grid-header-cell-padding-block` | `0.85rem` | Header cell vertical padding |
| `--ui-grid-header-cell-padding-inline` | `1rem` | Header cell horizontal padding |
| `--ui-grid-header-cell-background-active` | `color-mix(in srgb, var(--ui-grid-accent) 8%, var(--ui-grid-header-background))` | Background for an active sorted header cell |
| `--ui-grid-pinned-divider-shadow-left` | `2px 0 4px rgba(0, 0, 0, 0.06)` | Shadow on the trailing edge of left-pinned columns |
| `--ui-grid-pinned-divider-shadow-right` | `-2px 0 4px rgba(0, 0, 0, 0.06)` | Shadow on the leading edge of right-pinned columns |
| `--ui-grid-pinned-divider-clip-left` | `inset(0 -4px 0 0)` | Clip path for the left pinned divider shadow |
| `--ui-grid-pinned-divider-clip-right` | `inset(0 0 0 -4px)` | Clip path for the right pinned divider shadow |
| `--ui-grid-header-actions-gap` | `0.4rem` | Gap between sort, group, and pin actions |
| `--ui-grid-control-size` | `2rem` | Shared square size for header action chips |
| `--ui-grid-control-radius` | `999px` | Shared control radius |
| `--ui-grid-control-icon-size` | `1rem` | Icon size for action chips and pin arrows |
| `--ui-grid-control-background` | `var(--ui-grid-surface)` | Base control background |
| `--ui-grid-control-border-color` | `var(--ui-grid-border-color)` | Base control border color |
| `--ui-grid-control-color` | `var(--ui-grid-muted-color)` | Base control icon and text color |
| `--ui-grid-control-background-active` | `color-mix(in srgb, var(--ui-grid-accent) 12%, var(--ui-grid-surface))` | Active control background |
| `--ui-grid-control-border-color-active` | `color-mix(in srgb, var(--ui-grid-accent) 35%, var(--ui-grid-border-color))` | Active control border |
| `--ui-grid-control-color-active` | `var(--ui-grid-accent)` | Active control icon and text color |
| `--ui-grid-pin-menu-gap` | `0.25rem` | Gap between inline pin chooser arrows |
| `--ui-grid-pin-menu-padding` | `0.25rem` | Padding inside the inline pin chooser |
| `--ui-grid-pin-menu-radius` | `999px` | Inline pin chooser radius |
| `--ui-grid-pin-menu-shadow` | `0 10px 24px color-mix(in srgb, var(--ui-grid-cell-color) 10%, transparent)` | Inline pin chooser shadow |
| `--ui-grid-pin-menu-action-size` | `1.75rem` | Size of each left or right pin action |
| `--ui-grid-pin-control-collapsed-size` | `1px` | Final collapsed size of the pin button while the chooser opens |
| `--ui-grid-pin-control-transition-duration` | `160ms` | Duration for the inline pin control animation |
| `--ui-grid-pin-control-transition-easing` | `cubic-bezier(0.22, 1, 0.36, 1)` | Easing curve for pin control motion |
| `--ui-grid-pin-menu-scale-closed` | `0.72` | Closed-state scale used before the chooser expands in place |

### Filters

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-filter-cell-padding-block-start` | `0.75rem` | Filter row top padding |
| `--ui-grid-filter-cell-padding-block-end` | `1rem` | Filter row bottom padding |
| `--ui-grid-filter-cell-padding-inline` | `1rem` | Filter row horizontal padding |
| `--ui-grid-filter-input-padding-block` | `0.55rem` | Filter input vertical padding |
| `--ui-grid-filter-input-padding-inline` | `0.7rem` | Filter input horizontal padding |
| `--ui-grid-filter-input-focus-outline` | `2px solid color-mix(in srgb, var(--ui-grid-accent) 18%, transparent)` | Focus outline for filter inputs |

### Rows, Cells, And Editing

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-body-cell-padding-block` | `0.95rem` | Body cell vertical padding |
| `--ui-grid-body-cell-padding-inline` | `1rem` | Body cell horizontal padding |
| `--ui-grid-cell-shell-gap` | `0.55rem` | Gap between toggles and content inside a cell |
| `--ui-grid-row-toggle-size` | `1.55rem` | Tree and expandable toggle size |
| `--ui-grid-row-toggle-radius` | `999px` | Tree and expandable toggle radius |
| `--ui-grid-toggle-icon-size` | `1.1rem` | General toggle icon size |
| `--ui-grid-group-disclosure-icon-size` | `0.9rem` | Group row disclosure icon size |
| `--ui-grid-pagination-icon-size` | `1.2rem` | Pagination icon size |
| `--ui-grid-expandable-row-padding-block` | `1rem` | Expanded row vertical padding |
| `--ui-grid-expandable-row-padding-inline` | `1.25rem` | Expanded row horizontal padding |
| `--ui-grid-expandable-row-background` | `color-mix(in srgb, var(--ui-grid-accent) 4%, var(--ui-grid-surface))` | Expanded row background |
| `--ui-grid-cell-focus-background` | `color-mix(in srgb, var(--ui-grid-accent) 10%, var(--ui-grid-surface))` | Focused cell background |
| `--ui-grid-cell-focus-ring` | `inset 0 0 0 1px color-mix(in srgb, var(--ui-grid-accent) 28%, transparent)` | Focus ring for keyboard-focused cells |
| `--ui-grid-cell-editing-background` | `color-mix(in srgb, var(--ui-grid-accent) 7%, var(--ui-grid-surface))` | Editing cell background |
| `--ui-grid-cell-editor-border-color` | `color-mix(in srgb, var(--ui-grid-accent) 45%, var(--ui-grid-border-color))` | Editor border color |
| `--ui-grid-cell-editor-radius` | `calc(var(--ui-grid-radius) - 1px)` | Inline cell editor radius |
| `--ui-grid-cell-editor-padding-block` | `0.45rem` | Editor vertical padding |
| `--ui-grid-cell-editor-padding-inline` | `0.55rem` | Editor horizontal padding |
| `--ui-grid-cell-editor-focus-outline` | `2px solid color-mix(in srgb, var(--ui-grid-accent) 18%, transparent)` | Editor focus outline |

### Group Rows And Drag States

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-group-row-gap` | `0.9rem` | Gap inside group rows |
| `--ui-grid-group-row-padding-block` | `0.9rem` | Group row vertical padding |
| `--ui-grid-group-row-padding-inline` | `1rem` | Group row horizontal padding |
| `--ui-grid-drag-preview-shadow` | `0 12px 30px rgba(15, 23, 42, 0.16)` | Drag preview shadow for column reordering |
| `--ui-grid-drag-preview-radius` | `0.9rem` | Drag preview radius |
| `--ui-grid-drag-placeholder-opacity` | `0.3` | Placeholder opacity during drag |
| `--ui-grid-drag-placeholder-background` | `color-mix(in srgb, var(--ui-grid-accent) 9%, var(--ui-grid-header-background))` | Placeholder background during drag |
| `--ui-grid-drag-placeholder-border` | `1px dashed color-mix(in srgb, var(--ui-grid-accent) 45%, var(--ui-grid-border-color))` | Placeholder border during drag |
| `--ui-grid-drag-placeholder-shadow` | `inset 0 0 0 1px color-mix(in srgb, var(--ui-grid-accent) 18%, transparent)` | Placeholder shadow during drag |

### Empty State And Pagination

| Variable | Default | Controls |
|----------|---------|----------|
| `--ui-grid-empty-state-gap` | `0.35rem` | Gap between empty-state lines |
| `--ui-grid-empty-state-min-height` | `16rem` | Minimum empty-state height |
| `--ui-grid-empty-state-padding` | `2rem` | Empty-state padding |
| `--ui-grid-pagination-gap` | `1rem` | Gap in the pagination bar |
| `--ui-grid-pagination-padding-block` | `0.85rem` | Pagination bar vertical padding |
| `--ui-grid-pagination-padding-inline` | `1.25rem` | Pagination bar horizontal padding |
| `--ui-grid-pagination-controls-gap` | `0.75rem` | Gap between pagination controls |
| `--ui-grid-pagination-size-gap` | `0.4rem` | Gap between rows-per-page label and select |
| `--ui-grid-pagination-button-padding-block` | `0.45rem` | Pagination button vertical padding |
| `--ui-grid-pagination-button-padding-inline` | `0.8rem` | Pagination button horizontal padding |
| `--ui-grid-pagination-select-padding-block` | `0.35rem` | Pagination select vertical padding |
| `--ui-grid-pagination-select-padding-inline` | `0.55rem` | Pagination select horizontal padding |

## `::part()` Hooks

| Part Name | Target Element |
|-----------|----------------|
| `shell` | Outermost grid wrapper |
| `hero` | Title/toolbar area |
| `grid-frame` | Scrollable grid container |
| `grid-toolbar` | Toolbar with row count |
| `header` | Header row container |
| `header-cell` | Individual header cell |
| `filter-cell` | Filter input cell |
| `body-cell` | Data cell |
| `group-row` | Group header row |
| `expandable-row` | Expanded detail row |
| `pagination` | Pagination footer |
| `empty-state` | Empty state message |

## Example: Custom Brand Theme

```scss
.my-app {
  /* foundation */
  --ui-grid-surface: #1e1b2e;
  --ui-grid-border-color: rgba(139, 92, 246, 0.2);
  --ui-grid-header-background: #2d2640;
  --ui-grid-cell-color: #e2e0f0;
  --ui-grid-muted-color: #8b7fb0;
  --ui-grid-row-odd: #1e1b2e;
  --ui-grid-row-even: #252238;
  --ui-grid-row-hover: #322e4a;
  --ui-grid-accent: #8b5cf6;
  --ui-grid-group-background: #2d2640;

  /* structure */
  --ui-grid-radius: 16px;
  --ui-grid-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
  --ui-grid-toolbar-padding-inline: 1.5rem;
  --ui-grid-body-cell-padding-inline: 1.25rem;

  /* controls */
  --ui-grid-control-size: 2.15rem;
  --ui-grid-pin-control-transition-duration: 200ms;
  --ui-grid-pin-menu-action-size: 1.9rem;
}

/* Target specific parts for full CSS access */
.my-app app-ui-grid::part(header) {
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
```

## Host Attribute Pattern

The demo app uses `data-color-mode` and `data-visual-mode` attributes on the root component to drive a 2×2 theme matrix (dark/light × studio/wireframe). Each combination defines a full set of semantic CSS tokens that cascade into the grid:

```scss
:host([data-color-mode='dark'][data-visual-mode='default']) {
  --grid-surface: #0b1824;
  --grid-header: #112434;
  --grid-text: #ebf5f9;
  --grid-accent: #67e8f9;
  /* ...then map to --ui-grid-* on the grid container */
}
```

You can use this same pattern in your own app to support multiple themes without JavaScript logic. If you already have legacy `--app-ui-grid-*` aliases in your codebase, you can leave them in place while standardizing new theme work on the public `--ui-grid-*` names.

---

### Tree View (`docs/tree-view.md`)

# Tree View

Display hierarchical data with collapsible parent/child rows.

## Data Shape

Nest child rows under a configurable property (default: `children`):

```typescript
const treeData = [
  {
    name: 'Engineering',
    headcount: 42,
    children: [
      { name: 'Frontend', headcount: 18 },
      { name: 'Backend', headcount: 24 },
    ],
  },
];

const gridOptions: GridOptions = {
  id: 'org-tree',
  data: treeData,
  enableTreeView: true,
  treeChildrenField: 'children',
  treeIndent: 20,
  columnDefs: [
    { name: 'name' },
    { name: 'headcount', type: 'number' },
  ],
};
```

## Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enableTreeView` | `boolean` | `false` | Enable tree mode |
| `treeChildrenField` | `string` | `'children'` | Property name for child array |
| `treeIndent` | `number` | `10` | Pixels per indent level |
| `showTreeExpandNoChildren` | `boolean` | `true` | Show toggle for childless rows |
| `treeRowHeaderAlwaysVisible` | `boolean` | `false` | Always show expand column |

## Filtering

When a filter is active, child matches bubble up — parent rows are shown even when they don't match the filter, as long as at least one child does.

## API

| Method | Description |
|--------|-------------|
| `gridApi.treeBase.toggleRowTreeState(row)` | Toggle node expansion |
| `gridApi.treeBase.expandRow(row)` | Expand a node |
| `gridApi.treeBase.collapseRow(row)` | Collapse a node |
| `gridApi.treeBase.expandAllRows()` | Expand all nodes |
| `gridApi.treeBase.collapseAllRows()` | Collapse all nodes |
| `gridApi.treeBase.getRowChildren(row)` | Get child GridRow objects |
| `gridApi.treeView.getTreeView()` | Get current expansion state map |
| `gridApi.treeView.setTreeView(state)` | Bulk set expansion state |

## Tree View vs Grouping

| | Tree View | Row Grouping |
|-|-----------|-------------|
| Data source | Pre-structured with `children` array | Flat data — groups created dynamically |
| Nesting | Your data defines the hierarchy | Grid creates groups from column values |
| Mutual exclusivity | Disables grouping when active | Disables tree view when active |

---

### Web Component (`docs/web-component.md`)

## Web Component Output (`@ornery/ui-grid-vanilla`)

A framework-free custom element built on `@ornery/ui-grid-core` with pure DOM rendering and Shadow DOM encapsulation. The package entry points are designed for package-aware tools, and the browser bundle is designed for direct static HTML usage.

### Install

```bash
npm install @ornery/ui-grid-vanilla @ornery/ui-grid-core
```

### Declarative HTML Usage

This is the non-Angular custom element build. Use it in static sites, plain JavaScript apps, or any host that should not carry Angular runtime code.

For a no-bundler static host, use the browser bundle. It includes the core runtime and auto-registers `<ui-grid-element>` when loaded:

```html
<script type="module" src="./ui-grid-element.js"></script>

<ui-grid-element
  grid-id="vanilla-demo"
  title="Team Roster"
  enable-sorting
  enable-filtering
  column-defs='[
    { "name": "name" },
    { "name": "role" },
    { "name": "salary", "type": "number", "align": "end" }
  ]'
  data='[
    { "name": "Alice", "role": "Engineer", "salary": 120000 },
    { "name": "Bob", "role": "Designer", "salary": 95000 }
  ]'
>
</ui-grid-element>
```

After `npm run build:vanilla`, the file is available at `projects/ui-grid-vanilla/dist/browser/ui-grid-element.js`. Copy or serve that file with your static app. The package `dist/index.js` file is CommonJS, and `dist/index.mjs` is package ESM that still expects package resolution; neither is the direct browser drop-in file.

The vanilla element supports the same declarative attribute surface. Small and medium datasets can be configured directly in markup with zero grid setup JavaScript beyond registration:

```html
<ui-grid-element
  grid-id="vanilla-demo"
  title="Team Roster"
  enable-sorting
  enable-filtering
  column-defs='[
    { "name": "name" },
    { "name": "role" },
    { "name": "salary", "type": "number", "align": "end" }
  ]'
  data='[
    { "name": "Alice", "role": "Engineer", "salary": 120000 },
    { "name": "Bob", "role": "Designer", "salary": 95000 }
  ]'
>
</ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement(); // registers <ui-grid-element>
</script>
```

Supported declarative inputs include:

- Boolean attributes such as `enable-sorting`, `enable-filtering`, `enable-grouping`, `enable-pinning`, `enable-pagination`, and `enable-virtualization`
- JSON attributes such as `column-defs`, `data`, `grouping`, and `pagination-page-sizes`

Use the declarative surface for static pages, docs, CMS-rendered content, and simple embeds. For callbacks like `onRegisterApi` or function-based column logic, use the `options` property.

### Performance Guidance

- **Best use for declarative attributes**: initial mount, static markup-first pages, and infrequent option changes.
- **Cost of declarative updates**: changing JSON attributes such as `data` or `column-defs` forces the element to parse the new payload, merge options again, and may trigger more DOM work than necessary for live feeds.
- **Best use for imperative updates**: callbacks, one-shot bulk configuration, host-framework integration, and any case where application code already owns the current row set.
- **High-frequency updates**: for streaming or trading-style feeds, mount declaratively if you want, but switch live row updates to `grid.setData(rows)` instead of replacing the `data` attribute. `setData(rows)` is the vanilla element's cheapest update path because it patches the existing grid surface instead of re-running the full declarative sync flow.

### JavaScript Property Usage

The vanilla element also exposes individual JS properties that mirror the declarative attributes. This is useful when a framework or host app wants property binding without constructing a full `options` object:

```html
<ui-grid-element id="my-grid"></ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement();

  const grid = document.querySelector('#my-grid');
  grid.gridId = 'vanilla-props';
  grid.title = 'Team Roster';
  grid.enableSorting = true;
  grid.enableFiltering = true;
  grid.columnDefs = [
    { name: 'name' },
    { name: 'role' },
    { name: 'salary', type: 'number', align: 'end' },
  ];
  grid.data = [
    { name: 'Alice', role: 'Engineer', salary: 120000 },
    { name: 'Bob', role: 'Designer', salary: 95000 },
  ];
  grid.paginationPageSizes = [10, 25, 50];
</script>
```

### Full `options` Usage

```html
<ui-grid-element id="my-grid"></ui-grid-element>

<script type="module">
  import { defineStandaloneUiGridElement } from '@ornery/ui-grid-vanilla';

  await defineStandaloneUiGridElement(); // registers <ui-grid-element>

  document.querySelector('#my-grid').options = {
    id: 'vanilla-demo',
    data: [
      { name: 'Alice', role: 'Engineer', salary: 120000 },
      { name: 'Bob', role: 'Designer', salary: 95000 },
    ],
    columnDefs: [
      { name: 'name' },
      { name: 'role' },
      { name: 'salary', type: 'number', align: 'end' },
    ],
  };
</script>
```

### Mount Helper

The `mountVanillaUiGrid` helper registers the element, creates it, sets options, and appends it to a target element in one call:

```typescript
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';

const grid = await mountVanillaUiGrid(document.getElementById('app'), {
  id: 'mounted-grid',
  data: myData,
  columnDefs: myColumns,
});
```

To use the optional WASM engine with the vanilla element, install and pass the compiled module published by the [Rust project](https://github.com/orneryd/ui-grid):

```bash
npm install @ornery/ui-grid-wasm
```

```typescript
import { mountVanillaUiGrid } from '@ornery/ui-grid-vanilla';
import * as wasmModule from '@ornery/ui-grid-wasm';

const grid = await mountVanillaUiGrid(
  document.getElementById('app'),
  { id: 'wasm-grid', data: myData, columnDefs: myColumns },
  wasmModule,
);
```

If no WASM module is provided, or if WASM initialization fails, the grid automatically falls back to the JavaScript engine.

### Slot Templates

The vanilla element supports `<template>` elements with `slot` attributes for custom cell and expandable row content. Templates use `{{ expression }}` interpolation:

```html
<ui-grid-element id="my-grid">
  <template slot="cell-name">
    <strong>{{ value }}</strong>
  </template>
  <template slot="expandable-row">
    <p>Details for {{ row.name }}</p>
  </template>
</ui-grid-element>
```

### State API

The vanilla element exposes `getState()` and `setState()` methods for save/restore:

```javascript
const state = grid.getState(); // returns GridSaveState | null
grid.setState(state); // applies a previously saved state
```

---

## Shared: Setting Options

## Shared Grid Configuration Surface

Both outputs accept the same `GridOptions` object as the Angular component, plus the same declarative attributes for common markup-first setup:

For both elements, the `options` property is the advanced escape hatch rather than the only configuration path. Prefer attributes when they are sufficient; use `options` when you need callbacks, function-valued column definitions, or one-shot bulk assignment.

In performance terms, think of the surfaces like this:

- **Declarative attributes** optimize for authoring convenience and HTML-first setup.
- **`options` assignment** optimizes for programmatic control and coarse reconfiguration.
- **Vanilla `setData(rows)`** optimizes for repeated row-data refreshes and is the preferred path for high-frequency updates.

```javascript
const grid = document.querySelector('ui-grid-element');
grid.options = {
  id: 'my-grid',
  data: myData,
  columnDefs: myColumns,
  enableSorting: true,
  enableFiltering: true,
  onRegisterApi: (api) => {
    window.gridApi = api;
  },
};
```

## Shared: Styling

Both variants use Shadow DOM. Style them via CSS custom properties on an ancestor:

```css
.my-container {
  --ui-grid-surface: #1a1a2e;
  --ui-grid-cell-color: #e0e0e0;
  --ui-grid-accent: #00d4aa;
  --ui-grid-border-color: rgba(0, 212, 170, 0.2);
  --ui-grid-header-background: #242440;
}
```

## Shared: Events

Use `onRegisterApi` in the options to receive the grid API, then subscribe to events:

```javascript
grid.options = {
  // ...
  onRegisterApi: (api) => {
    api.core.on.sortChanged((column, direction) => {
      console.log('Sort:', column, direction);
    });

    api.edit.on.afterCellEdit((row, col, newVal, oldVal) => {
      console.log('Edited:', col, oldVal, '->', newVal);
    });
  },
};
```

See [orneryd/ui-grid](https://github.com/orneryd/ui-grid) for Rust implementation and native adapter documentation.

---

--- METRICS ---
- Files Extracted: 16
- Estimated Token Budget: ~27728 tokens
- Canonical Reference: https://codewiki.google/github.com/angular-ui/ui-grid
