npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chainsys/sab-react-grid

v2.0.2

Published

Enterprise React data grid (TanStack Table): virtualization, filters, grouping, frozen columns, aggregations, field navigation, Intl currency / URL / rich-text formatters, Excel & CSV export, Tailwind UI and themes.

Readme

@chainsys/sab-react-grid

A professional, enterprise-grade data grid for React applications. Built on TanStack Table and TanStack React Virtual, it delivers high-performance tables with sorting, filtering, grouping, column visibility, virtualization, and Excel/CSV export. The UI is Tailwind CSS–based (utility classes and dark: variants), with light / dark / system theme handling that integrates with your app’s toggle or document-level styling.

Package: @chainsys/sab-react-grid
Version: 2.0.2
License: MIT

Technical documentation: On the npm package page, only this README.md is rendered as formatted Markdown. The full technical reference is included in this same document below (Technical documentation) so it uses the same preview as the rest of the readme.


Features

| Feature | Description | |--------|-------------| | TanStack Table | Full control over sorting, filtering, grouping, pagination, column visibility, order, and sizing | | Row virtualization | Renders only visible rows via @tanstack/react-virtual for smooth scrolling with large datasets | | Layout modes | fit-default, fit-window, or fit-content (content-based column widths) | | Column filters | Text, number, date, datetime, time (Flatpickr), multiselect, checkbox, boolean, dropdown, and radio with operand support (contains, equals, greater than, etc.) | | Filter placeholders | Search icon (🔎︎) for text/number and dropdown filters; calendar icon for date/datetime/time filters (with SVG placeholder when empty) | | Export | Excel (.xlsx) via ExcelJS and CSV via plain Blob from the toolbar Data Export menu | | Grouping | Drag columns into the grouping zone; expand/collapse groups | | Aggregations (group + list footers) | Optional per-column aggregates with Slickgrid-style group footers (under expanded groups) and an optional list footer (<tfoot>) for grand totals | | Column reorder | Drag-and-drop column reordering in the header | | Column visibility | Show/hide columns from the Columns menu | | Sticky headers | Header and filter rows stay fixed while the body scrolls | | Row selection | Opt-in checkbox selector column (header select-all + per-row checkbox) with onRowSelectionChange callback | | Actions column | Row actions (Add, View, Edit, Delete, List, or custom) with configurable icon, label, and navigation URL from meta.actions | | Builtin navigation for actions | Actions can reuse the same built-in router/dialog flow as field navigation (no app callback required) | | Performance | Memoized header, filter, row, and cell components | | Tailwind UI | Toolbar, filters, cells, and chrome styled with Tailwind utilities; optional built-in toolbar light/dark toggle | | Theme API | theme, showThemeToggle, and onThemeChange for controlled or document-driven (system) appearance; root uses data-sab-theme for debugging and CSS hooks | | Tailwind content helper | Subpath export @chainsys/sab-react-grid/tailwind-content so your build scans the package and generates all grid classes (avoids “unstyled” or wrong-mode cells after purge) | | Card styles (fields & actions) | Per-column card/frame and typography via meta.labelStyle, meta.boxStyle, meta.valueStyle; per-action buttonStyle / labelStyle on TableAction | | Frozen columns | Column header menu: Freeze Upto (pin left through selected column) and Unfreeze all columns; optional columnDef.enablePinning: false to exclude a column from freeze | | Field navigation (click & hover) | meta.fieldNavigationInfo: click activates on pointer/keyboard; hover uses a short dwell timer before firing — header glyph + cell data-sab-* attributes | | Currency columns | fieldType: 'currency' (or legacy dataType) with formatOptions.currencySymbol and formatOptions.decimals; locale-aware number grouping in DataFormatter and plain-text export via formatCellValueAsPlainText | | URL columns | fieldType: 'url': chip links for { displayValue?, urlValue }, arrays, or strings; +N overflow in a fixed portal; filters match urlValue only; CSV/Excel export emits comma-separated URLs | | Rich text columns | fieldType: 'richtext': list cell shows View Detail with sanitized HTML (RichTextFormatter / DOMPurify); optional RichtextHeaderIcon in the header; export strips tags and media for readable plain text |

Card styles (fields & actions)

Use inline React CSS objects on column meta so JSON-driven configs can style cells without custom cell renderers:

| Key | Where it applies | Purpose | |-----|------------------|---------| | labelStyle | Header and filter cells | Chrome for the title/filter row: background, borders, radius (not body “cards”) | | boxStyle | Body value cells | Card / frame: backgroundColor, borderRadius, borders, boxShadow — applied on the cell chrome | | valueStyle | Body value content | Typography / presentation: color, fontWeight, textDecoration — frame keys are stripped and should live in boxStyle |

Actions: each entry in meta.actions can set buttonStyle, labelStyle, className, iconClassName for per-button styling.

