import { d as FilterOp, l as Facet, u as FilterCondition, x as SetFilterValue } from "./filter.types-DnF8td18.mjs"; import { i as LazyArray, m as LazyPages } from "./lazy-C0Y_eCnB.mjs"; import { r as SlowLoadingOptions } from "./use-slow-loading-D5q4M47N.mjs"; import { CSSProperties, ComponentPropsWithRef, FC, HTMLAttributes, ReactNode } from "react"; //#region src/table/table.types.d.ts type RowData = Record; type TableData = RowData[]; type DotPath = T extends object ? T extends Date | readonly unknown[] | ((...args: any) => any) ? never : { [K in keyof T & string]: K | `${K}.${DotPath}` }[keyof T & string] : never; type ColumnDef = DotPath | FieldColumnDef | ComputedColumnDef | SelectionColumnDef; type ColumnsDef = (ColumnDef | ((firstRow: T) => ColumnDef | ColumnDef[]))[]; /** * Decides what to do with a key found on the first row that no configured column covers. Return a * def to configure that column, `true` for the default treatment (the key as a field column), or * `false`/`undefined` to leave the key out. */ type AutoColumnFn = (key: string, value: unknown, row: T) => ColumnDef | boolean | undefined; type RowId = string | number; interface TableConfig { /** * Where the table's rows come from, in any of three shapes — and they differ in *who decides the * rows changed*, which is what decides when row-keyed state resets (see `setData`). * * Named for what it is rather than for one of its shapes: only the first is literally rows. * Whatever you pass, the resolved array is always `table.rows`. * * **An array** — React decides. `useTable` re-applies it whenever it is a different array than * the one last applied, so it must be referentially stable (a MobX `computed`, or `useMemo`). * Rebuilding it inline on every render (`data={rows.filter(isActive)}`) reads as a new dataset * every time and clears selection with it. * * **A getter** — MobX decides. `() => store.activeRows` is tracked in a reaction, so it * re-applies when the observables it *read* change, on MobX's cadence rather than React's. Two * caveats, both silent if missed: the getter must read observables (a getter over React props or * state is never re-run, and the table keeps the first dataset forever), and it is captured once * — so close over observables, not over render-scoped values, which would go stale. * * **A lazy** — the lazy decides, and it is the only shape that knows anything beyond the rows * themselves. It says whether a request is running and how the last one ended, which is what * lets the table tell a first load from an empty result from a failure with nothing to show: * see `loading`, `error` and `isEmpty` on the model. A keyed collection works because * `store.byOrg({ orgId })` hands back a *different* lazy per key, and the table follows it. * * With the first two, that same information is yours to supply — see * {@link UseTableConfig.loading} and {@link UseTableConfig.error}. */ data?: T[] | (() => T[]) | LazyArray | LazyPages; /** * Who narrows and orders the rows: this table, or whatever produced them. * * `"client"` (the default) is the fully-loaded table — it sorts and filters the rows it holds. * `"server"` says the rows arrive already narrowed and ordered, and flips three defaults at once: * * | | `"client"` | `"server"` | * | --- | --- | --- | * | `sortMode` | `"auto"` — sorted here | `"manual"` — `sorts` is state to send | * | each column's `filterMode` | `"client"` | `"server"` — serialized into `filterQuery` | * | `search.mode` | `"client"` | `"server"` | * * They are defaults, not a lock: a column may still say `filterMode: "client"` to narrow what * came back, and `sortMode` / `search.mode` override individually. * * **Inferred when `data` is a paged lazy**, since a table holding one page of fifty thousand rows * that sorts what it has is the failure this exists to prevent — it looks like it works. Set it * explicitly for any other server-driven dataset: an array you refetch yourself, a query hook. * * Everything the server needs is then on {@link TableModel.query}, as one structurally-compared * object. */ mode?: "client" | "server"; /** Fixed pixel height of every row (the virtualization contract). Default 40. */ rowHeight?: number; /** * Fixed pixel height of the header's **whole box** — the row plus any padding used to space the * rows away from it. Defaults to `rowHeight`, so a table that says nothing looks exactly as it * did before this existed. * * `` applies it as a border-box height, so this number *is* the space the header * occupies rather than a claim about it: `` starts below it and * `--table-header-height` publishes it, and neither can disagree with what is rendered. Header * content needing more room than this overflows onto the first row rather than growing the * header — visible where it is set, which is the trade for the number being knowable at all. * * Unlike {@link rowHeight} nothing in the virtualization math reads this: body offsets are * relative to the body, so the header is free to be a different height without rows drifting. */ headerHeight?: number; /** * The curated columns. Read once, at construction — change the set at runtime through the model * (`setColumns`, `addColumn`, `removeColumn`), which preserves what the user has done to the * columns that survive the change. * * Configuring these does not stop `autoColumns` from filling in the rest; the two compose. */ columns?: ColumnsDef; /** * What to do with keys on the first row that `columns` doesn't cover. * * `true` gives each one a default field column. A function decides per key — return a def to * configure it, `true` for the default, `false` to leave it out — which is how you get automatic * columns *and* control over them: * * ```ts * autoColumns: (key, value) => { * if (key.startsWith("_")) return false; * if (typeof value === "number") return { key, align: "right" }; * return true; * }; * ``` * * Defaults to `true` when `columns` is omitted, so a table with no column config still works, and * to `false` when it isn't. Read once, at construction. * * Auto columns follow the data: they appear as keys appear and go as keys go. Curating a column or * removing one no longer switches this off — use an allowlist in the function if you want a fixed * set. */ autoColumns?: boolean | AutoColumnFn; /** * Height (px) of the detail panel below an expanded row. The binary-height contract: a row is * `rowHeight` or `rowHeight + expansionHeight`, never measured — panel content taller than * this scrolls internally (`` owns that). Default 320. */ expansionHeight?: number; /** Whether expanding a row collapses all others. Default "multiple". */ expandMode?: "single" | "multiple"; /** * How the sort list is applied. `"auto"` sorts rows client-side through each column's value * accessor / `compare`. `"manual"` treats `sorts` as pure reactive state and leaves row order * untouched — send them with the next request instead. The sort state APIs (`setSort`, * `clearSort`, `sortDirection`, `sortIndex`) behave identically in both modes, so header sort * UIs need no changes. * * Defaults to `"manual"` under {@link TableConfig.mode} `"server"` and `"auto"` otherwise, so * this only needs setting to mix the two — a server-filtered table whose rows all fit, and which * would rather sort them here than round-trip. */ sortMode?: "auto" | "manual"; /** Extra rows rendered above/below the visible window. Default 3. */ rowOverscan?: number; /** Extra columns rendered on either side of the visible window. Default 1. */ columnOverscan?: number; /** * Stable row identity, used to key all row-scoped state (selection, expansion) and React row * keys. Must be unique per row. * * Defaults to the row's own **object identity** — one id per distinct row object, held in a * `WeakMap` for as long as that object is around. So this is dead config for rows that are * identity-mapped model instances: the same record is the same object, and its state survives a * reload, an append and a switch between keyed collections for free. * * Configure it when row objects are **replaced by fresh ones that mean the same row** — a * plain-JSON refetch, or a `keys: false` model that is constructed per payload. Without it those * rows are new objects with new ids, so `setData` finds none of the old ids and drops the * selection with them. * * For an identity-mapped model, `getRowId: (r) => MyModel.identityKey(r)` is the right spelling * rather than `r.id`, which is only there if the schema declared it. * * ⚠️ Uniqueness is yours to guarantee, and paginated sources are where it breaks: a record * returned on two pages produces two rows sharing one id — one React key, and one selection * toggle that hits both. Deduplicate at the source (`lazyPages`' `dedupeBy`). */ getRowId?: (row: T, index: number) => RowId; /** * Configures the built-in cross-column search (`TableModel.search`). * * `mode: "server"` means the server does the searching: the query stops narrowing rows here and * becomes a `{ op: "search" }` entry in `filterQuery` instead. Per-column `searchable` is then * irrelevant — the server decides what it searches. Defaults to whichever * {@link TableConfig.mode} resolves to. * * Debouncing is deliberately not offered. Like `onStateChange`, the cadence belongs to whoever * owns the input. */ search?: { mode?: FilterMode; }; /** * Fires whenever persisted table state changes (see `getState`) — including as a result of * `applyState`. The snapshot is JSON-serializable; debouncing/storage is the consumer's job. */ onStateChange?: (state: TableState) => void; } /** * What {@link useTable} accepts: everything a {@link TableModel} takes, plus the two status props * that stand in for what a lazy would have known by itself. * * They live here rather than on `TableConfig` because mirroring React state on every render is a * thing only a hook can do. A `TableModel` built directly has no render to mirror from, so its * answer for a dataset with a loading story is a lazy, which carries its own. */ /** * The status props that stand in for what a lazy would have known by itself. * * They exist only on {@link useTable} and not on `TableConfig` because mirroring React state on * every render is a thing only a hook can do. A `TableModel` built directly has no render to mirror * from, so its answer for a dataset with a loading story is a lazy, which carries its own. */ interface TableStatus { /** * Whether a request is in flight, for a `data` that cannot say so itself — an array or a getter. * **Ignored when `data` is a lazy**, which already knows. * * What it produces depends on whether there are rows. With none it is a first load, reported as * `table.loading`. Behind rows already on screen it is a refresh, which the table deliberately has * no state for — the rows stay exactly as they are, and you already know a request is running, * since you are the one passing this. * * Pass it from the first render, not from the first effect. No rows and `loading: false` is a * settled empty result by definition, and the table will say so. */ loading?: boolean; /** * How the last request ended, for a `data` that cannot say so itself. **Ignored when `data` is a * lazy.** * * Read the same way as `loading`: it matters when there are no rows, where it is fatal * (`table.error`, what `` renders) and stops the table reporting a load that will * never finish. Behind rows still on screen the table ignores it on purpose — they are good rows, * and a failed refresh is not worth blanking them for. * * Clear it when the next request starts, or the table will keep describing the older failure. */ error?: unknown; } /** * What {@link useTable} accepts — a `TableConfig`, in one of two combinations that cannot be mixed. * * A lazy already knows whether a request is running and how the last one ended, so pairing it with * `loading` or `error` is a contradiction rather than a preference. Expressed as a union so it is a * compile error rather than a prop that silently does nothing: passing both tells you at the call * site, which is where the mistake is. */ type UseTableConfig = (TableConfig & { data?: LazyArray | LazyPages; } & { [K in keyof TableStatus]?: never }) | (TableConfig & { data?: T[] | (() => T[]); } & TableStatus); /** * Everything a server needs in order to answer for this table: which rows, in what order. * * Read it as *the work this table is deliberately not doing*. Both halves are already scoped that * way, so nothing here duplicates what the table applied itself: * * - `filters` is {@link TableModel.filterQuery} — only the columns set to `filterMode: "server"`, * plus a server-mode search. A client-side filter narrows `rows` here and never appears. * - `sorts` is empty unless `sortMode` is `"manual"`. Under `"auto"` the table has already sorted, * so sending them would ask for work that is done — and would make a client-side sort churn this * object and refetch for nothing. * * Compared structurally, which is the point of it being one object: its identity is stable while * its contents are, so it works directly as a `useEffect` dependency or a query key and a column * resize can't trigger a request. * * ```tsx * const query = table.query; * useEffect(() => void refetch(query), [query]); * ``` */ interface TableQuery { /** The server-side filter conditions, or `undefined` when none are active. */ filters: FilterCondition[] | undefined; /** The sort list to apply server-side; empty when this table is sorting for itself. */ sorts: ColumnSort[]; } /** Persisted per-column state (see TableState). `width` is the manual resize override; absent = automatic. */ interface ColumnState { hidden: boolean; pinned: ColumnPin; width?: number; } /** * JSON-serializable snapshot of what the user has done to the table: column order, per-column * visibility/pinning/manual widths, the sort list, active filters and the search query. Ephemeral * state (selection, scroll, expansion) is deliberately excluded. * * Note that `filters` and `search` change far more often than the rest — per keystroke rather than * per drag — so `onStateChange` now fires that often too. They are separate top-level keys so a * consumer can debounce them apart from the arrangement; debouncing and storage remain its job. * * Produced by `getState`, restored by `applyState`, observed by `onStateChange`. */ interface TableState { columnOrder: string[]; columns: Record; sorts: ColumnSort[]; /** * Per-column filter state, keyed by column key. Only columns whose filter is **active** get an * entry, so the map stays small — but `getState` always emits the key, even empty, exactly as it * does `columns` and `sorts`. * * That matters for restoring: the map is a *complete picture*, so `applyState` clears any filter * it does not mention. Restoring a view saved with nothing filtered therefore clears filters the * user applied since — which is what "restore that view" has to mean. (Omitting the key entirely, * as a hand-built partial snapshot may, still leaves filters alone.) * * Kept apart from `columns` on purpose: filter state churns per keystroke where an arrangement * barely churns at all, so a consumer wanting to debounce the two differently can split them * without unpicking one object. */ columnFilters?: Record; /** The built-in search query. Always emitted by `getState`, empty string included. */ search?: string; } /** * The table's contract with a *per-column* filter: a reactive predicate over one already-extracted * value. The table calls `matches(column.getValue(row))`, so the filter needs no accessor, no path * and no row type — which is what makes a computed column filterable with no extra config. * * Structural rather than an `instanceof`: `SetFilter` / `NumberFilter` / `DateFilter` / `TextFilter` * from the `filter` subpath satisfy it, but `table` declares the shape rather than importing the * classes, so a page's own `new SetFilter()` is the only thing that pulls them into the bundle. A * string discriminant (`filter: "set"`) would force a `"set" -> SetFilter` map into the table and * defeat exactly that. */ interface ColumnFilter { /** Whether the filter is currently narrowing anything. Inactive filters are skipped entirely. */ readonly active: boolean; matches(value: unknown): boolean; clear(): void; /** * Seeds the facet domain; its presence also selects the no-walk tier. See `ColumnModel.facets`. * * Narrowed to {@link SetFilterValue} because that is what facets actually are: `facetValues` * stringifies anything else, so a declared option outside that set could never match a tallied * one. Better a compile error than a facet that silently selects nothing. */ readonly options?: readonly SetFilterValue[]; /** Whether facets should carry cross-filtered counts — the expensive tier. */ readonly counts?: boolean; /** * Groups raw values before comparing them (scores into grades). `matches` applies it itself; the * table applies it when walking rows for facets, so the list offers projected values rather than * raw ones that would select nothing. */ readonly project?: (value: unknown) => unknown; /** * Whether picking more narrows rather than widens. When true, this column's facet counts are taken * against its own selection as well as the other filters — see `ColumnModel.facets`. */ readonly intersecting?: boolean; /** JSON-serializable state, persisted into `TableState.filters`. See `ValueFilter.value`. */ readonly value?: unknown; /** Restore state produced by `value`. A filter missing either is not persisted at all. */ setValue?(value?: unknown): void; /** * The filter's state as plain JSON, for a column set to `filterMode: "server"`. The table adds * the column's `field` and collects these into `TableModel.filterQuery`; it never calls `matches` * on such a column. */ readonly condition?: FilterCondition | undefined; } /** Where an active filter is applied. See {@link BaseColumnDef.filterMode}. */ type FilterMode = "client" | "server"; interface ColumnConfig { key: string; /** See {@link BaseColumnDef.meta}. */ meta?: ColumnMeta; /** Raw cell value — feeds sorting and the default render. Resolved dot-path for field columns, the `value` fn for computed ones. */ value: (row: RowData) => unknown; render: (row: RowData) => any; /** Sort comparator over extracted values. Omitted = default (numeric / chronological / locale string). */ compare?: (a: any, b: any) => number; title?: string; /** See BaseColumnDef.order — declarative placement, lower first, default 0. */ order?: number; pinned?: ColumnPin; width?: ColumnWidth; minWidth?: number; maxWidth?: number; resizable?: boolean; /** See BaseColumnDef.sortable — advisory flag for header sort UIs. Defaults to true. */ sortable?: boolean; /** Marks the built-in row-selection column (rendered via ``). */ selection?: boolean; /** See BaseColumnDef.filter — already resolved, so a factory def has been called by `fromDef`. */ filter?: ColumnFilter; /** See BaseColumnDef.filterable — advisory flag for header filter UIs. Defaults to true. */ filterable?: boolean; /** See BaseColumnDef.searchable. Defaults to true. */ searchable?: boolean | ((row: RowData) => string); /** See BaseColumnDef.hidden. Initial value only. */ hidden?: boolean; /** See BaseColumnDef.hideable — advisory for pickers, and proof against a snapshot. */ hideable?: boolean; /** See BaseColumnDef.pinnable — advisory for header UIs, and proof against a snapshot. */ pinnable?: boolean; /** See BaseColumnDef.filterMode. Defaults to the table's resolved `mode`. */ filterMode?: FilterMode; /** See BaseColumnDef.field. Defaults to `key`. */ field?: string; } /** * What `ColumnModel.setConfig` accepts — everything on {@link ColumnConfig} except the three that * cannot be swapped after the fact: * * - `key` identifies the column in `columns`, `columnOrder`, the sort list and any persisted * snapshot; changing it would orphan all of them. * - `filter` holds the user's live selection. Replacing the instance would silently discard it — * set the filter's own state instead, or `removeColumn` + `addColumn` to change its type. * - `selection` decides which components render the column at all. */ type ColumnConfigPatch = Partial>; /** Which edge a column is pinned to, or `false` for not pinned. */ type ColumnPin = false | "left" | "right"; type ColumnWidth = number | `${number}fr`; type SortDirection = "asc" | "desc"; /** One entry in the table's sort priority list. */ interface ColumnSort { key: string; direction: SortDirection; } /** * Whatever your app knows about a column that the table itself has no use for — the schema field a * column was generated from, a unit, a question, a metric definition. * * Empty by design, and **open for augmentation**: declare what your columns carry and it becomes * type-checked at every def and every read of `column.meta`. * * ```ts * declare module "@jayalfredprufrock/mobx-toolbox/table" { * interface ColumnMeta { * question?: SurveyQuestion; * } * } * ``` * * This exists because a column def could previously say everything about how a column *behaves* and * nothing about what it *represents* — so anything rendered about a column rather than about a row * had to reconstruct the column's identity from its key, or carry a `Map` alongside the * defs. Both re-derive something the def already knew, and the first duplicates the key format, * which persisted view state depends on. * * Spelled `meta` rather than `props` (which is what the equivalent on a filter is called) for two * reasons: `column.props` reads as React props on something that is not a component, and this is * wider than view concerns — a cell formatting itself from a unit, or a filter reading a choice * list, wants the same field. * * The same rule governs when the library declares a named option instead: only when it *reads* the * value, supplies a non-trivial default, or the concept is universal and precisely typable. See * {@link SetFilterProps}, which this mirrors. */ interface ColumnMeta {} interface BaseColumnDef { title?: string; /** * Application data about this column — see {@link ColumnMeta}. Read through * `ColumnModel.meta`, which every render-prop already receives. * * Unlike `filter`, this **is** re-read from a new def on `setColumns`: what a column represents * can legitimately change while its key stays the same (a republished survey rewording a question * whose id, and so whose column key, is unchanged). Compared shallowly, so a def rebuilt around * the same values is not a change. * * Excluded from `getState()`: it is structure supplied by the def, not state the user produced, * and it may hold things that do not serialize. */ meta?: ColumnMeta; render?: (row: T) => any; /** * Custom sort comparator. Receives the two rows' *extracted* values (the dot-path lookup for * field columns, the `value` fn's result for computed ones), not the rows. Return negative / * zero / positive as usual; the table handles direction. Defaults to numbers numerically, * Dates chronologically, everything else by locale string, nullish first. */ compare?: (a: any, b: any) => number; /** * Declarative placement, like CSS `order`: lower comes first, default `0`, and columns sharing a * value keep their relative position — configured columns before auto ones. It decides where a * column *lands*, not where it stays: dragging a column overrides it, and a column appearing later * is inserted at the position its `order` implies rather than appended. */ order?: number; /** Initial pin side; can also be changed at runtime via ColumnModel.setPinned. */ pinned?: ColumnPin; /** * Whether the column starts hidden. Defaults to false. * * The *initial* value only — `setHidden` and a persisted snapshot both move it afterwards. Pair * with `hideable: false` for a column that is only ever there to carry data or a filter. */ hidden?: boolean; /** * Whether a column picker should offer to change this column's visibility. Defaults to true. * * Read it as **locking `hidden` at whatever it starts as**, not as "cannot be hidden" — on a * `hidden: true` column it means always hidden, which is how you declare a column that exists * only to carry a value or a filter. * * Advisory for UI, like `sortable` and `filterable`: `setHidden` is never gated, so a page's own * responsive layout can still hide whatever it likes. What it *does* enforce is that a persisted * snapshot cannot override it — structure outranks a stale saved view. See `applyState`. */ hideable?: boolean; /** * Whether a header UI should offer to pin this column. Defaults to true. Advisory in the same way * as `hideable`, and likewise proof against a snapshot. */ pinnable?: boolean; /** Fixed pixel width (`number`) or a flex weight (`"Nfr"`). Defaults to `"1fr"`. */ width?: ColumnWidth; /** Minimum width for flex columns (px). Defaults to 120. Ignored for fixed-px columns. */ minWidth?: number; /** Maximum width for flex columns (px). Ignored for fixed-px columns. */ maxWidth?: number; /** Whether the column can be resized by dragging its header edge. Defaults to true. */ resizable?: boolean; /** * Whether the column participates in sorting. Defaults to true. Advisory for header UIs * (hide the sort controls); the model's sort APIs are not gated, so programmatic * `setSort`/`applyState` still work. */ sortable?: boolean; /** * A filter over this column's values. The table feeds it `getValue(row)`, so it filters a computed * column as readily as a field one. * * **Prefer the factory form** — `filter: () => new SetFilter()` — whenever the column defs live * outside the component, which is the usual place to put them: * * ```ts * const columns = [{ key: "category", filter: () => new SetFilter() }]; * ``` * * A bare instance in a module-level `const` is constructed once for the lifetime of the module, so * it is shared by every table built from those defs and by every mount of the same one — the * user's selection would survive navigating away and back, and two tables on screen at once would * fight over it. The factory is called once per `ColumnModel`, so each table gets its own and a * remount starts clean. * * Pass an instance when you want exactly that sharing, or when you need a direct reference to * drive the filter from outside the table (a sidebar control). Otherwise reach it through * `table.column(key)?.filter`. * * A discriminant (`filter: "set"`) is the one form deliberately not supported: it would force a * `"set" -> SetFilter` map into the table, so every consumer would ship every filter type. * * Either way the filter survives everything that rebuilds column definitions — `setData`, * `appendRows`, `setColumns` — because `syncColumns` preserves the `ColumnModel` behind an * existing key, and the factory is not called again for a key that already has one. It does *not* * survive `removeColumn`, which destroys the model; the filter type on a key cannot be swapped at * runtime, so use `removeColumn` + `addColumn` if you must. */ filter?: ColumnFilter | (() => ColumnFilter); /** * Whether header UIs should offer this column's filter control. Defaults to true wherever a * `filter` is attached. * * Advisory in exactly the way `sortable` is: the model is never gated, so a `filterable: false` * column whose filter is active still narrows rows. That is the point — it is how a filter driven * from somewhere else (a sidebar, a route param) hides its funnel without giving up the column. */ filterable?: boolean; /** * Whether the built-in cross-column search reads this column, or a text projection to search * instead — `searchable: (r) => fmtTime(r.time)` searches a date column as text rather than as * epoch millis. Defaults to true. * * Applies to hidden columns: it describes the data, not what is on screen. */ searchable?: boolean | ((row: T) => string); /** * Who applies this column's filter. `"client"` narrows rows here; `"server"` means whoever * produced the rows already did. * * Defaults to whichever {@link TableConfig.mode} resolves to, so a server-driven table needs no * per-column annotation — and the default is *resolved through the table* rather than baked in * when the column is built, so pointing an existing table at a paged source with `setData` flips * its columns with it. * * The two sets are **disjoint**, which is what makes a mixed table cheap: a server-mode filter is * never evaluated client-side, so every filter is applied exactly once, in exactly one place. A * server-mode filter contributes to `TableModel.filterQuery` instead of to `predicate`; react to * that, refetch, and `setData`. Client filters then narrow the server's results, because the * table filters over `rows` without replacing them. * * Two consequences for facets, both because `rows` here are already narrowed by this very filter: * a server-mode column never walks the rows (it would discover only the values that survived the * current selection, and the list could never be widened again) and never carries counts (they * would be counts of an already-filtered set). Declare `options` on the filter to give it a * domain; without one its facet list is empty. */ filterMode?: FilterMode; /** * The name this column's data goes by on the server — what lands in `FilterCondition.field`. * Defaults to `key`, which is usually right for a field column and usually wrong for a computed * one. */ field?: string; } interface FieldColumnDef extends BaseColumnDef { key: DotPath; value?: never; } interface ComputedColumnDef extends BaseColumnDef { key: string & Record; value: (row: T) => any; } /** * The built-in row-selection column. Rendered by `` (body) and * `` (header), so it needs no `render`/`value`. `key` is optional — * the table assigns one when omitted. */ interface SelectionColumnDef { selection: true; key?: string; /** See BaseColumnDef.order — declarative placement, lower first, default 0. */ order?: number; pinned?: ColumnPin; hidden?: boolean; hideable?: boolean; pinnable?: boolean; width?: ColumnWidth; minWidth?: number; maxWidth?: number; resizable?: boolean; } //#endregion //#region src/table/search-filter.model.d.ts /** * The table's built-in search filter: one query matched across many columns. * * The second of the table's two kinds of filter, and the reason there are two: matching one query * against *many* columns needs every column's accessor at once, which is the one thing a * `matches(value)` predicate structurally cannot do. So it holds a row `predicate` where a * `ColumnFilter` holds `matches`, and having no column of its own is what the `column` qualifier * excludes it from — `activeColumnFilters`, `clearColumnFilters`, `TableState.columnFilters`. * * It joins `filterPredicate` and `filterQuery` like any column filter, and gets its own * `TableState.search` key. * * * Which columns it reads is per-column config (`searchable`), including hidden ones — see * {@link BaseColumnDef.searchable}. Comparison goes through the same `textMatches` a `TextFilter` * uses, so a per-column "contains" and the search box agree. * * Debouncing is deliberately not here. Like `onStateChange`, the cadence belongs to whoever owns * the input: a client-side search over rows already in memory usually wants none at all. */ declare class TableSearchFilter { readonly table: TableModel; /** The query. Not trimmed — a trailing space is a legitimate part of a "contains" query. */ text: string; get active(): boolean; /** Who does the searching. See {@link TableConfig.search}. */ get mode(): FilterMode; /** * Row predicate, or `undefined` when nothing is typed (the pass-through convention every filter * source here follows). A row passes when *any* searchable column matches — OR across columns, * unlike the AND across filters. */ get predicate(): ((row: RowData) => boolean) | undefined; /** * The query as a `{ op: "search" }` condition for {@link TableModel.filterQuery}, or `undefined` * unless the search is server-mode and non-empty. No `field`: it is not tied to one. */ get condition(): FilterCondition | undefined; constructor(table: TableModel); setText(text: string): void; /** * Clear the query. Note `TableModel.clearColumnFilters()` does *not* call this — wiping text the user * typed as a side effect of "clear filters" is more surprising than leaving it. */ clear(): void; } //#endregion //#region src/table/table.model.d.ts declare class TableModel { readonly config?: TableConfig; rows: RowData[]; columns: Map; columnOrder: string[]; private configuredDefs; private runtimeDefs; private suppressedKeys; /** * The built-in cross-column text search. Always present and inert until something is typed, so * there is no config to switch it on. See {@link TableSearchFilter}. */ readonly searchFilter: TableSearchFilter; scrollX: number; scrollY: number; height: number; width: number; sorts: ColumnSort[]; selectedIds: Set; expandedIds: Set; scrollRequest: { y: number | "end"; } | undefined; private appliedState; private stateReactionDisposer; private rowsReactionDisposer; /** * The live `data` binding: the lazy or getter currently driving the dataset, if it is one of * those. An array is applied outright and leaves nothing to hold. * * Held here rather than read off `config` because it can be replaced — a keyed collection hands * out a *different* lazy per key, so `store.byOrg({ orgId })` is a new lazy whenever `orgId` * changes. See {@link setData}. */ private binding; /** * The status a caller supplies for a dataset that cannot describe its own — see * {@link UseTableConfig.loading}. Meaningless, and ignored, when {@link TableModel.lazy} is set. */ private givenLoading; private givenError; private columnsReactionDisposer; private queryReactionDisposer; private loadMoreReactionDisposer; private restartReactionDisposer; get rowHeight(): number; /** * The space the header occupies: the configured {@link TableConfig.headerHeight}, or `rowHeight` * when it says nothing. * * `` applies this as its border-box height rather than reading it back, so this is * the number *and* the rendered geometry. `` starts below it, and `` * publishes it as `--table-header-height` for consumer CSS that has to line up with where the * rows start. * * Declared rather than derived from what is rendered, so it is right on the first paint and there * is no frame where an overlay is placed against a header height of zero. The consequence is that * a table composed **without** a `` still reserves this much: set * `headerHeight: 0` for one. Nothing but the three overlay slots and the CSS variable read this, * so getting it wrong misplaces an empty state — it cannot drift a row. */ get headerHeight(): number; get rowOverscan(): number; get expansionHeight(): number; get columnOverscan(): number; /** * Stable ids for rows when no `getRowId` is configured, keyed by the row object itself. * * Weak, so it never holds a row alive, and it needs no knowledge of what a row *is* — a dataset * that hands back the same objects keeps its row-keyed state, and one that rebuilds them drops * it. That covers identity-mapped records without the table knowing anything about models. */ private readonly identityIds; private nextIdentityId; private identityId; /** * row → id, from `config.getRowId` when given and from the row's own object identity otherwise. * * The default used to be the row's *index*, which is only safe while the dataset is re-applied * wholesale: a source that replaces its contents in place — which is what a `LazyArray` * does — would leave a selected index pointing at whatever row later occupied that slot. */ get rowIds(): Map; get allColumns(): ColumnModel[]; get orderedColumns(): ColumnModel[]; /** * Resolved pixel width for every column, distributed across the viewport (`width`). * Fixed columns (explicit px or a manual override) claim their width; the rest are flex * (`"Nfr"`, default `1fr`) and share the remaining space by weight, clamped to * [minWidth, maxWidth] via a freeze-redistribute pass (a column that hits a clamp is frozen * and its share is re-split among the others). Any leftover slack — every flex column capped * at its max — is absorbed by the last column so the columns always fill the viewport (this * also soaks up sub-pixel rounding). When the minimums don't fit, the total exceeds the * viewport and the table scrolls horizontally. */ get columnWidths(): Map; get virtualWidth(): number; get virtualHeight(): number; get expandedDisplayIndices(): number[]; get unpinnedColumns(): ColumnModel[]; get firstUnpinnedRenderedIndex(): number; get lastUnpinnedRenderedIndex(): number; get unpinnedRenderedColumns(): ColumnModel[]; get leftPinnedRenderedColumns(): ColumnModel[]; get rightPinnedRenderedColumns(): ColumnModel[]; /** * Everything narrowing the rows client-side, AND-composed into one predicate: every active * client-mode column filter, and the search. `undefined` when nothing is active. * * One predicate rather than several is the point: "what is hiding my rows" has a single answer. */ get filterPredicate(): ((row: RowData) => boolean) | undefined; /** * The rows this table narrowed itself: `rows` with {@link predicate} applied. * * "Client" because that is the only half it applies — a server-mode filter was already applied to * `rows` before they arrived, so running it again here would filter twice. The pipeline reads * `rows` -> `clientFilteredRows` -> `displayRows`, each name saying what that step added. * * Narrowing happens *over* `rows` rather than replacing them, which is what lets selection survive * a filter change and what makes `rows.length` vs this length answer "no data" vs "filtered to * nothing". */ get clientFilteredRows(): RowData[]; /** Columns the built-in search reads — hidden ones included, since `searchable` describes data. */ get searchableColumns(): ColumnModel[]; /** * The columns whose filter is currently narrowing rows — `.length` is the count a filter chip * shows, and the models themselves are what a rail renders removable chips from. * * **Search is not in here.** It holds a row `predicate` rather than a `ColumnFilter`, belongs to * no column, and `clearColumnFilters` does not reset it. The `column` in the name is doing real * work — say what you mean at the call site instead: * * ```ts * table.activeColumnFilters.length + (table.searchFilter.active ? 1 : 0); // everything narrowing * table.activeColumnFilters.some((c) => c.filterMode === "client"); // what Clear would reset * ``` * * Includes hidden and `filterable: false` columns — a filter with no visible control is exactly * the one a chip needs to disclose. */ get activeColumnFilters(): ColumnModel[]; /** * The active column filters this table applies itself — what `clearColumnFilters({ mode: * "client" })` would reset, and so what a facet rail's Clear should gate on. */ get activeClientColumnFilters(): ColumnModel[]; /** The active column filters the server applied — the ones behind `filterQuery`. */ get activeServerColumnFilters(): ColumnModel[]; private activeColumnFiltersIn; /** * The conditions of every active **server-mode** filter, plus the search when it is server-mode * too. `undefined` when there are none. * * Disjoint from `predicate` by construction — a filter is either evaluated here or serialized * here, never both — so nothing is double-applied and there is nothing to reconcile. * * Plain JSON, so it compares with `comparer.structural`: react to it, map the conditions onto * your endpoint's shape, refetch, and `setData`. Debouncing and cursor invalidation are yours — * the table has no idea what a request costs you. * * ```ts * reaction( * () => table.filterQuery, * (query) => void refetch({ where: query?.map(toClause) }), * { equals: comparer.structural }, * ); * ``` */ get filterQuery(): FilterCondition[] | undefined; /** * The lazy currently driving the table, or `undefined` if `data` was an array or a getter. * * Exposed for the one case that cannot be served any other way: a component handed a `TableModel` * and nothing else — a generic table wrapper, a toolbar rendered from context — that wants to * know whether this dataset can be refreshed and offer a control for it. `table.lazy?.reload()` * triggers one, `refreshing` says whether a request is running behind rows already on screen, and * `error` alongside `loaded` says how the last one ended. * * `fetching` covers requests the lazy started by *itself* — revalidating on reobservation, a * `reloadEvery` tick — so an indicator reading it is honest about background work. (The warning * on the lazy's own `fetching` is narrower than it looks: reading it doesn't mark the lazy * *observed*, so it can't keep one alive or trigger a load. A mounted table is already observing * its lazy, so reading through here is safe.) */ get lazy(): LazyArray | undefined; /** * The paged lazy driving the table, or `undefined` — the narrower counterpart of {@link lazy}. * * Exposed for the same reason `lazy` is: a component handed only a `TableModel` — a generic * wrapper, a footer rendered from context — can ask whether this dataset has more to fetch and * get the real source if it does. `loadingMore`, `hasMore` and `total` are all on it. * * A paged source is also what {@link mode} infers from, and the only shape the table drives by * itself: it pushes {@link query} into it and asks for the next page as the window nears the end. */ get pages(): LazyPages | undefined; /** * Who narrows and orders the rows — see {@link TableConfig.mode}. Explicit config wins; * otherwise a paged source means `"server"` and anything else means `"client"`. * * A getter rather than a constructor-time decision, so `setData` pointing an existing table at a * paged source flips its sorting and its columns' filter modes with it. */ get mode(): "client" | "server"; /** Resolved {@link TableConfig.sortMode}: `"manual"` under `mode: "server"` unless overridden. */ get sortMode(): "auto" | "manual"; /** * What a column's `filterMode` falls back to, and what the built-in search's does. Read through * by `ColumnModel.filterMode` rather than copied into each column, so it tracks `mode`. */ get filterMode(): FilterMode; /** * Everything a server needs in order to answer for this table — see {@link TableQuery}. * * **The identity is stable while the contents are**, which is load-bearing rather than an * optimization: it is what lets this be a `useEffect` dependency or a query key without a column * resize, a scroll, or a selection change issuing a request. That takes structural equality * *and* `keepAlive` — mobx applies `equals` only on its cached path, so an unobserved computed * would hand back a new object on every read and defeat the whole point. * * ```tsx * const query = table.query; * useEffect(() => void refetch(query), [query]); * ``` */ get query(): TableQuery; /** * What `aria-rowcount` should report: the extent of the dataset, not of what happens to be * loaded, plus one for the header row. * * It matters most for exactly the tables this is about. A virtualized table already tells * assistive tech the true extent because only a window is in the DOM — but a paged one had been * reporting the rows *fetched so far*, so a screen reader announced "row 30 of 30" about a * dataset of four thousand, and the number grew under the user as they scrolled. * * `-1` is ARIA's own answer for an unknown total, and a cursor-paginated list genuinely has one: * there is more, and nothing has said how much. A client-side filter puts us in the same * position from the other direction — the server's `total` counts rows this table is hiding — so * it falls back rather than reporting a number it knows is wrong. */ get ariaRowCount(): number; /** How many rows one viewport holds, at the fixed row height. */ get visibleRowCount(): number; /** * How many rows lie below the render window — the distance to the end of the content, in rows. * * This is the load-more trigger, and it is a **magnitude rather than a threshold** on purpose. A * boolean (`nearEnd`) only changes on its edges, so the case that matters most silently stalls: * a page lands, a client-side filter rejects most of it, the window is still near the end, the * boolean never changed, and nothing asks for the next page. A number moves every time rows * arrive, so the same `if` fires again and the list keeps filling until it can't: * * ```tsx * useEffect(() => { * if (table.rowsToEnd < PAGE_SIZE) void loadMore(); * }, [table.rowsToEnd, table.rows.length]); * ``` * * The second dependency covers the one gap a display-row count can't: a page whose rows are * *entirely* filtered out doesn't move this at all, and `rows` is the dataset before filtering. * Bind `data` to a paged lazy and none of this is yours — the table drives `loadMore()` itself. * * Measured from the end of the **rendered** window, overscan included, so it is the distance to * the end of what has been committed to the DOM. `0` on an empty table, which reads correctly as * "nothing below here". * * Changes at row granularity rather than per scroll frame (the window bounds are integers), so * reading it in a render subscribes that component to roughly one update per row scrolled — the * cadence `` already re-renders at. */ get rowsToEnd(): number; /** * Nothing has arrived yet and nothing has gone wrong — the state a first-load treatment belongs * to, and the one where the empty slot would be a lie. * * The `error` term is load-bearing. Without it a failed first load reads as loading forever: * nothing ever arrives to end it, and `isEmpty` stays `false` too, so the table shows a permanent * spinner with no way out. `lazy` removed a property with exactly this bug (its own * `loading`, which mishandled a failed first load), and this is the same fix one module over. * * Deliberately not gated on `fetching`. A source typically defers its first request past the * render that observes it, so there is a beat where nothing has arrived and nothing is in flight * either; gating on `fetching` would call that beat "not loading" and flash the empty slot before * the spinner. Absence of a value with no error to explain it is the honest reading. * * Only ever a *first* load. A request running behind rows already on screen is not this and has * no state here at all: the rows stay rendered and fully interactive, because replacing them to * fetch mostly-identical ones would throw away scroll position, column arrangement and selection. * Whoever owns the fetching knows a refresh is running — `refreshing` on a lazy, `isFetching` on * a query — and can say so somewhere that isn't the rows. * * Reported for either form of dataset: a lazy works it out itself, and an array or getter is * described by the `loading` prop passed to `useTable`. A model given a bare array and never told * otherwise has no loading story, and does not invent one. */ get loading(): boolean; /** * The request failed and there is nothing to show for it — the fatal state, and the only one * `` renders for. * * A failure behind rows that are still on screen is deliberately *not* this. Blanking a working * table over a background request would destroy scroll position, column arrangement and selection * for something the user never asked for, so a failed refresh leaves this `undefined` and the * table carries on showing what it has. Whoever owns the fetching still has that error — on the * lazy as `error`, or in hand as the prop they passed — and can surface it somewhere that isn't * the rows. * * Raw passthrough — whatever the lazy was rejected with, or whatever was handed to `useTable` as * the `error` prop. Unwrapped and uninterpreted either way. */ get error(): unknown; /** * Supply the status for a dataset that cannot describe its own. Called by `useTable` on every * render whose `loading` or `error` prop changed; pointless for a lazy, which is asked directly. */ setStatus(loading: boolean, error: unknown): void; /** * There is genuinely nothing to show — as opposed to nothing *yet*, or nothing *because the * request failed*. This is the gate the empty slot uses, and the reason a table over a loading * source never claims "no results". * * A failed first load is excluded for the same reason a running one is: "No results" is a lie * about a request that never came back with any. Fixing `loading` without fixing this would only * trade a permanent spinner for a permanent — and wrong — empty state. */ get isEmpty(): boolean; get displayRows(): RowData[]; get firstRenderedIndex(): number; get lastRenderedIndex(): number; get renderedRows(): RowData[]; get virtualOffsetX(): number; get virtualOffsetY(): number; get renderedColumns(): ColumnModel[]; get visualColumns(): ColumnModel[]; get displayRowIndexMap(): Map; /** Whether the table has a selection column (drives aria-multiselectable / aria-selected). */ get selectable(): boolean; get gridTemplateColumns(): string; /** Whether the viewport is scrolled to (within one row of) the end of the content. */ get atEnd(): boolean; /** The selected row objects, in source order. Derived from `selectedIds`, so ids without a * matching row (possible only if a consumer mutates `selectedIds` directly) drop out. */ get selectedRows(): RowData[]; /** * The selected rows the user can currently see — selection intersected with the filter. The * counterpart to `selectedRows`, which spans the whole dataset: selection is keyed to a row * *existing*, not to it being visible, so filtering something out does not deselect it. * * Use this for a bulk action that should only touch what is on screen, and `selectedRows` for one * that should touch everything the user has picked. */ get visibleSelectedRows(): RowData[]; /** * Whether every *visible* row is selected — the header checkbox's state. Derived from * `visibleSelectedRows` rather than `selectedRows`, so a selection hidden by the filter can't * report the header as fully checked when nothing on screen is selected. */ get allRowsSelected(): boolean; get someRowsSelected(): boolean; constructor(config?: TableConfig); /** * (Re)start the model's reactions. Pairs with `dispose` — `useTable` calls both across * effect cycles, so a StrictMode dev remount (mount → cleanup → mount against the same model) * re-arms them instead of leaving the surviving model deaf. No-op for a reaction already * running, or one the config gives nothing to do. */ activate(): void; /** * Point the table at a different dataset — a new array, getter or lazy. * * The one setter, because the three shapes differ only in who decides the rows changed. An array * is applied outright; a getter or a lazy becomes the binding a reaction reads through, which is * what makes a keyed collection work: `store.byOrg({ orgId })` hands back a different lazy per * key, and the table has to follow it rather than keep reading the one it was built with. * * Row-keyed state is not cleared: rows are intersected, so with `getRowId` configured a row * present in both datasets keeps its selection and expansion. */ setData(data: RowData[] | (() => RowData[]) | LazyArray | LazyPages): void; /** Drop the model's reactions. Pairs with `activate`. */ dispose(): void; rowId(row: RowData): RowId | undefined; /** Snapshot of the user-curated arrangement (see `TableState`). JSON-serializable. */ getState(): TableState; /** * Restore a (possibly partial) snapshot. Keys with no matching column are kept aside and land * when a matching column appears (see `appliedState`); columns the snapshot doesn't mention are * left as they are, ordered after the snapshot's columns. */ applyState(state: Partial): void; private applyFilterState; /** The column under this key, if it exists. */ column(key: string): ColumnModel | undefined; /** * `predicate` without the named column's own filter — what that column's facet counts are tallied * over, so each option answers "how many rows would this add". * * Everything else stays in, including the search and page-level sources: a row those already * exclude must not be counted, or the tally promises rows the selection could never surface. */ filterPredicateExcluding(key: string): ((row: RowData) => boolean) | undefined; /** * Reset every column filter, or only those on one side of the client/server split * (`clearColumnFilters({ mode: "client" })`). * * Leaves the search filter alone — it is the other kind of filter, and the `column` in this name * says so. Wiping text the user typed as a side effect would be surprising anyway; clear it * explicitly with `searchFilter.clear()`. */ clearColumnFilters(opts?: { mode?: FilterMode; }): void; private composePredicate; /** * Replace the column definitions. Takes over from `config.columns` — and from the * derive-from-the-first-row default, so a table that was deriving its columns stops doing so. * * Columns that survive the change keep everything the user did to them: display position, * visibility, pinning and manual width. Columns no longer defined are dropped; their entries in * the sort list are left in place but inert (as for any column that disappears), so restoring * the column restores its sort. */ setColumns(defs: ColumnsDef): void; /** * Add one column definition — the runtime counterpart to a `config.columns` entry, for * user-curated columns (a picker adding a metric that isn't on the row objects; see * `ComputedColumnDef.value`, which can read any observable, not just the row). * * `index` is the position in the display order; omitted, the column lands last. The column is * always shown, even if a persisted snapshot had it hidden — adding a column means showing it. * * Throws when the key is already taken. Column pickers should offer only what isn't added yet * (`table.columns.has(key)`), so a collision here is a bug rather than a user action. */ addColumn(def: ColumnDef, index?: number): void; /** * Remove the column with this key. A no-op when no def matches — including for columns produced * by a factory def, whose keys aren't known until they're built: hide those * (`ColumnModel.setHidden`) or replace the list with `setColumns`. * * Removal drops the column's live state but not any *persisted* state for it, so a later * `addColumn` with the same key restores the pinning and width the user had given it. */ removeColumn(key: string): void; /** Move a column to a new index in the display order. */ moveColumn(key: string, toIndex: number): void; /** * Replace the dataset. Row-keyed state (selection, expansion) is **intersected** against the * incoming rows: an id that still resolves to a row survives, and one that does not is dropped. * A refresh — a refetch, a poll, an invalidation — therefore arrives without clearing the user's * selection, while genuinely switching datasets drops it naturally. * * What "still resolves" means depends on where the ids come from: * * - **With `getRowId`** they are derived from the data, so the same record survives even when it * arrives as a different object. That is what a plain-JSON refetch needs. * - **Without it** they follow the row's object identity, so state survives for a dataset that * hands back the same objects — anything identity-mapped — and is dropped for one that rebuilds * them, which is the honest answer there. * * Use `appendRows` to add without resetting. Re-passing the array already in place is a no-op: * same array, same dataset. (`rows` is an `observable.ref`, so mutating one in place is invisible * either way — hand over a new array to change the data.) */ private applyRows; /** Append rows without resetting row-keyed state — the "load more" path. Existing rows keep * their ids either way, so selection survives. */ appendRows(rows: RowData[]): void; setScroll(x: number, y: number): void; /** Content offset of a display index's block top (row plus any expansion panels above it). */ blockOffset(index: number): number; /** Scroll so the row's block top lands at the viewport top, or its block end at the bottom. */ scrollToRow(row: RowData, align?: "top" | "bottom"): void; /** * Scroll back to the first row. * * Called by the model itself when a paged source restarts — a query change, a reload — because * a scroll offset measured against fifty pages is meaningless against one, and leaves the user * parked past the end of the new results. */ scrollToTop(): void; /** Scroll to the very end of the content. */ scrollToEnd(): void; clearScrollRequest(): void; setWidth(width: number): void; setHeight(height: number): void; /** * Set a column's sort. By default the whole sort list is replaced (single-sort behavior). * With `preserve: true` existing sorts are kept: a column already in the list changes * direction in place (keeping its priority), a new column is appended at the lowest priority. */ setSort(key: string, direction: SortDirection, opts?: { preserve?: boolean; }): void; /** Replace the whole sort list at once (restoring a saved view); `setSort` covers per-column interactions. */ setSorts(sorts: ColumnSort[]): void; /** Remove one column from the sort (later entries move up in priority), or all sorts when no key is given. */ clearSort(key?: string): void; isRowSelected(row: RowData): boolean; toggleRow(row: RowData): void; selectAllRows(): void; clearSelection(): void; toggleAllRows(): void; isRowExpanded(row: RowData): boolean; toggleRowExpanded(row: RowData): void; collapseAllRows(): void; /** * The def list to build columns from: the explicit list when there is one, otherwise the first * row's keys. Materializing the fallback here is what lets `addColumn`/`removeColumn` build on * a derived column set instead of replacing it. */ /** * The defs a sync builds from: what the consumer curated, then what was added at runtime, then * whatever `autoColumns` makes of the first row's remaining keys — minus anything `removeColumn` * suppressed. */ private effectiveDefs; /** Defs for first-row keys no curated column covers. Empty unless `autoColumns` is in play. */ private autoDefs; /** Keys the consumer configured, ignoring factory defs, which resolve only at sync time. */ private configuredKeys; private assertUniqueKeys; private syncColumns; private applyColumnState; private mergedOrder; private expandedAbove; /** * The display index of the row whose block (row + its expansion panel, if any) contains the * vertical content offset `y`. Walks the expanded indices accumulating their extra height — * a row scrolled past its own top stays "at" `y` while its panel is in view, so expanded rows * render as long as any part of their block does. */ private indexAtOffset; } //#endregion //#region src/table/column.model.d.ts /** Key assigned to a selection column when its def doesn't supply one. */ declare const SELECTION_COLUMN_KEY = "__selection__"; declare class ColumnModel { readonly table: TableModel; /** * The resolved column configuration. An `observable.ref` — replaced wholesale by * {@link setConfig}, never mutated in place — so every getter over it is genuinely reactive. */ config: ColumnConfig; /** Which edge this column is pinned to. Never `undefined` — an unpinned column is `false`. */ pinned: ColumnPin; hidden: boolean; manualWidth: number | undefined; /** Resolved pixel width — distributed across the viewport by the table (see `columnWidths`). */ get width(): number; /** Fixed pixel width when the column isn't flexible (manual override or an explicit number). */ get fixedWidth(): number | undefined; /** Flex weight (the `N` in `"Nfr"`); `0` when fixed. An unspecified width means `1fr`. */ get grow(): number; get minWidth(): number; get maxWidth(): number; get resizable(): boolean; /** * Whether a column picker should offer to change this column's visibility. See * {@link BaseColumnDef.hideable} — it locks `hidden` at its initial value rather than forbidding * hiding, and `setHidden` is never gated by it. */ get hideable(): boolean; /** Whether a header UI should offer to pin this column. See {@link BaseColumnDef.pinnable}. */ get pinnable(): boolean; /** Whether header UIs should offer sorting on this column (selection columns never do). */ get sortable(): boolean; /** Whether this is the built-in row-selection column. */ get selection(): boolean; /** * True for the innermost pinned column on its side (the one bordering the scrollable area). * Consumers hang the pinned boundary shadow off `[data-pinned-edge]` so a group of pinned * columns shows a single shadow at the seam. */ get isPinnedEdge(): boolean; /** * True for the outermost pinned column on its side (the one at the viewport edge). Used by the * header to round its outer corners so a pinned column doesn't paint a square over the rounded * header background. Both rendered pinned arrays are ordered outer-edge-first. */ get isPinnedOuterEdge(): boolean; get offset(): number; get title(): string; /** 1-based visual column position (pinned blocks at the edges) — the aria-colindex value. */ get ariaColIndex(): number; get key(): string; /** Active sort direction for this column, or undefined when it doesn't participate in the sort. */ get sortDirection(): SortDirection | undefined; /** 1-based position in the sort priority (the "1"/"2" badge in multi-sort UIs); undefined when unsorted. */ get sortIndex(): number | undefined; /** * The filter attached to this column's def, if any — already resolved, so a factory def has been * called exactly once, when this column was built. See {@link BaseColumnDef.filter}. */ get filter(): ColumnFilter | undefined; /** * Whatever the def said this column represents — see {@link ColumnMeta}. * * A plain getter over `config`, which is an `observable.ref` replaced wholesale, so this is * reactive with no state of its own. Every render-prop the table has already receives the * `ColumnModel`: header cells through ``, body cells through ``. */ get meta(): ColumnMeta | undefined; /** * Whether header UIs should offer a filter control. Advisory exactly like `sortable`: the model is * never gated, so a `filterable: false` column with an active filter still narrows rows. */ get filterable(): boolean; /** * Who applies this column's filter. See {@link BaseColumnDef.filterMode}. * * Falls back to the table's resolved mode rather than to a literal, and reads *through* the table * rather than capturing it: columns are built before the data exists and `setData` can point an * existing table at a paged source, so a default baked in at construction would leave those * columns filtering client-side over one page of a server-driven dataset. */ get filterMode(): FilterMode; /** The name this column's data goes by on the server. Defaults to `key`. */ get field(): string; /** * This column's contribution to {@link TableModel.filterQuery} — its filter's condition tagged * with `field`. `undefined` unless the column is server-mode with an active filter. */ get filterCondition(): FilterCondition | undefined; /** Whether the built-in search reads this column. See {@link BaseColumnDef.searchable}. */ get searchable(): boolean; /** * The value this column's filter compares against — its own `facets` domain, in other words. * * `[]` when no filter is attached: a distinct-values API for every column would be a different * (and much more expensive) feature, and nothing here should be mistaken for one. * * A filter with a `project` (a `BucketFilter`, say) lists its *projected* domain — grades rather * than scores — while the column goes on showing and sorting the raw value. * * Three cost tiers, chosen by the filter rather than configured here: * * | tier | when | walk | * | --- | --- | --- | * | static | `options` declared, `counts` falsy | none | * | values | the default | `rows`, invalidated by `rows` alone | * | counted | `counts: true` | `rows` narrowed by every *other* active filter | * * The walk itself is not what costs — running every other filter per row is, plus the * invalidation storm where one toggle dirties every other column's facets. That is what `counts` * gates, and why the default tier still populates a checkbox list. * * Ordering is declared `options` first in declaration order, then discovered values sorted by * value, blank last. Insertion order alone would be first-appearance-in-rows, which reshuffles * the list every time the table is sorted. * * A count means "how many rows carry this value", among rows passing every *other* filter — so it * previews what picking it gives you. Under a set filter's `"all"` mode, where each pick narrows * instead of widening, it is the size of the intersection with the current selection instead. * * Zero-count entries are kept: a popover is exactly where you go to undo an over-narrowed filter. * A standing facet rail drops them at the call site — * `facets.filter((f) => f.count > 0 || filter.has(f.value))`. */ get facets(): Facet[]; private get facetScan(); private get pinnedSiblings(); constructor(table: TableModel, config: ColumnConfig); /** Pin to an edge, or `false` to unpin. */ setPinned(pinned: ColumnPin): void; setManualWidth(width: number | undefined): void; setHidden(hidden: boolean): void; /** * Patch this column's configuration — the way to drive a column option from something the table * was not constructed with, such as React state or a prop: * * ```tsx * useEffect(() => table.column("amount")?.setConfig({ title: label }), [label]); * ``` * * `setColumns` deliberately cannot do this: it preserves the `ColumnModel` behind a key it already * has, so a new def for an existing key is ignored wholesale. That is what keeps a column's * position, width, pinning and filter through a def change — and it is why patching is a separate * operation rather than a side effect of redeclaring. * * Everything the user has done to the column survives: `hidden`, `pinned` and `manualWidth` are * their own state, not configuration. `key`, `filter` and `selection` cannot be patched — see * {@link ColumnConfigPatch}. * * One coupling worth knowing: a def with no `render` had it defaulted to `value` when the column * was built, so patching `value` alone changes what is sorted and filtered but not what is * displayed. Patch both to change both. */ setConfig(patch: ColumnConfigPatch): void; /** Sort by this column — replaces the sort list unless `preserve: true` (see TableModel.setSort). */ sortBy(direction: SortDirection, opts?: { preserve?: boolean; }): void; /** Remove this column from the sort; other columns' sorts are untouched. */ clearSort(): void; /** Raw cell value for a row — what sorting compares and the default render displays. */ getValue(row: RowData): unknown; /** * What the built-in search matches this row against: the `searchable` projection when one is * given, otherwise the raw cell value. */ searchValue(row: RowData): unknown; /** Reset this column's filter, if it has one. A no-op otherwise. */ clearFilter(): void; /** Ascending comparison of two rows by this column's extracted values (`compare` def or the default). */ compareRows(a: RowData, b: RowData): number; /** * The key a def will produce, without building the column — what `TableModel` matches defs by * (`removeColumn`) and checks for collisions with. Mirrors `fromDef`: a string def is its own * key, and a selection def may omit one. */ /** * The `meta` a def carries, or `undefined`. Narrowing lives here rather than at the call site * because a string def and a selection def have no place for one — a selection column is a * checkbox, not a column *about* something. */ static metaOf(def: ColumnDef): ColumnMeta | undefined; static keyOf(def: ColumnDef): string; static fromDef(table: TableModel, def: ColumnDef): ColumnModel; } //#endregion //#region src/table/components/cell-slot.d.ts type RenderColumn = (column: ColumnModel) => ReactNode; /** * Per-cell reactive boundary. `Table.Header`/`Table.Row` iterate the rendered columns and hand each * off to a `CellSlot` rather than calling the consumer's render function inline — so the render runs * inside *this* component's MobX reaction. The upshot: a cell re-renders only when the observables * *it* reads change (e.g. one field of one row), never because a sibling cell or the row did. * * It renders a transparent fragment, so whatever the consumer returns (a ``) lands * directly in the parent grid with no wrapper element. */ declare const CellSlot: import("react").FunctionComponent<{ column: ColumnModel; render: RenderColumn; }>; //#endregion //#region src/table/components/cell-style.d.ts /** * Structural style shared by header and body cells: pinned cells stick to their edge at the * column's offset and must be opaque (they overlap scrolling cells). The opaque fill is a CSS var * so the consumer owns the color — set `--table-pinned-bg` once (and override it inside the header * to match a header background). `Canvas` is a theme-aware system default for zero-config use. * * These are the *only* styles the library forces on a cell; everything cosmetic (padding, font, * borders, hover) is left to the consumer's `className`/`style`. */ declare const pinnedCellStyle: (column: ColumnModel) => CSSProperties; //#endregion //#region src/table/components/checkbox.d.ts /** * Props a selection-control component receives. Kept intentionally minimal so any checkbox — the * native input below, a Chakra `Checkbox`, a Tailwind one — can satisfy it. */ interface TableCheckboxProps { checked: boolean; indeterminate?: boolean; onChange: () => void; "aria-label"?: string; } /** * Zero-config fallback used by `` / `` when the consumer * neither registers a `checkbox` on `` nor passes a render-prop. `indeterminate` is a * DOM-only property, so it's applied via ref rather than an attribute. */ declare const NativeCheckbox: FC; //#endregion //#region src/table/components/column-resizer.d.ts interface TableResizerProps { column: ColumnModel; } /** * Drag handle on a header cell's edge that resizes its column. Dragging sets the column's * `manualWidth` (treated as a fixed width in the distribution, so the remaining flex columns reflow * to fill); double-click resets it to auto. Right-pinned columns are anchored to the right, so their * handle sits on the left edge and the drag delta is inverted. * * Move/up listeners live on the window for the duration of the drag, so the resize tracks and ends * wherever the pointer is released — not only over the handle. */ declare const TableResizer: FC; //#endregion //#region src/table/components/table-root.d.ts interface TableRootProps { table: TableModel; children?: React.ReactNode; style?: React.CSSProperties; className?: string; /** * Selection control used by `` / `` when no render-prop is * given. Register it once here to capture your app's checkbox everywhere. Defaults to a native * ``. */ checkbox?: FC; } /** * The table's outer box: a flex column holding `` and whatever chrome sits outside * the scrollbars — ``, a toolbar, pagination. Owns the shared CSS variables and * the model context; measures nothing itself. * * Chrome is an ordinary flex child, so the space it takes is subtracted from the scrolling box by * the browser rather than reserved by arithmetic here. Nothing has to be told how tall a status bar * is, and the vertical scrollbar terminates at it instead of running past. * * `className`/`style` land here, on the box a border and a `border-radius` belong on; * `` takes its own for styling the scrolling area. * * **The table's shape is three CSS values on this element**, which is why there is no prop for any * of them — `style` reaches the right box, and the browser does the rest: * * | | | * | --- | --- | * | *(default)* | `height: 100%` — fills a sized parent; a short list leaves dead space below the rows, which is the classic table shape and what a bordered design wants | * | `maxHeight: 480` | caps the box, so whatever follows in flow sits directly beneath it | * | `height: "auto"` | hugs the rows — no dead space, and `` follows the last row. Pair it with a `minHeight` if the table can be empty, since an empty box hugs to just its header and `` has nowhere to put the message | */ declare const TableRoot: FC; //#endregion //#region src/table/components/table-header.d.ts interface TableHeaderProps { className?: string; style?: CSSProperties; /** * Renders one header cell. Called once per *rendered* column (the library owns which columns are * live, their order, and the virtualization spacer); return a `` / * ``. */ children: RenderColumn; } /** * The sticky header row group. Owns layout — the grid track template, the left-pinned / spacer / * unpinned / right-pinned ordering — and defers each cell's content to the `children` render-prop * via a per-cell `CellSlot`. * * Sized by `table.headerHeight` — the configured `headerHeight`, or `rowHeight` when the config * says nothing — applied as a **border-box** height so that number is the space the header * occupies rather than a claim about it. `` starts below it and `` * publishes it as `--table-header-height`, and nothing has to be kept in agreement because there * is one number. * * Express the gap between the header and the rows as padding here: border-box means padding comes * out of that height rather than adding to it, so the published number stays true. Content needing * more room than the height overflows onto the first row instead of growing the header — raise * `headerHeight` for a taller one. * * The inner `role="row"` stretches to fill whatever is left, so it carries no height of its own — * header cells are free to be styled without fighting an inline number. * * Nothing here reports back to the model: `table.headerHeight` is what the config says, so it is * already right when the first frame paints. A table composed without this component still * reserves that height for its overlays — `headerHeight: 0` is how you say there is no header. */ declare const TableHeader: FC; interface TableColumnHeaderProps extends HTMLAttributes { column: ColumnModel; } /** * A single header cell. Owns the structural bits (sticky pinning, offset, `data-pinned*`) and stays * cosmetically open — the consumer's `className`/`style` add padding, font, borders, etc.; other * DOM props pass through. */ declare const TableColumnHeader: FC; //#endregion //#region src/table/components/table-body.d.ts interface TableBodyProps { className?: string; style?: CSSProperties; /** Renders one row. Called once per *rendered* row; return a ``. */ children: (row: RowData) => ReactNode; } /** * The virtualized body. Owns the scroll-sized spacer and the `translate3d` window offset, then maps * the rendered slice of rows through the `children` render-prop. Rows are keyed by their row id * (see `rowIds`): by default the row's own object identity — stable under sort, filter, scroll and * `appendRows` — or the consumer's `getRowId`, which stays stable even when a refetch replaces the * row objects with fresh ones. */ declare const TableBody: FC; interface TableRowProps extends Omit, "children"> { row: RowData; /** Renders one body cell. Called once per rendered column; return a `` / ``. */ children: RenderColumn; } /** * Memoized on identity so scrolling (and filtering) only renders rows that actually entered/changed. * `children` (the per-column render-prop) gets a fresh closure on every `TableBody` render, so it is * *deliberately excluded* from the comparison — for a row still in the window that closure is * equivalent (it closes over the same `row` and reads row/column state live). Every other prop — * including pass-through DOM props like `onClick` — is compared shallowly. Layout changes still * flow through because the inner `observer` re-renders on the column/width observables it reads, and * per-cell data changes flow through each `CellSlot`'s own observer — neither is gated by this memo. * (Pass stable `className`/`style`/handlers, not fresh inline values, or the row re-renders every frame.) */ declare const TableRow: import("react").NamedExoticComponent; interface TableCellProps extends HTMLAttributes { column: ColumnModel; } /** * A single body cell. Owns pinning/offset/`data-pinned*`; cosmetics are the consumer's via * `className`/`style`, other DOM props pass through. */ declare const TableCell: FC; //#endregion //#region src/table/components/table-overlay.d.ts type TableOverlayProps = HTMLAttributes; /** * The placement primitive every table-wide message is built from, and the one the gated slots — * ``, ``, `` — each wrap. Render it anywhere inside * `` and show it yourself. * * It exists as public API because the placement is the hard part and the gate isn't. Filling the * viewport below a sticky header, staying centred in the visible area at any scroll offset, and * sizing off the table's own height takes a measured header and an anchor the consumer can't move; * deciding whether to mention a failed save takes an `if`. The gated slots cover the states the * table can evaluate for itself — everything else is yours: * * ```tsx * {saveError && Couldn't save changes} * ``` * * Carries no data attribute of its own: `data-empty` and friends mean "the table decided this", * and a hand-shown overlay hasn't earned that claim. Pass your own if you want a styling hook. * * Structurally it is an out-of-flow anchor with a sticky child. Both halves are load-bearing: * * - The anchor is **absolutely positioned at the top of the scrollport**, so where it lands does * not depend on where in `` this was written. A sticky element would inherit its * flow position instead, putting the message below the rows rather than over them — and there is * no ordering rule that could fix that, since it would have to be `virtualHeight` above wherever * the consumer put it. * - Being out of flow is also what keeps the overlay from contributing to the scrollport's content * height, which would otherwise make a root that hugs its content circular — the box sized from * the overlay, the overlay sized from the box. * - The anchor spans the **scrollable extent** rather than the viewport, because that is the room * the sticky child needs to travel: a shorter anchor clamps it and the message drifts up as you * reach the bottom. `table.height` is the floor for the empty case, where there is no extent to * speak of, and it is the measured client box — so the anchor adds no scrollable overflow of its * own and an empty table gets no scrollbar out of it. * * The extent is the model's (`virtualHeight`), not the scrollport's measured `scrollHeight`, * which is what keeps this free of a second measurement. The one thing that gets past it is * ``: it adds a row's worth of flow content the model doesn't count here, so an * overlay shown *over rows* with a gutter present drifts up by the gutter's height at the very * bottom of the scroll. Nothing in the library can reach that today — all three gated slots * render only when there are no rows, and so nothing to scroll — and the alternative is the * gutter reporting its height to the model for one bounded edge case. * - The sticky child is what keeps the message in the visible area at any scroll offset, on the * compositor rather than through a re-render per scroll event. * * `z-index` sits between the rows (`auto`) and the header (`20`), so what the overlay covers is * decided here rather than by the order the consumer happened to write things in. The anchor is * `pointer-events: none` so only the sized box intercepts, exactly as when the box was the whole * of it. */ declare const TableOverlay: FC; //#endregion //#region src/table/components/table-loading.d.ts interface TableLoadingProps extends TableOverlayProps { /** * Timing for the indicator, passed to `useSlowLoading`. Defaults to 300 ms before it appears and * 300 ms minimum on screen, so a fast first load renders nothing at all rather than flashing. * * Pass `false` to show it the moment loading starts. */ sustain?: boolean | SlowLoadingOptions; } /** * The first-load surface. Render it inside `` alongside ``; it shows * itself only while the table has nothing yet and a request is in flight, and only once that wait * has gone on long enough to be worth mentioning. * * It has nothing to say about a *refresh* — rows already on screen stay put and stay interactive, * because replacing them to fetch mostly-identical rows would throw away scroll position, column * arrangement and selection. Nor does the table: a request running behind rows it already has is * not its business, and whoever owns the fetching knows about it anyway (`refreshing` on a lazy, * `isFetching` on a query). Put a quiet indication somewhere that isn't the rows themselves. * * Needs the table to have been told about loading — a `data` that is a lazy, which knows on its * own, or a `loading` prop passed to `useTable` alongside an array or getter. */ declare const TableLoading: FC; //#endregion //#region src/table/components/table-error.d.ts interface TableErrorProps extends Omit { /** * What to say about the failure. A function is called with whatever the source failed with, so a * message can be derived from it without reaching back into the model: * * ```tsx * {(error) => (error instanceof HttpError ? error.status : "Something went wrong")} * ``` */ children?: ReactNode | ((error: unknown) => ReactNode); } /** * The failure surface. Render it inside `` alongside `` and * ``; it shows itself only when the request failed and left nothing to show for it. * * **A failed *refresh* does not render this**, and that is the whole point of the gate. Rows * already on screen are still perfectly good rows, and blanking a working table because a * background request came back 500 destroys scroll position, column arrangement and selection over * something the user never asked for. The table says nothing at all about that case; the error is * still on your lazy, or still in the prop you passed, and belongs on a refresh control or in a * toast — somewhere that isn't the rows. * * The three slots are mutually exclusive by construction, so ordering them is not your problem: * `loading` excludes a failure, `isEmpty` excludes both, and this renders only for the failure * with nothing behind it. * * Needs the table to have been told about the failure — a `data` that is a lazy, which carries its * own `error`, or an `error` prop passed to `useTable` alongside an array or getter. If the error * isn't about the dataset at all, `` gives you the same surface with no gate. * * Owns placement only — cosmetics are the consumer's, and `data-error` is the styling hook. */ declare const TableError: FC; //#endregion //#region src/table/components/table-gutter.d.ts interface TableGutterProps extends HTMLAttributes { children?: ReactNode; /** * Height in pixels. Defaults to the table's `rowHeight`, so the strip reads as one more row and * needs no measuring — the same fixed-height contract the rows themselves are under. */ height?: number; } /** * One more row's worth of space at the **end of the rows**, inside the scroll flow. * * You only see it by scrolling to the bottom of the list, which is exactly what it is for: the * indicator that shows up when you outrun the fetch. With a paged source the table is already * loading the next page as the window nears the end, so the only thing left to say down there is * either "still coming" or "that was all": * * ```tsx * * {feed.loadingMore ? : !feed.hasMore && } * * ``` * * Render it after ``. It is part of the scroll content rather than floating over it, * which is the whole point and the reason it can't be built from ``: an overlay * fills the viewport and stays centred in it, so a message built on one would cover the rows * instead of following them. `` already reserves the virtualized height in normal flow, * so this lands after it with no geometry of its own. * * **It is not a bar across the bottom of the table.** A persistent "Showing 1,000 of 2,000" belongs * on screen whether or not you have scrolled anywhere, which is a different component in a * different place — and the two are wanted together, a spinner at the tail *and* a count that is * always visible. Nor is it a ``: that is a row, aligned to the columns and scrolling * horizontally with them, where this is a strip that knows nothing about columns. * * **Ungated, unlike `` / `` / ``** — what goes here is * what the *source* knows, so the condition is yours. Nothing needs guarding for the empty and * error states in practice, since both render an overlay across the viewport and there are no rows * to scroll past to reach this. * * **Entirely ungated** — unlike `` / `` / ``, which render * the states the table can evaluate for itself. What goes here is what the *source* knows, so the * condition is yours: * * ```tsx * * {feed.loadingMore ? : feed.hasMore ? null : } * * ``` * * Reach the source through `table.pages` when all you have is the model: * * ```tsx * const FooterStatus = observer(() => { * const { pages } = useTableContext(); * if (!pages) return null; * return pages.loadingMore ? : pages.hasMore ? null : ; * }); * ``` * * Rendering `null` children still occupies the strip. Skip the element entirely for no footer at * all — a table with nothing to say below its rows shouldn't reserve a row's worth of space. * * Sticky-left at the visible width, like every other table-wide surface, so it stays put under * horizontal scrolling rather than sliding out of view with the columns. Vertically it scrolls with * the rows — it is part of the list, not part of the frame. */ declare const TableGutter: FC; //#endregion //#region src/table/components/table-status-bar.d.ts interface TableStatusBarProps extends HTMLAttributes { children?: ReactNode; /** * Height in pixels. Defaults to the table's `rowHeight` — the same fixed-height contract the * rows are under, so nothing has to be measured. */ height?: number; } /** * A bar across the bottom of the table — "Showing 1,000 of 2,000", a Load all button, page * controls. Render it as a direct child of ``, **after** ``: * * ```tsx * * … * * Showing {table.rows.length} of {table.pages?.total ?? table.rows.length} * {table.pages?.hasMore && ( * * )} * * * ``` * * **Outside the scrolling box**, which is the whole design. Inside it the bar sits within the box * the scrollbars measure: the vertical scrollbar spans past it no matter what, and the bar stops * short of the scrollbar gutter — a divider along its top edge visibly runs out into the middle of * a scrollbar. Out here it spans the full width, gutter included, and the scrollbar terminates at * its top edge. * * Being an ordinary flex child of the root is also what makes it free of arithmetic. The space it * takes is subtracted from `` by the browser, so `table.height` — and with it the * render window, the auto-fetch threshold and `` — is already correct. Nothing has * to be told how tall the bar is. * * It follows the rows on a short list only if the table hugs its content; by default the table * fills its parent and the bar sits at the bottom with the dead space above it. Hug with * `style={{ height: "auto" }}` on ``. * * A plain block: no `position`, no `z-index`, no background, because nothing scrolls behind it. * Style it through your own `className`, or `[data-table-status-bar]`. * * **Not a ``.** That is a row: aligned to the columns, scrolling horizontally with them, one * cell per column — and therefore something that has to live *inside* the scrollport. This spans * the table and knows nothing about columns. The name `Table.Footer` is left free for the row. * * **Ungated**, like `` and unlike `` / `` / * ``. Those three describe states the table can evaluate; a row count is the source's * business. Note that out here it no longer overlaps the overlay surfaces, so a bar reading * "Showing 0 of 0" now sits *below* "Couldn't load" rather than painting over it. Gate it yourself * if showing both at once reads badly: * * ```tsx * {!table.error && !table.loading && …} * ``` */ declare const TableStatusBar: FC; //#endregion //#region src/table/components/table-expansion.d.ts interface TableExpansionProps { row: RowData; className?: string; style?: CSSProperties; children?: ReactNode; } /** * Memoized on row identity like `TableRow`, with `children` deliberately excluded: the panel's * element tree is rebuilt by the body render-prop every window shift, but for the same row it is * equivalent. Panel content must derive from `row` (or be an observer reading live state) — not * from other values captured in the render-prop closure. */ declare const TableExpansion: import("react").NamedExoticComponent; //#endregion //#region src/table/components/selection.d.ts type RowSelectState = Pick; type SelectAllState = Pick; interface SelectionCellProps { column: ColumnModel; row: RowData; /** Custom control. Omit to use the checkbox registered on `` (native by default). */ children?: (state: RowSelectState) => ReactNode; } /** A body cell wired to per-row selection. Renders the registered checkbox unless given a render-prop. */ declare const SelectionCell: FC; interface SelectAllProps { /** Custom control. Omit to use the checkbox registered on `` (native by default). */ children?: (state: SelectAllState) => ReactNode; } /** The select-all control (checked / indeterminate / none). Place inside a header cell, or use ``. */ declare const SelectAll: FC; interface SelectionHeaderCellProps { column: ColumnModel; children?: (state: SelectAllState) => ReactNode; } /** A header cell holding the centered select-all control — the header twin of ``. */ declare const SelectionHeaderCell: FC; //#endregion //#region src/table/components/namespace.d.ts /** * Compound namespace for the table skeleton. Consumers compose these into their own closed * component (styles + defaults captured once), e.g. * `…`. * * Two boxes, and which one a part goes in is the whole structure: `Root` is the outer frame, * `Scroll` is the box that overflows. Header, body, rows and the overlay surfaces go inside * `Scroll`; chrome that must sit outside the scrollbars — `StatusBar`, a toolbar, pagination — * goes directly in `Root`. */ declare const Table: { Root: import("react").FC; Scroll: import("react").FC, HTMLDivElement>>; Header: import("react").FC; ColumnHeader: import("react").FC; Body: import("react").FC; Row: import("react").NamedExoticComponent; Cell: import("react").FC; Empty: import("react").FC; Loading: import("react").FC; Error: import("react").FC; Gutter: import("react").FC; StatusBar: import("react").FC; Overlay: import("react").FC; Expansion: import("react").NamedExoticComponent; Resizer: import("react").FC; SelectionCell: import("react").FC; SelectionHeaderCell: import("react").FC; SelectAll: import("react").FC; }; //#endregion //#region src/table/components/table-empty.d.ts /** * The empty-state surface. Render it anywhere inside ``; it shows itself only when * the table is genuinely empty — settled, with no rows — and renders nothing while a first load is * still running. * * That gating used to be the consumer's, which meant every table author wrote * `list.loading ? undefined : ` once they noticed their table claiming "no results" during * the first fetch. The table can tell the difference now, so it does. The library decides *when*; * what to say is still entirely yours, including the distinction the gate can't make for you. * * To tell "no data" from "a filter hid it all", read `rows` — the dataset *before* filtering. * Inside this slot there is nothing on screen by definition, so any rows at all mean the filter * is what emptied it: * * ```tsx * {table.rows.length ? "No matches" : "No studies yet"} * ``` * * Owns placement only — cosmetics are the consumer's, and `data-empty` is the styling hook. */ declare const TableEmpty: FC; //#endregion //#region src/table/components/table-scroll.d.ts type TableScrollProps = ComponentPropsWithRef<"div">; /** * The scrolling box: the element that actually overflows, and the one whose size the whole * virtualization budget is measured from. Header, body and the overlay surfaces go inside it; * anything that belongs *outside* the scrollbars — ``, a toolbar, pagination — * goes directly in `` alongside this. * * That separation is the point. A bar rendered inside here is inside the box the vertical scrollbar * measures, so the scrollbar always runs past it and no amount of consumer styling reaches the * overlap. As a sibling, the bar spans the full width — scrollbar gutter included — and the * scrollbar terminates at its top edge. * * **Sizing lives in flex, not in JS.** This is `flex: 1 1 auto; min-height: 0` inside ``'s * column, so the browser resolves all four cases — filling a sized parent, shrinking when the * content overflows, hugging the rows when the root is `height: auto`, and absorbing the difference * under a `maxHeight` — and `table.height` is then measured off the result rather than computed * ahead of it. `min-height: 0` is load-bearing: flex items default to `min-height: auto` and refuse * to shrink below their content, which would leave the box uncapped. * * Takes every div prop, and merges an incoming `ref` with its own, so a scroll-area primitive that * needs the scrolling element (Ark UI's ``, for instance) can compose * onto it. Nested that way it is no longer a direct child of the root's flex column, so move the * sizing to the wrapper and pass `style={{ flex: "initial", height: "100%" }}` here. */ declare const TableScroll: FC; //#endregion //#region src/table/table.context.d.ts declare const tableContext: import("react").Context; declare const useTableContext: () => TableModel; declare const TableProvider: import("react").Provider; /** * Slots let a consumer register defaults once on `` (currently just the selection * `checkbox`) that the built-in parts fall back to. Defaults to a native checkbox so selection * works with zero wiring. */ interface TableSlots { checkbox: FC; } declare const slotsContext: import("react").Context; declare const TableSlotsProvider: import("react").Provider; declare const useTableSlots: () => TableSlots; /** * Marks the scrollport. `` provides it; the parts that only work inside a scrolling * box check for it. */ declare const scrollportContext: import("react").Context; declare const TableScrollportProvider: import("react").Provider; /** * Throws when a part that belongs inside the scrollport is mounted outside one. * * Worth a hard error rather than a degraded render: without `` no width is ever * measured, so the render gate never opens and the table is simply blank — no message, nothing in * the DOM to inspect, and every symptom pointing at the data rather than the markup. * * **Development only**, behind the same `process.env.NODE_ENV` guard mobx uses (see * `makeRoutes`), so a consumer's bundler strips it from production builds. The check is purely * structural, so production has nothing left to learn from it. */ declare const useOutsideScrollportGuard: (component: string) => void; declare const useScrollportGuard: (component: string) => void; //#endregion //#region src/table/use-scroll.d.ts /** * Reports a scroll container's offsets on every scroll event. * * `onScroll` is read through a ref so an inline arrow doesn't re-subscribe on every render — the * table's root re-renders as the window shifts, and re-attaching the listener each time would be * pure waste. */ declare const useScroll: (ref: React.RefObject, onScroll: (x: number, y: number) => void) => void; //#endregion //#region src/table/use-table.d.ts /** * Creates a `TableModel` that lives as long as the component. * * The config is read once, at construction — with three exceptions: `data`, `loading` and `error` * are kept in sync, because a route's params can change without remounting the page (same component * type at the same tree position), and a table that ignored the new data would keep rendering the * previous org's rows. * * How "changed" is decided depends on which shape of `config.data` you pass, and the difference * matters — see {@link TableConfig.data}. An **array** is re-applied when its identity changes, so * it must be referentially stable. A **getter** is tracked by MobX instead, and must read * observables. A **lazy** is re-pointed when you hand over a different one, which is what makes a * keyed collection work: `data={store.byOrg({ orgId })}` is a new lazy each time `orgId` changes. * * A lazy also knows whether a request is running and how the last one ended, so it needs no help * describing itself and `loading` / `error` are ignored. The other two shapes carry no such story, * which is what those props are for — pass what your fetching already knows and the table derives * `loading`, `error` and `isEmpty` from it just the same. * * Everything else (`columns`, `getRowId`, `onStateChange`) is captured at construction; change them * through the model (`setColumns`/`addColumn`/`removeColumn`, `applyState`) rather than by * re-rendering. Per-column filters need none of that — they are instances the caller holds and * mutates directly, and the model reads through to them. */ declare const useTable: (config?: UseTableConfig) => TableModel; //#endregion //#region src/table/util.d.ts declare const titleCase: (str: string) => string; /** * Resolve a column key against a row: a direct property hit wins (so a literal "a.b" property * still works), otherwise the key is walked as a dot-path ("owner.name"). */ declare const getPath: (obj: unknown, path: string) => unknown; /** * Default sort comparator over extracted cell values — nullish first, numbers numerically, Dates * chronologically, everything else by locale string. */ declare const compareValues: (a: unknown, b: unknown) => number; //#endregion export { AutoColumnFn, BaseColumnDef, CellSlot, ColumnConfig, ColumnConfigPatch, ColumnDef, ColumnFilter, ColumnMeta, ColumnModel, ColumnPin, ColumnSort, ColumnState, ColumnWidth, ColumnsDef, ComputedColumnDef, DotPath, FieldColumnDef, type FilterCondition, FilterMode, type FilterOp, NativeCheckbox, RenderColumn, RowData, RowId, SELECTION_COLUMN_KEY, SelectAll, SelectAllProps, SelectionCell, SelectionCellProps, SelectionColumnDef, SelectionHeaderCell, SelectionHeaderCellProps, type SetFilterValue, SortDirection, Table, TableBody, TableBodyProps, TableCell, TableCellProps, TableCheckboxProps, TableColumnHeader, TableColumnHeaderProps, TableConfig, TableData, TableEmpty, TableError, TableErrorProps, TableExpansion, TableExpansionProps, TableGutter, TableGutterProps, TableHeader, TableHeaderProps, TableLoading, TableLoadingProps, TableModel, TableOverlay, TableOverlayProps, TableProvider, TableQuery, TableResizer, TableResizerProps, TableRoot, TableRootProps, TableRow, TableRowProps, TableScroll, TableScrollProps, TableScrollportProvider, TableSearchFilter, TableSlots, TableSlotsProvider, TableState, TableStatus, TableStatusBar, TableStatusBarProps, UseTableConfig, compareValues, getPath, pinnedCellStyle, scrollportContext, slotsContext, tableContext, titleCase, useOutsideScrollportGuard, useScroll, useScrollportGuard, useTable, useTableContext, useTableSlots }; //# sourceMappingURL=table.d.mts.map