@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.
@@ -0,0 +1,524 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+
4
+ /** The three ways a record detail can show. `window` is the platform default. */
5
+ declare const DETAIL_PRESENTATIONS: readonly ["window", "docked", "page"];
6
+ type DetailPresentation = (typeof DETAIL_PRESENTATIONS)[number];
7
+ /** A record, named the way the platform names records: type token + id. */
8
+ interface DetailRef {
9
+ type: string;
10
+ id: string;
11
+ }
12
+ /** What the opener already knows, shown instantly until the row loads. */
13
+ interface DetailSeed {
14
+ name?: string | null;
15
+ about?: string | null;
16
+ }
17
+ /**
18
+ * The list the record was opened FROM, so `[` / `]` move to the previous /
19
+ * next record without going back to the list. Plain data — it travels through
20
+ * the host's state store and the URL.
21
+ */
22
+ interface DetailListContext {
23
+ items: DetailRef[];
24
+ index: number;
25
+ /**
26
+ * Set when this list is a WINDOW cut out of a longer one (the page
27
+ * presentation carries the list in the URL and it is capped —
28
+ * `ui.detail.list_context_max_ids`): the length of the list it was cut from,
29
+ * so the detail can say the neighbours beyond it are not reachable from here
30
+ * instead of presenting the window as the whole list (NEW-7).
31
+ */
32
+ trimmedFrom?: number;
33
+ }
34
+ /** What `useOpenDetail` accepts. */
35
+ interface DetailOpenRequest extends DetailRef {
36
+ /** Skip the setting and open this way. Omit to honour the user's setting. */
37
+ presentation?: DetailPresentation;
38
+ seed?: DetailSeed | null;
39
+ list?: DetailListContext | null;
40
+ }
41
+ /** The payload a presentation receives — everything an open request carried. */
42
+ interface DetailInstanceData extends DetailRef {
43
+ seed: DetailSeed | null;
44
+ list: DetailListContext | null;
45
+ }
46
+ type DetailRow = Record<string, unknown>;
47
+ type DetailLoadResult<Row extends DetailRow = DetailRow> = {
48
+ row: Row;
49
+ } | {
50
+ notFound: true;
51
+ };
52
+ /** One rendered field. `ref` makes the value a door instead of text. */
53
+ interface DetailField {
54
+ key: string;
55
+ label: string;
56
+ text: string;
57
+ /** Render in a monospace block (JSON, long identifiers). */
58
+ mono?: boolean;
59
+ /**
60
+ * This value names another record — render it as a door.
61
+ *
62
+ * 🚨 AND THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). The body used to
63
+ * render the door from `ref.id` alone, so a field whose `text` was
64
+ * "Acme Robotics" printed a truncated uuid instead: a door nobody can read is
65
+ * half a dead end. `name` is the record's own name for the host's `RefCell` to
66
+ * render; when it is omitted the body falls back to `text` (unless `text` is
67
+ * the id itself, in which case there is nothing better to say).
68
+ */
69
+ ref?: {
70
+ token: string;
71
+ id: string;
72
+ name?: string | null;
73
+ } | null;
74
+ }
75
+ /**
76
+ * The "source health strip" for a synced record — where it came from, when it
77
+ * was last refreshed, whether the grant behind it still works, and how to open
78
+ * it at the source. Absent for records the platform owns outright.
79
+ */
80
+ interface DetailSourceHealth {
81
+ /** Human name of the source ("Google Drive", "Search Console"). */
82
+ source: string;
83
+ lastRefreshedAt?: string | null;
84
+ /**
85
+ * 🚨 THE GRANT-STATE VOCABULARY. Five of the six say what is wrong with the
86
+ * CONNECTION; `blocked` says the record cannot be refreshed and NOTHING the
87
+ * person can click repairs it (chair, 2026-09-18).
88
+ *
89
+ * - `ok` → the grant behind this record works;
90
+ * - `expired` → it lapsed and a reconnect renews it;
91
+ * - `revoked` → someone withdrew it and a reconnect re-grants it;
92
+ * - `missing` → we never had it, or the record is not shared with us;
93
+ * - `unknown` → we could not check (a read failed) — reconnecting is safe;
94
+ * - `blocked` → the source itself refuses, and no control repairs it: a
95
+ * provider outage, a quota, a platform-configuration fault,
96
+ * a connection the server reports as unavailable. The strip
97
+ * says BLOCKED, states the reason, and offers NO Reconnect —
98
+ * which `reconnectFor` enforces for this word whatever the
99
+ * producer or the host supplies, because a button that
100
+ * cannot work is a control that does nothing (law 4).
101
+ *
102
+ * `unknown` used to carry both meanings, so a blocked record was offered the
103
+ * consent window it could not be repaired by (VERIFY-U-P1-R5, N1's class).
104
+ */
105
+ grant: "ok" | "expired" | "revoked" | "missing" | "unknown" | "blocked";
106
+ /** One sentence for the grant state, in the person's language. */
107
+ grantDetail?: string | null;
108
+ openAtSourceHref?: string | null;
109
+ /**
110
+ * The per-record label for the "open at source" control, when the
111
+ * registration knows it precisely (chair, 2026-09-18). `DetailBody` derives
112
+ * a label from `source` on its own (see `deriveOpenAtSourceLabel`) for a
113
+ * producer that has no opinion, but that derivation guesses from a family
114
+ * name ("Google Docs, Sheets & Drive files" → "Open in Google") — a
115
+ * registration that knows the row's exact kind (a Doc vs. a Sheet vs. a
116
+ * plain Drive file; a Calendar event) sets this instead and it wins.
117
+ */
118
+ openAtSourceLabel?: string;
119
+ /**
120
+ * The repair offered when the grant is not `ok`. 🚨 THREE-VALUED, AND THE
121
+ * THREE MEAN DIFFERENT THINGS (Bugbot round 18 on frontend PR 228):
122
+ *
123
+ * - a function → offer THIS action;
124
+ * - `null` → offer NOTHING: the producer has decided a reconnect
125
+ * would not repair this refusal (a share the owner must
126
+ * grant, a record we own, an unclassified failure) and a
127
+ * Reconnect button would be a control that does nothing;
128
+ * - omitted → the producer has no opinion; the host's `reconnectSource`
129
+ * port supplies the action when it has one.
130
+ *
131
+ * `useDetailHealth` honours `null` explicitly — it used to fill the host
132
+ * action back in with `??`, which reads `null` as "unset", so a synced record
133
+ * whose refusal a reconnect cannot fix still showed Reconnect.
134
+ */
135
+ onReconnect?: (() => void) | null;
136
+ /** Offered when the record can be refreshed on demand. */
137
+ onRefresh?: (() => void | Promise<void>) | null;
138
+ }
139
+ /** What a `health` producer is told about the record it is answering for. */
140
+ interface DetailHealthContext {
141
+ ref: DetailRef;
142
+ /** Aborted when the record changes or the detail closes. */
143
+ signal: AbortSignal;
144
+ }
145
+ /**
146
+ * The health producer: sync or async, and free to answer `null` for a record
147
+ * that is not synced. A throw or a rejection is rendered as an honest "we could
148
+ * not check the connection" strip, never as silence (`useDetailHealth`).
149
+ */
150
+ type DetailHealthProducer<Row extends DetailRow = DetailRow> = (row: Row, ctx: DetailHealthContext) => DetailSourceHealth | null | Promise<DetailSourceHealth | null>;
151
+ /** An extra section a record type adds under the fixed ones. */
152
+ interface DetailSection {
153
+ id: string;
154
+ label: string;
155
+ content: ReactNode;
156
+ }
157
+ /** What a registration's optional `Frame` and sections receive. */
158
+ interface DetailFrameContext {
159
+ ref: DetailRef;
160
+ recordType: DetailRecordType;
161
+ title: string;
162
+ about: string | null;
163
+ status: DetailStatus;
164
+ fields: DetailField[];
165
+ presentation: DetailPresentation;
166
+ }
167
+ /**
168
+ * ONE registration per record type. The core component (header + fixed
169
+ * sections) is derived from this; a record type never writes presentation
170
+ * code. Everything optional degrades to "section absent", never to a dead
171
+ * or disabled-looking control.
172
+ */
173
+ interface DetailRecordType<Row extends DetailRow = DetailRow> {
174
+ /** The type token this registration answers for (as opened). */
175
+ type: string;
176
+ /** Human label ("Task", "File"). */
177
+ label: string;
178
+ icon: ComponentType<{
179
+ className?: string;
180
+ }>;
181
+ /** Accent classes for the icon and type chip; semantic tokens only. */
182
+ accent?: {
183
+ text: string;
184
+ bg: string;
185
+ ring: string;
186
+ } | null;
187
+ /**
188
+ * The canonical entity-registry token for doors, associations and history.
189
+ * `null` when the type is not a registry entity — the header then offers
190
+ * copy-id only, and the associations / history sections are absent.
191
+ */
192
+ entityToken: string | null;
193
+ /**
194
+ * Load the full row. `null` when the type has no single canonical source
195
+ * (the detail then renders from the seed alone and says so).
196
+ */
197
+ load: ((id: string, signal: AbortSignal) => Promise<DetailLoadResult<Row>>) | null;
198
+ /** The title to show; receives the loaded row (or null) and the seed. */
199
+ title: (row: Row | null, seed: DetailSeed | null) => string;
200
+ /**
201
+ * 🚨 WHAT THIS ROW IS, WHEN THE TYPE CANNOT SAY (chair, 2026-09-18). `label` is
202
+ * per TYPE, and a registered type is regularly a family: `crm.party` holds
203
+ * 1,432 companies and 460 persons, so a type labelled "Person" told a person
204
+ * that a company is a person — twice on screen, above a field reading
205
+ * `organization` (VERIFY-U-P1-R5, N6). A registration that can tell the
206
+ * difference from the row answers here.
207
+ *
208
+ * It names the RECORD, so it is used for the header's type chip and for the
209
+ * stand-in titles ("Company 3f2a1b9c") only. The presentation pane keeps the
210
+ * type-level `label`, because "Every contact record now opens as a window" is a
211
+ * statement about the TYPE's setting, not about the row on screen.
212
+ *
213
+ * `null` (or omitted) falls back to `label`, then to the raw type token.
214
+ */
215
+ labelForRow?: (row: Row | null) => string | null;
216
+ /** The field list for the fields section. */
217
+ fields: (row: Row) => DetailField[];
218
+ /**
219
+ * 🚨 THE SOURCE HEALTH STRIP'S PRODUCER. Source health for a SYNCED record;
220
+ * omit it, or answer `null`, for a record the platform owns outright.
221
+ *
222
+ * It may be ASYNC, because whether the grant behind a synced record still works
223
+ * is not in the row — it is the connector's recorded per-capability health, read
224
+ * from the server. The host wires this field when it registers the record type
225
+ * (never a second reader of the connector's health), and `useDetailHealth`
226
+ * resolves it. Until 2026-09-17 nothing set it, so the strip could not render on
227
+ * any record and the promise that a refusal anywhere shows on every dependent
228
+ * record with the same Reconnect had no witness (VERIFY-U-P1-R4).
229
+ */
230
+ health?: DetailHealthProducer<Row> | null;
231
+ /**
232
+ * Tokens the associations section shows. `null` hides the section.
233
+ * Omitted → the host's default set minus this record's own token.
234
+ */
235
+ associationTokens?: string[] | null;
236
+ /** Show the history section (row versions). Default: when `entityToken` is set. */
237
+ history?: boolean;
238
+ /** Extra sections under the fixed ones. */
239
+ extraSections?: ((row: Row | null, ctx: DetailFrameContext) => DetailSection[]) | null;
240
+ /**
241
+ * Wraps the whole body (surface runtime, right-click menu, …). Receives the
242
+ * live context so a host can expose the dossier to its agents. Optional.
243
+ */
244
+ Frame?: ComponentType<{
245
+ ctx: DetailFrameContext;
246
+ children: ReactNode;
247
+ }> | null;
248
+ }
249
+ type DetailStatus = "loading" | "ready" | "not-found" | "error"
250
+ /** The type has no `load` — rendered from the seed alone. */
251
+ | "none";
252
+ type DetailLoadState<Row extends DetailRow = DetailRow> = {
253
+ status: "loading";
254
+ } | {
255
+ status: "ready";
256
+ row: Row;
257
+ } | {
258
+ status: "not-found";
259
+ } | {
260
+ status: "error";
261
+ message: string;
262
+ } | {
263
+ status: "none";
264
+ };
265
+ /** One row of the history section. */
266
+ interface DetailHistoryEntry {
267
+ version: number;
268
+ operation: string;
269
+ actorId: string | null;
270
+ occurredAt: string;
271
+ isCurrent: boolean;
272
+ }
273
+
274
+ /** Props every shell receives; the package fills the slots, the host owns the chrome. */
275
+ interface DetailShellSlots {
276
+ /** Accessible plain-text title (window chrome, drawer title, document title). */
277
+ title: string;
278
+ /** Rich title: icon · name · type chip · the record's doors. */
279
+ titleNode: ReactNode;
280
+ /** Presentation switcher · previous / next · copy id. Compact. */
281
+ actions: ReactNode;
282
+ children: ReactNode;
283
+ onClose: () => void;
284
+ }
285
+ interface DetailWindowShellProps extends DetailShellSlots {
286
+ /** `type.id` — the deep-link instance and the window manager's key. */
287
+ instanceKey: string;
288
+ target: DetailRef;
289
+ /**
290
+ * 🚨 NEW-15 — THE LIST TRAVELS INTO THE DEEP LINK. The window's `?panels=`
291
+ * token used to carry the record and the presentation only, so a REFRESH lost
292
+ * the previous / next controls and the counter without a word. The shell puts
293
+ * this into the token the way the page presentation puts it into the query.
294
+ */
295
+ list: DetailListContext | null;
296
+ }
297
+ interface DetailDockedShellProps extends DetailShellSlots {
298
+ instanceKey: string;
299
+ target: DetailRef;
300
+ }
301
+ interface DetailPageShellProps extends DetailShellSlots {
302
+ target: DetailRef;
303
+ /** Back affordance — the page is the one presentation that left the URL. */
304
+ onBack: () => void;
305
+ }
306
+ interface DetailEffectivePresentation {
307
+ /** The effective presentation for this type, `undefined` when unresolved. */
308
+ value: DetailPresentation | undefined;
309
+ /** The per-record-type exception still in effect, from ANY rung. */
310
+ forType: DetailPresentation | undefined;
311
+ }
312
+ interface DetailPresentationSetting {
313
+ /** `undefined` while unresolved; `error` names the failure — never both silent. */
314
+ value: DetailPresentation | undefined;
315
+ error: string | null;
316
+ /**
317
+ * The PER-RECORD-TYPE exception in effect for this type, when there is one
318
+ * (`undefined` = this type opens the way the default says). The pane offers
319
+ * "use the default for this type" only then — an offer to remove something
320
+ * that does not exist is a control that does nothing (NEW-2).
321
+ */
322
+ forType?: DetailPresentation | undefined;
323
+ }
324
+ interface DetailShells {
325
+ Window?: ComponentType<DetailWindowShellProps>;
326
+ Docked?: ComponentType<DetailDockedShellProps>;
327
+ Page?: ComponentType<DetailPageShellProps>;
328
+ }
329
+ interface DetailHostPorts {
330
+ /**
331
+ * The ONE record-type map. `null` = the host has no registration for the
332
+ * type. Bound by each presentation's entry (with its shell), not at boot —
333
+ * the map pulls the registry and the frame into whatever imports it.
334
+ */
335
+ resolveType?: (type: string) => DetailRecordType | null;
336
+ /** React face — the effective `ui.detail.default_presentation` for a type. A hook. */
337
+ usePresentationSetting: (type: string) => DetailPresentationSetting;
338
+ /** Awaited face for click handlers. Rejects with the resolver's error. */
339
+ resolvePresentation: (type: string) => Promise<DetailPresentation>;
340
+ /** Warm the cache so the click path is synchronous. */
341
+ warmPresentation: (type: string) => void;
342
+ /**
343
+ * 🚨 NEW-10 (VERIFY-U-P1-R3) — RE-READ THE LADDER AFTER A WRITE, AND SAY WHAT
344
+ * ANSWERS NOW. Removing your own per-type exception while your ORGANIZATION
345
+ * holds one for the same type succeeded and the pane promised the records
346
+ * "now open the way you normally open records" — they did not; the
347
+ * organization's entry still won on the next open and nothing re-read the
348
+ * ladder. A host binds this to a FRESH ladder read (cache invalidated), never
349
+ * to the cached value the write just made stale. Optional: a host that cannot
350
+ * re-read says so and the pane promises nothing.
351
+ */
352
+ reReadPresentation?: (type: string) => Promise<DetailEffectivePresentation>;
353
+ /**
354
+ * Write the person's presentation setting from inside a detail (the ONE
355
+ * screen that writes `ui.detail.default_presentation`). `forType` non-null
356
+ * writes the PER-RECORD-TYPE override instead of the default.
357
+ *
358
+ * `clear` with a `forType` REMOVES that record type's exception instead of
359
+ * setting one, so an exception set from a record can be taken back from the
360
+ * same place (NEW-2). It is the same map-entry write with the key absent — a
361
+ * host never grows a second writer for it.
362
+ *
363
+ * Optional: a host that can only READ the setting binds nothing and the pane
364
+ * is absent — never a disabled-looking control. A refusal comes back as
365
+ * `{ ok: false, reason }` (the settings ladder's reasons are sentences and
366
+ * the surface renders them), never as a thrown error.
367
+ */
368
+ savePresentation?: (args: {
369
+ presentation: DetailPresentation;
370
+ forType: string | null;
371
+ clear?: boolean;
372
+ }) => Promise<{
373
+ ok: true;
374
+ } | {
375
+ ok: false;
376
+ reason: string;
377
+ }>;
378
+ /** Open an in-place presentation (window / docked). Page goes through `navigate`. */
379
+ open: (args: {
380
+ presentation: Exclude<DetailPresentation, "page">;
381
+ data: DetailInstanceData;
382
+ }) => void;
383
+ close: (presentation: Exclude<DetailPresentation, "page">) => void;
384
+ navigate: {
385
+ pageHref: (ref: DetailRef, extra?: {
386
+ list?: DetailListContext | null;
387
+ }) => string;
388
+ toPage: (ref: DetailRef, extra?: {
389
+ seed?: DetailSeed | null;
390
+ list?: DetailListContext | null;
391
+ /**
392
+ * 🚨 NEW-14 (VERIFY-U-P1-R3) — MOVING INSIDE A LIST REPLACES, NEVER
393
+ * PUSHES. Arrowing to a neighbour on the page pushed a history entry,
394
+ * so twenty records meant twenty Backs and the chevron labelled "Back"
395
+ * returned to the previous RECORD instead of the list. The record whose
396
+ * history entry is being replaced, so the host can carry its "this tab
397
+ * pushed a detail page" answer (`canGoBack`) forward to the record now
398
+ * showing. Absent ⇒ a push, which is what opening a page IS.
399
+ */
400
+ replacing?: DetailRef | null;
401
+ }) => void;
402
+ back: () => void;
403
+ /**
404
+ * 🚨 D1. Whether THIS tab reached the detail page from inside the app, so
405
+ * `back()` lands on a screen the person was actually on. A deep link
406
+ * opened in a fresh tab has NOTHING behind it: `back()` there leaves the
407
+ * tab on `about:blank` with the whole app gone (measured 2026-09-17).
408
+ * A host answers `false` whenever it is not certain.
409
+ */
410
+ canGoBack: (ref: DetailRef) => boolean;
411
+ /**
412
+ * Where the page goes when there is nothing behind it: the record's own
413
+ * home (its canonical route), or the host's home when the type has none.
414
+ * REPLACES the detail page — it is being left, not stacked on.
415
+ */
416
+ toRecordHome: (ref: DetailRef, entityToken: string | null) => void;
417
+ };
418
+ shells: DetailShells;
419
+ doors: {
420
+ /** The record's own doors (open / new tab / peek) — sibling of the title. */
421
+ RecordDoors: ComponentType<{
422
+ token: string;
423
+ id: string;
424
+ name?: string | null;
425
+ }>;
426
+ /** A reference inside the record — never a bare uuid. */
427
+ /**
428
+ * One id rendered as a door. `name` is the record's own name when the caller
429
+ * has it — a door showing a truncated uuid is half a dead end (chair,
430
+ * 2026-09-18), so a host that can name the record renders the name and keeps
431
+ * the id for the copy control.
432
+ */
433
+ RefCell: ComponentType<{
434
+ value: string;
435
+ label: string;
436
+ token: string;
437
+ name?: string | null;
438
+ }>;
439
+ tokenFromColumnName: (column: string) => string | null;
440
+ isUuidValue: (value: unknown) => value is string;
441
+ /**
442
+ * 🚨 NEW-17 (VERIFY-U-P1-R4) — WHETHER THIS RECORD REALLY OPENS ANYWHERE.
443
+ * `RecordDoors` renders nothing when the token has no route and no peek, so
444
+ * the honest absent state promised "the controls above still open it where it
445
+ * lives" beside a header that carried no such control — for `session`, and for
446
+ * every unregistered type. The body's sentence is derived from this answer,
447
+ * never fixed.
448
+ */
449
+ hasDoor: (token: string, id: string) => boolean;
450
+ };
451
+ associations: {
452
+ /** Tokens shown when a registration does not name its own. */
453
+ defaultTokens: readonly string[];
454
+ /** Whether a token may anchor an associations section at all. */
455
+ canAnchor: (token: string) => boolean;
456
+ };
457
+ history: {
458
+ list: (token: string, id: string, signal: AbortSignal) => Promise<DetailHistoryEntry[]>;
459
+ /**
460
+ * 🚨 NEW-23 (VERIFY-U-P1-R5) — WHO MADE THE CHANGE IS A PERSON, NOT AN ID.
461
+ * Every history row printed a bare 36-character uuid under "Changed by",
462
+ * because no `actor` or `user` token has a door in a host's registry: an
463
+ * identity the UI names that opens nothing and says nothing (the
464
+ * no-dead-ends class). A host binds its ONE existing identity resolver here
465
+ * — never a second one — and the row shows the person's name. Optional: a
466
+ * host that has no directory binds nothing and the row keeps the id under a
467
+ * title that says what it is, which is honest rather than silent.
468
+ *
469
+ * It is given the record's own row too, because the directory a host may
470
+ * legitimately read is usually the owning organization's members and the
471
+ * row is where that organization is named.
472
+ */
473
+ ActorName?: ComponentType<{
474
+ actorId: string;
475
+ row: DetailRow | null;
476
+ }>;
477
+ };
478
+ notify: {
479
+ error: (message: string) => void;
480
+ success: (message: string) => void;
481
+ };
482
+ copyText: (text: string) => Promise<boolean>;
483
+ /**
484
+ * 🚨 THE SAME RECONNECT THE CONNECTOR ROWS SHOW. A synced record whose grant
485
+ * has expired, been revoked or lost a scope offers exactly the repair the
486
+ * connector surface offers, from the record's own health strip, so a person
487
+ * never has to go and find the connections screen. Optional: a host that
488
+ * cannot open a consent flow binds nothing and the strip states the problem
489
+ * without offering a control that would not help (law 4). A producer that
490
+ * answers `onReconnect: null` overrides this for its record — see
491
+ * `DetailSourceHealth.onReconnect`.
492
+ */
493
+ reconnectSource?: (ref: DetailRef, source: string) => void;
494
+ /**
495
+ * Where the developer goes to fix an unregistered or sourceless type — a
496
+ * phrase for the console remedy the core prints once per type per tab (the
497
+ * PERSON sees a plain sentence; the developer sees this). Optional: the
498
+ * default names the `resolveType` port. A host names its own registry, e.g.
499
+ * "the item registry (features/item-presentation/registry.tsx)".
500
+ */
501
+ remedy?: {
502
+ typeMap: string;
503
+ };
504
+ }
505
+ /**
506
+ * Binds ports. Nested providers merge over their parent, shells included, so
507
+ * a presentation entry can add only its own shell.
508
+ */
509
+ declare function DetailHostProvider({ ports, children, }: {
510
+ ports: Partial<DetailHostPorts>;
511
+ children: ReactNode;
512
+ }): react.JSX.Element;
513
+ /**
514
+ * The bound ports. Throws with the remedy when a port is missing — a detail
515
+ * that opened with no host would otherwise render every section empty and
516
+ * look finished.
517
+ */
518
+ declare function useDetailHost(): DetailHostPorts;
519
+ /** The record-type map, or an error naming which entry forgot to bind it. */
520
+ declare function requireResolveType(host: DetailHostPorts): NonNullable<DetailHostPorts["resolveType"]>;
521
+ /** A presentation's shell, or an error naming which entry forgot to bind it. */
522
+ declare function requireShell<K extends keyof DetailShells>(shells: DetailShells, which: K): NonNullable<DetailShells[K]>;
523
+
524
+ export { type DetailRef as D, type DetailInstanceData as a, type DetailPresentation as b, type DetailRecordType as c, type DetailStatus as d, type DetailRow as e, type DetailField as f, type DetailSourceHealth as g, type DetailFrameContext as h, type DetailListContext as i, type DetailLoadState as j, type DetailOpenRequest as k, type DetailDockedShellProps as l, type DetailEffectivePresentation as m, type DetailHostPorts as n, DetailHostProvider as o, type DetailPageShellProps as p, type DetailPresentationSetting as q, type DetailShellSlots as r, type DetailShells as s, type DetailWindowShellProps as t, requireResolveType as u, requireShell as v, useDetailHost as w };