@ai-matrx/design-system 0.61.3 → 0.61.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/data-table/copy-helpers.d.cts +1 -1
- package/dist/data-table/copy-helpers.d.ts +1 -1
- package/dist/data-table/filter-engine.d.cts +1 -1
- package/dist/data-table/filter-engine.d.ts +1 -1
- package/dist/data-table/host.d.cts +1 -1
- package/dist/data-table/host.d.ts +1 -1
- package/dist/data-table/index.cjs +48 -28
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +2 -2
- package/dist/data-table/index.d.ts +2 -2
- package/dist/data-table/index.js +48 -28
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +1 -1
- package/dist/data-table/infer-filter.d.ts +1 -1
- package/dist/data-table/layered-filters.d.cts +1 -1
- package/dist/data-table/layered-filters.d.ts +1 -1
- package/dist/data-table/query-control.d.cts +1 -1
- package/dist/data-table/query-control.d.ts +1 -1
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +1 -1
- package/dist/data-table/types.d.ts +1 -1
- package/dist/data-table/url-state.d.cts +1 -1
- package/dist/data-table/url-state.d.ts +1 -1
- package/dist/data-table/xlsx.d.cts +1 -1
- package/dist/data-table/xlsx.d.ts +1 -1
- package/dist/{layered-filters-CkkbZfmk.d.ts → layered-filters-CCWzLsOC.d.ts} +4 -0
- package/dist/{layered-filters-dlFZjEqc.d.cts → layered-filters-DL5BU8Mf.d.cts} +4 -0
- package/dist/tap-target.css +6 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MatrxColumnDef, f as ColumnFilterKind } from '../layered-filters-
|
|
1
|
+
import { M as MatrxColumnDef, f as ColumnFilterKind } from '../layered-filters-DL5BU8Mf.cjs';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../content-transfer.cjs';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MatrxColumnDef, f as ColumnFilterKind } from '../layered-filters-
|
|
1
|
+
import { M as MatrxColumnDef, f as ColumnFilterKind } from '../layered-filters-CCWzLsOC.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../content-transfer.js';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ai as LAYERED_FILTER_OPERATORS, aj as LAYERED_FILTER_OPERATOR_LABELS, ak as LayeredFilterField, al as LayeredFilterOperator, L as LayeredFilterRule, am as columnFiltersToLayeredRules, an as completeLayeredFilterRules, ao as decodeLayeredFilterRules, ap as encodeLayeredFilterRules, aq as isCompleteLayeredFilterRule, ar as isLayeredFilterOperator, as as layeredFilterMatchesValue, at as layeredFilterNeedsValue, au as layeredFilterRuleSummary, av as operatorsForLayeredField } from '../layered-filters-
|
|
1
|
+
export { ai as LAYERED_FILTER_OPERATORS, aj as LAYERED_FILTER_OPERATOR_LABELS, ak as LayeredFilterField, al as LayeredFilterOperator, L as LayeredFilterRule, am as columnFiltersToLayeredRules, an as completeLayeredFilterRules, ao as decodeLayeredFilterRules, ap as encodeLayeredFilterRules, aq as isCompleteLayeredFilterRule, ar as isLayeredFilterOperator, as as layeredFilterMatchesValue, at as layeredFilterNeedsValue, au as layeredFilterRuleSummary, av as operatorsForLayeredField } from '../layered-filters-DL5BU8Mf.cjs';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../content-transfer.cjs';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ai as LAYERED_FILTER_OPERATORS, aj as LAYERED_FILTER_OPERATOR_LABELS, ak as LayeredFilterField, al as LayeredFilterOperator, L as LayeredFilterRule, am as columnFiltersToLayeredRules, an as completeLayeredFilterRules, ao as decodeLayeredFilterRules, ap as encodeLayeredFilterRules, aq as isCompleteLayeredFilterRule, ar as isLayeredFilterOperator, as as layeredFilterMatchesValue, at as layeredFilterNeedsValue, au as layeredFilterRuleSummary, av as operatorsForLayeredField } from '../layered-filters-
|
|
1
|
+
export { ai as LAYERED_FILTER_OPERATORS, aj as LAYERED_FILTER_OPERATOR_LABELS, ak as LayeredFilterField, al as LayeredFilterOperator, L as LayeredFilterRule, am as columnFiltersToLayeredRules, an as completeLayeredFilterRules, ao as decodeLayeredFilterRules, ap as encodeLayeredFilterRules, aq as isCompleteLayeredFilterRule, ar as isLayeredFilterOperator, as as layeredFilterMatchesValue, at as layeredFilterNeedsValue, au as layeredFilterRuleSummary, av as operatorsForLayeredField } from '../layered-filters-CCWzLsOC.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../content-transfer.js';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ResolvedFilterKind } from './infer-filter.cjs';
|
|
2
|
-
import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../layered-filters-
|
|
2
|
+
import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../layered-filters-DL5BU8Mf.cjs';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '../content-transfer.cjs';
|
|
5
5
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ResolvedFilterKind } from './infer-filter.js';
|
|
2
|
-
import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../layered-filters-
|
|
2
|
+
import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../layered-filters-CCWzLsOC.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '../content-transfer.js';
|
|
5
5
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-table/types.ts"],"sourcesContent":["import type { TableScopeBarProps } from \"./TableScopeBar\";\nimport type { ContentTransferReference } from \"../content-transfer/extensions\";\nimport type { ComponentType, MouseEvent, ReactNode } from \"react\";\nimport type { TableViewTabsStore } from \"./TableViewTabs\";\nimport type { TableSearchRole, TableSearchScope } from \"./ranked-search\";\nimport type { AgentPayloadInput } from \"./copy\";\nimport type { CopyExportConfig } from \"./copy-types\";\nimport type {\n AiCustomSource,\n AiVariant,\n} from \"./copy\";\nimport type { LayeredFilterField, LayeredFilterRule } from \"./layered-filters\";\nimport type { ChoiceColorLookup, TableStyle } from \"./table-style\";\nimport type { FieldFormatConfig } from \"../field-formats/types\";\nimport type { GroupAggregateSpec, GroupOrder, TableGroup } from \"./grouping\";\nimport type { MatrxDrillAnswers, MatrxDrillDimension, MatrxDrillMeasure, MatrxDrillQuestion } from \"./drill\";\nimport type { CellPatch } from \"./fill\";\nimport type { ColumnFacets } from \"./facets\";\nimport type { SpreadsheetController, SpreadsheetIntent, SpreadsheetPastePlan } from \"./useSpreadsheetGrid\";\n\nexport type { SpreadsheetPastePlan };\nimport type { DateColumnFilterValue } from \"./date-filter\";\nimport type { MatrxTableMenuItem, MatrxTableMenuPayload, MatrxTableMenuSection } from \"./menu-targets\";\n\n/** How a column's filter UI behaves. `auto` infers from sample values. */\nexport type ColumnFilterKind =\n \"auto\" | \"text\" | \"select\" | \"boolean\" | \"number\" | \"date\" | false;\n\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * A compact boolean marker with the language people expect for a saved item.\n *\n * `undefined` recognizes only the exact conventional boolean accessor names\n * (`favorite`, `is_favorite`, `favourite`, `is_favourite`, `pinned`, and\n * `is_pinned`). Pass `false` to keep one of those fields as an ordinary\n * boolean column. `\"favorite\"` and `\"pin\"` make the intent explicit for\n * differently named fields.\n */\nexport type ColumnMarker = \"favorite\" | \"pin\" | false;\n\n/** How the table's primary search text is matched. */\nexport type TableSearchMatchMode = \"contains\" | \"whole_words\";\n\n/** Cell value type for typed inline editors (Supabase-style popovers for non-strings). */\nexport type CellEditType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"select\"\n /** Free-text multi-value chips (string[] cells: tags, labels, aliases). */\n | \"tags\"\n | \"date\"\n | false;\n\n/** The table owns action markup and geometry; hosts supply behavior and an icon component only. */\nexport interface MatrxTableIconAction {\n id: string;\n icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n label: string;\n tooltip?: string;\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n disabled?: boolean;\n loading?: boolean;\n tone?: \"default\" | \"primary\" | \"destructive\" | \"success\" | \"warning\";\n variant?: \"default\" | \"secondary\" | \"destructive\" | \"outline\" | \"ghost\" | \"link\";\n}\n\nexport interface MatrxColumnDef<T> {\n /** Stable id used for sort/filter state. Defaults to `accessorKey` when set. */\n id?: string;\n /** Dot-free key on the row for default value access + auto filter. */\n accessorKey?: keyof T & string;\n /** Custom value for sort/filter when `accessorKey` is insufficient. */\n accessorFn?: (row: T) => unknown;\n /** Value used only for local sort. Falls back to `accessorFn` / `accessorKey`. */\n sortValue?: (row: T) => unknown;\n /** Value used only for local filters. Falls back to `accessorFn` / `accessorKey`. */\n filterValue?: (row: T) => unknown;\n /**\n * WHAT COPY, COPY FOR AI AND EVERY EXPORT CARRY — the value the person SEES in the cell (a\n * reference's name, never the id it stores). Falls back to `accessorFn` / `accessorKey`. When\n * set, the stored value (the id) is still offered in the preparation workspace as an optional\n * \"<column> id\" column, left out by default — never instead of the name.\n */\n copyValue?: (row: T) => unknown;\n header: ReactNode;\n /** Plain-text name used for header controls when `header` is visual-only. */\n label?: string;\n /** Cell renderer. Defaults to stringified accessor value. */\n cell?: (row: T, index: number) => ReactNode;\n /** An explicitly declared custom actions column. Arbitrary content never enters the built-in Actions column. */\n customActions?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /** Sortable unless explicitly false. Default true. */\n sortable?: boolean;\n /**\n * WHY A COLUMN OFFERS NO SORT, in a sentence (merged-grid review 2: Created / Last changed could\n * not be sorted and nothing said why). Shown in the header's menu where the sort would be.\n */\n sortRefusal?: string | undefined;\n /** Direction used by the first header-click sort. Default `\"asc\"`. */\n defaultSortDirection?: SortDirection;\n /**\n * Filter kind. Default `\"auto\"` — every column gets a filter.\n * Pass `false` only when a column truly must not filter (e.g. actions).\n */\n filter?: ColumnFilterKind;\n /** Explicit select options (when filter is `\"select\"` or auto-detected). */\n filterOptions?: Array<{ value: string; label: string }>;\n /**\n * Single-choice select filter. The default select filter is a multi-select\n * that APPENDS (OR semantics) — correct for a status column, wrong for a\n * column whose options are mutually exclusive VIEWS of the list (a\n * record-class scope, a relative-date bucket). There, appending makes the\n * filter inert: the consumer reads one value, the popover accumulates a set,\n * and the first-selected value wins forever (D218). With `filterSingle`,\n * choosing an option REPLACES the selection, and choosing the active option\n * again clears it.\n */\n filterSingle?: boolean;\n /**\n * THE CONSUMER'S OWN COLUMN ACTIONS, in the header's one menu — rename,\n * insert left/right, use as the row label, colour by, settings — drawn under\n * the built-in sort and filter, in the same popover, never a second control\n * beside it. An item with `disabledReason` stays visible and says why it\n * cannot run here. Absent or empty, the header menu is exactly what it was.\n */\n headerMenu?: MatrxTableMenuItem[];\n /** Allow an exact typed value not present in loaded select options. */\n filterAllowCustom?: boolean;\n /**\n * Inline edit. Default false. `\"string\"` edits in-cell; other types open a\n * small popover (Supabase-style). Edits stay local until Save on the dirty pill.\n */\n editable?: CellEditType;\n /**\n * Per-row edit gate for an `editable` column. Return false to render the\n * plain cell (no pencil, no click-to-edit) for that row — for heterogeneous\n * lists where some kinds cannot take the write (e.g. a transcripts row of\n * kind \"unsorted\" has no user-facing title). Default: every row editable.\n */\n editableIf?: (row: T) => boolean;\n /**\n * How inline edit starts. Default `\"click\"` (click the cell body). `\"pencil\"`\n * shows a hover/focus pencil; the cell body no longer starts edit — so a\n * whole-row click can own that gesture. Forced to `\"pencil\"` when `href` is\n * set (D112 — the body is a real link).\n */\n editTrigger?: \"click\" | \"pencil\";\n /**\n * Options when `editable === \"select\"`. Also used by `\"tags\"` as the\n * suggestion list (existing values), while still allowing new entries.\n */\n editOptions?: Array<{ value: string; label: string }>;\n /**\n * Row link for the primary/title cell (D112): renders the cell content as a\n * real `next/link` anchor, so the row is reachable by keyboard, announced as\n * a link by screen readers, and cmd/middle-clickable into a new tab. The\n * whole-row `onRowOpen` click stays as a mouse convenience; clicks on the\n * anchor never double-fire it. Combine with `editable` and the link renders\n * with a hover/focus pencil that opens the inline editor instead of\n * click-text-to-edit.\n */\n href?: (row: T) => string | undefined;\n /**\n * Canonical entity token for the record this column NAMES. When set, the cell\n * renders through `EntityRef`, so the name carries the full door set — Open,\n * new tab, and Peek — instead of the Open-only `<Link>` that `href` alone\n * produces.\n *\n * THE INVENTORY LAW, applied to this component: the table grew its own door\n * (`href`) beside the platform's (`EntityRef`), and every column that named a\n * record picked one and silently lost the other half. This field collapses\n * them — `href` still works and still forces the pencil trigger, and it\n * OVERRIDES the registry route when both are set (for an admin-side route on\n * a satellite deployment).\n *\n * Needs the record's id: `entityToken` is paired with `entityId`, defaulting\n * to the table's own `getRowId`.\n *\n * **PER ROW, not per column** — a hub can be heterogeneous. `/transcripts`\n * lists transcripts, studio sessions, cleanup runs and an \"unsorted\" bucket\n * in one table, each with its own destination; a constant token would have\n * sent a session id to the transcript processor route and opened the\n * transcript peek on a record that is not one. Return `undefined` for a row\n * that names no entity — it falls back to the plain `href` link, or to inert\n * text. Pair with a per-row `href` when the kinds diverge.\n */\n entityToken?: string | ((row: T) => string | undefined);\n /**\n * NOTE: give the column a `cell` when you set this. Without one, a\n * UUID-shaped value renders the default `MatrxUuidCell`, which has its own\n * controls — wrapping those in the door's anchor would nest interactive\n * elements inside a link, and is redundant besides. The shell detects that\n * combination, skips the door, and screams once per column.\n */\n /** The id `entityToken` refers to. Defaults to the table's `getRowId(row)`. */\n entityId?: (row: T) => string | undefined;\n /**\n * Drop this column below `sm` (the phone breakpoint).\n *\n * A wide table on a phone becomes a horizontal scroller: the frozen\n * identity column stays, and everything after the second column sits off\n * the right edge where a reviewer will never find it. Marking the columns\n * that do NOT earn their width on a phone is how a surface declares an\n * INTENTIONAL mobile column set instead of an accidental one.\n *\n * The column is still fully sortable/filterable from the toolbar and still\n * rides every copy/export payload — this hides the CELL, not the data.\n * Default `false` = today's behavior everywhere.\n */\n mobileHidden?: boolean;\n /**\n * Built-in cell kinds. `\"uuid\"` / `\"fk\"` use MatrxUuidCell (short + copy +\n * optional open). `\"auto\"` (default) detects UUID-shaped strings.\n */\n cellKind?: \"auto\" | \"uuid\" | \"fk\" | \"text\";\n /**\n * FK / UUID navigation. Prefer `onOpen` → WindowPanel of the target.\n * Return `\"forbidden\"` when the caller lacks access.\n */\n fk?: {\n label?: string;\n /**\n * Canonical entity token this column's ids point at (`agent`, `note`, …).\n * THE DOOR LAW made declarative: the cell resolves route + new tab + peek\n * from the registries, so a column of ids stops being a dead end without\n * hand-wiring a link. `href` / `onOpen` still win when both are set.\n * A function form resolves the token per row (an audit log whose target\n * type varies by row).\n *\n * `\"auto\"` derives the token from the COLUMN NAME (`task_id` → `task`).\n * It is opt-in on purpose: the guess is only correct when you have checked\n * the actual FK. `scheduler.sch_run.task_id` references `scheduler.sch_task`,\n * not the workspace `task` the name implies, and `app_id` /\n * `conversation_id` / `file_id` / `workflow_id` each have several candidate\n * tables. A wrong door opens a DIFFERENT record — worse than no door.\n */\n token?: string | null | \"auto\" | ((row: T) => string | null | undefined);\n href?: (id: string, row: T) => string | null | undefined;\n onOpen?: (\n id: string,\n row: T,\n ) => void | \"forbidden\" | Promise<void | \"forbidden\">;\n /** Force non-navigable for this column. */\n forbidden?: boolean | ((id: string, row: T) => boolean);\n };\n /**\n * How this column's value READS — money, percent, a date style, decimal\n * places. The vocabulary is the platform's one field-format registry\n * (`@ai-matrx/design-system/field-formats`), the same `{id, options}` a user\n * data table stores on its column and the record store stores on its Field,\n * so `$1,234.56` is the same `$1,234.56` on every table in the product.\n *\n * A declarative format, not another render function, because a `cell`\n * callback is invisible to everything else: with `format` the column also\n * right-aligns itself, and a consumer's own `cell` asks the primitive for the\n * same text through `formatColumnValue(col, value)` rather than growing a\n * second money formatter beside this one.\n *\n * THE FALLBACK LAW travels with it: a value that does not fit the declared\n * format is never blanked and never throws — it renders its base-type text in\n * amber with the reason on hover, so the person SEES the mismatch.\n *\n * `cell` still wins when both are set.\n */\n format?: FieldFormatConfig;\n /**\n * Keep this column in place while the rest of the table scrolls sideways.\n *\n * Only the LEADING RUN freezes, only from `sm:` up, and only when the widths\n * the offsets need are known — `./frozen-columns.ts` says why each of those\n * three is a rule and not a default. Give a frozen column an explicit\n * `width`; without one the table declines to freeze anything rather than pin\n * a column at a guessed offset.\n */\n frozen?: boolean;\n className?: string;\n headerClassName?: string;\n width?: string | number;\n /** Enable desktop resizing for this column. Defaults to true when the table enables it. */\n resizable?: boolean;\n /** Desktop resize boundary. Defaults to 80px. */\n minWidth?: number;\n /** Desktop resize boundary. Defaults to 1200px. */\n maxWidth?: number;\n align?: \"left\" | \"center\" | \"right\";\n /**\n * ICON COLUMN. A column whose whole content is one glyph — a star, a lock, a\n * status dot — and whose `width` is therefore a lie without this flag.\n *\n * `width` is only a hint on a table cell: min-content wins. A 40px star column\n * still rendered ~70px wide because the HEADER carried three separate\n * controls beside the glyph (the sort button, its arrow, the filter funnel),\n * and the cell carried the default `px-2` on both sides. Every surface that\n * wanted a tight icon column was paying for chrome it never used.\n *\n * `compact` fixes it AT THE PRIMITIVE, and does NOT cost the column anything:\n * horizontal padding drops to `px-1`, and the header collapses its three\n * controls into ONE popover trigger that still offers Sort ascending / Sort\n * descending / Clear sort / the full filter body. The column stays fully\n * sortable and filterable — the affordances moved into the menu, they did not\n * disappear. Active sort/filter still show, as a 2px dot on the trigger.\n *\n * Pair it with `width` and `align: \"center\"`.\n */\n compact?: boolean;\n /**\n * Saved-item marker semantics for a compact boolean column. Markers default\n * to 40px, keep the existing sort/filter menu, and name its choices as\n * Favorites/Pins. See `ColumnMarker` for automatic recognition and opt-out.\n */\n marker?: ColumnMarker;\n /** Hide from the table (still available in column picker when we add it). */\n hidden?: boolean;\n /** Whether the package-owned Columns dialog may hide this column. Default true. */\n hideable?: boolean;\n /**\n * Whether this column's header label is a drag handle for column reordering. Default true.\n * `false` for a column that IS a control (records-ui's \"+\" add-column header): its label stays\n * its own, never a button inside the reorder button, and it never moves among the data columns.\n */\n reorderable?: boolean;\n}\n\n/** How a text filter matches. Default `\"contains\"`. */\nexport type TextFilterMode = \"contains\" | \"empty\" | \"not_empty\" | \"null\" | \"not_null\";\n\n/** Active per-column filter value. Shape depends on filter kind. */\nexport type ColumnFilterValue =\n | { kind: \"text\"; value: string; mode?: TextFilterMode; negated?: boolean }\n | {\n kind: \"select\";\n /** Single-choice value (legacy writers). Ignored when `values` is set. */\n value: string;\n /** Multi-choice OR set — an explicit empty array matches no rows. */\n values?: string[];\n /** Exclude matching values instead of including them. */\n negated?: boolean;\n }\n | { kind: \"boolean\"; value: boolean; negated?: boolean }\n | {\n kind: \"number\";\n min?: number | undefined;\n max?: number | undefined;\n negated?: boolean;\n /**\n * How the number is compared (the champion's operators: =, ≠, <, >, between, empty).\n * Absent means \"between\" — `min`/`max` inclusive, as every older writer meant it. For\n * `eq`/`ne` both bounds hold the value; `lt` reads `max`, `gt` reads `min`, both strict.\n */\n op?: NumberFilterOp | undefined;\n }\n | DateColumnFilterValue;\n\n/** A number filter's comparison. */\nexport type NumberFilterOp = \"eq\" | \"ne\" | \"lt\" | \"gt\" | \"between\" | \"empty\" | \"not_empty\";\n\nexport type ColumnFiltersState = Record<string, ColumnFilterValue | undefined>;\n\nexport interface SortState {\n id: string;\n direction: SortDirection;\n}\n\n/**\n * Opt-in durable URL state for a local table.\n *\n * Every table gets an explicit stable id, producing namespaced parameters such\n * as `table.accounts.q` and `table.accounts.sort`. This prevents collisions\n * with page-owned parameters and with sibling tables on the same route.\n */\nexport interface MatrxDataTableUrlStateConfig {\n /** Stable lowercase identifier: letters, numbers, and hyphens; max 64 chars. */\n id: string;\n /** Initial sort when the URL carries none. Default: none. */\n defaultSort?: SortState | null | undefined;\n /** Browser history behavior for table transitions. Default: `push`. */\n history?: \"push\" | \"replace\" | undefined;\n /**\n * History behavior while typing search/any-of text. `session` pushes the\n * first edit, then replaces rapid keystrokes. Default: `session`.\n */\n textHistory?: \"session\" | \"push\" | \"replace\" | undefined;\n /** Persist the open side-panel row. Default true. */\n selectedRow?: boolean;\n /** Persist the open table-owned window row. Default true. */\n windowRow?: boolean;\n /** Persist checkbox selection. Opt-in because large selections lengthen URLs. */\n selection?: boolean;\n}\n\n/**\n * Complete view state for a remotely queried table page. The table owns none\n * of this state in controlled mode: callers may mirror it to URL search params\n * and use it as part of a direct database-query cache key.\n */\nexport interface MatrxDataTableQueryState {\n /** One-based page number, matching the table's pagination UI. */\n page: number;\n pageSize: number;\n search: string;\n /** Defaults to `contains` when omitted, preserving every existing table. */\n searchMatchMode?: TableSearchMatchMode;\n /** Selected intelligent-search scope. Omitted means the backward-compatible All scope. */\n searchScope?: string;\n anyOf: string;\n /** Ordered AND rules from the compact advanced-filter builder. */\n layeredFilters?: LayeredFilterRule[];\n columnFilters: ColumnFiltersState;\n sort: SortState | null;\n}\n\n/**\n * Optional data-processing contract. Omit it (or use `local`) to preserve the\n * original in-memory filter/sort/pagination behavior. In controlled mode,\n * `data` is already the current page and the caller performs all querying.\n */\n/** Structural match for @ai-matrx/data/react usePaginatedData. */\nexport interface MatrxTablePagination<T> {\n queryKey: string;\n rows: T[];\n loading: boolean;\n isFetchingNextPage: boolean;\n error: Error | null;\n hasNextPage: boolean;\n loadNextPage: () => Promise<void>;\n refresh: () => void;\n totalItems: number | undefined;\n loadAll?: { isLoading: boolean; request: () => void; stop: () => void };\n retrySource?: () => void;\n sourceUntil?: string;\n /** Source records differ from rendered rows when the host groups occurrences. */\n sourceRecords?: { loaded: number; total?: number | undefined; label: string; rowLabel: string; matchedRecords?: number };\n sourcePageSize?: { value: number; options: readonly number[]; onChange: (value: number) => void };\n}\n\n/**\n * A controlled-local table can retain local search/pagination while its source\n * owns column filters and/or sorting. `sourceTotal` is display metadata only:\n * it never changes local page slicing, because the loaded rows remain local.\n */\nexport interface MatrxDataTableSourceProcessing {\n search?: \"local\" | \"source\";\n /** Source accepts and ranks `searchScope`; omit to keep scope UI local-only. */\n intelligentSearch?: \"local\" | \"source\";\n /**\n * Source ownership may cover every column filter or a declared subset. With\n * `{ source: [\"kind\"] }`, the table applies every other active filter to\n * loaded rows and leaves `kind` to the source query.\n */\n columnFilters?: \"local\" | \"source\" | { source: readonly string[] };\n sort?: \"local\" | \"source\";\n sourceTotal?: number;\n}\n\nexport type MatrxDataTableQueryControl<T = unknown> =\n | { mode: \"local\" }\n | {\n /** Accumulated server rows; page stays 1 and sort/filter remain server-owned. */\n mode: \"controlled-append\";\n state: MatrxDataTableQueryState;\n onStateChange: (next: MatrxDataTableQueryState) => void;\n pagination: MatrxTablePagination<T>;\n sourceProcessing?: MatrxDataTableSourceProcessing;\n /** Source-connected tables append on user scroll by default. */\n scroll?: { thresholdPx?: number; intentTimeoutMs?: number } & (\n | { mode?: \"scroll\" }\n | { mode: \"manual\"; reason: string; approvedBy: string }\n /** Temporary configuration state: automatic append is off, but Load more stays available. */\n | { mode: \"suspended\"; reason: string }\n );\n }\n | {\n /** Local rows, but every query control is owned by the caller (for URL state). */\n mode: \"controlled-local\";\n state: MatrxDataTableQueryState;\n onStateChange: (next: MatrxDataTableQueryState) => void;\n sourceProcessing?: MatrxDataTableSourceProcessing;\n }\n | {\n mode: \"controlled\";\n state: MatrxDataTableQueryState;\n /** Total rows matching the controlled query, not just `data.length`. */\n totalItems: number;\n onStateChange: (next: MatrxDataTableQueryState) => void;\n /** Declare source-owned query behavior before enabling source-aware controls. */\n sourceProcessing?: MatrxDataTableSourceProcessing;\n };\n\n/**\n * Toolbar facets — first-class, Mars-extensible filter controls above the grid.\n * Start with button-group; add radio / switch / complex later without forking.\n */\nexport type ToolbarFacet =\n | {\n type: \"button-group\";\n id: string;\n label?: string;\n value: string;\n /** Reset target for per-facet + global clear. Default: first option value. */\n defaultValue?: string;\n options: Array<{\n value: string;\n label: string;\n icon?: ReactNode;\n }>;\n onChange: (value: string) => void;\n }\n | {\n type: \"custom\";\n id: string;\n render: () => ReactNode;\n /** Declare filter state and its reset together so global Clear includes custom controls. */\n filter?: { active: boolean; onReset: () => void };\n };\n\n/**\n * Cross-column OR search — matches if ANY listed column contains the query.\n * Relationships use case: filter by entity type without picking source vs target.\n */\nexport interface AnyOfColumnSearch {\n columnIds: string[];\n placeholder?: string;\n /** Controlled value. Uncontrolled if omitted. */\n value?: string;\n onChange?: (value: string) => void;\n}\n\nexport type MatrxDataTableAppearance = \"standalone\" | \"embedded\";\n\nexport interface MatrxDataTableToolbar {\n /** Optional human-readable table title, rendered by the shared title row. */\n title?: string;\n /** Source-owned total displayed beside the shared title. */\n titleCount?: { value: number; label: string };\n /** Global search across all accessor values. Default true. */\n search?: boolean;\n /**\n * A caller-owned search control for a deliberately narrower query contract.\n * It occupies the canonical title-row search slot and never changes table\n * query state, so the caller remains honest about its source boundary.\n */\n customSearch?: ReactNode;\n searchPlaceholder?: string;\n searchValue?: string;\n onSearchChange?: (value: string) => void;\n /**\n * Show a compact, visible choice between substring and whole-word search.\n * Omit it when a data source cannot honor both modes server-side.\n */\n searchMatch?: {\n defaultMode?: TableSearchMatchMode;\n };\n /** Optional field-scope controls. Local search always ranks all loaded fields before pagination. */\n intelligentSearch?: {\n roles?: Readonly<Record<string, TableSearchRole>>;\n scopes?: readonly TableSearchScope[];\n };\n /**\n * OR-search across specific columns (e.g. source_type OR target_type).\n * Shown as its own input beside global search when set.\n */\n anyOf?: AnyOfColumnSearch;\n /**\n * Optional compact advanced-filter builder beside the regular search. In\n * controlled mode rules live in `query.state.layeredFilters`; local tables\n * evaluate them against matching column ids.\n */\n layeredFilters?: {\n fields: readonly LayeredFilterField[];\n maxRules?: number;\n label?: string;\n };\n /** Extensible facet strip (button groups, later radios/switches/…). */\n facets?: ToolbarFacet[];\n /** Source controls in a separate wrapping row below the standard toolbar. */\n leading?: ReactNode;\n /**\n * Draw the toolbar row INTO this element — the placing page's own toolbar row — so the page\n * and the table share one row (a saved-view picker, a layout chooser and the table's search\n * side by side) instead of two stacked bars. `null`: the element is not mounted yet, nothing\n * is drawn. Absent: the row is drawn in place, as always.\n */\n portalInto?: HTMLElement | null;\n /**\n * Draw the saved-view tabs INTO this element — the leading edge of the placing page's own\n * toolbar row — while the rest of the table's row (its actions) goes to `portalInto`. The\n * tabs then open the page row on the left and the table's actions close it on the right,\n * instead of both riding one slot. Drawn without the strip's bottom rule (the page row is\n * not the table's top edge). `null`: not mounted yet, the tabs are not drawn. Absent: the\n * tabs stay in the table's own row.\n */\n tabsPortalInto?: HTMLElement | null;\n /** Keep the toolbar on ONE row at every width; a narrow screen scrolls it sideways. */\n singleRow?: boolean;\n /**\n * `false`: no Columns button — the placing page already owns the ONE column picker (a list\n * shell with its own picker that hands the table only the visible columns; two pickers that\n * disagree about which columns exist is worse than one). Default: shown.\n */\n columns?: boolean;\n /**\n * WHERE THE ROW'S CONTROLS GO WHEN IT HAS NO ROOM (lane C, merged-grid review 2026-09-26):\n * `inline` (default) draws them on the row; `menu` puts the table's controls and `actions`\n * behind ONE \"…\" at the row's end and keeps `pinned` beside it; `sheet` (a phone) puts every\n * one of them — `sheetTop`, the controls, `pinned`, `sheetExtras` — in ONE bottom sheet behind\n * a \"Tools\" button. No control is ever dropped.\n */\n overflow?: {\n mode: \"inline\" | \"menu\" | \"sheet\";\n /** Stays on the row in `menu` mode (the primary action, undo); inside the sheet on a phone. */\n pinned?: ReactNode;\n /** Drawn first inside the sheet (the search, so it is there with everything else). */\n sheetTop?: ReactNode;\n /** The host's controls that sit elsewhere on a wide row (the view filter, the layout, share). */\n sheetExtras?: ReactNode;\n /** The sheet button's word. Default \"Tools\". */\n label?: string;\n };\n /**\n * Standard host refresh affordance. The package owns its placement, busy\n * state, accessible tooltip, and glyphs (refresh arrows at rest, spinner\n * only while busy); the host remains the sole data owner.\n */\n refresh?: {\n onRefresh: () => void | Promise<void>;\n label?: string;\n };\n /**\n * Standard create affordance. The host owns authorization and the actual\n * create flow; disabledReason is exposed through the shared tooltip.\n */\n add?: {\n onAdd: () => void | Promise<void>;\n disabled?: boolean;\n disabledReason?: string;\n };\n /** Right-side domain actions, rendered after standard table controls. */\n actions?: ReactNode;\n}\n\n/** Query fields safe to persist in a named table view. */\nexport type TableViewQuery = Omit<MatrxDataTableQueryState, \"page\">;\n\n/** Portable, persistence-neutral table state saved by a host adapter. */\nexport interface TableViewSnapshot {\n __kind: \"matrx-table-view\";\n version: 1;\n /** Page/cursor/selection/loaded rows are deliberately never saved. */\n query: TableViewQuery;\n /** `widths` is sparse so views saved before resize remain valid. */\n columns: { order: string[]; hidden: string[]; widths?: Record<string, number> };\n /**\n * The view's COLORS — color-by-a-column, rules, and manual highlights\n * (`./table-style.ts`). Optional so every view saved before colors existed\n * still parses; absent means \"this view paints nothing\".\n *\n * Colors belong to the VIEW and not to the data, for the same reason the\n * filters do: two people can read one table two ways, and neither reading is\n * a fact about the records. That is also why this snapshot is the only place\n * a style is carried — copy, export and agents read the rows, never this.\n */\n style?: TableStyle;\n}\n\n/** Host port for durable named views. The host owns actor, tenancy and CRUD. */\nexport interface TableSavedViewsProps {\n tableId: string;\n snapshot: TableViewSnapshot;\n defaultSnapshot: TableViewSnapshot;\n onApply: (snapshot: TableViewSnapshot) => void;\n presentation?: \"menu\" | \"tabs\";\n related?: ReactNode;\n}\n\n/** Optional deferred-apply copy for column filter popovers. */\nexport interface MatrxDataTableFilterUi {\n /** Enables the Include/Exclude control. Default false for remote-safe filters. */\n allowNegation?: boolean;\n /** Enables null/not-null text modes. Default false for remote-safe filters. */\n allowNullModes?: boolean;\n applyLabel?: string;\n showCancel?: boolean;\n footerHint?: ReactNode;\n}\n\nexport interface MatrxDataTableCopyConfig<T> {\n /** Explicit registered identities; never inferred from table labels or row shapes. */\n rowReferences?: (row: T) => readonly ContentTransferReference[];\n references?: (visible: T[], all: T[]) => readonly ContentTransferReference[];\n triggerVariant?: \"transparent\" | \"glass\" | \"outline\";\n /** Toast / tooltip label base, e.g. \"Relationship rule\". */\n label: string;\n listLabel?: string;\n location: string;\n rowKind: string;\n listKind: string;\n rowDescription?: string;\n listDescription?: string;\n humanRow: (row: T) => string;\n /**\n * Toolbar-only plain-text representation of the current filtered/sorted\n * view. Omit to retain the canonical table/row summary.\n */\n listHuman?: (visible: T[], all: T[]) => string;\n /**\n * Toolbar-only JSON representation of the current filtered/sorted view.\n * Omit to retain the projected agent rows.\n */\n listJson?: (visible: T[], all: T[]) => unknown;\n /**\n * Toolbar-only AI envelope for the current filtered/sorted view. Omit to\n * retain the canonical list envelope and its query metadata.\n */\n listAgent?: (visible: T[], all: T[]) => AgentPayloadInput;\n /** Project row for agent JSON. Default: full row. */\n agentRow?: (row: T) => unknown;\n rowAttributes?: (\n row: T,\n ) => Record<string, string | number | boolean | null | undefined>;\n listAttributes?: (\n visible: T[],\n all: T[],\n ) => Record<string, string | number | boolean | null | undefined>;\n /**\n * Live view state rendered inside the list payload's <context>. Unlike\n * per-row data, this remains present when the current view has zero rows.\n */\n listContext?: (\n visible: T[],\n all: T[],\n ) => Record<string, string | number | boolean | null | undefined>;\n /**\n * Additional row-scoped AI actions. Use this to fold a domain action such\n * as a paste-ready repair brief into the table-owned row copy menu instead\n * of rendering a separate third control. Builders run at click time.\n */\n rowAiVariants?: (row: T) => AiVariant[];\n /**\n * Graded AI variants for the toolbar's view copy (e.g. \"Top 25\", \"Summary\n * only\"). When set, the toolbar's Copy-for-AI upgrades to a dropdown with\n * these variants + the full-view payload as the automatic \"Everything\"\n * escape hatch. Receives (visible, all) rows at render; builders run at\n * click time.\n */\n aiVariants?: (visible: T[], all: T[]) => AiVariant[];\n /** Custom-preview source (options dialog + live size counts) for the view. */\n aiCustom?: (visible: T[], all: T[]) => AiCustomSource;\n /** Additional domain exports in the same canonical menu; builders run on demand. */\n export?: (visible: T[], all: T[]) => CopyExportConfig;\n /** Show toolbar copy (this view). Default true when copy is set. */\n showToolbar?: boolean;\n /** Show per-row copy. Default true when copy is set. */\n showRow?: boolean;\n}\n\nexport interface MatrxDataTableDetailConfig<T> {\n /** Side-panel title. Default: first string column or \"Details\". */\n title?: (row: T) => ReactNode;\n description?: (row: T) => ReactNode | undefined;\n /** Override the default key/value inspector. */\n render?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /** Header actions inside the side panel. */\n headerActions?: (row: T) => ReactNode;\n defaultWidth?: number;\n enabled?: boolean;\n /**\n * Maps a field name to the entity token its id points at, turning that field\n * into a door (route + peek) in the default inspector — side panel AND row\n * window.\n *\n * There is no default guess: this inspector renders whatever columns the row\n * has, and a wrong door opens a DIFFERENT record (`sch_run.task_id` is a\n * SCHEDULED task, not a workspace `task`). A table whose FKs you HAVE checked\n * can pass `tokenFromColumnName` (`components/official/entity-ref/doors`) to\n * open every `<token>_id` field at once.\n *\n * The ROW is passed too, because a table whose target type varies per row (an\n * audit log, an exposure report) cannot answer from the column name alone.\n */\n tokenForField?: (key: string, row: T) => string | null;\n}\n\n/** Actions a record-owned control can use without reaching into table state. */\nexport interface MatrxDataTableRecordControls {\n /** Close the row's side-panel detail, if open. */\n closeDetail: () => void;\n /** Open the row in the canonical adjustable side panel. */\n openDetail: () => void;\n /** Open the row in its canonical table-owned WindowPanel. */\n openWindow: () => void;\n /** Close the row's table-owned WindowPanel, if open. */\n closeWindow: () => void;\n /** Whether this row currently has visible, unpersisted inline edits. */\n hasPendingEdits: boolean;\n /**\n * Discard this row's pending inline edits.\n *\n * Row actions that persist the already-merged visible row (for example an\n * explicit Confirm action) call this only after that write succeeds. This\n * prevents the floating Save pill from later replaying the same draft as a\n * different, weaker write.\n */\n discardPendingEdits: () => void;\n beginEdit?: () => void;\n saveEdits?: () => void | Promise<void>;\n cancelEdits?: () => void;\n /** Whether this row's optional inline detail is expanded in the table. */\n isExpanded?: boolean;\n /** Toggle this row's optional inline detail. */\n toggleExpanded?: () => void;\n}\n\n/**\n * Controlled inline detail rendered immediately below its owning table row.\n *\n * Use this when detail belongs in the scan path of a dense table. The caller\n * owns which row or rows are expanded and supplies the domain-specific body;\n * the canonical renderer retains sorting, filtering, selection, row actions,\n * and the correct column span.\n */\ninterface MatrxDataTableExpandedDetailSharedConfig<T> {\n canExpand?: (row: T) => boolean;\n render: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n className?: string;\n}\n\n/** The default detail mode: exactly zero or one row may be expanded. */\nexport interface MatrxDataTableSingleExpandedDetailConfig<T>\n extends MatrxDataTableExpandedDetailSharedConfig<T> {\n expandedId: string | null;\n onExpandedIdChange: (id: string | null) => void;\n expandedIds?: never;\n onExpandedIdsChange?: never;\n}\n\n/**\n * Multi-detail mode: every ID in `expandedIds` renders its own inline detail.\n *\n * Pass a new Set to `onExpandedIdsChange`; the table never mutates the\n * caller-owned set.\n */\nexport interface MatrxDataTableMultiExpandedDetailConfig<T>\n extends MatrxDataTableExpandedDetailSharedConfig<T> {\n expandedIds: ReadonlySet<string>;\n onExpandedIdsChange: (ids: Set<string>) => void;\n expandedId?: never;\n onExpandedIdChange?: never;\n}\n\nexport type MatrxDataTableExpandedDetailConfig<T> =\n | MatrxDataTableSingleExpandedDetailConfig<T>\n | MatrxDataTableMultiExpandedDetailConfig<T>;\n\n/**\n * THE FOUR LEVELS of a right-click, as opaque host-owned descriptors.\n *\n * A spreadsheet's right-click menu is four menus, chosen by where the pointer\n * was: the CELL, the ROW it sits in, the COLUMN it sits under, and — when the\n * click lands inside an extended range — the SELECTION. The canonical table\n * used to expose exactly one of them, which is why every surface that wanted\n * the other three forked the table.\n *\n * Each resolver is optional and each returns something the package never\n * interprets; the host builds its menu from it. A level with no resolver here\n * falls back to the host's own `createDefaultMenuContext`, so a table that\n * declares nothing still gets all four through the platform's universal menu.\n */\nexport interface MatrxDataTableContextMenuConfig<T> {\n resolveRowContext?: (row: T, controls: MatrxDataTableRecordControls) => unknown;\n resolveCellContext?: (args: {\n row: T;\n rowId: string;\n columnId: string;\n columnLabel: string;\n value: unknown;\n /** The cell's value as text, for the menu's own Copy. */\n text: string;\n controls: MatrxDataTableRecordControls;\n }) => unknown;\n resolveColumnContext?: (args: {\n columnId: string;\n columnLabel: string;\n /** Every rendered row's value for this column, in visual order. */\n values: unknown[];\n }) => unknown;\n resolveSelectionContext?: (args: {\n rowIds: string[];\n columnIds: string[];\n /** The selected rectangle as an Excel/Sheets-compatible TSV block. */\n tsv: string;\n cellCount: number;\n }) => unknown;\n resolveTableContext?: (args: { rowCount: number; columnIds: string[] }) => unknown;\n /**\n * NEUTRAL SECTIONS for whatever was right-clicked. Asked each time the menu\n * opens, with the level's own payload — the cell's row, column and value; the\n * row; the column's label and values; the selected range's ids and TSV; the\n * table's row count — and answered with plain `MatrxTableMenuSection`s the\n * host draws in its own right-click menu, beside what it already offers.\n * Nothing here names a menu system. Absent, the right-click menu is exactly\n * what it was.\n */\n sections?: (payload: MatrxTableMenuPayload<T>) => MatrxTableMenuSection[];\n}\n\nexport interface MatrxDataTableWindowConfig<T> {\n /** Window title. */\n title?: (row: T) => string;\n /**\n * @deprecated Prefer `renderView` + `renderEdit` so the window stays editable.\n * Full-body override with no View/Edit tabs.\n */\n render?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /** View tab body. Defaults to DataRowInspector. */\n renderView?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /**\n * Edit tab body. When set, the WindowPanel shows View / Edit sidebar tabs\n * (WindowPanel built-in sidebar). Defaults to `detail.render` when present.\n * Pass `false` to keep a view-only window even when `detail.render` exists.\n */\n renderEdit?:\n ((row: T, controls: MatrxDataTableRecordControls) => ReactNode) | false;\n /**\n * Called when the panel icon opens the window — hydrate edit state here\n * without opening the side panel (prefer this over `onRowOpen` for windows).\n */\n onOpen?: (row: T) => void;\n /**\n * Make full-row click open the WindowPanel instead of the side panel.\n * The trailing row action and the window header then expose the side panel\n * as the explicit secondary presentation. Default false.\n */\n openOnRowClick?: boolean;\n /** Which tab to open. Default: `\"edit\"` when an edit body exists. */\n defaultTab?: \"view\" | \"edit\";\n /** Show the panel-icon that opens the window. Default true when detail enabled. */\n enabled?: boolean;\n width?: number;\n height?: number;\n}\n\nexport interface MatrxDataTableEmptyState {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n}\n\n/** Where the read behind a table's rows stands (RC-B12 round 13). */\nexport type MatrxDataTableReadStatus = \"loading\" | \"error\" | \"ready\";\n\n/**\n * The outcome of the read that produced `data`. An `emptyState` is an answer\n * only after a read that SUCCEEDED and returned nothing, so a table that is\n * handed its read's outcome shows:\n *\n * - `\"error\"` and no rows → the failure (the host's `ReadFailure` port: its\n * error view with the host's actions) and a retry — never the empty state;\n * - `\"error\"` WITH rows → the rows, under a stale notice (the host's\n * `StaleNotice` port) — never blanked;\n * - `\"loading\"` and no rows → the loading skeleton;\n * - `\"ready\"` and no rows → `emptyState`.\n *\n * Structurally identical to matrx-frontend's `ReadOutcome`\n * (`components/read-state/ReadGate.tsx`), so a host passes its own value.\n */\nexport interface MatrxDataTableRead {\n status: MatrxDataTableReadStatus;\n /** The failure itself, or just a truthy flag when the read only says it failed. */\n error?: unknown;\n /** Run the read again. Absent, the failure is shown without a retry control. */\n onRetry?: (() => void) | undefined;\n /** What was read, in the reader's words (\"your tasks\"). Default \"these rows\". */\n what?: string | undefined;\n}\n\n/** The rows a table summary is allowed to describe. */\nexport interface MatrxDataTableSummaryContext<T> {\n /** Filtered and sorted rows, always before pagination. */\n rows: readonly T[];\n /** True when any table-owned search, facet, or column filter is active. */\n isFiltered: boolean;\n /** Whether the browser holds all matching rows, loaded rows, or one controlled page. */\n coverage: \"all\" | \"loaded\" | \"page\";\n}\n\nexport interface MatrxDataTableSummaryMetric<T> {\n id: string;\n /** Plain text keeps the KPI's visible label and unavailable announcement identical. */\n label: string;\n value: (context: MatrxDataTableSummaryContext<T>) => ReactNode;\n}\n\n/**\n * Optional KPIs and aligned column totals owned by the canonical table.\n *\n * Callback totals run over the filtered/sorted row set before pagination. A\n * source override is for an externally computed aggregate: once declared, the\n * table never substitutes its loaded rows for a missing source value.\n */\nexport interface MatrxDataTableSummaryConfig<T> {\n metrics: MatrxDataTableSummaryMetric<T>[];\n totals?: Record<string, (context: MatrxDataTableSummaryContext<T>) => ReactNode>;\n source?: {\n isFiltered: boolean;\n metrics?: Record<string, ReactNode>;\n totals?: Record<string, ReactNode>;\n };\n}\n\n/** Pending cell edits keyed by row id → partial field map. */\nexport type CellEditsMap = Record<string, Record<string, unknown>>;\n\nexport interface MatrxDataTableEditConfig<T> {\n /** Enable inline editing for columns with `editable` set. */\n enabled?: boolean;\n /**\n * Persist all pending edits. Called when the user clicks Save on the dirty pill.\n * Return resolved when done; throw/reject to keep the draft (with toast).\n */\n onSave: (edits: CellEditsMap, rows: T[]) => void | Promise<void>;\n /** Persist each committed cell immediately. Failed writes remain in the\n * dirty pill so the user can retry or cancel them. */\n autoSave?: boolean;\n /** Optional cancel hook (draft already discarded). */\n onCancel?: () => void;\n /**\n * Judge a value the person MEANT to write, before it enters the draft.\n *\n * Return a sentence to REFUSE it: the editor stays open, the value is not\n * taken, and the sentence is shown IN THE CELL, where the person is standing.\n * Return undefined to accept. Excel's posture, and the reason is the product —\n * \"Amount must be at least 0\" is a thing a person can act on; a silent revert\n * is not.\n *\n * It never judges what is already stored: a value that predates the rule is\n * legal, is never rewritten, and is the point of declaring a rule at all.\n */\n validate?: ((args: { row: T; rowId: string; columnId: string; value: unknown }) => string | undefined) | undefined;\n}\n\nexport interface MatrxDataTableHierarchyConfig<T> {\n /** Complete hierarchy when `data` is only the current controlled page. */\n rows?: T[];\n getParentId: (row: T) => string | null;\n /** Persist the exact structural intent represented by the drop shadow. */\n onMove: (row: T, move: MatrxDataTableHierarchyMove) => void | Promise<void>;\n /** Enables sibling insertion shadows in addition to parent/root drops. */\n manualOrder?: boolean;\n canReparent?: (row: T) => boolean;\n itemLabel?: (row: T) => string;\n rootDropLabel?: string;\n}\n\nexport interface MatrxDataTableHierarchyMove {\n parentId: string | null;\n /**\n * Insert immediately before this sibling. Null means first child for\n * \"inside\"/\"root\", and LAST sibling for \"after\" (target had no successor).\n */\n beforeId: string | null;\n position: \"before\" | \"after\" | \"inside\" | \"root\";\n targetId: string | null;\n}\n\n/**\n * Multi-row selection — a leading checkbox column plus a bulk bar that appears\n * only while rows are checked.\n *\n * OPT-IN and fully CONTROLLED: the consumer owns the id set, so selection\n * survives (or is deliberately cleared by) a re-fetch, a filter change, or an\n * optimistic list update — the table never holds hidden selection state that\n * can disagree with the surface around it.\n *\n * Why this is a primitive and not a per-surface checkbox column: a register\n * the user cannot clear in bulk is a register they stop reading, and the fifth\n * hand-rolled selection column — each with its own shift-click, its own\n * select-all semantics, its own bar — is exactly the fork `components/official/`\n * exists to prevent.\n *\n * Selection does not alter mobile horizontal scrolling. The complete row moves\n * as one surface below `sm`; no leading column is allowed to pin over the data.\n */\nexport interface MatrxDataTableSelectionConfig<T> {\n /**\n * Selected entity ids. Defaults to `getRowId(row)`, so ids not on the\n * current page are kept exactly as before.\n */\n selectedIds: string[];\n onSelectedIdsChange: (ids: string[]) => void;\n /**\n * The underlying entity identity for selection when one entity is rendered\n * more than once. `getRowId` still identifies the individual rendered\n * occurrence; this makes every occurrence of the same entity share one\n * checkbox state and one bulk-action row.\n */\n getSelectionId?: (row: T) => string;\n /**\n * The bulk bar's actions, given the currently-selected rows THAT ARE LOADED.\n * Selection can outlive a page change, so also take `selectedIds` when an\n * action only needs ids.\n */\n actions?: (selected: T[], selectedIds: string[]) => ReactNode;\n /**\n * Rows that cannot be acted on in bulk render NO checkbox at all — the cell\n * stays for alignment, the control is absent from the DOM. A greyed control\n * still advertises a choice the surface underneath would refuse.\n */\n isRowSelectable?: (row: T) => boolean;\n /** Singular noun for the bar's count (\"finding\" → \"3 findings selected\"). */\n noun?: string;\n}\n\n/** Table-owned state and actions exposed to an opt-in phone card renderer. */\nexport interface MatrxDataTableMobileCardControls {\n /** Render a declared column with the same editable/draft/link behavior as the grid. */\n renderCell: (columnId: string) => ReactNode;\n /** Whether this row is in the canonical selection set. */\n selected: boolean;\n /** Whether the consumer allows this row to be selected. */\n selectable: boolean;\n /** Update selection through the table's controlled/URL-backed contract. */\n onSelectedChange: (selected: boolean) => void;\n /**\n * The table's canonical per-row copy controls plus consumer row actions.\n * Render this instead of rebuilding either action path inside the card.\n */\n actions: ReactNode;\n /** Optional inline-detail state for card renderers. */\n isExpanded?: boolean;\n /** Toggle this row's optional inline detail. */\n toggleExpanded?: () => void;\n /** Optional inline detail body; cards choose its placement. */\n expandedDetail: ReactNode;\n}\n\nexport type MatrxDataTableDensity = \"condensed\" | \"normal\" | \"spacious\";\n\n/**\n * What the table needs in order to paint a `TableStyle` itself.\n *\n * `fieldOf` exists because a style names DATA fields (\"tint the row when\n * `status` is Blocked\") while the table addresses COLUMNS. In the common case\n * the column id IS the field name and the default is right; a table whose\n * column ids are decorated (`records-grid-notes`) maps them here.\n */\nexport interface MatrxDataTableStyleConfig<T> {\n /** The style document. */\n style: TableStyle;\n /** The row's data as the rules read it. Defaults to the row itself. */\n documentOf?: (row: T) => Record<string, unknown>;\n /** Column id to the field name a rule would name. Defaults to identity. */\n fieldOf?: (columnId: string) => string;\n /**\n * The color a choice column's option paints with, for color-by. The consumer\n * supplies it because option colors live on the resolved option list, which\n * may come from a shared pick list this package cannot reach.\n */\n choiceColorFor?: ChoiceColorLookup;\n}\n\n/**\n * GROUPING — \"group these rows by Status\" with collapsible sections, a count\n * and a subtotal on each.\n *\n * This is the primitive's answer to the biggest gap against Airtable, Google\n * Sheets and Notion. It is opt-in, and it is CONTROLLED for the two pieces of\n * state a person expects to survive: which column groups, and which sections\n * are shut. Both are ordinary values, so a saved view carries them like any\n * other reading of the table.\n *\n * Grouping runs AFTER local processing and over the rows the table is about to\n * render. It never re-sorts inside a group: the sort the person chose is the\n * order they see.\n */\nexport interface MatrxDataTableGroupingConfig<T> {\n /** Column id to group by. `null` means grouping is available but off. */\n columnId: string | null;\n /** Emitted when the person picks a different column (or clears it). */\n onColumnIdChange?: ((columnId: string | null) => void) | undefined;\n /**\n * Columns offered in the group-by control. Defaults to every visible column\n * that is not an actions column. A column with thousands of distinct values\n * is a bad grouping and the consumer is the only one who knows that.\n */\n groupableColumnIds?: string[] | undefined;\n /** Per-column subtotal shown on every group header, keyed by column id. */\n aggregates?: Record<string, GroupAggregateSpec> | undefined;\n /** Controlled collapsed group keys (`TableGroup.key`). */\n collapsed?: string[] | undefined;\n onCollapsedChange?: ((collapsed: string[]) => void) | undefined;\n /** Section order. Default `\"value-asc\"`; blanks always sort last. */\n order?: GroupOrder | undefined;\n /** Label for the section of rows whose grouping value is blank. */\n emptyLabel?: string | undefined;\n /** Singular noun for a group's summary (\"job\" → \"12 jobs\"). */\n rowNoun?: string | undefined;\n /**\n * Replaces the group's plain-text label while retaining the canonical\n * collapse control, count, and aggregate cells. Use when a group identity is\n * an addressable platform entity and must keep its Open/new-tab/Peek doors.\n */\n renderLabel?: ((group: TableGroup<T>) => ReactNode) | undefined;\n /**\n * WHAT THE WHOLE SOURCE SAYS ABOUT A GROUP, when the table holds only a page of it (grids review\n * 3). A paged table's group holds the rows on THIS page; the host that can ask its store for the\n * group's real count and subtotals answers here, and the header shows those — the page's own\n * count and subtotals are the fallback, never mixed with them. `count` null/absent = the page's;\n * an aggregate absent for a column = the page's for that column.\n */\n groupFacts?: ((group: TableGroup<T>) => { count?: number | null; aggregates?: Record<string, ReactNode> } | undefined) | undefined;\n /**\n * Reads the grouping value for a column. Defaults to the column's own\n * accessor.\n *\n * NOT named `valueOf`: every object in JavaScript already has one, so an\n * optional config field by that name is always \"present\" and a `config.valueOf\n * ? … : …` check silently calls `Object.prototype.valueOf` and groups every\n * row under the config object itself. It did exactly that here once.\n */\n readCell?: ((row: T, columnId: string) => unknown) | undefined;\n}\n\n/**\n * DRILL — group by a Dimension with SERVER-computed groups, pivot a second Dimension across the\n * top, pick the Measures shown, and drill on a group (lane D3; `./drill.ts` is the whole model).\n *\n * CONTROLLED like `grouping`: the question is the consumer's (it lives in the address and saves\n * as a Saved view), the table only asks for it to change. THE TABLE ASKS, THE HOST ANSWERS:\n * `drillRequests(question)` names each grouping the screen needs and the host puts each door's\n * rows into `answers` under the request's key. While `question.by` is empty the table lists its\n * records as always (narrowed by the host to `question.where`), and the trail stays in the\n * toolbar so a person can zoom back out.\n *\n * With `drill` the toolbar's group-by control groups by Dimensions on the server; the\n * page-scoped `grouping` is not drawn at the same time.\n */\nexport interface MatrxDataTableDrillConfig {\n dimensions: readonly MatrxDrillDimension[];\n measures: readonly MatrxDrillMeasure[];\n /**\n * Declared drill paths (levels, outermost first — `created_at:year`, `created_at:quarter`, …):\n * a click on a group follows the path its level sits on before any other Dimension.\n */\n paths?: readonly (readonly string[])[] | undefined;\n /**\n * The time Dimension a window runs along (its key). Given, the toolbar offers the window\n * presets and, once windowed, the comparison with the prior window.\n */\n windowDimension?: string | undefined;\n question: MatrxDrillQuestion;\n onQuestionChange: (next: MatrxDrillQuestion) => void;\n answers: MatrxDrillAnswers;\n /** A read that failed, in words the person can act on. */\n error?: string | null | undefined;\n /** The trail's first crumb. Default \"All records\". */\n rootLabel?: string | undefined;\n /** Singular noun (\"record\" → \"12 records\"). */\n rowNoun?: string | undefined;\n /** How a blank group reads. Default \"No value\". */\n emptyLabel?: string | undefined;\n}\n\n/**\n * VIRTUALIZATION — render the rows a person can actually see.\n *\n * Off by default, because most tables in the product are short and a windowed\n * body costs a scroll container contract that a short table does not need.\n * On, it engages only past `threshold` rows, so a 40-row table behaves exactly\n * as it always did and a 100,000-row table scrolls.\n */\nexport interface MatrxDataTableVirtualizeConfig {\n enabled?: boolean | undefined;\n /** Row height in px used for the window math. Default follows density. */\n rowHeight?: number | undefined;\n /** Rows rendered beyond each edge of the viewport. Default 8. */\n overscan?: number | undefined;\n /** Row count below which the body renders whole. Default 150. */\n threshold?: number | undefined;\n /**\n * Window the COLUMNS too, for tables hundreds of columns wide. Frozen\n * columns are always rendered, never windowed out.\n */\n columns?: boolean | undefined;\n /** Column width in px used for the horizontal window. Default 160. */\n columnWidth?: number | undefined;\n}\n\n/**\n * THE HONEST COUNT.\n *\n * A table that filters over part of its rows and says nothing is a screen that\n * lies. Declare what the answer actually covers and the table states it, in\n * plain English, above the rows — and never presents a partial count as exact.\n *\n * Omit this entirely and the table says nothing, which is correct for a table\n * whose rows are all in hand.\n */\nexport interface MatrxDataTableCoverageConfig {\n /**\n * Number of rows loaded from the source before the host narrows `data` with\n * a domain filter. Defaults to `data.length`.\n */\n loaded?: number | undefined;\n /** Rows the source says match the current query, when it can say. */\n matched?: number | undefined;\n /** Rows in the table irrespective of the query, when known. */\n total?: number | undefined;\n /** A ceiling the source imposed on this answer (a fetch cap). */\n cap?: number | undefined;\n /** Who answered the filters and sort. Default `\"client\"`. */\n answeredBy?: \"source\" | \"client\" | undefined;\n /** Singular noun for the sentence. Default \"row\". */\n noun?: string | undefined;\n}\n\n/**\n * SPREADSHEET GESTURES — range selection, Excel paste, fill, undo.\n *\n * The `/data` grid had all of this and nothing else in the product could reach\n * it. Opt-in, because a reference table with one editable column does not want\n * a cell cursor; a table a person works IN does.\n *\n * The table owns the gestures and the selection rectangle; the CONSUMER owns\n * the writes. Every mutation arrives as a list of `CellPatch`, so the caller\n * applies it through the door it already has and the table never invents a\n * persistence path of its own. That is also what makes undo honest: the stack\n * holds before-values, and an undo is just another patch list.\n */\nexport interface MatrxDataTableSpreadsheetConfig<T> {\n enabled?: boolean | undefined;\n /**\n * Apply cell writes. Called for a paste, a fill-down, a fill-handle drag, a\n * clear, an undo and a redo. The table re-reads from `data`, so a write the\n * caller refuses simply never appears.\n */\n onPatch?: ((patches: CellPatch[], intent: SpreadsheetIntent) => void | Promise<void>) | undefined;\n /**\n * RANGE SELECTION AND COPY FOR A READER — a flag of its own, separate from\n * editing. A person who may not change a table may still drag out a range,\n * move it with the arrows, select all and press Cmd-C (or right-click the\n * range) to take a real Excel/Sheets block out of it. With `readOnly` the\n * grid offers every selection gesture and no write: no fill handle, no\n * editor on Enter or typing, and cut, paste, clear, fill-down, undo and redo\n * each say `readOnlyReason` rather than doing anything. `onPatch` is not\n * needed. Default false.\n */\n readOnly?: boolean | undefined;\n /** What a refused write says on a read-only grid. Defaults to a plain view-only sentence. */\n readOnlyReason?: string | undefined;\n /** Column ids a patch may target. Defaults to every column marked `editable`. */\n writableColumnIds?: string[] | undefined;\n /**\n * TYPE-TO-EDIT FOR A CONSUMER THAT DRAWS ITS OWN CELL EDITORS. The table opens its own editor\n * for a column marked `editable`; a consumer whose cells carry their own editor (a record grid\n * whose cell IS the Field's editor) is asked here instead, with the cell the person is on and\n * what they typed — `seed` is the text (a letter, an IME composition, an Option/AltGr\n * character, dictation), `null` for Enter/F2. Return true when an editor opened, so the\n * keystroke is consumed. Never asked on a `readOnly` grid.\n */\n onEditRequest?: ((address: { rowId: string; columnId: string }, seed: string | null) => boolean) | undefined;\n /**\n * A CELL WHOSE OWN GESTURE IS SPACE (a tick box). Answer true and Space on that cell is sent to\n * `onEditRequest` (seed `null`) instead of expanding the row (Sheets and Airtable toggle a tick\n * box on Space). A consumer's open editor marks itself `data-matrx-cell-editor` so every key\n * inside it is the editor's (`CELL_EDITOR_ATTR`).\n */\n cellTakesSpace?: ((address: { rowId: string; columnId: string }) => boolean) | undefined;\n /** Why a column refuses a write, as a sentence a person can act on. */\n columnRefusal?: ((columnId: string) => string | undefined) | undefined;\n /** Reads a cell for copy and fill. Defaults to the column's own accessor. */\n readCell?: ((row: T, columnId: string) => unknown) | undefined;\n /** The little square at the corner of a selection. Default true. */\n fillHandle?: boolean | undefined;\n /** Paste real Excel/Sheets TSV into the selection. Default true. */\n paste?: boolean | undefined;\n /** Undo depth. 0 turns undo off. Default 100. */\n undoDepth?: number | undefined;\n /** Says what just happened, and why something did not. */\n notify?: ((message: string) => void) | undefined;\n /**\n * Judge ONE value before it is written, for a paste, a fill or a clear.\n *\n * Return a sentence to REFUSE it — the cell is left alone and the sentence is\n * what the person is told. Return undefined to accept. This is the same rule\n * the inline editor enforces (`edit.validate`), asked in the one other place\n * a value can enter a cell, because a rule a paste can walk around is not a\n * rule.\n */\n validateCell?: ((args: { rowId: string; columnId: string; value: unknown }) => string | undefined) | undefined;\n /**\n * ASK BEFORE A PASTE IS WRITTEN, AND HAND OVER THE ROWS THAT DID NOT FIT.\n *\n * Absent (the default, and byte-for-byte what a paste did before): the table\n * screens the block and applies it straight away, which is right for a paste\n * of a handful of cells into rows that already exist.\n *\n * Present: the table computes the whole plan and applies NOTHING. The\n * consumer is handed what would be written, what was refused and why, and the\n * rows that landed past the last row — and calls `apply()` itself if the\n * person says yes.\n *\n * WHY THE OVERFLOW IS HANDED OVER AT ALL. A paste of fifty spreadsheet rows\n * into a table that holds three is the ordinary way a person fills a new\n * table, and until 2026-09-21 forty-seven of those rows were counted and\n * dropped (\"landed past the edge of the table and were ignored\"). The table\n * cannot create rows — it does not own the writes — but it must not be the\n * reason nobody can. So it says exactly which rows they were, in the grid's\n * own column order, and a consumer that knows how to make a row makes them.\n * A consumer that does not simply leaves `overflow` alone and the sentence is\n * the same as before.\n */\n onPastePlan?: ((plan: SpreadsheetPastePlan) => void) | undefined;\n /**\n * WHERE THE PERSON IS STANDING, told as it moves (records-ui merge tranche 6l, inventory H1).\n *\n * An agent beside the grid is asked about \"this cell\" and \"these cells\"; only the table knows\n * which cell the cursor is on and which block is selected. Called after every change of the\n * cell cursor or the selected rectangle — `null` when nothing is selected. `tsv` is the block as\n * Excel / Sheets would copy it (no header row), `cellCount` how many cells it spans.\n */\n onSelectionChange?: ((selection: SpreadsheetSelection | null) => void) | undefined;\n /**\n * THE GRID'S HANDS, for a consumer whose cells carry their own editors (merged-grid review\n * 2026-09-26, A1). The table writes its `SpreadsheetController` here: `endEdit(exit, from)` when\n * such an editor closes gives the grid the keyboard back with the cursor moved the way Sheets\n * moves it (Enter down, Tab right, Escape stays); `select` puts the cursor on a cell. The\n * table's own editors need nothing — the grid's key net covers them.\n */\n controllerRef?: { current: SpreadsheetController | null } | undefined;\n}\n\n/** The cell cursor and the selected rectangle, by id (`spreadsheet.onSelectionChange`). */\nexport interface SpreadsheetSelection {\n /** The cell the cursor is on (the moving end of the range). */\n active: { rowId: string; columnId: string };\n /** The rows and columns the rectangle covers, in the grid's visual order. */\n rowIds: string[];\n columnIds: string[];\n cellCount: number;\n /** The rectangle's values as a TSV block, the way a copy would carry them. */\n tsv: string;\n}\n\n\n/**\n * THE VALUES A COLUMN HOLDS, with a count beside each one.\n *\n * A filter that only offers \"Contains…\" asks the reader to already know what is\n * in the column. The checklist tells them, and the counts are what make it\n * usable: \"Blocked 12\" is a decision, \"Blocked\" is a guess.\n *\n * THE HONESTY RULE: counts are computed in the browser ONLY when the browser\n * holds every row they describe. Otherwise `source` is asked — the database\n * counts over the whole column — and if there is no source, the popover SAYS\n * the counts cover only the loaded rows. A partial count is never printed as if\n * it were the whole truth.\n */\nexport interface MatrxDataTableFacetsConfig<T> {\n enabled?: boolean | undefined;\n /**\n * Database-computed values and counts for one column. Called only when the\n * rows in the browser do not cover `totalRows` — local-data-first is the same\n * rule for every source. Return null (or throw) and the popover says so and\n * falls back to matching text.\n */\n source?: ((args: { columnId: string; search: string; limit: number }) => Promise<ColumnFacets | null>) | undefined;\n /**\n * Rows the whole table holds after the active search. This is how the table\n * knows whether the rows it has are all of them. Omit it and the table treats\n * its own rows as a partial answer and says so.\n */\n totalRows?: number | undefined;\n /** Most values to list. Default 200, hard ceiling 500. */\n limit?: number | undefined;\n /** Reads a cell for counting. Defaults to the column's own filter accessor. */\n readCell?: ((row: T, columnId: string) => unknown) | undefined;\n}\n\nexport interface MatrxDataTableProps<T> {\n data: T[];\n columns: MatrxColumnDef<T>[];\n /** Table-local visual density. An explicit value wins over the host default. */\n density?: MatrxDataTableDensity;\n /**\n * `standalone` (default): the table draws its own frame, grey uppercase header and pager.\n * `embedded`: the table sits inside a surface that already frames it (a `MatrxTableCard`):\n * no frame, sentence-case header on the surface's own background, no zebra, and a pager only\n * when there is something to page. Inside a `MatrxTableCard` this is the default.\n */\n appearance?: MatrxDataTableAppearance;\n /** Stable persistence identity for host-installed named views. */\n tableId?: string;\n /**\n * THE REGISTRY TOKEN OF THE ROWS (lane 7 STANDARD-TABLES W5). When set, the host's\n * `useCustomFieldColumns` port adds the custom-field columns of that token, for the organizations\n * the rows belong to (each row's `organization_id`), hidden until picked in Columns. Rows carry\n * their values in `custom_fields`. No per-list code: every table of registry rows inherits them.\n */\n rowToken?: string;\n /** Declared reset state for the host Saved Views control. */\n defaultViewSnapshot?: TableViewSnapshot;\n /** Working view tabs, on by default. Disable for embedded/single-purpose tables. */\n viewTabs?: boolean;\n /**\n * Where the working view tabs are kept. Absent: in memory, gone on reload. A page people\n * return to passes its preferences store, so a view made with \"+\" is named and survives.\n */\n viewTabsStore?: TableViewTabsStore;\n /** Related-data controls shown at the right of the working-view tab strip. */\n relatedTableActions?: ReactNode;\n /** Optional URL/source-owned column order and show/hide state. */\n columnState?: {\n order: string[];\n hidden: string[];\n onChange: (next: { order: string[]; hidden: string[] }) => void;\n /** Optional source-owned desktop widths keyed by canonical column id. */\n widths?: Record<string, number>;\n /** Required to persist controlled width changes across remounts. */\n onWidthsChange?: (widths: Record<string, number>) => void;\n };\n /** Enables package-owned drag reordering in table headers. Default true. */\n reorderableColumns?: boolean;\n /** Enables desktop column resizing. Default true; handles are absent below `sm`. */\n resizableColumns?: boolean;\n columnManager?: { open: boolean; onOpenChange: (open: boolean) => void };\n getRowId: (row: T) => string;\n /**\n * Additional row identity included in local global search without becoming\n * a visible/sortable/filterable column. Use for canonical composite keys or\n * aliases whose displayed parts live in separate columns.\n *\n * Ignored in remote controlled mode, where the query owner applies search.\n */\n searchText?: (row: T) => string;\n /**\n * Hierarchy-aware local processing seam. The canonical table still owns the\n * toolbar, URL state, headers, pagination, editing, copy, and rendering; the\n * consumer only preserves domain ordering that a flat sort would destroy.\n *\n * The processor must honor every active query control in `state` and return\n * the complete filtered/sorted local result before pagination. It is ignored\n * in remote controlled mode, where `data` is already the queried page.\n */\n processLocalRows?: (rows: T[], state: MatrxDataTableQueryState) => T[];\n /** Initial local sort when neither URL state nor a controlled query owns it. */\n defaultSort?: SortState | null;\n isLoading?: boolean;\n /**\n * Background refresh state. Unlike `isLoading`, this preserves rendered rows\n * and shows only the table's non-blocking refresh indicator.\n */\n isFetching?: boolean;\n /**\n * Controlled query state for direct remote data sources. The component never\n * fetches data itself; it only emits state changes to the caller.\n */\n query?: MatrxDataTableQueryControl<T>;\n /**\n * Persist local query and record-view state in namespaced URL parameters.\n * This is intentionally opt-in and cannot be combined with controlled query\n * mode; remote tables use `useTableUrlState({ tableId })` in their query owner.\n */\n urlState?: MatrxDataTableUrlStateConfig;\n\n toolbar?: MatrxDataTableToolbar;\n /** Hide the package toolbar while retaining table-owned query behavior. */\n hideToolbar?: boolean;\n /** Hide numbered pagination; local rows remain available to the caller. */\n hidePagination?: boolean;\n /** Host wording for a controlled source range when visible rows are a local projection. */\n paginationLabelFormat?: (start: number, end: number, total: number) => string;\n /** Optional deferred Apply/Cancel copy for column filter popovers. */\n filterUi?: MatrxDataTableFilterUi;\n /** Row click opens the side panel unless `window.openOnRowClick` is true. */\n detail?: MatrxDataTableDetailConfig<T>;\n /** Controlled detail rendered directly below a table row. */\n expandedDetail?: MatrxDataTableExpandedDetailConfig<T>;\n /** Panel icon opens a WindowPanel (page-local; supports ReactNode override). */\n window?: MatrxDataTableWindowConfig<T>;\n /** Copy + Copy for AI (rows + this view). Omit for the declared-column default; false suppresses it. */\n copy?: false | MatrxDataTableCopyConfig<T>;\n /** Independently hide table or row copy without replacing canonical payloads. */\n copyControls?: { table?: boolean; row?: boolean };\n /** Where canonical per-row Alchemy appears. Inline preserves current behavior. */\n rowCopyPlacement?: \"inline\" | \"menu\";\n /** One pane-level right-click resolver; host owns the opaque descriptor. */\n contextMenu?: MatrxDataTableContextMenuConfig<T>;\n /** Inline edit session with floating Save/Cancel pill. */\n edit?: MatrxDataTableEditConfig<T>;\n /** Opt-in tree reparenting owned by the canonical row renderer. */\n hierarchy?: MatrxDataTableHierarchyConfig<T>;\n\n /** Controlled selection (selected row id for highlight). */\n selectedId?: string | null | undefined;\n onSelectedIdChange?: ((id: string | null) => void) | undefined;\n\n /** Controlled table-owned window row (normally supplied by URL state). */\n windowRowId?: string | null | undefined;\n onWindowRowIdChange?: ((id: string | null) => void) | undefined;\n\n /**\n * Controlled multi-row selection and domain bulk actions. When omitted,\n * the table owns checkbox selection and Alchemy copy for loaded rows.\n */\n selection?: false | MatrxDataTableSelectionConfig<T>;\n\n /** Icon-only actions in the built-in Actions column. Labels are accessible names/tooltips, never button text. */\n rowActions?: (row: T, controls: MatrxDataTableRecordControls) => readonly MatrxTableIconAction[];\n /** Extra rows rendered in the table footer with the same columns as data rows. */\n footerRows?: T[];\n /** Optional KPIs above the toolbar and aligned totals in the canonical footer. */\n /** Controlled list filters. Hosts reset source paging/selection in callbacks. */\n scopeBar?: TableScopeBarProps;\n summary?: MatrxDataTableSummaryConfig<T>;\n /** Per-row presentation class, after canonical selection/highlight state. */\n rowClassName?: (row: T, index: number) => string | undefined;\n /**\n * WHAT ELSE CHANGES HOW THIS ROW DRAWS, besides the record itself (lane RENDER-AUDIT, 2026-09-26).\n *\n * Body rows are memoised: a row is drawn again only when its record changed identity, its own\n * table facts changed (ticked, open, highlighted, its pending edits, spreadsheet flags), or the\n * table's shape changed. A host whose `cell` renderers read state OUTSIDE the record — a cell\n * editor it owns, an optimistic value, a refusal beside one cell — answers here with a value\n * that changes whenever that state changes for THIS row (compared with `Object.is`; an array is\n * compared item by item). Keep the function itself stable; it is called for every row on every\n * render and must be cheap.\n */\n rowVersion?: (row: T) => unknown;\n /**\n * Per-CELL presentation class, after the column's own static `className`.\n *\n * The primitive had `rowClassName` and a static per-column `className`, and\n * nothing in between — so every table that needed to say something about ONE\n * cell had to grow a `cell` render function and paint inside it, where the\n * table can no longer see it. This is the missing rung, and `tableStyle`\n * below is the declarative thing built on top of it.\n */\n cellClassName?: (\n row: T,\n columnId: string,\n index: number,\n ) => string | undefined;\n /**\n * COLORS, declaratively — color by a column, rules, manual highlights.\n *\n * The model is `./table-style.ts`: one document, evaluated live on every\n * render so a tint is never stale, with a documented precedence between the\n * three ways a cell can earn a color. Pass the document and the table paints\n * itself; the consumer owns only where that document is STORED and how a\n * person edits it.\n */\n tableStyle?: MatrxDataTableStyleConfig<T>;\n /**\n * GROUPS — collapsible sections by a column, with a count and per-column\n * subtotals on each header. Opt-in and controlled; see the config's own doc.\n */\n grouping?: MatrxDataTableGroupingConfig<T>;\n /**\n * DRILL — server-computed groups, a pivot, a measure picker and drill-on-a-group, with the\n * breadcrumb in the toolbar. Controlled; see the config's own doc.\n */\n drill?: MatrxDataTableDrillConfig | undefined;\n /** Render only the rows in view. Opt-in; engages past its own threshold. */\n virtualize?: boolean | MatrxDataTableVirtualizeConfig;\n /**\n * What the current answer actually covers. Declare it and the table says so\n * on screen when a filter is answering over part of the table, instead of\n * presenting a partial count as the whole truth.\n */\n coverage?: MatrxDataTableCoverageConfig;\n /** Range selection, Excel paste, fill handle, fill-down, undo. Opt-in. */\n spreadsheet?: MatrxDataTableSpreadsheetConfig<T>;\n /** Real values with their counts in every column filter popover. */\n facets?: MatrxDataTableFacetsConfig<T>;\n /**\n * Explicit body row height in px. Overrides the density default. Use it when\n * a saved view remembers a row height the person chose.\n */\n rowHeight?: number;\n /**\n * FIT TO THE WIDTH (from `sm` up): every data column takes its share of the space in proportion\n * to the width it would otherwise have, so the table is exactly as wide as its container and\n * never scrolls sideways — Airtable's \"fit\". Off, columns keep their widths and the table\n * scrolls. A phone always scrolls: a fitted phone table is columns too narrow to read.\n */\n fitToWidth?: boolean | undefined;\n /** Highlight an index in the currently rendered page without selecting it. */\n highlightedIndex?: number | undefined;\n /** Receives the complete visible local view after local processing, before paging. Attaching after absence receives one snapshot; callback identity changes alone do not notify. */\n onViewChange?: (rows: T[]) => void;\n /**\n * Wrap the whole `<tr>`. Return `children` unchanged for no-op.\n *\n * `rowActions` only reaches the actions CELL, so anything that must own the\n * entire row — a right-click menu, a drag handle, a drop target — had no\n * seam and would have forced a surface to fork the table. THE INVENTORY LAW:\n * the fork is the defect, so the seam exists instead.\n *\n * Whatever you return must render `children` as a direct `<tbody>` child, so\n * the wrapper has to be a component that emits the `<tr>` unchanged\n * (`ItemContextMenu` does — it renders a Radix trigger with `asChild`).\n */\n rowWrapper?: (row: T, children: ReactNode) => ReactNode;\n\n emptyState?: MatrxDataTableEmptyState;\n /**\n * The outcome of the read behind `data` — see `MatrxDataTableRead`. Every\n * table fed by a read passes it; `emptyState` then renders only after a read\n * that succeeded. matrx-frontend's lint rule `matrx/empty-state-needs-read-gate`\n * refuses an `emptyState` without it on read-backed rows.\n */\n read?: MatrxDataTableRead | undefined;\n /** Default 25. Pass 0 to show all. */\n pageSize?: number;\n /** Exact numbered pages are the default. Progressive reveal is an explicit\n * table-specific exception and must be reviewed before adoption. */\n localPagination?:\n | { mode: \"progressive\" }\n | { mode: \"numbered\"; reason: string; approvedBy: string };\n pageSizeOptions?: number[];\n zebra?: boolean;\n /**\n * Keep the one semantic table header visible while its nearest vertical\n * scroll surface moves. Defaults to true. Set false only where surrounding\n * chrome already supplies the column labels.\n */\n stickyHeader?: boolean;\n className?: string;\n tableClassName?: string;\n /**\n * `fill` (default): the bordered frame takes the full height it is given. `content`: the frame\n * ends at its last row and scrolls only once the rows outgrow that height — a list of three\n * rows draws no tall empty box.\n */\n frameHeight?: \"fill\" | \"content\";\n /**\n * `show` (default): the column header is always drawn. `hide`: a settled result with no rows\n * draws no column header (chrome over rows that do not exist) — unless a column filter is\n * set, because the header is where that filter is cleared.\n */\n emptyHeader?: \"show\" | \"hide\";\n /**\n * Optional compact row presentation rendered below `mobileCardsBreakpoint`\n * in place of the horizontal table. The caller supplies the record summary\n * because only the product surface knows which values and actions are\n * essential at narrow widths; MatrxDataTable still owns query state,\n * loading/empty states, and pagination.\n *\n * `controls.actions` carries the table-owned copy controls and consumer row\n * actions, so a card does not fork them. Prefer the default horizontal table\n * unless the product explicitly requires every essential value/action to be\n * discoverable without horizontal scrolling.\n */\n mobileCards?: (\n row: T,\n index: number,\n controls: MatrxDataTableMobileCardControls,\n ) => ReactNode;\n /** Card/table handoff. `sm` is phone-only; `lg` includes portrait tablets. */\n mobileCardsBreakpoint?: \"sm\" | \"lg\";\n /**\n * Resolves a record's canonical destination for modified row gestures.\n * Cmd/Ctrl/Shift-click and middle-click open this URL in a new browsing\n * context; an unresolved URL deliberately does not fall back to row open.\n * Plain row clicks retain the table's detail/window/onRowOpen behavior.\n */\n getRowHref?: (row: T) => string | undefined;\n /** Called after a row is selected for detail (in addition to opening the panel). */\n onRowOpen?: (row: T) => void;\n}\n\n/** Re-export for callers building custom agent payloads. */\nexport type { AgentPayloadInput };\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/types.ts"],"sourcesContent":["import type { TableScopeBarProps } from \"./TableScopeBar\";\nimport type { ContentTransferReference } from \"../content-transfer/extensions\";\nimport type { ComponentType, MouseEvent, ReactNode } from \"react\";\nimport type { TableViewTabsStore } from \"./TableViewTabs\";\nimport type { TableSearchRole, TableSearchScope } from \"./ranked-search\";\nimport type { AgentPayloadInput } from \"./copy\";\nimport type { CopyExportConfig } from \"./copy-types\";\nimport type {\n AiCustomSource,\n AiVariant,\n} from \"./copy\";\nimport type { LayeredFilterField, LayeredFilterRule } from \"./layered-filters\";\nimport type { ChoiceColorLookup, TableStyle } from \"./table-style\";\nimport type { FieldFormatConfig } from \"../field-formats/types\";\nimport type { GroupAggregateSpec, GroupOrder, TableGroup } from \"./grouping\";\nimport type { MatrxDrillAnswers, MatrxDrillDimension, MatrxDrillMeasure, MatrxDrillQuestion } from \"./drill\";\nimport type { CellPatch } from \"./fill\";\nimport type { ColumnFacets } from \"./facets\";\nimport type { SpreadsheetController, SpreadsheetIntent, SpreadsheetPastePlan } from \"./useSpreadsheetGrid\";\n\nexport type { SpreadsheetPastePlan };\nimport type { DateColumnFilterValue } from \"./date-filter\";\nimport type { MatrxTableMenuItem, MatrxTableMenuPayload, MatrxTableMenuSection } from \"./menu-targets\";\n\n/** How a column's filter UI behaves. `auto` infers from sample values. */\nexport type ColumnFilterKind =\n \"auto\" | \"text\" | \"select\" | \"boolean\" | \"number\" | \"date\" | false;\n\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * A compact boolean marker with the language people expect for a saved item.\n *\n * `undefined` recognizes only the exact conventional boolean accessor names\n * (`favorite`, `is_favorite`, `favourite`, `is_favourite`, `pinned`, and\n * `is_pinned`). Pass `false` to keep one of those fields as an ordinary\n * boolean column. `\"favorite\"` and `\"pin\"` make the intent explicit for\n * differently named fields.\n */\nexport type ColumnMarker = \"favorite\" | \"pin\" | false;\n\n/** How the table's primary search text is matched. */\nexport type TableSearchMatchMode = \"contains\" | \"whole_words\";\n\n/** Cell value type for typed inline editors (Supabase-style popovers for non-strings). */\nexport type CellEditType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"select\"\n /** Free-text multi-value chips (string[] cells: tags, labels, aliases). */\n | \"tags\"\n | \"date\"\n | false;\n\n/** The table owns action markup and geometry; hosts supply behavior and an icon component only. */\nexport interface MatrxTableIconAction {\n id: string;\n icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n label: string;\n tooltip?: string;\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n disabled?: boolean;\n loading?: boolean;\n tone?: \"default\" | \"primary\" | \"destructive\" | \"success\" | \"warning\";\n variant?: \"default\" | \"secondary\" | \"destructive\" | \"outline\" | \"ghost\" | \"link\";\n}\n\nexport interface MatrxColumnDef<T> {\n /** Stable id used for sort/filter state. Defaults to `accessorKey` when set. */\n id?: string;\n /** Dot-free key on the row for default value access + auto filter. */\n accessorKey?: keyof T & string;\n /** Custom value for sort/filter when `accessorKey` is insufficient. */\n accessorFn?: (row: T) => unknown;\n /** Value used only for local sort. Falls back to `accessorFn` / `accessorKey`. */\n sortValue?: (row: T) => unknown;\n /** Value used only for local filters. Falls back to `accessorFn` / `accessorKey`. */\n filterValue?: (row: T) => unknown;\n /**\n * WHAT COPY, COPY FOR AI AND EVERY EXPORT CARRY — the value the person SEES in the cell (a\n * reference's name, never the id it stores). Falls back to `accessorFn` / `accessorKey`. When\n * set, the stored value (the id) is still offered in the preparation workspace as an optional\n * \"<column> id\" column, left out by default — never instead of the name.\n */\n copyValue?: (row: T) => unknown;\n header: ReactNode;\n /** Plain-text name used for header controls when `header` is visual-only. */\n label?: string;\n /** Cell renderer. Defaults to stringified accessor value. */\n cell?: (row: T, index: number) => ReactNode;\n /** An explicitly declared custom actions column. Arbitrary content never enters the built-in Actions column. */\n customActions?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /** Sortable unless explicitly false. Default true. */\n sortable?: boolean;\n /**\n * WHY A COLUMN OFFERS NO SORT, in a sentence (merged-grid review 2: Created / Last changed could\n * not be sorted and nothing said why). Shown in the header's menu where the sort would be.\n */\n sortRefusal?: string | undefined;\n /** Direction used by the first header-click sort. Default `\"asc\"`. */\n defaultSortDirection?: SortDirection;\n /**\n * Filter kind. Default `\"auto\"` — every column gets a filter.\n * Pass `false` only when a column truly must not filter (e.g. actions).\n */\n filter?: ColumnFilterKind;\n /** Explicit select options (when filter is `\"select\"` or auto-detected). */\n filterOptions?: Array<{ value: string; label: string }>;\n /**\n * Single-choice select filter. The default select filter is a multi-select\n * that APPENDS (OR semantics) — correct for a status column, wrong for a\n * column whose options are mutually exclusive VIEWS of the list (a\n * record-class scope, a relative-date bucket). There, appending makes the\n * filter inert: the consumer reads one value, the popover accumulates a set,\n * and the first-selected value wins forever (D218). With `filterSingle`,\n * choosing an option REPLACES the selection, and choosing the active option\n * again clears it.\n */\n filterSingle?: boolean;\n /**\n * THE CONSUMER'S OWN COLUMN ACTIONS, in the header's one menu — rename,\n * insert left/right, use as the row label, colour by, settings — drawn under\n * the built-in sort and filter, in the same popover, never a second control\n * beside it. An item with `disabledReason` stays visible and says why it\n * cannot run here. Absent or empty, the header menu is exactly what it was.\n */\n headerMenu?: MatrxTableMenuItem[];\n /** Allow an exact typed value not present in loaded select options. */\n filterAllowCustom?: boolean;\n /**\n * Inline edit. Default false. `\"string\"` edits in-cell; other types open a\n * small popover (Supabase-style). Edits stay local until Save on the dirty pill.\n */\n editable?: CellEditType;\n /**\n * Per-row edit gate for an `editable` column. Return false to render the\n * plain cell (no pencil, no click-to-edit) for that row — for heterogeneous\n * lists where some kinds cannot take the write (e.g. a transcripts row of\n * kind \"unsorted\" has no user-facing title). Default: every row editable.\n */\n editableIf?: (row: T) => boolean;\n /**\n * How inline edit starts. Default `\"click\"` (click the cell body). `\"pencil\"`\n * shows a hover/focus pencil; the cell body no longer starts edit — so a\n * whole-row click can own that gesture. Forced to `\"pencil\"` when `href` is\n * set (D112 — the body is a real link).\n */\n editTrigger?: \"click\" | \"pencil\";\n /**\n * Options when `editable === \"select\"`. Also used by `\"tags\"` as the\n * suggestion list (existing values), while still allowing new entries.\n */\n editOptions?: Array<{ value: string; label: string }>;\n /**\n * Row link for the primary/title cell (D112): renders the cell content as a\n * real `next/link` anchor, so the row is reachable by keyboard, announced as\n * a link by screen readers, and cmd/middle-clickable into a new tab. The\n * whole-row `onRowOpen` click stays as a mouse convenience; clicks on the\n * anchor never double-fire it. Combine with `editable` and the link renders\n * with a hover/focus pencil that opens the inline editor instead of\n * click-text-to-edit.\n */\n href?: (row: T) => string | undefined;\n /**\n * Canonical entity token for the record this column NAMES. When set, the cell\n * renders through `EntityRef`, so the name carries the full door set — Open,\n * new tab, and Peek — instead of the Open-only `<Link>` that `href` alone\n * produces.\n *\n * THE INVENTORY LAW, applied to this component: the table grew its own door\n * (`href`) beside the platform's (`EntityRef`), and every column that named a\n * record picked one and silently lost the other half. This field collapses\n * them — `href` still works and still forces the pencil trigger, and it\n * OVERRIDES the registry route when both are set (for an admin-side route on\n * a satellite deployment).\n *\n * Needs the record's id: `entityToken` is paired with `entityId`, defaulting\n * to the table's own `getRowId`.\n *\n * **PER ROW, not per column** — a hub can be heterogeneous. `/transcripts`\n * lists transcripts, studio sessions, cleanup runs and an \"unsorted\" bucket\n * in one table, each with its own destination; a constant token would have\n * sent a session id to the transcript processor route and opened the\n * transcript peek on a record that is not one. Return `undefined` for a row\n * that names no entity — it falls back to the plain `href` link, or to inert\n * text. Pair with a per-row `href` when the kinds diverge.\n */\n entityToken?: string | ((row: T) => string | undefined);\n /**\n * NOTE: give the column a `cell` when you set this. Without one, a\n * UUID-shaped value renders the default `MatrxUuidCell`, which has its own\n * controls — wrapping those in the door's anchor would nest interactive\n * elements inside a link, and is redundant besides. The shell detects that\n * combination, skips the door, and screams once per column.\n */\n /** The id `entityToken` refers to. Defaults to the table's `getRowId(row)`. */\n entityId?: (row: T) => string | undefined;\n /**\n * Drop this column below `sm` (the phone breakpoint).\n *\n * A wide table on a phone becomes a horizontal scroller: the frozen\n * identity column stays, and everything after the second column sits off\n * the right edge where a reviewer will never find it. Marking the columns\n * that do NOT earn their width on a phone is how a surface declares an\n * INTENTIONAL mobile column set instead of an accidental one.\n *\n * The column is still fully sortable/filterable from the toolbar and still\n * rides every copy/export payload — this hides the CELL, not the data.\n * Default `false` = today's behavior everywhere.\n */\n mobileHidden?: boolean;\n /**\n * Built-in cell kinds. `\"uuid\"` / `\"fk\"` use MatrxUuidCell (short + copy +\n * optional open). `\"auto\"` (default) detects UUID-shaped strings.\n */\n cellKind?: \"auto\" | \"uuid\" | \"fk\" | \"text\";\n /**\n * FK / UUID navigation. Prefer `onOpen` → WindowPanel of the target.\n * Return `\"forbidden\"` when the caller lacks access.\n */\n fk?: {\n label?: string;\n /**\n * Canonical entity token this column's ids point at (`agent`, `note`, …).\n * THE DOOR LAW made declarative: the cell resolves route + new tab + peek\n * from the registries, so a column of ids stops being a dead end without\n * hand-wiring a link. `href` / `onOpen` still win when both are set.\n * A function form resolves the token per row (an audit log whose target\n * type varies by row).\n *\n * `\"auto\"` derives the token from the COLUMN NAME (`task_id` → `task`).\n * It is opt-in on purpose: the guess is only correct when you have checked\n * the actual FK. `scheduler.sch_run.task_id` references `scheduler.sch_task`,\n * not the workspace `task` the name implies, and `app_id` /\n * `conversation_id` / `file_id` / `workflow_id` each have several candidate\n * tables. A wrong door opens a DIFFERENT record — worse than no door.\n */\n token?: string | null | \"auto\" | ((row: T) => string | null | undefined);\n href?: (id: string, row: T) => string | null | undefined;\n onOpen?: (\n id: string,\n row: T,\n ) => void | \"forbidden\" | Promise<void | \"forbidden\">;\n /** Force non-navigable for this column. */\n forbidden?: boolean | ((id: string, row: T) => boolean);\n };\n /**\n * How this column's value READS — money, percent, a date style, decimal\n * places. The vocabulary is the platform's one field-format registry\n * (`@ai-matrx/design-system/field-formats`), the same `{id, options}` a user\n * data table stores on its column and the record store stores on its Field,\n * so `$1,234.56` is the same `$1,234.56` on every table in the product.\n *\n * A declarative format, not another render function, because a `cell`\n * callback is invisible to everything else: with `format` the column also\n * right-aligns itself, and a consumer's own `cell` asks the primitive for the\n * same text through `formatColumnValue(col, value)` rather than growing a\n * second money formatter beside this one.\n *\n * THE FALLBACK LAW travels with it: a value that does not fit the declared\n * format is never blanked and never throws — it renders its base-type text in\n * amber with the reason on hover, so the person SEES the mismatch.\n *\n * `cell` still wins when both are set.\n */\n format?: FieldFormatConfig;\n /**\n * Keep this column in place while the rest of the table scrolls sideways.\n *\n * Only the LEADING RUN freezes, only from `sm:` up, and only when the widths\n * the offsets need are known — `./frozen-columns.ts` says why each of those\n * three is a rule and not a default. Give a frozen column an explicit\n * `width`; without one the table declines to freeze anything rather than pin\n * a column at a guessed offset.\n */\n frozen?: boolean;\n className?: string;\n headerClassName?: string;\n width?: string | number;\n /** Enable desktop resizing for this column. Defaults to true when the table enables it. */\n resizable?: boolean;\n /** Desktop resize boundary. Defaults to 80px. */\n minWidth?: number;\n /** Desktop resize boundary. Defaults to 1200px. */\n maxWidth?: number;\n align?: \"left\" | \"center\" | \"right\";\n /**\n * ICON COLUMN. A column whose whole content is one glyph — a star, a lock, a\n * status dot — and whose `width` is therefore a lie without this flag.\n *\n * `width` is only a hint on a table cell: min-content wins. A 40px star column\n * still rendered ~70px wide because the HEADER carried three separate\n * controls beside the glyph (the sort button, its arrow, the filter funnel),\n * and the cell carried the default `px-2` on both sides. Every surface that\n * wanted a tight icon column was paying for chrome it never used.\n *\n * `compact` fixes it AT THE PRIMITIVE, and does NOT cost the column anything:\n * horizontal padding drops to `px-1`, and the header collapses its three\n * controls into ONE popover trigger that still offers Sort ascending / Sort\n * descending / Clear sort / the full filter body. The column stays fully\n * sortable and filterable — the affordances moved into the menu, they did not\n * disappear. Active sort/filter still show, as a 2px dot on the trigger.\n *\n * Pair it with `width` and `align: \"center\"`.\n */\n compact?: boolean;\n /**\n * Saved-item marker semantics for a compact boolean column. Markers default\n * to 40px, keep the existing sort/filter menu, and name its choices as\n * Favorites/Pins. See `ColumnMarker` for automatic recognition and opt-out.\n */\n marker?: ColumnMarker;\n /** Hide from the table (still available in column picker when we add it). */\n hidden?: boolean;\n /** Whether the package-owned Columns dialog may hide this column. Default true. */\n hideable?: boolean;\n /**\n * Whether this column's header label is a drag handle for column reordering. Default true.\n * `false` for a column that IS a control (records-ui's \"+\" add-column header): its label stays\n * its own, never a button inside the reorder button, and it never moves among the data columns.\n */\n reorderable?: boolean;\n}\n\n/** How a text filter matches. Default `\"contains\"`. */\nexport type TextFilterMode = \"contains\" | \"empty\" | \"not_empty\" | \"null\" | \"not_null\";\n\n/** Active per-column filter value. Shape depends on filter kind. */\nexport type ColumnFilterValue =\n | { kind: \"text\"; value: string; mode?: TextFilterMode; negated?: boolean }\n | {\n kind: \"select\";\n /** Single-choice value (legacy writers). Ignored when `values` is set. */\n value: string;\n /** Multi-choice OR set — an explicit empty array matches no rows. */\n values?: string[];\n /** Exclude matching values instead of including them. */\n negated?: boolean;\n }\n | { kind: \"boolean\"; value: boolean; negated?: boolean }\n | {\n kind: \"number\";\n min?: number | undefined;\n max?: number | undefined;\n negated?: boolean;\n /**\n * How the number is compared (the champion's operators: =, ≠, <, >, between, empty).\n * Absent means \"between\" — `min`/`max` inclusive, as every older writer meant it. For\n * `eq`/`ne` both bounds hold the value; `lt` reads `max`, `gt` reads `min`, both strict.\n */\n op?: NumberFilterOp | undefined;\n }\n | DateColumnFilterValue;\n\n/** A number filter's comparison. */\nexport type NumberFilterOp = \"eq\" | \"ne\" | \"lt\" | \"gt\" | \"between\" | \"empty\" | \"not_empty\";\n\nexport type ColumnFiltersState = Record<string, ColumnFilterValue | undefined>;\n\nexport interface SortState {\n id: string;\n direction: SortDirection;\n}\n\n/**\n * Opt-in durable URL state for a local table.\n *\n * Every table gets an explicit stable id, producing namespaced parameters such\n * as `table.accounts.q` and `table.accounts.sort`. This prevents collisions\n * with page-owned parameters and with sibling tables on the same route.\n */\nexport interface MatrxDataTableUrlStateConfig {\n /** Stable lowercase identifier: letters, numbers, and hyphens; max 64 chars. */\n id: string;\n /** Initial sort when the URL carries none. Default: none. */\n defaultSort?: SortState | null | undefined;\n /** Browser history behavior for table transitions. Default: `push`. */\n history?: \"push\" | \"replace\" | undefined;\n /**\n * History behavior while typing search/any-of text. `session` pushes the\n * first edit, then replaces rapid keystrokes. Default: `session`.\n */\n textHistory?: \"session\" | \"push\" | \"replace\" | undefined;\n /** Persist the open side-panel row. Default true. */\n selectedRow?: boolean;\n /** Persist the open table-owned window row. Default true. */\n windowRow?: boolean;\n /** Persist checkbox selection. Opt-in because large selections lengthen URLs. */\n selection?: boolean;\n}\n\n/**\n * Complete view state for a remotely queried table page. The table owns none\n * of this state in controlled mode: callers may mirror it to URL search params\n * and use it as part of a direct database-query cache key.\n */\nexport interface MatrxDataTableQueryState {\n /** One-based page number, matching the table's pagination UI. */\n page: number;\n pageSize: number;\n search: string;\n /** Defaults to `contains` when omitted, preserving every existing table. */\n searchMatchMode?: TableSearchMatchMode;\n /** Selected intelligent-search scope. Omitted means the backward-compatible All scope. */\n searchScope?: string;\n anyOf: string;\n /** Ordered AND rules from the compact advanced-filter builder. */\n layeredFilters?: LayeredFilterRule[];\n columnFilters: ColumnFiltersState;\n sort: SortState | null;\n}\n\n/**\n * Optional data-processing contract. Omit it (or use `local`) to preserve the\n * original in-memory filter/sort/pagination behavior. In controlled mode,\n * `data` is already the current page and the caller performs all querying.\n */\n/** Structural match for @ai-matrx/data/react usePaginatedData. */\nexport interface MatrxTablePagination<T> {\n queryKey: string;\n rows: T[];\n loading: boolean;\n isFetchingNextPage: boolean;\n error: Error | null;\n hasNextPage: boolean;\n loadNextPage: () => Promise<void>;\n refresh: () => void;\n totalItems: number | undefined;\n loadAll?: { isLoading: boolean; request: () => void; stop: () => void };\n retrySource?: () => void;\n sourceUntil?: string;\n /** Source records differ from rendered rows when the host groups occurrences. */\n sourceRecords?: { loaded: number; total?: number | undefined; label: string; rowLabel: string; matchedRecords?: number };\n sourcePageSize?: { value: number; options: readonly number[]; onChange: (value: number) => void };\n}\n\n/**\n * A controlled-local table can retain local search/pagination while its source\n * owns column filters and/or sorting. `sourceTotal` is display metadata only:\n * it never changes local page slicing, because the loaded rows remain local.\n */\nexport interface MatrxDataTableSourceProcessing {\n search?: \"local\" | \"source\";\n /** Source accepts and ranks `searchScope`; omit to keep scope UI local-only. */\n intelligentSearch?: \"local\" | \"source\";\n /**\n * Source ownership may cover every column filter or a declared subset. With\n * `{ source: [\"kind\"] }`, the table applies every other active filter to\n * loaded rows and leaves `kind` to the source query.\n */\n columnFilters?: \"local\" | \"source\" | { source: readonly string[] };\n sort?: \"local\" | \"source\";\n sourceTotal?: number;\n}\n\nexport type MatrxDataTableQueryControl<T = unknown> =\n | { mode: \"local\" }\n | {\n /** Accumulated server rows; page stays 1 and sort/filter remain server-owned. */\n mode: \"controlled-append\";\n state: MatrxDataTableQueryState;\n onStateChange: (next: MatrxDataTableQueryState) => void;\n pagination: MatrxTablePagination<T>;\n sourceProcessing?: MatrxDataTableSourceProcessing;\n /** Source-connected tables append on user scroll by default. */\n scroll?: { thresholdPx?: number; intentTimeoutMs?: number } & (\n | { mode?: \"scroll\" }\n | { mode: \"manual\"; reason: string; approvedBy: string }\n /** Temporary configuration state: automatic append is off, but Load more stays available. */\n | { mode: \"suspended\"; reason: string }\n );\n }\n | {\n /** Local rows, but every query control is owned by the caller (for URL state). */\n mode: \"controlled-local\";\n state: MatrxDataTableQueryState;\n onStateChange: (next: MatrxDataTableQueryState) => void;\n sourceProcessing?: MatrxDataTableSourceProcessing;\n }\n | {\n mode: \"controlled\";\n state: MatrxDataTableQueryState;\n /** Total rows matching the controlled query, not just `data.length`. */\n totalItems: number;\n onStateChange: (next: MatrxDataTableQueryState) => void;\n /** Declare source-owned query behavior before enabling source-aware controls. */\n sourceProcessing?: MatrxDataTableSourceProcessing;\n };\n\n/**\n * Toolbar facets — first-class, Mars-extensible filter controls above the grid.\n * Start with button-group; add radio / switch / complex later without forking.\n */\nexport type ToolbarFacet =\n | {\n type: \"button-group\";\n id: string;\n label?: string;\n value: string;\n /** Reset target for per-facet + global clear. Default: first option value. */\n defaultValue?: string;\n options: Array<{\n value: string;\n label: string;\n icon?: ReactNode;\n }>;\n onChange: (value: string) => void;\n }\n | {\n type: \"custom\";\n id: string;\n render: () => ReactNode;\n /** Declare filter state and its reset together so global Clear includes custom controls. */\n filter?: { active: boolean; onReset: () => void };\n };\n\n/**\n * Cross-column OR search — matches if ANY listed column contains the query.\n * Relationships use case: filter by entity type without picking source vs target.\n */\nexport interface AnyOfColumnSearch {\n columnIds: string[];\n placeholder?: string;\n /** Controlled value. Uncontrolled if omitted. */\n value?: string;\n onChange?: (value: string) => void;\n}\n\nexport type MatrxDataTableAppearance = \"standalone\" | \"embedded\";\n\nexport interface MatrxDataTableToolbar {\n /** Optional human-readable table title, rendered by the shared title row. */\n title?: string;\n /** Source-owned total displayed beside the shared title. */\n titleCount?: { value: number; label: string };\n /** Global search across all accessor values. Default true. */\n search?: boolean;\n /**\n * A caller-owned search control for a deliberately narrower query contract.\n * It occupies the canonical title-row search slot and never changes table\n * query state, so the caller remains honest about its source boundary.\n */\n customSearch?: ReactNode;\n searchPlaceholder?: string;\n searchValue?: string;\n onSearchChange?: (value: string) => void;\n /**\n * Show a compact, visible choice between substring and whole-word search.\n * Omit it when a data source cannot honor both modes server-side.\n */\n searchMatch?: {\n defaultMode?: TableSearchMatchMode;\n };\n /** Optional field-scope controls. Local search always ranks all loaded fields before pagination. */\n intelligentSearch?: {\n roles?: Readonly<Record<string, TableSearchRole>>;\n scopes?: readonly TableSearchScope[];\n };\n /**\n * OR-search across specific columns (e.g. source_type OR target_type).\n * Shown as its own input beside global search when set.\n */\n anyOf?: AnyOfColumnSearch;\n /**\n * Optional compact advanced-filter builder beside the regular search. In\n * controlled mode rules live in `query.state.layeredFilters`; local tables\n * evaluate them against matching column ids.\n */\n layeredFilters?: {\n fields: readonly LayeredFilterField[];\n maxRules?: number;\n label?: string;\n };\n /** Extensible facet strip (button groups, later radios/switches/…). */\n facets?: ToolbarFacet[];\n /** Source controls in a separate wrapping row below the standard toolbar. */\n leading?: ReactNode;\n /**\n * Draw the toolbar row INTO this element — the placing page's own toolbar row — so the page\n * and the table share one row (a saved-view picker, a layout chooser and the table's search\n * side by side) instead of two stacked bars. `null`: the element is not mounted yet, nothing\n * is drawn. Absent: the row is drawn in place, as always.\n */\n portalInto?: HTMLElement | null;\n /**\n * Draw the saved-view tabs INTO this element — the leading edge of the placing page's own\n * toolbar row — while the rest of the table's row (its actions) goes to `portalInto`. The\n * tabs then open the page row on the left and the table's actions close it on the right,\n * instead of both riding one slot. Drawn without the strip's bottom rule (the page row is\n * not the table's top edge). `null`: not mounted yet, the tabs are not drawn. Absent: the\n * tabs stay in the table's own row.\n */\n tabsPortalInto?: HTMLElement | null;\n /** Keep the toolbar on ONE row at every width; a narrow screen scrolls it sideways. */\n singleRow?: boolean;\n /**\n * `false`: no Columns button — the placing page already owns the ONE column picker (a list\n * shell with its own picker that hands the table only the visible columns; two pickers that\n * disagree about which columns exist is worse than one). Default: shown.\n */\n columns?: boolean;\n /**\n * WHERE THE ROW'S CONTROLS GO WHEN IT HAS NO ROOM (lane C, merged-grid review 2026-09-26):\n * `inline` (default) draws them on the row; `menu` puts the table's controls and `actions`\n * behind ONE \"…\" at the row's end and keeps `pinned` beside it; `sheet` (a phone) puts every\n * one of them — `sheetTop`, the controls, `pinned`, `sheetExtras` — in ONE bottom sheet behind\n * a \"Tools\" button. No control is ever dropped.\n */\n overflow?: {\n mode: \"inline\" | \"menu\" | \"sheet\";\n /** Stays on the row in `menu` mode (the primary action, undo); inside the sheet on a phone. */\n pinned?: ReactNode;\n /** Drawn first inside the sheet (the search, so it is there with everything else). */\n sheetTop?: ReactNode;\n /** The host's controls that sit elsewhere on a wide row (the view filter, the layout, share). */\n sheetExtras?: ReactNode;\n /** The sheet button's word. Default \"Tools\". */\n label?: string;\n };\n /**\n * Standard host refresh affordance. The package owns its placement, busy\n * state, accessible tooltip, and glyphs (refresh arrows at rest, spinner\n * only while busy); the host remains the sole data owner.\n */\n refresh?: {\n onRefresh: () => void | Promise<void>;\n label?: string;\n };\n /**\n * Standard create affordance. The host owns authorization and the actual\n * create flow; disabledReason is exposed through the shared tooltip.\n */\n add?: {\n onAdd: () => void | Promise<void>;\n disabled?: boolean;\n disabledReason?: string;\n };\n /** Right-side domain actions, rendered after standard table controls. */\n actions?: ReactNode;\n}\n\n/** Query fields safe to persist in a named table view. */\nexport type TableViewQuery = Omit<MatrxDataTableQueryState, \"page\">;\n\n/** Portable, persistence-neutral table state saved by a host adapter. */\nexport interface TableViewSnapshot {\n __kind: \"matrx-table-view\";\n version: 1;\n /** Page/cursor/selection/loaded rows are deliberately never saved. */\n query: TableViewQuery;\n /** `widths` is sparse so views saved before resize remain valid. */\n columns: { order: string[]; hidden: string[]; widths?: Record<string, number> };\n /**\n * The view's COLORS — color-by-a-column, rules, and manual highlights\n * (`./table-style.ts`). Optional so every view saved before colors existed\n * still parses; absent means \"this view paints nothing\".\n *\n * Colors belong to the VIEW and not to the data, for the same reason the\n * filters do: two people can read one table two ways, and neither reading is\n * a fact about the records. That is also why this snapshot is the only place\n * a style is carried — copy, export and agents read the rows, never this.\n */\n style?: TableStyle;\n}\n\n/** Host port for durable named views. The host owns actor, tenancy and CRUD. */\nexport interface TableSavedViewsProps {\n tableId: string;\n snapshot: TableViewSnapshot;\n defaultSnapshot: TableViewSnapshot;\n onApply: (snapshot: TableViewSnapshot) => void;\n presentation?: \"menu\" | \"tabs\";\n related?: ReactNode;\n}\n\n/** Optional deferred-apply copy for column filter popovers. */\nexport interface MatrxDataTableFilterUi {\n /** Enables the Include/Exclude control. Default false for remote-safe filters. */\n allowNegation?: boolean;\n /** Enables null/not-null text modes. Default false for remote-safe filters. */\n allowNullModes?: boolean;\n applyLabel?: string;\n showCancel?: boolean;\n footerHint?: ReactNode;\n}\n\nexport interface MatrxDataTableCopyConfig<T> {\n /** Explicit registered identities; never inferred from table labels or row shapes. */\n rowReferences?: (row: T) => readonly ContentTransferReference[];\n references?: (visible: T[], all: T[]) => readonly ContentTransferReference[];\n triggerVariant?: \"transparent\" | \"glass\" | \"outline\";\n /** Toast / tooltip label base, e.g. \"Relationship rule\". */\n label: string;\n listLabel?: string;\n location: string;\n rowKind: string;\n listKind: string;\n rowDescription?: string;\n listDescription?: string;\n humanRow: (row: T) => string;\n /**\n * Toolbar-only plain-text representation of the current filtered/sorted\n * view. Omit to retain the canonical table/row summary.\n */\n listHuman?: (visible: T[], all: T[]) => string;\n /**\n * Toolbar-only JSON representation of the current filtered/sorted view.\n * Omit to retain the projected agent rows.\n */\n listJson?: (visible: T[], all: T[]) => unknown;\n /**\n * Toolbar-only AI envelope for the current filtered/sorted view. Omit to\n * retain the canonical list envelope and its query metadata.\n */\n listAgent?: (visible: T[], all: T[]) => AgentPayloadInput;\n /** Project row for agent JSON. Default: full row. */\n agentRow?: (row: T) => unknown;\n rowAttributes?: (\n row: T,\n ) => Record<string, string | number | boolean | null | undefined>;\n listAttributes?: (\n visible: T[],\n all: T[],\n ) => Record<string, string | number | boolean | null | undefined>;\n /**\n * Live view state rendered inside the list payload's <context>. Unlike\n * per-row data, this remains present when the current view has zero rows.\n */\n listContext?: (\n visible: T[],\n all: T[],\n ) => Record<string, string | number | boolean | null | undefined>;\n /**\n * Additional row-scoped AI actions. Use this to fold a domain action such\n * as a paste-ready repair brief into the table-owned row copy menu instead\n * of rendering a separate third control. Builders run at click time.\n */\n rowAiVariants?: (row: T) => AiVariant[];\n /**\n * Graded AI variants for the toolbar's view copy (e.g. \"Top 25\", \"Summary\n * only\"). When set, the toolbar's Copy-for-AI upgrades to a dropdown with\n * these variants + the full-view payload as the automatic \"Everything\"\n * escape hatch. Receives (visible, all) rows at render; builders run at\n * click time.\n */\n aiVariants?: (visible: T[], all: T[]) => AiVariant[];\n /** Custom-preview source (options dialog + live size counts) for the view. */\n aiCustom?: (visible: T[], all: T[]) => AiCustomSource;\n /** Additional domain exports in the same canonical menu; builders run on demand. */\n export?: (visible: T[], all: T[]) => CopyExportConfig;\n /** Show toolbar copy (this view). Default true when copy is set. */\n showToolbar?: boolean;\n /** Show per-row copy. Default true when copy is set. */\n showRow?: boolean;\n}\n\nexport interface MatrxDataTableDetailConfig<T> {\n /** Side-panel title. Default: first string column or \"Details\". */\n title?: (row: T) => ReactNode;\n description?: (row: T) => ReactNode | undefined;\n /** Override the default key/value inspector. */\n render?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /** Header actions inside the side panel. */\n headerActions?: (row: T) => ReactNode;\n defaultWidth?: number;\n enabled?: boolean;\n /**\n * Maps a field name to the entity token its id points at, turning that field\n * into a door (route + peek) in the default inspector — side panel AND row\n * window.\n *\n * There is no default guess: this inspector renders whatever columns the row\n * has, and a wrong door opens a DIFFERENT record (`sch_run.task_id` is a\n * SCHEDULED task, not a workspace `task`). A table whose FKs you HAVE checked\n * can pass `tokenFromColumnName` (`components/official/entity-ref/doors`) to\n * open every `<token>_id` field at once.\n *\n * The ROW is passed too, because a table whose target type varies per row (an\n * audit log, an exposure report) cannot answer from the column name alone.\n */\n tokenForField?: (key: string, row: T) => string | null;\n}\n\n/** Actions a record-owned control can use without reaching into table state. */\nexport interface MatrxDataTableRecordControls {\n /** Close the row's side-panel detail, if open. */\n closeDetail: () => void;\n /** Open the row in the canonical adjustable side panel. */\n openDetail: () => void;\n /** Open the row in its canonical table-owned WindowPanel. */\n openWindow: () => void;\n /** Close the row's table-owned WindowPanel, if open. */\n closeWindow: () => void;\n /** Whether this row currently has visible, unpersisted inline edits. */\n hasPendingEdits: boolean;\n /**\n * Discard this row's pending inline edits.\n *\n * Row actions that persist the already-merged visible row (for example an\n * explicit Confirm action) call this only after that write succeeds. This\n * prevents the floating Save pill from later replaying the same draft as a\n * different, weaker write.\n */\n discardPendingEdits: () => void;\n beginEdit?: () => void;\n saveEdits?: () => void | Promise<void>;\n cancelEdits?: () => void;\n /** Whether this row's optional inline detail is expanded in the table. */\n isExpanded?: boolean;\n /** Toggle this row's optional inline detail. */\n toggleExpanded?: () => void;\n}\n\n/**\n * Controlled inline detail rendered immediately below its owning table row.\n *\n * Use this when detail belongs in the scan path of a dense table. The caller\n * owns which row or rows are expanded and supplies the domain-specific body;\n * the canonical renderer retains sorting, filtering, selection, row actions,\n * and the correct column span.\n */\ninterface MatrxDataTableExpandedDetailSharedConfig<T> {\n canExpand?: (row: T) => boolean;\n render: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n className?: string;\n}\n\n/** The default detail mode: exactly zero or one row may be expanded. */\nexport interface MatrxDataTableSingleExpandedDetailConfig<T>\n extends MatrxDataTableExpandedDetailSharedConfig<T> {\n expandedId: string | null;\n onExpandedIdChange: (id: string | null) => void;\n expandedIds?: never;\n onExpandedIdsChange?: never;\n}\n\n/**\n * Multi-detail mode: every ID in `expandedIds` renders its own inline detail.\n *\n * Pass a new Set to `onExpandedIdsChange`; the table never mutates the\n * caller-owned set.\n */\nexport interface MatrxDataTableMultiExpandedDetailConfig<T>\n extends MatrxDataTableExpandedDetailSharedConfig<T> {\n expandedIds: ReadonlySet<string>;\n onExpandedIdsChange: (ids: Set<string>) => void;\n expandedId?: never;\n onExpandedIdChange?: never;\n}\n\nexport type MatrxDataTableExpandedDetailConfig<T> =\n | MatrxDataTableSingleExpandedDetailConfig<T>\n | MatrxDataTableMultiExpandedDetailConfig<T>;\n\n/**\n * THE FOUR LEVELS of a right-click, as opaque host-owned descriptors.\n *\n * A spreadsheet's right-click menu is four menus, chosen by where the pointer\n * was: the CELL, the ROW it sits in, the COLUMN it sits under, and — when the\n * click lands inside an extended range — the SELECTION. The canonical table\n * used to expose exactly one of them, which is why every surface that wanted\n * the other three forked the table.\n *\n * Each resolver is optional and each returns something the package never\n * interprets; the host builds its menu from it. A level with no resolver here\n * falls back to the host's own `createDefaultMenuContext`, so a table that\n * declares nothing still gets all four through the platform's universal menu.\n */\nexport interface MatrxDataTableContextMenuConfig<T> {\n resolveRowContext?: (row: T, controls: MatrxDataTableRecordControls) => unknown;\n resolveCellContext?: (args: {\n row: T;\n rowId: string;\n columnId: string;\n columnLabel: string;\n value: unknown;\n /** The cell's value as text, for the menu's own Copy. */\n text: string;\n controls: MatrxDataTableRecordControls;\n }) => unknown;\n resolveColumnContext?: (args: {\n columnId: string;\n columnLabel: string;\n /** Every rendered row's value for this column, in visual order. */\n values: unknown[];\n }) => unknown;\n resolveSelectionContext?: (args: {\n rowIds: string[];\n columnIds: string[];\n /** The selected rectangle as an Excel/Sheets-compatible TSV block. */\n tsv: string;\n cellCount: number;\n }) => unknown;\n resolveTableContext?: (args: { rowCount: number; columnIds: string[] }) => unknown;\n /**\n * NEUTRAL SECTIONS for whatever was right-clicked. Asked each time the menu\n * opens, with the level's own payload — the cell's row, column and value; the\n * row; the column's label and values; the selected range's ids and TSV; the\n * table's row count — and answered with plain `MatrxTableMenuSection`s the\n * host draws in its own right-click menu, beside what it already offers.\n * Nothing here names a menu system. Absent, the right-click menu is exactly\n * what it was.\n */\n sections?: (payload: MatrxTableMenuPayload<T>) => MatrxTableMenuSection[];\n}\n\nexport interface MatrxDataTableWindowConfig<T> {\n /** Window title. */\n title?: (row: T) => string;\n /**\n * @deprecated Prefer `renderView` + `renderEdit` so the window stays editable.\n * Full-body override with no View/Edit tabs.\n */\n render?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /** View tab body. Defaults to DataRowInspector. */\n renderView?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;\n /**\n * Edit tab body. When set, the WindowPanel shows View / Edit sidebar tabs\n * (WindowPanel built-in sidebar). Defaults to `detail.render` when present.\n * Pass `false` to keep a view-only window even when `detail.render` exists.\n */\n renderEdit?:\n ((row: T, controls: MatrxDataTableRecordControls) => ReactNode) | false;\n /**\n * Called when the panel icon opens the window — hydrate edit state here\n * without opening the side panel (prefer this over `onRowOpen` for windows).\n */\n onOpen?: (row: T) => void;\n /**\n * Make full-row click open the WindowPanel instead of the side panel.\n * The trailing row action and the window header then expose the side panel\n * as the explicit secondary presentation. Default false.\n */\n openOnRowClick?: boolean;\n /** Which tab to open. Default: `\"edit\"` when an edit body exists. */\n defaultTab?: \"view\" | \"edit\";\n /** Show the panel-icon that opens the window. Default true when detail enabled. */\n enabled?: boolean;\n width?: number;\n height?: number;\n}\n\nexport interface MatrxDataTableEmptyState {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n}\n\n/** Where the read behind a table's rows stands (RC-B12 round 13). */\nexport type MatrxDataTableReadStatus = \"loading\" | \"error\" | \"ready\";\n\n/**\n * The outcome of the read that produced `data`. An `emptyState` is an answer\n * only after a read that SUCCEEDED and returned nothing, so a table that is\n * handed its read's outcome shows:\n *\n * - `\"error\"` and no rows → the failure (the host's `ReadFailure` port: its\n * error view with the host's actions) and a retry — never the empty state;\n * - `\"error\"` WITH rows → the rows, under a stale notice (the host's\n * `StaleNotice` port) — never blanked;\n * - `\"loading\"` and no rows → the loading skeleton;\n * - `\"ready\"` and no rows → `emptyState`.\n *\n * Structurally identical to matrx-frontend's `ReadOutcome`\n * (`components/read-state/ReadGate.tsx`), so a host passes its own value.\n */\nexport interface MatrxDataTableRead {\n status: MatrxDataTableReadStatus;\n /** The failure itself, or just a truthy flag when the read only says it failed. */\n error?: unknown;\n /** Run the read again. Absent, the failure is shown without a retry control. */\n onRetry?: (() => void) | undefined;\n /** What was read, in the reader's words (\"your tasks\"). Default \"these rows\". */\n what?: string | undefined;\n}\n\n/** The rows a table summary is allowed to describe. */\nexport interface MatrxDataTableSummaryContext<T> {\n /** Filtered and sorted rows, always before pagination. */\n rows: readonly T[];\n /** True when any table-owned search, facet, or column filter is active. */\n isFiltered: boolean;\n /** Whether the browser holds all matching rows, loaded rows, or one controlled page. */\n coverage: \"all\" | \"loaded\" | \"page\";\n}\n\nexport interface MatrxDataTableSummaryMetric<T> {\n id: string;\n /** Plain text keeps the KPI's visible label and unavailable announcement identical. */\n label: string;\n value: (context: MatrxDataTableSummaryContext<T>) => ReactNode;\n /** Optional plain-text third line in the large card presentation. */\n description?: string;\n}\n\n/**\n * Optional KPIs and aligned column totals owned by the canonical table.\n *\n * Callback totals run over the filtered/sorted row set before pagination. A\n * source override is for an externally computed aggregate: once declared, the\n * table never substitutes its loaded rows for a missing source value.\n */\nexport interface MatrxDataTableSummaryConfig<T> {\n /** Compact is the default; large uses equal-sized cards with a description line. */\n variant?: \"compact\" | \"large\";\n metrics: MatrxDataTableSummaryMetric<T>[];\n totals?: Record<string, (context: MatrxDataTableSummaryContext<T>) => ReactNode>;\n source?: {\n isFiltered: boolean;\n metrics?: Record<string, ReactNode>;\n totals?: Record<string, ReactNode>;\n };\n}\n\n/** Pending cell edits keyed by row id → partial field map. */\nexport type CellEditsMap = Record<string, Record<string, unknown>>;\n\nexport interface MatrxDataTableEditConfig<T> {\n /** Enable inline editing for columns with `editable` set. */\n enabled?: boolean;\n /**\n * Persist all pending edits. Called when the user clicks Save on the dirty pill.\n * Return resolved when done; throw/reject to keep the draft (with toast).\n */\n onSave: (edits: CellEditsMap, rows: T[]) => void | Promise<void>;\n /** Persist each committed cell immediately. Failed writes remain in the\n * dirty pill so the user can retry or cancel them. */\n autoSave?: boolean;\n /** Optional cancel hook (draft already discarded). */\n onCancel?: () => void;\n /**\n * Judge a value the person MEANT to write, before it enters the draft.\n *\n * Return a sentence to REFUSE it: the editor stays open, the value is not\n * taken, and the sentence is shown IN THE CELL, where the person is standing.\n * Return undefined to accept. Excel's posture, and the reason is the product —\n * \"Amount must be at least 0\" is a thing a person can act on; a silent revert\n * is not.\n *\n * It never judges what is already stored: a value that predates the rule is\n * legal, is never rewritten, and is the point of declaring a rule at all.\n */\n validate?: ((args: { row: T; rowId: string; columnId: string; value: unknown }) => string | undefined) | undefined;\n}\n\nexport interface MatrxDataTableHierarchyConfig<T> {\n /** Complete hierarchy when `data` is only the current controlled page. */\n rows?: T[];\n getParentId: (row: T) => string | null;\n /** Persist the exact structural intent represented by the drop shadow. */\n onMove: (row: T, move: MatrxDataTableHierarchyMove) => void | Promise<void>;\n /** Enables sibling insertion shadows in addition to parent/root drops. */\n manualOrder?: boolean;\n canReparent?: (row: T) => boolean;\n itemLabel?: (row: T) => string;\n rootDropLabel?: string;\n}\n\nexport interface MatrxDataTableHierarchyMove {\n parentId: string | null;\n /**\n * Insert immediately before this sibling. Null means first child for\n * \"inside\"/\"root\", and LAST sibling for \"after\" (target had no successor).\n */\n beforeId: string | null;\n position: \"before\" | \"after\" | \"inside\" | \"root\";\n targetId: string | null;\n}\n\n/**\n * Multi-row selection — a leading checkbox column plus a bulk bar that appears\n * only while rows are checked.\n *\n * OPT-IN and fully CONTROLLED: the consumer owns the id set, so selection\n * survives (or is deliberately cleared by) a re-fetch, a filter change, or an\n * optimistic list update — the table never holds hidden selection state that\n * can disagree with the surface around it.\n *\n * Why this is a primitive and not a per-surface checkbox column: a register\n * the user cannot clear in bulk is a register they stop reading, and the fifth\n * hand-rolled selection column — each with its own shift-click, its own\n * select-all semantics, its own bar — is exactly the fork `components/official/`\n * exists to prevent.\n *\n * Selection does not alter mobile horizontal scrolling. The complete row moves\n * as one surface below `sm`; no leading column is allowed to pin over the data.\n */\nexport interface MatrxDataTableSelectionConfig<T> {\n /**\n * Selected entity ids. Defaults to `getRowId(row)`, so ids not on the\n * current page are kept exactly as before.\n */\n selectedIds: string[];\n onSelectedIdsChange: (ids: string[]) => void;\n /**\n * The underlying entity identity for selection when one entity is rendered\n * more than once. `getRowId` still identifies the individual rendered\n * occurrence; this makes every occurrence of the same entity share one\n * checkbox state and one bulk-action row.\n */\n getSelectionId?: (row: T) => string;\n /**\n * The bulk bar's actions, given the currently-selected rows THAT ARE LOADED.\n * Selection can outlive a page change, so also take `selectedIds` when an\n * action only needs ids.\n */\n actions?: (selected: T[], selectedIds: string[]) => ReactNode;\n /**\n * Rows that cannot be acted on in bulk render NO checkbox at all — the cell\n * stays for alignment, the control is absent from the DOM. A greyed control\n * still advertises a choice the surface underneath would refuse.\n */\n isRowSelectable?: (row: T) => boolean;\n /** Singular noun for the bar's count (\"finding\" → \"3 findings selected\"). */\n noun?: string;\n}\n\n/** Table-owned state and actions exposed to an opt-in phone card renderer. */\nexport interface MatrxDataTableMobileCardControls {\n /** Render a declared column with the same editable/draft/link behavior as the grid. */\n renderCell: (columnId: string) => ReactNode;\n /** Whether this row is in the canonical selection set. */\n selected: boolean;\n /** Whether the consumer allows this row to be selected. */\n selectable: boolean;\n /** Update selection through the table's controlled/URL-backed contract. */\n onSelectedChange: (selected: boolean) => void;\n /**\n * The table's canonical per-row copy controls plus consumer row actions.\n * Render this instead of rebuilding either action path inside the card.\n */\n actions: ReactNode;\n /** Optional inline-detail state for card renderers. */\n isExpanded?: boolean;\n /** Toggle this row's optional inline detail. */\n toggleExpanded?: () => void;\n /** Optional inline detail body; cards choose its placement. */\n expandedDetail: ReactNode;\n}\n\nexport type MatrxDataTableDensity = \"condensed\" | \"normal\" | \"spacious\";\n\n/**\n * What the table needs in order to paint a `TableStyle` itself.\n *\n * `fieldOf` exists because a style names DATA fields (\"tint the row when\n * `status` is Blocked\") while the table addresses COLUMNS. In the common case\n * the column id IS the field name and the default is right; a table whose\n * column ids are decorated (`records-grid-notes`) maps them here.\n */\nexport interface MatrxDataTableStyleConfig<T> {\n /** The style document. */\n style: TableStyle;\n /** The row's data as the rules read it. Defaults to the row itself. */\n documentOf?: (row: T) => Record<string, unknown>;\n /** Column id to the field name a rule would name. Defaults to identity. */\n fieldOf?: (columnId: string) => string;\n /**\n * The color a choice column's option paints with, for color-by. The consumer\n * supplies it because option colors live on the resolved option list, which\n * may come from a shared pick list this package cannot reach.\n */\n choiceColorFor?: ChoiceColorLookup;\n}\n\n/**\n * GROUPING — \"group these rows by Status\" with collapsible sections, a count\n * and a subtotal on each.\n *\n * This is the primitive's answer to the biggest gap against Airtable, Google\n * Sheets and Notion. It is opt-in, and it is CONTROLLED for the two pieces of\n * state a person expects to survive: which column groups, and which sections\n * are shut. Both are ordinary values, so a saved view carries them like any\n * other reading of the table.\n *\n * Grouping runs AFTER local processing and over the rows the table is about to\n * render. It never re-sorts inside a group: the sort the person chose is the\n * order they see.\n */\nexport interface MatrxDataTableGroupingConfig<T> {\n /** Column id to group by. `null` means grouping is available but off. */\n columnId: string | null;\n /** Emitted when the person picks a different column (or clears it). */\n onColumnIdChange?: ((columnId: string | null) => void) | undefined;\n /**\n * Columns offered in the group-by control. Defaults to every visible column\n * that is not an actions column. A column with thousands of distinct values\n * is a bad grouping and the consumer is the only one who knows that.\n */\n groupableColumnIds?: string[] | undefined;\n /** Per-column subtotal shown on every group header, keyed by column id. */\n aggregates?: Record<string, GroupAggregateSpec> | undefined;\n /** Controlled collapsed group keys (`TableGroup.key`). */\n collapsed?: string[] | undefined;\n onCollapsedChange?: ((collapsed: string[]) => void) | undefined;\n /** Section order. Default `\"value-asc\"`; blanks always sort last. */\n order?: GroupOrder | undefined;\n /** Label for the section of rows whose grouping value is blank. */\n emptyLabel?: string | undefined;\n /** Singular noun for a group's summary (\"job\" → \"12 jobs\"). */\n rowNoun?: string | undefined;\n /**\n * Replaces the group's plain-text label while retaining the canonical\n * collapse control, count, and aggregate cells. Use when a group identity is\n * an addressable platform entity and must keep its Open/new-tab/Peek doors.\n */\n renderLabel?: ((group: TableGroup<T>) => ReactNode) | undefined;\n /**\n * WHAT THE WHOLE SOURCE SAYS ABOUT A GROUP, when the table holds only a page of it (grids review\n * 3). A paged table's group holds the rows on THIS page; the host that can ask its store for the\n * group's real count and subtotals answers here, and the header shows those — the page's own\n * count and subtotals are the fallback, never mixed with them. `count` null/absent = the page's;\n * an aggregate absent for a column = the page's for that column.\n */\n groupFacts?: ((group: TableGroup<T>) => { count?: number | null; aggregates?: Record<string, ReactNode> } | undefined) | undefined;\n /**\n * Reads the grouping value for a column. Defaults to the column's own\n * accessor.\n *\n * NOT named `valueOf`: every object in JavaScript already has one, so an\n * optional config field by that name is always \"present\" and a `config.valueOf\n * ? … : …` check silently calls `Object.prototype.valueOf` and groups every\n * row under the config object itself. It did exactly that here once.\n */\n readCell?: ((row: T, columnId: string) => unknown) | undefined;\n}\n\n/**\n * DRILL — group by a Dimension with SERVER-computed groups, pivot a second Dimension across the\n * top, pick the Measures shown, and drill on a group (lane D3; `./drill.ts` is the whole model).\n *\n * CONTROLLED like `grouping`: the question is the consumer's (it lives in the address and saves\n * as a Saved view), the table only asks for it to change. THE TABLE ASKS, THE HOST ANSWERS:\n * `drillRequests(question)` names each grouping the screen needs and the host puts each door's\n * rows into `answers` under the request's key. While `question.by` is empty the table lists its\n * records as always (narrowed by the host to `question.where`), and the trail stays in the\n * toolbar so a person can zoom back out.\n *\n * With `drill` the toolbar's group-by control groups by Dimensions on the server; the\n * page-scoped `grouping` is not drawn at the same time.\n */\nexport interface MatrxDataTableDrillConfig {\n dimensions: readonly MatrxDrillDimension[];\n measures: readonly MatrxDrillMeasure[];\n /**\n * Declared drill paths (levels, outermost first — `created_at:year`, `created_at:quarter`, …):\n * a click on a group follows the path its level sits on before any other Dimension.\n */\n paths?: readonly (readonly string[])[] | undefined;\n /**\n * The time Dimension a window runs along (its key). Given, the toolbar offers the window\n * presets and, once windowed, the comparison with the prior window.\n */\n windowDimension?: string | undefined;\n question: MatrxDrillQuestion;\n onQuestionChange: (next: MatrxDrillQuestion) => void;\n answers: MatrxDrillAnswers;\n /** A read that failed, in words the person can act on. */\n error?: string | null | undefined;\n /** The trail's first crumb. Default \"All records\". */\n rootLabel?: string | undefined;\n /** Singular noun (\"record\" → \"12 records\"). */\n rowNoun?: string | undefined;\n /** How a blank group reads. Default \"No value\". */\n emptyLabel?: string | undefined;\n}\n\n/**\n * VIRTUALIZATION — render the rows a person can actually see.\n *\n * Off by default, because most tables in the product are short and a windowed\n * body costs a scroll container contract that a short table does not need.\n * On, it engages only past `threshold` rows, so a 40-row table behaves exactly\n * as it always did and a 100,000-row table scrolls.\n */\nexport interface MatrxDataTableVirtualizeConfig {\n enabled?: boolean | undefined;\n /** Row height in px used for the window math. Default follows density. */\n rowHeight?: number | undefined;\n /** Rows rendered beyond each edge of the viewport. Default 8. */\n overscan?: number | undefined;\n /** Row count below which the body renders whole. Default 150. */\n threshold?: number | undefined;\n /**\n * Window the COLUMNS too, for tables hundreds of columns wide. Frozen\n * columns are always rendered, never windowed out.\n */\n columns?: boolean | undefined;\n /** Column width in px used for the horizontal window. Default 160. */\n columnWidth?: number | undefined;\n}\n\n/**\n * THE HONEST COUNT.\n *\n * A table that filters over part of its rows and says nothing is a screen that\n * lies. Declare what the answer actually covers and the table states it, in\n * plain English, above the rows — and never presents a partial count as exact.\n *\n * Omit this entirely and the table says nothing, which is correct for a table\n * whose rows are all in hand.\n */\nexport interface MatrxDataTableCoverageConfig {\n /**\n * Number of rows loaded from the source before the host narrows `data` with\n * a domain filter. Defaults to `data.length`.\n */\n loaded?: number | undefined;\n /** Rows the source says match the current query, when it can say. */\n matched?: number | undefined;\n /** Rows in the table irrespective of the query, when known. */\n total?: number | undefined;\n /** A ceiling the source imposed on this answer (a fetch cap). */\n cap?: number | undefined;\n /** Who answered the filters and sort. Default `\"client\"`. */\n answeredBy?: \"source\" | \"client\" | undefined;\n /** Singular noun for the sentence. Default \"row\". */\n noun?: string | undefined;\n}\n\n/**\n * SPREADSHEET GESTURES — range selection, Excel paste, fill, undo.\n *\n * The `/data` grid had all of this and nothing else in the product could reach\n * it. Opt-in, because a reference table with one editable column does not want\n * a cell cursor; a table a person works IN does.\n *\n * The table owns the gestures and the selection rectangle; the CONSUMER owns\n * the writes. Every mutation arrives as a list of `CellPatch`, so the caller\n * applies it through the door it already has and the table never invents a\n * persistence path of its own. That is also what makes undo honest: the stack\n * holds before-values, and an undo is just another patch list.\n */\nexport interface MatrxDataTableSpreadsheetConfig<T> {\n enabled?: boolean | undefined;\n /**\n * Apply cell writes. Called for a paste, a fill-down, a fill-handle drag, a\n * clear, an undo and a redo. The table re-reads from `data`, so a write the\n * caller refuses simply never appears.\n */\n onPatch?: ((patches: CellPatch[], intent: SpreadsheetIntent) => void | Promise<void>) | undefined;\n /**\n * RANGE SELECTION AND COPY FOR A READER — a flag of its own, separate from\n * editing. A person who may not change a table may still drag out a range,\n * move it with the arrows, select all and press Cmd-C (or right-click the\n * range) to take a real Excel/Sheets block out of it. With `readOnly` the\n * grid offers every selection gesture and no write: no fill handle, no\n * editor on Enter or typing, and cut, paste, clear, fill-down, undo and redo\n * each say `readOnlyReason` rather than doing anything. `onPatch` is not\n * needed. Default false.\n */\n readOnly?: boolean | undefined;\n /** What a refused write says on a read-only grid. Defaults to a plain view-only sentence. */\n readOnlyReason?: string | undefined;\n /** Column ids a patch may target. Defaults to every column marked `editable`. */\n writableColumnIds?: string[] | undefined;\n /**\n * TYPE-TO-EDIT FOR A CONSUMER THAT DRAWS ITS OWN CELL EDITORS. The table opens its own editor\n * for a column marked `editable`; a consumer whose cells carry their own editor (a record grid\n * whose cell IS the Field's editor) is asked here instead, with the cell the person is on and\n * what they typed — `seed` is the text (a letter, an IME composition, an Option/AltGr\n * character, dictation), `null` for Enter/F2. Return true when an editor opened, so the\n * keystroke is consumed. Never asked on a `readOnly` grid.\n */\n onEditRequest?: ((address: { rowId: string; columnId: string }, seed: string | null) => boolean) | undefined;\n /**\n * A CELL WHOSE OWN GESTURE IS SPACE (a tick box). Answer true and Space on that cell is sent to\n * `onEditRequest` (seed `null`) instead of expanding the row (Sheets and Airtable toggle a tick\n * box on Space). A consumer's open editor marks itself `data-matrx-cell-editor` so every key\n * inside it is the editor's (`CELL_EDITOR_ATTR`).\n */\n cellTakesSpace?: ((address: { rowId: string; columnId: string }) => boolean) | undefined;\n /** Why a column refuses a write, as a sentence a person can act on. */\n columnRefusal?: ((columnId: string) => string | undefined) | undefined;\n /** Reads a cell for copy and fill. Defaults to the column's own accessor. */\n readCell?: ((row: T, columnId: string) => unknown) | undefined;\n /** The little square at the corner of a selection. Default true. */\n fillHandle?: boolean | undefined;\n /** Paste real Excel/Sheets TSV into the selection. Default true. */\n paste?: boolean | undefined;\n /** Undo depth. 0 turns undo off. Default 100. */\n undoDepth?: number | undefined;\n /** Says what just happened, and why something did not. */\n notify?: ((message: string) => void) | undefined;\n /**\n * Judge ONE value before it is written, for a paste, a fill or a clear.\n *\n * Return a sentence to REFUSE it — the cell is left alone and the sentence is\n * what the person is told. Return undefined to accept. This is the same rule\n * the inline editor enforces (`edit.validate`), asked in the one other place\n * a value can enter a cell, because a rule a paste can walk around is not a\n * rule.\n */\n validateCell?: ((args: { rowId: string; columnId: string; value: unknown }) => string | undefined) | undefined;\n /**\n * ASK BEFORE A PASTE IS WRITTEN, AND HAND OVER THE ROWS THAT DID NOT FIT.\n *\n * Absent (the default, and byte-for-byte what a paste did before): the table\n * screens the block and applies it straight away, which is right for a paste\n * of a handful of cells into rows that already exist.\n *\n * Present: the table computes the whole plan and applies NOTHING. The\n * consumer is handed what would be written, what was refused and why, and the\n * rows that landed past the last row — and calls `apply()` itself if the\n * person says yes.\n *\n * WHY THE OVERFLOW IS HANDED OVER AT ALL. A paste of fifty spreadsheet rows\n * into a table that holds three is the ordinary way a person fills a new\n * table, and until 2026-09-21 forty-seven of those rows were counted and\n * dropped (\"landed past the edge of the table and were ignored\"). The table\n * cannot create rows — it does not own the writes — but it must not be the\n * reason nobody can. So it says exactly which rows they were, in the grid's\n * own column order, and a consumer that knows how to make a row makes them.\n * A consumer that does not simply leaves `overflow` alone and the sentence is\n * the same as before.\n */\n onPastePlan?: ((plan: SpreadsheetPastePlan) => void) | undefined;\n /**\n * WHERE THE PERSON IS STANDING, told as it moves (records-ui merge tranche 6l, inventory H1).\n *\n * An agent beside the grid is asked about \"this cell\" and \"these cells\"; only the table knows\n * which cell the cursor is on and which block is selected. Called after every change of the\n * cell cursor or the selected rectangle — `null` when nothing is selected. `tsv` is the block as\n * Excel / Sheets would copy it (no header row), `cellCount` how many cells it spans.\n */\n onSelectionChange?: ((selection: SpreadsheetSelection | null) => void) | undefined;\n /**\n * THE GRID'S HANDS, for a consumer whose cells carry their own editors (merged-grid review\n * 2026-09-26, A1). The table writes its `SpreadsheetController` here: `endEdit(exit, from)` when\n * such an editor closes gives the grid the keyboard back with the cursor moved the way Sheets\n * moves it (Enter down, Tab right, Escape stays); `select` puts the cursor on a cell. The\n * table's own editors need nothing — the grid's key net covers them.\n */\n controllerRef?: { current: SpreadsheetController | null } | undefined;\n}\n\n/** The cell cursor and the selected rectangle, by id (`spreadsheet.onSelectionChange`). */\nexport interface SpreadsheetSelection {\n /** The cell the cursor is on (the moving end of the range). */\n active: { rowId: string; columnId: string };\n /** The rows and columns the rectangle covers, in the grid's visual order. */\n rowIds: string[];\n columnIds: string[];\n cellCount: number;\n /** The rectangle's values as a TSV block, the way a copy would carry them. */\n tsv: string;\n}\n\n\n/**\n * THE VALUES A COLUMN HOLDS, with a count beside each one.\n *\n * A filter that only offers \"Contains…\" asks the reader to already know what is\n * in the column. The checklist tells them, and the counts are what make it\n * usable: \"Blocked 12\" is a decision, \"Blocked\" is a guess.\n *\n * THE HONESTY RULE: counts are computed in the browser ONLY when the browser\n * holds every row they describe. Otherwise `source` is asked — the database\n * counts over the whole column — and if there is no source, the popover SAYS\n * the counts cover only the loaded rows. A partial count is never printed as if\n * it were the whole truth.\n */\nexport interface MatrxDataTableFacetsConfig<T> {\n enabled?: boolean | undefined;\n /**\n * Database-computed values and counts for one column. Called only when the\n * rows in the browser do not cover `totalRows` — local-data-first is the same\n * rule for every source. Return null (or throw) and the popover says so and\n * falls back to matching text.\n */\n source?: ((args: { columnId: string; search: string; limit: number }) => Promise<ColumnFacets | null>) | undefined;\n /**\n * Rows the whole table holds after the active search. This is how the table\n * knows whether the rows it has are all of them. Omit it and the table treats\n * its own rows as a partial answer and says so.\n */\n totalRows?: number | undefined;\n /** Most values to list. Default 200, hard ceiling 500. */\n limit?: number | undefined;\n /** Reads a cell for counting. Defaults to the column's own filter accessor. */\n readCell?: ((row: T, columnId: string) => unknown) | undefined;\n}\n\nexport interface MatrxDataTableProps<T> {\n data: T[];\n columns: MatrxColumnDef<T>[];\n /** Table-local visual density. An explicit value wins over the host default. */\n density?: MatrxDataTableDensity;\n /**\n * `standalone` (default): the table draws its own frame, grey uppercase header and pager.\n * `embedded`: the table sits inside a surface that already frames it (a `MatrxTableCard`):\n * no frame, sentence-case header on the surface's own background, no zebra, and a pager only\n * when there is something to page. Inside a `MatrxTableCard` this is the default.\n */\n appearance?: MatrxDataTableAppearance;\n /** Stable persistence identity for host-installed named views. */\n tableId?: string;\n /**\n * THE REGISTRY TOKEN OF THE ROWS (lane 7 STANDARD-TABLES W5). When set, the host's\n * `useCustomFieldColumns` port adds the custom-field columns of that token, for the organizations\n * the rows belong to (each row's `organization_id`), hidden until picked in Columns. Rows carry\n * their values in `custom_fields`. No per-list code: every table of registry rows inherits them.\n */\n rowToken?: string;\n /** Declared reset state for the host Saved Views control. */\n defaultViewSnapshot?: TableViewSnapshot;\n /** Working view tabs, on by default. Disable for embedded/single-purpose tables. */\n viewTabs?: boolean;\n /**\n * Where the working view tabs are kept. Absent: in memory, gone on reload. A page people\n * return to passes its preferences store, so a view made with \"+\" is named and survives.\n */\n viewTabsStore?: TableViewTabsStore;\n /** Related-data controls shown at the right of the working-view tab strip. */\n relatedTableActions?: ReactNode;\n /** Optional URL/source-owned column order and show/hide state. */\n columnState?: {\n order: string[];\n hidden: string[];\n onChange: (next: { order: string[]; hidden: string[] }) => void;\n /** Optional source-owned desktop widths keyed by canonical column id. */\n widths?: Record<string, number>;\n /** Required to persist controlled width changes across remounts. */\n onWidthsChange?: (widths: Record<string, number>) => void;\n };\n /** Enables package-owned drag reordering in table headers. Default true. */\n reorderableColumns?: boolean;\n /** Enables desktop column resizing. Default true; handles are absent below `sm`. */\n resizableColumns?: boolean;\n columnManager?: { open: boolean; onOpenChange: (open: boolean) => void };\n getRowId: (row: T) => string;\n /**\n * Additional row identity included in local global search without becoming\n * a visible/sortable/filterable column. Use for canonical composite keys or\n * aliases whose displayed parts live in separate columns.\n *\n * Ignored in remote controlled mode, where the query owner applies search.\n */\n searchText?: (row: T) => string;\n /**\n * Hierarchy-aware local processing seam. The canonical table still owns the\n * toolbar, URL state, headers, pagination, editing, copy, and rendering; the\n * consumer only preserves domain ordering that a flat sort would destroy.\n *\n * The processor must honor every active query control in `state` and return\n * the complete filtered/sorted local result before pagination. It is ignored\n * in remote controlled mode, where `data` is already the queried page.\n */\n processLocalRows?: (rows: T[], state: MatrxDataTableQueryState) => T[];\n /** Initial local sort when neither URL state nor a controlled query owns it. */\n defaultSort?: SortState | null;\n isLoading?: boolean;\n /**\n * Background refresh state. Unlike `isLoading`, this preserves rendered rows\n * and shows only the table's non-blocking refresh indicator.\n */\n isFetching?: boolean;\n /**\n * Controlled query state for direct remote data sources. The component never\n * fetches data itself; it only emits state changes to the caller.\n */\n query?: MatrxDataTableQueryControl<T>;\n /**\n * Persist local query and record-view state in namespaced URL parameters.\n * This is intentionally opt-in and cannot be combined with controlled query\n * mode; remote tables use `useTableUrlState({ tableId })` in their query owner.\n */\n urlState?: MatrxDataTableUrlStateConfig;\n\n toolbar?: MatrxDataTableToolbar;\n /** Hide the package toolbar while retaining table-owned query behavior. */\n hideToolbar?: boolean;\n /** Hide numbered pagination; local rows remain available to the caller. */\n hidePagination?: boolean;\n /** Host wording for a controlled source range when visible rows are a local projection. */\n paginationLabelFormat?: (start: number, end: number, total: number) => string;\n /** Optional deferred Apply/Cancel copy for column filter popovers. */\n filterUi?: MatrxDataTableFilterUi;\n /** Row click opens the side panel unless `window.openOnRowClick` is true. */\n detail?: MatrxDataTableDetailConfig<T>;\n /** Controlled detail rendered directly below a table row. */\n expandedDetail?: MatrxDataTableExpandedDetailConfig<T>;\n /** Panel icon opens a WindowPanel (page-local; supports ReactNode override). */\n window?: MatrxDataTableWindowConfig<T>;\n /** Copy + Copy for AI (rows + this view). Omit for the declared-column default; false suppresses it. */\n copy?: false | MatrxDataTableCopyConfig<T>;\n /** Independently hide table or row copy without replacing canonical payloads. */\n copyControls?: { table?: boolean; row?: boolean };\n /** Where canonical per-row Alchemy appears. Inline preserves current behavior. */\n rowCopyPlacement?: \"inline\" | \"menu\";\n /** One pane-level right-click resolver; host owns the opaque descriptor. */\n contextMenu?: MatrxDataTableContextMenuConfig<T>;\n /** Inline edit session with floating Save/Cancel pill. */\n edit?: MatrxDataTableEditConfig<T>;\n /** Opt-in tree reparenting owned by the canonical row renderer. */\n hierarchy?: MatrxDataTableHierarchyConfig<T>;\n\n /** Controlled selection (selected row id for highlight). */\n selectedId?: string | null | undefined;\n onSelectedIdChange?: ((id: string | null) => void) | undefined;\n\n /** Controlled table-owned window row (normally supplied by URL state). */\n windowRowId?: string | null | undefined;\n onWindowRowIdChange?: ((id: string | null) => void) | undefined;\n\n /**\n * Controlled multi-row selection and domain bulk actions. When omitted,\n * the table owns checkbox selection and Alchemy copy for loaded rows.\n */\n selection?: false | MatrxDataTableSelectionConfig<T>;\n\n /** Icon-only actions in the built-in Actions column. Labels are accessible names/tooltips, never button text. */\n rowActions?: (row: T, controls: MatrxDataTableRecordControls) => readonly MatrxTableIconAction[];\n /** Extra rows rendered in the table footer with the same columns as data rows. */\n footerRows?: T[];\n /** Optional KPIs above the toolbar and aligned totals in the canonical footer. */\n /** Controlled list filters. Hosts reset source paging/selection in callbacks. */\n scopeBar?: TableScopeBarProps;\n summary?: MatrxDataTableSummaryConfig<T>;\n /** Per-row presentation class, after canonical selection/highlight state. */\n rowClassName?: (row: T, index: number) => string | undefined;\n /**\n * WHAT ELSE CHANGES HOW THIS ROW DRAWS, besides the record itself (lane RENDER-AUDIT, 2026-09-26).\n *\n * Body rows are memoised: a row is drawn again only when its record changed identity, its own\n * table facts changed (ticked, open, highlighted, its pending edits, spreadsheet flags), or the\n * table's shape changed. A host whose `cell` renderers read state OUTSIDE the record — a cell\n * editor it owns, an optimistic value, a refusal beside one cell — answers here with a value\n * that changes whenever that state changes for THIS row (compared with `Object.is`; an array is\n * compared item by item). Keep the function itself stable; it is called for every row on every\n * render and must be cheap.\n */\n rowVersion?: (row: T) => unknown;\n /**\n * Per-CELL presentation class, after the column's own static `className`.\n *\n * The primitive had `rowClassName` and a static per-column `className`, and\n * nothing in between — so every table that needed to say something about ONE\n * cell had to grow a `cell` render function and paint inside it, where the\n * table can no longer see it. This is the missing rung, and `tableStyle`\n * below is the declarative thing built on top of it.\n */\n cellClassName?: (\n row: T,\n columnId: string,\n index: number,\n ) => string | undefined;\n /**\n * COLORS, declaratively — color by a column, rules, manual highlights.\n *\n * The model is `./table-style.ts`: one document, evaluated live on every\n * render so a tint is never stale, with a documented precedence between the\n * three ways a cell can earn a color. Pass the document and the table paints\n * itself; the consumer owns only where that document is STORED and how a\n * person edits it.\n */\n tableStyle?: MatrxDataTableStyleConfig<T>;\n /**\n * GROUPS — collapsible sections by a column, with a count and per-column\n * subtotals on each header. Opt-in and controlled; see the config's own doc.\n */\n grouping?: MatrxDataTableGroupingConfig<T>;\n /**\n * DRILL — server-computed groups, a pivot, a measure picker and drill-on-a-group, with the\n * breadcrumb in the toolbar. Controlled; see the config's own doc.\n */\n drill?: MatrxDataTableDrillConfig | undefined;\n /** Render only the rows in view. Opt-in; engages past its own threshold. */\n virtualize?: boolean | MatrxDataTableVirtualizeConfig;\n /**\n * What the current answer actually covers. Declare it and the table says so\n * on screen when a filter is answering over part of the table, instead of\n * presenting a partial count as the whole truth.\n */\n coverage?: MatrxDataTableCoverageConfig;\n /** Range selection, Excel paste, fill handle, fill-down, undo. Opt-in. */\n spreadsheet?: MatrxDataTableSpreadsheetConfig<T>;\n /** Real values with their counts in every column filter popover. */\n facets?: MatrxDataTableFacetsConfig<T>;\n /**\n * Explicit body row height in px. Overrides the density default. Use it when\n * a saved view remembers a row height the person chose.\n */\n rowHeight?: number;\n /**\n * FIT TO THE WIDTH (from `sm` up): every data column takes its share of the space in proportion\n * to the width it would otherwise have, so the table is exactly as wide as its container and\n * never scrolls sideways — Airtable's \"fit\". Off, columns keep their widths and the table\n * scrolls. A phone always scrolls: a fitted phone table is columns too narrow to read.\n */\n fitToWidth?: boolean | undefined;\n /** Highlight an index in the currently rendered page without selecting it. */\n highlightedIndex?: number | undefined;\n /** Receives the complete visible local view after local processing, before paging. Attaching after absence receives one snapshot; callback identity changes alone do not notify. */\n onViewChange?: (rows: T[]) => void;\n /**\n * Wrap the whole `<tr>`. Return `children` unchanged for no-op.\n *\n * `rowActions` only reaches the actions CELL, so anything that must own the\n * entire row — a right-click menu, a drag handle, a drop target — had no\n * seam and would have forced a surface to fork the table. THE INVENTORY LAW:\n * the fork is the defect, so the seam exists instead.\n *\n * Whatever you return must render `children` as a direct `<tbody>` child, so\n * the wrapper has to be a component that emits the `<tr>` unchanged\n * (`ItemContextMenu` does — it renders a Radix trigger with `asChild`).\n */\n rowWrapper?: (row: T, children: ReactNode) => ReactNode;\n\n emptyState?: MatrxDataTableEmptyState;\n /**\n * The outcome of the read behind `data` — see `MatrxDataTableRead`. Every\n * table fed by a read passes it; `emptyState` then renders only after a read\n * that succeeded. matrx-frontend's lint rule `matrx/empty-state-needs-read-gate`\n * refuses an `emptyState` without it on read-backed rows.\n */\n read?: MatrxDataTableRead | undefined;\n /** Default 25. Pass 0 to show all. */\n pageSize?: number;\n /** Exact numbered pages are the default. Progressive reveal is an explicit\n * table-specific exception and must be reviewed before adoption. */\n localPagination?:\n | { mode: \"progressive\" }\n | { mode: \"numbered\"; reason: string; approvedBy: string };\n pageSizeOptions?: number[];\n zebra?: boolean;\n /**\n * Keep the one semantic table header visible while its nearest vertical\n * scroll surface moves. Defaults to true. Set false only where surrounding\n * chrome already supplies the column labels.\n */\n stickyHeader?: boolean;\n className?: string;\n tableClassName?: string;\n /**\n * `fill` (default): the bordered frame takes the full height it is given. `content`: the frame\n * ends at its last row and scrolls only once the rows outgrow that height — a list of three\n * rows draws no tall empty box.\n */\n frameHeight?: \"fill\" | \"content\";\n /**\n * `show` (default): the column header is always drawn. `hide`: a settled result with no rows\n * draws no column header (chrome over rows that do not exist) — unless a column filter is\n * set, because the header is where that filter is cleared.\n */\n emptyHeader?: \"show\" | \"hide\";\n /**\n * Optional compact row presentation rendered below `mobileCardsBreakpoint`\n * in place of the horizontal table. The caller supplies the record summary\n * because only the product surface knows which values and actions are\n * essential at narrow widths; MatrxDataTable still owns query state,\n * loading/empty states, and pagination.\n *\n * `controls.actions` carries the table-owned copy controls and consumer row\n * actions, so a card does not fork them. Prefer the default horizontal table\n * unless the product explicitly requires every essential value/action to be\n * discoverable without horizontal scrolling.\n */\n mobileCards?: (\n row: T,\n index: number,\n controls: MatrxDataTableMobileCardControls,\n ) => ReactNode;\n /** Card/table handoff. `sm` is phone-only; `lg` includes portrait tablets. */\n mobileCardsBreakpoint?: \"sm\" | \"lg\";\n /**\n * Resolves a record's canonical destination for modified row gestures.\n * Cmd/Ctrl/Shift-click and middle-click open this URL in a new browsing\n * context; an unresolved URL deliberately does not fall back to row open.\n * Plain row clicks retain the table's detail/window/onRowOpen behavior.\n */\n getRowHref?: (row: T) => string | undefined;\n /** Called after a row is selected for detail (in addition to opening the panel). */\n onRowOpen?: (row: T) => void;\n}\n\n/** Re-export for callers building custom agent payloads. */\nexport type { AgentPayloadInput };\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AnyOfColumnSearch, n as CellEditType, o as CellEditsMap, f as ColumnFilterKind, a as ColumnFilterValue, C as ColumnFiltersState, m as ColumnMarker, M as MatrxColumnDef, j as MatrxDataTableAppearance, p as MatrxDataTableContextMenuConfig, g as MatrxDataTableCopyConfig, q as MatrxDataTableCoverageConfig, c as MatrxDataTableDensity, r as MatrxDataTableDetailConfig, s as MatrxDataTableDrillConfig, t as MatrxDataTableEditConfig, u as MatrxDataTableEmptyState, v as MatrxDataTableExpandedDetailConfig, w as MatrxDataTableFacetsConfig, x as MatrxDataTableFilterUi, y as MatrxDataTableGroupingConfig, z as MatrxDataTableHierarchyConfig, B as MatrxDataTableHierarchyMove, D as MatrxDataTableMobileCardControls, E as MatrxDataTableMultiExpandedDetailConfig, i as MatrxDataTableProps, F as MatrxDataTableQueryControl, e as MatrxDataTableQueryState, l as MatrxDataTableRead, G as MatrxDataTableReadStatus, d as MatrxDataTableRecordControls, H as MatrxDataTableSelectionConfig, I as MatrxDataTableSingleExpandedDetailConfig, J as MatrxDataTableSourceProcessing, K as MatrxDataTableSpreadsheetConfig, N as MatrxDataTableStyleConfig, O as MatrxDataTableSummaryConfig, P as MatrxDataTableSummaryContext, Q as MatrxDataTableSummaryMetric, R as MatrxDataTableToolbar, U as MatrxDataTableUrlStateConfig, V as MatrxDataTableVirtualizeConfig, W as MatrxDataTableWindowConfig, h as MatrxTableIconAction, X as MatrxTablePagination, Y as NumberFilterOp, _ as SortDirection, S as SortState, $ as SpreadsheetSelection, b as TableSavedViewsProps, T as TableSearchMatchMode, a8 as TableViewQuery, k as TableViewSnapshot, ad as TextFilterMode, ae as ToolbarFacet } from '../layered-filters-
|
|
1
|
+
export { A as AnyOfColumnSearch, n as CellEditType, o as CellEditsMap, f as ColumnFilterKind, a as ColumnFilterValue, C as ColumnFiltersState, m as ColumnMarker, M as MatrxColumnDef, j as MatrxDataTableAppearance, p as MatrxDataTableContextMenuConfig, g as MatrxDataTableCopyConfig, q as MatrxDataTableCoverageConfig, c as MatrxDataTableDensity, r as MatrxDataTableDetailConfig, s as MatrxDataTableDrillConfig, t as MatrxDataTableEditConfig, u as MatrxDataTableEmptyState, v as MatrxDataTableExpandedDetailConfig, w as MatrxDataTableFacetsConfig, x as MatrxDataTableFilterUi, y as MatrxDataTableGroupingConfig, z as MatrxDataTableHierarchyConfig, B as MatrxDataTableHierarchyMove, D as MatrxDataTableMobileCardControls, E as MatrxDataTableMultiExpandedDetailConfig, i as MatrxDataTableProps, F as MatrxDataTableQueryControl, e as MatrxDataTableQueryState, l as MatrxDataTableRead, G as MatrxDataTableReadStatus, d as MatrxDataTableRecordControls, H as MatrxDataTableSelectionConfig, I as MatrxDataTableSingleExpandedDetailConfig, J as MatrxDataTableSourceProcessing, K as MatrxDataTableSpreadsheetConfig, N as MatrxDataTableStyleConfig, O as MatrxDataTableSummaryConfig, P as MatrxDataTableSummaryContext, Q as MatrxDataTableSummaryMetric, R as MatrxDataTableToolbar, U as MatrxDataTableUrlStateConfig, V as MatrxDataTableVirtualizeConfig, W as MatrxDataTableWindowConfig, h as MatrxTableIconAction, X as MatrxTablePagination, Y as NumberFilterOp, _ as SortDirection, S as SortState, $ as SpreadsheetSelection, b as TableSavedViewsProps, T as TableSearchMatchMode, a8 as TableViewQuery, k as TableViewSnapshot, ad as TextFilterMode, ae as ToolbarFacet } from '../layered-filters-DL5BU8Mf.cjs';
|
|
2
2
|
import '../content-transfer.cjs';
|
|
3
3
|
import 'react';
|
|
4
4
|
export { AgentPayloadInput } from './copy-types.cjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AnyOfColumnSearch, n as CellEditType, o as CellEditsMap, f as ColumnFilterKind, a as ColumnFilterValue, C as ColumnFiltersState, m as ColumnMarker, M as MatrxColumnDef, j as MatrxDataTableAppearance, p as MatrxDataTableContextMenuConfig, g as MatrxDataTableCopyConfig, q as MatrxDataTableCoverageConfig, c as MatrxDataTableDensity, r as MatrxDataTableDetailConfig, s as MatrxDataTableDrillConfig, t as MatrxDataTableEditConfig, u as MatrxDataTableEmptyState, v as MatrxDataTableExpandedDetailConfig, w as MatrxDataTableFacetsConfig, x as MatrxDataTableFilterUi, y as MatrxDataTableGroupingConfig, z as MatrxDataTableHierarchyConfig, B as MatrxDataTableHierarchyMove, D as MatrxDataTableMobileCardControls, E as MatrxDataTableMultiExpandedDetailConfig, i as MatrxDataTableProps, F as MatrxDataTableQueryControl, e as MatrxDataTableQueryState, l as MatrxDataTableRead, G as MatrxDataTableReadStatus, d as MatrxDataTableRecordControls, H as MatrxDataTableSelectionConfig, I as MatrxDataTableSingleExpandedDetailConfig, J as MatrxDataTableSourceProcessing, K as MatrxDataTableSpreadsheetConfig, N as MatrxDataTableStyleConfig, O as MatrxDataTableSummaryConfig, P as MatrxDataTableSummaryContext, Q as MatrxDataTableSummaryMetric, R as MatrxDataTableToolbar, U as MatrxDataTableUrlStateConfig, V as MatrxDataTableVirtualizeConfig, W as MatrxDataTableWindowConfig, h as MatrxTableIconAction, X as MatrxTablePagination, Y as NumberFilterOp, _ as SortDirection, S as SortState, $ as SpreadsheetSelection, b as TableSavedViewsProps, T as TableSearchMatchMode, a8 as TableViewQuery, k as TableViewSnapshot, ad as TextFilterMode, ae as ToolbarFacet } from '../layered-filters-
|
|
1
|
+
export { A as AnyOfColumnSearch, n as CellEditType, o as CellEditsMap, f as ColumnFilterKind, a as ColumnFilterValue, C as ColumnFiltersState, m as ColumnMarker, M as MatrxColumnDef, j as MatrxDataTableAppearance, p as MatrxDataTableContextMenuConfig, g as MatrxDataTableCopyConfig, q as MatrxDataTableCoverageConfig, c as MatrxDataTableDensity, r as MatrxDataTableDetailConfig, s as MatrxDataTableDrillConfig, t as MatrxDataTableEditConfig, u as MatrxDataTableEmptyState, v as MatrxDataTableExpandedDetailConfig, w as MatrxDataTableFacetsConfig, x as MatrxDataTableFilterUi, y as MatrxDataTableGroupingConfig, z as MatrxDataTableHierarchyConfig, B as MatrxDataTableHierarchyMove, D as MatrxDataTableMobileCardControls, E as MatrxDataTableMultiExpandedDetailConfig, i as MatrxDataTableProps, F as MatrxDataTableQueryControl, e as MatrxDataTableQueryState, l as MatrxDataTableRead, G as MatrxDataTableReadStatus, d as MatrxDataTableRecordControls, H as MatrxDataTableSelectionConfig, I as MatrxDataTableSingleExpandedDetailConfig, J as MatrxDataTableSourceProcessing, K as MatrxDataTableSpreadsheetConfig, N as MatrxDataTableStyleConfig, O as MatrxDataTableSummaryConfig, P as MatrxDataTableSummaryContext, Q as MatrxDataTableSummaryMetric, R as MatrxDataTableToolbar, U as MatrxDataTableUrlStateConfig, V as MatrxDataTableVirtualizeConfig, W as MatrxDataTableWindowConfig, h as MatrxTableIconAction, X as MatrxTablePagination, Y as NumberFilterOp, _ as SortDirection, S as SortState, $ as SpreadsheetSelection, b as TableSavedViewsProps, T as TableSearchMatchMode, a8 as TableViewQuery, k as TableViewSnapshot, ad as TextFilterMode, ae as ToolbarFacet } from '../layered-filters-CCWzLsOC.js';
|
|
2
2
|
import '../content-transfer.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
export { AgentPayloadInput } from './copy-types.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SortState, e as MatrxDataTableQueryState } from '../layered-filters-
|
|
1
|
+
import { S as SortState, e as MatrxDataTableQueryState } from '../layered-filters-DL5BU8Mf.cjs';
|
|
2
2
|
import { UrlHistoryMode } from '@ai-matrx/kit/url-state';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '../content-transfer.cjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SortState, e as MatrxDataTableQueryState } from '../layered-filters-
|
|
1
|
+
import { S as SortState, e as MatrxDataTableQueryState } from '../layered-filters-CCWzLsOC.js';
|
|
2
2
|
import { UrlHistoryMode } from '@ai-matrx/kit/url-state';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '../content-transfer.js';
|
|
@@ -1073,6 +1073,8 @@ interface MatrxDataTableSummaryMetric<T> {
|
|
|
1073
1073
|
/** Plain text keeps the KPI's visible label and unavailable announcement identical. */
|
|
1074
1074
|
label: string;
|
|
1075
1075
|
value: (context: MatrxDataTableSummaryContext<T>) => ReactNode;
|
|
1076
|
+
/** Optional plain-text third line in the large card presentation. */
|
|
1077
|
+
description?: string;
|
|
1076
1078
|
}
|
|
1077
1079
|
/**
|
|
1078
1080
|
* Optional KPIs and aligned column totals owned by the canonical table.
|
|
@@ -1082,6 +1084,8 @@ interface MatrxDataTableSummaryMetric<T> {
|
|
|
1082
1084
|
* table never substitutes its loaded rows for a missing source value.
|
|
1083
1085
|
*/
|
|
1084
1086
|
interface MatrxDataTableSummaryConfig<T> {
|
|
1087
|
+
/** Compact is the default; large uses equal-sized cards with a description line. */
|
|
1088
|
+
variant?: "compact" | "large";
|
|
1085
1089
|
metrics: MatrxDataTableSummaryMetric<T>[];
|
|
1086
1090
|
totals?: Record<string, (context: MatrxDataTableSummaryContext<T>) => ReactNode>;
|
|
1087
1091
|
source?: {
|
|
@@ -1073,6 +1073,8 @@ interface MatrxDataTableSummaryMetric<T> {
|
|
|
1073
1073
|
/** Plain text keeps the KPI's visible label and unavailable announcement identical. */
|
|
1074
1074
|
label: string;
|
|
1075
1075
|
value: (context: MatrxDataTableSummaryContext<T>) => ReactNode;
|
|
1076
|
+
/** Optional plain-text third line in the large card presentation. */
|
|
1077
|
+
description?: string;
|
|
1076
1078
|
}
|
|
1077
1079
|
/**
|
|
1078
1080
|
* Optional KPIs and aligned column totals owned by the canonical table.
|
|
@@ -1082,6 +1084,8 @@ interface MatrxDataTableSummaryMetric<T> {
|
|
|
1082
1084
|
* table never substitutes its loaded rows for a missing source value.
|
|
1083
1085
|
*/
|
|
1084
1086
|
interface MatrxDataTableSummaryConfig<T> {
|
|
1087
|
+
/** Compact is the default; large uses equal-sized cards with a description line. */
|
|
1088
|
+
variant?: "compact" | "large";
|
|
1085
1089
|
metrics: MatrxDataTableSummaryMetric<T>[];
|
|
1086
1090
|
totals?: Record<string, (context: MatrxDataTableSummaryContext<T>) => ReactNode>;
|
|
1087
1091
|
source?: {
|
package/dist/tap-target.css
CHANGED
|
@@ -659,3 +659,9 @@
|
|
|
659
659
|
animation: none;
|
|
660
660
|
}
|
|
661
661
|
}
|
|
662
|
+
|
|
663
|
+
/* Double top-section spacing while preserving embedded and footer contracts.
|
|
664
|
+
Direct children exclude toolbars placed in caller-owned slots/portals. */
|
|
665
|
+
[data-matrx-table][data-matrx-table-density]:not([data-matrx-table-embedded]) > :is([data-matrx-table-summary], [data-matrx-table-scope-bar], [data-matrx-table-toolbar]) {
|
|
666
|
+
margin-block-end: var(--matrx-table-region-gap);
|
|
667
|
+
}
|
package/package.json
CHANGED