@ai-matrx/design-system 0.14.3 → 0.15.2

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +32 -0
  3. package/dist/data-table/copy-helpers.cjs +149 -0
  4. package/dist/data-table/copy-helpers.cjs.map +1 -0
  5. package/dist/data-table/copy-helpers.d.cts +31 -0
  6. package/dist/data-table/copy-helpers.d.ts +31 -0
  7. package/dist/data-table/copy-helpers.js +127 -0
  8. package/dist/data-table/copy-helpers.js.map +1 -0
  9. package/dist/data-table/copy-types.cjs +19 -0
  10. package/dist/data-table/copy-types.cjs.map +1 -0
  11. package/dist/data-table/copy-types.d.cts +137 -0
  12. package/dist/data-table/copy-types.d.ts +137 -0
  13. package/dist/data-table/copy-types.js +1 -0
  14. package/dist/data-table/copy-types.js.map +1 -0
  15. package/dist/data-table/data-row-inspector.cjs +1016 -0
  16. package/dist/data-table/data-row-inspector.cjs.map +1 -0
  17. package/dist/data-table/data-row-inspector.d.cts +34 -0
  18. package/dist/data-table/data-row-inspector.d.ts +34 -0
  19. package/dist/data-table/data-row-inspector.js +988 -0
  20. package/dist/data-table/data-row-inspector.js.map +1 -0
  21. package/dist/data-table/data-row-window.cjs +1136 -0
  22. package/dist/data-table/data-row-window.cjs.map +1 -0
  23. package/dist/data-table/data-row-window.d.cts +29 -0
  24. package/dist/data-table/data-row-window.d.ts +29 -0
  25. package/dist/data-table/data-row-window.js +1110 -0
  26. package/dist/data-table/data-row-window.js.map +1 -0
  27. package/dist/data-table/filter-engine.cjs +314 -0
  28. package/dist/data-table/filter-engine.cjs.map +1 -0
  29. package/dist/data-table/filter-engine.d.cts +35 -0
  30. package/dist/data-table/filter-engine.d.ts +35 -0
  31. package/dist/data-table/filter-engine.js +292 -0
  32. package/dist/data-table/filter-engine.js.map +1 -0
  33. package/dist/data-table/host.cjs +40 -0
  34. package/dist/data-table/host.cjs.map +1 -0
  35. package/dist/data-table/host.d.cts +73 -0
  36. package/dist/data-table/host.d.ts +73 -0
  37. package/dist/data-table/host.js +20 -0
  38. package/dist/data-table/host.js.map +1 -0
  39. package/dist/data-table/index.cjs +7198 -0
  40. package/dist/data-table/index.cjs.map +1 -0
  41. package/dist/data-table/index.d.cts +27 -0
  42. package/dist/data-table/index.d.ts +27 -0
  43. package/dist/data-table/index.js +7202 -0
  44. package/dist/data-table/index.js.map +1 -0
  45. package/dist/data-table/infer-filter.cjs +111 -0
  46. package/dist/data-table/infer-filter.cjs.map +1 -0
  47. package/dist/data-table/infer-filter.d.cts +17 -0
  48. package/dist/data-table/infer-filter.d.ts +17 -0
  49. package/dist/data-table/infer-filter.js +89 -0
  50. package/dist/data-table/infer-filter.js.map +1 -0
  51. package/dist/data-table/layered-filters.cjs +249 -0
  52. package/dist/data-table/layered-filters.cjs.map +1 -0
  53. package/dist/data-table/layered-filters.d.cts +3 -0
  54. package/dist/data-table/layered-filters.d.ts +3 -0
  55. package/dist/data-table/layered-filters.js +229 -0
  56. package/dist/data-table/layered-filters.js.map +1 -0
  57. package/dist/data-table/pagination.cjs +595 -0
  58. package/dist/data-table/pagination.cjs.map +1 -0
  59. package/dist/data-table/pagination.d.cts +34 -0
  60. package/dist/data-table/pagination.d.ts +34 -0
  61. package/dist/data-table/pagination.js +563 -0
  62. package/dist/data-table/pagination.js.map +1 -0
  63. package/dist/data-table/query-control.cjs +126 -0
  64. package/dist/data-table/query-control.cjs.map +1 -0
  65. package/dist/data-table/query-control.d.cts +32 -0
  66. package/dist/data-table/query-control.d.ts +32 -0
  67. package/dist/data-table/query-control.js +104 -0
  68. package/dist/data-table/query-control.js.map +1 -0
  69. package/dist/data-table/scroll-pagination.cjs +222 -0
  70. package/dist/data-table/scroll-pagination.cjs.map +1 -0
  71. package/dist/data-table/scroll-pagination.d.cts +37 -0
  72. package/dist/data-table/scroll-pagination.d.ts +37 -0
  73. package/dist/data-table/scroll-pagination.js +202 -0
  74. package/dist/data-table/scroll-pagination.js.map +1 -0
  75. package/dist/data-table/types.cjs +19 -0
  76. package/dist/data-table/types.cjs.map +1 -0
  77. package/dist/data-table/types.d.cts +3 -0
  78. package/dist/data-table/types.d.ts +3 -0
  79. package/dist/data-table/types.js +1 -0
  80. package/dist/data-table/types.js.map +1 -0
  81. package/dist/data-table/url-state.cjs +201 -0
  82. package/dist/data-table/url-state.cjs.map +1 -0
  83. package/dist/data-table/url-state.d.cts +33 -0
  84. package/dist/data-table/url-state.d.ts +33 -0
  85. package/dist/data-table/url-state.js +184 -0
  86. package/dist/data-table/url-state.js.map +1 -0
  87. package/dist/data-table/uuid-cell.cjs +371 -0
  88. package/dist/data-table/uuid-cell.cjs.map +1 -0
  89. package/dist/data-table/uuid-cell.d.cts +31 -0
  90. package/dist/data-table/uuid-cell.d.ts +31 -0
  91. package/dist/data-table/uuid-cell.js +345 -0
  92. package/dist/data-table/uuid-cell.js.map +1 -0
  93. package/dist/data-table/xlsx.cjs +100 -0
  94. package/dist/data-table/xlsx.cjs.map +1 -0
  95. package/dist/data-table/xlsx.d.cts +16 -0
  96. package/dist/data-table/xlsx.d.ts +16 -0
  97. package/dist/data-table/xlsx.js +70 -0
  98. package/dist/data-table/xlsx.js.map +1 -0
  99. package/dist/index.cjs +9 -5
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +9 -5
  102. package/dist/index.js.map +1 -1
  103. package/dist/styles.css +22 -0
  104. package/dist/types-ClaxTaXq.d.ts +798 -0
  105. package/dist/types-XXbVB1sh.d.cts +798 -0
  106. package/package.json +164 -1
