npm package: @reforgium/data-grid
Description: reforgium DataGrid component
Author: rtommievich
Latest version: 3.1.1

# @reforgium/data-grid

[![npm version](https://badge.fury.io/js/%40reforgium%2Fdata-grid.svg)](https://www.npmjs.com/package/@reforgium/data-grid)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**High-performance data grid for Angular (18+).**

`@reforgium/data-grid` provides a flexible and performant component for displaying large tabular datasets.  
It focuses on smooth scrolling, predictable layout, and full control over rendering via templates and signals.

Designed for **real-world datasets**, not demo tables.

---

## Features

- Horizontal and vertical scrolling
- Virtual row rendering (smooth, no jumps)
- Infinity scroll (loads data when reaching the bottom)
- Jitter-free fixed (sticky) columns
- Two-line text clamp (header + body) with ellipsis
- Declarative column DSL (`<re-dg-column>`) *[NEW in 2.0.0]*
- Column expanders (hidden columns via toggler)
- Scrollable overlay scrollbar
- Pinned rows (top and bottom)
- Custom templates for headers, cells, pinned rows, icons
- Skeleton loading rows for pagination/infinity *[NEW in 2.0.0]*
- Row selection (single / multi)
- Signals-based API (`signal()` first)
- Paginator component *[NEW in 1.1.0]*
- Column manager dropdown *[NEW in 2.0.0]*

---

## Installation

```bash
npm install @reforgium/data-grid
```

```ts
import { DataGrid } from '@reforgium/data-grid';
import { DataGridPaginator } from '@reforgium/data-grid/paginator';
import { DataGridColumnManager } from '@reforgium/data-grid/column-manager';

@Component({ imports: [DataGrid, DataGridPaginator] })
export class SomeComponent {
}
```

```html

<re-data-grid
  mode="infinity"
  [data]="users"
  [columns]="columns"
  [pageSize]="50"
  [loading]="loading"
  (pageChange)="loadMore($event)"
/>
```

For small and medium lists, `data` remains the simplest contract.

For server-paged lists, you can now pass a page-oriented source instead:

```ts
import type { GridPagedDataSource } from '@reforgium/data-grid';

type User = { id: number; name: string };

declare const usersSource: GridPagedDataSource<User>;
```

```html
<re-data-grid
  mode="infinity"
  [source]="usersSource"
  [columns]="columns"
  [pageSize]="20"
/>
```

In `source` mode, the grid keeps its own internal page buffer for infinity scrolling, so the parent does not need to maintain one ever-growing accumulated `data[]`.

Important:

- The grid does not auto-fetch on mount in `source` mode.
- The parent should first apply filters / route params / query state and then call the source explicitly (`fetch(...)`, `updatePage(0)`, etc.).
- After the source is initialized, the grid may call `source.updatePage(...)` for user-driven page changes and infinity buffering.
- If the source exposes `updatePageSize(...)`, the grid uses it when the page size changes.
- If the source exposes `sort`, `updateSort(...)`, or `updateSorts(...)`, the grid keeps sort state in sync and can delegate user sorting directly to the source.
- Infinity buffering uses `prefetchMode: 'sequential'` by default for compatibility with latest-wins sources such as `PagedQueryStore`.
- Set `prefetchMode: 'parallel'` only for sources that safely support concurrent `updatePage(...)` calls.

`PagedQueryStore` from `@reforgium/statum` fits this contract directly, so `[source]="store"` is enough for the common server-table case.
---

## Configuration

### Global defaults provider

You can override default input values for all grid instances via DI:

```ts
import { provideDataGridDefaults } from '@reforgium/data-grid';

export const appConfig: ApplicationConfig = {
  providers: [
    provideDataGridDefaults({
      mode: 'pagination',
      hasIndexColumn: true,
      resizable: true,
      pageSize: 50,
      translations: {
        indexColumnHeader: 'No.',
      },
    }),
  ],
};
```

Supported default fields:

- `mode`
- `hasIndexColumn`
- `selection`
- `pageSize`
- `resizable`
- `rowHeight`
- `headerHeight`
- `height`
- `virtualBuffer`
- `loadingMode`
- `deferContent`
- `deferHeader`
- `deferPinned`
- `deferCells`
- `pageStartFromZero`
- `translations`
- `debounce`

`translations` supports: `emptyState`, `itemsPerPageLabel`, `nextPageLabel`, `prevPageLabel`, `indexColumnHeader`.

### Global header text resolver

If your app keeps column headers as i18n keys, you can resolve them globally via DI without wiring `headerTemplate` for every table.

- `provideDataGridHeaderTextResolver(...)` registers `(text, ctx) => string | Signal<string>`
- The resolver applies to plain column headers and header-group titles
- `headerTemplate` / `titleTemplate` still win for markup, but receive already-resolved text in `$implicit`
- `provideDataGridHeaderTextResolverWithParent(...)` is available for advanced parent-resolver composition

```ts
import { inject } from '@angular/core';
import { provideDataGridHeaderTextResolver } from '@reforgium/data-grid';
import { LangService } from '@reforgium/presentia';

export const appConfig: ApplicationConfig = {
  providers: [
    provideDataGridHeaderTextResolver(() => {
      const lang = inject(LangService);

      return (text) => (text.includes('.') ? lang.observe(text) : text);
    }),
  ],
};
```

```ts
columns = [
  { key: 'name', header: 'users.columns.name' },
  { key: 'email', header: 'users.columns.email' },
];
```

### Type registries (global + local)

You can register type-based transformers and renderers globally via DI.

- `provideDataGridTypeTransformers(...)` registers `type -> (row, ctx) => value`
- `provideDataGridTypeRenderers(...)` registers `type -> TemplateRef`
- `reDataGridTypeCell="..."` registers an instance-local renderer (only for that grid instance)

```ts
import {
  provideDataGridTypeTransformers,
} from '@reforgium/data-grid';

export const routes: Routes = [
  {
    path: 'users',
    providers: [
      provideDataGridTypeTransformers({
        money: (_row, ctx) => `$${Number(ctx.value ?? 0).toLocaleString('en-US')}`,
      }),
    ],
    loadComponent: () => import('./users.page').then((m) => m.UsersPage),
  },
];
```

```ts
columns = [
  { key: 'salary', header: 'Salary', type: 'money' },
];
```

```html
<re-data-grid [data]="users" [columns]="columns">
  <!-- Local renderer overrides global renderer for this grid only -->
  <ng-template reDataGridTypeCell="money" let-value="value" let-row="row">
    <b>{{ value }}</b> <small>{{ row.status }}</small>
  </ng-template>
</re-data-grid>
```

Renderer precedence:

1. `column.renderTemplate`
2. local `reDataGridTypeCell`
3. DI `provideDataGridTypeRenderers(...)`
4. built-in renderer (`date`, `number`, `index`, ...)
5. default text renderer

Value transform precedence:

1. `column.transformer(row, ctx)`
2. DI `provideDataGridTypeTransformers(...)`
3. default value pipeline

`ctx` includes: `value`, `col`, `index`, `type`.

Type/value callback context (`ctx`) and pinned rows:

- `value(row, ctx)` receives: `ctx.col`, `ctx.index`, `ctx.isPinned`
- `transformer(row, ctx)` receives: `ctx.value`, `ctx.col`, `ctx.index`, `ctx.type`, `ctx.isPinned`
- Cell template contexts (including `reDataGridTypeCell`, `reDataGridCell`) expose `isPinned`

```ts
columns = [
  {
    key: 'code',
    header: 'Code',
    value: (row, ctx) => (ctx.isPinned ? 'PINNED' : row.code),
  },
  {
    key: 'salary',
    header: 'Salary',
    type: 'money',
    transformer: (row, ctx) => (ctx.isPinned ? `PIN: ${ctx.value}` : ctx.value),
  },
];
```

```html
<ng-template reDataGridTypeCell="money" let-value="value" let-isPinned="isPinned">
  <b>{{ value }}</b>
  @if (isPinned) {
    <small>PIN</small>
  }
</ng-template>
```

### Inputs

| Parameter          | Type                                             | Default            | Description                                                                                     |                  |
|--------------------|--------------------------------------------------|--------------------|-------------------------------------------------------------------------------------------------|------------------|
| data               | `T[]`                                            | `[]`               | Data array to render                                                                            |                  |
| source             | `GridPagedDataSource<T> \| null`                 | `null`             | Optional page-oriented source for pagination/infinity flows                                     | *[NEW in 3.x]*   |
| columns            | `GridColumn<T>[]`                                | `[]`               | Programmatic column configuration (used when no declarative columns are provided)               |                  |
| headerGroups       | `GridHeaderGroup<T>[]`                           | `[]`               | Optional top header row groups (`from`..`to`) for 2-level headers                               | *[NEW in 2.0.0]* |
| pinnedRows         | `GridPinnedRow<T>[]`                             | `[]`               | Top / bottom pinned rows                                                                        |                  |
| isRowSticky        | `(row: T, index: number) => boolean`             | `undefined`        | Predicate that marks rows as sticky at the top                                                  |                  |
| isRowDisabled      | `(row: T, index: number) => boolean`             | `undefined`        | Predicate that marks rows as disabled (no select/click/context/double-click interactions)       |                  |
| getRowTemplate     | `(row: T, index: number) => TemplateRef \| null` | `undefined`        | Optional resolver for custom row template                                                       |                  |
| sortMode           | 'single' \| 'multi'                              | 'single'           | Sorting mode for header clicks                                                                  |                  |
| pageSize           | `number`                                         | `20`               | Number of items per page (pagination/infinity modes)                                            |                  |
| pageStartFromZero  | `boolean`                                        | `true`             | If true, page indexing starts from 0, otherwise from 1                                          |                  |
| hasIndexColumn     | `boolean`                                        | `false`            | If true, an index column will be shown                                                          |                  |
| selection          | `GridSelection<T>`                               | `{ mode: 'none' }` | Row selection configuration                                                                     |                  |
| rowHeight          | `number`                                         | `40`               | Fixed height of each row in pixels                                                              | *[FIX in 1.1.0]* |
| virtualBuffer      | `number`                                         | `8`                | Extra rows to render above/below viewport to reduce flickering during scrolling                 |                  |
| lockVerticalScroll | `boolean`                                        | `false`            | Locks vertical scrolling while keeping horizontal scroll enabled                                |                  |
| height             | `number \| 'full' \| 'default'`                  | `'default'`        | Grid height configuration (`number` for px, `'full'` for 100%, `'default'` for CSS variable)    |                  |
| loading            | `boolean`                                        | `false`            | Displays loading state template when true                                                       |                  |
| loadingMode        | `'spinner' \| 'skeleton'`                        | `'spinner'`        | Loading rendering mode (`spinner` shows centered spinner, `skeleton` enables row skeleton mode) | *[NEW in 2.0.0]* |
| deferContent       | `boolean`                                        | `true`             | Defers rendering of main content (code-splitting)                                               |                  |
| deferHeader        | `boolean`                                        | `false`            | Defers rendering of header (groups + columns)                                                   |                  |
| deferPinned        | `boolean`                                        | `false`            | Defers rendering of pinned rows (top + bottom)                                                  |                  |
| deferCells         | `boolean`                                        | `false`            | Defers rendering of cell content                                                                |                  |
| rowKey             | `DataKey<T> \| ((item: T) => string \| number)`  | `undefined`        | Property name or function to resolve unique row key                                             |                  |

When selection mode is `'single'` or `'multi'`, provide a `key` (data property) and optionally `defaultSelected`.

Selection mode guidance:

- Prefer using row selection with `mode="pagination"` for clearer UX and predictable “select all” behavior.
- In `mode="infinity"`, selection typically applies only to currently loaded rows, so use it only when that behavior is acceptable.

Data source guidance:

- Use `data` when the parent already owns the full rendered collection.
- Use `source` for server-driven pagination or infinity flows where the parent should expose only the current page.
- In `mode="infinity"` + `source`, the grid uses `totalElements` for scroll height when available and stores loaded page chunks internally instead of forcing the parent to append rows into one large array.
- `GridPagedDataSource.totalElements` is optional for infinity sources. If omitted, the grid keeps requesting pages until it receives a short page (`items.length < pageSize`) and then treats that page as the end of the dataset.
- In `source` mode, initialize the source from the parent after all filters / params are ready; the grid does not perform an automatic first fetch on mount.
- `GridPagedDataSource.error` is optional and can be used by consumers that want to project source-level error UI near the grid.
- `GridPagedDataSource.sort`, `updateSort(...)`, and `updateSorts(...)` let the grid use source-owned sort state instead of forcing a separate parent bridge.
- `GridPagedDataSource.prefetchMode` defaults to `sequential`; keep that default for `PagedQueryStore` and similar latest-wins sources.
- Use `prefetchMode: 'parallel'` only when the source supports multiple concurrent `updatePage(...)` requests without canceling each other.
- When `source.version` changes or a new source is assigned, the grid invalidates its internal buffer immediately; any in-flight `updatePage()` responses that arrive after invalidation are discarded and do not overwrite the fresh buffer.

Feature lazy-loading behavior (runtime `import()`):

- Selection feature is loaded when `selection.mode !== 'none'`.
- Sticky feature is loaded when `isRowSticky` is provided.
- Tooltip feature is loaded when at least one column has `tooltip`.
- Overlay-scroll feature is loaded on first scroll lifecycle usage.

Loading behavior:

- `loadingMode="spinner"`: shows centered spinner over grid content.
- `loadingMode="skeleton"` + `mode="infinity"` + `loading=true`: appends 4 skeleton rows at the end of the current data.
- `loadingMode="skeleton"` + `mode="pagination"` + `loading=true` + empty data: renders 4 skeleton rows in the table body.

Sticky rows:

- Provide `isRowSticky` to mark rows that should stick to the top during scroll.
- You can customize the sticky row rendering with `reDataGridStickyRow` template.

```html

<re-data-grid [data]="items" [columns]="columns" [isRowSticky]="isSticky">
  <ng-template reDataGridStickyRow let-row let-index="index">
    <div class="my-sticky-row">{{ index + 1 }}. {{ row.name }}</div>
  </ng-template>
</re-data-grid>
```

Row templates:

- Provide `getRowTemplate` to select a custom row template per row.
- Use `reDataGridRow` as a default row template.

```html

<re-data-grid [data]="items" [columns]="columns" [getRowTemplate]="rowTpl">
  <ng-template reDataGridRow let-index="index">
    <div class="my-row">{{ index + 1 }}. {{ row.name }}</div>
  </ng-template>
</re-data-grid>
```

### Outputs

| Event           | Type                          | Description                                            |                  |
|-----------------|-------------------------------|--------------------------------------------------------|------------------|
| cellClick       | `GridCellClickEvent<T>`       | Emitted when a cell is clicked (includes native event) | *[UPD in 2.0.0]* |
| cellContext     | `GridCellContextEvent<T>`     | Emitted on cell context menu                           | *[NEW in 2.0.0]* |
| cellDoubleClick | `GridCellDoubleClickEvent<T>` | Emitted when a cell is double-clicked                  | *[NEW in 2.0.0]* |
| rowClick        | `GridRowClickEvent<T>`        | Emitted when a row is clicked (includes native event)  | *[UPD in 2.0.0]* |
| rowContext      | `GridRowContextEvent<T>`      | Emitted on row context menu                            | *[NEW in 2.0.0]* |
| rowDoubleClick  | `GridRowDoubleClickEvent<T>`  | Emitted when a row is double-clicked                   | *[NEW in 2.0.0]* |
| columnResizeEnd | `GridColumnResizeEndEvent<T>` | Emitted when a column resize drag ends (key + px width); use to persist widths | *[NEW in 3.1.0]* |
| sortChange      | `GridSortEvent<T>`            | Emitted when single-sort order changes                 |                  |
| multiSortChange | `GridMultiSortEvent<T>`       | Emitted when multi-sort order changes                  | *[NEW in 2.2.0]* |
| pageChange      | `GridPageChangeEvent`         | Emitted when requesting data for a new page            |                  |
| selectChange    | `GridSelectEvent<T>`          | Emitted when selected rows change                      |                  |

Notes:

- A cell click also triggers the row click event (bubbling), so listen to one or stop propagation if needed.
- In `source` mode, sort events are still emitted, but if the source implements sort hooks the grid also updates the source directly.
- `selectChange` is emitted not only on user interaction but also when the grid automatically prunes selected keys after a data update removes rows that were previously selected. Always treat `selectChange` as the authoritative selected state.

### Public API methods

`DataGrid` exposes imperative helpers via component instance (e.g. `@ViewChild(DataGrid)`):

- `clearSelection()` - clears current selection and emits `selectChange`
- `selectAllLoaded()` - selects currently loaded rows (multi mode) and emits `selectChange`
- `resetSort()` - resets sort state and emits sort events
- `setSort(event)` - applies single-sort state and emits sort events
- `setMultiSort(items)` - applies multi-sort state and emits sort events

### GridPagedDataSource<T> reference

The optional `source` input accepts a page-oriented contract:

| Field / method | Type | Description |
|---|---|---|
| `items` | `Signal<T[]>` | Current page items |
| `loading` | `Signal<boolean>` | Source loading state |
| `error` | `Signal<unknown \| null>` | Optional source error state |
| `page` | `number` | Current page index |
| `pageSize` | `number` | Current page size |
| `totalElements` | `number \| undefined` | Optional total size |
| `sort` | `ReadonlyArray<GridSortItem<T>> \| undefined` | Optional source-owned sort state |
| `version` | `Signal<number> \| undefined` | Dataset reset marker for local buffer invalidation |
| `updatePage(page)` | `(page: number) => Promise<unknown>` | Load a page |
| `updatePageSize(size)` | `((size: number) => Promise<unknown>) \| undefined` | Optional page-size handler |
| `updateSort(sort)` | `((sort?: GridSortItem<T> \| null) => Promise<unknown>) \| undefined` | Optional single-sort handler |
| `updateSorts(sort)` | `((sort?: ReadonlyArray<GridSortItem<T>> \| null) => Promise<unknown>) \| undefined` | Optional multi-sort handler |
| `prefetchMode` | `'sequential' \| 'parallel' \| undefined` | Infinity prefetch strategy |

This contract is intentionally grid-like, not `statum`-specific, but `PagedQueryStore` already matches it well enough to be used directly.

### GridColumn<T> reference

`columns` accepts `GridColumn<T>[]`, where `GridColumn<T>` is a union of three column variants:

- default column (`type` + optional `typeParams`)
- value column (`value` + required `track`)
- template column (`renderTemplate` + required `track`)

Common (base) fields:

| Field                   | Type                                                                        | Description                                                                            |                  |
|-------------------------|-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------|
| `sortKey`               | `string`                                                                    | Alternative key used for sorting when display `key` differs from sortable data.        |                  |
| `sticky`                | `'left' \| 'right' \| true`                                                 | Keeps the column fixed while horizontally scrolling. `true` pins to the left.          |                  |
| `expandBy`              | `DataKey<T>`                                                                | Data key that controls expand/collapse behavior for the column.                        |                  |
| `flex`                  | `number`                                                                    | Flex grow factor for width distribution in flexible layouts.                           |                  |
| `minWidth` / `maxWidth` | `number`                                                                    | Column width limits in pixels.                                                         |                  |
| `resizable`             | `boolean`                                                                   | Enables drag resize from header. `false` disables resize handle for the column.        |                  |
| `cellClass`             | `string \| ((row: T) => string)`                                            | Static CSS class or resolver per row.                                                  |                  |
| `tooltip`               | `true \| string \| ((row: T) => string) \| TemplateRef<GridTooltipContext>` | Popover tooltip content shown on cell hover. `true` uses the cell value automatically. | *[NEW in 2.2.0]* |

Renderer-specific fields:

| Variant                  | Fields                                                                           | Notes                                                                                                                  |
|--------------------------|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| Built-in / type renderer | `type?: GridBuiltInCellType \| string`, `typeParams?: any`, `defaultValue?: any` | Built-in types are `plain/date/number/index/checkbox`; custom string types can be handled by local/DI type registries. |
| Value renderer           | `value: (row: T, ctx) => string \| number`, `track: (row: T) => string`          | `track` is required for stable row identity and efficient updates. `ctx` includes `col`, `index`, `isPinned`.          |
| Template renderer        | `renderTemplate: TemplateRef<...>`, `track: (row: T) => string`                  | `track` is required when rendering through Angular templates.                                                          |

Cell text wrapping/clamp behavior:

- Default header text and default body renderers (`plain`, `date`, `number`, `index`) are limited to 2 lines with ellipsis.
- Template-based renderers (`renderTemplate`, `reDataGridCell`, `reDataGridTypeCell`, etc.) are not clamped by default and are fully controlled by your template styles.
- Cell template contexts also include `isPinned` so pinned top/bottom rows can be rendered differently.

### Declarative columns *[NEW in 2.0.0]*

You can define columns directly in markup via `<re-dg-column>`, then the grid will normalize them to `GridColumn<T>` internally.

```html

<re-data-grid [data]="items" [rowKey]="'id'">
  <re-dg-column key="name">
    <ng-template reHeader>Name</ng-template>
    <ng-template reCell let-row="row">{{ row.name }}</ng-template>
  </re-dg-column>

  <re-dg-column key="age" sortKey="age">
    <ng-template reHeader>Age</ng-template>
    <ng-template reCell let-row="row">{{ row.age }}</ng-template>
  </re-dg-column>

  <re-dg-column key="extra" sticky="left" expandBy="age" disabled>
    <ng-template reHeader>Extra</ng-template>
    <ng-template reCell let-row="row">{{ row.extra }}</ng-template>
  </re-dg-column>
</re-data-grid>
```

Notes:

- If at least one `<re-dg-column>` is present, declarative columns are used as the base configuration.
- When used with column manager state, `visible`, `disabled`, and `sticky` are taken from state when defined.
- `reHeader` maps to `headerTemplate`.
- `reCell` receives value as `$implicit`, and the row is available as `let-row="row"` via `RenderTemplateData`.
- Most `GridColumn<T>` fields are available as `<re-dg-column>` inputs (`sortKey`, `sticky`, `expandBy`, `disabled`, `width`, `minWidth`, `maxWidth`, `flex`, `resizable`, `align`, `cellClass`, `type`, `typeParams`, `defaultValue`, `value`, `track`, `tooltip`).
- `sticky` accepts `'left' | 'right'`; legacy `true` maps to `'left'` for backward compatibility.

### Tooltip

`tooltip` is opt-in per column and shows a popover on cell hover.

```ts
columns = [
  { key: 'name', header: 'Name', tooltip: true }, // auto tooltip from cell value
  { key: 'email', header: 'Email', tooltip: (row) => row.email },
  { key: 'status', header: 'Status', tooltip: 'User status' },
];
```

Template tooltip:

```html

<ng-template #tip let-row let-col="col" let-index="index" let-value="value">
  <div><b>{{ col.header }}</b> #{{ index + 1 }}</div>
  <div>{{ value }}</div>
</ng-template>

<re-dg-column key="email" [tooltip]="tip"></re-dg-column>
```

Tooltip template context (`GridTooltipContext`):

- `$implicit` / `row`
- `col`
- `index`
- `value`

### Header groups *[NEW in 2.0.0]*

Use `headerGroups` to render an optional top header row (2-level header layout) above regular column headers.

| Field           | Type                              | Description                                                      |
|-----------------|-----------------------------------|------------------------------------------------------------------|
| `key`           | `string`                          | Unique id of the header group.                                   |
| `from`          | `DataKey<T>`                      | Start column key (inclusive).                                    |
| `to`            | `DataKey<T>`                      | End column key (inclusive). If omitted, group covers one column. |
| `title`         | `string`                          | Plain text title for the group.                                  |
| `titleTemplate` | `TemplateRef<HeaderTemplateData>` | Template-based title for the group.                              |
| `align`         | `'left' \| 'center' \| 'right'`   | Optional group title alignment (plain title variant).            |

Notes:

- Groups are normalized against visible columns only.
- Overlapping or invalid ranges are ignored during normalization.
- Columns not covered by groups are automatically placed into technical spacer groups to keep width alignment.

### Templates

| Directive              | Parameters                        | Description                                       |                  |
|------------------------|-----------------------------------|---------------------------------------------------|------------------|
| reHeader               | -                                 | Declarative header template inside `re-dg-column` | *[NEW in 2.0.0]* |
| reCell                 | let-row                           | Declarative cell template inside `re-dg-column`   | *[NEW in 2.0.0]* |
| reDataGridHeader       | key: string                       | Template for specific column header by key        |                  |
| reDataGridCell         | key: string                       | Template for specific column cell by key          | *[NEW in 1.1.0]* |
| reDataGridTypeCell     | type: string                      | Template for cells of a specific column type      |                  |
| reDataGridEmpty        | -                                 | Template for the empty state (no data)            |                  |
| reDataGridLoading      | -                                 | Template for the loading state                    |                  |
| reDataGridSortIcon     | order: GridSortOrder \| undefined | Template for custom sorting icon                  | *[NEW in 1.1.0]* |
| reDataGridExpanderIcon | expanded: boolean                 | Template for custom expander icon                 | *[NEW in 1.1.0]* |

### CSS Variables

Layout / Appearance:

- `--re-data-grid-min-height` - component min height (`200px`)
- `--re-data-grid-height` - component height (`400px`)
- `--re-data-grid-rounded` - border radius (`var(--radius-md, 6px)`)
- `--re-data-grid-separator-color` - outer border color (`var(--border-color)`)
- `--re-data-grid-separator` - outer border (`1px solid var(--re-data-grid-separator-color)`)
- `--re-data-grid-surface` - table background (`#fff`)
- `--re-data-grid-active` - active color (`#2a90f4`)

Empty / Loading States:

- `--re-data-grid-empty-color` - empty text color (`#777`)
- `--re-data-grid-empty-surface` - empty text background (`transparent`)
- `--re-data-grid-loading-color` - loading indicator color (`#444`)
- `--re-data-grid-loading-surface` - loading spinner (`rgba(255,255,255,0.5)`)
- `--re-data-grid-spinner-size` - spinner size (`2rem`)
- `--re-data-grid-spinner-width` - spinner ring thickness (`0.25rem`)
- `--re-data-grid-spinner-track-color` - spinner track color (`rgba(0, 0, 0, 0.12)`)
- `--re-data-grid-skeleton-width` - skeleton line width (`100%`)
- `--re-data-grid-skeleton-height` - skeleton line height (`100%`)
- `--re-data-grid-skeleton-rounded` - skeleton border radius (`var(--re-data-grid-rounded, 0.75rem)`)
- `--re-data-grid-skeleton-line` - skeleton base color (`#e7ebf0`)
- `--re-data-grid-skeleton-shine` - skeleton highlight color (`rgba(255, 255, 255, 0.8)`)

Tooltip:

- `--re-data-grid-tooltip-surface` - tooltip background (`#0f172a`)
- `--re-data-grid-tooltip-color` - tooltip text color (`#f8fafc`)
- `--re-data-grid-tooltip-radius` - tooltip radius (`0.5rem`)
- `--re-data-grid-tooltip-padding` - tooltip padding (`0.4rem 0.6rem`)
- `--re-data-grid-tooltip-shadow` - tooltip shadow (`0 8px 24px rgba(15, 23, 42, 0.25)`)
- `--re-data-grid-tooltip-z` - tooltip z-index (`60`)

Note: for `--re-data-grid-skeleton-height` it's usually better to use percentages (for example `60%` / `70%`) so the line scales naturally with row height.

Scrollbar:

- `--re-data-grid-scrollbar-size` - track size (`4px`)
- `--re-data-grid-scrollbar-offset` - inner offset (`2px`)
- `--re-data-grid-scrollbar-track-rounded` - track radius (`0.25rem`)
- `--re-data-grid-scrollbar-track-surface` - track surface (`transparent`)
- `--re-data-grid-scrollbar-thumb-size` - thumb size (`8px`)
- `--re-data-grid-scrollbar-thumb-color` - thumb color (`rgba(0,0,0,0.25)`)
- `--re-data-grid-scrollbar-thumb-active-color` - thumb active color (`rgba(0,0,0,0.45)`)
- `--re-data-grid-scrollbar-thumb-rounded` - thumb radius (`var(--re-data-grid-scrollbar-track-rounded)`)

Header:

- `--re-data-grid-header-rounded` - header corner radius (`var(--re-data-grid-rounded)`)
- `--re-data-grid-header-surface` - header area background (`#fff`)
- `--re-data-grid-header-body-gap` - gap between header and body (`0px`)
- `--re-data-grid-header-row-height` - main header row height (`40px`)
- `--re-data-grid-header-row-separator-color` - main header separator color (`#ccc`)
- `--re-data-grid-header-row-separator` - main header separator (`1px solid var(--re-data-grid-header-row-separator-color)`)
- `--re-data-grid-header-group-row-height` - group header row height (`var(--re-data-grid-header-row-height)`)
- `--re-data-grid-header-group-row-separator-color` - group header separator color (`var(--re-data-grid-header-row-separator-color)`)
- `--re-data-grid-header-group-row-separator` - group header separator (`1px solid var(--re-data-grid-header-group-row-separator-color)`)

Header Cells:

- `--re-data-grid-header-cell-font-weight` - font weight (`600`)
- `--re-data-grid-header-cell-font-size` - font size (`0.8rem`)
- `--re-data-grid-header-cell-color` - text color (`#000`)
- `--re-data-grid-header-cell-surface` - cell background (`#fafafa`)
- `--re-data-grid-header-cell-line-height` - header text line-height for clamp (`1.2`)
- `--re-data-grid-header-cell-max-lines` - header max text lines (`2`)
- `--re-data-grid-header-group-cell-font-weight` - group font weight (`var(--re-data-grid-header-cell-font-weight)`)
- `--re-data-grid-header-group-cell-font-size` - group font size (`var(--re-data-grid-header-cell-font-size)`)
- `--re-data-grid-header-group-cell-color` - group text color (`var(--re-data-grid-header-cell-color)`)
- `--re-data-grid-header-group-cell-surface` - group cell background (`var(--re-data-grid-header-cell-surface)`)

Footer:

- `--re-data-grid-footer-separator-color` - separator color (`#ccc`)
- `--re-data-grid-footer-separator` - separator line (`1px solid var(--re-data-grid-footer-separator-color)`)
- `--re-data-grid-footer-surface` - footer background (`#fff`)

Rows:

- `--re-data-grid-row-separator-color` - row separator color (`#bbb`)
- `--re-data-grid-row-separator` - separator line (`1px solid var(--re-data-grid-row-separator-color)`)
- `--re-data-grid-row-odd-surface` - odd rows background (`var(--re-data-grid-cell-surface)`)
- `--re-data-grid-row-hover-surface` - hover rows background (`var(--re-data-grid-cell-surface)`)
- `--re-data-grid-row-hover-color` - hover rows text color (`var(--re-data-grid-cell-color)`)
- `--re-data-grid-row-hover-rounded` - hover row corner radius (`0px`)

Columns:

- `--re-data-grid-column-separator-color` - column divider color (`transparent`)
- `--re-data-grid-column-separator` - column divider (`1px solid var(--re-data-grid-column-separator-color)`)
- `--re-data-grid-column-odd-surface` - odd column background (`var(--re-data-grid-cell-surface)`)

Cells:

- `--re-data-grid-cell-paddings` - inner paddings (`0.4rem 0.625rem`)
- `--re-data-grid-cell-font-weight` - font weight (`400`)
- `--re-data-grid-cell-font-size` - font size (`0.75rem`)
- `--re-data-grid-cell-color` - text color (`#000`)
- `--re-data-grid-cell-surface` - cell background (`#fff`)
- `--re-data-grid-cell-line-height` - body text line-height for clamp (`1.2`)
- `--re-data-grid-cell-max-lines` - body max text lines (`2`)

Checkbox:

- `--re-data-grid-checkbox-size` - checkbox size (`20px`)
- `--re-data-grid-checkbox-stroke` - checkbox border width (`2px`)
- `--re-data-grid-checkbox-border` - checkbox border color (`var(--border-color, #9aa3af)`)
- `--re-data-grid-checkbox-tick` - checkbox tick color (`var(--surface-neutral, #fff)`)
- `--re-data-grid-checkbox-surface` - checkbox background (`var(--surface-neutral, #fff)`)
- `--re-data-grid-checkbox-active-color` - checkbox active color (`var(--primary-color, #2563eb)`)

Focus Ring:

- `--re-data-grid-focus-ring-color` - keyboard focus outline color (`color-mix(in srgb, var(--primary-color, #2a90f4) 55%, transparent)`)
- `--re-data-grid-focus-ring-width` - keyboard focus outline width (`2px`)
- `--re-data-grid-focus-ring-offset` - keyboard focus outline offset (`-2px`)

Sticky Cells:

- `--re-data-grid-sticky-header-cell-surface` - sticky header cell background (`#fff`)
- `--re-data-grid-sticky-cell-surface` - sticky body cell background (`#fdfdfd`)
- `--re-data-grid-sticky-cell-row-odd-surface` - sticky odd rows background (`#fdfdfd`)
- `--re-data-grid-sticky-cell-left-shadow` - left shadow (`2px 0 2px rgba(0,0,0,0.03)`)
- `--re-data-grid-sticky-cell-right-shadow` - right shadow (`-2px 0 2px rgba(0,0,0,0.03)`)

Pinned Sections:

- `--re-data-grid-pinned-surface` - pinned sections background (`#fcfcfc`)
- `--re-data-grid-pinned-separator-color` - separator color (`#eee`)
- `--re-data-grid-pinned-separator` - separator line (`1px solid var(--re-data-grid-pinned-separator-color)`)

Misc:

- `--re-data-grid-expander-color` - expander indicator color (`var(--primary-color, currentColor)`)
- `--re-data-grid-expanded-color` - expanded column color (`var(--re-data-grid-cell-color, #000)`)
- `--re-data-grid-expanded-surface` - expanded column background (`var(--re-data-grid-cell-surface, #fff)`)

---

## Paginator *[NEW in 1.1.0]*

The paginator component is used to display a page selector and total count.

### Inputs

| Parameter       | Type       | Default           | Description                            |
|-----------------|------------|-------------------|----------------------------------------|
| current         | `number`   | 0                 | Current page                           |
| pageSize        | `number`   | 0                 | Number of items per page               |
| totalElements   | `number`   | 0                 | Total number of elements               |
| maxShowPages    | `number`   | 7                 | Maximum number of page buttons to show |
| showFirstLast   | `boolean`  | `false`           | Show "First" and "Last" buttons        |
| showPerPage     | `boolean`  | `false`           | Show page-size dropdown                |
| pageSizeOptions | `number[]` | `[10,20,50,100]`  | Options for per-page dropdown          |
| perPageLabel    | `string`   | `Items per page:` | Label near dropdown                    |
| firstLabel      | `string`   | `First`           | Fallback label for first-page button   |
| lastLabel       | `string`   | `Last`            | Fallback label for last-page button    |

### Outputs

| Event          | Type     | Description                                                          |
|----------------|----------|----------------------------------------------------------------------|
| pageChange     | `number` | Emitted when the page changes. Returns the new page index (0-based). |
| pageSizeChange | `number` | Emitted when per-page value changes.                                 |

### Paginator templates

You can customize first/last controls with `ng-template`:

```html

<re-data-grid-paginator
  showFirstLast
  showPerPage
  [current]="page"
  [pageSize]="20"
  [pageSizeOptions]="[10, 20, 50]"
  [totalElements]="total"
  (pageChange)="page = $event"
  (pageSizeChange)="pageSize = $event"
>
  <ng-template reDataGridPaginatorFirst let-targetPage let-disabled="disabled">
    <span [style.opacity]="disabled ? 0.5 : 1">&lt;&lt; First</span>
  </ng-template>

  <ng-template reDataGridPaginatorPage let-label="label" let-active="active">
    <span [style.fontWeight]="active ? 700 : 400">{{ label }}</span>
  </ng-template>

  <ng-template reDataGridPaginatorLast let-targetPage let-disabled="disabled">
    <span [style.opacity]="disabled ? 0.5 : 1">Last &gt;&gt;</span>
  </ng-template>
</re-data-grid-paginator>
```

Template contexts:

- `reDataGridPaginatorFirst` / `reDataGridPaginatorLast`: `$implicit` (target page), `current`, `total`, `disabled`
- `reDataGridPaginatorPage`: `$implicit` / `page` (0-based index), `label` (1-based display), `current`, `total`, `active`

### CSS Variables

First / Last controls:

- `--re-data-grid-paginator-edge-min-width` - min width (`2.5rem`)
- `--re-data-grid-paginator-edge-height` - control height (`var(--re-data-grid-paginator-page-size)`)
- `--re-data-grid-paginator-edge-paddings` - control paddings (`0 0.625rem`)
- `--re-data-grid-paginator-edge-border` - control border (`var(--re-data-grid-paginator-page-border)`)
- `--re-data-grid-paginator-edge-rounded` - control border radius (`var(--re-data-grid-paginator-page-rounded)`)
- `--re-data-grid-paginator-edge-surface` - control background (`var(--re-data-grid-paginator-page-surface)`)
- `--re-data-grid-paginator-edge-color` - control text color (`var(--re-data-grid-paginator-page-color)`)
- `--re-data-grid-paginator-edge-font-size` - control font size (`var(--re-data-grid-paginator-page-font-size)`)
- `--re-data-grid-paginator-edge-hover-surface` - hover background (`var(--re-data-grid-paginator-page-hover-surface)`)
- `--re-data-grid-paginator-edge-hover-color` - hover text color (`var(--re-data-grid-paginator-page-hover-color)`)
- `--re-data-grid-paginator-edge-disabled-opacity` - disabled opacity (`0.5`)

Layout:

- `--re-data-grid-paginator-gap` - gap between paginator elements (`0.5rem`)

Page:

- `--re-data-grid-paginator-page-size` - base page button size, used as default min-width and height (`1.75rem`)
- `--re-data-grid-paginator-page-min-width` - page button min width (`var(--re-data-grid-paginator-page-size)`)
- `--re-data-grid-paginator-page-height` - page button height (`var(--re-data-grid-paginator-page-size)`)
- `--re-data-grid-paginator-page-paddings` - page button paddings (`0 0.5rem`)
- `--re-data-grid-paginator-page-border` - page button border (`1px solid var(--re-data-grid-paginator-separator-color, #e2e8f0)`)
- `--re-data-grid-paginator-page-separator-color` - page button border color (`var(--re-data-grid-separator-color, --border-color)`)
- `--re-data-grid-paginator-page-rounded` - page button border radius (`var(--re-data-grid-rounded, --radius-md)`)
- `--re-data-grid-paginator-page-surface` - page button background (`var(--re-data-grid-surface, white)`)
- `--re-data-grid-paginator-page-color` - page button text color (`var(--text-primary, #1e293b)`)
- `--re-data-grid-paginator-page-font-size` - page button font size (`0.875rem`)

Active Page:

- `--re-data-grid-paginator-page-active-surface` - active page button background (`var(--re-data-grid-active, #3b82f6)`)
- `--re-data-grid-paginator-page-active-color` - active page button text color (`white`)

Hover Page:

- `--re-data-grid-paginator-page-hover-surface` - page button hover background (`var(--re-data-grid-active, #3b82f6)`)
- `--re-data-grid-paginator-page-hover-color` - page button hover text color (`white`)


### Column manager *[NEW in 2.0.0]*

Column manager is a secondary entrypoint that provides a dropdown UI for reordering, show/hide, and pinning columns.

```ts
import { DataGridColumnManager } from '@reforgium/data-grid/column-manager';
```

```html

<re-data-grid-column-manager
  triggerLabel="Columns"
  [columns]="managedColumns()"
  (columnsChange)="managedColumns.set($event)"
/>

<re-data-grid [columns]="managedColumns()" ... />
```

Custom trigger via content projection:

```html

<re-data-grid-column-manager
  triggerLabel="Columns"
  [columns]="managedColumns()"
  (columnsChange)="managedColumns.set($event)"
>
  <ng-template reDataGridColumnManagerTrigger>Manage columns</ng-template>
</re-data-grid-column-manager>
```

Custom column title template (controls stay built-in):

```html
<re-data-grid-column-manager
  [columns]="managedColumns()"
  (columnsChange)="managedColumns.set($event)"
>
  <ng-template reDataGridColumnManagerColumnTitle let-title let-column="column">
    <span class="font-medium">{{ title }}</span>
  </ng-template>
</re-data-grid-column-manager>
```

`reDataGridColumnManagerColumnTitle` context:

- `$implicit` / `title` - resolved column title (`header || key`)
- `column` - current `GridColumn<T>`

Standalone imports for custom trigger/title markers:

```ts
import {
  DataGridColumnManager,
  DataGridColumnManagerColumnTitleDirective,
  DataGridColumnManagerTriggerDirective,
} from '@reforgium/data-grid/column-manager';
```

```ts
@Component({
  imports: [
    DataGridColumnManager,
    DataGridColumnManagerTriggerDirective,
    DataGridColumnManagerColumnTitleDirective,
  ],
})
export class ExampleComponent {}
```

Example with both trigger and column title customizations:

```html
<re-data-grid-column-manager [columns]="managedColumns()" (columnsChange)="managedColumns.set($event)">
  <ng-template reDataGridColumnManagerTrigger>Manage columns</ng-template>
  <ng-template reDataGridColumnManagerColumnTitle let-title>
    <span>{{ title }}</span>
  </ng-template>
</re-data-grid-column-manager>
```

Inputs:

- `columns: GridColumn<T>[]`
- `triggerLabel?: string`
- `showAllLabel?: string`
- `hideAllLabel?: string`
- `controlsVisible?: boolean` - hides search and "show all / hide optional" panel
- `searchable?: boolean`
- `allowReorder?: boolean`
- `allowPin?: boolean`
- `allowVisibility?: boolean`

Outputs:

- `columnsChange: GridColumn<T>[]`

### Column manager CSS Variables

- `--re-data-grid-cm-gap` - layout gap (`0.5rem`)
- `--re-data-grid-cm-rounded` - panel/trigger radius (`0.625rem`)
- `--re-data-grid-cm-border` - border (`1px solid var(--surface-border, #dfe1e6)`)
- `--re-data-grid-cm-surface` - surface color (`var(--surface-neutral, #fff)`)
- `--re-data-grid-cm-muted` - muted text color (`var(--text-muted, #64748b)`)
- `--re-data-grid-cm-active` - active color (`var(--primary-color, #2a90f4)`)
- `--re-data-grid-cm-shadow` - panel shadow (`0 12px 30px rgba(15, 23, 42, 0.18)`)

---

### Theming via CSS Variables

Variables can be overridden globally (`:root`) or per grid container.

### Quick Theming Examples

Dark Theme Example:

```css
.dark .data-grid {
  --re-data-grid-surface: #121416;
  --re-data-grid-header-cell-surface: #161a1d;
  --re-data-grid-cell-color: #eef2f6;
  --re-data-grid-row-separator-color: #262a2e;
  --re-data-grid-column-separator-color: #1e2226;
}
```

Compact Mode Example:

```css
.compact-grid {
  --re-data-grid-header-row-height: 32px;
  --re-data-grid-cell-font-size: 12px;
  --re-data-grid-cell-paddings: 2px 8px;
}
```

### Custom Template Examples

Cell Template by Type

```html

<ng-template reDataGridTypeCell="link" let-row="row">
  <a [href]="value" target="_blank">{{ value }}</a>
</ng-template>
```

Cell Template *[NEW in 1.1.0]*

```html

<ng-template reDataGridCell="fullName" let-row="row">
  <span><b>{{ row.firstName }}</b> {{ row.lastName }}</span>
</ng-template>
```

Custom Header Template:

```html

<ng-template reDataGridHeader="name" let-value="value">
  <span class="header-with-icon">
    <icon name="user" size="20" />
    {{ value }}
  </span>
</ng-template>
```

## Compatibility

- Angular: 18+
- Rendering: ESM
- Signals: required
- Zone.js: optional

## License

MIT