Interfaces
Option bags, props and shape contracts.
AutoColumnsOptions
Tuning for auto-generated columns (X27). All optional.
interface AutoColumnsOptions {
/** How many rows to sample when collecting keys. Default 50. */
sampleRows?: number;
/** Only generate these keys (in this order). Omit → every key found. */
include?: string[];
/** Never generate columns for these keys (e.g. an internal id). */
exclude?: string[];
/** Turn a data key into a column header. Default: humanize (`unitPrice` → "Unit Price"). */
header?: (key: string) => string;
/**
* Infer a Bst cell type (`meta.type`) from a key + a sample value. Return
* `undefined` to leave the column untyped (plain text). Omit → the built-in
* guesser (number / boolean / date, else text).
*/
inferType?: (key: string, sampleValue: unknown) => string | undefined;
}
AutoSizeOptions
Options for computeAutoWidth.
interface AutoSizeOptions {
/** CSS `font` shorthand used for measurement. Default a 14px system UI stack. */
font?: string;
/** Horizontal padding to add (cell padding + sort/resizer affordance). Default 28. */
padding?: number;
/** Lower clamp. Default 56. */
min?: number;
/** Upper clamp. Default 480. */
max?: number;
}
BarcodeResult
Dependency-free Code 128 (subset B) barcode encoder → bar-width pattern. The barcode cell type renders it to inline SVG, so a barcode cell needs no runtime dependency. Code 128B covers all printable ASCII (space–~), which is what SKUs / IDs / short codes use. Verified by an encode→decode round-trip in tests.
interface BarcodeResult {
/** Concatenated bar/space run widths (digits 1–4), starting with a bar. */
pattern: string;
/** The encoded text (echoed for an optional human-readable line). */
text: string;
}
BstCellApi
Bound per-cell API handed to renderers so action / file cells can drive the grid.
interface BstCellApi<TData extends RowData = any> {
/** Whether this cell is currently in edit mode. */
isEditing: boolean;
/** Whether editing is blocked for this cell (access control / not editable). */
isDisabled: boolean;
/** Whether this cell (or its row session) holds an unsaved draft. */
isDirty: boolean;
/** Enter edit mode for this cell (no-op when not editable). */
startEditing: () => void;
/** Abandon the active edit, restoring the source value. */
cancelEditing: () => void;
/** Persist a value for this cell per the save policy. */
commitCell: (value: unknown) => void;
/** Begin a deferred row-edit session for this row (C2 ≡ I2). */
editRow: () => void;
/** Commit the row session for this row. */
saveRow: () => void;
/** Delete this row (I1 / row lifecycle). */
deleteRow: () => void;
/** Duplicate this row with a fresh temp id. */
duplicateRow: () => void;
/** Current validation findings for this cell. */
errors: FieldError[];
}
BstCellEdit
One unsaved cell edit — previous → new value, plus display strings formatted via the column's cell type. What the review-changes sheet lists per cell.
interface BstCellEdit<TData> {
rowId: string;
columnId: string;
/** The data field written (`cellMeta.field` ?? `accessorKey` ?? column id). */
field: string;
/** The value currently upstream (before the save). */
oldValue: unknown;
/** The pending draft value. */
newValue: unknown;
/** `oldValue` formatted for display (cell-type `format`, else `String`). */
oldText: string;
/** `newValue` formatted for display. */
newText: string;
/** The row as it is upstream, when still present. */
row: TData | undefined;
}
BstCellSpan
Cell spanning (A5) — a render-layer feature (Plan.md §2.7a). v9 9.1.2 ships no spanning feature, and there is no virtualizer yet, so spanning is a pure paint concern: compute which body cell is the top-left origin of a merged block and which cells it covers; the renderer draws origins with colSpan/rowSpan and skips covered cells entirely. Two ways to declare a span (both need enableCellSpanning): - meta.rowSpan: 'group' on a column → auto-merge vertically-consecutive cells with an equal value (the classic "merge same values" case); - a grid-level getCellSpan(ctx) returning { colSpan?, rowSpan? } for the origin cell — full control over both axes.
interface BstCellSpan {
colSpan?: number;
rowSpan?: number;
}
BstClassNames
Consumer-owned class names for the grid's structural slots — the custom-CSS hook (K1/K2). Static strings for fixed parts; headerCell / row / cell also accept a function for per-item classes. Every slot composes with (never replaces) the built-in bst-* classes, so themes keep working. root targets the same element as the <BstTable className> prop (the scroll wrapper). Slots map to: root→.bst-table-scroll, table→<table>, header→<thead>, headerRow→header <tr>, headerCell→<th>, filterRow→the per-column filter <tr>, body→<tbody>, row→body <tr>, cell→body <td>, empty→the "No rows" <td>.
interface BstClassNames<TData extends RowData = any> {
root?: string;
table?: string;
header?: string;
headerRow?: string;
headerCell?: string | ((ctx: BstHeaderSlotContext) => string | undefined);
filterRow?: string;
body?: string;
row?: string | ((ctx: BstRowContext<TData>) => string | undefined);
cell?: string | ((ctx: CellRenderProps<TData>) => string | undefined);
empty?: string;
}
BstColumnMeta
Per-column meta the registry + features read. Authored on columnDef.meta and typed via the columnMeta slot on tableFeatures (no global declaration merging — Plan.md §2.1). All fields optional; type defaults to 'text'.
interface BstColumnMeta<TData extends RowData = any, TValue = any> {
/** Selects the CellType from the registry. Default: `'text'`. */
type?: string;
/** Free-form settings passed through to the CellType (precision, variant, …). */
cellMeta?: Record<string, unknown>;
/** Force inline vs popup editing for this column (overrides the CellType default). */
editMode?: 'inline' | 'popup';
/** Whether this column is editable — static or per-row. Default: `false`. */
editable?: boolean | ((row: TData) => boolean);
/**
* Access control (F3 column / F4 cell). Disable interaction for this column —
* `true` disables the whole column; a predicate disables per row/cell. A
* disabled cell is never editable and renders in a muted style; it cascades
* under the grid `disabled` + `rowDisabled` options and overrides `editable`.
* Selection + copy still work on a disabled cell.
*/
disabled?: boolean | ((row: TData) => boolean);
/** Whether notes / comments are allowed on this column. Defaults to true when enableNotes is on. */
notesAllowed?: boolean;
/** Options for option-based cells (singleSelect / multiSelect / radio). */
options?: BstOption[];
/**
* Per-column filter UI in the filter row (X4 / X11). With `enableSetFilter` on,
* `'set'` forces the **Set Filter** (a checklist of distinct values) for this
* column, and `'condition'` opts back into the default operator input. When
* unset, categorical columns (`singleSelect` / `multiSelect` / `radio` /
* `boolean`) use the
// …truncated
BstContextMenuContext
Context handed to getContextMenuItems — the right-clicked cell + the defaults.
interface BstContextMenuContext<TData extends RowData = any> {
rowId: string;
columnId: string;
value: unknown;
row: TData | undefined;
/** The items Bst-Table would show by default — spread and extend them. */
defaultItems: BstContextMenuItem[];
}
BstContextMenuItem
One entry in the right-click context menu (X6).
interface BstContextMenuItem {
/** Stable key (React list key). */
key?: string;
/** Menu label. */
label?: React.ReactNode;
/** Invoked when the item is chosen; the menu closes afterwards. */
onSelect?: () => void;
/** Greyed out + non-interactive. */
disabled?: boolean;
/** Render a divider instead of an item (`label` / `onSelect` ignored). */
separator?: boolean;
/** Optional leading icon. */
icon?: React.ReactNode;
}
BstDataSourceResult
The current page's rows (also in tableProps.data).
interface BstDataSourceResult<TData> {
/** The current page's rows (also in `tableProps.data`). */
rows: TData[];
/** Total matching rows across all pages. */
totalCount: number;
/** A fetch is in flight. */
loading: boolean;
/** The last fetch's error, or null. */
error: Error | null;
/** Re-run the current query (e.g. after a mutation). */
refetch: () => void;
/** Spread into the grid to put it in server mode. */
tableProps: BstServerTableProps<TData>;
}
BstExportColumn
Export (Phase 5, X1–X3) — dependency-free CSV / Excel (.xlsx) / print builders. The core is pure and DOM-free (so it unit-tests in node): the runtime gathers a BstExportMatrix from the grid — values formatted per cell type, exactly like the clipboard — and these functions serialize it. Two DOM helpers (downloadBlob, printHtml) trigger the browser download / print and are guarded so SSR / jsdom simply no-op. Excel is a real OOXML .xlsx: a store-only (uncompressed) ZIP of minimal SpreadsheetML, assembled here with a hand-rolled CRC-32 + ZIP writer, so no exceljs / sheetjs dependency is pulled into the engine (matching the repo's dep-free ethos — QR, barcode, charts and PDF preview are all dep-free too). Numbers are emitted as typed numeric cells; everything else is an inline string. Opens in Excel, LibreOffice and Google Sheets.
interface BstExportColumn {
/** Column id. */
id: string;
/** Header text written to the first row (when headers are included). */
header: string;
/** `true` (a `number` cell) → emit typed numeric Excel cells from the raw value. */
numeric?: boolean;
}
BstExportMatrix
The tabular payload every exporter serializes. rows holds display strings (already formatted per cell type, so they match what the grid shows and what copy/paste produces); values carries the parallel raw values, used only to type numeric Excel cells.
interface BstExportMatrix {
columns: BstExportColumn[];
/** Display strings, row-major — `rows[r][c]` aligns with `columns[c]`. */
rows: string[][];
/** Raw values parallel to `rows` — used only for Excel numeric typing. */
values?: unknown[][];
}
BstExportOptions
Customization for the enableExport toggle (passing an object implies enabled, §12). The per-format fields (csv/excel/print) are ALSO exposed as the top-level enableCsvExport / enableExcelExport / enablePrint settings-sheet switches, which win over the matching field here.
interface BstExportOptions {
/** Offer CSV. Default `true`. */
csv?: boolean;
/** Offer Excel (`.xlsx`). Default `true`. */
excel?: boolean;
/** Offer Print. Default `true`. */
print?: boolean;
/** Base download file name (the extension is added per format). Default `"export"`. */
fileName?: string;
/** Rows to export — `'all'` pages (default) or the current `'page'`. */
scope?: BstExportScope;
/** Include the header row. Default `true`. */
includeHeaders?: boolean;
}
BstExportRunOptions
Per-call overrides for the runtime.export* methods (all optional).
interface BstExportRunOptions {
/** Row scope for this call — overrides the configured default. */
scope?: BstExportScope;
/** Base file name for this call — overrides the configured default. */
fileName?: string;
/** Header inclusion for this call — overrides the configured default. */
includeHeaders?: boolean;
/** CSV only — field delimiter. Default `","`. */
delimiter?: string;
}
BstFileCellHandlers
The files cell hooks a createFileHandlers bridge produces.
interface BstFileCellHandlers {
onUpload?: (file: File) => Promise<BstFileRef>;
onDelete?: (ref: BstFileRef) => Promise<void>;
}
BstFileRef
A stored file reference the grid keeps in row data (I3, Plan.md §2.2). url should be a short-lived / signed view URL, not a permanent link — resolve it on demand with getFileUrl so B5 thumbnails never bake a permanent URL into the row. Structurally a superset of the adapters' file-cell shape, so a ref returned by uploadFile drops straight into a files cell.
interface BstFileRef {
/** Display name. */
name: string;
/** Stable storage id/key — what `uploadFile` returns and `deleteFile`/`getFileUrl` take. */
id?: string;
/** A (possibly short-lived) URL to view / download the file. */
url?: string;
/** MIME type, when known. */
contentType?: string;
/** Size in bytes, when known. */
size?: number;
}
BstFindOptions
Configuration for Find (X8) — the object form of enableFind.
interface BstFindOptions {
/** Match case. Default: false (case-insensitive). */
caseSensitive?: boolean;
/**
* Which rows Find searches: `'view'` = the rows currently in the view (the
* active page / virtual window), `'all'` = every row across all pages
* (pre-pagination). Default: `'view'`.
*/
scope?: 'view' | 'all';
}
BstFloatingActionBarOptions
Options for Floating Selection Action Bar (enableFloatingActionBar).
interface BstFloatingActionBarOptions {
/** Optional custom position: 'bottom-center' | 'bottom-right' | 'top-center'. Default: 'bottom-center'. */
position?: 'bottom-center' | 'bottom-right' | 'top-center';
}
BstFloatingActionsContext
Context handed to renderFloatingActions.
interface BstFloatingActionsContext<TData extends RowData = any> {
/** Selected row models. */
selectedRows: Array<{
id: string;
original: TData;
}>;
/** Count of selected rows. */
count: number;
/** Clear current selection. */
clearSelection: () => void;
/** Copy selected rows to clipboard as TSV. */
copySelected: () => void;
/** Delete selected rows (if row actions enabled). */
deleteSelected?: () => void;
/** Export selected rows (if export enabled). */
exportSelected?: () => void;
}
BstFormatBuilderColumn
Minimal column descriptor the builder needs (id + label + cell type).
interface BstFormatBuilderColumn {
id: string;
header?: string;
type?: string;
}
BstFormatContext
Context passed to a predicate when.
interface BstFormatContext<TData = any> {
value: unknown;
row: TData;
rowId: string;
/** The column being evaluated (cell scope) or the rule's trigger column (row scope). */
columnId?: string;
}
BstFormatPreset
A named style preset for the conditional-format builder.
interface BstFormatPreset {
id: string;
label: string;
className?: string;
style?: React.CSSProperties;
}
BstFormatRule
A single conditional-format rule.
interface BstFormatRule<TData = any> {
/** Stable id (useful for a builder UI). */
id?: string;
/**
* The column whose value the rule tests. For `scope: 'cell'`, also the column
* whose cells get styled (omit to style **every** cell against its own value).
* For `scope: 'row'`, the trigger column whose value decides the whole row.
*/
columnId?: string;
/** `'cell'` (default) styles the matched cell; `'row'` styles the whole row. */
scope?: BstFormatScope;
/** A `{ op, value }` condition (E3 operators) or a predicate over the row/cell. */
when: FilterCondition | ((ctx: BstFormatContext<TData>) => boolean);
/** Class name(s) applied on match. */
className?: string;
/** Inline style / CSS vars applied on match. */
style?: React.CSSProperties;
/** F5 — blank the matched cell's content (cell scope only). */
hideContent?: boolean;
}
BstGridState
A serializable grid view-state snapshot (plain JSON — safe to persist per user).
interface BstGridState {
/** Schema version — {@link BST_GRID_STATE_VERSION}. */
version: number;
sorting?: Array<{
id: string;
desc: boolean;
}>;
columnFilters?: Array<{
id: string;
value: unknown;
}>;
globalFilter?: unknown;
columnOrder?: string[];
columnSizing?: Record<string, number>;
columnVisibility?: Record<string, boolean>;
/** v9 pinning is `{ start, end }` (renamed from v8 left/right). */
columnPinning?: {
start?: string[];
end?: string[];
};
grouping?: string[];
expanded?: true | Record<string, boolean>;
rowPinning?: {
top?: string[];
bottom?: string[];
};
rowSelection?: Record<string, boolean>;
pagination?: {
pageIndex: number;
pageSize: number;
};
}
BstGridStateController
Imperative handle returned by useBstGridState.
interface BstGridStateController {
/** The full `bst-table:state:<key>` storage key, or `null` when persistence is off. */
storageKey: string | null;
/** Current snapshot of the grid. */
getState: () => BstGridState;
/** Restore a snapshot onto the grid. */
applyState: (state: Partial<BstGridState> | null | undefined) => void;
/** Force-write the current snapshot now (bypasses the debounce). */
save: () => void;
/** Delete the persisted snapshot (the live view is untouched). */
clear: () => void;
/**
* Reset the grid's view (sort / filter / column layout / grouping) to defaults.
* With auto-persist on, the default view is then written back; call `clear()`
* too if you want to forget the persisted snapshot entirely.
*/
reset: () => void;
}
BstGridStateOptions
Key suffix, namespaced under bst-table:state:.
interface BstGridStateOptions extends BstGridStateSelect {
/** Key suffix, namespaced under `bst-table:state:`. */
key: string;
/** Storage backend (default `window.localStorage`). */
storage?: BstGridStateStorage;
/** Persist changes automatically. Default `true`. */
persist?: boolean;
/** Debounce writes, in ms (rapid column-resize etc.). Default `300`; `0` = synchronous. */
debounceMs?: number;
}
BstGridStateSelect
Narrow which slices a snapshot / restore touches.
interface BstGridStateSelect {
/** Only these slices (default: all). */
include?: BstGridStateKey[];
/** Drop these slices (applied after `include`). */
exclude?: BstGridStateKey[];
}
BstGridStateStorage
Minimal localStorage-shaped contract, so sessionStorage / a mock also work.
interface BstGridStateStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
BstHeaderSlotContext
Column context handed to the headerCell class/style slot.
interface BstHeaderSlotContext {
columnId: string;
}
BstIcons
The body-icon slots the engine renders (distinct from an adapter's chrome icons).
interface BstIcons {
/** Header: column sorted ascending. */
sortAsc: IconComponent;
/** Header: column sorted descending. */
sortDesc: IconComponent;
/** Header: sortable but unsorted. */
sortNone: IconComponent;
/** Expander / group row: expanded (points down). */
expandExpanded: IconComponent;
/** Expander / group row: collapsed (points right). */
expandCollapsed: IconComponent;
/** Row-pinning control. */
pin: IconComponent;
/** Boolean cell — true. */
booleanTrue: IconComponent;
/** Builder remove-rule button (filter / conditional-format). */
remove: IconComponent;
/** KPI cell — positive delta (trend up). */
trendUp: IconComponent;
/** KPI cell — negative delta (trend down). */
trendDown: IconComponent;
/** File cell — generic / unknown type. */
fileGeneric: IconComponent;
/** File cell — PDF. */
filePdf: IconComponent;
/** File cell — Word / rich text. */
fileDoc: IconComponent;
/** File cell — spreadsheet. */
fileSheet: IconComponent;
/** File cell — presentation. */
fileSlides: IconComponent;
/** File cell — archive. */
fileArchive: IconComponent;
/** File cell — audio. */
fileAudio: IconComponent;
/** File cell — video. */
fileVideo: IconComponent;
}
BstInfiniteDataSourceResult
All rows accumulated so far (across every loaded window).
interface BstInfiniteDataSourceResult<TData> {
/** All rows accumulated so far (across every loaded window). */
rows: TData[];
/** Total rows matching the current query across all windows. */
totalCount: number;
/** The first-window (reset) fetch is in flight. */
loading: boolean;
/** A follow-on window (append) fetch is in flight. */
isFetchingNextPage: boolean;
/** More rows remain to load (accumulated < total). */
hasNextPage: boolean;
/** The last fetch's error, or null. */
error: Error | null;
/** Load the next window and append it. No-op while fetching or at the end. */
fetchNextPage: () => void;
/** Alias of `fetchNextPage`, named for `<BstTable onReachEnd>`. */
onReachEnd: () => void;
/** Reset and re-fetch the first window (e.g. after a mutation). */
refetch: () => void;
/** Spread into the grid to run it in infinite server mode. */
tableProps: BstInfiniteTableProps<TData>;
}
BstInfiniteTableProps
Props to spread into the grid to run it in infinite (append) server mode.
interface BstInfiniteTableProps<TData> {
data: TData[];
manualSorting: true;
manualFiltering: true;
manualPagination: true;
autoResetPageIndex: false;
rowCount: number;
state: {
sorting: DsSort[];
columnFilters: DsColumnFilter[];
globalFilter: string;
pagination: {
pageIndex: number;
pageSize: number;
};
};
onSortingChange: (u: Updater<DsSort[]>) => void;
onColumnFiltersChange: (u: Updater<DsColumnFilter[]>) => void;
onGlobalFilterChange: (u: Updater<string>) => void;
onPaginationChange: (u: Updater<unknown>) => void;
/** Initial fetch in flight (X23) — drives the loading overlay (not appends). */
loading: boolean;
/** Last fetch error, or null (X23) — drives the error overlay. */
error: Error | null;
}
BstNoteEditorProps
interface BstNoteEditorProps {
rowId: string;
columnId: string;
initialText: string;
anchorRect: DOMRect | null;
onSave: (text: string) => void;
onDelete?: () => void;
onClose: () => void;
placeholder?: string;
}
BstNotePopoverProps
interface BstNotePopoverProps {
note: string;
anchorRect: DOMRect | null;
onEdit?: () => void;
}
BstNoteSaveEvent
Event payload emitted on note save/delete.
interface BstNoteSaveEvent {
rowId: string;
columnId: string;
note: string | undefined;
prevNote: string | undefined;
}
BstNotesOptions
Options for cell notes / comments (enableNotes).
interface BstNotesOptions {
/** Optional placeholder for the note editor. */
placeholder?: string;
/** Whether notes can be edited on a given cell. Defaults to true. */
canEditNote?: (rowId: string, columnId: string, currentNote?: string) => boolean;
}
BstOption
Option for option-based cells — singleSelect / multiSelect / radio (overlap #12).
interface BstOption {
value: string;
label?: string;
/** Swatch / badge color (any CSS color). */
color?: string;
/** Leading icon node (adapter-provided). */
icon?: React.ReactNode;
/** Avatar / image URL rendered as a small circle. */
avatar?: string;
/** Free-form image URL. */
image?: string;
description?: string;
disabled?: boolean;
}
BstPageSizeChoice
A resolved dropdown choice: the option value to render and its label.
interface BstPageSizeChoice {
value: number;
label: string;
}
BstRowChange
A row's unsaved edits, grouped — patch is a ready-made PATCH body.
interface BstRowChange<TData> {
rowId: string;
/** The row as it is upstream (before the save). */
original: TData | undefined;
/** The row with every pending edit applied (what it becomes after the save). */
updated: TData | undefined;
/** `field → new value` for just the changed fields. */
patch: Record<string, unknown>;
changes: BstCellEdit<TData>[];
}
BstRowContext
Row context handed to the row class/style slot (K2 dynamic row styling).
interface BstRowContext<TData extends RowData = any> {
row: TData;
rowId: string;
/** Visual (post sort/filter/paginate) row index. */
index: number;
}
BstRuntime
Get the note text for a cell, or undefined if no note exists.
interface BstRuntime<TData extends RowData> {
store: InteractionStore;
updateCtx: (ctx: RuntimeCtx<TData>) => void;
reset: () => void;
/** Get the note text for a cell, or undefined if no note exists. */
getNote: (rowId: string, columnId: string) => string | undefined;
/** Set or delete a cell's note. */
setNote: (rowId: string, columnId: string, note: string | undefined) => void;
/** Delete a cell's note. */
deleteNote: (rowId: string, columnId: string) => void;
/** Open the note editor popover for a cell. */
openNoteEditor: (rowId: string, columnId: string) => void;
/** Close the note editor popover. */
closeNoteEditor: () => void;
/** Check if a cell has a non-empty note. */
hasNote: (rowId: string, columnId: string) => boolean;
/** Check if notes are allowed on a cell. */
isNoteAllowed: (rowId: string, columnId: string) => boolean;
/** Open the find bar (highlights matches; never hides rows). */
openFind: () => void;
/** Close the find bar and clear all match highlights. */
closeFind: () => void;
/** Set the query and recompute matches (the cursor resets to the first match). */
setFindQuery: (query: string) => void;
/** Recompute matches for the current query after a view / data change, keeping
* the cursor on the same cell when it survives (else clamping to the first). */
refreshFind: () => void;
/** Advance the cursor to the next match (wraps). */
findNext: () => void;
/** Move the cursor to the previous match (wraps). */
findPrev: () => voi
// …truncated
BstRuntimeHandle
Row selection checkbox column is active (Phase 3).
interface BstRuntimeHandle<TData extends RowData> {
runtime: BstRuntime<TData>;
registry: CellTypeRegistry;
editingMode: 'cell' | 'row' | 'batch';
enableRowActions: boolean;
/** Row selection checkbox column is active (Phase 3). */
enableRowSelection: boolean;
/** Column pinning (sticky) is active (Phase 3) — affects the select column too. */
enableColumnPinning: boolean;
/** Column drag-to-reorder is active (Phase 3). */
enableColumnOrdering: boolean;
/** Per-column header filter row is rendered (Phase 3, "dual filter"). */
enableColumnFilterRow: boolean;
/** Set Filter (X4) — eligible columns show a distinct-values checklist in the filter row. */
enableSetFilter: boolean;
/** Multi-filter (X11) — columns with an array `meta.filter` stack those filters. */
enableMultiFilter: boolean;
/** Cell/range selection + keyboard nav is active (Phase 3). */
enableCellSelection: boolean;
/** Type-to-edit — spreadsheet-style entry (type to overwrite; Enter/Tab
* commit-and-move). True only when editing + cell selection are also on.
* Read by the keydown handler and the shortcuts overlay (`<BstShortcuts>`). */
enableTypeToEdit: boolean;
/** Clipboard copy/paste is active (Phase 3). */
enableClipboard: boolean;
/** Undo/redo is active (Phase 3, C5). */
enableUndoRedo: boolean;
/** Inline editing is active — read by the shortcuts overlay (`<BstShortcuts>`). */
enableEditing: boolean;
/** Whole-column copy gesture is active (clipboard on + not opted out). */
en
// …truncated
BstSaveEvent
The payload of ONE save action (I2/I4). Handed to onSave exactly once per confirm — never per cell/row/column — so the backend call stays a single batched request whichever granularity the API wants: changes (cell-wise), rows[].patch (row-wise), or next (whole grid).
interface BstSaveEvent<TData> {
/** Every pending cell edit, flat. */
changes: BstCellEdit<TData>[];
/** The same edits grouped per row. */
rows: BstRowChange<TData>[];
/** The full next data array (what `onDataChange` receives after the save). */
next: TData[];
}
BstSaveResult
What an onSave handler may RETURN to reconcile the backend's response back into the grid (I4). It is the resolved value of the save promise — think Promise.allSettled, not resolve-vs-reject: throwing still aborts the whole save and keeps every draft, while returning this settles each row/cell. - Return nothing → today's behaviour: every draft commits with the typed values. - Return a result → applied rows adopt the server's authoritative values and their drafts clear; failed rows/cells keep their draft and show an error; every other changed row commits with its local value.
interface BstSaveResult<TData> {
/**
* Rows the server accepted, each with the server's official values (a full or
* PARTIAL row) merged onto the grid row — so IDs, timestamps, recomputed or
* normalised fields display what was actually stored. These rows' drafts clear.
* Key by the same `rowId` the save event used (a new row's temporary id maps to
* the server row here, so its real id lands in `values`).
*/
applied?: Array<{
rowId: string;
values: Partial<TData>;
}>;
/**
* Rows/cells the server rejected. Their drafts are KEPT and the `error` is shown
* on the cell(s) (reusing the validation-error UI). Omit `columnId` for a
* whole-row failure — the error then flags every edited cell in that row.
*/
failed?: Array<{
rowId: string;
columnId?: string;
error: string;
}>;
}
BstSelectionStats
Aggregate of the current cell selection — shown in the status bar (X5).
interface BstSelectionStats {
/** Total selected cells (real data rows only). */
count: number;
/** How many of the selected cells hold a numeric value. */
numericCount: number;
/** Sum of the numeric cells. */
sum: number;
/** Mean of the numeric cells (0 when none). */
avg: number;
/** Min of the numeric cells (0 when none). */
min: number;
/** Max of the numeric cells (0 when none). */
max: number;
}
BstServerTableProps
Props to spread into useBstTable / an adapter to run the grid in server mode.
interface BstServerTableProps<TData> {
data: TData[];
manualSorting: true;
manualFiltering: true;
manualPagination: true;
autoResetPageIndex: false;
rowCount: number;
state: {
sorting: DsSort[];
columnFilters: DsColumnFilter[];
globalFilter: string;
pagination: DsPagination;
};
onSortingChange: (u: Updater<DsSort[]>) => void;
onColumnFiltersChange: (u: Updater<DsColumnFilter[]>) => void;
onGlobalFilterChange: (u: Updater<string>) => void;
onPaginationChange: (u: Updater<DsPagination>) => void;
/** Fetch in flight (X23) — drives the loading overlay. */
loading: boolean;
/** Last fetch error, or null (X23) — drives the error overlay. */
error: Error | null;
}
BstSetFilterOption
Set Filter (X4) — an Excel-style checklist of distinct values for one column. A dependency-free popover (search · select-all / clear · per-value counts · a "(Blanks)" entry) that drives the column's own columnFilters entry via setFilterValue({ op: 'set', value }) — interpreted by the bstCondition filterFn, so it composes with the filter builder and the rest of the filter row. When every value is checked the filter clears (inactive); an empty selection matches nothing. Distinct values come from meta.options when present, else from the grid's rows (client mode — under a server DataSource it lists the loaded page). Rendered inside the per-column filter row for eligible columns; also exported for bespoke filter UIs.
interface BstSetFilterOption {
/** The stored value (stringified) — what the filter matches on. */
value: string;
/** Human label (from `meta.options`, else the value itself). */
label: string;
/** How many rows carry this value (when derivable). */
count?: number;
}
BstSetFilterProps
The TanStack column this filter targets.
interface BstSetFilterProps {
/** The TanStack column this filter targets. */
column: any;
/** The TanStack table instance (source of distinct values). */
table: any;
/** Accessible label; defaults to the column header / id. */
label?: string;
/**
* Controlled slot (multi-filter, X11). When `onChange` is given, the checklist
* reads/writes this `{ op:'set', value }` condition instead of the column's whole
* filter value — so it can be one part of a stacked filter. Omit both for the
* default standalone mode (drives `column.setFilterValue` directly).
*/
value?: {
op?: string;
value?: string[];
};
onChange?: (condition: {
op: 'set';
value: string[];
} | undefined) => void;
}
BstSettingsGroup
Items grouped for sheet rendering (a labelled section per group).
interface BstSettingsGroup {
name: string;
items: BstSettingsItem[];
}
BstSettingsItem
One toggle in the settings sheet — its live value plus mutators.
interface BstSettingsItem {
key: BstSettingKey;
label: string;
group: string;
layer: 'engine' | 'chrome';
hint?: string;
/** Current effective value — the user override if present, else the developer prop / default. */
value: boolean;
/** True when the user has changed this away from the developer-provided value. */
overridden: boolean;
/**
* A prerequisite (`requires`) is off, so this toggle can't take effect — the
* sheet renders it disabled and non-interactive until the parent is back on.
*/
disabled: boolean;
/** Label of the prerequisite blocking it (for a tooltip), when `disabled`. */
disabledBy?: string;
/**
* The prerequisite this item hangs off **within the same group** — set when the
* required parent is also rendered in this section, so the sheet can draw a
* tree/branch connector from parent to child. Cross-group prerequisites (parent
* in another section) stay unlinked; the "Needs …" hint still names them.
*/
parentKey?: BstSettingKey;
/** This is the last child of its `parentKey` in the group — the connector's
* vertical branch line stops at this row. */
lastChild?: boolean;
set: (next: boolean) => void;
toggle: () => void;
reset: () => void;
}
BstSettingsModel
The headless model an adapter renders as the settings sheet.
interface BstSettingsModel {
/** Flat, in registry order. */
items: BstSettingsItem[];
/** Grouped for sheet rendering. */
groups: BstSettingsGroup[];
/** How many settings the user has changed from the developer configuration. */
overrideCount: number;
/** Clear every override — back to the developer-provided configuration. */
reset: () => void;
/** The `localStorage` key in use, or `null` when persistence is off. */
storageKey: string | null;
}
BstSettingsOptions
Options for the settings sheet (showSettings={{ … }}). Passing an object implies enabled (§12).
interface BstSettingsOptions {
/**
* Restrict the sheet to these keys (rendered in registry order). Omit → auto:
* the default-on data/display features plus any opt-in feature the developer
* has enabled.
*/
features?: BstSettingKey[];
/** Sheet heading. Default `"Table settings"`. */
title?: string;
/**
* Explicit `localStorage` key. Omit → a key derived from the column ids (stable
* per grid shape). Set this to disambiguate two grids with identical columns.
*/
persistKey?: string;
/** Persist the user's choices to `localStorage`. Default `true`. */
persist?: boolean;
/**
* Show a search box in the sheet to filter the toggle list — the sheet can list
* 30+ features, so it's on by default but appears **only once the sheet has more
* than a handful of items** (`{@link shouldShowSettingsSearch}`). `true` always
* shows it; `false` never. Filters by label / hint / group name.
*/
search?: boolean;
}
BstShortcut
Key tokens for one chord. 'Mod' → ⌘ on Mac / Ctrl elsewhere; 'Shift' → ⇧ / Shift; 'Arrows' → the arrow cluster; others render literally.
interface BstShortcut {
/** Key tokens for one chord. `'Mod'` → ⌘ on Mac / Ctrl elsewhere; `'Shift'` →
* ⇧ / Shift; `'Arrows'` → the arrow cluster; others render literally. */
keys: string[];
/** Short human label. */
label: string;
category: ShortcutCategory;
/** Engine flags that must ALL be active for this shortcut to fire. */
requires: string[];
/** Show this entry only on the given platform (e.g. `⌘Y` redo is a PC-only
* convention). Omit → shown on both. Filtered by `resolveActiveShortcuts`. */
platform?: 'mac' | 'pc';
}
BstShortcutsProps
The table from useBstTable / useBstGrid — its resolved flags decide which shortcuts are shown.
interface BstShortcutsProps<TData extends RowData> {
/** The table from `useBstTable` / `useBstGrid` — its resolved flags decide which
* shortcuts are shown. */
table: unknown;
/** Extra class on the trigger button (adapters can blend it into their toolbar). */
className?: string;
/** Key rendering: `'mac'` forces ⌘/⇧, `'pc'` forces Ctrl, `'auto'` (default)
* detects from `navigator`. Use it when detection is unreliable (remote/proxied). */
platform?: 'mac' | 'pc' | 'auto';
}
BstSpanContext
Context handed to getCellSpan for one visible body cell.
interface BstSpanContext<TData = any> {
row: TData;
rowId: string;
columnId: string;
value: unknown;
/** Visual (post sort/filter/paginate) row index. */
rowIndex: number;
/** Visible-leaf column index. */
colIndex: number;
}
BstStickyHeaderOptions
Sticky-header viewport (G3/G4) — the small render-layer companion that caps the scroll box to a bounded height so the body scrolls inside the grid under a header (and per-column filter row) that stays pinned, instead of the whole table growing taller as the page size grows. Opt-in per §12 (enableStickyHeader), off by default. Row virtualization already does exactly this (its .bst-virtualized rules cap the body + stick the header); this is the same viewport without row windowing, for the small / medium grids where windowing isn't warranted. So BstTable adds the .bst-sticky-header class only when virtualization is off — see BstTable.tsx. This module holds only the pure resolve helper; the class + inline --bst-max-height var are applied in BstTable.tsx.
interface BstStickyHeaderOptions {
/**
* Max body height as a CSS pixel number (e.g. `440`) or any CSS length string
* (e.g. `'60vh'`). Wins over `maxRows`. Default: 440px.
*/
maxHeight?: number | string;
/**
* Cap the viewport to roughly this many body rows. Converted to a pixel height
* with a fixed row-height estimate (see {@link STICKY_ROW_PX}), so it is
* **approximate** across densities — use `maxHeight` when you need an exact box.
*/
maxRows?: number;
}
BstStyles
Inline styles / CSS variables per structural slot — parallels BstClassNames.
interface BstStyles<TData extends RowData = any> {
root?: React.CSSProperties;
table?: React.CSSProperties;
header?: React.CSSProperties;
headerRow?: React.CSSProperties;
headerCell?: React.CSSProperties | ((ctx: BstHeaderSlotContext) => React.CSSProperties | undefined);
filterRow?: React.CSSProperties;
body?: React.CSSProperties;
row?: React.CSSProperties | ((ctx: BstRowContext<TData>) => React.CSSProperties | undefined);
cell?: React.CSSProperties | ((ctx: CellRenderProps<TData>) => React.CSSProperties | undefined);
empty?: React.CSSProperties;
}
BstTableEngineToggles
Engine-behaviour toggles (§12 enable* layer). OOTB data features default ON (opt-out); heavy features (editing, validation) default OFF (opt-in).
interface BstTableEngineToggles {
/** Column sorting. Maps to v9 `enableSorting`. Default: true. */
enableSorting?: boolean;
/** Global search filtering behaviour. Maps to v9 `enableGlobalFilter`. Default: true. */
enableGlobalFilter?: boolean;
/** Per-column filtering behaviour (drives the E3 filter builder). Maps to v9 `enableColumnFilters`. Default: true. */
enableColumnFilters?: boolean;
/**
* Set Filter (X4) — an Excel-style **checklist of distinct values** per column,
* rendered in the per-column filter row. When on, categorical columns
* (`singleSelect` / `multiSelect` / `radio` / `boolean`) use the checklist; any
* column can force it via `meta.filter: 'set'` or opt out via
* `meta.filter: 'condition'`. Needs `enableColumnFilters` + the filter row
* (`enableColumnFilterRow`) to be visible. Default: false.
*/
enableSetFilter?: boolean;
/**
* Multi-filter (X11) — **stack several filter types on one column**. A column
* opts in with `meta.filter` as an array (e.g. `['condition', 'set']`), which
* renders the listed filters **stacked** in its filter row; a row must satisfy
* **all** of them (AND). Needs `enableColumnFilters` + `enableColumnFilterRow`
* (and `enableSetFilter` for a `'set'` part). Default: false.
*/
enableMultiFilter?: boolean;
/** Column show/hide behaviour. Maps to v9 `enableHiding`. Default: true. */
enableHiding?: boolean;
/** Column resizing. Maps to v9 `enableColumnResizing`. Default: true. */
enableColumnResizing?: boolean
// …truncated
CellAccess
Resolved access for a cell (F1–F4 cascade).
interface CellAccess {
/** Interaction is turned off (grid / row / column / cell disable). */
disabled: boolean;
/** The cell can be edited (editing on, not disabled, `meta.editable` true). */
editable: boolean;
}
CellChange
interface CellChange<TData> {
rowId: string;
columnId: string;
value: unknown;
row: TData | undefined;
}
CellEditProps
Props passed to renderEdit. Adds the draft + commit/cancel lifecycle.
interface CellEditProps<TData extends RowData = any, TValue = unknown> extends CellRenderProps<TData, TValue> {
/** Current draft value (seeded from the source value or an existing draft). */
draft: TValue;
/** Update the local draft — does NOT persist until `commit`. */
setDraft: (v: TValue) => void;
/** Persist per the save policy (Enter / explicit / blur). */
commit: (override?: TValue) => void;
/** Abandon the edit, restoring the source value. */
cancel: () => void;
/** Whether the editor should grab focus on mount. */
autoFocus: boolean;
}
CellRef
A cell address by stable ids (never indices — Plan.md §2.4/§2.5).
interface CellRef {
rowId: string;
columnId: string;
}
CellRenderProps
Props passed to renderRead. Cheap + scalar-friendly (hot path).
interface CellRenderProps<TData extends RowData = any, TValue = unknown> {
value: TValue;
row: TData;
rowId: string;
columnId: string;
meta: BstColumnMeta<TData, TValue>;
/** True when a dirty draft is being shown (deferred / row-session edits). */
isDirty: boolean;
api: BstCellApi<TData>;
}
CellType
A registered cell type (Plan.md §2.3).
interface CellType<TValue = unknown, TMeta = unknown, TData extends RowData = any> {
/** Registry key — matched against `columnDef.meta.type`. */
id: string;
/** Hot-path read renderer — plain DOM, no component library. */
renderRead: (p: CellRenderProps<TData, TValue>) => React.ReactNode;
/** Edit renderer — adapters provide the rich version (MUI / shadcn). */
renderEdit?: (p: CellEditProps<TData, TValue>) => React.ReactNode;
/** Inline (in-cell) vs popup (dialog) editing. Default: `'inline'`. */
editMode?: 'inline' | 'popup';
/** Parse typed / pasted text into a value (clipboard + text I/O). */
parse?: (raw: string, meta: BstColumnMeta<TData, TValue>) => TValue;
/** Format a value for display + copy. */
format?: (v: TValue, meta: BstColumnMeta<TData, TValue>) => string;
/** Built-in validator, composed before the column-level validator (C3). */
validate?: (v: TValue, ctx: CellValidateContext<TData>) => FieldError[] | Promise<FieldError[]>;
/** When true the editor consumes arrow keys (keyboard-nav yields to it). */
capturesArrowKeys?: boolean;
/**
* When true the editor opens its own portalled overlay (e.g. a MUI `Select`
* menu, a date-picker popper) that renders OUTSIDE this cell in the DOM.
* The host then skips its default commit-on-blur — opening the overlay moves
* focus out of the cell, which would otherwise commit and tear the editor down
* before the user can pick a value. Such editors MUST commit/cancel themselves
* (e.g. `commit(v)` on change, `cancel()`/`co
// …truncated
CellTypeRegistry
A cell-type registry (Plan.md §2.3, concern #2). Keyed by CellType.id, which is matched against columnDef.meta.type. Adapters seed one via their preset (createMuiPreset / createShadcnPreset) and may override individual types.
interface CellTypeRegistry {
register: (ct: CellType<any, any, any>) => void;
/** Resolve a type id, falling back to `'text'` when unknown / undefined. */
get: (id: string | undefined) => CellType<any, any, any>;
has: (id: string) => boolean;
list: () => string[];
/** A shallow copy so adapters can extend without mutating a shared instance. */
clone: () => CellTypeRegistry;
}
CellValidateContext
Context handed to validators (supports cross-column + async — C3).
interface CellValidateContext<TData extends RowData = any> {
row: TData;
rowId: string;
columnId: string;
meta: BstColumnMeta<TData>;
/** Read a sibling cell's current (draft-aware) value for cross-column rules. */
getSiblingValue: (columnId: string) => unknown;
/** Aborts when a newer edit supersedes this async validation. */
signal?: AbortSignal;
}
CsvOptions
Options for toCsv.
interface CsvOptions {
/** Field separator. Default `","`. */
delimiter?: string;
/** Include the header row. Default `true`. */
includeHeaders?: boolean;
/** Prepend a UTF-8 BOM so Excel opens non-ASCII correctly. Default `true`. */
bom?: boolean;
/** Line terminator. Default `"\r\n"` (RFC 4180). */
newline?: string;
}
DataSource
A pluggable data source. signal aborts a request the grid has superseded. Sort/filter ids are the column ids (map them to your DB fields server-side). Grouping and expansion are NOT part of the query — server-side grouping/aggregation is a separate concern, so enable enableGrouping only in client mode. The three file verbs (I3, Plan.md §2.2) are optional — implement them to move file storage server-side; bridge them to a files cell with createFileHandlers.
interface DataSource<TData> {
fetch(query: DataSourceQuery, signal?: AbortSignal): Promise<DataSourcePage<TData>>;
/**
* I3 — upload one picked file and return the stored {@link BstFileRef} to put in
* the row. Throw/reject to surface the failure (the cell keeps its busy state off).
*/
uploadFile?(file: File, ctx?: DataSourceFileContext, signal?: AbortSignal): Promise<BstFileRef>;
/** I3 — delete a stored file (runs before the cell removes it). */
deleteFile?(ref: BstFileRef, ctx?: DataSourceFileContext, signal?: AbortSignal): Promise<void>;
/** I3 — resolve a fresh (short-lived) view URL for a stored file — B5 thumbnails / preview. */
getFileUrl?(ref: BstFileRef, ctx?: DataSourceFileContext, signal?: AbortSignal): Promise<string>;
}
DataSourceFileContext
Where a file verb is acting — lets the server scope the upload/delete to a cell.
interface DataSourceFileContext {
/** Row the file belongs to (if any). */
rowId?: string;
/** Column the file belongs to (if any). */
columnId?: string;
}
DataSourceFilter
One per-column filter. value is a { op, value, value2? } condition or a bare value.
interface DataSourceFilter {
/** Column id. */
id: string;
/** The column-filter value (operator-aware condition, or a plain value → `contains`). */
value: unknown;
}
DataSourcePage
One page of results.
interface DataSourcePage<TData> {
/** The rows for this page. */
rows: TData[];
/** Total rows matching the query across ALL pages — drives the page count. */
totalCount: number;
/** Optional grand total BEFORE filtering (for an "X of Y — filtered from Z" UI). */
unfilteredCount?: number;
}
DataSourceQuery
The query a page request carries — everything the server needs to return one page.
interface DataSourceQuery {
/** Multi-column sort. */
sort: DataSourceSort[];
/** Per-column filters (E3 conditions). */
filters: DataSourceFilter[];
/** Global quick-filter text (the toolbar search box). */
quickFilter?: string;
/** Zero-based row offset of the requested page (`pageIndex * pageSize`). */
offset: number;
/** Page size (rows requested). */
limit: number;
}
DataSourceSort
DataSource — the client/server seam (Plan.md §2.2, §5). A grid runs identically over an in-memory array (tiers 1–2) or a server query (tier 3) by swapping the source; useBstDataSource drives it and puts the grid into TanStack manual mode (server does sort/filter/paginate). This is the foundation the artifact flagged as missing — the engine was client-only.
interface DataSourceSort {
/** Column id. */
id: string;
/** Descending? */
desc: boolean;
}
DsColumnFilter
interface DsColumnFilter {
id: string;
value: unknown;
}
DsPagination
interface DsPagination {
pageIndex: number;
pageSize: number;
}
DsSort
Minimal TanStack-compatible state shapes the grid controls.
interface DsSort {
id: string;
desc: boolean;
}
EditingOptions
Options for the Editing feature (Phase 2). Passing an object implies enabled (§12).
interface EditingOptions {
/**
* `'cell'` = commit each cell; `'row'` = deferred row session (C2 ≡ I2);
* `'batch'` = EVERY edit (typed or pasted) stays an unsaved draft until an
* explicit save — the mode behind the review-changes sheet + single `onSave`
* API call. Default `'cell'`.
*/
mode?: 'cell' | 'row' | 'batch';
/** When a committed value is saved. Default `['enter', 'blur']`. */
saveOn?: SaveTrigger | SaveTrigger[];
/** What happens when a commit is invalid. Default `'blockCommitOnError'`. */
policy?: CommitPolicy;
}
FieldError
A single validation finding for a cell or row (feeds L1–L3 error UI).
interface FieldError {
level: FieldErrorLevel;
message: string;
code?: string;
}
FieldFormat
Field-format presets (ERP / Frappe-style, B2/B1). A named validation + input-mask applied to a text or number cell through cellMeta.pattern, so the common identity / finance fields an ERP form needs — Aadhaar, PAN, GSTIN, IFSC, email, phone, PIN code, … — validate, mask and normalize consistently without hand-writing a meta.validate for each column.
{ id: 'aadhaar', meta: { type: 'number', editable: true, cellMeta: { pattern: 'aadhaar' } } } { id: 'pan', meta: { type: 'text', editable: true, cellMeta: { pattern: 'pan' } } }
cellMeta.pattern accepts a built-in name ('aadhaar'), a RegExp (with an optional cellMeta.patternMessage), or a custom FieldFormat object. Register your own reusable formats with defineFieldFormat or by adding to FIELD_FORMATS. Checksum validators (Aadhaar/Verhoeff, GSTIN) are exported for use outside the grid too.
interface FieldFormat {
/** Stable id. */
name: string;
/** Human label (docs / builder UIs). */
label: string;
/**
* Validate a **non-empty** value → an error message, or `null` when valid.
* Emptiness is the `required` check's job — return `null` for `''` here.
*/
validate?: (value: string) => string | null;
/** Format the stored value for display (the mask). Default: shown as-is. */
mask?: (value: string) => string;
/** Normalize raw editor input as the user types (uppercase / strip separators). */
normalize?: (raw: string) => string;
/** Placeholder for an empty editor. */
placeholder?: string;
/** `inputMode` hint for the editor (mobile keyboards). */
inputMode?: 'text' | 'decimal' | 'numeric' | 'tel' | 'email' | 'url' | 'search';
/** Max input length after normalization. */
maxLength?: number;
}
FilterCondition
A single column condition stored as the column filter value.
interface FilterCondition {
op: string;
value?: unknown;
value2?: unknown;
}
FilterConditionGroup
A compound of column conditions combined with and / or — the value a multi-filter column stores (X11), so several filter types (e.g. a text condition + a Set Filter) stack on ONE column. evalCondition / isConditionActive understand it, so it flows through the same bstCondition filterFn as a single condition. Slots are positional (one per filter part) and may be undefined (an empty part), so the stacked widgets keep a stable index.
interface FilterConditionGroup {
op: 'and' | 'or';
conditions: (FilterCondition | undefined)[];
}
FilterOperator
One selectable operator in the filter builder.
interface FilterOperator {
op: string;
label: string;
/** Number of value inputs: 0 = unary (is empty), 1 = default, 2 = between. */
arity?: 0 | 1 | 2;
}
FormatResult
The merged formatting for one cell or row.
interface FormatResult {
className?: string;
style?: React.CSSProperties;
hideContent?: boolean;
}
IconProps
Engine-body icon system. The neutral <BstTable> body, the filter / format builders and the file & boolean cells render injectable icons instead of emoji/Unicode glyphs. Defaults are dependency-free inline SVGs drawn in the lucide idiom (24×24 viewBox, currentColor, 2px round strokes), skin-neutral so both the MUI and shadcn adapters can override any slot with their own set.
interface IconProps {
/** Icon size (width = height): px number or a CSS length. Body call-sites pass a number. */
size?: number | string;
/** Extra class on the rendered `<svg>`. */
className?: string;
}
InteractionState
The interaction store (Plan.md §2.5 rule 4). All high-frequency edit / validation / selection state lives here — NOT in table.setState — so a keystroke or an active-cell move never re-runs the whole row model. It is a tiny, framework-agnostic external store consumed via useSyncExternalStore. Live keystrokes are held in the active editor's local React state and only land here on commit, so store mutations are coarse (commit / cancel / selection / validation) and never per-character.
interface InteractionState {
/** The cell currently in inline edit mode, or `null`. */
editingCell: {
rowId: string;
columnId: string;
} | null;
/**
* Initial draft for a *type-to-edit* session (`enableTypeToEdit`) — set when a
* printable keystroke opens the editor so the first character overwrites the
* old value (Excel-style). The editor host reads it once at mount, only for the
* matching cell (single-cell edits, never a row session). `null` = no seed.
*/
editSeed: {
rowId: string;
columnId: string;
value: unknown;
} | null;
/** The row currently in a deferred row-edit session, or `null` (C2 ≡ I2). */
rowSession: string | null;
/**
* The focused ("active") cell — the moving end of a range + keyboard-nav
* cursor (Phase 3). `null` when nothing is selected.
*/
activeCell: CellRef | null;
/**
* The fixed end ("anchor") of a range selection. Equal to `activeCell` when
* the selection is a single cell; the rectangle between the two is
* materialised lazily at paint (§2.4 — "rectangles materialized at paint").
*/
anchorCell: CellRef | null;
/**
* Marks the current selection as a *whole* column or row (Ctrl+Space /
* Shift+Space, or a "copy column" action). Copy then grabs the ENTIRE column
* across all pages (or the whole row), not just the on-screen rectangle
* (H3 copy-column / H2 copy-row under pagination). Cleared by any cell-level
* selection change.
*/
wholeSelect: {
type:
// …truncated
MoveActiveOptions
Options for moveActive — how the keyboard cursor should step.
interface MoveActiveOptions {
/** Keep the anchor and only move the focus end (Shift+Arrow range grow). */
extend?: boolean;
/** Wrap to the previous/next row when stepping off a row edge (Tab). */
wrap?: boolean;
/** Jump to the first/last row/column in the step direction (Home/End). */
toEdge?: boolean;
}
PdfThumbnailerOptions
Extra rendering scale on top of fit + devicePixelRatio (crispness). Default 1.5.
interface PdfThumbnailerOptions {
/** Extra rendering scale on top of fit + devicePixelRatio (crispness). Default 1.5. */
scale?: number;
/** Cache rendered thumbnails by URL. Default true. */
cache?: boolean;
/** Canvas factory (test seam). Default `document.createElement('canvas')`. */
createCanvas?: () => HTMLCanvasElement;
}
PrintOptions
Options for buildPrintHtml.
interface PrintOptions {
/** Document `<title>` + heading. Default `"Table"`. */
title?: string;
/** Include the header row. Default `true`. */
includeHeaders?: boolean;
}
QrMatrix
Side length in modules.
interface QrMatrix {
/** Side length in modules. */
size: number;
/** `modules[row][col]` — true = dark. */
modules: boolean[][];
}
ResolvedShortcut
interface ResolvedShortcut {
keys: string[];
label: string;
}
ResolvedShortcutGroup
interface ResolvedShortcutGroup {
category: ShortcutCategory;
items: ResolvedShortcut[];
}
ResolvedStickyHeader
The resolved, always-defined sticky-header config the renderer reads.
interface ResolvedStickyHeader {
/** The viewport is bounded + the header is pinned. */
enabled: boolean;
/** CSS `max-height` value for the scroll box (e.g. `'440px'`), when enabled. */
maxHeight?: string;
}
ResolvedVirtualization
The resolved, always-defined virtualization config the renderer reads.
interface ResolvedVirtualization {
/** Row virtualization requested. */
enabled: boolean;
/** Column virtualization requested (only meaningful when `enabled`). */
columns: boolean;
overscan: number;
estimateRowSize: number;
estimateColumnSize: number;
}
RuntimeCtx
Everything the runtime needs from the current render, refreshed each render by the hook. Keeping it behind a ref means the stable runtime methods always read fresh data / options without the runtime object itself changing identity.
interface RuntimeCtx<TData extends RowData> {
registry: CellTypeRegistry;
data: TData[];
rowIndexById: Map<string, number>;
getRowId: (row: TData, index: number) => string;
metaByColumn: Map<string, BstColumnMeta<TData>>;
fieldByColumn: Map<string, string>;
/** `columnId → header text` (string header, else the column id) — for export. */
headerByColumn: Map<string, string>;
columnIds: string[];
/** Row ids in current visual (post sort/filter/pagination) order. */
visibleRowIds: string[];
/** Row ids after filter + sort but BEFORE pagination — the full column for
* copy-column (H3), which must span every page, not just the visible one. */
allRowIds: string[];
/** Visible leaf column ids in current left-to-right order. */
visibleColumnIds: string[];
/** `rowId → visual row index` for the current render. */
rowVisualIndex: Map<string, number>;
/** `columnId → visual column index` for the current render. */
colVisualIndex: Map<string, number>;
enableEditing: boolean;
enableValidation: boolean;
enableCellSelection: boolean;
enableClipboard: boolean;
/** Column-copy (H3) enabled. `false` disables `selectColumn`/`copyColumn`. Default: true. */
enableCopyColumn?: boolean;
/** Row-copy (H2) enabled. `false` disables `selectRow`/`copyRow`. Default: true. */
enableCopyRow?: boolean;
enableUndoRedo: boolean;
/** Find is enabled (search box + highlight + jump). */
enableFind?: boolean;
/** Find matches case-sensitively. Default: false. */
findCaseS
// …truncated
SpanCol
Minimal column shape the planner needs.
interface SpanCol {
id: string;
meta: BstColumnMeta;
}
SpanPlan
The resolved span plan for the current page.
interface SpanPlan {
/** Origin cell key → its span (colSpan/rowSpan ≥ 1). */
origin: Map<string, {
colSpan: number;
rowSpan: number;
}>;
/** Keys of cells hidden under an origin's block — never rendered. */
covered: Set<string>;
}
SpanRow
Minimal row shape the planner needs (adapts a TanStack row).
interface SpanRow {
id: string;
original: unknown;
getValue: (columnId: string) => unknown;
}
Store
interface Store<S> {
getState: () => S;
setState: (updater: S | ((prev: S) => S)) => void;
subscribe: (listener: () => void) => () => void;
}
ToolbarItemWidth
Higher = more important = kept inline longer (collapses last).
interface ToolbarItemWidth {
id: string;
/** Higher = more important = kept inline longer (collapses last). */
priority: number;
width: number;
}
UseBstDataSourceOptions
Initial page size. Default 10.
interface UseBstDataSourceOptions {
/** Initial page size. Default 10. */
pageSize?: number;
initialSorting?: DsSort[];
initialColumnFilters?: DsColumnFilter[];
initialGlobalFilter?: string;
/**
* Debounce (ms) applied to **filter / quick-filter** changes so typing doesn't
* fire a request per keystroke. Sort + pagination changes fetch immediately.
* Default 300. Set `0` to disable.
*/
debounceMs?: number;
/**
* Identity of the source. Change it to force a refetch when you swap to a
* genuinely different source (endpoint / tenant / dataset). Re-creating the
* *same* logical source each render (without changing this) will NOT refetch —
* which is what prevents an unmemoized source from looping.
*/
sourceKey?: string | number;
}
UseBstInfiniteDataSourceOptions
Rows fetched per window (the scroll increment). Default 50.
interface UseBstInfiniteDataSourceOptions {
/** Rows fetched per window (the scroll increment). Default 50. */
pageSize?: number;
/** Debounce (ms) on filter / quick-filter changes before the reset fetch. Default 300. */
debounceMs?: number;
initialSorting?: DsSort[];
initialColumnFilters?: DsColumnFilter[];
initialGlobalFilter?: string;
/** Change to force a full reset when you swap to a genuinely different source. */
sourceKey?: string | number;
}
UseBstTableOptions
Stable row identity — required for editing/selection; strongly recommended always.
interface UseBstTableOptions<TData extends RowData> extends BstTableEngineToggles {
data: TData[];
columns: BstTableColumn<TData>[];
/** Stable row identity — required for editing/selection; strongly recommended always. */
getRowId?: (row: TData, index: number) => string;
/** Extra initial state merged over engine defaults (sorting, filters, …). */
initialState?: Record<string, unknown>;
/** Initial or controlled cell notes map, keyed by cell key (`${rowId}:${columnId}`). */
notes?: Record<string, string>;
/** Fired whenever the cell notes collection changes. */
onNotesChange?: (notes: Record<string, string>) => void;
/** Fired when an individual cell note is saved or deleted. */
onNoteSave?: (event: BstNoteSaveEvent) => void;
/** Sort server-side — the grid renders `data` as-is (no client sort). */
manualSorting?: boolean;
/** Filter server-side — the grid renders `data` as-is (no client filter). */
manualFiltering?: boolean;
/** Paginate server-side — `data` is the current page; set `rowCount`. */
manualPagination?: boolean;
/** Group server-side. */
manualGrouping?: boolean;
/** Total rows across all pages (server mode) — drives the page count. */
rowCount?: number;
/** Explicit page count (alternative to `rowCount`). */
pageCount?: number;
/** Disable TanStack's automatic page-index reset (server mode manages it). */
autoResetPageIndex?: boolean;
/** Controlled table state (partial) — `{ sorting, columnFilters, globalFilter, pagination }`. */
state?: Re
// …truncated
ValidationOptions
Options for the Validation feature (Phase 2).
interface ValidationOptions {
policy?: CommitPolicy;
}
VirtualizationCompat
Features whose DOM shape a windowed body can't represent faithfully in v1 — multi-<tr> items (master-detail, grouping) or spans/pins that must know rows outside the window (cell spanning, row pinning). When any is active alongside virtualization, the renderer falls back to the un-windowed path so the layout stays correct (these target small, curated datasets — not the large flat data virtualization is for). Keyed so callers can log which one caused the bypass.
interface VirtualizationCompat {
enableExpanding?: boolean;
enableGrouping?: boolean;
enableCellSpanning?: boolean;
enableRowPinning?: boolean;
}
VirtualizationOptions
Virtualization (D1) — the render-layer companion that lets the grid paint only the rows (and, optionally, columns) inside the scroll viewport, so a 10k / 1M row dataset stays at 60fps with a bounded DOM. Built on @tanstack/react-virtual (MIT). This module holds only the pure resolve + compatibility helpers; the useVirtualizer wiring lives in BstTable.tsx (it needs the live table + refs). Opt-in per §12 (enableVirtualization), off by default: it retrofits into a grid whose other features assume the whole row model is in the DOM, so a handful of structurally-incompatible features (below) make it yield rather than corrupt the layout — see virtualizationBypassReason.
interface VirtualizationOptions {
/** Rows/columns rendered beyond each edge of the viewport (smoother scroll, more DOM). Default 8. */
overscan?: number;
/** Estimated row height in px before a row is measured. Keep close to the real height. Default 36. */
estimateRowSize?: number;
/** Estimated column width in px for column virtualization (real widths are used once known). Default 150. */
estimateColumnSize?: number;
}
VisualIndex
A visual (paint-order) coordinate for a cell — row + column index.
interface VisualIndex {
r: number;
c: number;
}
XlsxOptions
Options for toXlsx.
interface XlsxOptions {
/** Include the header row (bold). Default `true`. */
includeHeaders?: boolean;
/** Worksheet tab name. Default `"Sheet1"` (sanitised, ≤31 chars). */
sheetName?: string;
}