@ai-matrx/records-ui 0.115.70 → 0.115.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/first-page.cjs +0 -1
- package/dist/first-page.cjs.map +1 -1
- package/dist/first-page.js +0 -1
- package/dist/first-page.js.map +1 -1
- package/dist/helpers.cjs +618 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +1258 -0
- package/dist/helpers.d.ts +1258 -0
- package/dist/helpers.js +559 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.cjs +309 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +182 -164
- package/dist/index.js.map +1 -1
- package/dist/pickers.cjs +94 -72
- package/dist/pickers.cjs.map +1 -1
- package/dist/pickers.js +69 -53
- package/dist/pickers.js.map +1 -1
- package/package.json +16 -6
|
@@ -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 };
|