@ai-matrx/detail 0.1.0
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 +111 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/host-eKqulRpB.d.cts +524 -0
- package/dist/host-eKqulRpB.d.ts +524 -0
- package/dist/index.cjs +308 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +505 -0
- package/dist/index.d.ts +505 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1572 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +191 -0
- package/dist/react.d.ts +191 -0
- package/dist/react.js +1553 -0
- package/dist/react.js.map +1 -0
- package/dist/testing.cjs +162 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +54 -0
- package/dist/testing.d.ts +54 -0
- package/dist/testing.js +142 -0
- package/dist/testing.js.map +1 -0
- package/package.json +115 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** The three ways a record detail can show. `window` is the platform default. */
|
|
4
|
+
declare const DETAIL_PRESENTATIONS: readonly ["window", "docked", "page"];
|
|
5
|
+
type DetailPresentation = (typeof DETAIL_PRESENTATIONS)[number];
|
|
6
|
+
/**
|
|
7
|
+
* The knob every presentation decision reads (`platform.feature_knob`,
|
|
8
|
+
* feature `ui.detail`, key `default_presentation`; seeded by matrx-frontend's
|
|
9
|
+
* `migrations/detail_presentation_knob.sql`). Enum of DETAIL_PRESENTATIONS,
|
|
10
|
+
* overridable by organization and user. The package never reads the settings
|
|
11
|
+
* ladder itself — the host resolves this key and answers through the
|
|
12
|
+
* `usePresentationSetting` / `resolvePresentation` ports.
|
|
13
|
+
*/
|
|
14
|
+
declare const DETAIL_PRESENTATION_KNOB = "ui.detail.default_presentation";
|
|
15
|
+
/**
|
|
16
|
+
* The PER-RECORD-TYPE override of the setting above (`platform.feature_knob`,
|
|
17
|
+
* feature `ui.detail`, key `presentation_by_type`): a json object keyed by
|
|
18
|
+
* record type token, e.g. `{"file": "docked"}`. Nearest wins over
|
|
19
|
+
* `default_presentation` for the type it names, and the same organization →
|
|
20
|
+
* user ladder resolves it, so an organization can set "contracts open as a
|
|
21
|
+
* page" and a person can still choose otherwise for themselves.
|
|
22
|
+
*
|
|
23
|
+
* WHY A SECOND KEY AND NOT THE `table` RUNG. The platform's per-table rung
|
|
24
|
+
* (DD-131, precedence 50) is keyed by a `platform.entity_types` ROW ID, and
|
|
25
|
+
* that table is admin-only by a restrictive policy — a browser cannot read the
|
|
26
|
+
* id, so a `table` rung would be a setting the screen offers and the reader
|
|
27
|
+
* never honours. One json key on the ladder the client already has is the
|
|
28
|
+
* override the settings platform supports today.
|
|
29
|
+
*/
|
|
30
|
+
declare const DETAIL_PRESENTATION_BY_TYPE_KNOB = "ui.detail.presentation_by_type";
|
|
31
|
+
/**
|
|
32
|
+
* How many records of the list a detail was opened FROM may ride the page
|
|
33
|
+
* presentation's URL (`platform.feature_knob`, feature `ui.detail`, key
|
|
34
|
+
* `list_context_max_ids`; seeded by matrx-frontend's
|
|
35
|
+
* `migrations/detail_list_context_max_knob.sql`). Organization-overridable.
|
|
36
|
+
*
|
|
37
|
+
* Why a knob and not a constant: it is a ceiling, and every ceiling here is a
|
|
38
|
+
* row an admin owns (`common-docs/policies/limits-are-knobs-agents-set-them.md`).
|
|
39
|
+
* Why it exists at all: uncapped, a 500-row list produced a >20 KB href no
|
|
40
|
+
* server accepts (NEW-7, VERIFY-U-P1-R2). Beyond the cap the URL carries the
|
|
41
|
+
* window around the current record and the detail says the list was trimmed.
|
|
42
|
+
*/
|
|
43
|
+
declare const DETAIL_LIST_CONTEXT_MAX_KNOB = "ui.detail.list_context_max_ids";
|
|
44
|
+
/**
|
|
45
|
+
* The cap when the knob has not answered yet (a cold cache, a signed-out
|
|
46
|
+
* render, a host that binds no settings ladder). 100 `type.id` pairs with uuid
|
|
47
|
+
* ids is the most the strictest presentation's FINAL URL can carry inside the
|
|
48
|
+
* budget below, and it is far more neighbours than a person arrows through in
|
|
49
|
+
* one sitting.
|
|
50
|
+
*/
|
|
51
|
+
declare const DEFAULT_DETAIL_LIST_CONTEXT_MAX = 100;
|
|
52
|
+
/**
|
|
53
|
+
* 🚨 NEW-19 (VERIFY-U-P1-R4) — THE BUDGET BELONGS TO THE FINAL URL, ONCE.
|
|
54
|
+
*
|
|
55
|
+
* It used to be 6,000 characters measured on the list VALUE, one escaping layer
|
|
56
|
+
* too early, and nothing bounded the whole address:
|
|
57
|
+
*
|
|
58
|
+
* * a detail window's deep link measured 5,992 against that 6,000 and reached
|
|
59
|
+
* the address bar at 7,416, because the host's panel-URL writer re-serializes
|
|
60
|
+
* the `?panels=` token through `new URLSearchParams(...).toString()`, which
|
|
61
|
+
* re-escapes every `%` the token's own escaping already produced (a uuid's
|
|
62
|
+
* `-` goes `-` → `%2D` → `%252D`);
|
|
63
|
+
* * a detail PAGE that also carried an open window was 13,433 characters —
|
|
64
|
+
* its own `?l=` plus the merged `panels=` — past the 8 KB request line, so
|
|
65
|
+
* the edge answers 414 and the link is dead (VERIFY-U-P1-R4, NEW-19).
|
|
66
|
+
*
|
|
67
|
+
* 8,000 characters is the strictest edge in front of this platform (an 8 KB
|
|
68
|
+
* request line), and the method, the path, every other parameter and the
|
|
69
|
+
* token's own non-list args share it: a caller passes what the rest of the URL
|
|
70
|
+
* already costs as `reservedBytes` and `trimListContext` measures the FINAL
|
|
71
|
+
* serialized form. No caller may skip it, and the record SAYS what was cut
|
|
72
|
+
* (`trimmedFrom`).
|
|
73
|
+
*/
|
|
74
|
+
declare const DETAIL_URL_BUDGET_BYTES = 8000;
|
|
75
|
+
/**
|
|
76
|
+
* The hard ceiling on the knob — the number of records the final URL can really
|
|
77
|
+
* carry in EVERY presentation, so a value below it always changes what travels.
|
|
78
|
+
*
|
|
79
|
+
* 🚨 NEW-20 (VERIFY-U-P1-R4). The ceiling was 500 while the byte budget stopped
|
|
80
|
+
* a uuid list at 139, so every value an administrator could set from 139 to 500
|
|
81
|
+
* behaved identically and the knob's own live basis text told them 200 records
|
|
82
|
+
* would travel. Measured against the final URL: 100 uuid entries under a short
|
|
83
|
+
* type token cost ~6.7 KB in the window's `?panels=` token (the strictest
|
|
84
|
+
* spelling) and ~4.3 KB in the page query, both inside the budget above; 150
|
|
85
|
+
* costs ~10 KB in the token and does not fit. matrx-frontend's
|
|
86
|
+
* `migrations/detail_list_context_max_deliverable.sql` lowers the knob's own
|
|
87
|
+
* `max_value` and default to the same number and rewrites its basis text, so
|
|
88
|
+
* the settings screen cannot offer records the URL can never carry. A longer
|
|
89
|
+
* type token still trims below the cap — and still says so.
|
|
90
|
+
*/
|
|
91
|
+
declare const DETAIL_LIST_CONTEXT_MAX_IDS_CEILING = 100;
|
|
92
|
+
/**
|
|
93
|
+
* The knob's value as a usable cap; the default for anything that is not one,
|
|
94
|
+
* and never more than the ceiling the byte budget can honour (NEW-12). The ONE
|
|
95
|
+
* place the knob is turned into a number — the page href builder, the window's
|
|
96
|
+
* deep-link token and the trim itself all read it through here.
|
|
97
|
+
*/
|
|
98
|
+
declare function detailListContextMax(raw: unknown): number;
|
|
99
|
+
/** The per-type entry for `type`, when the map holds a usable one. */
|
|
100
|
+
declare function presentationForTypeFromMap(raw: unknown, type: string): DetailPresentation | undefined;
|
|
101
|
+
declare function isDetailPresentation(value: unknown): value is DetailPresentation;
|
|
102
|
+
/** A record, named the way the platform names records: type token + id. */
|
|
103
|
+
interface DetailRef {
|
|
104
|
+
type: string;
|
|
105
|
+
id: string;
|
|
106
|
+
}
|
|
107
|
+
/** What the opener already knows, shown instantly until the row loads. */
|
|
108
|
+
interface DetailSeed {
|
|
109
|
+
name?: string | null;
|
|
110
|
+
about?: string | null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The list the record was opened FROM, so `[` / `]` move to the previous /
|
|
114
|
+
* next record without going back to the list. Plain data — it travels through
|
|
115
|
+
* the host's state store and the URL.
|
|
116
|
+
*/
|
|
117
|
+
interface DetailListContext {
|
|
118
|
+
items: DetailRef[];
|
|
119
|
+
index: number;
|
|
120
|
+
/**
|
|
121
|
+
* Set when this list is a WINDOW cut out of a longer one (the page
|
|
122
|
+
* presentation carries the list in the URL and it is capped —
|
|
123
|
+
* `ui.detail.list_context_max_ids`): the length of the list it was cut from,
|
|
124
|
+
* so the detail can say the neighbours beyond it are not reachable from here
|
|
125
|
+
* instead of presenting the window as the whole list (NEW-7).
|
|
126
|
+
*/
|
|
127
|
+
trimmedFrom?: number;
|
|
128
|
+
}
|
|
129
|
+
/** What `useOpenDetail` accepts. */
|
|
130
|
+
interface DetailOpenRequest extends DetailRef {
|
|
131
|
+
/** Skip the setting and open this way. Omit to honour the user's setting. */
|
|
132
|
+
presentation?: DetailPresentation;
|
|
133
|
+
seed?: DetailSeed | null;
|
|
134
|
+
list?: DetailListContext | null;
|
|
135
|
+
}
|
|
136
|
+
/** The payload a presentation receives — everything an open request carried. */
|
|
137
|
+
interface DetailInstanceData extends DetailRef {
|
|
138
|
+
seed: DetailSeed | null;
|
|
139
|
+
list: DetailListContext | null;
|
|
140
|
+
}
|
|
141
|
+
type DetailRow = Record<string, unknown>;
|
|
142
|
+
type DetailLoadResult<Row extends DetailRow = DetailRow> = {
|
|
143
|
+
row: Row;
|
|
144
|
+
} | {
|
|
145
|
+
notFound: true;
|
|
146
|
+
};
|
|
147
|
+
/** One rendered field. `ref` makes the value a door instead of text. */
|
|
148
|
+
interface DetailField {
|
|
149
|
+
key: string;
|
|
150
|
+
label: string;
|
|
151
|
+
text: string;
|
|
152
|
+
/** Render in a monospace block (JSON, long identifiers). */
|
|
153
|
+
mono?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* This value names another record — render it as a door.
|
|
156
|
+
*
|
|
157
|
+
* 🚨 AND THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). The body used to
|
|
158
|
+
* render the door from `ref.id` alone, so a field whose `text` was
|
|
159
|
+
* "Acme Robotics" printed a truncated uuid instead: a door nobody can read is
|
|
160
|
+
* half a dead end. `name` is the record's own name for the host's `RefCell` to
|
|
161
|
+
* render; when it is omitted the body falls back to `text` (unless `text` is
|
|
162
|
+
* the id itself, in which case there is nothing better to say).
|
|
163
|
+
*/
|
|
164
|
+
ref?: {
|
|
165
|
+
token: string;
|
|
166
|
+
id: string;
|
|
167
|
+
name?: string | null;
|
|
168
|
+
} | null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The "source health strip" for a synced record — where it came from, when it
|
|
172
|
+
* was last refreshed, whether the grant behind it still works, and how to open
|
|
173
|
+
* it at the source. Absent for records the platform owns outright.
|
|
174
|
+
*/
|
|
175
|
+
interface DetailSourceHealth {
|
|
176
|
+
/** Human name of the source ("Google Drive", "Search Console"). */
|
|
177
|
+
source: string;
|
|
178
|
+
lastRefreshedAt?: string | null;
|
|
179
|
+
/**
|
|
180
|
+
* 🚨 THE GRANT-STATE VOCABULARY. Five of the six say what is wrong with the
|
|
181
|
+
* CONNECTION; `blocked` says the record cannot be refreshed and NOTHING the
|
|
182
|
+
* person can click repairs it (chair, 2026-09-18).
|
|
183
|
+
*
|
|
184
|
+
* - `ok` → the grant behind this record works;
|
|
185
|
+
* - `expired` → it lapsed and a reconnect renews it;
|
|
186
|
+
* - `revoked` → someone withdrew it and a reconnect re-grants it;
|
|
187
|
+
* - `missing` → we never had it, or the record is not shared with us;
|
|
188
|
+
* - `unknown` → we could not check (a read failed) — reconnecting is safe;
|
|
189
|
+
* - `blocked` → the source itself refuses, and no control repairs it: a
|
|
190
|
+
* provider outage, a quota, a platform-configuration fault,
|
|
191
|
+
* a connection the server reports as unavailable. The strip
|
|
192
|
+
* says BLOCKED, states the reason, and offers NO Reconnect —
|
|
193
|
+
* which `reconnectFor` enforces for this word whatever the
|
|
194
|
+
* producer or the host supplies, because a button that
|
|
195
|
+
* cannot work is a control that does nothing (law 4).
|
|
196
|
+
*
|
|
197
|
+
* `unknown` used to carry both meanings, so a blocked record was offered the
|
|
198
|
+
* consent window it could not be repaired by (VERIFY-U-P1-R5, N1's class).
|
|
199
|
+
*/
|
|
200
|
+
grant: "ok" | "expired" | "revoked" | "missing" | "unknown" | "blocked";
|
|
201
|
+
/** One sentence for the grant state, in the person's language. */
|
|
202
|
+
grantDetail?: string | null;
|
|
203
|
+
openAtSourceHref?: string | null;
|
|
204
|
+
/**
|
|
205
|
+
* The per-record label for the "open at source" control, when the
|
|
206
|
+
* registration knows it precisely (chair, 2026-09-18). `DetailBody` derives
|
|
207
|
+
* a label from `source` on its own (see `deriveOpenAtSourceLabel`) for a
|
|
208
|
+
* producer that has no opinion, but that derivation guesses from a family
|
|
209
|
+
* name ("Google Docs, Sheets & Drive files" → "Open in Google") — a
|
|
210
|
+
* registration that knows the row's exact kind (a Doc vs. a Sheet vs. a
|
|
211
|
+
* plain Drive file; a Calendar event) sets this instead and it wins.
|
|
212
|
+
*/
|
|
213
|
+
openAtSourceLabel?: string;
|
|
214
|
+
/**
|
|
215
|
+
* The repair offered when the grant is not `ok`. 🚨 THREE-VALUED, AND THE
|
|
216
|
+
* THREE MEAN DIFFERENT THINGS (Bugbot round 18 on frontend PR 228):
|
|
217
|
+
*
|
|
218
|
+
* - a function → offer THIS action;
|
|
219
|
+
* - `null` → offer NOTHING: the producer has decided a reconnect
|
|
220
|
+
* would not repair this refusal (a share the owner must
|
|
221
|
+
* grant, a record we own, an unclassified failure) and a
|
|
222
|
+
* Reconnect button would be a control that does nothing;
|
|
223
|
+
* - omitted → the producer has no opinion; the host's `reconnectSource`
|
|
224
|
+
* port supplies the action when it has one.
|
|
225
|
+
*
|
|
226
|
+
* `useDetailHealth` honours `null` explicitly — it used to fill the host
|
|
227
|
+
* action back in with `??`, which reads `null` as "unset", so a synced record
|
|
228
|
+
* whose refusal a reconnect cannot fix still showed Reconnect.
|
|
229
|
+
*/
|
|
230
|
+
onReconnect?: (() => void) | null;
|
|
231
|
+
/** Offered when the record can be refreshed on demand. */
|
|
232
|
+
onRefresh?: (() => void | Promise<void>) | null;
|
|
233
|
+
}
|
|
234
|
+
/** What a `health` producer is told about the record it is answering for. */
|
|
235
|
+
interface DetailHealthContext {
|
|
236
|
+
ref: DetailRef;
|
|
237
|
+
/** Aborted when the record changes or the detail closes. */
|
|
238
|
+
signal: AbortSignal;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The health producer: sync or async, and free to answer `null` for a record
|
|
242
|
+
* that is not synced. A throw or a rejection is rendered as an honest "we could
|
|
243
|
+
* not check the connection" strip, never as silence (`useDetailHealth`).
|
|
244
|
+
*/
|
|
245
|
+
type DetailHealthProducer<Row extends DetailRow = DetailRow> = (row: Row, ctx: DetailHealthContext) => DetailSourceHealth | null | Promise<DetailSourceHealth | null>;
|
|
246
|
+
/** An extra section a record type adds under the fixed ones. */
|
|
247
|
+
interface DetailSection {
|
|
248
|
+
id: string;
|
|
249
|
+
label: string;
|
|
250
|
+
content: ReactNode;
|
|
251
|
+
}
|
|
252
|
+
/** What a registration's optional `Frame` and sections receive. */
|
|
253
|
+
interface DetailFrameContext {
|
|
254
|
+
ref: DetailRef;
|
|
255
|
+
recordType: DetailRecordType;
|
|
256
|
+
title: string;
|
|
257
|
+
about: string | null;
|
|
258
|
+
status: DetailStatus;
|
|
259
|
+
fields: DetailField[];
|
|
260
|
+
presentation: DetailPresentation;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* ONE registration per record type. The core component (header + fixed
|
|
264
|
+
* sections) is derived from this; a record type never writes presentation
|
|
265
|
+
* code. Everything optional degrades to "section absent", never to a dead
|
|
266
|
+
* or disabled-looking control.
|
|
267
|
+
*/
|
|
268
|
+
interface DetailRecordType<Row extends DetailRow = DetailRow> {
|
|
269
|
+
/** The type token this registration answers for (as opened). */
|
|
270
|
+
type: string;
|
|
271
|
+
/** Human label ("Task", "File"). */
|
|
272
|
+
label: string;
|
|
273
|
+
icon: ComponentType<{
|
|
274
|
+
className?: string;
|
|
275
|
+
}>;
|
|
276
|
+
/** Accent classes for the icon and type chip; semantic tokens only. */
|
|
277
|
+
accent?: {
|
|
278
|
+
text: string;
|
|
279
|
+
bg: string;
|
|
280
|
+
ring: string;
|
|
281
|
+
} | null;
|
|
282
|
+
/**
|
|
283
|
+
* The canonical entity-registry token for doors, associations and history.
|
|
284
|
+
* `null` when the type is not a registry entity — the header then offers
|
|
285
|
+
* copy-id only, and the associations / history sections are absent.
|
|
286
|
+
*/
|
|
287
|
+
entityToken: string | null;
|
|
288
|
+
/**
|
|
289
|
+
* Load the full row. `null` when the type has no single canonical source
|
|
290
|
+
* (the detail then renders from the seed alone and says so).
|
|
291
|
+
*/
|
|
292
|
+
load: ((id: string, signal: AbortSignal) => Promise<DetailLoadResult<Row>>) | null;
|
|
293
|
+
/** The title to show; receives the loaded row (or null) and the seed. */
|
|
294
|
+
title: (row: Row | null, seed: DetailSeed | null) => string;
|
|
295
|
+
/**
|
|
296
|
+
* 🚨 WHAT THIS ROW IS, WHEN THE TYPE CANNOT SAY (chair, 2026-09-18). `label` is
|
|
297
|
+
* per TYPE, and a registered type is regularly a family: `crm.party` holds
|
|
298
|
+
* 1,432 companies and 460 persons, so a type labelled "Person" told a person
|
|
299
|
+
* that a company is a person — twice on screen, above a field reading
|
|
300
|
+
* `organization` (VERIFY-U-P1-R5, N6). A registration that can tell the
|
|
301
|
+
* difference from the row answers here.
|
|
302
|
+
*
|
|
303
|
+
* It names the RECORD, so it is used for the header's type chip and for the
|
|
304
|
+
* stand-in titles ("Company 3f2a1b9c") only. The presentation pane keeps the
|
|
305
|
+
* type-level `label`, because "Every contact record now opens as a window" is a
|
|
306
|
+
* statement about the TYPE's setting, not about the row on screen.
|
|
307
|
+
*
|
|
308
|
+
* `null` (or omitted) falls back to `label`, then to the raw type token.
|
|
309
|
+
*/
|
|
310
|
+
labelForRow?: (row: Row | null) => string | null;
|
|
311
|
+
/** The field list for the fields section. */
|
|
312
|
+
fields: (row: Row) => DetailField[];
|
|
313
|
+
/**
|
|
314
|
+
* 🚨 THE SOURCE HEALTH STRIP'S PRODUCER. Source health for a SYNCED record;
|
|
315
|
+
* omit it, or answer `null`, for a record the platform owns outright.
|
|
316
|
+
*
|
|
317
|
+
* It may be ASYNC, because whether the grant behind a synced record still works
|
|
318
|
+
* is not in the row — it is the connector's recorded per-capability health, read
|
|
319
|
+
* from the server. The host wires this field when it registers the record type
|
|
320
|
+
* (never a second reader of the connector's health), and `useDetailHealth`
|
|
321
|
+
* resolves it. Until 2026-09-17 nothing set it, so the strip could not render on
|
|
322
|
+
* any record and the promise that a refusal anywhere shows on every dependent
|
|
323
|
+
* record with the same Reconnect had no witness (VERIFY-U-P1-R4).
|
|
324
|
+
*/
|
|
325
|
+
health?: DetailHealthProducer<Row> | null;
|
|
326
|
+
/**
|
|
327
|
+
* Tokens the associations section shows. `null` hides the section.
|
|
328
|
+
* Omitted → the host's default set minus this record's own token.
|
|
329
|
+
*/
|
|
330
|
+
associationTokens?: string[] | null;
|
|
331
|
+
/** Show the history section (row versions). Default: when `entityToken` is set. */
|
|
332
|
+
history?: boolean;
|
|
333
|
+
/** Extra sections under the fixed ones. */
|
|
334
|
+
extraSections?: ((row: Row | null, ctx: DetailFrameContext) => DetailSection[]) | null;
|
|
335
|
+
/**
|
|
336
|
+
* Wraps the whole body (surface runtime, right-click menu, …). Receives the
|
|
337
|
+
* live context so a host can expose the dossier to its agents. Optional.
|
|
338
|
+
*/
|
|
339
|
+
Frame?: ComponentType<{
|
|
340
|
+
ctx: DetailFrameContext;
|
|
341
|
+
children: ReactNode;
|
|
342
|
+
}> | null;
|
|
343
|
+
}
|
|
344
|
+
type DetailStatus = "loading" | "ready" | "not-found" | "error"
|
|
345
|
+
/** The type has no `load` — rendered from the seed alone. */
|
|
346
|
+
| "none";
|
|
347
|
+
type DetailLoadState<Row extends DetailRow = DetailRow> = {
|
|
348
|
+
status: "loading";
|
|
349
|
+
} | {
|
|
350
|
+
status: "ready";
|
|
351
|
+
row: Row;
|
|
352
|
+
} | {
|
|
353
|
+
status: "not-found";
|
|
354
|
+
} | {
|
|
355
|
+
status: "error";
|
|
356
|
+
message: string;
|
|
357
|
+
} | {
|
|
358
|
+
status: "none";
|
|
359
|
+
};
|
|
360
|
+
/** One row of the history section. */
|
|
361
|
+
interface DetailHistoryEntry {
|
|
362
|
+
version: number;
|
|
363
|
+
operation: string;
|
|
364
|
+
actorId: string | null;
|
|
365
|
+
occurredAt: string;
|
|
366
|
+
isCurrent: boolean;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** One half of a `type.id` pair, carrying no character the decoders split on. */
|
|
370
|
+
declare function encodeRefPart(value: string): string;
|
|
371
|
+
/** Inverse of `encodeRefPart`; a hand-edited value comes back as its own text. */
|
|
372
|
+
declare function decodeRefPart(value: string): string;
|
|
373
|
+
/** `type.id,type.id` — the ONE spelling of a list in a URL, page or panel token. */
|
|
374
|
+
declare function encodeListItems(items: readonly DetailRef[]): string;
|
|
375
|
+
/** Inverse of `encodeListItems`; entries that do not name a record are dropped. */
|
|
376
|
+
declare function decodeListItems(value: string | null | undefined): DetailRef[];
|
|
377
|
+
/** How many characters these records cost in a URL — what the budget measures. */
|
|
378
|
+
declare function listQueryBytes(items: readonly DetailRef[]): number;
|
|
379
|
+
interface TrimListContextOptions {
|
|
380
|
+
/**
|
|
381
|
+
* The character budget for the whole URL. Defaults to the platform's request
|
|
382
|
+
* line (`DETAIL_URL_BUDGET_BYTES`).
|
|
383
|
+
*/
|
|
384
|
+
budgetBytes?: number;
|
|
385
|
+
/**
|
|
386
|
+
* What the rest of the FINAL address already costs — the path, the query it is
|
|
387
|
+
* being merged into, the token's own non-list args. The list gets what is
|
|
388
|
+
* left, never a private allowance of its own (NEW-19).
|
|
389
|
+
*/
|
|
390
|
+
reservedBytes?: number;
|
|
391
|
+
/**
|
|
392
|
+
* How many characters these records cost IN THE FINAL URL. The `?panels=`
|
|
393
|
+
* token escapes every separator and `URLSearchParams` escapes the result
|
|
394
|
+
* again, so the same records cost about 1.6× there what they cost in the page
|
|
395
|
+
* query (NEW-15, NEW-19).
|
|
396
|
+
*/
|
|
397
|
+
measure?: (items: readonly DetailRef[]) => number;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The list as it may travel, plus what was left behind. Returns the same list
|
|
401
|
+
* when it already fits both limits, and `null` for no list at all.
|
|
402
|
+
*/
|
|
403
|
+
declare function trimListContext(list: DetailListContext | null | undefined, max: number, options?: TrimListContextOptions): DetailListContext | null;
|
|
404
|
+
|
|
405
|
+
declare const DETAIL_URL_TYPE_KEY = "detail";
|
|
406
|
+
declare const DETAIL_URL_AS_ARG = "as";
|
|
407
|
+
/** The list the window was opened from, so a REFRESH keeps it (NEW-15). */
|
|
408
|
+
declare const DETAIL_URL_LIST_ARG = "l";
|
|
409
|
+
declare const DETAIL_URL_INDEX_ARG = "i";
|
|
410
|
+
declare const DETAIL_URL_LIST_TOTAL_ARG = "lt";
|
|
411
|
+
/**
|
|
412
|
+
* `type.id` — the panel URL splits tokens on `:` and args on `-`/`_`, never on
|
|
413
|
+
* `.`, so the two halves carry no `.` `:` or `,` of their own.
|
|
414
|
+
*
|
|
415
|
+
* 🚨 NEW-25 (VERIFY-U-P1-R4) — the halves used to be raw, so a type token or an
|
|
416
|
+
* id containing a dot came back as a DIFFERENT record: `{type: "gr.ant", id:
|
|
417
|
+
* "x.y"}` parsed as `{type: "gr", id: "ant.x.y"}`. One encoder, one decoder
|
|
418
|
+
* (`encodeRefPart` / `decodeRefPart`), the same pair the list spelling uses.
|
|
419
|
+
*/
|
|
420
|
+
declare function detailInstanceKey(ref: DetailRef): string;
|
|
421
|
+
/** Inverse of `detailInstanceKey`; `null` when the key is not `type.id`. */
|
|
422
|
+
declare function parseDetailInstanceKey(key: string | null | undefined): DetailRef | null;
|
|
423
|
+
/** Which in-place presentation a URL arg names; `window` when unspecified. */
|
|
424
|
+
declare function presentationFromUrlArg(value: string | undefined): Exclude<DetailPresentation, "page">;
|
|
425
|
+
/** A value safe to carry as a `?panels=` arg — no `,` `:` `_` or `-`. */
|
|
426
|
+
declare function encodePanelArgValue(value: string): string;
|
|
427
|
+
/** Inverse of `encodePanelArgValue`. */
|
|
428
|
+
declare function decodePanelArgValue(value: string): string;
|
|
429
|
+
/**
|
|
430
|
+
* 🚨 NEW-19 (VERIFY-U-P1-R4) — MEASURE THE VALUE AS THE ADDRESS BAR CARRIES IT.
|
|
431
|
+
* The token's own escaping is only the first layer: the host's panel-URL writer
|
|
432
|
+
* serializes the whole `?panels=` value through `new URLSearchParams(...).toString()`,
|
|
433
|
+
* which escapes every `%` again (a uuid's `-` goes `-` → `%2D` → `%252D`, three
|
|
434
|
+
* characters becoming five). Measuring the token's spelling said 5,992 for a URL
|
|
435
|
+
* the browser carried at 7,416.
|
|
436
|
+
*/
|
|
437
|
+
declare function panelListValueBytes(items: readonly DetailRef[]): number;
|
|
438
|
+
/**
|
|
439
|
+
* The length of the FINAL address a detail window's deep link produces: the path
|
|
440
|
+
* and query it is merged into, plus the `panels` parameter as
|
|
441
|
+
* `URLSearchParams` serializes it. What the budget is measured against, and what
|
|
442
|
+
* a guard can assert.
|
|
443
|
+
*/
|
|
444
|
+
declare function finalPanelUrlLength(pathAndQuery: string, tokenWithoutListArgs: string, listArgs: Record<string, string>): number;
|
|
445
|
+
/**
|
|
446
|
+
* What the address a panel token is written INTO really costs, after
|
|
447
|
+
* `URLSearchParams` re-serializes it — a detail page's own `?l=` list carries raw
|
|
448
|
+
* commas that become `%2C` the moment a `panels` parameter is merged beside it,
|
|
449
|
+
* so the page's query grows by ~2× its comma count at the instant the window
|
|
450
|
+
* opens. Reserving the pre-serialization length is how a page + window address
|
|
451
|
+
* still overran the budget by 70 characters in the guard that found this.
|
|
452
|
+
*/
|
|
453
|
+
declare function panelUrlReserveBytes(pathAndQuery: string): number;
|
|
454
|
+
/**
|
|
455
|
+
* The window's deep-link args for a list context — `{}` when there is no list.
|
|
456
|
+
* `max` is the resolved `ui.detail.list_context_max_ids`; the budget is the
|
|
457
|
+
* platform's REQUEST LINE, measured on the final serialized value, with whatever
|
|
458
|
+
* the rest of the address already costs reserved (NEW-19). A caller that knows
|
|
459
|
+
* the address it is writing into passes its length; one that does not gets the
|
|
460
|
+
* conservative reserve above, which is what a long detail-page query costs.
|
|
461
|
+
*/
|
|
462
|
+
declare function detailListToUrlArgs(list: DetailListContext | null | undefined, max: number, options?: {
|
|
463
|
+
reservedBytes?: number;
|
|
464
|
+
}): Record<string, string>;
|
|
465
|
+
/** The list a `?panels=detail:` token carries; `null` when it carries none. */
|
|
466
|
+
declare function detailListFromUrlArgs(args: Record<string, string> | null | undefined): DetailListContext | null;
|
|
467
|
+
/**
|
|
468
|
+
* 🚨 NEW-7 — CAPPED, ALWAYS. `max` is the resolved
|
|
469
|
+
* `ui.detail.list_context_max_ids` knob; the default is used when the host has
|
|
470
|
+
* no answer yet, never "no cap". A 500-row list uncapped produced a >20 KB href
|
|
471
|
+
* no server accepts (VERIFY-U-P1-R2).
|
|
472
|
+
*
|
|
473
|
+
* 🚨 NEW-19 (VERIFY-U-P1-R4) — AND THE BUDGET IS THE WHOLE URL'S. The path this
|
|
474
|
+
* query hangs off is part of the request line, so the caller passes what it
|
|
475
|
+
* costs (`reservedBytes`) and the list gets what is left. Measuring the list
|
|
476
|
+
* alone is how a detail page reached 8,464 characters while the code claimed a
|
|
477
|
+
* 2 KB margin.
|
|
478
|
+
*/
|
|
479
|
+
declare function encodeListQuery(list: DetailListContext | null | undefined, max?: number, options?: {
|
|
480
|
+
reservedBytes?: number;
|
|
481
|
+
}): string;
|
|
482
|
+
/** Inverse of `encodeListQuery`, from the three query values. */
|
|
483
|
+
declare function decodeListQuery(l: string | null | undefined, i: string | null | undefined,
|
|
484
|
+
/** `lt` — the length of the list this window was cut from, when it was. */
|
|
485
|
+
lt?: string | null | undefined): DetailListContext | null;
|
|
486
|
+
|
|
487
|
+
/** Columns that are pure plumbing — hidden from a formatted field list. */
|
|
488
|
+
declare const HIDDEN_FIELDS: Set<string>;
|
|
489
|
+
declare function titleizeKey(key: string): string;
|
|
490
|
+
declare function formatValue(value: unknown): {
|
|
491
|
+
text: string;
|
|
492
|
+
mono?: boolean;
|
|
493
|
+
} | null;
|
|
494
|
+
/**
|
|
495
|
+
* Every populated scalar column of a row as a field; a `<token>_id` column
|
|
496
|
+
* holding a real uuid becomes a door (the host decides which token names a
|
|
497
|
+
* registry entity — a wrong link is worse than no link).
|
|
498
|
+
*/
|
|
499
|
+
declare function fieldsFromRow(row: Record<string, unknown>, doors: {
|
|
500
|
+
tokenFromColumnName: (column: string) => string | null;
|
|
501
|
+
isUuidValue: (value: unknown) => value is string;
|
|
502
|
+
}): DetailField[];
|
|
503
|
+
declare function formatWhen(iso: string): string;
|
|
504
|
+
|
|
505
|
+
export { DEFAULT_DETAIL_LIST_CONTEXT_MAX, DETAIL_LIST_CONTEXT_MAX_IDS_CEILING, DETAIL_LIST_CONTEXT_MAX_KNOB, DETAIL_PRESENTATIONS, DETAIL_PRESENTATION_BY_TYPE_KNOB, DETAIL_PRESENTATION_KNOB, DETAIL_URL_AS_ARG, DETAIL_URL_BUDGET_BYTES, DETAIL_URL_INDEX_ARG, DETAIL_URL_LIST_ARG, DETAIL_URL_LIST_TOTAL_ARG, DETAIL_URL_TYPE_KEY, type DetailField, type DetailFrameContext, type DetailHealthContext, type DetailHealthProducer, type DetailHistoryEntry, type DetailInstanceData, type DetailListContext, type DetailLoadResult, type DetailLoadState, type DetailOpenRequest, type DetailPresentation, type DetailRecordType, type DetailRef, type DetailRow, type DetailSection, type DetailSeed, type DetailSourceHealth, type DetailStatus, HIDDEN_FIELDS, type TrimListContextOptions, decodeListItems, decodeListQuery, decodePanelArgValue, decodeRefPart, detailInstanceKey, detailListContextMax, detailListFromUrlArgs, detailListToUrlArgs, encodeListItems, encodeListQuery, encodePanelArgValue, encodeRefPart, fieldsFromRow, finalPanelUrlLength, formatValue, formatWhen, isDetailPresentation, listQueryBytes, panelListValueBytes, panelUrlReserveBytes, parseDetailInstanceKey, presentationForTypeFromMap, presentationFromUrlArg, titleizeKey, trimListContext };
|