// Data column: soft card + bold value
meta: {
  dataType: 'currency',
  boxStyle: { backgroundColor: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0' },
  valueStyle: { fontWeight: 600, color: '#0f172a' },
},
// Actions: compact primary button
meta: {
  actions: [
    {
      id: 'edit',
      label: 'Edit',
      icon: 'Edit',
      buttonStyle: { backgroundColor: '#4f46e5', color: '#fff', borderRadius: 6, padding: '4px 10px' },
      labelStyle: { fontWeight: 600 },
    },
  ],
},

Frozen columns

  • Open the column header chevron (⋮) menu on any column that allows pinning.
  • Freeze Upto — pins all visible leaf columns from the left through this column (inclusive). The grid validates the request (for example you cannot freeze through the last visible column; viewport hints may appear as toasts).
  • Unfreeze all columns — clears the custom freeze and returns to normal horizontal scroll.
  • To disable freeze for a specific column (no “Freeze Upto” effect on that column), set TanStack columnDef.enablePinning: false.

Internally the grid may use sticky pinning or a split layout (separate frozen vs scroll panes) depending on state; behavior is the same from the user’s perspective: left columns stay visible while the rest scrolls horizontally.

Field navigation: click vs hover

Configure meta.fieldNavigationInfo on data columns (default DataFormatter / date cells only — custom column.cell is not wrapped). The column header shows a small glyph and tooltip (“field click” vs “field hover”). Data attributes on the cell wrapper help tests and theming: data-sab-field-interaction, data-sab-field-id, data-sab-column-id.

| triggerEvent | When navigation runs | UX notes | |----------------|------------------------|----------| | click | On click (and Enter / Space when focused) | cursor-pointer, role="button", tabIndex={0}; click does not bubble to the row | | hover | After the pointer stays over the value ~400ms (mouseenter → timer; cleared on mouseleave) | Avoids accidental navigation while moving the mouse; native title on the value can show the formatted cell string |

For both modes, handling follows type (router vs dialog) and fieldNavigationBehavior (builtin vs callback). Use onFieldNavigation only for callback; builtin dialog uses renderFieldNavigationDialog or builtinDialogPreset.

Minimal click (builtin router):

meta: {
  fieldNavigationInfo: {
    id: 'region-nav',
    triggerEvent: 'click',
    type: 'router',
    navigationUrl: (row) => `/regions/${(row as { id: string }).id}`,
  },
},

Minimal hover (callback — app handles preview):

<SabReactTable
  data={rows}
  columns={columns}
  onFieldNavigation={(payload) => {
    if (payload.triggerEvent !== 'hover') return
    // open preview / tooltip app using payload.row, payload.cellValue, payload.fieldId
  }}
/>

// column meta:
meta: {
  fieldNavigationInfo: {
    id: 'preview-units',
    triggerEvent: 'hover',
    type: 'dialog',
    fieldNavigationBehavior: 'callback',
  },
},

Installation

npm install @chainsys/sab-react-grid @tanstack/react-table @tanstack/react-virtual react react-dom

Peer dependencies

Install in your application if not already present:

  • react (^18.0.0 or ^19.0.0)
  • react-dom (^18.0.0 or ^19.0.0)
  • @tanstack/react-table (^8.0.0)
  • @tanstack/react-virtual (^3.0.0)
  • flatpickr and react-flatpickr (date/datetime/time filters)
  • tailwindcss (^3.4.0) — recommended whenever you style the app with Tailwind; marked optional in peerDependenciesMeta so non-Tailwind consumers can still install the package, but the grid’s classes require Tailwind in the host build for correct appearance

Dependencies (installed automatically)

  • date-fns – date formatting
  • exceljs – Excel (.xlsx) export (larger files; NPM vulnerabilities mitigated via package overrides)
  • flatpickr, react-flatpickr – date/datetime/time filters

CSV export uses no extra libraries (plain string/Blob).

Tailwind CSS and theming

The component library does not ship a separate CSS bundle: styling is Tailwind utility classes (bg-*, text-*, borders, dark:*, etc.). Your application must run Tailwind v3 in its build and include this package’s sources in Tailwind content, or unused classes are purged and the grid can look unstyled or stuck in light mode while the rest of the app is dark.

Subpath export: tailwind-content

The package publishes @chainsys/sab-react-grid/tailwind-content, which resolves to absolute globs for dist/**/*.{js,mjs,cjs} and src/**/*.{ts,tsx} inside the installed package. Spread it into content so paths work in monorepos and different node_modules layouts.

ESM tailwind.config.js (recommended):

import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const sabGridContent = require('@chainsys/sab-react-grid/tailwind-content')

export default {
  darkMode: 'selector', // or 'class'; align with how you set `dark` on `<html>` / `:root`
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', ...sabGridContent],
}

CommonJS tailwind.config.cjs:

const sabGridContent = require('@chainsys/sab-react-grid/tailwind-content')
module.exports = {
  darkMode: 'selector',
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', ...sabGridContent],
}

Important: If your Tailwind config sets content, it replaces any preset’s content entirely. Do not assume a shared preset alone will scan this package—you must merge ...sabGridContent (or equivalent manual globs) into your app’s content array.

Manual globs (only if you cannot use the subpath; more brittle):

content: [
  './index.html',
  './src/**/*.{js,ts,jsx,tsx}',
  './node_modules/@chainsys/sab-react-grid/dist/**/*.{js,mjs,cjs}',
  './node_modules/@chainsys/sab-react-grid/src/**/*.{ts,tsx}',
],

Install tailwindcss, postcss, and autoprefixer in the app and load a global stylesheet with @tailwind directives.

Theme behavior (theme, showThemeToggle, onThemeChange)

| Mode | How it works | |------|----------------| | Controlled | Pass theme="light" or theme="dark" from the same state you use for the rest of the app (e.g. next-themes, Zustand, or useState). The grid follows your value even if <html> differs; it applies Tailwind’s dark class on the grid root when needed. | | Uncontrolled | Omit theme (or use theme="system"). The grid starts in system mode: it observes class / data-theme on documentElement, body, and #root so it tracks <html class="dark">, data-theme="dark", etc. | | Toolbar toggle | Set showThemeToggle to show a sun/moon control. If theme is controlled, you must pass onThemeChange so toolbar clicks update your app state (and typically <html class="dark">). Otherwise the button cannot sync the grid with your app. |

Example (controlled, matches a typical app toggle):

const [dark, setDark] = useState(false)

return (
  <>
    <button type="button" onClick={() => setDark((d) => !d)}>
      Toggle theme
    </button>
    <SabReactTable
      data={data}
      columns={columns}
      theme={dark ? 'dark' : 'light'}
      showThemeToggle
      onThemeChange={(t) => setDark(t === 'dark')}
    />
  </>
)

The grid root exposes data-sab-theme="light" | "dark" for debugging and optional host CSS. Types: SabGridTheme ('light' | 'dark' | 'system'), SabGridResolvedTheme ('light' | 'dark' for callbacks).


Project structure and main files

| Path | Purpose | |------|---------| | src/index.ts | Package entry; re-exports component, formatters, export helpers, and types | | src/SabReactTable.tsx | Main grid component: table UI, toolbar, filter row, virtualization, grouping, export menu | | src/utils/tableFormatters.tsx | DataFormatter, ActionFormatter, date formatting, TableAction / ACTION_ICON_KEYS / ActionIcons | | src/utils/exportUtils.ts | exportTableToExcel (ExcelJS), exportTableToCSV (plain Blob) | | src/utils/tableColumnResizeUtils.ts | resizeColumnsByCellContent, getWidthInPixel, getWidthInPixelTitle for fit-to-content layout | | dist/ | Built output (ESM + CJS + types) | | tailwind-content.cjs | Published alongside dist for Tailwind content globs (exports["./tailwind-content"]) |

Build: npm run build (runs clean then build:esm and build:cjs). The prepublishOnly script runs the same build before publish.
Clean: npm run clean removes dist before build to avoid stale artifacts.


Quick start

import { SabReactTable } from '@chainsys/sab-react-grid'
import type { ColumnDef } from '@tanstack/react-table'

interface Person {
  id: number
  name: string
  email: string
  role: string
}

const columns: ColumnDef<Person, unknown>[] = [
  { id: 'id', accessorKey: 'id', header: 'ID' },
  { id: 'name', accessorKey: 'name', header: 'Name' },
  { id: 'email', accessorKey: 'email', header: 'Email' },
  { id: 'role', accessorKey: 'role', header: 'Role', meta: { dataType: 'text' } },
]

const data: Person[] = [
  { id: 1, name: 'Alice', email: '[email protected]', role: 'Admin' },
  { id: 2, name: 'Bob', email: '[email protected]', role: 'User' },
]

export function MyTable() {
  return (
    <SabReactTable<Person>
      data={data}
      columns={columns}
      title="Users"
      defaultPageSize={50}
      pageSizeOptions={[10, 20, 50, 100]}
      initialLayoutMode="fit-content"
      onSortedDataChange={(rows) => console.log('Filtered/sorted rows', rows)}
      onRowClick={(row) => console.log('Clicked', row)}
    />
  )
}

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | TData[] | required | Row data | | columns | ColumnDef<TData, any>[] | required | TanStack Table column definitions | | title | string | 'Table_List' | Table title in the toolbar | | onSortedDataChange | (rows: TData[]) => void | — | Called with filtered/sorted rows (pre-pagination) | | defaultPageSize | number | 50 | Initial page size | | pageSizeOptions | number[] | [10, 20, 50, 100, 500, 1000] | Page size dropdown options | | groupingLabels | Record<string, string> | {} | Display labels for grouping columns | | onRowClick | (row: TData) => void | — | Row click handler | | onActionClick | (actionId, row, action) => void | — | Handler for action buttons; use action.navigate or action.id to route | | onFieldNavigationOverride | (args) => boolean \| void | — | Optional override hook; return true to stop built-in navigation when column meta.fieldNavigationInfo is set | | onFieldNavigation | (payload) => void | — | callback columns only — navigate or app-owned dialog. Not called for builtin + dialog (use renderFieldNavigationDialog) | | routerNavigate | SabRouterNavigateFn | — | Optional override for built-in router navigation; omit when the table is under react-router’s Router (wired automatically) | | routerLocation | { pathname; search; hash } | — | Router location snapshot (only needed when you pass a custom routerNavigate and use preserveLocation) | | renderFieldNavigationDialog | (args) => ReactNode | — | Builtin type: 'dialog' body inside the grid modal; onFieldNavigation is not used for this path | | initialLayoutMode | 'fit-default' \| 'fit-window' \| 'fit-content' | 'fit-content' | Initial layout mode | | theme | SabGridTheme | — | 'light', 'dark', or 'system' (follows document dark / data-theme). Omit for uncontrolled system mode. | | showThemeToggle | boolean | false | Toolbar sun/moon control; pair with onThemeChange when theme is controlled. | | onThemeChange | (theme: SabGridResolvedTheme) => void | — | Fires when the user changes theme via the toolbar; update app state and document class when using controlled theme. | | fillParent | boolean | false | If true, grid uses flex-1 min-h-0 so it fills a flex parent and scrolls within available space | | className | string | — | Extra classes on the root grid card | | getRowId | (row, index) => string | — | Stable row id for selection when rows lack a unique id field. | | onRowSelectionChange | (selection, selectedRows) => void | — | TanStack RowSelectionState and selected data rows when row selection is used. | | toolbarIcons | SabGridToolbarIcons | — | Replace default toolbar glyphs (title badge, command menu, filter toggle, pagination prev/next) |


Column meta

Use the meta property on column definitions to control formatting and filtering.

Column kind resolution: the grid uses resolveColumnFieldKind(meta) everywhere it used to read dataType alone: meta.fieldType first, then meta.dataType if fieldType is unset. Hosts can migrate configs gradually; fieldType overrides dataType when both are present.

| Meta key | Type | Description | |----------|------|-------------| | fieldType | 'text' \| 'number' \| 'decimal' \| 'currency' \| 'email' \| 'boolean' \| 'date' \| 'datetime' \| 'time' \| 'percentage' \| 'icon-boolean' \| 'richtext' \| 'url' | Preferred column kind for formatters, filters, alignment, and export | | dataType | Same union as fieldType | Legacy alias of fieldType. If both are set, fieldType wins (resolveColumnFieldKind in source) | | labelStyle | React.CSSProperties | Header / filter cell chrome (background, borders) — not body card frame | | boxStyle | React.CSSProperties | Body cell card / frame (background, radius, borders, shadow) | | valueStyle | React.CSSProperties | Body cell content (color, font weight, etc.); frame keys belong in boxStyle | | formatOptions | { decimals?, currencySymbol?, dateFormat?, datetimeFormat?, timeFormat?, maxPrimaryChips? } | Number, currency, date, and url formatting (url / fieldType: 'url': inline chips before +N) | | filterType | 'text' \| 'select' \| 'multiselect' \| 'checkbox' \| 'boolean' \| 'date' \| 'number' \| 'dropdown' \| 'radio' | Filter UI type | | filterOptions | { label: string; value: any }[] | Options for select, multiselect, dropdown, and radio filters | | actions | TableAction[] | Row action buttons (Add, View, Edit, Delete, List, or custom) | | invokeActionClickCallback | boolean | Actions only — false opts into package navigation when an action includes navigationUrl/navigate (unless overridden per action) | | checkboxSelector | boolean | When true, column becomes the row selection checkbox selector (header select-all + per-row checkbox) | | fieldNavigationInfo | TableFieldNavigationInfo | Clickable/hoverable cell: built-in navigation (router URL update) or dialog (see below) | | aggregationFn | AggregationFnOption<any> | Enables group footer aggregates for this column (under expanded groups) | | groupFooter | SabFooterPresentation | Presentation for group footer cells (label + styling such as Avg: / Total:) | | footerAggregationFn | AggregationFnOption<any> | Enables list footer (<tfoot>) aggregates for this column | | footer | SabFooterPresentation | Presentation for list footer cells | | footerScope | 'filtered' \| 'page' | List footer aggregate scope: filtered totals across all filtered rows, page totals current page only |

Filter placeholders

  • Text, number, and dropdown/select filters: search placeholder (🔎︎).
  • Date, datetime, and time filters: calendar icon (SVG) when empty, plus optional text placeholder; Flatpickr for picking date/time.
  • Rich text (fieldType: 'richtext' or legacy dataType: 'richtext'): the filter row does not render a filter control for that column (HTML is not meant to be filtered as raw markup).
  • Operand persistence (text / number filters): when the user clears the filter value (empty input or clear control), all rows match again (advancedFilterFn treats empty compare value as “no filter”), but the grid keeps { value: '', operand: <user’s choice> } in column filter state so the operand selector does not snap back to the column default until the user picks a different operand.

Currency (fieldType: 'currency')

Use meta: { fieldType: 'currency' } (or legacy dataType: 'currency') for monetary values. This is the grid’s currency formatter (analogous to a template “currency pipe”): it formats numbers for display and export, it does not perform FX conversion.

formatOptions:

| Option | Default | Description | |--------|---------|-------------| | currencySymbol | '$' | Prefix shown before the numeric part (UI and plain-text export). | | decimals | 2 | Minimum and maximum fractional digits (toLocaleString). |

Cell rendering: DataFormatter shows the symbol and a locale-grouped amount (bold weight in the default theme). Null, undefined, or non-numeric values render as symbol + zero with the configured decimal count.

Export / copy: formatCellValueAsPlainText uses the same symbol and decimals so CSV/Excel and clipboard text match the intended monetary presentation.

Aggregations: Currency columns work with meta.aggregationFn / meta.footerAggregationFn like other numeric kinds (for example sum) when the underlying cell values are numbers.

Rich text (fieldType: 'richtext')

  • Cell UI: shows a View Detail control; body uses sanitized HTML (RichTextFormatter).
  • Header: the package exports RichtextHeaderIcon so host apps or JSON-driven configs can show a consistent RICHTEXT marker beside the title (optional glyph override).
  • Filter row: no filter panel for this column (see above).
  • Export / plain text: formatCellValueAsPlainText strips tags, removes media elements where possible, and collapses whitespace so spreadsheets get readable text (aligned with what users read from the detail view, without HTML noise).
  • Grouping: URL and rich-text kinds cannot be used as group keys; the grid sets enableGrouping: false for those columns so drag-to-group stays meaningful.

URL field type (fieldType: 'url')

Use meta: { fieldType: 'url' } (or legacy dataType: 'url') so the grid treats the column as structured links, not generic text.

Cell value shapes (normalized in code):

| Shape | Behavior | |-------|----------| | Single object | { urlValue } or { displayValue?, urlValue } — one chip. If displayValue is omitted or empty, the chip label is the URL. | | Array | Mix of objects { displayValue?, urlValue }, { urlValue }, or plain strings — each becomes one link. | | Plain string | Treated as URL; optional https:// is prepended for values like www.example.com when there is no scheme. |

Display vs hover:

  • Chip shows display when provided; otherwise the URL text.
  • Hover (title on chip and anchor): if a non-empty display exists and differs from the URL → display:url (no space after the colon). If only a URL is shown → tooltip is only the URL.

Multiple URLs in one cell:

  • Up to formatOptions.maxPrimaryChips chips inline (default capped at 2 in the formatter for list UX). Additional entries appear under a +N control; the overflow list opens in a fixed portal so table overflow does not clip it.

Filtering:

  • The advanced filter compares against urlValue only (objects/arrays are flattened to URL strings), so users can type https://… or a path fragment without matching display labels.

Export:

  • CSV/Excel export uses formatCellValueAsPlainText: for url, exported text is comma-separated urlValue list only (no JSON, no display labels).

Grouping: fieldType: 'url' is not a valid group key; enableGrouping is forced off for that column (same as rich text).


Actions (from config / JSON)

Actions are fully driven by column meta.actions. Each action can specify:

| Field | Type | Description | |-------|------|-------------| | id | string | Unique key passed to onActionClick | | label | string | Button label (optional for icon-only buttons) | | icon | 'View' \| 'Edit' \| 'Delete' \| 'Add' \| 'List' or lowercase in JSON, or ReactNode | Built-in icon or custom node | | navigate | string \| (row) => string | URL or function; use in onActionClick for routing (e.g. router.push(...)) | | navigationUrl | string \| (row) => string | Preferred alias of navigate (used by built-in action navigation) | | actionNavigationBehavior | 'builtin' \| 'callback' | callback (default): your onActionClick / action.onClick runs. builtin: grid performs built-in router/dialog navigation (same as field navigation). | | navigationType | 'router' \| 'dialog' | Built-in action navigation type (defaults to router) | | preserveLocation | boolean | Built-in router only — keep current URL and store context in location.state (see Field navigation) | | builtinDialogPreset | 'embedCurrentRoute' \| 'fieldContextCard' | Built-in dialog only — package-provided dialog bodies | | onClick | (row) => void | Per-action handler (if not using table onActionClick) | | show | (row) => boolean | Hide button for specific rows | | className | string | Extra CSS class for the button | | buttonStyle | React.CSSProperties | Inline styles on the action <button> | | labelStyle | React.CSSProperties | Inline styles on the label <span> (icon / label modes) |

Built-in icon keys: View, Edit, Delete, Add, List (exported as ACTION_ICON_KEYS). In JSON you can use lowercase ("view", "add", etc.).

Example:

meta: {
  actions: [
    { id: 'add', label: 'Add', icon: 'Add', navigate: '/create' },
    { id: 'view', label: 'View', icon: 'View', navigate: (row) => `/item/${row.id}` },
    { id: 'edit', label: 'Edit', icon: 'Edit', navigate: (row) => `/edit/${row.id}` },
    { id: 'list', label: 'List', icon: 'List', navigate: '/list' },
  ],
}

react-router-dom (built-in navigation)

When you use meta.invokeActionClickCallback: false (package resolves action.navigate) or built-in field navigation with type: 'router', the grid calls react-router’s navigate() for you.

Default: render SabReactTable inside your app’s <Router> / <Routes> tree (add react-router-dom; it is an optional peer). The table detects the router context and wires navigation internally — no routerNavigate prop is required.

Override: pass routerNavigate yourself if you use a custom history, tests, or a table instance that is not under a Router:

import { useNavigate } from 'react-router-dom'

function MyGrid() {
  const navigate = useNavigate()
  return (
    <SabReactTable
      data={rows}
      columns={columns}
      routerNavigate={(to, o) => navigate(to, { replace: o?.replace, state: o?.state })}
    />
  )
}

http(s):// URLs still use a full document navigation (location.assign). Dialog / popup modes are unchanged.

Linked package / monorepo (Vite): if the table is already under BrowserRouter but built-in navigation still warns, the bundler may be resolving two copies of react-router-dom, so the grid’s hooks do not see your router context. Add resolve.dedupe for react-router and react-router-dom (see sab-grid-demo vite.config.ts). Avoid aliasing react-router to a single file — that breaks subpath imports such as react-router/dom.

Optional helpers: getActionClickChannel, type SabInteractionChannel — see column meta.invokeActionClickCallback.


Field navigation (click / hover)

Use column meta.fieldNavigationInfo to mark data cells (default formatter or date cells) as navigable. This is separate from meta.actions. Custom column.cell renderers are not wrapped; action columns take precedence.

Click vs hover (timing, keyboard, data-sab-* attributes, and minimal samples) is documented under Field navigation: click vs hover (see the Features section above).

Builtin vs callback: fieldNavigationBehavior controls whether the grid performs navigation (builtin, default) or calls your app (callback) via onFieldNavigation.

| Field | Type | Description | |-------|------|-------------| | id | string | Stable identifier (telemetry, tests, data-sab-field-id) | | triggerEvent | 'click' \| 'hover' | click: immediate on click + Enter/Space when focused. hover: ~400ms dwell after mouseenter (cleared on mouseleave) before firing | | type | 'router' \| 'dialog' | router: in-app URL (automatic navigate() when under Router). dialog: callbackonFieldNavigation; builtin → grid modal + renderFieldNavigationDialog (not onFieldNavigation) | | navigationUrl | string \| (row) => string | In-app path for router; URL hint for dialog/iframe; normalized for router when needed | | navigate | string \| (row) => string | Deprecated alias of navigationUrl | | fieldNavigationBehavior | 'builtin' \| 'callback' | builtin (default): grid navigates or opens dialog. callback: grid calls onFieldNavigation only | | preserveLocation | boolean | Builtin router only — keep current URL and pass context via location.state only | | builtinDialogPreset | 'embedCurrentRoute' \| 'fieldContextCard' | Builtin dialog only — package-provided dialog bodies | | show | (row) => boolean | Optional row filter |

Builtin router context: The grid passes SAB_FIELD_NAV_CONTEXT_KEYS inside location.state[SAB_FIELD_NAV_ROUTER_STATE_KEY] (not in the URL query string). Read it on the destination route with useLocation().

UI: Interaction hint on the column header. Cells expose data-sab-field-interaction, data-sab-field-id, data-sab-column-id.

Router wiring: Usually omitted — the table uses react-router’s navigate() when rendered under a Router. Pass routerNavigate only to override.

// Builtin router: path only; context in location.state (see SAB_FIELD_NAV_ROUTER_STATE_KEY)
meta: {
  fieldNavigationInfo: {
    id: 'open-region',
    triggerEvent: 'click',
    type: 'router',
    navigationUrl: () => '/regions/detail',
    // preserveLocation: true, // keep URL unchanged; context flows via location.state
  },
},
// Callback dialog: app modal via `onFieldNavigation` (recommended for dialogs)
meta: {
  fieldNavigationInfo: {
    id: 'units-preview',
    triggerEvent: 'hover',
    type: 'dialog',
    fieldNavigationBehavior: 'callback',
  },
},
// Full app control: no grid navigation
meta: {
  fieldNavigationInfo: {
    id: 'custom',
    triggerEvent: 'click',
    type: 'router',
    fieldNavigationBehavior: 'callback',
    navigationUrl: (row) => `/items/${(row as { id: string }).id}`,
  },
},

Aggregations (group footer + list footer)

Aggregations are opt-in per column and rendered as:

  • Group footer rows (under expanded groups): enable with column meta.aggregationFn (or TanStack column.aggregationFn), and optionally style with meta.groupFooter.
  • List footer row (<tfoot>): enable with column meta.footerAggregationFn, optionally style with meta.footer, and control scope with meta.footerScope ('filtered' vs 'page').

Example:

import type { ColumnDef } from '@tanstack/react-table'

type Row = { id: string; amount: number; region: string }

const columns: ColumnDef<Row, unknown>[] = [
  { id: 'region', accessorKey: 'region', header: 'Region' },
  {
    id: 'amount',
    accessorKey: 'amount',
    header: 'Amount',
    meta: {
      dataType: 'currency',
      aggregationFn: 'sum',
      groupFooter: { label: 'Total:', labelClassName: 'font-semibold' },
      footerAggregationFn: 'sum',
      footerScope: 'filtered',
      footer: { label: 'Total:', labelClassName: 'font-semibold' },
    },
  },
]

Row selection (checkbox selector column)

Row selection is enabled when you add a checkbox selector column:

  • Preferred: set column meta.checkboxSelector: true
  • Legacy: a column id ending in '_checkbox_selector' is treated as a selector

When enabled, the grid renders a header select-all checkbox and per-row checkboxes, and calls onRowSelectionChange(selection, selectedRows).


Example: filters and formatting

const columns: ColumnDef<Person, unknown>[] = [
  { id: 'id', accessorKey: 'id', header: 'ID', meta: { dataType: 'number' } },
  { id: 'name', accessorKey: 'name', header: 'Name', meta: { dataType: 'text' } },
  { id: 'email', accessorKey: 'email', header: 'Email', meta: { dataType: 'email' } },
  {
    id: 'role',
    accessorKey: 'role',
    header: 'Role',
    meta: {
      dataType: 'text',
      filterType: 'select',
      filterOptions: [
        { label: 'Admin', value: 'Admin' },
        { label: 'User', value: 'User' },
      ],
    },
  },
]

Export

  • Excel: exportTableToExcel(data, filename?, tableTitle?) – uses ExcelJS; suitable for larger files. NPM audit issues from transitive deps are addressed via overrides in package.json (minimatch).
  • CSV: exportTableToCSV(data, filename?) – plain string/Blob; no ExcelJS.

Both are available from the toolbar Data Export menu and as standalone imports.


Exports

| Export | Description | |--------|-------------| | SabReactTable | Main grid component | | SabReactTableProps, LayoutMode, SabGridTheme, SabGridResolvedTheme, OnActionClickHandler, OnFieldNavigationOverrideHandler | Component, layout, and theme types | | RichtextHeaderIcon, RichtextHeaderIconProps | Optional column header glyph for fieldType: 'richtext' | | DataFormatter, ActionFormatter | Default cell and action formatters | | formatCellValueAsPlainText, normalizeRichtextHtmlValue | Export / clipboard plain text; HTML normalization for rich text | | RichTextFormatter, RichTextFormatterProps | Sanitized HTML body used inside the rich-text detail view | | formatDateValue, renderDateCell, isIsoDateLike | Date formatting helpers | | ActionIcons, ACTION_ICON_KEYS, TableAction, ActionConfig, ActionIconKey | Action types and built-in icons | | FieldNavigationCellWrapper, resolveFieldNavigate, buildFieldNavigationPayload, buildBuiltinRouterNavigateOptions, buildInternalFieldNavigationContext, isFieldNavigationMetaPlausible, normalizeNavigationPath, SAB_FIELD_NAV_CONTEXT_KEYS, SAB_FIELD_NAV_ROUTER_STATE_KEY, TableFieldNavigationInfo, FieldNavigationType, FieldNavigationTriggerEvent, FieldNavigationPayload | Field navigation config and helpers | | FieldNavigationDialogRenderArgs, SabRouterNavigateFn | Router/dialog integration types | | exportTableToExcel, exportTableToCSV | Standalone export helpers | | resizeColumnsByCellContent, getWidthInPixel, getWidthInPixelTitle | Column resize utilities | | ColumnResizeDef, ResizeColumnsByCellContentParams | Resize utility types | | DateFormatOptions | Date format options type | | ColumnDef, RowSelectionState | Re-exported from @tanstack/react-table |

Subpaths: @chainsys/sab-react-grid/tailwind-content — CommonJS module exporting a string[] of glob paths for Tailwind content (see Tailwind CSS and theming).


Technical documentation

The reference below is injected from TechnicalDocumentation.md so it appears on npm with the same Markdown rendering as the rest of this readme.

Table of Contents

  1. Overview
  2. Repository Structure
  3. Architecture
  4. SabReactTable — Main Component
  5. Column Meta — Extended Type System
  6. URL Field Type — Custom Formatter
  7. RichText Field Type — Custom Formatter
  8. Field Navigation
  9. Action Buttons
  10. Filtering
  11. Aggregations
  12. Export (Excel & CSV)
  13. Column Resize Utilities
  14. Theming
  15. Public API — Full Exports Index
  16. Usage Examples
  17. Security
  18. Dependencies
  19. Changelog Summary

1. Overview

@chainsys/sab-react-grid is an enterprise-grade, TanStack Table–powered React data-grid library developed by Chainsys. It ships as a dual-format (ESM + CommonJS) npm package with full TypeScript declarations and is designed to be dropped into any React 18/19 application that uses Tailwind CSS.

Package identity:

| Attribute | Value | |---|---| | NPM scope & name | @chainsys/sab-react-grid | | Current version | 2.0.2 | | License | MIT | | Node requirement | >=16 | | React peer | ^18.0.0 \|\| ^19.0.0 | | TanStack Table peer | 8.21.3 | | TanStack Virtual peer | 3.13.23 | | Tailwind CSS peer | ^4.2.2 (optional) |

Key capabilities at a glance: row virtualisation, multi-column sorting, per-column filtering (text/number/date/select/boolean), row grouping with aggregations, column freeze/split-freeze, column drag-reorder, column resize, Excel (.xlsx) + CSV export, light/dark theme, field navigation (click/hover → router or dialog), URL chip cells, RichText cells, and action buttons.


2. Repository Structure

The project root contains the following key files and directories:

| Path | Purpose | |---|---| | src/ | All TypeScript source files (compiled to dist/) | | src/SabReactTable.tsx | Main component — SabReactTable + all internal UI | | src/index.ts | Public package entry — re-exports all public API symbols | | src/SabBuiltinFieldDialogPresetBodies.tsx | Preset dialog bodies (embedCurrentRoute, fieldContextCard) | | src/components/RichtextHeaderIcon.tsx | SVG header badge for richtext columns | | src/components/grid/NormalGrid.tsx | Alias export: NormalGrid = SabReactTable | | src/components/grid/ColumnVisibilityMenu.tsx | Column show/hide dropdown | | src/formatters/RichTextFormatter.tsx | Standalone richtext renderer (DOMPurify + CSS) | | src/utils/tableFormatters.tsx | DataFormatter, ActionFormatter, URL chip, date renderers, types | | src/utils/columnFieldKind.ts | resolveColumnFieldKindfieldType / dataType resolution | | src/utils/sabAggregation.ts | Group + list footer aggregation engine | | src/utils/sabFilterNormalize.ts | Filter operand normalisation helpers | | src/utils/exportUtils.ts | Excel (ExcelJS) + CSV export utilities | | src/utils/fieldNavigationResolve.ts | Payload builders for field / action navigation | | src/utils/richtextDetailOverlay.tsx | Portal modal for richtext detail view | | src/utils/tableColumnResizeUtils.ts | Fit-to-content column width calculation | | dist/esm/ | ESM build with .d.ts declarations | | dist/cjs/ | CommonJS build | | tailwind-content.cjs | Tailwind content globs for host tailwind.config | | package.json | Package manifest, scripts, peer deps | | tsconfig.esm.json / tsconfig.cjs.json | TypeScript build configs |


3. Architecture

The library is structured in three conceptual layers that compose to produce the final grid.

3.1 Component Layer

SabReactTable is the single public React component. Internally it is split into a thin router-aware shell (SabReactTableSabReactTableAutoRouterNavigate) and the real implementation in SabReactTableCore. This avoids breaking the Rules of Hooks when useNavigate() / useLocation() are conditionally unavailable (non-Router context).

Internal sub-components (not exported individually):

  • MemoizedHeaderCell — renders one <th> with sort, filter menu, freeze-up-to, drag handles, richtext badge, field-nav glyph.
  • MemoizedFilterCell — renders the per-column filter input row (text, number, date/flatpickr, select, multiselect, boolean, radio, dropdown).
  • ColumnVisibilityMenu — show/hide column panel (exported separately for embedded use).
  • RichtextDetailOverlayProvider + modal — portal dialog that renders sanitised HTML for richtext cells.
  • SabBuiltinFieldDialogEmbedCurrentRoute / SabBuiltinFieldDialogFieldContextCard — preset dialog bodies.

3.2 Utility Layer

Pure functions and hooks with no React component state — all exported from the public index.

3.3 Build System

TypeScript is compiled twice: once for ESM (tsconfig.esm.json, moduleResolution bundler, target esnext) and once for CommonJS (tsconfig.cjs.json). Both emit declaration files (.d.ts + source maps). Tailwind scanning is enabled via the tailwind-content.cjs subpath export so host apps include this package's dist/ in their Tailwind content array.

3.4 Data Flow

Host app passes data[] + columns[]
  → TanStack useReactTable()
  → sorted/filtered/grouped row model
  → @tanstack/react-virtual virtualiser renders only visible rows
  → each row cell calls DataFormatter (or custom cell)
  → on export, formatCellValueAsPlainText strips HTML/objects to plain text

4. SabReactTable — Main Component

File: src/SabReactTable.tsx

The primary export. Accepts a generic TData type for strong typing of row data. The component manages all TanStack table state internally (sorting, filtering, grouping, pagination, column visibility, column order, column sizing, row selection, expansion) and exposes callbacks for events the host needs to act on.

4.1 Props Reference

| Prop | Type | Default | Description | |---|---|---|---| | data | TData[] | (required) | Row data array. | | columns | ColumnDef<TData>[] | (required) | TanStack column definitions. | | title | string | 'Table_List' | Toolbar title and export filename prefix. | | onSortedDataChange | (rows: TData[]) => void | undefined | Fired when visible/sorted rows change. | | defaultPageSize | number | 50 | Initial page size. | | pageSizeOptions | number[] | [10,20,50,100,500,1000] | Dropdown options for items-per-page. | | groupingLabels | Record<string, string> | {} | Human labels for group-zone chips. | | onRowClick | (row: TData) => void | undefined | Row click / keyboard Enter handler. | | onActionClick | OnActionClickHandler<TData> | undefined | Action button click handler. | | onFieldNavigationOverride | OnFieldNavigationOverrideHandler | undefined | Return true to suppress default navigation. | | onFieldNavigation | OnFieldNavigationHandler | undefined | Callback-mode field navigation. | | renderFieldNavigationDialog | (args) => ReactNode | undefined | Renders body inside the grid dialog shell. | | initialLayoutMode | LayoutMode | 'fit-content' | Initial column width strategy. | | showThemeToggle | boolean | false | Shows sun/moon button in toolbar. | | theme | 'light' \| 'dark' \| 'system' | undefined | Controlled theme (from host). | | onThemeChange | (theme) => void | undefined | Called when built-in toggle is clicked. | | fillParent | boolean | false | flex-1 min-h-0 so grid fills a flex parent. | | className | string | undefined | Extra CSS classes on root container. | | getRowId | (row, idx) => string | undefined | Stable row id for selection. | | onRowSelectionChange | (state, rows) => void | undefined | Fired when selection changes. | | toolbarIcons | SabGridToolbarIcons | undefined | Override SVG glyphs in toolbar. | | routerNavigate | SabRouterNavigateFn | undefined | Custom SPA navigate function. | | routerLocation | {pathname, search, hash} | undefined | Current location for preserveLocation. | | embeddedDataGridOnly | boolean | false | Render only the grid table, no outer chrome. |

4.2 Layout Modes

| Mode | Behaviour | |---|---| | fit-content | Columns sized to content (default). Uses resizeColumnsByCellContent(). | | fit-window | Columns share the available container width proportionally. | | fit-default | TanStack default sizing — no automatic column width logic. |

4.3 Freeze / Split-Freeze

Columns can be frozen left via the column header context menu ("Freeze up to…"). Freeze strategies exist:

  • split: Renders two separate table panes side-by-side — a frozen left pane with no scrollbar and a scrolling right pane with a visible scrollbar rail above the footer. Column widths are locked to pixel values; % widths are not allowed in split mode.

Returns toast messages FREEZE_TOAST_ALL_VISIBLE, FREEZE_TOAST_SCREEN_WIDTH, or FREEZE_TOAST_RESIZE when the requested freeze configuration would produce a poor UX.

4.4 Row Virtualisation

All rows pass through @tanstack/react-virtual's useVirtualizer. Three row heights are defined to keep scroll-offset calculations accurate when grouping is enabled:

| Row Kind | Height (px) | Constant | |---|---|---| | Data row (leaf) | 40 | SAB_VIRTUAL_ROW_HEIGHT_PX | | Group header row | 50 | SAB_GROUP_HEADER_ROW_HEIGHT_PX | | Group footer row | 36 | SAB_GROUP_FOOTER_ROW_HEIGHT_PX |


5. Column Meta — Extended Type System

sab-react-grid extends TanStack's ColumnMeta interface (module augmentation in SabReactTable.tsx) to carry all Chainsys-specific column configuration. Column definitions pass meta as a plain object; no runtime type checking is performed — TypeScript enforces correctness at compile time.

5.1 Field Type (fieldType / dataType)

fieldType is the canonical field kind. dataType is the legacy alias; when both are set, fieldType wins. The resolution is performed by resolveColumnFieldKind() in columnFieldKind.ts.

| fieldType value | Filter operands | |---|---| | text | String (contains/equals/…) | | number | Number (=/</>/ …) | | decimal | Number | | currency | Number | | percentage | Number | | email | String | | boolean | Boolean toggle | | date | Number (date ms) | | datetime | Number (date ms) | | time | Number (date ms) | | url | String (urlValue) | | richtext | String (inner text) |

5.2 Styling Meta

| Meta key | Applies to | Purpose | |---|---|---| | labelStyle | Header + filter <th> | Typography + header chrome (borders, bg). | | boxStyle | Body <td> frame | Card chrome: backgroundColor, borderRadius, shadow. | | valueStyle | Body cell inner text | Typography only: color, fontWeight, fontFamily. Box keys are stripped automatically via valueStyleInnerPresentation(). | | headerClassName | Header <th> | Extra Tailwind / CSS classes. | | cellClassName | Body <td> | Extra Tailwind / CSS classes. |

5.3 Filter Meta

| Meta key | Type | Description | |---|---|---| | filterType | string | Override: 'text'|'number'|'date'|'datetime'|'time'|'select'|'multiselect'|'checkbox'|'boolean'|'dropdown'|'radio' | | filterOptions | {label, value}[] | Options for select/multiselect/radio/dropdown filters. |

5.4 Aggregation Meta

| Meta key | Purpose | |---|---| | aggregationFn | Group footer aggregation function (sum/mean/count/…). | | groupFooter | SabFooterPresentation: label, variant (total/average/plain), label/value className. | | footerAggregationFn | List <tfoot> aggregation function. | | footer | SabFooterPresentation for list footer. | | footerScope | 'filtered' (default) | 'page' — scope of list footer aggregation. |

5.5 Action Meta

| Meta key | Purpose | |---|---| | actions | TableAction[] — array of action button definitions. | | actionDisplayMode | 'icon' | 'iconLabel' | 'label' — default display for all actions in this column. | | actionGroupClassName | Tailwind classes on the actions wrapper <div>. | | invokeActionClickCallback | false = package handles navigation; true/omitted = call onActionClick. | | fieldNavigationInfo | TableFieldNavigationInfo — makes cell value clickable/hoverable. | | checkboxSelector | boolean — column renders row-selection checkboxes. |


6. URL Field Type — Custom Formatter

File: src/utils/tableFormatters.tsxUrlChipCell, normalizeUrlItems, normalizeUrlCandidateToHref

The URL formatter renders structured hyperlinks as pill-shaped chips directly inside grid cells. It was introduced in v2.0.2 as one of the Phase-3 custom formatter additions.

6.1 Value Format

The formatter accepts multiple input shapes via normalizeUrlItems():

| Input shape | Example | Result | |---|---|---| | Plain string | "https://example.com" | Single chip. display = url. | | Bare domain | "example.com" | https:// prepended. | | Object {urlValue, displayValue} | {urlValue:"…", displayValue:"Docs"} | Single chip with custom label. | | Object {url, display} | {url:"…", display:"Home"} | Single chip with custom label. | | Object {href, label} | {href:"…", label:"Home"} | Resolved via fallback key order. | | Array of any above | [{…},{…}] | Multiple chips; overflow to +N menu. |

6.2 Chip Layout Algorithm

A hidden measurement div (position:fixed, far off-screen) renders all chips to obtain their real pixel widths. A ResizeObserver on the cell root re-runs the layout whenever the column width changes. Visible chips are rendered from the front; overflow chips become a "+N" popover button positioned via a document.body portal (viewport-fixed so it never clips).

6.3 Export Behaviour

formatCellValueAsPlainText for fieldType "url" outputs only the URL values (urlValue), comma-separated. Display labels are intentionally excluded to keep spreadsheet cells navigable.

6.4 Grouping Restriction

URL columns are passed through applySabColumnGroupingFieldRestrictions() at table initialisation, which forces enableGrouping: false. Attempting to drag a URL column to the grouping zone shows the message: "URL and Rich text fields are not allowed for grouping."


7. RichText Field Type — Custom Formatter

Files: src/utils/tableFormatters.tsx, src/utils/richtextDetailOverlay.tsx, src/formatters/RichTextFormatter.tsx, src/components/RichtextHeaderIcon.tsx

Columns with fieldType: "richtext" store HTML as their cell value. Because full HTML cannot be displayed inside a 40 px grid row, the formatter uses a two-stage presentation model.

7.1 In-Cell Presentation

Each richtext cell renders a small "View Detail" button (RichtextDetailButtonCell). The button is disabled when the cell value is empty. Clicking it calls overlay.show() from the RichtextDetailOverlayContext, which opens the portal modal.

7.2 Detail Overlay Modal

The RichtextDetailOverlayProvider wraps SabReactTable and manages a single modal state. The modal uses createPortal(…, document.body) to escape overflow clipping. HTML content is sanitised with DOMPurify before rendering. Forbidden tags: script, style, iframe, object, embed, link, meta.

CSS is injected once into document.head (style id sab-grid-richtext-detail-style):

  • Max-width 100% on all elements prevents overflow.
  • Images are max-width:100%, height:auto.
  • Tables get full border-collapse styling.
  • Paragraphs, lists, headings, bold/italic are all normalised.

7.3 normalizeRichtextHtmlValue()

Handles any cell value shape — plain string, or objects with keys html, content, body, value, richText, rich_text, innerHTML. Returns an empty string when the value is null/undefined.

normalizeRichtextHtmlValue({ html: "<p>Hello</p>" }) // → "<p>Hello</p>"
normalizeRichtextHtmlValue("<b>Bold</b>")           // → "<b>Bold</b>"

7.4 RichtextHeaderIcon Component

A memoised column header badge that renders a serif "T" SVG icon in blue (#2563eb) on a slate-100 background. The grid automatically inserts this icon in headers for richtext columns. The glyph can be overridden via toolbarIcons.richtextHeaderGlyph (host SVG/image).

import { RichtextHeaderIcon } from "@chainsys/sab-react-grid"

7.5 Export Behaviour

formatCellValueAsPlainText for fieldType "richtext" uses DOMParser to extract inner text, removes media elements (img/video/svg/canvas/iframe/…), then collapses whitespace. Falls back to regex tag stripping in non-browser environments.

7.6 Grouping Restriction

Like URL columns, richtext columns are disallowed from grouping via applySabColumnGroupingFieldRestrictions().


8. Field Navigation

Any data column can be configured to navigate on click or hover by setting meta.fieldNavigationInfo. Two navigation targets exist: in-app SPA router and a modal dialog.

8.1 TableFieldNavigationInfo Interface

| Property | Type | Description | |---|---|---| | id | string | Stable identifier (for telemetry / testing). | | triggerEvent | 'click' \| 'hover' | How the navigation is triggered. | | type | 'router' \| 'dialog' | Navigation target. | | fieldNavigationBehavior | 'builtin' \| 'callback' | builtin: grid handles it; callback: grid fires onFieldNavigation. | | navigationUrl | string \| (row) => string | In-app path for router, or URL hint for dialog. | | preserveLocation | boolean | Router only. Keeps current URL; passes context in location.state. | | builtinDialogPreset | 'embedCurrentRoute' \| 'fieldContextCard' | Builtin preset dialog body. | | show | (row) => boolean | Hide navigation for specific rows. |

8.2 Hover Intent

Hover navigation fires after a 400 ms intent delay (SAB_FIELD_HOVER_INTENT_MS) to avoid accidental triggers during mouse movement. The timer is cleared on mouse leave.

8.3 Header Glyphs

FieldNavigationHeaderGlyph renders a small indicator SVG next to the column header label:

  • click + dialog → stroked frame + solid tile overlay.
  • click + router → two overlapping outlined squares.
  • hover → balloon on a string icon.

8.4 FieldNavigationPayload

All navigation events produce a FieldNavigationPayload with: triggerEvent, fieldId, columnId, row, cellValue, type, navigationUrl, fieldNavigationBehavior, builtinDialogPreset, preserveLocation.

8.5 Router Navigation Context

When type: "router" and fieldNavigationBehavior: "builtin", buildBuiltinRouterNavigateOptions() builds the navigate() call: the target path goes in the first argument, and field context (columnId, fieldId, cellValue, rowId) is serialised into location.state[sabFieldNavigation].

Context keys:

SAB_FIELD_NAV_CONTEXT_KEYS = {
  columnId:  "sabColumnId",
  fieldId:   "sabFieldId",
  cellValue: "sabCellValue",
  rowId:     "sabRowId"
}

9. Action Buttons

Action columns render buttons inside cells. The TableAction interface describes each button.

9.1 TableAction Properties

| Property | Type | Description | |---|---|---| | id | string | Unique key (used in onActionClick). | | label | string | Button label text (optional when icon-only). | | icon | ActionIconKey \| ReactNode | Built-in: 'View'|'Edit'|'Delete'|'Add'|'List'. Or custom ReactNode. | | display | 'icon'\|'iconLabel'\|'label' | Overrides column meta.actionDisplayMode for this button. | | imageUrl | string | Image URL shown instead of icon SVG. | | actionNavigationBehavior | 'builtin'\|'callback' | builtin: grid navigates; callback: onActionClick fires. | | navigationUrl | string \| (row)=>string | In-app path for builtin navigation. | | navigationType | 'router'\|'dialog' | Navigation target (builtin only). | | preserveLocation | boolean | Same-URL navigation with context in state. | | builtinDialogPreset | string | Preset dialog body for builtin dialog actions. | | className | string | Tailwind classes on the <button>. | | buttonStyle | CSSProperties | Inline styles on the <button>. | | labelStyle | CSSProperties | Inline styles on the label <span>. | | show | (row)=>boolean | Hide button for specific rows. | | onClick | (row)=>void | Per-action click handler (fallback from onActionClick). |

9.2 Built-in Action Icons

Five built-in SVG icons (14×14 px, currentColor stroke): View (eye), Edit (pencil), Delete (trash), Add (plus), List (lines). JSON configs can use lowercase keys ("view", "edit", etc.) — the grid normalises them via toActionIconKey().


10. Filtering

Filters are rendered in a toggle-able row below the column headers. Each filter cell type is determined by meta.filterType (or inferred from fieldType). All filter functions run through the advancedFilterFn registered as TanStack filterFns.advanced.

10.1 Filter Types

| filterType | UI control | Operands | |---|---|---| | text | Text input + operand selector | contains, notContains, equals, notEquals, startsWith, endsWith | | number | Number input + operand selector | =, <, >, <=, >=, != | | date / datetime / time | Flatpickr date/time picker + operand | Number (milliseconds comparison) | | select | Dropdown single-select popover | Equality (filterOptions[].value) | | multiselect / checkbox | Multi-select popover with Select All | Array.includes check | | dropdown | Dropdown single-select | Equality | | radio | Radio group | Equality | | boolean | Yes / No / (clear) toggle | Strict boolean equality |

10.2 Filter Operand Normalisation

normalizeSabFilterOperand() maps human-readable labels ("less than", "does not contain", symbols "<", ">=") to internal switch keys (lt, notContains, gte, …). This allows filter state from JSON configs to use either form.


11. Aggregations

File: src/utils/sabAggregation.ts

Two aggregation surfaces exist: group footers (under each expanded group in the body) and list footers (a fixed <tfoot> row showing grand totals).

11.1 Available Aggregation Functions

sabAggregationFns extends TanStack's built-in set with a custom count that returns distinct values for string columns (instead of raw row count):

| Key | Description | |---|---| | sum | Sum of all leaf row values. | | mean | Arithmetic mean. | | median | Median value. | | min / max | Minimum / maximum. | | count | Distinct string count (Chainsys override); row count for non-strings. | | extent | Range: [min, max] tuple. | | uniqueCount | Count of unique values. |

11.2 Group Footer Visibility

shouldShowGroupFooter() returns true only when the group is expanded AND has leaf rows AND at least one visible column defines an aggregation function. buildDisplayRowsWithGroupFooters() inserts SabDisplayRow{kind:"groupFooter"} entries after each qualifying group.

11.3 Footer Presentation

| Variant | Rendering | |---|---| | total | Bold label + regular value. Default for sum/count/min/max/… | | average | Italic grey label + value. Default for mean/median. | | plain | Formatted value only, no label. |

11.4 List Footer Scope

meta.footerScope controls which rows are aggregated: "filtered" (default) = all filtered leaf rows across all pages (true grand total); "page" = current page only.


12. Export (Excel & CSV)

File: src/utils/exportUtils.ts

12.1 Excel Export

exportTableToExcel(data, filename?, tableTitle?, footerOptions?): Promise<void>

Uses ExcelJS to build a Workbook with one frozen-header sheet. Optional tableTitle inserts two rows (title + blank) before headers. Footer row (from buildExportFooterRowFromTable) renders in bold. Triggers browser download via an object URL.

12.2 CSV Export

exportTableToCSV(data, filename?, footerOptions?): void

Pure string / Blob export — no ExcelJS dependency. Values are double-quoted with internal quotes escaped. Lines joined with (LF). Footer row appended after a blank line.

12.3 Footer Row Helper

buildExportFooterRowFromTable(table, visibleColumns, getExportColumnKey): Record<string,string> | undefined

Iterates visible columns, reads meta.footerAggregationFn, computes the aggregate, formats with formatCellValueAsPlainText, and builds a label:value string per the footer's variant setting. Returns undefined if no column has a footer aggregation.


13. Column Resize Utilities

File: src/utils/tableColumnResizeUtils.ts

resizeColumnsByCellContent() calculates pixel widths from actual cell content without DOM measurement. It uses two per-character width lookup tables (cell content and header/title) to estimate pixel widths, then applies a max of 250 px per column.

13.1 Algorithm

  1. Step 1: Initialise column width from header text (title width map) or fixed value for checkbox/action columns.
  2. Step 2: Iterate all data rows; for each cell compute character-width sum from the display text.
  3. Step 3: Take max(headerWidth, cellWidth) capped at MAX_COLUMN_WIDTH (250 px).
  4. Step 4: If distributeExtraSpace=true and total < containerWidth → distribute extra space proportionally; if total > containerWidth → scale down (min 40 px per column).

13.2 Split-Freeze Sizing

applyFreezeSplitFitContentSizing(intrinsic, leftColumnIds, scrollContainerWidth, distributeExtraSpace)

Applies extra-space distribution or scale-down only to the scroll-pane columns; frozen left columns keep their intrinsic widths unchanged.


14. Theming

The grid supports three theme modes via the theme prop: "light", "dark", or "system" (default). In system mode, the grid observes the document for class="dark" or data-theme="dark" on html, body, or #root via a MutationObserver. The resolved theme is set on the grid root as data-sab-theme.

Tailwind integration:

All Tailwind utility classes use the dark: variant. Host apps must include this package's dist/ in their Tailwind content array. The subpath export @chainsys/sab-react-grid/tailwind-content provides the glob paths.

// tailwind.config.cjs
const { sabReactGridContent } = require('@chainsys/sab-react-grid/tailwind-content')

module.exports = {
  content: ["./src/**/*", ...sabReactGridContent],
  // ...
}

15. Public API — Full Exports Index

Sourced from src/index.ts:

| Export Name | Kind | Source Module | |---|---|---| | SabReactTable | Component | SabReactTable.tsx | | NormalGrid | Alias Component | components/grid/NormalGrid.tsx | | isCheckboxSelectorColumn | Function | SabReactTable.tsx | | evaluateFreezeUpto | Function | SabReactTable.tsx | | FREEZE_TOAST_ALL_VISIBLE | Constant | SabReactTable.tsx | | FREEZE_TOAST_SCREEN_WIDTH | Constant | SabReactTable.tsx | | FREEZE_TOAST_RESIZE | Constant | SabReactTable.tsx | | SabReactTableProps | Type | SabReactTable.tsx | | SabGridToolbarIcons | Type | SabReactTable.tsx | | LayoutMode | Type | SabReactTable.tsx | | SabGridTheme | Type | SabReactTable.tsx | | SabGridResolvedTheme | Type | SabReactTable.tsx | | OnActionClickHandler | Type | SabReactTable.tsx | | OnFieldNavigationOverrideHandler | Type | SabReactTable.tsx | | OnFieldNavigationHandler | Type | SabReactTable.tsx | | `FieldNavigationDialo