---
id: "packages/active-grid/cells-and-utilities"
title: "Cells and utilities"
description: "Cell components, column helpers and the utility functions."
url: "/packages/active-grid/cells-and-utilities"
locale: "en"
revision: "b3b9d7286c6b7320"
---

# Cells and utilities

> [!NOTE]
> **The package's own reference**
>
> The text below is the package's own API reference, included unchanged. The
> complete list of exports, generated from the source, is on [All
> exports](/packages/active-grid/api).

Cell renderers, system-column factories, export functions, auto-size
utilities, hooks, `ContextMenuBuilder`, `EditingCommitPayload`, and the
translation/locale typing.

## LookupGrid

`LookupGrid` presents a single-record picker in a controlled dialog.
Supply `open`, `onOpenChange`, `title`, `columns`, `onSelect`, `t` and `locale`.
Server mode is the default and requires `fetchFn`; `mode="client"` requires
`data` instead. Confirmation or a selectable row's double-click calls
`onSelect(record)`.

Optional inputs include `gridId`, `getRowId`, `isRowSelectable(record)`,
`columnTypes`, `serverCapabilities`, `description`, `searchPlaceholder`,
`maxWidth`, `gridHeight`, `initialPageSize` and `pageSizeOptions`. The capability
map is forwarded to the inner grid. The picker does not define a server query
or authorize the selected record.

## Cell Components

```typescript
import {
  TextCell,
  BadgeCell,
  StatusCell,
  DateCell,
  LinkCell,
  SelectionCell,
  ExpanderCell,
} from '@workspace/active-grid';
```

Pre-built cell renderers for use in custom column `cell` render functions.

| Component | Description |
| --- | --- |
| `TextCell` | Default text renderer with truncation and title tooltip. |
| `BadgeCell` | Categorical badge: type, category, department, source. One fixed presentation. |
| `StatusCell` | Status badge on a `GridStatusTone`, drawn with the `--grid-status-*` tokens. |
| `DateCell` | Locale-aware date display. Date-only by default; see below. |
| `LinkCell` | Link renderer: a native anchor, or the host's `ui.linkComponent`; see below. |
| `SelectionCell` | Checkbox for row selection (used by `getSelectionColumn`). |
| `ExpanderCell` | Chevron toggle for row expanding (used by `getExpanderColumn`). |

### Calendar day versus instant

`DateCell` carries a `semantic` prop with two values, and the choice is not
cosmetic — it decides whether the shown day may move with the reader's time
zone.

| `semantic` | Meaning | Behaviour |
| --- | --- | --- |
| `'date-only'` (default) | A calendar fact: birth, order, measurement or effective date | The same day in every time zone |
| `'datetime'` | A real moment: `createdAt`, an audit or execution timestamp | Direct DateCell uses runtime-local date-fns formatting; use the dateTime semantic column for presentation-zone formatting |

Date-only is the default because that is what this cell actually receives:
`body-cell.tsx` renders it for columns with `meta.filterType: 'date'`, while a
column that genuinely shows a timestamp goes through the `dateTime` column-type
preset, whose semantic declaration is formatted by the cell orchestration.

```tsx
<DateCell value={contract.startDate} />                      // calendar day
<DateCell value={row.createdAt} semantic="datetime" />       // moment in time
```

The same distinction lives in the column-type presets: `type: 'date'` is a
calendar day, `type: 'dateTime'` is an instant. Prefer those over a hand-written
`meta.filterType: 'date'`.

*Added 2026-08-12 (AUI-01). Until then the cell did `new Date(value)` for its
whole `Date | string | number` contract. For an ISO date-only string that is UTC
midnight, after which `format()` rendered the local calendar day: under
`TZ=America/New_York` `2026-03-09` showed as `08-03-2026`. Measured, not
reasoned — eight of the nine tests in `date-cell.test.tsx` went red as soon as
the zone shifted. The parsing now runs through `dateOnlyToLocalDate` from
`@workspace/active-format`, the one boundary the whole repo shares.*

### Status versus categorical

