@ai-matrx/records-ui 0.115.70 → 0.115.71

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.
@@ -0,0 +1,1258 @@
1
+ import { Uuid, RecordsActor, RecordsDataSource, PermissionLevel, RecordScopeContext, Field, RowAction, RecordDocument, AggregateFilter, Table, RecordRead, RowAgentTarget, ReadRow, HistoryActor, FieldHistoryEntry, HiddenFieldNotice, DeclarableFieldKind, NewerFieldKind, FieldKind, ParityFieldType, TableDecorations } from '@ai-matrx/records';
2
+ import { ReactNode, KeyboardEvent } from 'react';
3
+ import { ReversibleAnnouncement } from '@ai-matrx/kit/reversible';
4
+ import { Uuid as Uuid$1 } from '@ai-matrx/records/react';
5
+ export { DEFAULT_FIELDS, DeclareResult, NewFieldSpec, NewTableSpec, addFields, declareTable, fieldDeclarationFor, tokenFor } from '@ai-matrx/records/core';
6
+ import { TableStyle } from '@ai-matrx/design-system/data-table';
7
+
8
+ declare function personActor(userId: Uuid | null | undefined): RecordsActor;
9
+ declare function recordsDataSource(client: object, fallbackSchema?: string): RecordsDataSource;
10
+
11
+ /** The host app's saved zone (IANA). `null` / junk clears it: the device zone applies. */
12
+ declare function setPersonTimeZone(next: string | null | undefined): void;
13
+ declare function getPersonTimeZone(): string | null;
14
+ /** Calls `listener` whenever the zone changes; returns the unsubscribe. */
15
+ declare function subscribePersonTimeZone(listener: () => void): () => void;
16
+ /**
17
+ * `at` as a Date whose LOCAL fields (getFullYear, getHours, ...) read the wall clock in the person's zone.
18
+ * For day / hour math only: never compare it with a stored instant.
19
+ */
20
+ declare function personWallNow(at?: Date): Date;
21
+
22
+ /** The six things a person does to a record or a table, as the screens offer them. */
23
+ type Capability = "read" | "comment" | "write" | "organise" | "remove" | "share" | "structure";
24
+ interface WhatYouMayDo {
25
+ /** The word the store answered, or null when it answered nothing. */
26
+ level: PermissionLevel | null;
27
+ /**
28
+ * Whether the store has ANSWERED yet. `false` is a third state and not a
29
+ * refusal: a screen that drew "you may not" while the answer was in flight
30
+ * would blink every control off on every reload, which is its own lie.
31
+ */
32
+ known: boolean;
33
+ read: boolean;
34
+ comment: boolean;
35
+ write: boolean;
36
+ /** Duplicate, merge, restore, make a layout the default: the doors that ask `editor`, above content. */
37
+ organise: boolean;
38
+ remove: boolean;
39
+ share: boolean;
40
+ structure: boolean;
41
+ /** Why this person may not do that, in one sentence naming the way in. */
42
+ why: (what: Capability) => string;
43
+ }
44
+
45
+ /**
46
+ * What a person may do to one table. The four words and the ladder are the
47
+ * store's (`rights.ts`), not ours.
48
+ *
49
+ * `admin`, `comment` and `write` are kept with the names they have always had,
50
+ * because every screen in this package and in the host apps reads them; they
51
+ * are now the store's ANSWER rather than a host's guess.
52
+ */
53
+ interface TableRights extends WhatYouMayDo {
54
+ /** SCR-2: holders of `admin` on the table see the "+" and the settings panel. */
55
+ admin: boolean;
56
+ /**
57
+ * Why this answer, in one sentence. Kept for callers that show a single
58
+ * reason; `why(what)` is the per-control sentence and is what the screens use.
59
+ */
60
+ reason: string;
61
+ }
62
+ /**
63
+ * Kept so a host that imported it still compiles. It is no longer any screen's
64
+ * default: the store answers this question now, and a package that said "I
65
+ * assume nothing" while holding the answer would be hiding controls from the
66
+ * people who hold them.
67
+ */
68
+ declare const NO_RIGHTS: TableRights;
69
+ /**
70
+ * THE ONE WAY TO BUILD A `TableRights` BY HAND — for a host with its own
71
+ * authority, and for a suite that wants to mount one seat's screen. It takes
72
+ * the LEVEL, never six booleans, so nobody can hand a screen a combination the
73
+ * ladder cannot produce (write without read, share without edit).
74
+ */
75
+ declare function tableRightsAt(level: PermissionLevel | null): TableRights;
76
+
77
+ /** What the settings panel can see right now, in the words the agent reads. */
78
+ interface TableSettingsSnapshot {
79
+ table_id: Uuid;
80
+ /** True when the description or an open row action holds edits not saved yet. */
81
+ has_unsaved_changes: boolean;
82
+ table_details_draft: {
83
+ table_name: string;
84
+ description: string;
85
+ row_label: string | null;
86
+ };
87
+ saved_row_actions?: Array<Record<string, unknown>>;
88
+ editing_row_action?: Record<string, unknown>;
89
+ editing_row_action_problems?: string[];
90
+ }
91
+ /** What the host's surface reads and calls. Every call reads the panel as it is at that moment. */
92
+ interface TableSettingsAgent {
93
+ read: () => TableSettingsSnapshot;
94
+ /** Replace the open row action (or open a new one) with a whole action. */
95
+ stageRowAction: (input: unknown) => Promise<void>;
96
+ /** Write one Calculate step's formula in the open update action. */
97
+ stageStepFormula: (input: unknown) => Promise<void>;
98
+ /** Stage the description in "About this table". */
99
+ stageTableDetails: (input: unknown) => Promise<void>;
100
+ }
101
+
102
+ /** What a door answers when it mints or changes a link. The token is the only time it is seen. */
103
+ interface CalendarFeedMade {
104
+ id: string;
105
+ token: string;
106
+ expires_at: string | null;
107
+ }
108
+ /** One line of the person's list. Never a token — `token_prefix` is its first characters. */
109
+ interface CalendarFeedRow {
110
+ id: string;
111
+ title: string;
112
+ organization_id: string;
113
+ table_id: string;
114
+ view_id: string | null;
115
+ token_prefix: string;
116
+ time_zone: string;
117
+ created_at: string;
118
+ expires_at: string | null;
119
+ last_read_at: string | null;
120
+ read_count: number;
121
+ expired: boolean;
122
+ }
123
+ interface CalendarFeedPort {
124
+ create(args: {
125
+ organizationId: string;
126
+ tableId: string;
127
+ viewId: string;
128
+ title: string;
129
+ timeZone: string;
130
+ }): Promise<CalendarFeedMade>;
131
+ rotate(id: string): Promise<CalendarFeedMade>;
132
+ revoke(id: string): Promise<void>;
133
+ list(): Promise<CalendarFeedRow[]>;
134
+ /** The https address a calendar app reads for this token (the host's route). */
135
+ urlFor(token: string): string;
136
+ }
137
+
138
+ /** One named piece of grounding, shaped the way the platform's context slice takes them. */
139
+ interface RecordChatEntry {
140
+ key: string;
141
+ value: unknown;
142
+ label: string;
143
+ /** `text` for a scalar, `json` for a structure. The host maps this to its own vocabulary. */
144
+ type: "text" | "json";
145
+ /**
146
+ * DYN: a merge field resolves to a TRIPLE, not a string. The prompt gets the
147
+ * rendered value; the RUN gets this, and it is the only thing a "where did
148
+ * that come from" question can be answered from — so a host can turn a cited
149
+ * answer into a link to the record AND to the version it was read at.
150
+ */
151
+ cite?: {
152
+ record_id: Uuid$1;
153
+ field_id: Uuid$1;
154
+ value_version: number;
155
+ written_at: string | null;
156
+ };
157
+ }
158
+ /** A Field the agent was NOT given, named rather than silently dropped. */
159
+ interface RecordChatWithheld {
160
+ key: string;
161
+ label: string;
162
+ /** The store's own reason. */
163
+ reason: string;
164
+ /**
165
+ * WHICH ABSENCE THIS IS. `masked` — the store kept it from THIS READER.
166
+ * `policy` — the organization keeps it out of conversations, and the same
167
+ * person can read it on the record two inches away. A screen that collapsed
168
+ * the two would tell somebody the wrong thing about their own access.
169
+ */
170
+ because?: "masked" | "policy";
171
+ /** The whole sentence, already written. A screen prints it; it composes nothing. */
172
+ says?: string;
173
+ }
174
+ /** Everything the host's chat surface needs, and nothing about how it should look. */
175
+ interface RecordChatContext {
176
+ /**
177
+ * A unique key per mounted conversation. Two record panels open at once must
178
+ * not fight over focus, so this is derived from the record and never a
179
+ * constant.
180
+ */
181
+ surfaceKey: string;
182
+ tableId: Uuid$1;
183
+ recordId: Uuid$1;
184
+ tableName: string;
185
+ /** The record's own title, for the panel's one-row header and the chip. */
186
+ title: string;
187
+ /** What the scope chip says: `About: Fairhaven Steelworks`. */
188
+ chip: string;
189
+ /** The grounding, already read through the read door. */
190
+ entries: RecordChatEntry[];
191
+ /** What this reader is NOT being given, with the store's reason for each. */
192
+ withheld: RecordChatWithheld[];
193
+ /** Questions derived from THIS record's shape. Never a fixed list. */
194
+ suggested: string[];
195
+ /** One sentence naming what is not in the conversation. Printed verbatim. */
196
+ says: string;
197
+ /** The whole door answer, for a host that wants the history or the relations. */
198
+ scope: RecordScopeContext;
199
+ }
200
+
201
+ /** What the host is asked to have the SERVER's `records` tool do. */
202
+ interface EnrichAsk {
203
+ tableId: Uuid$1;
204
+ fieldId: Uuid$1;
205
+ fieldKey: string;
206
+ /** How many records this run covers. */
207
+ rows: number;
208
+ /** True: ask the model and report what it WOULD write, writing nothing. */
209
+ previewOnly: boolean;
210
+ }
211
+ interface EnrichOutcome {
212
+ ok: boolean;
213
+ /** What happened, in a sentence — the server's own words when it has some. */
214
+ message: string;
215
+ /** For a preview: what would go in each cell. */
216
+ wouldWrite?: Array<{
217
+ record_id: string;
218
+ value?: unknown;
219
+ absent?: string;
220
+ says?: string;
221
+ }>;
222
+ /** What the run cost, in cents; shown only as points at the host's rate. */
223
+ costCents?: number;
224
+ /** How many values it wrote (0 for a preview). */
225
+ rowsWritten?: number;
226
+ }
227
+
228
+ /** One record as the grid shows it: its id, its name (the row label) and its masked document. */
229
+ interface GridContextRecord {
230
+ id: Uuid;
231
+ title: string;
232
+ document: RecordDocument;
233
+ }
234
+ interface GridContextSnapshot {
235
+ tableId: Uuid;
236
+ tableName: string;
237
+ /** The table's own description (`metadata.description`), when it has one. */
238
+ tableDescription: string | null;
239
+ /** The Field key that names each record (`title_field`), or null. */
240
+ titleField: string | null;
241
+ /** The columns in the order the grid draws them (hidden columns left out). */
242
+ fields: Field[];
243
+ /** The table's row actions as the store lists them (update and agent). */
244
+ rowActions: RowAction[];
245
+ /** May this person change records here at all. */
246
+ canWrite: boolean;
247
+ /** The cell the cursor is on: its record, its Field key and the value drawn in it. */
248
+ currentCell: {
249
+ rowId: Uuid;
250
+ key: string;
251
+ value: unknown;
252
+ } | null;
253
+ /** The record the cursor is on (or whose editor is open). */
254
+ currentRow: GridContextRecord | null;
255
+ /** A block of more than one cell: Excel/Sheets TSV with a header line of Field keys. */
256
+ selectedRange: {
257
+ tsv: string;
258
+ cellCount: number;
259
+ rowIds: Uuid[];
260
+ keys: string[];
261
+ } | null;
262
+ /** The rows ticked with the row boxes. */
263
+ selectedRows: GridContextRecord[];
264
+ /** The page of records on screen, in screen order. */
265
+ visibleRows: GridContextRecord[];
266
+ /** How many records match the search and filters, when the store said. */
267
+ total: number | null;
268
+ search: string;
269
+ }
270
+
271
+ /**
272
+ * The authored-text slots `renderText` is asked to draw. `cell`: a long-text cell of the merged
273
+ * grid and the whole-text dialog's preview — the host may return block content there (the grid
274
+ * clips it to the row); the form slots take phrasing content only.
275
+ */
276
+ type RenderTextSlot = "form-intro" | "form-help" | "form-thank-you" | "cell";
277
+
278
+ /** One reference a chip names (`renderReference`). `record` is a relation's value, read side only. */
279
+ /** What a record page hands the host's rich editor (`editRichText`). */
280
+ interface RichTextEditProps {
281
+ /** The stored Markdown. A later value is the store's echo or a colleague's change. */
282
+ value: string;
283
+ /** Every change the person makes, as the whole Markdown text. Saving is the package's. */
284
+ onChange: (text: string) => void;
285
+ readOnly: boolean;
286
+ placeholder: string;
287
+ }
288
+ /**
289
+ * What a multi-line text box hands the host's own text box (`textBox`): the words and a setter, and
290
+ * what the box is for. The host draws its platform text box around them, with that box's "…" menu.
291
+ */
292
+ interface TextBoxProps {
293
+ value: string;
294
+ /** Every change, as the whole text. Saving stays the package's. */
295
+ onChange: (next: string) => void;
296
+ /** The column's name: the box's accessible name and what its menu calls the text. */
297
+ label: string;
298
+ rows: number;
299
+ /** Where the box sits: an open grid cell, the whole-text editor, or a record form. */
300
+ where: "cell" | "whole-text" | "form";
301
+ id?: string | undefined;
302
+ autoFocus?: boolean | undefined;
303
+ readOnly?: boolean | undefined;
304
+ disabled?: boolean | undefined;
305
+ onKeyDown?: ((event: KeyboardEvent<HTMLTextAreaElement>) => void) | undefined;
306
+ }
307
+ interface ReferenceChipRef {
308
+ token: string;
309
+ id: Uuid;
310
+ label: string | null;
311
+ /** For `token: "record"`, the Table the relation points into. */
312
+ tableId?: Uuid | undefined;
313
+ /**
314
+ * The record it names sits in Archived items. An archive keeps every reference to what it
315
+ * archived (chair ruling B3-21), so the chip still names it and draws the archived state.
316
+ */
317
+ archived?: boolean | undefined;
318
+ /** The record's (or its table's) icon — an emoji or short glyph — when the host knows one. */
319
+ icon?: string | null | undefined;
320
+ }
321
+ /** One line typed into the table's Ask box, with the view the person was looking at ("" = none). */
322
+ interface TableAskRequest {
323
+ tableId: Uuid;
324
+ question: string;
325
+ viewId: string;
326
+ }
327
+ /** What the platform's `table.ask` agent answers: an answer, or edits it proposes for the person to apply. */
328
+ interface TableAskAgentAnswer {
329
+ mode: "answer" | "proposed_edits";
330
+ /** Markdown. */
331
+ answer: string;
332
+ cited_record_ids: string[];
333
+ proposed_changes: Array<{
334
+ record_id: string;
335
+ field_key: string;
336
+ from: unknown;
337
+ to: unknown;
338
+ reason: string;
339
+ }>;
340
+ could_not: string[];
341
+ }
342
+ type TableAskOutcome = {
343
+ ok: true;
344
+ answer: TableAskAgentAnswer;
345
+ /** What the run cost, in whole points at the viewer's rate. Absent or null = not measured; nothing is drawn (never dollars). */
346
+ costPoints?: number | null | undefined;
347
+ }
348
+ /** A refusal says the real reason (`message`) and, when the host knows one, what to do (`hint`); no hint, none is invented. */
349
+ | {
350
+ ok: false;
351
+ message: string;
352
+ hint?: string | undefined;
353
+ };
354
+ /** What `RecordsUiHost.writeSyncedCell` answers. */
355
+ type SyncedCellOutcome = {
356
+ ok: true;
357
+ status: "written";
358
+ version?: number | null;
359
+ } | {
360
+ ok: true;
361
+ status: "conflict";
362
+ sheetValue: string | null;
363
+ } | {
364
+ ok: false;
365
+ message: string;
366
+ };
367
+ interface RecordsUiHost {
368
+ /**
369
+ * OPTIONAL. THE TABLE'S ASK BOX, ANSWERED BY AN AGENT. Bound, the box sends the typed line here and
370
+ * draws the answer, the records it cites and any edits it proposes (each applied by the person,
371
+ * against the record's version). Unbound, the box answers with the Question runner as before.
372
+ */
373
+ askTable?: ((ask: TableAskRequest) => Promise<TableAskOutcome>) | undefined;
374
+ /**
375
+ * OPTIONAL. WRITE-THROUGH FOR A SYNCED CELL (lane SYNC-AIRTABLE, CFL-058). A column the store keeps
376
+ * from a source (`field.source === "synced"`) is written only by that source: bound, an edit of such
377
+ * a cell is sent here (the host's server writes the connected Google Sheet first, then lands it) and
378
+ * answers `written` (with the record's new version) or `conflict` (the sheet changed since the last
379
+ * sync — the cell keeps both and the person picks; Retry sends `force`). A refusal is `{ ok: false }`
380
+ * in the host's own words. Unbound, the store refuses the edit as before.
381
+ */
382
+ writeSyncedCell?: ((cell: {
383
+ tableId: Uuid;
384
+ recordId: Uuid;
385
+ fieldKey: string;
386
+ value: unknown;
387
+ force: boolean;
388
+ }) => Promise<SyncedCellOutcome>) | undefined;
389
+ /**
390
+ * Draws an icon kept as a NAME ("Briefcase" — how the store keeps an icon a person picked from the
391
+ * icon list). A relation cell in an embedded table wears its record's icon; an emoji is drawn as
392
+ * text and a name only through this port. Unbound (or `null` back), the page glyph stands in.
393
+ */
394
+ renderIcon?: ((name: string) => ReactNode) | undefined;
395
+ /**
396
+ * TEMPORARY (one-grid merge, steps 5-8). Which table grid draws: `classic` — the default Grid
397
+ * as it shipped in 0.86; `merged` — the same grid with the older /data grid's control layer on
398
+ * it (the column header menu, the right-click Cell / Row / Column / Table sections, column
399
+ * settings, column order and hidden columns kept on the view). Left out: `classic`. Removed at
400
+ * merge step 9, when `merged` is the only grid.
401
+ */
402
+ grid?: "classic" | "merged";
403
+ /**
404
+ * OPTIONAL. CLEAN A PASTED PIECE OF HTML INTO PLAIN TEXT (merge tranche 6m, inventory A7).
405
+ *
406
+ * A cell pasted from a web page or an email carries its tags, entities and inline styles. The
407
+ * platform has ONE cleaner (the host's content-cleanup), so this is a port rather than a second
408
+ * one here. The merged grid offers "Clean HTML" in a text cell's right-click Cell section and in
409
+ * the whole-text editor; the cleaned text is written through the grid's own one-cell write (one
410
+ * undo step). Unbound, the item is ABSENT, never dead.
411
+ */
412
+ cleanText?: (text: string) => string;
413
+ /**
414
+ * OPTIONAL. THE PRIVATE CALENDAR LINK (CAL-FEED-UI): the host's create / rotate / revoke / list doors
415
+ * and the address its feed route answers at. Bound, a view with a date field offers "Subscribe in
416
+ * your calendar…" in the table's menu. Unbound, the entry is ABSENT, never dead.
417
+ */
418
+ calendarFeed?: CalendarFeedPort;
419
+ /**
420
+ * OPTIONAL. WHERE THE PERSON IS IN THE GRID, told as it changes (merge tranche 6l, inventory H1).
421
+ *
422
+ * An agent in the host's side chat is asked about "this cell", "these rows", "the table". Only
423
+ * the grid knows the table, its columns, the cell the cursor is on, the selected block, the
424
+ * ticked rows, the rows on screen and the search. The merged grid calls this after any of them
425
+ * changes (identical snapshots are not repeated); the host builds its agent scope from it. The
426
+ * snapshot carries the store's own `Field` records and the masked documents exactly as drawn.
427
+ * Unbound, nothing is built.
428
+ */
429
+ onGridContext?: (snapshot: GridContextSnapshot) => void;
430
+ /**
431
+ * OPTIONAL. THE PLATFORM'S ONE ICON PICKER (merge tranche 6j, inventory G2). A row action's
432
+ * button may carry an icon from the canonical icon set; that set and its picker are the host's
433
+ * (this package carries no icon library). Unbound, the row-action editor draws no icon control
434
+ * and keeps whatever icon an action already carries.
435
+ */
436
+ iconPicker?: (props: {
437
+ value: string;
438
+ label: string;
439
+ onChange: (name: string) => void;
440
+ }) => ReactNode;
441
+ /**
442
+ * OPTIONAL. HELP WRITING A FORMULA (Sheet retirement). The formula box hands the host what it
443
+ * holds — the text, the table's other columns and the store's reading of what is wrong — and the
444
+ * host draws its own help control beside the box (matrx-frontend runs its formula-writing job).
445
+ * Whatever formula the help arrives at goes back through `onFormula` into the box, unsaved, for
446
+ * the person to keep or change. Unbound, nothing is drawn.
447
+ */
448
+ formulaHelp?: (ask: FormulaHelpAsk) => ReactNode;
449
+ /**
450
+ * OPTIONAL. THE PLATFORM'S TEXT BOX FOR LONG TEXT (Sheet retirement). The Sheet edited a text cell
451
+ * in the platform's one text box, whose "…" menu offers Clean up, Help with this…, Copy, Save to
452
+ * notes and Share. Bound, the long-text cell editor, the whole-text editor and a record form's
453
+ * long-text field draw the host's box with these props (one "…", no other chrome). Unbound, the
454
+ * package's plain box.
455
+ */
456
+ textBox?: (props: TextBoxProps) => ReactNode;
457
+ /**
458
+ * OPTIONAL. AN AGENT OVER A TABLE'S SETTINGS (Sheet retirement). While the settings panel is open
459
+ * it wraps itself in what this returns, handing the host a `TableSettingsAgent`: what the panel
460
+ * can see (the description as edited, the row actions and the one being edited, what stops it
461
+ * saving) and three staging calls (a whole row action, one step's formula, the description).
462
+ * Staging never saves; the person presses Save. Unbound, the panel draws bare.
463
+ */
464
+ settingsAgent?: (args: {
465
+ tableId: Uuid;
466
+ agent: TableSettingsAgent;
467
+ children: ReactNode;
468
+ }) => ReactNode;
469
+ /** Answer the rights question for one table. Synchronous: a screen renders now. */
470
+ rights?: (table: Table) => TableRights;
471
+ /** Where a record's own page lives, when the host has one. A peek is used when it does not. */
472
+ hrefForRecord?: (args: {
473
+ table: Table;
474
+ recordId: Uuid;
475
+ }) => string | undefined;
476
+ /**
477
+ * OPTIONAL. WHERE A FILE OPENS. A value too big for one cell is kept as a file and the cell
478
+ * holds its first words (`@ai-matrx/records/core` `wholeValueOf`); this is the host's page for
479
+ * that file. Unbound, the cell shows the words and says the whole text is kept as a file this
480
+ * screen cannot open — never a dead link.
481
+ */
482
+ hrefForFile?: (args: {
483
+ fileId: Uuid;
484
+ }) => string | undefined;
485
+ /**
486
+ * OPTIONAL. READ A FILE'S WHOLE TEXT, so an export writes a big value's whole content rather
487
+ * than its first words. Unbound, the exported cell carries the words and one sentence naming
488
+ * the file (`wholeValueNotRead`) — never the words passed off as the value.
489
+ */
490
+ readFileText?: (args: {
491
+ fileId: Uuid;
492
+ }) => Promise<string>;
493
+ /**
494
+ * OPTIONAL. A FILE DRAWN AS A PICTURE — the gallery card's cover when the view's picture column
495
+ * is a File column (lane DATA-V2-VIEWS-1). The host owns how a file becomes pixels (its media
496
+ * lane, its auth), so this is a port: it returns the element that fills the cover box. Unbound,
497
+ * the cover says in words that this screen cannot draw files, never a broken image.
498
+ */
499
+ fileImage?: (args: {
500
+ fileId: Uuid;
501
+ alt: string;
502
+ }) => ReactNode;
503
+ /**
504
+ * OPTIONAL. HOW AUTHORED TEXT READS. A form's intro, a question's help and the thank-you body
505
+ * are written by a person and may carry markdown and math (`**bold**`, `\\(x^2\\)`, links).
506
+ * This package does not parse markdown — the host owns its renderer — so this is a port: the
507
+ * host returns phrasing content (valid inside a `<p>`). `where` names the slot. Unbound, the
508
+ * text shows exactly as written.
509
+ */
510
+ renderText?: (text: string, where: RenderTextSlot) => ReactNode;
511
+ /**
512
+ * OPTIONAL. ONE REFERENCE, DRAWN AS A CHIP THAT OPENS WHAT IT NAMES (lane REFERENCE-CARRY).
513
+ *
514
+ * Every reference this package shows — a relation to a record (`token: "record"`, with the
515
+ * Table it points into), an entity reference to a platform thing (`note`, `agent`, `workflow`…)
516
+ * — is drawn through this port, so a person can always reach the thing (the host owns routes,
517
+ * peeks and new tabs). `label` is the store's words, or null when they were withheld from this
518
+ * reader. Unbound, the chip names the thing and opens nothing.
519
+ */
520
+ renderReference?: (ref: ReferenceChipRef) => ReactNode;
521
+ /**
522
+ * OPTIONAL. A VALUE THAT NAMES ITS OWN SHAPE, DRAWN THROUGH THE HOST'S KIND SYSTEM (lane
523
+ * REFERENCE-CARRY). A cell holding `__kind` JSON — or a Matrx directive stored before the Kind
524
+ * Directives merge (```matrx {"matrx_version":1,…}```) — is handed over whole (`kindValues.ts`);
525
+ * the host decodes it with its one decoder and returns a compact rendering for `where` (a cell,
526
+ * a panel row) that opens the real view. Unbound, the cell names the kind and shows the raw text
527
+ * on hover — never the fence at eye level.
528
+ */
529
+ renderKind?: (args: {
530
+ value: Record<string, unknown>;
531
+ kind: string;
532
+ where: "cell" | "panel";
533
+ title: string;
534
+ }) => ReactNode;
535
+ /** The host's own link element, so routing stays the host's business. */
536
+ Link?: (props: {
537
+ href: string;
538
+ children: ReactNode;
539
+ className?: string;
540
+ }) => ReactNode;
541
+ /** Toasts. Absent means the screen shows the sentence inline instead — never silence. */
542
+ notify?: {
543
+ /** `action`: one button on the toast (an Undo). A host without actions draws the sentence alone. */
544
+ success: (message: string, action?: {
545
+ label: string;
546
+ run: () => void;
547
+ }) => void;
548
+ error: (message: string) => void;
549
+ /**
550
+ * OPTIONAL. THE REVERSIBLE ACTION (`@ai-matrx/kit/reversible`): something this package did
551
+ * that can be undone — a table archived — handed to the host's one announcer, which acts it
552
+ * out at the person's level (taught the first time, a long Undo toast the next few, an ordinary
553
+ * one after) and answers ⌘Z. The package says WHAT happened and HOW to undo it; where the
554
+ * thing went (`foundAt`) is the host's routes. Unbound, the package falls back to `success`
555
+ * with an Undo button.
556
+ */
557
+ reversible?: (announcement: ReversibleAnnouncement) => void;
558
+ };
559
+ /** Density for every table this package renders. */
560
+ density?: "condensed" | "normal" | "spacious";
561
+ /**
562
+ * OPTIONAL. WHETHER A WITHHELD CONTROL SAYS WHAT LEVEL IT TAKES ("You are Viewer here, and it takes
563
+ * Editor to … Ask somebody who is Admin on it to move you up."). Default `true`. A host whose
564
+ * person has no level to be moved up to — a template preview, a public look at a table — sets
565
+ * `false`: the control stays absent exactly as before, without the sentence. It changes words
566
+ * only, never what is offered.
567
+ */
568
+ sayLevelNeeded?: boolean;
569
+ /**
570
+ * OPTIONAL. Where a file a person attached actually goes. A File is a record
571
+ * in the kernel File Table (REC-31), but the BYTES are the host's file
572
+ * service, not this package's business — so this is a port.
573
+ *
574
+ * Unbound, an attachment question does NOT draw a file chooser that quietly
575
+ * loses the file: it says, in one sentence, that no file store is bound and
576
+ * names this port. Absent or honest, never dead.
577
+ */
578
+ upload?: (file: File) => Promise<{
579
+ ok: true;
580
+ fileId: Uuid;
581
+ } | {
582
+ ok: false;
583
+ reason: string;
584
+ }>;
585
+ /**
586
+ * OPTIONAL. THE HOST'S OWN FILE WINDOW (one-grid merge, inventory: the older grid's
587
+ * AttachmentInput). An attachment cell adds files the person already holds through the host's
588
+ * file picker — Airtable's "Attach" over your own files — rather than a second browser this
589
+ * package would invent. `multiple`: the column holds several files; `accept`: the native
590
+ * `accept` list, when the column restricts types. Resolves the host's file ids (the store turns
591
+ * each into its kernel File record at the write door), or `null` when the person cancelled.
592
+ *
593
+ * Unbound, the editor offers `upload` alone; with neither bound it says no file store is bound
594
+ * (`NO_UPLOAD_REASON`) — never a dead button.
595
+ */
596
+ pickFiles?: (ask: {
597
+ multiple: boolean;
598
+ accept?: string;
599
+ }) => Promise<Uuid[] | null>;
600
+ /**
601
+ * Where this app serves its PUBLIC pages from, when that is not the origin
602
+ * the screen is running on. A form's link is built here, so an owner copying
603
+ * it from an admin host, a preview deployment or a desktop shell gets the
604
+ * address a stranger can actually open — never `http://localhost:3000/f/…`
605
+ * pasted into an email. Unbound, the browser's own origin is used.
606
+ */
607
+ publicOrigin?: string;
608
+ /**
609
+ * OPTIONAL. The organization's saved views as `platform.saved_view` holds
610
+ * them — which is what a subscription points at (DOOR-18). The record store
611
+ * has NO door onto that table, because it is the platform's and not the
612
+ * store's, so this is a port rather than a read this package invents.
613
+ *
614
+ * Unbound, `NotifyRuleEditor` shows the subscriptions that exist and says, in
615
+ * one sentence, that it cannot offer a view to subscribe to. It never renders
616
+ * an empty picker that looks like "you have no views".
617
+ */
618
+ savedViews?: () => Promise<Array<{
619
+ id: Uuid;
620
+ name: string;
621
+ }>>;
622
+ /**
623
+ * OPTIONAL. WHO IS IN THIS ORGANIZATION (FLD-11 `member`).
624
+ *
625
+ * A person field must offer PEOPLE. Who the people are is the platform's
626
+ * answer — organization membership lives in `iam`, not in the record store —
627
+ * and the store has no door onto it, so this is a port rather than a read
628
+ * this package invents. `PersonPicker` resolves whoever is picked to their
629
+ * record in the kernel Person Table, which is what the Field actually stores.
630
+ *
631
+ * Unbound, a person question is ABSENT with `NO_MEMBERS_REASON`. It never
632
+ * falls back to listing the kernel Person Table's contents: that Table also
633
+ * holds the Homes this package writes, and offering "Comments Home" as a
634
+ * person is exactly what this port exists to stop.
635
+ */
636
+ members?: () => Promise<OrganizationMember[]>;
637
+ /**
638
+ * OPTIONAL. THE HOST'S OWN CHAT SURFACE (SCR-26).
639
+ *
640
+ * This package ships no chat UI, on purpose: AI Matrx has exactly one, it
641
+ * lives in matrx-frontend (`AgentConversationColumn`), and it reads that app's
642
+ * Redux store — so a second one here would drift within a month and a package
643
+ * that imported the first would stop being a package. `RecordChat` therefore
644
+ * builds the CONTEXT (the record, through the read door, with the masked
645
+ * fields named) and hands it to this port.
646
+ *
647
+ * Unbound, the panel is ABSENT with that reason, never a composer that goes
648
+ * nowhere.
649
+ */
650
+ chat?: (context: RecordChatContext) => ReactNode;
651
+ /**
652
+ * OPTIONAL. MORE SECTIONS ON AN OPEN RECORD — drawn in the record rail after its history,
653
+ * one host-owned block about THIS record (e.g. "What ran on this record": the Workflow runs
654
+ * stamped with it). Unbound, nothing is drawn.
655
+ */
656
+ recordSections?: (target: {
657
+ tableId: Uuid;
658
+ recordId: Uuid;
659
+ }) => ReactNode;
660
+ /**
661
+ * OPTIONAL. THE PLATFORM'S ONE RICH EDITOR, for a record page's body (lane TABLE-EXPERIENCE,
662
+ * v7 item 1: a record is a page). The host draws its own editor — matrx-frontend's notes editor —
663
+ * reporting every change through `onChange`; this package owns the saving. Unbound, the body is
664
+ * drawn read-only and is edited through the record's form.
665
+ */
666
+ editRichText?: (props: RichTextEditProps) => ReactNode;
667
+ /**
668
+ * OPTIONAL. THE HOST DRAWS A RECORD PAGE'S WHOLE BODY (the "Spaces" page layer's block editor).
669
+ * When bound and it returns non-null, the record page draws that in place of the package's own
670
+ * body (the formatted-text column editor and its "Write on this page" offer); the host owns
671
+ * saving. Returning `null` (or leaving it unbound) draws today's body unchanged.
672
+ */
673
+ renderRecordBody?: (args: {
674
+ tableId: string;
675
+ recordId: string;
676
+ record: RecordRead;
677
+ readOnly: boolean;
678
+ /** Today's body as an element — return it (or null) where the host has no body of its own. */
679
+ fallback: ReactNode;
680
+ }) => ReactNode | null;
681
+ /**
682
+ * OPTIONAL. "LINK A RECORD…" — the HOST opens its one record picker pointed at this record and
683
+ * writes the link through its own reference door; this package draws only the menu entry (a
684
+ * card's ⋯ and right-click, the grid row's right-click). Unbound, the entry says it is not
685
+ * available here.
686
+ */
687
+ linkRecord?: (target: {
688
+ tableId: Uuid;
689
+ recordId: Uuid;
690
+ name: string;
691
+ }) => void;
692
+ /**
693
+ * OPTIONAL. "TALK TO THIS RECORD" — the HOST opens its own one chat surface
694
+ * (AGT-N-9, PRODUCTS row 11).
695
+ *
696
+ * `chat` is the port that RENDERS a chat inside a panel this package draws.
697
+ * This one is the other direction: a menu entry in the grid, the cell menu or
698
+ * the record header says "the person wants to talk about this record", and the
699
+ * host opens the chat where the host puts chats — full height, the platform's
700
+ * own pattern — and binds the conversation to the record through
701
+ * `conversationScopeBind`. Drawing a chat next to a grid row from inside this
702
+ * package would be the parallel layer the canvas ruling forbids.
703
+ *
704
+ * ABSENT, NEVER DEAD: where neither this nor `chat` is bound, the entries do
705
+ * not render at all.
706
+ */
707
+ talkToRecord?: (target: {
708
+ tableId: Uuid;
709
+ recordId: Uuid;
710
+ title: string;
711
+ }) => void;
712
+ /**
713
+ * OPTIONAL. A TABLE'S AGENT BUTTON, PRESSED (TABLE-PARITY M3, lane DEFAULT-GRID-PARITY).
714
+ *
715
+ * A row action of kind `agent` ("Ask Dr. Whitfield") hands the row to an agent with the
716
+ * action's own `prompt`. Only the host can start that conversation — this package ships no
717
+ * chat — so the grid hands the row, the action's name and its prompt here, and the host
718
+ * opens its one chat surface bound to the record with that prompt as the opening ask. It is
719
+ * a separate port from `talkToRecord` because that one carries no prompt: a host that bound
720
+ * only `talkToRecord` would open an empty chat and silently drop what the button was for.
721
+ *
722
+ * ABSENT, NEVER DEAD: unbound, agent buttons are not drawn.
723
+ *
724
+ * NO SECOND READ (lane GRID-TAILS, 2026-09-24). The grid already holds the table, its
725
+ * columns, the row as this reader may see it and this reader's level on the row, so it hands
726
+ * them over: a host needs no `tableList`, `fields`, `recordRead` or `myLevels` to start the
727
+ * job, and the agent is handed exactly what the person was looking at.
728
+ */
729
+ runAgentAction?: (target: RowAgentTarget) => void;
730
+ /**
731
+ * OPTIONAL. THE AI COLUMN'S RUNNER (SCR-27, custom-data PLAN §4 M2).
732
+ *
733
+ * An AI fill must be written with actor `agent` on behalf of the person, and
734
+ * only the server can honestly say that — a browser stamping itself `agent`
735
+ * is a browser claiming to be one. So `EnrichPanel` asks here, and the host
736
+ * calls the server's one enrichment runner (`POST /v1/tables/{table}/ai-columns/{field}/run`,
737
+ * model = the `table.column_fill` mandate), which lands each value through
738
+ * `custom.enrich_land` as `agent` on the person's behalf.
739
+ *
740
+ * Unbound, no Fill control is drawn at all and the reason names this port.
741
+ */
742
+ enrich?: (ask: EnrichAsk) => Promise<EnrichOutcome>;
743
+ /**
744
+ * OPTIONAL. POINTS PER DOLLAR — the platform's rate. Every cost these screens show is
745
+ * points at this rate (`costInPoints`); unbound, no cost is shown, never the vendor's money.
746
+ */
747
+ pointsRate?: number | null;
748
+ /**
749
+ * OPTIONAL. WHERE AN OFFLINE CAPTURE WAITS (SCR-30).
750
+ *
751
+ * `CaptureSheet` mints the id for a capture BEFORE it tries to send it, so a
752
+ * phone with no signal has already decided what the write will be called.
753
+ * Where that queue physically lives is the host's business — IndexedDB in a
754
+ * browser, a file in a desktop app — so it is a port. Unbound, the sheet
755
+ * keeps its queue in memory for the life of the page AND SAYS SO, because a
756
+ * queue that silently died with the tab is the failure this exists to avoid.
757
+ */
758
+ captureQueue?: CaptureQueuePort;
759
+ /**
760
+ * OPTIONAL. THE HOST'S OWN SHARE DIALOG (SCR-N-5).
761
+ *
762
+ * This package ships no share dialog, ON PURPOSE, and for the same reason it
763
+ * ships no chat: AI Matrx has exactly ONE sharing surface — matrx-frontend's
764
+ * `features/sharing` ShareModal, with its people picker, its level picker, its
765
+ * grant list and its "who can see this, and why" panel — and a second one here
766
+ * would be a parallel sharing UI within a month. Sharing is the access model's
767
+ * front door, and two front doors is how a platform ends up with two answers
768
+ * to "who can see this".
769
+ *
770
+ * So `ShareControl` draws the BUTTON (which belongs on the record and table
771
+ * screens, beside everything else a person does to a thing) and hands the
772
+ * subject to this port, which returns the host's own dialog.
773
+ *
774
+ * Unbound, the button is ABSENT and `NO_SHARE_REASON` says which port to bind.
775
+ * Never a Share button that opens nothing.
776
+ */
777
+ share?: (subject: ShareSubject) => ReactNode;
778
+ /**
779
+ * OPTIONAL. WHO CAN SEE AND EDIT ONE COLUMN (access-setup PLAN §12, Store Tables). The column header's
780
+ * "Who can see and edit…" item hands the column to this port, which returns the host's People involved
781
+ * panel (it says who loses sight before anything is saved; table admins change it, never an agent).
782
+ * Unbound, the item is ABSENT. The grid owns open/closed; the host owns the panel.
783
+ */
784
+ columnAccess?: (subject: ColumnAccessSubject) => ReactNode;
785
+ /**
786
+ * OPTIONAL. THE ARCHIVED-ORGANIZATION DOOR (lane ARCHIVED-ORG-RESTORE). When the store refuses because the
787
+ * organization is archived, `RefusalNotice` says so and asks this port two things: `state` - may THIS person
788
+ * restore it (an owner or a super admin) - and `restore`, which calls the platform's one restore door. Left
789
+ * unbound, the notice still says the organization is archived and who can restore it, and offers no button.
790
+ */
791
+ archivedOrganization?: {
792
+ state: (organizationId: Uuid) => Promise<{
793
+ mayRestore: boolean;
794
+ name?: string | null;
795
+ } | null>;
796
+ restore: (organizationId: Uuid) => Promise<void>;
797
+ /** Runs after a restore so the screen reloads; unbound, the page reloads. */
798
+ afterRestore?: () => void;
799
+ };
800
+ /**
801
+ * APPLETS ON A PAGE (v7 APPS-ON-DATA item 3). The host's applet system: which applets this person
802
+ * may place (`list`), and the applet itself drawn in place (`render`). Unbound, a page offers no
803
+ * Applet block and an applet block already on the page says it opens inside the app.
804
+ */
805
+ applets?: {
806
+ list: () => Promise<ReadonlyArray<{
807
+ id: string;
808
+ name: string;
809
+ }>>;
810
+ render: (appId: string) => ReactNode;
811
+ };
812
+ /**
813
+ * OPTIONAL. OPEN THE RECORDS BEHIND A NUMBER (SCR-16).
814
+ *
815
+ * Every number on a dashboard is a count of real records, and the whole
816
+ * product row is that a person can get from the number to them: click the
817
+ * "Awaiting parts" column and the grid opens on the twenty-eight jobs it
818
+ * counted, with the block's own filter still on. That is one navigation, and
819
+ * WHERE the grid lives is the host's routing — so it is a port, like
820
+ * `hrefForRecord`, rather than a route string this package invents.
821
+ *
822
+ * Unbound, the marks are NOT clickable and the canvas says so in one
823
+ * sentence naming this port. A chart whose bars looked pressable and did
824
+ * nothing is the dead control this package does not ship.
825
+ */
826
+ openRecords?: (ask: OpenRecordsAsk) => void;
827
+ /**
828
+ * OPTIONAL. WHERE A PAGE OPENS (v6 lane 11, wave D). A Page is a dashboard record whose
829
+ * `presentation.kind` is `"page"` (`pages.ts`); the host owns its route, so this hands back the
830
+ * address of an existing page, or of the screen that makes a new one on a Table. Unbound, the
831
+ * Dashboards area lists no pages and offers no "New page" — absent, never dead.
832
+ */
833
+ pageHref?: (target: {
834
+ pageId: Uuid;
835
+ } | {
836
+ newOnTable: Uuid;
837
+ }) => string;
838
+ /**
839
+ * OPTIONAL. ASK THE AGENT TO CHANGE THIS DASHBOARD (SCR-16).
840
+ *
841
+ * "break it down by owner" is how a dashboard is actually built — the agent
842
+ * calls `dashboard_propose` on the server's `records` tool with this
843
+ * dashboard's id and re-states it. Only the server can do that honestly (a
844
+ * browser stamping itself `agent` is a browser claiming to be one), which is
845
+ * the same reason `enrich` is a port, so this is one too.
846
+ *
847
+ * Unbound, the box is drawn DISABLED with one sentence naming this port —
848
+ * never a composer that goes nowhere and never silence.
849
+ */
850
+ onReask?: (question: string, context: ReaskContext) => Promise<void>;
851
+ /**
852
+ * OPTIONAL. ASK AN AGENT FOR A WHOLE FORM, BOOKING PAGE, PORTAL OR DIGEST.
853
+ *
854
+ * This is the FASTER of the two ways every builder offers (`BuildOrAsk`), and
855
+ * it is a port for the same reason `onReask` is: the server's `records` tool
856
+ * is what calls `form_propose`, `booking_propose`, `portal_propose` and
857
+ * `subscription_propose`, and a browser stamping itself `agent` would be a
858
+ * browser claiming to be one.
859
+ *
860
+ * `suggestion` is the sentence this package would put in the person's mouth —
861
+ * a host normally opens its composer with it already typed, so the person
862
+ * edits rather than starts.
863
+ *
864
+ * Unbound, the "Ask an agent" button is ABSENT — not greyed — and every
865
+ * builder's empty state says so in one sentence naming this port, with the
866
+ * hand-built way still right there. The product walk of 2026-09-21 found the
867
+ * opposite arrangement (the agent named and no builder at all), which left a
868
+ * person who did not know what to say with nothing to press.
869
+ */
870
+ onAskForOne?: (ask: AgentBuildAsk) => void;
871
+ }
872
+ /** What the formula box hands the host's help (`formulaHelp`). */
873
+ interface FormulaHelpAsk {
874
+ tableId: Uuid;
875
+ /** The formula as it stands in the box (may be empty). */
876
+ text: string;
877
+ /** The table's other columns, the ones the formula may read as {Name}. */
878
+ columns: readonly Field[];
879
+ /** The store's reading of what is wrong with the text, when it does not parse. */
880
+ mistake: string | null;
881
+ /** The functions the store works out, as a person types them. */
882
+ functions: readonly string[];
883
+ /** Put a formula into the box (unsaved). */
884
+ onFormula: (text: string) => void;
885
+ /**
886
+ * What the formula is for, in a person's words, when it is not a formula column: a row action's
887
+ * Work out step says which column it sets. Absent, it is a formula column.
888
+ */
889
+ purpose?: string | undefined;
890
+ }
891
+ /** What a person is asking an agent to make, and where they were standing. */
892
+ interface AgentBuildAsk {
893
+ /**
894
+ * `column`: a layout needs a column the table lacks (`ViewNeeds`, lane DATA-V2-VIEWS-1) — the
895
+ * agent adds it through the store's field door, held for approval like every agent schema change.
896
+ */
897
+ kind: "form" | "booking" | "portal" | "digest" | "column";
898
+ tableId: Uuid;
899
+ /** The sentence this package would say. A host opens its composer with it. */
900
+ suggestion: string;
901
+ }
902
+ /**
903
+ * WHAT A CLICK ON A NUMBER ASKS FOR. The filter is already merged: the block's
904
+ * own filter (which `custom.dashboard_run` echoes back with the canvas bar
905
+ * folded under it) with the clicked group's key and value over the top. A host
906
+ * that dropped a key would open the grid on more records than the number
907
+ * counted, which is the one way this control can lie.
908
+ */
909
+ interface OpenRecordsAsk {
910
+ tableId: Uuid;
911
+ filter: Record<string, AggregateFilter>;
912
+ /** What the person clicked, in words: "Jobs by stage this month · Awaiting parts". */
913
+ label: string;
914
+ }
915
+ /** Which dashboard a re-ask is about, so the agent re-states it rather than making a second one. */
916
+ interface ReaskContext {
917
+ tableId: Uuid;
918
+ /** Null while the canvas has no dashboard yet — the answer is the first one. */
919
+ dashboardId: Uuid | null;
920
+ dashboardName: string | null;
921
+ }
922
+ /**
923
+ * What a host's share dialog is being opened ON. A Table is a record in this
924
+ * store (a `custom.record` row whose table is the Table kernel), so ONE subject
925
+ * shape covers both screens and `kind` is what the sentences say, not what the
926
+ * store looks up.
927
+ */
928
+ interface ShareSubject {
929
+ kind: "record" | "table";
930
+ /** The store is keyed (organization_id, id); this package never guesses one. */
931
+ organizationId: Uuid;
932
+ subjectId: Uuid;
933
+ /** What to call it on screen. Never empty — the id's first eight characters at worst. */
934
+ name: string;
935
+ /** Close the dialog. The control owns the open/closed state, the host owns the dialog. */
936
+ onClose: () => void;
937
+ }
938
+ /** One column, handed to the host's "Who can see and edit" panel. */
939
+ interface ColumnAccessSubject {
940
+ tableId: Uuid;
941
+ fieldId: Uuid;
942
+ /** The column's name as the grid shows it. */
943
+ name: string;
944
+ /** Close the panel. */
945
+ onClose: () => void;
946
+ /** Called after the panel saved a change, so the grid re-reads the Table's columns. */
947
+ onChanged: () => void;
948
+ }
949
+ /** One member of the organization, in the words a person reads. */
950
+ interface OrganizationMember {
951
+ /** The platform's user id. This is what a Person record carries as `user_id`. */
952
+ userId: string;
953
+ /** Their display name, when the platform has one. */
954
+ name?: string | null;
955
+ email?: string | null;
956
+ avatarUrl?: string | null;
957
+ }
958
+ /** The durable half of offline capture: read the queue, write it, that is all. */
959
+ interface CaptureQueuePort {
960
+ load: () => Promise<PendingCapture[]>;
961
+ save: (pending: PendingCapture[]) => Promise<void>;
962
+ }
963
+ /** One capture waiting to be written, with the id the CLIENT minted for it. */
964
+ interface PendingCapture {
965
+ /**
966
+ * The client key. Minted offline, before the first attempt, and never
967
+ * re-minted on retry — that is exactly what lets the store's replay ledger
968
+ * tell a retry from a second capture.
969
+ */
970
+ clientKey: string;
971
+ tableId: Uuid;
972
+ payload: Record<string, unknown>;
973
+ capturedAt: string;
974
+ device: string;
975
+ /** How many times sending has been tried, and what the store last said. */
976
+ attempts: number;
977
+ lastRefusal?: string | null;
978
+ }
979
+
980
+ /** The name of one Field, as a header, a form label or a peek's term. */
981
+ declare function fieldName(field: Field): string;
982
+ /** The name of one Table, as a card, a heading or a breadcrumb. */
983
+ declare function tableName(table: Table): string;
984
+ /**
985
+ * The name of one RECORD, for a chip, a card, a relation cell or a peek title.
986
+ *
987
+ * `titleKey` is the Table's own `title_field` when the caller knows the Table —
988
+ * that is the store's answer to "what makes this record a chip" (REC-2) — and
989
+ * the conventional keys are tried after it. A record with nothing readable is
990
+ * NOT shown as a truncated uuid: it says it has no name yet, because "8f2c1b0a"
991
+ * tells a person nothing and looks like a bug.
992
+ */
993
+ declare function recordName(document: RecordDocument | undefined, titleKey?: string | null, fallback?: string): string;
994
+ /**
995
+ * Is this string the store's id for something, rather than words a person wrote?
996
+ *
997
+ * The same question `custom._card_words` asks on the database, with the same regex, so the
998
+ * screen and the store agree about what is a name (TAILS-2, 2026-09-21).
999
+ */
1000
+ declare function looksLikeId(value: unknown): boolean;
1001
+ /** The same question for a row the read door returned. */
1002
+ declare function rowName(row: ReadRow | undefined, titleKey?: string | null, fallback?: string): string;
1003
+ /** A Table as far as naming is concerned — its own declared title column (REC-2). */
1004
+ type TitledTable = Pick<Table, "title_field"> | null | undefined;
1005
+ /**
1006
+ * THE NAME OF A RECORD WHOSE TABLE YOU ALREADY HOLD.
1007
+ *
1008
+ * `table` is the Table the record belongs to; its `title_field` is the store's answer to
1009
+ * "what makes this record a chip" (REC-2) and is tried first, then the conventional keys,
1010
+ * then the fallback. A Table that has not loaded yet is `null` and the conventional keys
1011
+ * still answer — a name arriving a tick late is better than a screen that shows nothing.
1012
+ */
1013
+ declare function recordNameIn(table: TitledTable, document: RecordDocument | undefined, fallback?: string): string;
1014
+ /** The same, for a row the read door returned. */
1015
+ declare function rowNameIn(table: TitledTable, row: ReadRow | undefined, fallback?: string): string;
1016
+ /**
1017
+ * WHAT A RELATION CHIP SAYS WHEN THE RECORD IT POINTS AT IS NOT YOURS.
1018
+ *
1019
+ * A relation joins two records in two tables, and holding one end says nothing
1020
+ * about the other: a Job shared with you may point at a Client nobody shared.
1021
+ * The chip then has three possible answers and only one of them is honest —
1022
+ * the target's TITLE is a leak, a bare id is a bug with a hex number in it, and
1023
+ * a permanent "Loading…" is a lie that never resolves.
1024
+ *
1025
+ * This is the SAME sentence `platform.relation_withheld_label()` answers on the
1026
+ * database, word for word, so a person reads one wording whether the screen or
1027
+ * the store worked it out. Changing it changes both (RELATION-DECLARE,
1028
+ * 2026-09-20).
1029
+ */
1030
+ declare const WITHHELD_RECORD_LABEL = "A record you have not been given access to";
1031
+ /**
1032
+ * WHEN THE STORE COULD NOT BE ASKED (a 503, a dropped connection) the name is UNREAD, not
1033
+ * withheld: saying "not given access" for a failed read lies about the data (W1.1 verifier,
1034
+ * 2026-10-02). This is what a reference reads until a later read answers.
1035
+ */
1036
+ declare const UNREAD_RECORD_LABEL = "Couldn't load this name";
1037
+ /**
1038
+ * ONE RECORD'S NOUN, IN A SENTENCE ("Book a 30-minute visit") — lane HANDOVER, 2026-09-28.
1039
+ *
1040
+ * Cedar Ridge Physical Therapy's booking page was titled "Book a 30-minute Visits appointment":
1041
+ * the table's NAME (a plural, a heading) was put where one record's noun goes. The noun is the
1042
+ * table's own singular ("Visit"), else its name, else the fallback; a Title-case word is written
1043
+ * the way a sentence writes it ("visit"), an acronym is kept ("MRI scan").
1044
+ */
1045
+ declare function singularNoun(table: {
1046
+ name?: string | null;
1047
+ label_singular?: string | null;
1048
+ } | null | undefined, fallback?: string): string;
1049
+ /**
1050
+ * THE FIELD A CARD IS TITLED BY — kanban, calendar, gallery and pipeline alike.
1051
+ *
1052
+ * Order: the Table's `title_field` → a conventional name key (`title`, `name`, …) → the first
1053
+ * text column in declared `sort` order → the first other column that can title (never a
1054
+ * checkbox, never the group column, never a system stamp). `undefined` when nothing can, and
1055
+ * the card falls back to `rowName`, which says "Untitled" rather than "false".
1056
+ */
1057
+ declare function cardTitleField(fields: readonly Field[], titleKey: string | null | undefined, exclude?: readonly (string | null | undefined)[]): Field | undefined;
1058
+
1059
+ /** What the roster can tell us about a user id. `null` when it cannot. */
1060
+ type ResolveName = (userId: Uuid) => string | null;
1061
+ /** The honest stand-in when nobody and nothing can put a name to the author. */
1062
+ declare const SOMEBODY = "somebody in this organization";
1063
+ /** The platform's own word for itself. Never "null", never "system_user". */
1064
+ declare const THE_SYSTEM = "the system";
1065
+ /**
1066
+ * WHO, in the words a person reads. One place, so the grid cell, the record
1067
+ * timeline and the column history cannot drift into three different answers.
1068
+ */
1069
+ declare function actorWords(actor: HistoryActor, resolve?: ResolveName | undefined): string;
1070
+ /** How long ago, in the words a person uses out loud. */
1071
+ declare function whenWords(at: string, now?: Date): string;
1072
+ declare function lastChangeSentence(fieldLabel: string, entry: FieldHistoryEntry | null, resolve?: ResolveName | undefined): string;
1073
+ /**
1074
+ * The SHORT form the badge wears once it knows: a name a person recognises at a
1075
+ * glance, in the width of a cell corner. Never a uuid, never an email's domain.
1076
+ */
1077
+ declare function actorBadge(actor: HistoryActor, resolve?: ResolveName | undefined): string;
1078
+
1079
+ /** The word a withheld cell shows in place of a value. Never "—", which means empty. */
1080
+ declare const WITHHELD_WORD = "Withheld";
1081
+ type Hidden = Readonly<Record<string, HiddenFieldNotice | undefined>> | null | undefined;
1082
+ /** The store's notice for this column of this row, when the store withheld it. */
1083
+ declare function withheldNoticeOf(hidden: Hidden, key: string): HiddenFieldNotice | null;
1084
+ /**
1085
+ * THE SENTENCE, in the store's own words: the notice's `says` when the store wrote one (a
1086
+ * worked-out column names what it reads), else the notice's reason, the level it needs and the
1087
+ * other way in.
1088
+ */
1089
+ declare function withheldSentence(field: Pick<Field, "key" | "label" | "name">, notice: HiddenFieldNotice): string;
1090
+
1091
+ /**
1092
+ * What a person picks in the panel — AND IT IS THE STORE'S OWN LIST, not a union
1093
+ * assembled here.
1094
+ *
1095
+ * 🚨 THE CLASS FIX (FIX-10B-F6, VERIFIER-10). This used to read
1096
+ * `ParityFieldType | PlainFieldType | RelationFieldType` — the parity floor plus two words
1097
+ * this file knew about — and that union was the whole defect. `custom.doc_sign` accepts
1098
+ * exactly one field, a text column whose format is `signature`, and it is granted to
1099
+ * `authenticated`; the Documents screen told a person in so many words to go and declare
1100
+ * one; and no list any screen could read named it, so the control did not exist and the
1101
+ * e-sign half of Documents was unreachable from the product. A kind the store takes and no
1102
+ * screen offers is invisible, and nothing failed — which is the only reason it survived.
1103
+ *
1104
+ * `custom.field_kinds()` is now the ONE registry and `DECLARABLE_FIELD_KINDS` is generated
1105
+ * from it, so the next kind the store learns arrives here as a TYPE ERROR in
1106
+ * `FIELD_TYPE_CHOICES`'s coverage and as a red `kindsWithNoChoice()` below — never as a
1107
+ * silence.
1108
+ */
1109
+ type PickableFieldType = DeclarableFieldKind | SystemFieldKind | "entity_reference" | NewerFieldKind | LookFieldKind;
1110
+ /**
1111
+ * A KIND THAT IS A PLAIN COLUMN WEARING A LOOK (grids review 3, lane H item 5). The store keeps a
1112
+ * column's "Shows as" on the Field (`display_format`, `custom.display_format_ids()` has `rating` and
1113
+ * `time`), so a Rating is a whole number shown as stars and a Time is a time of day kept as its
1114
+ * 24-hour text — declared through the ordinary door (`{ plain, display_format }`) the same way the
1115
+ * "Shows as" picker makes one. Offered whenever the store does not yet publish the kind as its own
1116
+ * word (`custom.field_kinds()`): a Rating the store publishes goes as `{ type: "rating", max }`.
1117
+ */
1118
+ type LookFieldKind = "time";
1119
+ /**
1120
+ * THE STORE'S OWN STAMPS AS COLUMNS (GRID-PRIMITIVES G5; TABLE-PARITY H2 H3, lane
1121
+ * DEFAULT-GRID-PARITY). `custom.field_declare` takes these three WORDS and makes each a formula
1122
+ * Field whose expression is the system node (`fx.autonumber`, `fx.created_time`,
1123
+ * `fx.modified_time`, `config.system` naming which), so nobody types into them and every reader
1124
+ * that serves a worked-out value serves them. `custom.field_kinds()` does not publish them,
1125
+ * because each is a formula, not a behaviour of its own. So they are named here, sent as their
1126
+ * word through the `{ type }` arm, and a new autonumber column numbers the records already in
1127
+ * the table through `custom.autonumber_backfill`.
1128
+ */
1129
+ type SystemFieldKind = "autonumber" | "created_time" | "modified_time";
1130
+
1131
+ declare const PARITY_LABEL: Record<ParityFieldType, string>;
1132
+ /** The "made of" sentence the store itself publishes, for the field editor's help text. */
1133
+ declare const PARITY_MADE_OF: Record<string, string>;
1134
+ /**
1135
+ * Plain text and plain number are behaviours with no parity type of their own.
1136
+ *
1137
+ * 🚨 THIS IS NOW AN ALIAS, AND THAT IS THE POINT (TAILS-3, 2026-09-21). The
1138
+ * classifier moved into `@ai-matrx/records` as `FieldKind` so that the editor a
1139
+ * person types into and the COERCION applied to what they typed are decided by
1140
+ * one body. They were decided by two, and the public booking page paid for it:
1141
+ * "Vehicle Year takes a number, and it was given a string" on every table with
1142
+ * a number field.
1143
+ */
1144
+ type EditorKind = FieldKind;
1145
+ /**
1146
+ * Which editor one Field wants. Reads the Field's own declaration only — the
1147
+ * behaviour, the modifiers, the format, the relation target and the config —
1148
+ * because those are what the store stores, and a screen that guessed from a
1149
+ * value would draw a different editor for an empty record.
1150
+ *
1151
+ * It is `fieldKindFor` with this package's kernel ids bound. Nothing here
1152
+ * decides anything the core does not.
1153
+ */
1154
+ declare function editorKindFor(field: Field): EditorKind;
1155
+ /**
1156
+ * WHICH KIND A PERSON PICKED, read back off the Field the store keeps — the ONE reader every
1157
+ * "what does this column hold" answer goes through (the Settings column list, the column panel).
1158
+ *
1159
+ * `editorKindFor` answers which EDITOR to draw, and for two families that is not the kind a
1160
+ * person picked: a Signature is a text Field whose format is `signature` (it draws a line of
1161
+ * text), and the store's stamps — Record number, Created time, Last changed time — are formula
1162
+ * Fields whose `config.system` names the stamp. Read through the editor classifier alone, the
1163
+ * Settings list called a Signature "Text" and a Record number "Worked out from other columns"
1164
+ * (lane 10 W1-A, guard `every-pickable-kind-reads-back-as-itself.test.tsx`).
1165
+ *
1166
+ * `undefined` when the Field is none of the kinds the menu offers.
1167
+ */
1168
+ declare function pickableTypeOf(field: Field): PickableFieldType | undefined;
1169
+ /**
1170
+ * The type word a person sees in a header or a settings row — and it is the
1171
+ * SAME word the field panel offers, read from `FIELD_TYPE_CHOICES`, so a column
1172
+ * added as "Money" is never described anywhere else as "Currency".
1173
+ */
1174
+ declare function fieldTypeLabel(field: Field): string;
1175
+
1176
+ /** The facts a picker needs about one table, whichever list it came from. */
1177
+ interface PickerTable {
1178
+ id: string;
1179
+ name: string;
1180
+ platform_owned?: boolean | null | undefined;
1181
+ /** The Table document's own key for the same fact (`tableList()` spreads the document onto the Table). */
1182
+ kept_by_the_app?: boolean | null | undefined;
1183
+ /** `custom.table_facts`: which feature keeps it (`choices` for a value set). */
1184
+ kept_for?: string | null | undefined;
1185
+ /** `custom.data_home_tables`: the store's one word for what it is (`list` for a value set). */
1186
+ kind?: string | null | undefined;
1187
+ /** The table a value set belongs to (`custom.table_facts.used_in_table_id`). */
1188
+ used_in_table_id?: string | null | undefined;
1189
+ /** A checklist's steps, a booking page's slots, a workflow's states. */
1190
+ work_kind?: unknown;
1191
+ slug?: string | null | undefined;
1192
+ /** When the table was made — what tells two lists apart once their column is gone. */
1193
+ created_at?: string | null | undefined;
1194
+ }
1195
+ /** A value set: the List a choice column keeps its choices in. */
1196
+ declare function isValueSet(table: PickerTable): boolean;
1197
+ /** Anything the app keeps for itself rather than a table a person keeps. */
1198
+ declare function isKeptTable(table: PickerTable): boolean;
1199
+ interface TablePickerEntry<T extends PickerTable> {
1200
+ table: T;
1201
+ id: string;
1202
+ /** Unique among the entries returned. */
1203
+ label: string;
1204
+ /** A value set (shown only with "Show lists", or because it is the chosen one). */
1205
+ isList: boolean;
1206
+ }
1207
+ interface TablePickerEntries<T extends PickerTable> {
1208
+ entries: TablePickerEntry<T>[];
1209
+ /** How many value sets "Show lists" would add — 0 means the control is not worth drawing. */
1210
+ listCount: number;
1211
+ }
1212
+ /**
1213
+ * THE ONE RULE. Tables first (by name), then — with `showLists` — the value sets, each named
1214
+ * "<column> — <owning table>". Any names still equal get " (2)", " (3)" so no two entries match.
1215
+ */
1216
+ declare function tablePickerEntries<T extends PickerTable>(tables: readonly T[], options?: {
1217
+ showLists?: boolean;
1218
+ keep?: string | null | undefined;
1219
+ }): TablePickerEntries<T>;
1220
+
1221
+ /**
1222
+ * G1's TABLE-LEVEL COLORS (`custom.table_decorations` / `client.tableDecorations`),
1223
+ * TRANSLATED INTO A `TableStyle` — the same document a saved view's own `presentation.style`
1224
+ * is, so one resolver (`resolveTableStyle`, below) can treat "the table's colors" and "this
1225
+ * view's colors" as two layers of the SAME thing instead of two unrelated systems.
1226
+ *
1227
+ * THE TRANSLATION IS NECESSARY AND IT IS THE ONLY ONE. G1 addresses a Field by its ID
1228
+ * (REC-17 — a column renamed keeps its color), because it is the STORE's own door; a
1229
+ * `TableStyle` addresses a Field by its KEY, because it reads straight off `row.document`,
1230
+ * which is keyed by Field key. A rule or a color-by whose Field has been deleted (its id no
1231
+ * longer resolves to a key) is dropped rather than crashing the whole style — a defect in
1232
+ * one rule costs that rule, never the read.
1233
+ */
1234
+ declare function styleFromDecorations(decorations: TableDecorations | null | undefined, fields: readonly Field[]): TableStyle;
1235
+ /**
1236
+ * THE ONE PRESENTATION RESOLVER every colored surface in this package reads —
1237
+ * `Grid`, and (via `Card`) the kanban board, the calendar and the gallery.
1238
+ *
1239
+ * THE CLASS THIS CLOSES (lane UI-QUICK-1, TABLE-PARITY review, 2026-09-24). Threading
1240
+ * `view.presentation.style` into `Card` closed the instance the CALENDAR-GALLERY bench
1241
+ * found — a VIEW that had its own color-by looked right everywhere — but it left the
1242
+ * class open: a table whose colors are the ORGANIZATION's decorations (G1's
1243
+ * `custom.table_decorations` — "color rows by Account Type", "MAXED and NEAR rules" on
1244
+ * Arman's own Coding Accounts table) and whose views never set a `presentation.style` of
1245
+ * their own drew NOTHING, on the grid exactly as much as on every other layout, because
1246
+ * nothing in this package had ever read G1's door at all.
1247
+ *
1248
+ * TWO LAYERS, ONE DOCUMENT. The table's decorations are the base coat — what every view
1249
+ * of this table shows until a view says otherwise. A view's OWN `presentation.style` is
1250
+ * an override, piece by piece: a view that only ever set manual row highlights still
1251
+ * inherits the table's color-by and rules for everything it never touched, and a view
1252
+ * that fully redecorated (its own `colorBy` AND its own `rules`) is not haunted by a
1253
+ * table rule it meant to replace. Per-piece, never all-or-nothing, because a view is a
1254
+ * LOOK on top of the table, not a wholesale substitute for it.
1255
+ */
1256
+ declare function resolveTableStyle(decorations: TableDecorations | null | undefined, fields: readonly Field[], viewStyle: TableStyle | undefined): TableStyle;
1257
+
1258
+ export { type AgentBuildAsk, type EditorKind, NO_RIGHTS, type OpenRecordsAsk, PARITY_LABEL, PARITY_MADE_OF, type PickerTable, type RecordsUiHost, type ResolveName, SOMEBODY, THE_SYSTEM, type TablePickerEntries, type TablePickerEntry, type TableRights, UNREAD_RECORD_LABEL, WITHHELD_RECORD_LABEL, WITHHELD_WORD, actorBadge, actorWords, cardTitleField, editorKindFor, fieldName, fieldTypeLabel, getPersonTimeZone, isKeptTable, isValueSet, lastChangeSentence, looksLikeId, personActor, personWallNow, pickableTypeOf, recordName, recordNameIn, recordsDataSource, resolveTableStyle, rowName, rowNameIn, setPersonTimeZone, singularNoun, styleFromDecorations, subscribePersonTimeZone, tableName, tablePickerEntries, tableRightsAt, whenWords, withheldNoticeOf, withheldSentence };