@@ -0,0 +1,798 @@
1
+ import { ReactNode } from 'react';
2
+ import { AiVariant, AiCustomSource } from './data-table/copy-types.js';
3
+
4
+ /**
5
+ * Shared vocabulary for ordered, AND-combined table filters. The same rule
6
+ * shape is URL-safe, can be evaluated by local tables, and can be handed to a
7
+ * controlled table's server query without a surface inventing another query
8
+ * builder.
9
+ */
10
+
11
+ declare const LAYERED_FILTER_OPERATORS: readonly ["contains", "not_contains", "equals", "not_equals", "starts_with", "ends_with", "word", "not_word", "is_empty", "is_not_empty", "greater_than", "greater_or_equal", "less_than", "less_or_equal", "between"];
12
+ type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];
13
+ interface LayeredFilterRule {
14
+ id: string;
15
+ field: string;
16
+ operator: LayeredFilterOperator;
17
+ value: string;
18
+ valueTo?: string | undefined;
19
+ }
20
+ type LayeredFilterField = {
21
+ id: string;
22
+ label: string;
23
+ kind: "text";
24
+ operators?: LayeredFilterOperator[];
25
+ } | {
26
+ id: string;
27
+ label: string;
28
+ kind: "number";
29
+ operators?: LayeredFilterOperator[];
30
+ placeholder?: string;
31
+ } | {
32
+ id: string;
33
+ label: string;
34
+ kind: "select";
35
+ operators?: LayeredFilterOperator[];
36
+ options: Array<{
37
+ value: string;
38
+ label: string;
39
+ }>;
40
+ };
41
+ declare const LAYERED_FILTER_OPERATOR_LABELS: Record<LayeredFilterOperator, string>;
42
+ declare function operatorsForLayeredField(field: LayeredFilterField): LayeredFilterOperator[];
43
+ declare function layeredFilterNeedsValue(operator: LayeredFilterOperator): boolean;
44
+ declare function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean;
45
+ declare function completeLayeredFilterRules(rules: readonly LayeredFilterRule[] | undefined): LayeredFilterRule[];
46
+ declare function isLayeredFilterOperator(value: string): value is LayeredFilterOperator;
47
+ /** Decode URL-owned rules without trusting a hand-edited link. */
48
+ declare function decodeLayeredFilterRules(raw: string | null): LayeredFilterRule[];
49
+ /** Stable compact URL/wire form; rule order is preserved. */
50
+ declare function encodeLayeredFilterRules(rules: readonly LayeredFilterRule[] | undefined): string | null;
51
+ /**
52
+ * Translate ordinary per-column controls into the same ordered rule language
53
+ * used by the advanced builder. Controlled tables can send these rules to one
54
+ * server evaluator instead of rebuilding numeric/text filter semantics.
55
+ * Select filters are intentionally omitted because their multi-select control
56
+ * is OR-combined and should stay in its native query parameter.
57
+ */
58
+ declare function columnFiltersToLayeredRules(filters: ColumnFiltersState, fieldIds: readonly string[]): LayeredFilterRule[];
59
+ /** Local-table evaluator. Controlled tables send the same rule shape server-side. */
60
+ declare function layeredFilterMatchesValue(value: unknown, rule: LayeredFilterRule): boolean;
61
+ declare function layeredFilterRuleSummary(rule: LayeredFilterRule, fields: readonly LayeredFilterField[]): string;
62
+
63
+ /** How a column's filter UI behaves. `auto` infers from sample values. */
64
+ type ColumnFilterKind = "auto" | "text" | "select" | "boolean" | "number" | false;
65
+ type SortDirection = "asc" | "desc";
66
+ /** How the table's primary search text is matched. */
67
+ type TableSearchMatchMode = "contains" | "whole_words";
68
+ /** Cell value type for typed inline editors (Supabase-style popovers for non-strings). */
69
+ type CellEditType = "string" | "number" | "boolean" | "select"
70
+ /** Free-text multi-value chips (string[] cells: tags, labels, aliases). */
71
+ | "tags" | "date" | false;
72
+ interface MatrxColumnDef<T> {
73
+ /** Stable id used for sort/filter state. Defaults to `accessorKey` when set. */
74
+ id?: string;
75
+ /** Dot-free key on the row for default value access + auto filter. */
76
+ accessorKey?: keyof T & string;
77
+ /** Custom value for sort/filter when `accessorKey` is insufficient. */
78
+ accessorFn?: (row: T) => unknown;
79
+ /** Value used only for local sort. Falls back to `accessorFn` / `accessorKey`. */
80
+ sortValue?: (row: T) => unknown;
81
+ /** Value used only for local filters. Falls back to `accessorFn` / `accessorKey`. */
82
+ filterValue?: (row: T) => unknown;
83
+ header: ReactNode;
84
+ /** Plain-text name used for header controls when `header` is visual-only. */
85
+ label?: string;
86
+ /** Cell renderer. Defaults to stringified accessor value. */
87
+ cell?: (row: T, index: number) => ReactNode;
88
+ /** Sortable unless explicitly false. Default true. */
89
+ sortable?: boolean;
90
+ /** Direction used by the first header-click sort. Default `"asc"`. */
91
+ defaultSortDirection?: SortDirection;
92
+ /**
93
+ * Filter kind. Default `"auto"` — every column gets a filter.
94
+ * Pass `false` only when a column truly must not filter (e.g. actions).
95
+ */
96
+ filter?: ColumnFilterKind;
97
+ /** Explicit select options (when filter is `"select"` or auto-detected). */
98
+ filterOptions?: Array<{
99
+ value: string;
100
+ label: string;
101
+ }>;
102
+ /**
103
+ * Single-choice select filter. The default select filter is a multi-select
104
+ * that APPENDS (OR semantics) — correct for a status column, wrong for a
105
+ * column whose options are mutually exclusive VIEWS of the list (a
106
+ * record-class scope, a relative-date bucket). There, appending makes the
107
+ * filter inert: the consumer reads one value, the popover accumulates a set,
108
+ * and the first-selected value wins forever (D218). With `filterSingle`,
109
+ * choosing an option REPLACES the selection, and choosing the active option
110
+ * again clears it.
111
+ */
112
+ filterSingle?: boolean;
113
+ /**
114
+ * Inline edit. Default false. `"string"` edits in-cell; other types open a
115
+ * small popover (Supabase-style). Edits stay local until Save on the dirty pill.
116
+ */
117
+ editable?: CellEditType;
118
+ /**
119
+ * Per-row edit gate for an `editable` column. Return false to render the
120
+ * plain cell (no pencil, no click-to-edit) for that row — for heterogeneous
121
+ * lists where some kinds cannot take the write (e.g. a transcripts row of
122
+ * kind "unsorted" has no user-facing title). Default: every row editable.
123
+ */
124
+ editableIf?: (row: T) => boolean;
125
+ /**
126
+ * How inline edit starts. Default `"click"` (click the cell body). `"pencil"`
127
+ * shows a hover/focus pencil; the cell body no longer starts edit — so a
128
+ * whole-row click can own that gesture. Forced to `"pencil"` when `href` is
129
+ * set (D112 — the body is a real link).
130
+ */
131
+ editTrigger?: "click" | "pencil";
132
+ /**
133
+ * Options when `editable === "select"`. Also used by `"tags"` as the
134
+ * suggestion list (existing values), while still allowing new entries.
135
+ */
136
+ editOptions?: Array<{
137
+ value: string;
138
+ label: string;
139
+ }>;
140
+ /**
141
+ * Row link for the primary/title cell (D112): renders the cell content as a
142
+ * real `next/link` anchor, so the row is reachable by keyboard, announced as
143
+ * a link by screen readers, and cmd/middle-clickable into a new tab. The
144
+ * whole-row `onRowOpen` click stays as a mouse convenience; clicks on the
145
+ * anchor never double-fire it. Combine with `editable` and the link renders
146
+ * with a hover/focus pencil that opens the inline editor instead of
147
+ * click-text-to-edit.
148
+ */
149
+ href?: (row: T) => string | undefined;
150
+ /**
151
+ * Canonical entity token for the record this column NAMES. When set, the cell
152
+ * renders through `EntityRef`, so the name carries the full door set — Open,
153
+ * new tab, and Peek — instead of the Open-only `<Link>` that `href` alone
154
+ * produces.
155
+ *
156
+ * THE INVENTORY LAW, applied to this component: the table grew its own door
157
+ * (`href`) beside the platform's (`EntityRef`), and every column that named a
158
+ * record picked one and silently lost the other half. This field collapses
159
+ * them — `href` still works and still forces the pencil trigger, and it
160
+ * OVERRIDES the registry route when both are set (for an admin-side route on
161
+ * a satellite deployment).
162
+ *
163
+ * Needs the record's id: `entityToken` is paired with `entityId`, defaulting
164
+ * to the table's own `getRowId`.
165
+ *
166
+ * **PER ROW, not per column** — a hub can be heterogeneous. `/transcripts`
167
+ * lists transcripts, studio sessions, cleanup runs and an "unsorted" bucket
168
+ * in one table, each with its own destination; a constant token would have
169
+ * sent a session id to the transcript processor route and opened the
170
+ * transcript peek on a record that is not one. Return `undefined` for a row
171
+ * that names no entity — it falls back to the plain `href` link, or to inert
172
+ * text. Pair with a per-row `href` when the kinds diverge.
173
+ */
174
+ entityToken?: string | ((row: T) => string | undefined);
175
+ /**
176
+ * NOTE: give the column a `cell` when you set this. Without one, a
177
+ * UUID-shaped value renders the default `MatrxUuidCell`, which has its own
178
+ * controls — wrapping those in the door's anchor would nest interactive
179
+ * elements inside a link, and is redundant besides. The shell detects that
180
+ * combination, skips the door, and screams once per column.
181
+ */
182
+ /** The id `entityToken` refers to. Defaults to the table's `getRowId(row)`. */
183
+ entityId?: (row: T) => string | undefined;
184
+ /**
185
+ * Drop this column below `sm` (the phone breakpoint).
186
+ *
187
+ * A wide table on a phone becomes a horizontal scroller: the frozen
188
+ * identity column stays, and everything after the second column sits off
189
+ * the right edge where a reviewer will never find it. Marking the columns
190
+ * that do NOT earn their width on a phone is how a surface declares an
191
+ * INTENTIONAL mobile column set instead of an accidental one.
192
+ *
193
+ * The column is still fully sortable/filterable from the toolbar and still
194
+ * rides every copy/export payload — this hides the CELL, not the data.
195
+ * Default `false` = today's behavior everywhere.
196
+ */
197
+ mobileHidden?: boolean;
198
+ /**
199
+ * Built-in cell kinds. `"uuid"` / `"fk"` use MatrxUuidCell (short + copy +
200
+ * optional open). `"auto"` (default) detects UUID-shaped strings.
201
+ */
202
+ cellKind?: "auto" | "uuid" | "fk" | "text";
203
+ /**
204
+ * FK / UUID navigation. Prefer `onOpen` → WindowPanel of the target.
205
+ * Return `"forbidden"` when the caller lacks access.
206
+ */
207
+ fk?: {
208
+ label?: string;
209
+ /**
210
+ * Canonical entity token this column's ids point at (`agent`, `note`, …).
211
+ * THE DOOR LAW made declarative: the cell resolves route + new tab + peek
212
+ * from the registries, so a column of ids stops being a dead end without
213
+ * hand-wiring a link. `href` / `onOpen` still win when both are set.
214
+ * A function form resolves the token per row (an audit log whose target
215
+ * type varies by row).
216
+ *
217
+ * `"auto"` derives the token from the COLUMN NAME (`task_id` → `task`).
218
+ * It is opt-in on purpose: the guess is only correct when you have checked
219
+ * the actual FK. `scheduler.sch_run.task_id` references `scheduler.sch_task`,
220
+ * not the workspace `task` the name implies, and `app_id` /
221
+ * `conversation_id` / `file_id` / `workflow_id` each have several candidate
222
+ * tables. A wrong door opens a DIFFERENT record — worse than no door.
223
+ */
224
+ token?: string | null | "auto" | ((row: T) => string | null | undefined);
225
+ href?: (id: string, row: T) => string | null | undefined;
226
+ onOpen?: (id: string, row: T) => void | "forbidden" | Promise<void | "forbidden">;
227
+ /** Force non-navigable for this column. */
228
+ forbidden?: boolean | ((id: string, row: T) => boolean);
229
+ };
230
+ className?: string;
231
+ headerClassName?: string;
232
+ width?: string | number;
233
+ align?: "left" | "center" | "right";
234
+ /**
235
+ * ICON COLUMN. A column whose whole content is one glyph — a star, a lock, a
236
+ * status dot — and whose `width` is therefore a lie without this flag.
237
+ *
238
+ * `width` is only a hint on a table cell: min-content wins. A 40px star column
239
+ * still rendered ~70px wide because the HEADER carried three separate
240
+ * controls beside the glyph (the sort button, its arrow, the filter funnel),
241
+ * and the cell carried the default `px-2` on both sides. Every surface that
242
+ * wanted a tight icon column was paying for chrome it never used.
243
+ *
244
+ * `compact` fixes it AT THE PRIMITIVE, and does NOT cost the column anything:
245
+ * horizontal padding drops to `px-1`, and the header collapses its three
246
+ * controls into ONE popover trigger that still offers Sort ascending / Sort
247
+ * descending / Clear sort / the full filter body. The column stays fully
248
+ * sortable and filterable — the affordances moved into the menu, they did not
249
+ * disappear. Active sort/filter still show, as a 2px dot on the trigger.
250
+ *
251
+ * Pair it with `width` and `align: "center"`.
252
+ */
253
+ compact?: boolean;
254
+ /** Hide from the table (still available in column picker when we add it). */
255
+ hidden?: boolean;
256
+ /** Whether the package-owned Columns dialog may hide this column. Default true. */
257
+ hideable?: boolean;
258
+ }
259
+ /** How a text filter matches. Default `"contains"`. */
260
+ type TextFilterMode = "contains" | "empty" | "not_empty" | "null" | "not_null";
261
+ /** Active per-column filter value. Shape depends on filter kind. */
262
+ type ColumnFilterValue = {
263
+ kind: "text";
264
+ value: string;
265
+ mode?: TextFilterMode;
266
+ negated?: boolean;
267
+ } | {
268
+ kind: "select";
269
+ /** Single-choice value (legacy writers). Ignored when `values` is set. */
270
+ value: string;
271
+ /** Multi-choice OR set — a row passes if it matches ANY entry. */
272
+ values?: string[];
273
+ /** Exclude matching values instead of including them. */
274
+ negated?: boolean;
275
+ } | {
276
+ kind: "boolean";
277
+ value: boolean;
278
+ negated?: boolean;
279
+ } | {
280
+ kind: "number";
281
+ min?: number | undefined;
282
+ max?: number | undefined;
283
+ negated?: boolean;
284
+ };
285
+ type ColumnFiltersState = Record<string, ColumnFilterValue | undefined>;
286
+ interface SortState {
287
+ id: string;
288
+ direction: SortDirection;
289
+ }
290
+ /**
291
+ * Opt-in durable URL state for a local table.
292
+ *
293
+ * Every table gets an explicit stable id, producing namespaced parameters such
294
+ * as `table.accounts.q` and `table.accounts.sort`. This prevents collisions
295
+ * with page-owned parameters and with sibling tables on the same route.
296
+ */
297
+ interface MatrxDataTableUrlStateConfig {
298
+ /** Stable lowercase identifier: letters, numbers, and hyphens; max 64 chars. */
299
+ id: string;
300
+ /** Initial sort when the URL carries none. Default: none. */
301
+ defaultSort?: SortState | null | undefined;
302
+ /** Browser history behavior for table transitions. Default: `push`. */
303
+ history?: "push" | "replace" | undefined;
304
+ /**
305
+ * History behavior while typing search/any-of text. `session` pushes the
306
+ * first edit, then replaces rapid keystrokes. Default: `session`.
307
+ */
308
+ textHistory?: "session" | "push" | "replace" | undefined;
309
+ /** Persist the open side-panel row. Default true. */
310
+ selectedRow?: boolean;
311
+ /** Persist the open table-owned window row. Default true. */
312
+ windowRow?: boolean;
313
+ /** Persist checkbox selection. Opt-in because large selections lengthen URLs. */
314
+ selection?: boolean;
315
+ }
316
+ /**
317
+ * Complete view state for a remotely queried table page. The table owns none
318
+ * of this state in controlled mode: callers may mirror it to URL search params
319
+ * and use it as part of a direct database-query cache key.
320
+ */
321
+ interface MatrxDataTableQueryState {
322
+ /** One-based page number, matching the table's pagination UI. */
323
+ page: number;
324
+ pageSize: number;
325
+ search: string;
326
+ /** Defaults to `contains` when omitted, preserving every existing table. */
327
+ searchMatchMode?: TableSearchMatchMode;
328
+ anyOf: string;
329
+ /** Ordered AND rules from the compact advanced-filter builder. */
330
+ layeredFilters?: LayeredFilterRule[];
331
+ columnFilters: ColumnFiltersState;
332
+ sort: SortState | null;
333
+ }
334
+ /**
335
+ * Optional data-processing contract. Omit it (or use `local`) to preserve the
336
+ * original in-memory filter/sort/pagination behavior. In controlled mode,
337
+ * `data` is already the current page and the caller performs all querying.
338
+ */
339
+ /** Structural match for @ai-matrx/data/react usePaginatedData. */
340
+ interface MatrxTablePagination<T> {
341
+ queryKey: string;
342
+ rows: T[];
343
+ loading: boolean;
344
+ isFetchingNextPage: boolean;
345
+ error: Error | null;
346
+ hasNextPage: boolean;
347
+ loadNextPage: () => Promise<void>;
348
+ refresh: () => void;
349
+ totalItems: number | undefined;
350
+ }
351
+ /**
352
+ * A controlled-local table can retain local search/pagination while its source
353
+ * owns column filters and/or sorting. `sourceTotal` is display metadata only:
354
+ * it never changes local page slicing, because the loaded rows remain local.
355
+ */
356
+ interface MatrxDataTableSourceProcessing {
357
+ columnFilters?: "local" | "source";
358
+ sort?: "local" | "source";
359
+ sourceTotal?: number;
360
+ }
361
+ type MatrxDataTableQueryControl<T = unknown> = {
362
+ mode: "local";
363
+ } | {
364
+ /** Accumulated server rows; page stays 1 and sort/filter remain server-owned. */
365
+ mode: "controlled-append";
366
+ state: MatrxDataTableQueryState;
367
+ onStateChange: (next: MatrxDataTableQueryState) => void;
368
+ pagination: MatrxTablePagination<T>;
369
+ /** Source-connected tables append on user scroll by default. */
370
+ scroll?: {
371
+ mode?: "scroll" | "manual";
372
+ thresholdPx?: number;
373
+ intentTimeoutMs?: number;
374
+ };
375
+ } | {
376
+ /** Local rows, but every query control is owned by the caller (for URL state). */
377
+ mode: "controlled-local";
378
+ state: MatrxDataTableQueryState;
379
+ onStateChange: (next: MatrxDataTableQueryState) => void;
380
+ sourceProcessing?: MatrxDataTableSourceProcessing;
381
+ } | {
382
+ mode: "controlled";
383
+ state: MatrxDataTableQueryState;
384
+ /** Total rows matching the controlled query, not just `data.length`. */
385
+ totalItems: number;
386
+ onStateChange: (next: MatrxDataTableQueryState) => void;
387
+ };
388
+ /**
389
+ * Toolbar facets — first-class, Mars-extensible filter controls above the grid.
390
+ * Start with button-group; add radio / switch / complex later without forking.
391
+ */
392
+ type ToolbarFacet = {
393
+ type: "button-group";
394
+ id: string;
395
+ label?: string;
396
+ value: string;
397
+ /** Reset target for per-facet + global clear. Default: first option value. */
398
+ defaultValue?: string;
399
+ options: Array<{
400
+ value: string;
401
+ label: string;
402
+ icon?: ReactNode;
403
+ }>;
404
+ onChange: (value: string) => void;
405
+ } | {
406
+ type: "custom";
407
+ id: string;
408
+ render: () => ReactNode;
409
+ };
410
+ /**
411
+ * Cross-column OR search — matches if ANY listed column contains the query.
412
+ * Relationships use case: filter by entity type without picking source vs target.
413
+ */
414
+ interface AnyOfColumnSearch {
415
+ columnIds: string[];
416
+ placeholder?: string;
417
+ /** Controlled value. Uncontrolled if omitted. */
418
+ value?: string;
419
+ onChange?: (value: string) => void;
420
+ }
421
+ interface MatrxDataTableToolbar {
422
+ /** Global search across all accessor values. Default true. */
423
+ search?: boolean;
424
+ searchPlaceholder?: string;
425
+ searchValue?: string;
426
+ onSearchChange?: (value: string) => void;
427
+ /**
428
+ * Show a compact, visible choice between substring and whole-word search.
429
+ * Omit it when a data source cannot honor both modes server-side.
430
+ */
431
+ searchMatch?: {
432
+ defaultMode?: TableSearchMatchMode;
433
+ };
434
+ /**
435
+ * OR-search across specific columns (e.g. source_type OR target_type).
436
+ * Shown as its own input beside global search when set.
437
+ */
438
+ anyOf?: AnyOfColumnSearch;
439
+ /**
440
+ * Optional compact advanced-filter builder beside the regular search. In
441
+ * controlled mode rules live in `query.state.layeredFilters`; local tables
442
+ * evaluate them against matching column ids.
443
+ */
444
+ layeredFilters?: {
445
+ fields: readonly LayeredFilterField[];
446
+ maxRules?: number;
447
+ label?: string;
448
+ };
449
+ /** Extensible facet strip (button groups, later radios/switches/…). */
450
+ facets?: ToolbarFacet[];
451
+ /** Left-side extra nodes (after search / facets). */
452
+ leading?: ReactNode;
453
+ /** Right-side actions (create, refresh, …). */
454
+ actions?: ReactNode;
455
+ }
456
+ /** Optional deferred-apply copy for column filter popovers. */
457
+ interface MatrxDataTableFilterUi {
458
+ /** Enables the Include/Exclude control. Default false for remote-safe filters. */
459
+ allowNegation?: boolean;
460
+ /** Enables null/not-null text modes. Default false for remote-safe filters. */
461
+ allowNullModes?: boolean;
462
+ applyLabel?: string;
463
+ showCancel?: boolean;
464
+ footerHint?: ReactNode;
465
+ }
466
+ interface MatrxDataTableCopyConfig<T> {
467
+ /** Toast / tooltip label base, e.g. "Relationship rule". */
468
+ label: string;
469
+ listLabel?: string;
470
+ location: string;
471
+ rowKind: string;
472
+ listKind: string;
473
+ rowDescription?: string;
474
+ listDescription?: string;
475
+ humanRow: (row: T) => string;
476
+ /** Project row for agent JSON. Default: full row. */
477
+ agentRow?: (row: T) => unknown;
478
+ rowAttributes?: (row: T) => Record<string, string | number | boolean | null | undefined>;
479
+ listAttributes?: (visible: T[], all: T[]) => Record<string, string | number | boolean | null | undefined>;
480
+ /**
481
+ * Live view state rendered inside the list payload's <context>. Unlike
482
+ * per-row data, this remains present when the current view has zero rows.
483
+ */
484
+ listContext?: (visible: T[], all: T[]) => Record<string, string | number | boolean | null | undefined>;
485
+ /**
486
+ * Additional row-scoped AI actions. Use this to fold a domain action such
487
+ * as a paste-ready repair brief into the table-owned row copy menu instead
488
+ * of rendering a separate third control. Builders run at click time.
489
+ */
490
+ rowAiVariants?: (row: T) => AiVariant[];
491
+ /**
492
+ * Graded AI variants for the toolbar's view copy (e.g. "Top 25", "Summary
493
+ * only"). When set, the toolbar's Copy-for-AI upgrades to a dropdown with
494
+ * these variants + the full-view payload as the automatic "Everything"
495
+ * escape hatch. Receives (visible, all) rows at render; builders run at
496
+ * click time.
497
+ */
498
+ aiVariants?: (visible: T[], all: T[]) => AiVariant[];
499
+ /** Custom-preview source (options dialog + live size counts) for the view. */
500
+ aiCustom?: (visible: T[], all: T[]) => AiCustomSource;
501
+ /** Show toolbar copy (this view). Default true when copy is set. */
502
+ showToolbar?: boolean;
503
+ /** Show per-row copy. Default true when copy is set. */
504
+ showRow?: boolean;
505
+ }
506
+ interface MatrxDataTableDetailConfig<T> {
507
+ /** Side-panel title. Default: first string column or "Details". */
508
+ title?: (row: T) => ReactNode;
509
+ description?: (row: T) => ReactNode | undefined;
510
+ /** Override the default key/value inspector. */
511
+ render?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;
512
+ /** Header actions inside the side panel. */
513
+ headerActions?: (row: T) => ReactNode;
514
+ defaultWidth?: number;
515
+ enabled?: boolean;
516
+ /**
517
+ * Maps a field name to the entity token its id points at, turning that field
518
+ * into a door (route + peek) in the default inspector — side panel AND row
519
+ * window.
520
+ *
521
+ * There is no default guess: this inspector renders whatever columns the row
522
+ * has, and a wrong door opens a DIFFERENT record (`sch_run.task_id` is a
523
+ * SCHEDULED task, not a workspace `task`). A table whose FKs you HAVE checked
524
+ * can pass `tokenFromColumnName` (`components/official/entity-ref/doors`) to
525
+ * open every `<token>_id` field at once.
526
+ *
527
+ * The ROW is passed too, because a table whose target type varies per row (an
528
+ * audit log, an exposure report) cannot answer from the column name alone.
529
+ */
530
+ tokenForField?: (key: string, row: T) => string | null;
531
+ }
532
+ /** Actions a record-owned control can use without reaching into table state. */
533
+ interface MatrxDataTableRecordControls {
534
+ /** Close the row's side-panel detail, if open. */
535
+ closeDetail: () => void;
536
+ /** Open the row in the canonical adjustable side panel. */
537
+ openDetail: () => void;
538
+ /** Open the row in its canonical table-owned WindowPanel. */
539
+ openWindow: () => void;
540
+ /** Close the row's table-owned WindowPanel, if open. */
541
+ closeWindow: () => void;
542
+ /** Whether this row currently has visible, unpersisted inline edits. */
543
+ hasPendingEdits: boolean;
544
+ /**
545
+ * Discard this row's pending inline edits.
546
+ *
547
+ * Row actions that persist the already-merged visible row (for example an
548
+ * explicit Confirm action) call this only after that write succeeds. This
549
+ * prevents the floating Save pill from later replaying the same draft as a
550
+ * different, weaker write.
551
+ */
552
+ discardPendingEdits: () => void;
553
+ }
554
+ interface MatrxDataTableWindowConfig<T> {
555
+ /** Window title. */
556
+ title?: (row: T) => string;
557
+ /**
558
+ * @deprecated Prefer `renderView` + `renderEdit` so the window stays editable.
559
+ * Full-body override with no View/Edit tabs.
560
+ */
561
+ render?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;
562
+ /** View tab body. Defaults to DataRowInspector. */
563
+ renderView?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;
564
+ /**
565
+ * Edit tab body. When set, the WindowPanel shows View / Edit sidebar tabs
566
+ * (WindowPanel built-in sidebar). Defaults to `detail.render` when present.
567
+ * Pass `false` to keep a view-only window even when `detail.render` exists.
568
+ */
569
+ renderEdit?: ((row: T, controls: MatrxDataTableRecordControls) => ReactNode) | false;
570
+ /**
571
+ * Called when the panel icon opens the window — hydrate edit state here
572
+ * without opening the side panel (prefer this over `onRowOpen` for windows).
573
+ */
574
+ onOpen?: (row: T) => void;
575
+ /**
576
+ * Make full-row click open the WindowPanel instead of the side panel.
577
+ * The trailing row action and the window header then expose the side panel
578
+ * as the explicit secondary presentation. Default false.
579
+ */
580
+ openOnRowClick?: boolean;
581
+ /** Which tab to open. Default: `"edit"` when an edit body exists. */
582
+ defaultTab?: "view" | "edit";
583
+ /** Show the panel-icon that opens the window. Default true when detail enabled. */
584
+ enabled?: boolean;
585
+ width?: number;
586
+ height?: number;
587
+ }
588
+ interface MatrxDataTableEmptyState {
589
+ icon?: ReactNode;
590
+ title: string;
591
+ description?: string;
592
+ action?: ReactNode;
593
+ }
594
+ /** Pending cell edits keyed by row id → partial field map. */
595
+ type CellEditsMap = Record<string, Record<string, unknown>>;
596
+ interface MatrxDataTableEditConfig<T> {
597
+ /** Enable inline editing for columns with `editable` set. */
598
+ enabled?: boolean;
599
+ /**
600
+ * Persist all pending edits. Called when the user clicks Save on the dirty pill.
601
+ * Return resolved when done; throw/reject to keep the draft (with toast).
602
+ */
603
+ onSave: (edits: CellEditsMap, rows: T[]) => void | Promise<void>;
604
+ /** Persist each committed cell immediately. Failed writes remain in the
605
+ * dirty pill so the user can retry or cancel them. */
606
+ autoSave?: boolean;
607
+ /** Optional cancel hook (draft already discarded). */
608
+ onCancel?: () => void;
609
+ }
610
+ interface MatrxDataTableHierarchyConfig<T> {
611
+ /** Complete hierarchy when `data` is only the current controlled page. */
612
+ rows?: T[];
613
+ getParentId: (row: T) => string | null;
614
+ /** Persist the exact structural intent represented by the drop shadow. */
615
+ onMove: (row: T, move: MatrxDataTableHierarchyMove) => void | Promise<void>;
616
+ /** Enables sibling insertion shadows in addition to parent/root drops. */
617
+ manualOrder?: boolean;
618
+ canReparent?: (row: T) => boolean;
619
+ itemLabel?: (row: T) => string;
620
+ rootDropLabel?: string;
621
+ }
622
+ interface MatrxDataTableHierarchyMove {
623
+ parentId: string | null;
624
+ /**
625
+ * Insert immediately before this sibling. Null means first child for
626
+ * "inside"/"root", and LAST sibling for "after" (target had no successor).
627
+ */
628
+ beforeId: string | null;
629
+ position: "before" | "after" | "inside" | "root";
630
+ targetId: string | null;
631
+ }
632
+ /**
633
+ * Multi-row selection — a leading checkbox column plus a bulk bar that appears
634
+ * only while rows are checked.
635
+ *
636
+ * OPT-IN and fully CONTROLLED: the consumer owns the id set, so selection
637
+ * survives (or is deliberately cleared by) a re-fetch, a filter change, or an
638
+ * optimistic list update — the table never holds hidden selection state that
639
+ * can disagree with the surface around it.
640
+ *
641
+ * Why this is a primitive and not a per-surface checkbox column: a register
642
+ * the user cannot clear in bulk is a register they stop reading, and the fifth
643
+ * hand-rolled selection column — each with its own shift-click, its own
644
+ * select-all semantics, its own bar — is exactly the fork `components/official/`
645
+ * exists to prevent.
646
+ *
647
+ * Selection does not alter mobile horizontal scrolling. The complete row moves
648
+ * as one surface below `sm`; no leading column is allowed to pin over the data.
649
+ */
650
+ interface MatrxDataTableSelectionConfig<T> {
651
+ /** Selected row ids (`getRowId`). Ids not on the current page are kept. */
652
+ selectedIds: string[];
653
+ onSelectedIdsChange: (ids: string[]) => void;
654
+ /**
655
+ * The bulk bar's actions, given the currently-selected rows THAT ARE LOADED.
656
+ * Selection can outlive a page change, so also take `selectedIds` when an
657
+ * action only needs ids.
658
+ */
659
+ actions?: (selected: T[], selectedIds: string[]) => ReactNode;
660
+ /**
661
+ * Rows that cannot be acted on in bulk render NO checkbox at all — the cell
662
+ * stays for alignment, the control is absent from the DOM. A greyed control
663
+ * still advertises a choice the surface underneath would refuse.
664
+ */
665
+ isRowSelectable?: (row: T) => boolean;
666
+ /** Singular noun for the bar's count ("finding" → "3 findings selected"). */
667
+ noun?: string;
668
+ }
669
+ /** Table-owned state and actions exposed to an opt-in phone card renderer. */
670
+ interface MatrxDataTableMobileCardControls {
671
+ /** Whether this row is in the canonical selection set. */
672
+ selected: boolean;
673
+ /** Whether the consumer allows this row to be selected. */
674
+ selectable: boolean;
675
+ /** Update selection through the table's controlled/URL-backed contract. */
676
+ onSelectedChange: (selected: boolean) => void;
677
+ /**
678
+ * The table's canonical per-row copy controls plus consumer row actions.
679
+ * Render this instead of rebuilding either action path inside the card.
680
+ */
681
+ actions: ReactNode;
682
+ }
683
+ interface MatrxDataTableProps<T> {
684
+ data: T[];
685
+ columns: MatrxColumnDef<T>[];
686
+ getRowId: (row: T) => string;
687
+ /**
688
+ * Additional row identity included in local global search without becoming
689
+ * a visible/sortable/filterable column. Use for canonical composite keys or
690
+ * aliases whose displayed parts live in separate columns.
691
+ *
692
+ * Ignored in remote controlled mode, where the query owner applies search.
693
+ */
694
+ searchText?: (row: T) => string;
695
+ /**
696
+ * Hierarchy-aware local processing seam. The canonical table still owns the
697
+ * toolbar, URL state, headers, pagination, editing, copy, and rendering; the
698
+ * consumer only preserves domain ordering that a flat sort would destroy.
699
+ *
700
+ * The processor must honor every active query control in `state` and return
701
+ * the complete filtered/sorted local result before pagination. It is ignored
702
+ * in remote controlled mode, where `data` is already the queried page.
703
+ */
704
+ processLocalRows?: (rows: T[], state: MatrxDataTableQueryState) => T[];
705
+ /** Initial local sort when neither URL state nor a controlled query owns it. */
706
+ defaultSort?: SortState | null;
707
+ isLoading?: boolean;
708
+ /**
709
+ * Background refresh state. Unlike `isLoading`, this preserves rendered rows
710
+ * and shows only the table's non-blocking refresh indicator.
711
+ */
712
+ isFetching?: boolean;
713
+ /**
714
+ * Controlled query state for direct remote data sources. The component never
715
+ * fetches data itself; it only emits state changes to the caller.
716
+ */
717
+ query?: MatrxDataTableQueryControl<T>;
718
+ /**
719
+ * Persist local query and record-view state in namespaced URL parameters.
720
+ * This is intentionally opt-in and cannot be combined with controlled query
721
+ * mode; remote tables use `useTableUrlState({ tableId })` in their query owner.
722
+ */
723
+ urlState?: MatrxDataTableUrlStateConfig;
724
+ toolbar?: MatrxDataTableToolbar;
725
+ /** Hide the package toolbar while retaining table-owned query behavior. */
726
+ hideToolbar?: boolean;
727
+ /** Hide numbered pagination; local rows remain available to the caller. */
728
+ hidePagination?: boolean;
729
+ /** Optional deferred Apply/Cancel copy for column filter popovers. */
730
+ filterUi?: MatrxDataTableFilterUi;
731
+ /** Row click opens the side panel unless `window.openOnRowClick` is true. */
732
+ detail?: MatrxDataTableDetailConfig<T>;
733
+ /** Panel icon opens a WindowPanel (page-local; supports ReactNode override). */
734
+ window?: MatrxDataTableWindowConfig<T>;
735
+ /** Copy + Copy for AI (rows + this view). */
736
+ copy?: MatrxDataTableCopyConfig<T>;
737
+ /** Inline edit session with floating Save/Cancel pill. */
738
+ edit?: MatrxDataTableEditConfig<T>;
739
+ /** Opt-in tree reparenting owned by the canonical row renderer. */
740
+ hierarchy?: MatrxDataTableHierarchyConfig<T>;
741
+ /** Controlled selection (selected row id for highlight). */
742
+ selectedId?: string | null | undefined;
743
+ onSelectedIdChange?: ((id: string | null) => void) | undefined;
744
+ /** Controlled table-owned window row (normally supplied by URL state). */
745
+ windowRowId?: string | null | undefined;
746
+ onWindowRowIdChange?: ((id: string | null) => void) | undefined;
747
+ /** Opt-in multi-row checkbox selection + a bulk action bar. */
748
+ selection?: MatrxDataTableSelectionConfig<T>;
749
+ /** Extra row actions rendered in a trailing Actions column. */
750
+ rowActions?: (row: T, controls: MatrxDataTableRecordControls) => ReactNode;
751
+ /** Extra rows rendered in the table footer with the same columns as data rows. */
752
+ footerRows?: T[];
753
+ /** Per-row presentation class, after canonical selection/highlight state. */
754
+ rowClassName?: (row: T, index: number) => string | undefined;
755
+ /** Highlight an index in the currently rendered page without selecting it. */
756
+ highlightedIndex?: number | undefined;
757
+ /** Receives the complete visible local view after local processing, before paging. */
758
+ onViewChange?: (rows: T[]) => void;
759
+ /**
760
+ * Wrap the whole `<tr>`. Return `children` unchanged for no-op.
761
+ *
762
+ * `rowActions` only reaches the actions CELL, so anything that must own the
763
+ * entire row — a right-click menu, a drag handle, a drop target — had no
764
+ * seam and would have forced a surface to fork the table. THE INVENTORY LAW:
765
+ * the fork is the defect, so the seam exists instead.
766
+ *
767
+ * Whatever you return must render `children` as a direct `<tbody>` child, so
768
+ * the wrapper has to be a component that emits the `<tr>` unchanged
769
+ * (`ItemContextMenu` does — it renders a Radix trigger with `asChild`).
770
+ */
771
+ rowWrapper?: (row: T, children: ReactNode) => ReactNode;
772
+ emptyState?: MatrxDataTableEmptyState;
773
+ /** Default 25. Pass 0 to show all. */
774
+ pageSize?: number;
775
+ pageSizeOptions?: number[];
776
+ zebra?: boolean;
777
+ className?: string;
778
+ tableClassName?: string;
779
+ /**
780
+ * Optional compact row presentation rendered below `mobileCardsBreakpoint`
781
+ * in place of the horizontal table. The caller supplies the record summary
782
+ * because only the product surface knows which values and actions are
783
+ * essential at narrow widths; MatrxDataTable still owns query state,
784
+ * loading/empty states, and pagination.
785
+ *
786
+ * `controls.actions` carries the table-owned copy controls and consumer row
787
+ * actions, so a card does not fork them. Prefer the default horizontal table
788
+ * unless the product explicitly requires every essential value/action to be
789
+ * discoverable without horizontal scrolling.
790
+ */
791
+ mobileCards?: (row: T, index: number, controls: MatrxDataTableMobileCardControls) => ReactNode;
792
+ /** Card/table handoff. `sm` is phone-only; `lg` includes portrait tablets. */
793
+ mobileCardsBreakpoint?: "sm" | "lg";
794
+ /** Called after a row is selected for detail (in addition to opening the panel). */
795
+ onRowOpen?: (row: T) => void;
796
+ }
797
+
798
+ export { type AnyOfColumnSearch as A, LAYERED_FILTER_OPERATOR_LABELS as B, type ColumnFiltersState as C, type LayeredFilterField as D, type LayeredFilterOperator as E, columnFiltersToLayeredRules as F, completeLayeredFilterRules as G, decodeLayeredFilterRules as H, encodeLayeredFilterRules as I, isCompleteLayeredFilterRule as J, isLayeredFilterOperator as K, type LayeredFilterRule as L, type MatrxColumnDef as M, layeredFilterMatchesValue as N, layeredFilterNeedsValue as O, layeredFilterRuleSummary as P, operatorsForLayeredField as Q, type SortState as S, type TableSearchMatchMode as T, type ColumnFilterValue as a, type MatrxDataTableProps as b, type CellEditType as c, type CellEditsMap as d, type ColumnFilterKind as e, type MatrxDataTableCopyConfig as f, type MatrxDataTableDetailConfig as g, type MatrxDataTableEditConfig as h, type MatrxDataTableEmptyState as i, type MatrxDataTableFilterUi as j, type MatrxDataTableHierarchyConfig as k, type MatrxDataTableHierarchyMove as l, type MatrxDataTableMobileCardControls as m, type MatrxDataTableQueryControl as n, type MatrxDataTableQueryState as o, type MatrxDataTableRecordControls as p, type MatrxDataTableSelectionConfig as q, type MatrxDataTableSourceProcessing as r, type MatrxDataTableToolbar as s, type MatrxDataTableUrlStateConfig as t, type MatrxDataTableWindowConfig as u, type MatrxTablePagination as v, type SortDirection as w, type TextFilterMode as x, type ToolbarFacet as y, LAYERED_FILTER_OPERATORS as z };