{
  "$comment": "Generated by scripts/api-manifest.mjs from the TypeScript sources. Do not edit.",
  "package": "bo-grid",
  "version": "3.1.0",
  "grid": {
    "description": "Props of <Grid> (GridProps).",
    "props": [
      {
        "name": "rows",
        "type": "GridRow[]",
        "required": true,
        "doc": "The data: one plain object per row, each with a unique `id`, a number or a string (or name another key with `getRowId`).",
        "example": "let rows: GridRow[] = $state([\n  { id: 1, sym: 'VNM', last: 68.5 },\n  { id: 2, sym: 'FPT', last: 121.3 },\n]);"
      },
      {
        "name": "columns",
        "type": "ColumnDef[]",
        "required": true,
        "doc": "The columns, in display order. A column's `type` picks how its values show, sort and filter.",
        "example": "const columns: ColumnDef[] = [\n  { type: 'text', key: 'sym', header: 'Symbol', width: 90 },\n  { type: 'price', key: 'last', header: 'Last', flash: 'auto' },\n];"
      },
      {
        "name": "height",
        "type": "number | string",
        "required": true,
        "doc": "Viewport height. A **number** is the scroll viewport's pixel height (the grid's total height is that plus header/toolbar/footer chrome). A **CSS string** (e.g. `'100%'`, `'80vh'`, `'480px'`) sizes the whole grid element and the viewport auto-fits the space left after the chrome — give the grid a sized parent (or use `'100%'` inside a flex/grid cell).",
        "example": "<Grid {rows} {columns} height=\"100%\" />"
      },
      {
        "name": "rowHeight",
        "type": "number | ((row: GridRow, index: number) => number)",
        "required": false,
        "doc": "Row height in px (uniform), or a function for variable heights (in-memory mode only).",
        "default": "36",
        "example": "<Grid {rows} {columns} height={480} rowHeight={(row) => (row.note ? 56 : 36)} />"
      },
      {
        "name": "theme",
        "type": "'dark' | 'light' | GridTheme",
        "required": false,
        "doc": "Built-in theme name, or a token map (a preset such as `midnightTheme`, or your own, spread over one).",
        "default": "'dark'",
        "example": "<Grid {rows} {columns} height={480} theme={{ ...lightTheme, up: '#0a7f4f' }} />"
      },
      {
        "name": "resizable",
        "type": "boolean",
        "required": false,
        "doc": "Allow drag-to-resize column widths. A drag (or an autosize) changes that column only: the others keep the width they have on screen, flex columns included, so the ones after it move along. Double-click a grip to give that column back its own width; Reset widths in the column menu gives every column back its own. Opt out per column with `resizable: false`.",
        "default": "true"
      },
      {
        "name": "cellSelection",
        "type": "boolean",
        "required": false,
        "doc": "Highlight cells/ranges on click-drag (the blue selection fill + focus ring + fill handle). Set false for a read-only/display grid where clicks should pass straight through to `onRowClick`/`onCellClick` without a selection highlight.",
        "default": "true"
      },
      {
        "name": "rowSelection",
        "type": "boolean",
        "required": false,
        "doc": "Show a leading checkbox column for whole-row selection (keyed by row id, stable across sort/filter).",
        "default": "false"
      },
      {
        "name": "onRowSelectionChange",
        "type": "(selectedIds: Array<string | number>) => void",
        "required": false,
        "doc": "Called with the selected row ids whenever the row-selection set changes.",
        "example": "<Grid {rows} {columns} height={480} rowSelection onRowSelectionChange={(ids) => (selected = ids)} />"
      },
      {
        "name": "hiddenColumns",
        "type": "string[]",
        "required": false,
        "doc": "Column keys to hide (controlled). Build your own column-picker UI and drive this prop — the grid stays presentation-only. Composed (union) with columns the user hides at runtime via the column menu.",
        "default": "[]",
        "example": "<Grid {rows} {columns} height={480} hiddenColumns={showVolume ? [] : ['vol']} />"
      },
      {
        "name": "onColumnVisibilityChange",
        "type": "(hidden: string[]) => void",
        "required": false,
        "doc": "Called with all currently-hidden column keys whenever the runtime set changes (column menu hide/show).",
        "example": "<Grid {rows} {columns} height={480} columnMenu onColumnVisibilityChange={(hidden) => save('hidden', hidden)} />"
      },
      {
        "name": "columnMenu",
        "type": "boolean",
        "required": false,
        "doc": "Enable a per-column header menu (a ⋮ trigger) with sort, hide and (with `filterMenu`) filter actions.",
        "default": "false"
      },
      {
        "name": "columnsPanel",
        "type": "boolean",
        "required": false,
        "doc": "Show a \"Columns\" button that opens a panel to toggle column visibility (the place to restore columns hidden via the menu). Lazy-loaded.",
        "default": "false"
      },
      {
        "name": "virtualizeColumns",
        "type": "boolean",
        "required": false,
        "doc": "Render only the columns within the horizontal scroll window (+ overscan) for very wide grids (100+ columns). Forces fixed-width horizontal scroll; pinned columns always render. Off, the columns fit the grid's width.",
        "default": "false"
      },
      {
        "name": "rowClass",
        "type": "(row: GridRow) => string | undefined",
        "required": false,
        "doc": "Return extra CSS class(es) for a data row (e.g. to colour by value). Style them via `:global(.your-class)` since rows live inside the grid.",
        "example": "<Grid {rows} {columns} height={480} rowClass={(row) => (row.status === 'Rejected' ? 'rejected' : undefined)} />"
      },
      {
        "name": "getRowId",
        "type": "(row: GridRow) => string | number",
        "required": false,
        "doc": "Identity key for rows: selection, focus, pins and flashes follow it. Override for string/UUID/composite keys.",
        "default": "(row) => row.id",
        "example": "<Grid {rows} {columns} height={480} getRowId={(row) => `${row.account}:${row.sym}`} />"
      },
      {
        "name": "selectedRowId",
        "type": "string | number | null",
        "required": false,
        "doc": "Controlled \"active row\" highlight (keyed by `getRowId`) — for master-detail / list-detail where one row is selected. Independent of the checkbox `rowSelection` and the cell range selection. `null` = none.",
        "default": "null"
      },
      {
        "name": "onRowClick",
        "type": "(row: GridRow, event: MouseEvent | KeyboardEvent) => void",
        "required": false,
        "doc": "Called when a data row is activated by click or Enter (open a detail view, navigate, …). Edit-input and checkbox clicks are excluded.",
        "example": "<Grid {rows} {columns} height={480} {selectedRowId} onRowClick={(row) => (selectedRowId = row.id)} />"
      },
      {
        "name": "hotkeys",
        "type": "Hotkeys",
        "required": false,
        "doc": "Keys for the app, run before the grid's own. A binding is a key (`'b'`, `'Delete'`, `'Space'`; a symbol by the character it types: `'?'`, not `'Shift+/'`), after any of `Shift+`, `Alt+`, `Ctrl+`, `Meta+` and `Mod+` (Ctrl or ⌘, either). Case does not matter. The handler is given the focused row and column (null when there is none) and the event; return false to leave the key to the grid. Keys go to the grid, not to a binding, while a cell is edited or an input, button or menu inside the grid has them, and a key a binding took runs it once when held down. A binding that cannot be read, or binds a key twice, throws (BG1001–BG1004).",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480}\n  hotkeys={{ b: ({ row }) => { if (row) buy(row); }, 'Shift+C': cancelAll, Delete: ({ row }) => { if (row) cancel(row); } }} />"
      },
      {
        "name": "sort",
        "type": "SortState[]",
        "required": false,
        "doc": "Controlled sort order (multi-key, primary first). When set, the grid reflects this and reports changes via `onSortChange` instead of holding its own state. Omit for uncontrolled sorting.",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480} {sort} onSortChange={(next) => (sort = next)} />"
      },
      {
        "name": "onSortChange",
        "type": "(sort: SortState[]) => void",
        "required": false,
        "doc": "Called with the new sort order whenever a header is clicked.",
        "example": "<Grid {rows} {columns} height={480} onSortChange={(sort) => save('sort', sort)} />"
      },
      {
        "name": "columnFilters",
        "type": "Record<string, AnyFilter>",
        "required": false,
        "doc": "Controlled column filters (keyed by column key). When set, the grid reflects these and reports changes via `onFilterChange` instead of holding its own. Omit for uncontrolled filtering.",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480} filterMenu\n  columnFilters={{ sym: { kind: 'text', op: 'starts', q: 'V' }, last: { kind: 'number', op: 'gt', a: 50 } }} />"
      },
      {
        "name": "onFilterChange",
        "type": "(filters: Record<string, AnyFilter>) => void",
        "required": false,
        "doc": "Called with the full column-filter map whenever a header filter changes.",
        "example": "<Grid {rows} {columns} height={480} filterMenu {columnFilters} onFilterChange={(next) => (columnFilters = next)} />"
      },
      {
        "name": "footer",
        "type": "boolean",
        "required": false,
        "doc": "Show a pinned totals row: each column with a `groupAgg` shows that aggregate over all (filtered) rows. In-memory mode only.",
        "default": "false"
      },
      {
        "name": "onCellClick",
        "type": "( info: { row: GridRow; column: ColumnDef; value: unknown }, event: MouseEvent, ) => void",
        "required": false,
        "doc": "Called when a cell is clicked, with its row, column and value. Fires in addition to `onRowClick`; excluded for the edit input.",
        "example": "<Grid {rows} {columns} height={480} onCellClick={({ row, column }) => column.key === 'sym' && openChart(row.sym)} />"
      },
      {
        "name": "pinnedRows",
        "type": "GridRow[]",
        "required": false,
        "doc": "Rows pinned to the top, always visible above the scroll (a benchmark, a summary, \"your position\"). Display-only — not virtualized or selectable. Their `getRowId` must not collide with a row in `rows`/`source` — with a derived-flash column (`flash: 'auto'`), a shared id means the pinned cell and the data-row cell share one flash-tracker entry, so updating one can suppress or trigger a flash on the other. To let users pin rows of `rows` themselves, use `rowPinning`.",
        "default": "[]",
        "example": "<Grid {rows} {columns} height={480} pinnedRows={[{ id: -1, sym: 'VN30', last: index }]} />"
      },
      {
        "name": "filterRow",
        "type": "boolean",
        "required": false,
        "doc": "Show a per-column filter input row under the header. Rows must match every non-empty column filter (AND). In-memory mode only.",
        "default": "false"
      },
      {
        "name": "filterMenu",
        "type": "boolean",
        "required": false,
        "doc": "Enable a per-column header filter menu (lazy-loaded on first open). Each filterable column shows a funnel; the menu's control matches the column type (text/number/date). Override or disable per column with `col.filter`. Works in source mode too (filters are delegated to the `RowSource`); set filters need in-memory data.",
        "default": "false"
      },
      {
        "name": "quickFilter",
        "type": "boolean",
        "required": false,
        "doc": "Show a built-in quick-filter search box above the grid that matches across all column values (ANDed with the `filter` prop). With a `source`, its text is sent to `getRows` as `quickFilter`.",
        "default": "false"
      },
      {
        "name": "fillHandle",
        "type": "boolean",
        "required": false,
        "doc": "Show an Excel-style fill handle at the selection's bottom-right corner; drag it to copy the selected value(s) across the extended range (editable columns only). In-memory mode only.",
        "default": "false"
      },
      {
        "name": "emptyMessage",
        "type": "string",
        "required": false,
        "doc": "Message shown when there are no rows.",
        "default": "labels.noRows"
      },
      {
        "name": "labels",
        "type": "Partial<GridLabels>",
        "required": false,
        "doc": "Override any string the grid renders itself (menus, filter editor, pager, aria labels). Merged over English defaults — pass only what changes. Interpolated entries are functions so translations can reorder the parts. Resolved per grid.",
        "default": "{}",
        "example": "<Grid {rows} {columns} height={480} labels={{ noRows: 'Không có dữ liệu', sortAscending: 'Tăng dần' }} />"
      },
      {
        "name": "defaultColumn",
        "type": "DefaultColumn",
        "required": false,
        "doc": "Settings applied under every column — its own fields (and a registered `cellType`) win.",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480} defaultColumn={{ minWidth: 72, sortable: false }} />"
      },
      {
        "name": "cellTypes",
        "type": "Record<string, CellTypeDef>",
        "required": false,
        "doc": "Reusable column types, by name. A column with `cellType: 'name'` gets that entry's defaults (format, compare, align, filter, width, a `component` renderer…) under its own fields, and the built-in type named by `extends` for sorting, filtering and export.",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480}\n  cellTypes={{ qty: { extends: 'number', decimals: 0, width: 80, format: (v) => Number(v).toLocaleString() } }} />"
      },
      {
        "name": "filterTypes",
        "type": "FilterTypes",
        "required": false,
        "doc": "Custom filter kinds, by name. A column selects one with `filter: 'name'`; the header filter menu draws its `component` and the view keeps rows for which `test` passes. An unregistered kind filters nothing.",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480} filterMenu\n  filterTypes={{ band: { component: BandFilter, test: (v, f) => Number(v) >= f.min && Number(v) <= f.max } }} />"
      },
      {
        "name": "locale",
        "type": "string",
        "required": false,
        "doc": "BCP 47 locale for the grid's own number formatting (aggregation bar, pager row count), and the default for each column's `locale`. Unset, the runtime's locale.",
        "default": "undefined"
      },
      {
        "name": "onReady",
        "type": "(api: GridApi) => void",
        "required": false,
        "doc": "Called once, when the grid mounts, with a handle for imperative actions: `scrollToRow`, `focusCell`, `patchRows`, `getSelectedRows`, `autosizeColumns`, `exportCSV`, `getState` / `applyState` and more.",
        "example": "<Grid {rows} {columns} height={480} onReady={(a) => (api = a)} />"
      },
      {
        "name": "onViewportChange",
        "type": "(view: ViewportRange) => void",
        "required": false,
        "doc": "Called with the data rows on screen — the rows in view plus the small buffer rendered around them — whenever that set changes (scroll, sort, filter, new data), never for a tick that only changes values. Pass it `createViewportSubscriptions(...).update` from `bo-grid/realtime` to stream just the symbols in view.",
        "example": "<Grid {rows} {columns} height={480} onViewportChange={subs.update} />"
      },
      {
        "name": "flashMotion",
        "type": "'fade' | 'hold' | 'auto'",
        "required": false,
        "doc": "How a flash plays. `'fade'`: the tint fades out. `'hold'`: the tint holds for the flash window, then drops — no CSS animation, so a busy board restyles and repaints a flashing cell twice instead of every frame. `'auto'`: fade while few cells flash at once, hold when many do. A flash that lights whole rows (`flashNewRows`, `api.flashCells`) counts each cell it lights.",
        "default": "'fade'"
      },
      {
        "name": "liveSort",
        "type": "boolean | number",
        "required": false,
        "doc": "Keep the sort and filters current while `api.patchRows` changes values in place. When a patch changes a field the grid sorts or filters by, the rows it changed are merged back into place at most every `liveSort` ms (`true`: 500), instead of the whole view being re-sorted. The selection stays on the same rows; nothing moves while a cell is being edited. Off, rows hold their place until `api.refresh()`. It follows `patchRows`; values written into rows any other way still need `api.refresh()`.",
        "default": "false",
        "example": "<Grid {rows} {columns} height={480} liveSort={250} />"
      },
      {
        "name": "pauseOnHover",
        "type": "boolean",
        "required": false,
        "doc": "While the pointer is over the rows, `liveSort` waits, so a row does not move just as it is clicked; the rows re-sort once the pointer leaves (or the window loses focus, or the tab is hidden). Mouse and pen only. Set false for a screen nobody points at.",
        "default": "true"
      },
      {
        "name": "staleAfter",
        "type": "number",
        "required": false,
        "doc": "Dim a row when nothing new has come for it for this many ms: a quote whose feed went quiet. News is a patch (`api.patchRows`) that changed the row, or a new object for it in `rows` (the row arrived, or a copy replaced it); a patch that re-sends the values the row already has is not. The row comes back the moment it ticks. Rows already there count from mount, or from when `staleAfter` is turned on. Dimmed with `opacity: var(--bo-stale-opacity, 0.5)`. `0` turns it off.",
        "default": "0"
      },
      {
        "name": "flashNewRows",
        "type": "boolean | number",
        "required": false,
        "doc": "Flash a row once when it arrives in `rows` (a new order, a new fill): every cell, in the neutral flash colour, for this many ms (`true`: 600). A row arrives when its id is new; a copy of a row already there does not flash, nor do the rows an empty grid gets (a load or a reload), nor a wholesale replacement (more than 64 rows, and more than an eighth of them, at once). Rows on screen only, as with every flash.",
        "default": "false",
        "example": "<Grid {rows} {columns} height={480} flashNewRows={900} />"
      },
      {
        "name": "columnHover",
        "type": "boolean",
        "required": false,
        "doc": "Highlight the column under the pointer (body and header) — handy on a wide board. One overlay for the whole column, not a class per cell.",
        "default": "false"
      },
      {
        "name": "rowNumbers",
        "type": "boolean",
        "required": false,
        "doc": "A leading row-number column (1, 2, 3… in view order; data rows only), as in a spreadsheet. Its cells are row headers, so screen readers announce the number with each row.",
        "default": "false"
      },
      {
        "name": "rowPinning",
        "type": "boolean",
        "required": false,
        "doc": "Let users pin rows above the scroll from the row menu (right-click): \"Pin to top\" / \"Unpin\". A pinned row stays in the body too, keeps updating, and is remembered with `persistKey` and in `getState()`.",
        "default": "false"
      },
      {
        "name": "findBar",
        "type": "boolean",
        "required": false,
        "doc": "Ctrl/⌘+F in the grid opens a find bar: it matches the cells' displayed text (case-insensitive) and steps through matches with Enter / Shift+Enter, focusing and scrolling to each. Escape closes it. Off by default, as it takes Ctrl+F from the browser while the grid has focus. In-memory rows only.",
        "default": "false"
      },
      {
        "name": "fitColumns",
        "type": "boolean",
        "required": false,
        "doc": "Scale every column with the grid, in proportion to its `width`, instead of only `flex` columns taking up the change: the grid stays responsive without hand-tuned flex weights. A column never gets narrower than its `minWidth` (64 px, or its own width if smaller, without one) nor wider than its `maxWidth`; when the floors no longer fit, the grid scrolls sideways. It fits until the user sets a width (a drag, an autosize): from then on the columns keep the widths they were given, until Reset widths in the column menu.",
        "default": "false"
      },
      {
        "name": "onRowPinChange",
        "type": "(ids: (string | number)[]) => void",
        "required": false,
        "doc": "Called with the pinned row ids (in pin order) whenever they change.",
        "example": "<Grid {rows} {columns} height={480} rowPinning onRowPinChange={(ids) => save('pinned', ids)} />"
      },
      {
        "name": "loading",
        "type": "boolean",
        "required": false,
        "doc": "Show a loading overlay over the grid (for consumer-driven async work in in-memory mode; source mode shows skeleton rows automatically).",
        "default": "false"
      },
      {
        "name": "rowMenu",
        "type": "(row: GridRow) => Array<{ label: string; onSelect: () => void }>",
        "required": false,
        "doc": "Right-click row menu. Return the items for a row; an empty array shows no menu. Each item runs `onSelect` and closes the menu.",
        "example": "<Grid {rows} {columns} height={480}\n  rowMenu={(row) => [{ label: 'Cancel order', onSelect: () => cancel(row) }, { label: 'Copy id', onSelect: () => copy(String(row.id)) }]} />"
      },
      {
        "name": "detail",
        "type": "Snippet<[{ row: GridRow }]>",
        "required": false,
        "doc": "Master-detail: render an expandable detail panel under a row. Adds a leading expand-toggle column. In-memory mode only (overrides rowHeight).",
        "example": "<Grid {rows} {columns} height={480}>\n  {#snippet detail({ row })}<p>{row.sym}: {row.note}</p>{/snippet}\n</Grid>"
      },
      {
        "name": "detailHeight",
        "type": "number",
        "required": false,
        "doc": "Height (px) of the expanded detail panel.",
        "default": "160"
      },
      {
        "name": "getChildren",
        "type": "(row: GridRow) => GridRow[] | undefined",
        "required": false,
        "doc": "Tree data: return a row's children (undefined/empty = leaf). When set, `rows` are the roots; the grid renders an indented, expandable tree. In-memory mode; filter/sort/group/paginate are not applied to the tree.",
        "example": "<Grid rows={roots} {columns} height={480} getChildren={(row) => row.children as GridRow[] | undefined} />"
      },
      {
        "name": "loadChildren",
        "type": "(row: GridRow) => Promise<GridRow[]>",
        "required": false,
        "doc": "Async tree data: load a row's children on expand (server-backed trees). Returns a promise; the grid shows a loading row, then caches the result. Pair with `hasChildren` (a cheap predicate) so chevrons show without loading. Use instead of `getChildren`. In-memory roots.",
        "example": "<Grid rows={roots} {columns} height={480}\n  loadChildren={(row) => fetch(`/api/accounts/${row.id}/children`).then((r) => r.json())}\n  hasChildren={(row) => Number(row.childCount) > 0} />"
      },
      {
        "name": "hasChildren",
        "type": "(row: GridRow) => boolean",
        "required": false,
        "doc": "Cheap predicate for whether a row has children (drives the expand chevron without loading). Required with `loadChildren`; optional with `getChildren`.",
        "example": "<Grid rows={roots} {columns} height={480} {loadChildren} hasChildren={(row) => row.kind === 'folder'} />"
      },
      {
        "name": "lazyGroups",
        "type": "LazyGroup[]",
        "required": false,
        "doc": "Server-side grouping: top-level group summaries (header data, no leaf rows). Each group's rows load on expand via `loadGroup`. In-memory mode.",
        "default": "undefined",
        "example": "<Grid rows={[]} {columns} height={480}\n  lazyGroups={[{ key: 'HOSE', count: 412 }, { key: 'HNX', count: 330 }]}\n  loadGroup={(key) => fetch(`/api/board/${key}`).then((r) => r.json())} />"
      },
      {
        "name": "loadGroup",
        "type": "(key: string) => Promise<GridRow[]>",
        "required": false,
        "doc": "Load a lazy group's leaf rows on expand (returns a promise; shows a loading row, then caches). Required with `lazyGroups`.",
        "example": "<Grid rows={[]} {columns} height={480} {lazyGroups} loadGroup={(key) => loadExchange(key)} />"
      },
      {
        "name": "onRowReorder",
        "type": "(fromIndex: number, toIndex: number) => void",
        "required": false,
        "doc": "Enable drag-to-reorder rows via a handle in the first column. Called with the from/to indices on drop — reorder your own `rows` in here. The indices are positions in the rows as shown, so they match `rows` while the list is neither sorted nor filtered. Off for grouped, paged, tree and `source` grids.",
        "example": "<Grid {rows} {columns} height={480}\n  onRowReorder={(from, to) => { const next = [...rows]; next.splice(to, 0, ...next.splice(from, 1)); rows = next; }} />"
      },
      {
        "name": "pageSize",
        "type": "number",
        "required": false,
        "doc": "Rows per page. When > 0 (in-memory mode), shows a pager instead of one long scroll; rows still virtualize within a page. `0` turns paging off.",
        "default": "0"
      },
      {
        "name": "pageSizeOptions",
        "type": "number[]",
        "required": false,
        "doc": "Page-size choices for a dropdown in the pager. Picking one re-pages from page 0. Uncontrolled unless you also drive `pageSize` and update it from `onPageSizeChange`.",
        "default": "undefined",
        "example": "<Grid {rows} {columns} height={480} pageSize={50} pageSizeOptions={[25, 50, 100]} />"
      },
      {
        "name": "onPageSizeChange",
        "type": "(size: number) => void",
        "required": false,
        "doc": "Called with the new page size when the pager's size dropdown changes.",
        "example": "<Grid {rows} {columns} height={480} {pageSize} pageSizeOptions={[25, 50, 100]} onPageSizeChange={(n) => (pageSize = n)} />"
      },
      {
        "name": "page",
        "type": "number",
        "required": false,
        "doc": "Controlled current page (0-based). Omit for uncontrolled paging.",
        "default": "undefined"
      },
      {
        "name": "onPageChange",
        "type": "(page: number) => void",
        "required": false,
        "doc": "Called with the new page index when the pager is used.",
        "example": "<Grid {rows} {columns} height={480} pageSize={50} {page} onPageChange={(p) => (page = p)} />"
      },
      {
        "name": "onColumnReorder",
        "type": "(keys: string[]) => void",
        "required": false,
        "doc": "Called with the new column-key order after a header drag-reorder.",
        "example": "<Grid {rows} {columns} height={480} onColumnReorder={(keys) => save('order', keys)} />"
      },
      {
        "name": "onColumnResize",
        "type": "(key: string, width: number) => void",
        "required": false,
        "doc": "Called with a column key + new width after a drag-resize. Only the dragged column is reported; `getState()` has every width.",
        "example": "<Grid {rows} {columns} height={480} onColumnResize={(key, width) => save(`width:${key}`, width)} />"
      },
      {
        "name": "ariaLabel",
        "type": "string",
        "required": false,
        "doc": "Accessible name for the grid (`aria-label` on the `role=\"grid\"` root).",
        "default": "undefined"
      },
      {
        "name": "filter",
        "type": "string",
        "required": false,
        "doc": "Quick-search text: a row is kept when any column's value, as text, contains it (case-insensitive). Bind it to your own search input.",
        "default": "''"
      },
      {
        "name": "groupBy",
        "type": "string[]",
        "required": false,
        "doc": "Column keys to group rows by, outermost first (nested groups with subtotals). In-memory mode only.",
        "default": "[]",
        "example": "<Grid {rows} {columns} height={480} groupBy={['account', 'sym']} />"
      },
      {
        "name": "aggregations",
        "type": "AggKind[]",
        "required": false,
        "doc": "Which stats the selection footer shows over a multi-cell range.",
        "default": "['sum', 'avg', 'count', 'min', 'max']",
        "example": "<Grid {rows} {columns} height={480} aggregations={['sum', 'count']} />"
      },
      {
        "name": "persistKey",
        "type": "string",
        "required": false,
        "doc": "Remember the user's layout (column order, widths, hidden and pinned columns, pinned rows) in localStorage under this key.",
        "default": "undefined"
      },
      {
        "name": "source",
        "type": "RowSource",
        "required": false,
        "doc": "Back the grid with a windowed/server data source instead of `rows`. In source mode, sort + filter are delegated to the source; grouping is not applied.",
        "default": "undefined",
        "example": "<Grid rows={[]} {columns} height={480}\n  source={{ getRows: ({ range, sorts, filter }) => fetchPage(range.start, range.end, sorts, filter) }} />"
      },
      {
        "name": "onCellEdit",
        "type": "(e: CellEditEvent) => void",
        "required": false,
        "doc": "Called when an editable cell is committed. Update your row data in here.",
        "example": "<Grid {rows} {columns} height={480} onCellEdit={({ row, column, value }) => (row[column.key] = value)} />"
      },
      {
        "name": "cell",
        "type": "Snippet<[{ row: GridRow; column: ColumnDef; value: unknown }]>",
        "required": false,
        "doc": "Render content for `type: 'custom'` columns.",
        "example": "<Grid {rows} {columns} height={480}>\n  {#snippet cell({ row, column })}{#if column.key === 'chart'}<MiniChart sym={row.sym} />{/if}{/snippet}\n</Grid>"
      }
    ]
  },
  "column": {
    "description": "Options of a column definition (ColumnDef), shared by every type.",
    "props": [
      {
        "name": "key",
        "type": "string",
        "required": true,
        "doc": "Field on the row to read for this column's value. Also the column's id: sort, filter, width and hidden state are keyed by it."
      },
      {
        "name": "header",
        "type": "string",
        "required": true,
        "doc": "The column's title in the header row."
      },
      {
        "name": "locale",
        "type": "string",
        "required": false,
        "doc": "BCP 47 locale for this column's built-in formatting (`price`, `date`, `currency`, `relative`). Unset, the grid's `locale`, then `en-US`.",
        "default": "undefined"
      },
      {
        "name": "value",
        "type": "(row: GridRow) => unknown",
        "required": false,
        "doc": "Computed value: derive this cell's value from the whole row instead of reading `row[key]` (KPIs, ratios, deltas). Flows through display, sort, filter, aggregation, export and conditional formatting. `key` still names the column (and is the sort/filter key) but need not be a real row field. Keep it cheap and pure — it's called during sort/filter. Computed columns aren't editable. In-memory mode (a server source owns its own derivations).",
        "example": "{ type: 'percent', key: 'chg', header: 'Chg %', value: (row) => ((Number(row.last) - Number(row.ref)) / Number(row.ref)) * 100 }"
      },
      {
        "name": "spanRows",
        "type": "boolean | ((a: GridRow, b: GridRow) => boolean)",
        "required": false,
        "doc": "Merge this column's cell down over adjacent rows. `true` joins rows with equal (non-blank) values; a function decides for each adjacent pair. Hierarchical: a run also breaks wherever a spanning column to its left breaks, and at group headers. A drawing rule only — sort, filter, copy and export still see every row's own value. In-memory mode only.",
        "default": "false",
        "example": "{ type: 'text', key: 'account', header: 'Account', spanRows: true }"
      },
      {
        "name": "colSpan",
        "type": "(row: GridRow) => number",
        "required": false,
        "doc": "How many columns this cell covers for a given row (1 when unset). Covered cells are not drawn. Clamped at a pinned/scrolling boundary and the last column; the leftmost claim wins. Ignored under `virtualizeColumns`.",
        "example": "{ type: 'text', key: 'sym', header: 'Symbol', colSpan: (row) => (row.kind === 'note' ? 4 : 1) }"
      },
      {
        "name": "width",
        "type": "number",
        "required": false,
        "doc": "Width in px. A `flex` column starts from it where columns have fixed widths (pinned columns, `virtualizeColumns`), and `fitColumns` scales columns in proportion to it.",
        "default": "96"
      },
      {
        "name": "minWidth",
        "type": "number",
        "required": false,
        "doc": "Smallest width (px) a drag-resize or autosize can give this column; never less than 48. Under `fitColumns`, also the narrowest the column gets before the grid scrolls sideways (64 px without one, or the column's `width` if smaller).",
        "default": "48"
      },
      {
        "name": "maxWidth",
        "type": "number",
        "required": false,
        "doc": "Largest width (px) a drag-resize, an autosize, `autoWidth` or `fitColumns` can give this column.",
        "default": "Infinity"
      },
      {
        "name": "flex",
        "type": "number",
        "required": false,
        "doc": "flex-grow weight: the column stretches to fill the space the fixed-width columns leave, shared by weight. When the columns no longer fit, the grid scrolls sideways, counting a flex column as its `width`, else its `minWidth`, else 64 px. Unset, the column keeps its `width`.",
        "default": "undefined"
      },
      {
        "name": "align",
        "type": "Align",
        "required": false,
        "doc": "Which side the header, cells and footer sit on. Set it to line a custom column's values up under its title.",
        "default": "'right' for number types, 'left' for the rest",
        "example": "{ type: 'custom', key: 'pnl', header: 'P&L', align: 'right' }"
      },
      {
        "name": "cellClass",
        "type": "string | ((value: unknown, row: GridRow) => string | undefined)",
        "required": false,
        "doc": "Extra class(es) for this column's data cells — a static string, or a function of the cell value/row for conditional styling. Target via `:global(.bo-grid .c.your-class)`.",
        "default": "undefined",
        "example": "{ type: 'price', key: 'last', header: 'Last', cellClass: (v, row) => (Number(v) >= Number(row.ceiling) ? 'at-ceiling' : undefined) }"
      },
      {
        "name": "headerClass",
        "type": "string",
        "required": false,
        "doc": "Extra class(es) for this column's header. Target via `:global`.",
        "default": "undefined"
      },
      {
        "name": "headerTooltip",
        "type": "string",
        "required": false,
        "doc": "Show a styled tooltip on the column header (the same themed bubble as cell `tooltip`) — describe what the column means, units, caveats. Hovering anywhere on the header reveals it.",
        "default": "undefined"
      },
      {
        "name": "headerInfo",
        "type": "boolean",
        "required": false,
        "doc": "Render a small ⓘ info icon in the header as a visible cue that a `headerTooltip` is available. Ignored without `headerTooltip`.",
        "default": "false"
      },
      {
        "name": "flash",
        "type": "boolean | 'auto' | 'up-down' | 'change'",
        "required": false,
        "doc": "Flash the cell when its value changes. - `'auto'` (alias `'up-down'`) — the grid derives it: green on a rise, red on a fall, per cell. Nothing to maintain in app code; just stream new values in. This is what you want for a live price board. - `'change'` — a neutral flash on any change, numeric or not (status, qty). - `true` — legacy row-driven mode: the flash follows the row's own `flashSeq`/`flashDir`, so every `flash: true` column in a row flashes together, in one direction, and you bump the counter yourself. Derived modes never flash on first paint or when a row scrolls into view.",
        "default": "false"
      },
      {
        "name": "flashMs",
        "type": "number",
        "required": false,
        "doc": "Flash duration in ms — **derived modes only** (`'auto'`/`'up-down'`/ `'change'`); has no effect on legacy `flash: true`, which flashes for the CSS animation's fixed duration regardless. Raise it for a slow feed, lower it for a fast one so flashes don't smear together.",
        "default": "300"
      },
      {
        "name": "flashColor",
        "type": "boolean",
        "required": false,
        "doc": "Tint the text in the tick's direction while flashing. Set false when the column colours its own text — e.g. price-limit tones on a VN board (ceiling / floor / reference) — so the flash only lights the background.",
        "default": "true"
      },
      {
        "name": "showChange",
        "type": "boolean | ShowChangeOptions",
        "required": false,
        "doc": "Show the size of each change beside the value for a moment — `▲0.15` in the up colour, `▼` down — held, then faded. The delta is the last change, written in the column's own format. Numeric values only; first paint and rows scrolling into view show nothing. `{ format, ms }` sets the text (`format(delta, row)` gets the signed change) and how long it shows (1000 ms unless set). In a narrow column the delta gives way first: the value itself is never cut for it. Meant for one or two key columns (the match price): each change runs a short animation, so a busy board with it on every column pays real frame time.",
        "default": "false",
        "example": "{ type: 'price', key: 'last', header: 'Last', flash: 'auto', showChange: { ms: 1500, format: (d) => `${d > 0 ? '+' : ''}${d.toFixed(2)}` } }"
      },
      {
        "name": "updateMs",
        "type": "number",
        "required": false,
        "doc": "Repaint this column at most every this many ms under `api.patchRows`. Each tick is still written into the row at once (sort, filters, totals and other columns see it), but the cell shows the latest value on its next beat: a value that changes on every tick and is read at a glance (total volume, foreign buy and sell) stops repainting every frame. The first change after a quiet spell shows at once. A flash or `showChange` plays on the repaint, against the value shown before. For the field the column shows (`key`), not a `value` column; when several columns show a field, it waits only if they all set this. Unset, the cell repaints on every tick.",
        "default": "undefined"
      },
      {
        "name": "sortable",
        "type": "boolean",
        "required": false,
        "doc": "Set false to disable header-click sorting on this column.",
        "default": "true"
      },
      {
        "name": "compare",
        "type": "(a: unknown, b: unknown) => number",
        "required": false,
        "doc": "Custom ascending comparator for this column's values (e.g. enum priority or natural sort). Direction is applied by the grid. In-memory mode only.",
        "example": "{ type: 'badge', key: 'status', header: 'Status', compare: (a, b) => RANK[String(a)] - RANK[String(b)] }"
      },
      {
        "name": "groupAgg",
        "type": "AggKind",
        "required": false,
        "doc": "When grouping is active (or with `footer`), show this aggregate of the column on group headers and the totals row.",
        "default": "undefined",
        "example": "{ type: 'volume', key: 'qty', header: 'Qty', groupAgg: 'sum' }"
      },
      {
        "name": "pinned",
        "type": "boolean | 'left' | 'right'",
        "required": false,
        "doc": "Pin this column so it stays visible during horizontal scroll. `true` / `'left'` pins to the left edge, `'right'` to the right.",
        "default": "false"
      },
      {
        "name": "editable",
        "type": "boolean",
        "required": false,
        "doc": "Allow inline editing (double-click, Enter or F2 on the focused cell, or type to replace). Number types edit as numbers, `date` as a date. Display types (`badge`, `boolean`, `rating`, `progress`, `tags`, `link`, `avatar`, `relative`, `sparkline`, `custom`) edit only with an `editor`; a computed (`value`) column never does.",
        "default": "false"
      },
      {
        "name": "validate",
        "type": "(value: unknown, row: GridRow) => boolean",
        "required": false,
        "doc": "Validate an edited value before it commits (inline edit, custom editor, paste, fill). Return false to reject and keep the old value. Receives the parsed value.",
        "example": "{ type: 'number', key: 'qty', header: 'Qty', editable: true, decimals: 0, validate: (v) => Number(v) > 0 && Number(v) % 100 === 0 }"
      },
      {
        "name": "parse",
        "type": "(raw: string, row: GridRow) => unknown",
        "required": false,
        "doc": "Turn typed or pasted text into the stored value (runs before the built-in number/date coercion, which it replaces). Return `undefined` or a non-finite number to reject the input.",
        "example": "{ type: 'number', key: 'qty', header: 'Qty', editable: true, parse: (raw) => Number(raw.replace(/[,\\s]/g, '')) }"
      },
      {
        "name": "editor",
        "type": "Component<CellEditorProps>",
        "required": false,
        "doc": "Svelte component that edits this cell in place of the built-in input. Makes any column type editable (with `editable: true`), including display widgets like `rating` or `tags`. It calls `commit(value)` to save — any value, no parsing — or `cancel()`; Escape cancels.",
        "example": "{ type: 'rating', key: 'score', header: 'Score', editable: true, editor: StarPicker }"
      },
      {
        "name": "options",
        "type": "string[]",
        "required": false,
        "doc": "Editable choices: when set, editing renders a `<select>` of these options instead of a text input (enum/status columns).",
        "default": "undefined",
        "example": "{ type: 'text', key: 'side', header: 'Side', editable: true, options: ['Buy', 'Sell'] }"
      },
      {
        "name": "tooltip",
        "type": "boolean | ((value: unknown, row: GridRow) => string)",
        "required": false,
        "doc": "Show a styled floating tooltip on each cell (themed, instant — replaces the native `title`). `true` shows the full formatted value (handy when content truncates); a function returns custom text per value/row (any column type, including `custom`). Return '' to suppress for a given cell.",
        "default": "false",
        "example": "{ type: 'text', key: 'sym', header: 'Symbol', tooltip: (_, row) => String(row.name ?? '') }"
      },
      {
        "name": "wrap",
        "type": "boolean",
        "required": false,
        "doc": "Let long content wrap to multiple lines instead of truncating with an ellipsis. Pair with a taller `rowHeight`.",
        "default": "false"
      },
      {
        "name": "format",
        "type": "(value: unknown, row?: GridRow) => string",
        "required": false,
        "doc": "Custom display formatter, overriding the built-in type formatter. Applies to display, tooltip, copy and (formatted) export. `row` is absent for aggregate cells.",
        "example": "{ type: 'number', key: 'qty', header: 'Qty', format: (v) => Number(v).toLocaleString('vi-VN') }"
      },
      {
        "name": "render",
        "type": "(ctx: CellRenderContext) => string | Node | null | undefined",
        "required": false,
        "doc": "JS cell renderer — the framework-agnostic alternative to the `cell` snippet, for React/Vue/vanilla (and the `<bo-grid>` web component). Return an `HTMLElement`/`Node` (safe — appended as-is) or an HTML **string** (inserted as innerHTML — YOU must sanitize any untrusted data; prefer returning a Node for that). Overrides the cell's display only — sort, filter, tooltip, copy and export still use the value/`format`.",
        "example": "{ type: 'text', key: 'sym', header: 'Symbol', render: ({ value }) => Object.assign(document.createElement('b'), { textContent: String(value) }) }"
      },
      {
        "name": "component",
        "type": "Component<CellTypeProps>",
        "required": false,
        "doc": "Svelte component that draws this cell (receives `value`, `row`, `column` and the formatted `text`). Display only, like `render`. Usually supplied by a registered `cellTypes` entry, but any column can set it. It updates in place as its props change; under `api.patchRows`, `row` is a fresh view of the patched row on each patch (same fields, new identity), so read it reactively (`$derived`, markup) rather than once at mount.",
        "example": "{ type: 'text', key: 'country', header: 'Country', component: CountryCell }"
      },
      {
        "name": "resizable",
        "type": "boolean",
        "required": false,
        "doc": "Set false to disable drag-to-resize on this column.",
        "default": "true"
      },
      {
        "name": "autoWidth",
        "type": "boolean",
        "required": false,
        "doc": "Widen the column when a value on screen no longer fits (a price gaining a digit, a running total), checked at most every 500 ms. It only grows, so values flickering wider and narrower don't make it jump; `maxWidth` caps it, and a width the user dragged or autosized takes over.",
        "default": "false"
      },
      {
        "name": "group",
        "type": "string",
        "required": false,
        "doc": "Parent header label. Consecutive columns sharing a `group` render under a spanning header, which keeps the width of its columns (flex ones too).",
        "default": "undefined"
      },
      {
        "name": "filter",
        "type": "false | FilterKind | (string & {})",
        "required": false,
        "doc": "Header filter-menu control for this column (requires `filterMenu` on <Grid>). `'set'` shows a value checklist, a registered `filterTypes` name draws that filter, and `false` disables filtering for this column.",
        "default": "'date' for date columns, 'number' for number types, else 'text'",
        "example": "{ type: 'badge', key: 'status', header: 'Status', filter: 'set' }"
      },
      {
        "name": "dataBar",
        "type": "DataBarConfig",
        "required": false,
        "doc": "Conditional formatting — a horizontal bar painted behind the cell value, scaled across the column's value range. The range is auto-computed over the current view (in-memory) unless `min`/`max` are given; pass `min: 0` for absolute proportional bars. Bars cross a zero baseline when the range spans negatives. Numeric columns only.",
        "default": "undefined",
        "example": "{ type: 'volume', key: 'vol', header: 'Volume', dataBar: { min: 0 } }"
      },
      {
        "name": "icons",
        "type": "IconRule[]",
        "required": false,
        "doc": "Conditional formatting — show an icon beside the value, chosen by the highest threshold `at` that is ≤ the cell value (e.g. ▲ ● ▼ by sign or band). Numeric columns only.",
        "default": "undefined",
        "example": "{ type: 'percent', key: 'chg', header: 'Chg %', icons: [{ at: -Infinity, icon: '▼', tone: 'down' }, { at: 0, icon: '●' }, { at: 0.01, icon: '▲', tone: 'up' }] }"
      },
      {
        "name": "colorScale",
        "type": "ColorScaleConfig",
        "required": false,
        "doc": "Conditional formatting — tint the cell background across the column's value range (a soft heat ramp). Auto-ranged over the current view unless `min`/ `max` are given; pass `mid` for a 3-stop diverging scale (e.g. `mid: 0`). Numeric columns only.",
        "default": "undefined",
        "example": "{ type: 'percent', key: 'chg', header: 'Chg %', colorScale: { mid: 0 } }"
      }
    ],
    "types": [
      {
        "type": "text",
        "options": [
          {
            "name": "sub",
            "type": "string",
            "required": false,
            "doc": "Row field shown under the value, smaller (a company name under its symbol).",
            "default": "undefined"
          }
        ]
      },
      {
        "type": "price",
        "options": []
      },
      {
        "type": "percent",
        "options": []
      },
      {
        "type": "volume",
        "options": []
      },
      {
        "type": "number",
        "options": [
          {
            "name": "decimals",
            "type": "number",
            "required": false,
            "doc": "Digits after the decimal point.",
            "default": "2"
          }
        ]
      },
      {
        "type": "date",
        "options": [
          {
            "name": "dateStyle",
            "type": "DateStyle",
            "required": false,
            "doc": "`'short'` (4/7/26) or `'medium'` (Apr 7, 2026), in the column's locale.",
            "default": "'medium'",
            "example": "{ type: 'date', key: 'settled', header: 'Settled', dateStyle: 'short' }"
          }
        ]
      },
      {
        "type": "currency",
        "options": [
          {
            "name": "currency",
            "type": "string",
            "required": false,
            "doc": "ISO 4217 currency code.",
            "default": "'USD'"
          },
          {
            "name": "decimals",
            "type": "number",
            "required": false,
            "doc": "Digits after the decimal point. Unset, the currency's own (2 for USD, 0 for VND).",
            "default": "undefined"
          }
        ]
      },
      {
        "type": "relative",
        "options": []
      },
      {
        "type": "heatmap",
        "options": [
          {
            "name": "min",
            "type": "number",
            "required": true,
            "doc": "The value at the cold end of the colour ramp."
          },
          {
            "name": "max",
            "type": "number",
            "required": true,
            "doc": "The value at the hot end of the colour ramp."
          },
          {
            "name": "decimals",
            "type": "number",
            "required": false,
            "doc": "Digits after the decimal point.",
            "default": "2"
          }
        ]
      },
      {
        "type": "sparkline",
        "options": [
          {
            "name": "sparkKey",
            "type": "string",
            "required": true,
            "doc": "Row field holding the series: an array of candles (`{ open, high, low, close, volume }`), oldest first."
          }
        ]
      },
      {
        "type": "progress",
        "options": [
          {
            "name": "min",
            "type": "number",
            "required": false,
            "doc": "The value at an empty bar.",
            "default": "0"
          },
          {
            "name": "max",
            "type": "number",
            "required": false,
            "doc": "The value at a full bar.",
            "default": "100"
          }
        ]
      },
      {
        "type": "rating",
        "options": [
          {
            "name": "max",
            "type": "number",
            "required": false,
            "doc": "Stars drawn; the value fills that many.",
            "default": "5"
          }
        ]
      },
      {
        "type": "tags",
        "options": []
      },
      {
        "type": "badge",
        "options": [
          {
            "name": "tones",
            "type": "Record<string, BadgeTone>",
            "required": false,
            "doc": "A tone for each value; a value without one is neutral.",
            "default": "undefined",
            "example": "{ type: 'badge', key: 'status', header: 'Status', tones: { Filled: 'up', Rejected: 'down', Working: 'amber' } }"
          }
        ]
      },
      {
        "type": "boolean",
        "options": [
          {
            "name": "trueLabel",
            "type": "string",
            "required": false,
            "doc": "Text beside the ✓. Copy, tooltips and a `formatted` export write it, or `Yes` without one (a plain export writes `true`).",
            "default": "undefined"
          },
          {
            "name": "falseLabel",
            "type": "string",
            "required": false,
            "doc": "Text beside the ✕. Copy, tooltips and a `formatted` export write it, or `No` without one (a plain export writes `false`).",
            "default": "undefined"
          }
        ]
      },
      {
        "type": "avatar",
        "options": [
          {
            "name": "sub",
            "type": "string",
            "required": false,
            "doc": "Row field shown under the name, smaller (a role, an email).",
            "default": "undefined"
          }
        ]
      },
      {
        "type": "link",
        "options": [
          {
            "name": "href",
            "type": "(row: GridRow) => string",
            "required": false,
            "doc": "The link's URL for a row. Unset, the value itself is the URL. `javascript:`, `data:` and `vbscript:` URLs are dropped (the text shows unlinked).",
            "example": "{ type: 'link', key: 'sym', header: 'Symbol', href: (row) => `/symbols/${row.sym}` }"
          },
          {
            "name": "newTab",
            "type": "boolean",
            "required": false,
            "doc": "Open the link in a new tab (with `rel=\"noopener noreferrer\"`).",
            "default": "false"
          }
        ]
      },
      {
        "type": "custom",
        "options": []
      },
      {
        "type": "(cellType)",
        "options": [
          {
            "name": "cellType",
            "type": "string",
            "required": true,
            "doc": "The name of a type registered in `<Grid cellTypes>`."
          }
        ]
      }
    ]
  },
  "api": {
    "description": "Methods of the grid handle (GridApi): from onReady in Svelte, the ref in React and Vue, onReady in the element config.",
    "methods": [
      {
        "name": "scrollToRow",
        "signature": "scrollToRow(key: string | number, align?: ScrollAlign): boolean",
        "doc": "Scroll a row into view by its id; `align` is `'nearest'` unless given. Returns false when the row is not in the current view (filtered out, not loaded, or unknown).",
        "example": "api.scrollToRow(order.id, 'center');"
      },
      {
        "name": "focusCell",
        "signature": "focusCell(rowKey: string | number, columnKey: string): boolean",
        "doc": "Focus (and select) one cell by row id and column key. Returns false when the row or column is not currently visible.",
        "example": "if (!api.focusCell(row.id, 'qty')) api.scrollToRow(row.id);"
      },
      {
        "name": "getFocusedCell",
        "signature": "getFocusedCell(): { row: GridRow; column: ColumnDef } | null",
        "doc": "The focused cell's row and column, or null when no data cell has focus (nothing focused yet, or the focus is on a group row). For hotkeys that act on the row the user is on.",
        "example": "const cell = api.getFocusedCell();\nif (cell) openTicket(cell.row);"
      },
      {
        "name": "getSelectedRows",
        "signature": "getSelectedRows(): GridRow[]",
        "doc": "Ticked rows (needs `rowSelection`), in view order.",
        "example": "const ids = api.getSelectedRows().map((row) => row.id);"
      },
      {
        "name": "autosizeColumns",
        "signature": "autosizeColumns(keys?: string[]): void",
        "doc": "Fit columns to their content, as the column menu's Autosize does. All resizable columns when `keys` is omitted. The other columns keep their widths (flex ones are held where they are).",
        "example": "api.autosizeColumns(['sym', 'name']);"
      },
      {
        "name": "exportCSV",
        "signature": "exportCSV(filename?: string): Promise<void>",
        "doc": "Download the current view (after filter and sort) as CSV, named `export.csv` unless `filename` is given.",
        "example": "await api.exportCSV(`orders-${new Date().toISOString().slice(0, 10)}.csv`);"
      },
      {
        "name": "getState",
        "signature": "getState(): GridState",
        "doc": "Snapshot of the user's layout: order, widths, hidden, pins, sorts, filters. A plain object, safe to `JSON.stringify` and store.",
        "example": "localStorage.setItem('orders-layout', JSON.stringify(api.getState()));"
      },
      {
        "name": "applyState",
        "signature": "applyState(state: unknown): boolean",
        "doc": "Restore a snapshot, reconciled against the columns as they are now. Returns false when the state is unusable (foreign version, not an object).",
        "example": "const saved = localStorage.getItem('orders-layout');\nif (saved) api.applyState(JSON.parse(saved));"
      },
      {
        "name": "patchRows",
        "signature": "patchRows(patches: Iterable<readonly [string | number, Record<string, unknown>]>): number",
        "doc": "Realtime fast path: write `[rowId, fields]` patches into the rows in place and repaint only the rendered rows they change. Rows may be plain objects (no $state proxy) — far cheaper per write in a busy feed. Feed it a tick stream: `createTickStream({ apply: (b) => api.patchRows(b) })`. Sort and filter catch up on the next view change, as for any live data (or at once, with `liveSort`). A patch's id is the row's `getRowId` value (`row.id` unless set). Returns how many rows changed.",
        "example": "api.patchRows([\n  [1, { last: 68.6, vol: 1_204_300 }],\n  [2, { last: 121.1 }],\n]);"
      },
      {
        "name": "refresh",
        "signature": "refresh(): void",
        "doc": "Re-run filter and sort against the rows' current values. Values that change in place (a feed) never re-sort the view by themselves — that would mean re-sorting on every tick — so call this when the order should catch up, e.g. every second on a \"top movers\" board (or use `liveSort`).",
        "example": "const timer = setInterval(() => api.refresh(), 1000);"
      },
      {
        "name": "flashCells",
        "signature": "flashCells(opts?: FlashCellsOptions): number",
        "doc": "Flash cells on demand — a fill, an alert, a row to notice — with the tick animation, whether or not their values changed. Only rows on screen flash. Value cells flash (numbers, text); cells that draw their own content (badges, sparklines, components) do not. Returns how many rows it flashed.",
        "example": "api.flashCells({ rows: [fill.orderId], dir: fill.side === 'buy' ? 'up' : 'down', ms: 800 });"
      },
      {
        "name": "pinRow",
        "signature": "pinRow(key: string | number, pinned?: boolean): boolean",
        "doc": "Pin a row above the scroll (or unpin it with `pinned: false`), as the row menu does under `rowPinning`; `pinned` is true unless given. The row stays in the body too. Returns false when no row has that id.",
        "example": "api.pinRow(row.id);"
      },
      {
        "name": "getPinnedRowIds",
        "signature": "getPinnedRowIds(): (string | number)[]",
        "doc": "Ids of the rows pinned at runtime, in pin order.",
        "example": "for (const id of api.getPinnedRowIds()) api.pinRow(id, false);"
      },
      {
        "name": "openFind",
        "signature": "openFind(query?: string): void",
        "doc": "Open the find bar (`findBar`), optionally searching for `query` at once.",
        "example": "api.openFind('VNM');"
      }
    ]
  }
}