The two badge cells are separate on purpose. `BadgeCell` presents a value that
belongs to a set (a category, a type, a department); `StatusCell` presents a
value that says how something *is doing*, on the five-tone axis
`GridStatusTone` (`neutral`, `info`, `success`, `warning`, `danger`). A host
maps each tone onto its own status colours through the
`--grid-status-<tone>` and `--grid-status-<tone>-text` tokens.

`BadgeCell` has no variant or colour prop — it had one until 9.0.0 and it was
status semantics expressed in an axis that carries none (`default` is the
theme's accent colour, not "good"). A custom `cell` that genuinely needs a
coloured categorical badge renders the host's own badge component directly,
so that choice is visibly outside the grid's badge contract.

```tsx
// Declarative — preferred. The column says what each value means; the grid
// renders it. See api/columns.md § Status columns.
meta: {
  filterOptions: [{label: t('status.active'), value: 'ACTIVE'}],
  statusTones: {ACTIVE: 'success', LAPSED: 'danger'},
}

// Imperative — for values that are not a stable key (derived or already
// translated), where a value→tone map would break per locale.
cell: (info) => (
  <StatusCell
    value={info.getValue() ? t('session.active') : t('session.expired')}
    tone={info.getValue() ? 'success' : 'neutral'}
  />
),
```

| Prop | Type | Description |
| --- | --- | --- |
| `value` | `string \| null` | The text to display. Renders nothing when empty. |
| `tone` | `GridStatusTone` | Required. The generic UI-tone; the caller maps to it. |
| `labelMap` | `Record<string, string>` | Optional value→label resolution, as on `BadgeCell`. |
| `className` | `string` | Extra classes on the badge. |

### Links: native anchor or the host's link

`LinkCell` renders an internal link as a plain `<a href>`, so the browser
navigates. A host with a client-side router passes its link component once,
through the nearest `GridDefaults`:

```tsx
// `hostLink`: your router's link component, e.g. the default export of next/link

<GridDefaults ui={{linkComponent: hostLink}}>{children}</GridDefaults>
```

The component receives `{href, className, children}` and nothing else
(`GridLinkProps`; the type of the component is `GridLinkComponent`). A nearer
`GridDefaults` replaces it for the grids below; a nearer one that sets other
`ui` fields keeps it. An external link (`isExternal`) is always a native
anchor that opens in a new tab.

```tsx
cell: (info) => (
  <LinkCell value={info.getValue()} href={(id) => `/policies/${id}`} />
),
```

| Prop | Type | Description |
| --- | --- | --- |
| `value` | `string \| null \| undefined` | The link text, and the argument of an `href` function. |
| `href` | `string \| ((value: string) => string)` | The destination. |
| `isExternal` | `boolean` | New tab, `rel="noopener noreferrer"`, never the host link. |
| `className` | `string` | Extra classes on the anchor. |

## System Column Factories

```typescript
import {getSelectionColumn, getExpanderColumn} from '@workspace/active-grid';
```

| Function | Description |
| --- | --- |
| `getSelectionColumn()` | Returns a column definition for row-selection checkboxes. |
| `getExpanderColumn()` | Returns a column definition for row expand/collapse toggles. |

System columns have `enableSorting: false`, `enableResizing: false`, and no
header menu by default. They are excluded from export. Row-numbering is
available via the `showRowNumbers` prop on `<ActiveGrid>` rather than a
factory function.

## ContextMenuBuilder

```typescript
import {
  type ContextMenuBuilder,
  type ContextMenuItem,
  createContextMenu,
} from '@workspace/active-grid';
```

```typescript
type ContextMenuBuilder<TData> = (
  row: TData | null,
  context?: ContextMenuContext,
) => ContextMenuItem<TData>[];

interface ContextMenuContext {
  kind: 'range' | 'cell' | 'row'; // what the opened menu is about
  columnId?: string; // the column of the cell it opened on
  selection?: {rangeCount: number; cellCount: number}; // for a 'range' menu
  selectedRowCount: number; // rows selected in the grid
}

type ContextMenuItem<TData> =
  | 'separator'
  | {
      label: string;
      action?: (row: TData) => void; // optional for submenu triggers
      icon?: React.ElementType;
      variant?: 'default' | 'destructive';
      disabled?: boolean;
      disabledReason?: string;
      shortcut?: string;
      isLoading?: boolean;
      keepOpen?: boolean;
      permission?: string; // the RIGHT the viewer needs
      visible?: (row: TData) => boolean; // relevance for THIS row
      separatorBefore?: boolean; // explicit group boundary
      children?: ContextMenuItem<TData>[]; // nested submenu items
    };
```

### When the builder runs, and what it is told

The grid calls the builder for a row that stands for a record, when that
row's menu opens, and passes the `context` as the second argument. A builder
that declares only `row` keeps working unchanged; the argument is optional so
a builder can still be called with a row alone. A row the grid generated (a
client group row) never reaches the builder: its menu is the grid's copy of
what its cells show.

`kind` is `range` when the menu opened inside a cell selection of more than
one cell (`selection` then counts every range together), `cell` when it opened
on a single cell, and `row` when there is no cell target (cell selection is
off, or the menu opened outside the row's cells). The grid's own copy items
follow the same kinds; the builder's items are the row's actions and stay in
every kind.

```typescript
const contextMenu: ContextMenuBuilder<Person> = (row, context) => {
  if (!row) return [];
  return [
    {label: 'View details', action: (r) => open(r.id)},
    ...(context?.kind === 'range' ? [] : [{label: 'Edit', action: edit}]),
  ];
};
```

### Permission, visibility and disabled are three questions

They are answered separately and must not be substituted for one another:

| Field | Question | Result when it fails |
| --- | --- | --- |
| `permission` | May this VIEWER perform the action at all? | the item is absent |
| `visible` | Is the action relevant for THIS row/state? | the item is absent |
| `disabled` | Can it run right now, given the current state? | shown, with a reason |

An item carrying both `permission` and `visible` must pass both. Filtering
happens **before** separator normalization, so a group that filters away
completely leaves no orphaned rule behind.

Put a right in `permission`, never in `visible`: that is where the factory, the
docs and the next reader look for it. `visible` is for row and state predicates
(`(row) => row.status === 'FAILED'`). Both are client-side affordances -- the
server action behind the item authorizes independently.

**A declared `permission` is never ignored.** It is answered by the
`hasPermission` checker passed to `createContextMenu`, and nothing else:

| Item | Checker answers yes | Checker answers no | No checker |
| --- | --- | --- | --- |
| without `permission` | shown | shown | shown |
| with `permission`, via the factory | shown | absent | absent |
| with `permission`, direct builder | -- | -- | absent |

So a menu that declares rights passes the viewer's checker to the factory.
An item no checker answered is left out at the render boundary as well, and
outside production the grid reports it once per opening through the `<GridDefaults diagnostics>`
sink (`warn`, source `ActiveGrid.ContextMenu`, `data: {label, permission}`):
a declared right without its check is a configuration error, and failing
closed keeps it from offering an action the viewer may not take.

### Submenu support

Items with a `children` array render as a submenu trigger with a chevron
(`>`). `action` is optional for submenu parent items — the trigger itself
does not perform an action.

Permission filtering is recursive: `createContextMenu` filters both parent
items and their children by `hasPermission`. If the parent is filtered out,
its children are not rendered.

```typescript
const contextMenu = createContextMenu<Person>({
  hasPermission,
  customItems: (row) => {
    if (!row) return [];
    return [
      {
        label: 'View details',
        icon: Eye,
        action: (r) => router.push(`/records/${r.id}`),
      },
      'separator' as const,
      {
        label: 'Change status',
        icon: ArrowRightLeft,
        permission: 'record.update',
        children: transitions.map((status) => ({
          label: `Status: ${status}`,
          action: (r: Person) => changeStatus(r, status),
        })),
      },
    ];
  },
});
```

### The resolve pipeline

Every row menu passes through one pipeline, in this order:

1. **filter** on `permission` and `visible`, recursively into submenus;
2. **expand** `separatorBefore` into a rule in the sequence;
3. **give the destructive tail its rule**, when the menu itself makes that
   unambiguous;
4. **clean up**: no leading rule, no trailing rule, never two in a row.

Filtering comes first because filtering is what strands rules. The pipeline is
**idempotent** and never reorders, merges or drops an action. It is the one
pipeline of the Active product family (ADR-058): it lives in
`@workspace/active-utils/menu-items`, and Active Charts runs it over its
menus too; the grid supplies the permission check as its `keep` step.

**Submenus and reasons.** `children` nests to any depth, each level its own
submenu. A disabled item keeps its `disabledReason` as the pointer's `title`
and as `aria-description` for assistive technology, on a submenu trigger as
well; it stays focusable, so a keyboard reader hears why it cannot run.

`createContextMenu` runs it with the permission check, and the grid runs it
again at the render boundary on whatever a builder hands it. That second place
is what makes the contract hold product-wide: a menu passed directly as a
`ContextMenuBuilder` never meets the factory, but it does meet the renderer.
The renderer applies `visible` and the separator rules. It has no
`hasPermission`, so it keeps an item that declares `permission` only when the
factory approved it; the factory returns approved items as fresh copies and
never mutates or marks the consumer's own objects.

**Consumers declare plain items and write no clean-up of their own.** There is
nothing to import.

### Destructive actions sit in their own block

A destructive action, or a group of related lifecycle actions ending in one,
belongs at the bottom of the consumer block, separated from the ordinary
object actions by a single rule. The rule underneath it is drawn by the grid,
which always closes the consumer block before its own Copy / Export / Reset
items:

```text
View
Edit
─────────
Delete            <- variant: 'destructive'
─────────         <- drawn by the grid
Copy
Export view (10)
Reset columns
```

**You usually declare nothing.** The pipeline adds the rule when the menu ends
in a destructive action and something precedes it. It deliberately does nothing
when:

- the menu does not end in a destructive action (there is no tail, and only the
  consumer knows the intended boundary -- use `separatorBefore`);
- the destructive action is the first or only visible item (a rule above it
  would separate nothing);
- a rule already opens that group.

**A mixed lifecycle group stays whole.** The tail starts at the FIRST
destructive item of the last segment, not the last one, so a media block of
delete, restore and delete-permanently is one group with one rule above it --
no rule falls between the restore item and the permanent delete, even though
the restore item is not itself destructive.

Use `separatorBefore` for a group the pipeline cannot infer, for example one
that does not end the menu. It carries the same name and meaning as
`GridSelectionAction.separatorBefore` on the bulk bar: one vocabulary per
package. A doubled or leading rule is removed afterwards, so declaring it
defensively is harmless.

## GridContextMenu

```typescript
import {GridContextMenu} from '@workspace/active-grid';
```

Wrapper component for the Base UI right-click menu. Most consumers do
not import this directly — the grid uses it internally when a `contextMenu`
prop is supplied.

## EditingCommitPayload

```typescript
import {type EditingCommitPayload} from '@workspace/active-grid';
```

Payload received by `onDataCommit`:

| Property | Type | Description |
| --- | --- | --- |
| `rowId` | `string` | ID of the row being edited. |
| `originalRow` | `TData` | Row data before editing. |
| `changes` | `Record<string, unknown>` | Only the changed fields. |
| `newData` | `TData` | Complete row data after edits. |

Related types: `BatchCommitPayload`, `DirtyCellEntry`, `CellRange`,
`CellSelectionState`, `EditMode` — for the batch/range-editing surface
(advanced).

### Spreadsheet-style editing

Combine `editTrigger="click"` with `commitOnBlur` for spreadsheet-like
behavior:

```tsx
<ActiveGrid<Row>
  gridId="editor"
  mode="client"
  data={rows}
  columns={columns}
  editMode="cell"
  editTrigger="click"
  commitOnBlur
  onDataCommit={async (payload) => {
    setRows((prev) =>
      prev.map((row) =>
        getRowId(row) === payload.rowId ? payload.newData : row,
      ),
    );
  }}
/>
```

Behavior: single click starts editing; clicking another cell commits + moves;
clicking outside the grid commits; Enter commits and moves down; Tab commits
and moves right; Escape cancels.

## Export Functions

```typescript
import {
  exportToCSV,
  exportToCSVGrouped,
  exportToCSVServerGrouped,
  exportToJSON,
  exportToJSONGrouped,
  exportToJSONServerGrouped,
  exportToExcel,
  exportToExcelGrouped,
  exportToExcelServerGrouped,
  exportToPDF,
  exportToPDFGrouped,
  exportToPDFServerGrouped,
  copyToClipboard,
  GridExportError,
  type GridExportErrorCode,
  type GridExportErrorOptions,
  type GridExportMode,
  type GridExportRunOptions,
  type GridExcelExportOptions,
  type GridPdfExportOptions,
  type GridExportFonts,
  type GridExportFontFamily,
  type GridExportFontFace,
  type GridExportFontSource,
  type GridExportDirection,
  type GridClipboardOptions,
  type ServerGroupedExportConfig,
} from '@workspace/active-grid';
```

| Function | Description |
| --- | --- |
| `exportToCSV(rows, grid, filename, options?)` | Download CSV. Every field quoted, formulas neutralized. |
| `exportToJSON(rows, grid, filename, options?)` | Download JSON file. |
| `exportToExcel(rows, grid, filename, options?)` | Download Excel `.xlsx` with typed cells (optional `xlsx`, loaded on use). `options.direction: 'rtl'` opens the sheet right to left. |
| `exportToPDF(rows, grid, filename, options)` | Download PDF (optional `jspdf` + `jspdf-autotable`). Presentation-only; `options.fonts` and `options.direction` (ADR-054). |
| `copyToClipboard(rows, grid, options)` | Copy rows as TSV presentation copy. Formulas neutralized. A refused write resolves `false` and is reported to `options.diagnostics` (default: the console). |

`options` on the CSV/JSON/XLSX functions is a `GridExportRunOptions`
(`GridExportMode` union): omit it (or pass `{mode: 'data'}`) for the default
DATA export — machine/raw semantics, byte-stable under every viewer
language — or pass `{mode: 'presentation', locale}` for the explicit
human-readable form. `exportToExcel*` takes a `GridExcelExportOptions`: the
same options plus `direction` (`GridExportDirection`, `'ltr' | 'rtl'`). PDF
is presentation output by definition and takes a required
`GridPdfExportOptions` (`{locale, fonts?, direction?}`); the clipboard is a
presentation copy and takes a required `GridClipboardOptions`
(`{locale, includeHeaders?}`). See
`guides/export.md` for the full mode contract.

**Fonts are PDF-only, by type.** `GridExportFonts` (families in fallback
order; each face `source` is bytes or a lazy host loader, with `weight` and
`style`) is accepted by `GridPdfExportOptions.fonts`, the instance's
`export.fonts` and `GridDefaults`' `export.fonts`, and by nothing else: the
CSV, JSON and XLSX options have no font field. The shape is structurally the
one of active-charts' ChartExportFonts, so one host object serves both. See
`guides/export.md`.

**Errors.** An export the grid refuses, or whose optional backend is missing,
fails with a `GridExportError` whose `code` is stable
(`GridExportErrorCode`: `'EXPORT_DEPENDENCY_MISSING'`, `'EXPORT_FONT_REQUIRED'`,
`'EXPORT_FONT_FORMAT_UNSUPPORTED'`, `'EXPORT_FONT_FACE_MISSING'`,
`'EXPORT_FONT_SOURCE_INVALID'`, `'EXPORT_FONT_LOAD_FAILED'`,
`'EXPORT_TEXT_UNSUPPORTED'`, `'EXPORT_GLYPH_COVERAGE'`) and whose metadata never
holds exported text. See
`guides/export.md`.

**Outcome.** Every function reports by itself: a synchronous one throws, an
asynchronous one rejects, and a return means the file was handed to the
browser. An export from the row menu reports through
`GridExportOptions.onExportComplete(format, scope)` and
`onExportError(format, error, scope)` (`GridExportScope`: `'page'`,
`'loaded'`, `'selection'`); see
`guides/export.md`. No
export is cancellable.

Each export function has `-Grouped` and `-ServerGrouped` variants for
client-side and server-side grouped data respectively. The
`-ServerGrouped` variants accept a `ServerGroupedExportConfig` parameter.

### Delimited-output rules (CSV + clipboard)

All CSV and TSV rendering goes through one internal boundary
(`src/utils/export-delimited.ts` — not part of the public API), so the rules
below hold for data cells, header rows, group labels and group footers alike:

- **CSV**: every field is quoted, including headers; internal quotes doubled
  per RFC 4180; embedded newlines kept inside the quoted field.
- **TSV**: minimal quoting — only values containing a tab, newline or `"` are
  quoted, so plain values paste cleanly into text targets.
- **Formula neutralization**: a value a spreadsheet would execute gets a
  leading apostrophe. Always on, no opt-out. The output of `formatNumber` and
  the percent formatters is deliberately untouched in every supported locale, so
  negative amounts export unchanged.

XLSX, JSON and PDF do **not** use this boundary. XLSX writes typed cells;
a string cell is stored as text and is never evaluated as a formula.

Full behaviour, including the measured limits, is in
`guides/export.md`.

## Auto-size Utilities

```typescript
import {useAutoSize} from '@workspace/active-grid';
```

| Hook | Description |
| --- | --- |
| `useAutoSize(grid, options)` | React hook that triggers auto-size on data-changes. Use with `autoSizeOnMount` for first-render fit. |

The two standalone auto-size functions exported in v3 were removed in
v4.0.1 — the `useAutoSize` hook is the canonical entry point. See
`CHANGELOG.md` v4.0.1.

```typescript
import {
  type AutoSizeMode,
  type AutoSizeOnMountOptions,
  type AutoSizeOptions,
  type AutoSizeResult,
  type HeaderMeasureResult,
  type MeasureContext,
  DEFAULT_AUTO_SIZE_OPTIONS,
  DEFAULT_AUTO_SIZE_ON_MOUNT,
  getAutoSizeConfig,
  normalizeAutoSizeOnMount,
} from '@workspace/active-grid';
```

Three auto-size modes: `'fit-content'`, `'fit-viewport'`, `'balanced'`.

## Hooks

```typescript
import {
  useColumnSizingVars,
  useKeyboardNavigation,
  useAutoSize,
  useResponsiveColumns,
  type UseResponsiveColumnsOptions,
} from '@workspace/active-grid';
```

| Hook | Description |
| --- | --- |
| `useColumnSizingVars` | Computes CSS-variable map for column widths from engine state. |
| `useKeyboardNavigation` | Arrow-key navigation + scroll-to-focus. `getCellProps` returns `onMouseDown` (a primary press makes the cell active on the press) and `onClick`; spread both. |
| `useAutoSize` | Trigger auto-size on data-changes. |
| `useResponsiveColumns` | Show/hide columns based on container width breakpoints. |

In most cases `<ActiveGrid>` handles these internally. They are exported
for advanced consumers composing grid behavior outside of `<ActiveGrid>`.

## i18n Typing

```typescript
import {
  englishGridTranslator,
  useGridTranslation,
  useGridLocale,
  type GridTranslateFunction,
} from '@workspace/active-grid';
```

```typescript
type GridTranslateFunction = (
  key: string,
  variables?: Record<string, string | number | undefined>,
) => string;
```

Compatible with any i18n library. The translator comes from the grid's `t`
prop, else the nearest `<GridDefaults t>` (see
`columns.md` § GridDefaults); with neither, the grid
throws a configuration error. A translator is authoritative: its answer is the
text, including the raw key it returns for a key it does not carry, and the
grid never falls back per key (ADR-034, ADR-045).

`englishGridTranslator` is the package's English translator and the key
contract of every grid text (`src/i18n/english-grid-translator.ts`). A
standalone host, a fixture or an example passes it on purpose
(`t={englishGridTranslator}`); the grid never installs it itself. A key
outside its table returns the raw key.

`useGridTranslation()` and `useGridLocale()` return the EFFECTIVE translator
and presentation of the enclosing grid, including an instance override, for a
custom cell or editor that renders grid-owned words or formats a value. Outside
a grid both throw a configuration error; there is no default presentation
(active-i18n D14).

## Utility Exports

```typescript
import {
  serializeDecimals,
  clearAllGridStates,
  resolveColumnBorderMode,
} from '@workspace/active-grid';
```

| Function | Description |
| --- | --- |
| `serializeDecimals(rows)` | Convert Prisma `Decimal` instances to plain numbers/strings for client transit. |
| `clearAllGridStates()` | Remove all `active_grid_state_*` keys from localStorage (e.g. on logout). |
| `resolveColumnBorderMode(...)` | Internal helper for `showColumnBorders` (rarely needed by consumers). |

## Type Re-exports

### Grid objects and state slices

The objects a consumer receives in renderers and callbacks, and the state slices
it persists or controls, are declared by the package itself. They describe what
a consumer may rely on and nothing about how the grid computes it.

```typescript
import {
  type Grid,
  type Row,
  type Column,
  type Cell,
  type Header,
  type HeaderGroup,
  type CellContext,
  type HeaderContext,
  type SortingState,
  type ColumnFiltersState,
  type GroupingState,
  type SourceRow,
  type GeneratedRow,
  type SourceCell,
  type GeneratedCell,
  type GeneratedCellContext,
} from '@workspace/active-grid';
```

| Type | Carries |
| --- | --- |
| `Grid<TData>` | row models, column and header accessors, state setters and per-slice resets |
| `Row<TData>` | a `SourceRow` or a `GeneratedRow`; `getIsGrouped()` tells which and narrows |
| `SourceRow<TData>` | a row that stands for a record: identity, hierarchy, values, cells, state, `original` |
| `GeneratedRow<TData>` | a row grouping generated: the same members, no record, `groupingColumnId`/`groupingValue` |
| `Column<TData, TValue>` | definition, accessor, visibility, pinning, sizing, sorting, filtering, grouping, facets |
| `Cell<TData, TValue>` | a `SourceCell` or a `GeneratedCell`: the row and column it pairs, its value, predicates |
| `Header<TData, TValue>`, `HeaderGroup<TData>` | the header hierarchy, column spans, sizes and the resize handler; no row span |
| `CellContext` / `HeaderContext` | what a `cell` / `header` / `footer` renderer receives |
| `GeneratedCellContext` | what an `aggregatedCell` renderer receives: a generated row, which has no record |

A row a grid hands out stands for a record, or grouping generated it. Only a
source row carries a record, and the types say so: `row.original` is `TData` on
a `SourceRow` and `undefined` on a `GeneratedRow`. Code that can meet either
narrows first:

```typescript
sortFn: (left, right) =>
  left.getIsGrouped() || right.getIsGrouped()
    ? 0
    : left.original.name.localeCompare(right.original.name, locale),
```

Where only source rows arrive the record needs no narrowing: a filter function
receives a `SourceRow`, an aggregation's leaf rows are `SourceRow`s, and a body
`cell` renderer's `CellContext` carries a `SourceRow`. A comparator, an
eligibility predicate (`enableRowSelection`), `renderDetailPanel`,
`getRowCanExpand` and a row model can hand over a generated row.

`Column`, `Cell`, `Header` and the two contexts carry the column's value type:
a renderer of a numeric column reads a `number` from `getValue()` without an
annotation. `Row.getValue(columnId)` names a column by id, so the caller states
the type (`row.getValue<number>('amount')`).

A collection the grid hands out (`getRowModel().rows`, `getAllColumns()`,
`row.getVisibleCells()`, …) is read-only: build a new array to arrange it
differently. Every slice reset takes an optional flag: `resetSorting()` restores
the initial state, `resetSorting(true)` the grid's own default.

`row.getLeafRows()` returns every descendant of the row in preorder, interior
rows included, not only rows without children. Identifiers are not policed for
uniqueness: two records (or two columns) with the same identifier are both
presented, and a lookup by that identifier answers the last one. Supply unique
identifiers when uniqueness matters.

**Row models and selection.** Within one row model a logical row is one
object: the rows a model presents, its flat rows and its lookup hand back the
same object, and a flat row list holds each row once. Each selected-row model
projects the model it names: `getFilteredSelectedRowModel()` holds only
selected rows the filter kept, `getGroupedSelectedRowModel()` only rows the
grouped presentation carries, and `getPageSelectedRowModel()` the selected
rows of the current page.

**Pinning and positions.** A pinned row or column that no longer exists is
ignored, so restored state may name rows that have gone. A pinned row the
reader folded away, inside a collapsed row or group, is not presented.
`getPinnedIndex()` answers `-1` when not pinned and otherwise the zero-based
position within the pinned section, for a row and a column alike.
`column.getIndex(position?)` counts within one section: the column's own
without an argument, the named one with it, and `-1` for a column not laid out
there. Offsets (`column.getStart()`, `column.getAfter()`, `header.getStart()`)
are measured within the column's own section, because a pinned section does
not scroll with the centre. A header has column spans only: a shallower column
is carried up by placeholder headers.

**Group rows and facets.** A group row's id is its grouping path; when another
row already carries that id, the smallest free suffix `#2`, `#3`, … is
appended, so a group row never shadows a source row.
`column.getFacetedMinMaxValues()` counts present finite numbers only and
answers nothing when there are none. The grid has no global faceting:
`getGlobalFacetedRowModel()`, `getGlobalFacetedUniqueValues()` and
`getGlobalFacetedMinMaxValues()` throw instead of answering an empty result.
`getSortFn()`, `getFilterFn()` and `getAggregationFn()` answer the function the
grid actually applies. `grid.getRow(id)` for an id the grid does not carry
throws an error naming the id, in every environment.

### Other re-exported types

```typescript
import {
  type GroupAggregation,
  type GroupByColumn,
  type GroupInfo,
  type ServerGroupRow,
  type ServerLoadMoreRow,
  isServerGroupRow,
  isServerLoadMoreRow,
  type CellPosition,
  type ColumnBorderMode,
  type ColumnFilterType,
  type FilterComponentProps,
  type FacetOption,
  type HeaderRendererProps,
  type GridExportFormat,
  type GridExportOptions,
  type GridExportScope,
  type GridMeta,
  type GridQueryRequest,
} from '@workspace/active-grid';
```

## Editing types

```typescript
import {
  type EditMode,
  type EditingCommitPayload,
  type BatchCommitPayload,
  type DirtyCellEntry,
  type CellPosition,
  type CellRange,
  type CellSelectionState,
} from '@workspace/active-grid';
```

| Symbol | Type | Description |
| --- | --- | --- |
| `EditMode` | `'cell' \| 'row' \| 'batch'` | The three editing modes. Set via the `editMode` prop on `<ActiveGrid>`. |
| `EditingCommitPayload<TData>` | `{ rowId; originalRow; changes; newData }` | Argument to `onDataCommit`. Contains the diff and the merged row. |
| `BatchCommitPayload<TData>` | `{ changes: EditingCommitPayload<TData>[]; totalCellChanges: number }` | Argument to `onBatchCommit` (batch-mode). All pending edits in one payload. |
| `DirtyCellEntry` | `{ rowId; columnId; originalValue; currentValue; sequence; error? }` | One pending change in batch mode. Used by the internal batch-toolbar and dirty-tracking helpers. |
| `CellPosition` | `{ rowIndex: number; colIndex: number }` | Identifies a cell by numeric row and column indices. |
| `CellRange` | `{ startRow; startCol; endRow; endCol }` | A rectangular cell-range. Used by `enableCellSelection`. |
| `CellSelectionState` | `{ ranges: CellRange[]; activeRange: CellRange \| null }` | The grid's internal selection shape. No callback receives it: `onCellSelectionChange` gets `CellRange[]`. |
