@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/dist/react.js ADDED
@@ -0,0 +1,1553 @@
1
+ "use client";
2
+
3
+ // src/react/host.tsx
4
+ import { createContext, useContext } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ var CONTEXT_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.detail.host-context");
7
+ var DetailHostContext = globalThis[CONTEXT_SLOT] ??= createContext(null);
8
+ function DetailHostProvider({
9
+ ports,
10
+ children
11
+ }) {
12
+ const parent = useContext(DetailHostContext);
13
+ const merged = {
14
+ ...parent ?? {},
15
+ ...ports,
16
+ shells: { ...parent?.shells ?? {}, ...ports.shells ?? {} }
17
+ };
18
+ return /* @__PURE__ */ jsx(DetailHostContext.Provider, { value: merged, children });
19
+ }
20
+ var REQUIRED_PORTS = [
21
+ "usePresentationSetting",
22
+ "resolvePresentation",
23
+ "warmPresentation",
24
+ "open",
25
+ "close",
26
+ "navigate",
27
+ "doors",
28
+ "associations",
29
+ "history",
30
+ "notify",
31
+ "copyText"
32
+ ];
33
+ function useDetailHost() {
34
+ const ports = useContext(DetailHostContext);
35
+ if (!ports) {
36
+ throw new Error(
37
+ "[detail] No DetailHostProvider above this component. Mount the host binding \u2014 a <DetailHostProvider ports={\u2026}> carrying every required port (README \u2192 'Binding the ports') \u2014 around the tree that opens or renders record details."
38
+ );
39
+ }
40
+ const missing = REQUIRED_PORTS.filter((key) => ports[key] === void 0);
41
+ if (missing.length > 0) {
42
+ throw new Error(
43
+ `[detail] DetailHostProvider is missing the port(s): ${missing.join(", ")}. Bind them in the host binding (README \u2192 'Binding the ports').`
44
+ );
45
+ }
46
+ return ports;
47
+ }
48
+ function requireResolveType(host) {
49
+ if (!host.resolveType) {
50
+ throw new Error(
51
+ "[detail] No resolveType port is bound. A presentation's entry must pass `resolveType` (the host's ONE record-type map) beside its shell."
52
+ );
53
+ }
54
+ return host.resolveType;
55
+ }
56
+ function requireShell(shells, which) {
57
+ const shell = shells[which];
58
+ if (!shell) {
59
+ throw new Error(
60
+ `[detail] No ${which} shell is bound. The ${which} presentation's entry must wrap itself in <DetailHostProvider ports={{ shells: { ${which} } }}> (README \u2192 'The three shells').`
61
+ );
62
+ }
63
+ return shell;
64
+ }
65
+
66
+ // src/react/useDetailCore.ts
67
+ import { useEffect as useEffect3 } from "react";
68
+
69
+ // src/react/useDetailHealth.ts
70
+ import { useEffect, useRef, useState } from "react";
71
+ function reconnectFor(value, hostReconnect) {
72
+ if (value.grant === "blocked") return null;
73
+ if (value.onReconnect !== void 0) return value.onReconnect;
74
+ return hostReconnect ? () => hostReconnect(value.source) : null;
75
+ }
76
+ function useDetailHealth({
77
+ recordType,
78
+ row,
79
+ ref,
80
+ reconnect
81
+ }) {
82
+ const [health, setHealth] = useState(null);
83
+ const producer = recordType?.health ?? null;
84
+ const key = `${ref.type}:${ref.id}`;
85
+ const producerRef = useRef(producer);
86
+ producerRef.current = producer;
87
+ const reconnectRef = useRef(reconnect);
88
+ reconnectRef.current = reconnect;
89
+ const producerKey = producer ? `${recordType?.type ?? ""}:${key}` : null;
90
+ useEffect(() => {
91
+ const producer2 = producerRef.current;
92
+ const reconnect2 = reconnectRef.current;
93
+ if (!producer2 || !row) {
94
+ setHealth(null);
95
+ return void 0;
96
+ }
97
+ const controller = new AbortController();
98
+ let settled = false;
99
+ const onReconnect = reconnect2 ? (source) => reconnect2(ref, source) : null;
100
+ const publish = (value) => {
101
+ if (controller.signal.aborted) return;
102
+ settled = true;
103
+ setHealth(value ? { ...value, onReconnect: reconnectFor(value, onReconnect) } : null);
104
+ };
105
+ try {
106
+ const answer = producer2(row, { ref, signal: controller.signal });
107
+ if (answer && typeof answer.then === "function") {
108
+ void answer.then(publish).catch(
109
+ (error) => {
110
+ if (controller.signal.aborted) return;
111
+ publish(unreadable(error, onReconnect));
112
+ }
113
+ );
114
+ } else {
115
+ publish(answer ?? null);
116
+ }
117
+ } catch (error) {
118
+ publish(unreadable(error, onReconnect));
119
+ }
120
+ return () => {
121
+ controller.abort();
122
+ if (!settled) setHealth(null);
123
+ };
124
+ }, [producerKey, row, key]);
125
+ return health;
126
+ }
127
+ function unreadable(error, onReconnect) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ const source = "its source";
130
+ return {
131
+ source: "Source",
132
+ grant: "unknown",
133
+ grantDetail: `We could not check the connection this record refreshes through, so we cannot say whether it is still working (${message}). Reconnecting is safe and fixes it if the connection is the problem.`,
134
+ lastRefreshedAt: null,
135
+ onReconnect: onReconnect ? () => onReconnect(source) : null
136
+ };
137
+ }
138
+
139
+ // src/react/useDetailKeyboard.ts
140
+ import { useCallback, useRef as useRef2 } from "react";
141
+ var PREV_ARROWS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowLeft"]);
142
+ var NEXT_ARROWS = /* @__PURE__ */ new Set(["ArrowDown", "ArrowRight"]);
143
+ var PREV_BRACKET = "[";
144
+ var NEXT_BRACKET = "]";
145
+ function isTypingTarget(target) {
146
+ if (!(target instanceof HTMLElement)) return false;
147
+ if (target.isContentEditable) return true;
148
+ const tag = target.tagName;
149
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
150
+ }
151
+ var NON_TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
152
+ "checkbox",
153
+ "radio",
154
+ "button",
155
+ "submit",
156
+ "reset",
157
+ "range",
158
+ "color",
159
+ "file",
160
+ "image",
161
+ "hidden"
162
+ ]);
163
+ var ESCAPE_OWNING_ANCESTORS = '[data-radix-popper-content-wrapper], [role="menu"], [role="menubar"], [role="listbox"], [role="tree"], [role="grid"]';
164
+ function isOpenTrigger(element) {
165
+ if (element.getAttribute("aria-expanded") !== "true") return false;
166
+ const role = element.getAttribute("role");
167
+ if (role === "combobox") return true;
168
+ const haspopup = element.getAttribute("aria-haspopup");
169
+ return haspopup !== null && haspopup !== "false" && haspopup !== "dialog";
170
+ }
171
+ function escapeBelongsToTheControl(target) {
172
+ if (!(target instanceof HTMLElement)) return false;
173
+ if (target.tagName === "SELECT") return true;
174
+ if (isOpenTrigger(target)) return true;
175
+ if (target.closest(ESCAPE_OWNING_ANCESTORS)) return true;
176
+ if (target.isContentEditable) return true;
177
+ if (target instanceof HTMLTextAreaElement) return target.value !== target.defaultValue;
178
+ if (target instanceof HTMLInputElement) {
179
+ if (NON_TEXT_INPUT_TYPES.has(target.type)) return false;
180
+ return target.value !== target.defaultValue;
181
+ }
182
+ return false;
183
+ }
184
+ function isScrollTarget(element) {
185
+ if (element.scrollHeight - element.clientHeight <= 1) return false;
186
+ const overflowY = typeof window === "undefined" ? "" : window.getComputedStyle(element).overflowY;
187
+ return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
188
+ }
189
+ function bodyIsTheScrollTarget(target) {
190
+ let node = target instanceof HTMLElement ? target : null;
191
+ while (node) {
192
+ if (isScrollTarget(node)) return true;
193
+ node = node.parentElement;
194
+ }
195
+ return false;
196
+ }
197
+ function useDetailKeyboard(handlers) {
198
+ const saveRef = useRef2(null);
199
+ const handlersRef = useRef2(handlers);
200
+ handlersRef.current = handlers;
201
+ const onKeyDownCapture = useCallback((event) => {
202
+ const h = handlersRef.current;
203
+ const meta = event.metaKey || event.ctrlKey;
204
+ if (meta && event.key === "Enter") {
205
+ event.preventDefault();
206
+ event.stopPropagation();
207
+ if (saveRef.current) void saveRef.current();
208
+ else h.onSaveUnavailable?.();
209
+ return;
210
+ }
211
+ if (event.key === "Escape") {
212
+ if (!h.onClose || escapeBelongsToTheControl(event.target)) return;
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ h.onClose();
216
+ return;
217
+ }
218
+ if (isTypingTarget(event.target)) return;
219
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
220
+ if (event.key === PREV_BRACKET && h.onPrev) {
221
+ event.preventDefault();
222
+ h.onPrev();
223
+ return;
224
+ }
225
+ if (event.key === NEXT_BRACKET && h.onNext) {
226
+ event.preventDefault();
227
+ h.onNext();
228
+ return;
229
+ }
230
+ const prev = PREV_ARROWS.has(event.key);
231
+ const next = NEXT_ARROWS.has(event.key);
232
+ if (!prev && !next) return;
233
+ if (bodyIsTheScrollTarget(event.target)) return;
234
+ if (prev && h.onPrev) {
235
+ event.preventDefault();
236
+ h.onPrev();
237
+ return;
238
+ }
239
+ if (next && h.onNext) {
240
+ event.preventDefault();
241
+ h.onNext();
242
+ }
243
+ }, []);
244
+ const registerSave = useCallback((save) => {
245
+ saveRef.current = save;
246
+ return () => {
247
+ if (saveRef.current === save) saveRef.current = null;
248
+ };
249
+ }, []);
250
+ const hasSave = useCallback(() => saveRef.current !== null, []);
251
+ return {
252
+ rootProps: { tabIndex: -1, onKeyDownCapture },
253
+ registerSave,
254
+ hasSave
255
+ };
256
+ }
257
+
258
+ // src/react/useDetailRecord.ts
259
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
260
+ function useDetailRecord(recordType, id) {
261
+ const load = recordType?.load ?? null;
262
+ const [state, setState] = useState2(
263
+ () => load && id ? { status: "loading" } : { status: "none" }
264
+ );
265
+ const requestKey = `${recordType?.type ?? ""}|${id ?? ""}|${load ? "src" : "none"}`;
266
+ const [seenKey, setSeenKey] = useState2(requestKey);
267
+ if (seenKey !== requestKey) {
268
+ setSeenKey(requestKey);
269
+ setState(load && id ? { status: "loading" } : { status: "none" });
270
+ }
271
+ const loadRef = useRef3(load);
272
+ loadRef.current = load;
273
+ useEffect2(() => {
274
+ const load2 = loadRef.current;
275
+ if (!load2 || !id) return void 0;
276
+ const controller = new AbortController();
277
+ void (async () => {
278
+ try {
279
+ const result = await load2(id, controller.signal);
280
+ if (controller.signal.aborted) return;
281
+ if ("notFound" in result) {
282
+ setState({ status: "not-found" });
283
+ } else {
284
+ setState({ status: "ready", row: result.row });
285
+ }
286
+ } catch (error) {
287
+ if (controller.signal.aborted) return;
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ console.error("[detail] record load failed", { type: recordType?.type, id, error });
290
+ setState({ status: "error", message });
291
+ }
292
+ })();
293
+ return () => controller.abort();
294
+ }, [requestKey, id]);
295
+ return state;
296
+ }
297
+
298
+ // src/react/useDetailCore.ts
299
+ function entityTokenOf(recordType) {
300
+ return recordType?.entityToken ?? null;
301
+ }
302
+ var ANNOUNCED_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.detail.announced-remedies");
303
+ var announced = globalThis[ANNOUNCED_SLOT] ??= /* @__PURE__ */ new Set();
304
+ function announceOnce(key, message) {
305
+ if (announced.has(key)) return;
306
+ announced.add(key);
307
+ console.warn(message);
308
+ }
309
+ function neighbour(list, delta) {
310
+ if (!list) return null;
311
+ const target = list.index + delta;
312
+ if (target < 0 || target >= list.items.length) return null;
313
+ return list.items[target] ?? null;
314
+ }
315
+ var DEFAULT_TYPE_MAP_REMEDY = "the host's `resolveType` map (the DetailHostProvider's record-type port)";
316
+ function useDetailCore(data, presentation, options) {
317
+ const host = useDetailHost();
318
+ const ref = { type: data.type, id: data.id };
319
+ const recordType = requireResolveType(host)(data.type);
320
+ const state = useDetailRecord(recordType, data.id);
321
+ const row = state.status === "ready" ? state.row : null;
322
+ const typeLabel = recordType?.label ?? data.type;
323
+ const seedName = data.seed?.name?.trim() || null;
324
+ const loadedTitle = state.status === "ready" && recordType ? recordType.title(row, data.seed) : null;
325
+ const shortId = data.id.length > 8 ? data.id.slice(0, 8) : data.id;
326
+ const recordLabel = row && recordType?.labelForRow?.(row) || typeLabel;
327
+ const standInTitle = state.status === "not-found" ? `This ${recordLabel.toLowerCase()} could not be found` : state.status === "error" ? `This ${recordLabel.toLowerCase()} could not be loaded` : state.status === "none" ? `${recordLabel} ${shortId}` : `Loading this ${recordLabel.toLowerCase()}\u2026`;
328
+ const titleIsStandIn = !loadedTitle && !seedName;
329
+ const title = loadedTitle ?? seedName ?? standInTitle;
330
+ const doorsAvailable = entityTokenOf(recordType) !== null && state.status !== "not-found" && state.status !== "error";
331
+ const canOpenElsewhere = doorsAvailable && entityTokenOf(recordType) !== null && host.doors.hasDoor(entityTokenOf(recordType), data.id);
332
+ const about = data.seed?.about?.trim() || null;
333
+ const fields = row && recordType ? recordType.fields(row) : [];
334
+ const health = useDetailHealth({
335
+ recordType,
336
+ row,
337
+ ref,
338
+ reconnect: host.reconnectSource ?? null
339
+ });
340
+ const entityToken = entityTokenOf(recordType);
341
+ const frameCtx = recordType ? {
342
+ ref,
343
+ recordType,
344
+ title,
345
+ about,
346
+ status: state.status,
347
+ fields,
348
+ presentation
349
+ } : null;
350
+ const typeMapRemedy = host.remedy?.typeMap ?? DEFAULT_TYPE_MAP_REMEDY;
351
+ useEffect3(() => {
352
+ if (!recordType) {
353
+ announceOnce(
354
+ `no-type:${data.type}`,
355
+ `[detail] Nothing is registered for the record type "${data.type}", so its detail shows the type, the id and the doors only. Remedy: add an entry for it in ${typeMapRemedy}.`
356
+ );
357
+ return;
358
+ }
359
+ if (state.status === "none") {
360
+ announceOnce(
361
+ `no-source:${data.type}`,
362
+ `[detail] The record type "${data.type}" has no \`load\` (no single canonical source), so its detail can show nothing beyond what the opener already knew. Remedy: give the type a load in ${typeMapRemedy}, or leave it sourceless deliberately and say why there.`
363
+ );
364
+ }
365
+ }, [recordType, state.status, data.type, typeMapRemedy]);
366
+ const warmPresentation = host.warmPresentation;
367
+ useEffect3(() => {
368
+ warmPresentation(data.type);
369
+ }, [warmPresentation, data.type]);
370
+ const openNeighbour = (delta) => {
371
+ const target = neighbour(data.list, delta);
372
+ if (!target || !data.list) return;
373
+ const list = {
374
+ ...data.list,
375
+ items: data.list.items,
376
+ index: data.list.index + delta
377
+ };
378
+ const next = { ...target, seed: null, list };
379
+ if (presentation === "page") {
380
+ host.navigate.toPage(target, { list, replacing: ref });
381
+ } else {
382
+ host.open({ presentation, data: next });
383
+ }
384
+ };
385
+ const leave = () => {
386
+ if (host.navigate.canGoBack(ref)) host.navigate.back();
387
+ else host.navigate.toRecordHome(ref, entityToken);
388
+ };
389
+ const close = () => {
390
+ if (presentation === "page") {
391
+ leave();
392
+ return;
393
+ }
394
+ if (!options.onClose) {
395
+ throw new Error(
396
+ "[detail] The " + presentation + " presentation was mounted without an `onClose`, so nothing can dismiss it. Pass the overlay's close from the presentation's entry."
397
+ );
398
+ }
399
+ options.onClose();
400
+ };
401
+ const switchTo = (target) => {
402
+ if (target === presentation) return;
403
+ if (target === "page") {
404
+ host.navigate.toPage(ref, { seed: data.seed, list: data.list });
405
+ if (presentation !== "page") host.close(presentation);
406
+ return;
407
+ }
408
+ host.open({ presentation: target, data });
409
+ if (presentation !== "page") {
410
+ host.close(presentation);
411
+ return;
412
+ }
413
+ leave();
414
+ };
415
+ const hasPrev = neighbour(data.list, -1) !== null;
416
+ const hasNext = neighbour(data.list, 1) !== null;
417
+ const keyboard = useDetailKeyboard({
418
+ onClose: close,
419
+ onPrev: hasPrev ? () => openNeighbour(-1) : null,
420
+ onNext: hasNext ? () => openNeighbour(1) : null,
421
+ // 🚨 NEW-22 — Cmd+Enter is one of the three promised bindings; on a record
422
+ // with no editor open it ANSWERS rather than doing nothing in silence.
423
+ onSaveUnavailable: () => host.notify.error(
424
+ `There is nothing to save on this ${typeLabel.toLowerCase()} \u2014 nothing here is being edited. Changes to the record are made where it lives.`
425
+ )
426
+ });
427
+ return {
428
+ ref,
429
+ data,
430
+ presentation,
431
+ recordType,
432
+ status: state.status,
433
+ errorMessage: state.status === "error" ? state.message : null,
434
+ row,
435
+ title,
436
+ titleIsStandIn,
437
+ doorsAvailable,
438
+ canOpenElsewhere,
439
+ typeLabel,
440
+ recordLabel,
441
+ about,
442
+ fields,
443
+ health,
444
+ entityToken,
445
+ frameCtx,
446
+ keyboard,
447
+ list: {
448
+ context: data.list,
449
+ hasPrev,
450
+ hasNext,
451
+ prev: () => openNeighbour(-1),
452
+ next: () => openNeighbour(1)
453
+ },
454
+ switchTo,
455
+ leave,
456
+ close
457
+ };
458
+ }
459
+
460
+ // src/react/useOpenDetail.ts
461
+ import { useEffect as useEffect4 } from "react";
462
+
463
+ // src/types.ts
464
+ var DETAIL_PRESENTATIONS = ["window", "docked", "page"];
465
+ var DETAIL_PRESENTATION_KNOB = "ui.detail.default_presentation";
466
+ var DETAIL_URL_BUDGET_BYTES = 8e3;
467
+
468
+ // src/react/useOpenDetail.ts
469
+ function useOpenDetail(warmType) {
470
+ const host = useDetailHost();
471
+ const warmPresentation = host.warmPresentation;
472
+ useEffect4(() => {
473
+ if (warmType) warmPresentation(warmType);
474
+ }, [warmPresentation, warmType]);
475
+ return async (request) => {
476
+ const data = {
477
+ type: request.type,
478
+ id: request.id,
479
+ seed: request.seed ?? null,
480
+ list: request.list ?? null
481
+ };
482
+ let presentation = request.presentation;
483
+ if (!presentation) {
484
+ try {
485
+ presentation = await host.resolvePresentation(request.type);
486
+ } catch (error) {
487
+ const message = error instanceof Error ? error.message : String(error);
488
+ console.error(`[detail] ${DETAIL_PRESENTATION_KNOB} could not be resolved`, error);
489
+ host.notify.error(
490
+ `Your detail presentation setting could not be read (${DETAIL_PRESENTATION_KNOB}: ${message}). Opening as a window until it can be read; an administrator can register or repair the setting.`
491
+ );
492
+ presentation = "window";
493
+ }
494
+ }
495
+ if (presentation === "page") {
496
+ host.navigate.toPage({ type: data.type, id: data.id }, { seed: data.seed, list: data.list });
497
+ } else {
498
+ host.open({ presentation, data });
499
+ }
500
+ return presentation;
501
+ };
502
+ }
503
+
504
+ // src/react/DetailHeader.tsx
505
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState3 } from "react";
506
+
507
+ // src/react/icons.tsx
508
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
509
+ function glyph(name, children) {
510
+ function Icon(props) {
511
+ return /* @__PURE__ */ jsx2(
512
+ "svg",
513
+ {
514
+ xmlns: "http://www.w3.org/2000/svg",
515
+ width: 24,
516
+ height: 24,
517
+ viewBox: "0 0 24 24",
518
+ fill: "none",
519
+ stroke: "currentColor",
520
+ strokeWidth: 2,
521
+ strokeLinecap: "round",
522
+ strokeLinejoin: "round",
523
+ "aria-hidden": "true",
524
+ "data-detail-icon": name,
525
+ ...props,
526
+ children
527
+ }
528
+ );
529
+ }
530
+ Icon.displayName = `DetailIcon(${name})`;
531
+ return Icon;
532
+ }
533
+ var AlertCircleIcon = glyph(
534
+ "alert-circle",
535
+ /* @__PURE__ */ jsxs(Fragment, { children: [
536
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
537
+ /* @__PURE__ */ jsx2("line", { x1: "12", x2: "12", y1: "8", y2: "12" }),
538
+ /* @__PURE__ */ jsx2("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })
539
+ ] })
540
+ );
541
+ var ExternalLinkIcon = glyph(
542
+ "external-link",
543
+ /* @__PURE__ */ jsxs(Fragment, { children: [
544
+ /* @__PURE__ */ jsx2("path", { d: "M15 3h6v6" }),
545
+ /* @__PURE__ */ jsx2("path", { d: "M10 14 21 3" }),
546
+ /* @__PURE__ */ jsx2("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
547
+ ] })
548
+ );
549
+ var HistoryIcon = glyph(
550
+ "history",
551
+ /* @__PURE__ */ jsxs(Fragment, { children: [
552
+ /* @__PURE__ */ jsx2("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
553
+ /* @__PURE__ */ jsx2("path", { d: "M3 3v5h5" }),
554
+ /* @__PURE__ */ jsx2("path", { d: "M12 7v5l4 2" })
555
+ ] })
556
+ );
557
+ var Link2Icon = glyph(
558
+ "link-2",
559
+ /* @__PURE__ */ jsxs(Fragment, { children: [
560
+ /* @__PURE__ */ jsx2("path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }),
561
+ /* @__PURE__ */ jsx2("path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }),
562
+ /* @__PURE__ */ jsx2("line", { x1: "8", x2: "16", y1: "12", y2: "12" })
563
+ ] })
564
+ );
565
+ var RefreshCwIcon = glyph(
566
+ "refresh-cw",
567
+ /* @__PURE__ */ jsxs(Fragment, { children: [
568
+ /* @__PURE__ */ jsx2("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
569
+ /* @__PURE__ */ jsx2("path", { d: "M21 3v5h-5" }),
570
+ /* @__PURE__ */ jsx2("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
571
+ /* @__PURE__ */ jsx2("path", { d: "M8 16H3v5" })
572
+ ] })
573
+ );
574
+ var SHIELD = "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z";
575
+ var ShieldAlertIcon = glyph(
576
+ "shield-alert",
577
+ /* @__PURE__ */ jsxs(Fragment, { children: [
578
+ /* @__PURE__ */ jsx2("path", { d: SHIELD }),
579
+ /* @__PURE__ */ jsx2("path", { d: "M12 8v4" }),
580
+ /* @__PURE__ */ jsx2("path", { d: "M12 16h.01" })
581
+ ] })
582
+ );
583
+ var ShieldCheckIcon = glyph(
584
+ "shield-check",
585
+ /* @__PURE__ */ jsxs(Fragment, { children: [
586
+ /* @__PURE__ */ jsx2("path", { d: SHIELD }),
587
+ /* @__PURE__ */ jsx2("path", { d: "m9 12 2 2 4-4" })
588
+ ] })
589
+ );
590
+ var AppWindowIcon = glyph(
591
+ "app-window",
592
+ /* @__PURE__ */ jsxs(Fragment, { children: [
593
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
594
+ /* @__PURE__ */ jsx2("path", { d: "M10 4v4" }),
595
+ /* @__PURE__ */ jsx2("path", { d: "M2 8h20" }),
596
+ /* @__PURE__ */ jsx2("path", { d: "M6 4v4" })
597
+ ] })
598
+ );
599
+ var CheckIcon = glyph("check", /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }));
600
+ var ChevronLeftIcon = glyph("chevron-left", /* @__PURE__ */ jsx2("path", { d: "m15 18-6-6 6-6" }));
601
+ var ChevronRightIcon = glyph("chevron-right", /* @__PURE__ */ jsx2("path", { d: "m9 18 6-6-6-6" }));
602
+ var CopyIcon = glyph(
603
+ "copy",
604
+ /* @__PURE__ */ jsxs(Fragment, { children: [
605
+ /* @__PURE__ */ jsx2("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }),
606
+ /* @__PURE__ */ jsx2("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
607
+ ] })
608
+ );
609
+ var ExpandIcon = glyph(
610
+ "expand",
611
+ /* @__PURE__ */ jsxs(Fragment, { children: [
612
+ /* @__PURE__ */ jsx2("path", { d: "m15 15 6 6" }),
613
+ /* @__PURE__ */ jsx2("path", { d: "m15 9 6-6" }),
614
+ /* @__PURE__ */ jsx2("path", { d: "M21 16v5h-5" }),
615
+ /* @__PURE__ */ jsx2("path", { d: "M21 8V3h-5" }),
616
+ /* @__PURE__ */ jsx2("path", { d: "M3 16v5h5" }),
617
+ /* @__PURE__ */ jsx2("path", { d: "m3 21 6-6" }),
618
+ /* @__PURE__ */ jsx2("path", { d: "M3 8V3h5" }),
619
+ /* @__PURE__ */ jsx2("path", { d: "M9 9 3 3" })
620
+ ] })
621
+ );
622
+ var MoreHorizontalIcon = glyph(
623
+ "more-horizontal",
624
+ /* @__PURE__ */ jsxs(Fragment, { children: [
625
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
626
+ /* @__PURE__ */ jsx2("circle", { cx: "19", cy: "12", r: "1" }),
627
+ /* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "1" })
628
+ ] })
629
+ );
630
+ var PanelRightIcon = glyph(
631
+ "panel-right",
632
+ /* @__PURE__ */ jsxs(Fragment, { children: [
633
+ /* @__PURE__ */ jsx2("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
634
+ /* @__PURE__ */ jsx2("path", { d: "M15 3v18" })
635
+ ] })
636
+ );
637
+ var LoaderIcon = glyph("loader", /* @__PURE__ */ jsx2("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }));
638
+ var Settings2Icon = glyph(
639
+ "settings-2",
640
+ /* @__PURE__ */ jsxs(Fragment, { children: [
641
+ /* @__PURE__ */ jsx2("path", { d: "M14 17H5" }),
642
+ /* @__PURE__ */ jsx2("path", { d: "M19 7h-9" }),
643
+ /* @__PURE__ */ jsx2("circle", { cx: "17", cy: "17", r: "3" }),
644
+ /* @__PURE__ */ jsx2("circle", { cx: "7", cy: "7", r: "3" })
645
+ ] })
646
+ );
647
+
648
+ // src/react/DetailHeader.tsx
649
+ import { cn } from "@ai-matrx/design-system";
650
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
651
+ var ICON_BUTTON = "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40 disabled:hover:bg-transparent pointer-coarse:h-10 pointer-coarse:w-10";
652
+ var MENU_ITEM = "flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:py-2.5";
653
+ var TYPE_CHIP = "shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide ring-1 ring-inset";
654
+ var PRESENTATIONS = [
655
+ { value: "window", label: "Open as window", Icon: AppWindowIcon },
656
+ { value: "docked", label: "Open docked to the side", Icon: PanelRightIcon },
657
+ { value: "page", label: "Open as page", Icon: ExpandIcon }
658
+ ];
659
+ function TypeChip({ core, className }) {
660
+ const accent = core.recordType?.accent ?? null;
661
+ return /* @__PURE__ */ jsx3(
662
+ "span",
663
+ {
664
+ className: cn(
665
+ TYPE_CHIP,
666
+ accent?.bg ?? "bg-muted",
667
+ accent?.text ?? "text-muted-foreground",
668
+ accent?.ring ?? "ring-border",
669
+ className
670
+ ),
671
+ "data-detail-type-chip": true,
672
+ children: core.recordLabel
673
+ }
674
+ );
675
+ }
676
+ function DetailTitle({ core }) {
677
+ const host = useDetailHost();
678
+ const Icon = core.recordType?.icon ?? null;
679
+ const accent = core.recordType?.accent ?? null;
680
+ const RecordDoors = host.doors.RecordDoors;
681
+ return /* @__PURE__ */ jsxs2("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
682
+ Icon ? /* @__PURE__ */ jsx3(Icon, { className: cn("h-4 w-4 shrink-0", accent?.text) }) : null,
683
+ /* @__PURE__ */ jsx3(
684
+ "span",
685
+ {
686
+ className: cn(
687
+ "flex-1 truncate text-sm font-medium min-w-[6rem]",
688
+ core.titleIsStandIn ? "text-muted-foreground" : "text-foreground"
689
+ ),
690
+ "data-detail-title": true,
691
+ "data-detail-title-standin": core.titleIsStandIn ? "true" : void 0,
692
+ children: core.title
693
+ }
694
+ ),
695
+ /* @__PURE__ */ jsx3(TypeChip, { core, className: "hidden sm:inline-block" }),
696
+ core.entityToken && core.doorsAvailable ? /* @__PURE__ */ jsx3("span", { className: "hidden shrink-0 sm:inline-flex", children: /* @__PURE__ */ jsx3(RecordDoors, { token: core.entityToken, id: core.ref.id, name: core.title }) }) : null
697
+ ] });
698
+ }
699
+ function DetailRecordMeta({ core }) {
700
+ const host = useDetailHost();
701
+ const RecordDoors = host.doors.RecordDoors;
702
+ const trimmedFrom = core.list.context?.trimmedFrom;
703
+ return /* @__PURE__ */ jsxs2(
704
+ "div",
705
+ {
706
+ className: "flex flex-wrap items-center gap-2 border-b border-border/60 px-4 py-2",
707
+ "data-detail-record-meta": true,
708
+ children: [
709
+ /* @__PURE__ */ jsx3(TypeChip, { core, className: "sm:hidden" }),
710
+ core.entityToken && core.doorsAvailable ? /* @__PURE__ */ jsx3("span", { className: "inline-flex sm:hidden", children: /* @__PURE__ */ jsx3(RecordDoors, { token: core.entityToken, id: core.ref.id, name: core.title }) }) : null,
711
+ /* @__PURE__ */ jsx3("span", { className: "truncate font-mono text-[10px] text-muted-foreground", "data-detail-id": true, children: core.ref.id }),
712
+ trimmedFrom ? /* @__PURE__ */ jsx3("span", { className: "w-full text-[10px] text-muted-foreground", "data-detail-list-trimmed": true, children: `Stepping through ${core.list.context?.items.length} of the ${trimmedFrom} records in the list this was opened from \u2014 a link can only carry that many, so the rest are not reachable from here. Open the list again to reach them.` }) : null
713
+ ]
714
+ }
715
+ );
716
+ }
717
+ function DetailActions({ core }) {
718
+ const host = useDetailHost();
719
+ const [copied, setCopied] = useState3(false);
720
+ const [menuOpen, setMenuOpen] = useState3(false);
721
+ const menuRef = useRef4(null);
722
+ const copyId = async () => {
723
+ const ok = await host.copyText(core.ref.id);
724
+ if (!ok) {
725
+ host.notify.error("Could not copy the record id to the clipboard.");
726
+ return;
727
+ }
728
+ setCopied(true);
729
+ window.setTimeout(() => setCopied(false), 1500);
730
+ };
731
+ useEffect5(() => {
732
+ if (!menuOpen) return void 0;
733
+ const onDown = (event) => {
734
+ if (!(event.target instanceof Node)) return;
735
+ if (menuRef.current && !menuRef.current.contains(event.target)) setMenuOpen(false);
736
+ };
737
+ const onKey = (event) => {
738
+ if (event.key === "Escape") setMenuOpen(false);
739
+ };
740
+ document.addEventListener("mousedown", onDown, true);
741
+ document.addEventListener("keydown", onKey, true);
742
+ return () => {
743
+ document.removeEventListener("mousedown", onDown, true);
744
+ document.removeEventListener("keydown", onKey, true);
745
+ };
746
+ }, [menuOpen]);
747
+ const list = core.list;
748
+ const others = PRESENTATIONS.filter((p) => p.value !== core.presentation);
749
+ return /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-0.5", "data-detail-actions": true, children: [
750
+ list.context ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
751
+ /* @__PURE__ */ jsx3(
752
+ "button",
753
+ {
754
+ type: "button",
755
+ className: ICON_BUTTON,
756
+ onClick: list.prev,
757
+ disabled: !list.hasPrev,
758
+ "aria-label": "Previous record (Up arrow, or left bracket)",
759
+ title: "Previous record \u2191 or [",
760
+ children: /* @__PURE__ */ jsx3(ChevronLeftIcon, { className: "h-4 w-4" })
761
+ }
762
+ ),
763
+ /* @__PURE__ */ jsxs2("span", { className: "px-1 text-[11px] tabular-nums text-muted-foreground", children: [
764
+ list.context.index + 1,
765
+ "/",
766
+ list.context.items.length
767
+ ] }),
768
+ /* @__PURE__ */ jsx3(
769
+ "button",
770
+ {
771
+ type: "button",
772
+ className: ICON_BUTTON,
773
+ onClick: list.next,
774
+ disabled: !list.hasNext,
775
+ "aria-label": "Next record (Down arrow, or right bracket)",
776
+ title: "Next record \u2193 or ]",
777
+ children: /* @__PURE__ */ jsx3(ChevronRightIcon, { className: "h-4 w-4" })
778
+ }
779
+ ),
780
+ /* @__PURE__ */ jsx3("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true })
781
+ ] }) : null,
782
+ /* @__PURE__ */ jsxs2(
783
+ "span",
784
+ {
785
+ className: "flex items-center gap-0.5 @max-[26rem]/window-header:hidden",
786
+ "data-detail-actions-collapsible": true,
787
+ children: [
788
+ others.map(({ value, label, Icon }) => /* @__PURE__ */ jsx3(
789
+ "button",
790
+ {
791
+ type: "button",
792
+ className: ICON_BUTTON,
793
+ onClick: () => core.switchTo(value),
794
+ "aria-label": label,
795
+ title: label,
796
+ children: /* @__PURE__ */ jsx3(Icon, { className: "h-4 w-4" })
797
+ },
798
+ value
799
+ )),
800
+ /* @__PURE__ */ jsx3(
801
+ "button",
802
+ {
803
+ type: "button",
804
+ onClick: () => void copyId(),
805
+ className: ICON_BUTTON,
806
+ "aria-label": "Copy record id",
807
+ title: "Copy record id",
808
+ "data-detail-copy-id": true,
809
+ children: copied ? /* @__PURE__ */ jsx3(CheckIcon, { className: "h-3.5 w-3.5 text-primary" }) : /* @__PURE__ */ jsx3(CopyIcon, { className: "h-3.5 w-3.5" })
810
+ }
811
+ )
812
+ ]
813
+ }
814
+ ),
815
+ /* @__PURE__ */ jsxs2("span", { className: "relative hidden @max-[26rem]/window-header:inline-flex", ref: menuRef, children: [
816
+ /* @__PURE__ */ jsx3(
817
+ "button",
818
+ {
819
+ type: "button",
820
+ className: cn(ICON_BUTTON, "hidden @max-[26rem]/window-header:inline-flex"),
821
+ onClick: () => setMenuOpen((wasOpen) => !wasOpen),
822
+ "aria-label": "More record actions",
823
+ title: "More record actions",
824
+ "aria-haspopup": "menu",
825
+ "aria-expanded": menuOpen,
826
+ "data-detail-actions-overflow": true,
827
+ children: /* @__PURE__ */ jsx3(MoreHorizontalIcon, { className: "h-4 w-4" })
828
+ }
829
+ ),
830
+ menuOpen ? /* @__PURE__ */ jsxs2(
831
+ "div",
832
+ {
833
+ role: "menu",
834
+ "aria-label": "Record actions",
835
+ className: "absolute right-0 top-full z-50 mt-1 min-w-44 rounded-md border border-border bg-card p-1 shadow-md",
836
+ "data-detail-actions-menu": true,
837
+ children: [
838
+ others.map(({ value, label, Icon }) => /* @__PURE__ */ jsxs2(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ role: "menuitem",
843
+ className: MENU_ITEM,
844
+ onClick: () => {
845
+ setMenuOpen(false);
846
+ core.switchTo(value);
847
+ },
848
+ children: [
849
+ /* @__PURE__ */ jsx3(Icon, { className: "h-3.5 w-3.5 shrink-0" }),
850
+ label
851
+ ]
852
+ },
853
+ value
854
+ )),
855
+ /* @__PURE__ */ jsxs2(
856
+ "button",
857
+ {
858
+ type: "button",
859
+ role: "menuitem",
860
+ className: MENU_ITEM,
861
+ onClick: () => {
862
+ setMenuOpen(false);
863
+ void copyId();
864
+ },
865
+ children: [
866
+ /* @__PURE__ */ jsx3(CopyIcon, { className: "h-3.5 w-3.5 shrink-0" }),
867
+ "Copy record id"
868
+ ]
869
+ }
870
+ )
871
+ ]
872
+ }
873
+ ) : null
874
+ ] })
875
+ ] });
876
+ }
877
+
878
+ // src/react/DetailBody.tsx
879
+ import { useEffect as useEffect7, useRef as useRef6, useState as useState5 } from "react";
880
+ import { cn as cn3, Skeleton } from "@ai-matrx/design-system";
881
+ import {
882
+ ASSOCIATION_TARGET_TYPES,
883
+ isEntityTypeToken
884
+ } from "@ai-matrx/associations";
885
+ import { AssociationCardGrid, PrimaryEntityProvider } from "@ai-matrx/associations/react";
886
+
887
+ // src/format.ts
888
+ function formatWhen(iso) {
889
+ const d = new Date(iso);
890
+ if (Number.isNaN(d.getTime())) return iso;
891
+ return d.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
892
+ }
893
+
894
+ // src/react/DetailPresentationPane.tsx
895
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState4 } from "react";
896
+ import { cn as cn2 } from "@ai-matrx/design-system";
897
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
898
+ var WORD = {
899
+ window: "a window",
900
+ docked: "a docked panel",
901
+ page: "a full page"
902
+ };
903
+ var LABEL = {
904
+ window: "Window",
905
+ docked: "Docked",
906
+ page: "Page"
907
+ };
908
+ function DetailPresentationPane({ core }) {
909
+ const host = useDetailHost();
910
+ const save = host.savePresentation;
911
+ const setting = host.usePresentationSetting(core.ref.type);
912
+ const [choice, setChoice] = useState4(null);
913
+ const [forThisType, setForThisType] = useState4(false);
914
+ const [open, setOpen] = useState4(false);
915
+ const [state, setState] = useState4({ status: "idle" });
916
+ const effective = setting.value ?? core.presentation;
917
+ const pending = choice ?? effective;
918
+ const registerSave = core.keyboard.registerSave;
919
+ const hasTypeException = setting.forType !== void 0;
920
+ const commit = async () => {
921
+ if (!save) return;
922
+ setState({ status: "saving" });
923
+ const result = await save({
924
+ presentation: pending,
925
+ forType: forThisType ? core.ref.type : null
926
+ });
927
+ if (result.ok) {
928
+ setState({ status: "saved" });
929
+ host.notify.success(
930
+ forThisType ? `Every ${core.typeLabel.toLowerCase()} now opens as ${WORD[pending]} for you.` : `Record details now open as ${WORD[pending]} for you.`
931
+ );
932
+ } else {
933
+ setState({ status: "refused", reason: result.reason });
934
+ }
935
+ };
936
+ const sentenceAfterRemoval = async () => {
937
+ const reRead = host.reReadPresentation;
938
+ if (!reRead) {
939
+ return `Your own exception for ${core.typeLabel.toLowerCase()} records is gone. How they open now is whatever your organization and your own default say \u2014 open one to see.`;
940
+ }
941
+ try {
942
+ const effective2 = await reRead(core.ref.type);
943
+ if (effective2.forType) {
944
+ return `Your own exception is gone, but ${core.typeLabel.toLowerCase()} records still open as ${WORD[effective2.forType]} because your organization sets that for everyone.`;
945
+ }
946
+ if (effective2.value) {
947
+ return `${core.typeLabel} records now open as ${WORD[effective2.value]}, the way you normally open records.`;
948
+ }
949
+ return `Your own exception for ${core.typeLabel.toLowerCase()} records is gone. The setting that governs them now could not be read, so open one to see how it opens.`;
950
+ } catch (error) {
951
+ const message = error instanceof Error ? error.message : String(error);
952
+ return `Your own exception for ${core.typeLabel.toLowerCase()} records is gone, but what governs them now could not be read (${message}), so open one to see how it opens.`;
953
+ }
954
+ };
955
+ const clearForType = async () => {
956
+ if (!save) return;
957
+ setState({ status: "saving" });
958
+ const result = await save({ presentation: pending, forType: core.ref.type, clear: true });
959
+ if (result.ok) {
960
+ setState({ status: "saved" });
961
+ host.notify.success(await sentenceAfterRemoval());
962
+ } else {
963
+ setState({ status: "refused", reason: result.reason });
964
+ }
965
+ };
966
+ const commitRef = useRef5(commit);
967
+ commitRef.current = commit;
968
+ useEffect6(() => {
969
+ if (!open || !save) return void 0;
970
+ return registerSave(() => commitRef.current());
971
+ }, [open, save, registerSave]);
972
+ if (!save) return null;
973
+ if (!open) {
974
+ return /* @__PURE__ */ jsxs3(
975
+ "button",
976
+ {
977
+ type: "button",
978
+ onClick: () => setOpen(true),
979
+ className: "flex w-full items-center gap-1.5 rounded-md px-1 py-1 text-left text-[11px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
980
+ "data-detail-presentation-pane": "collapsed",
981
+ children: [
982
+ /* @__PURE__ */ jsx4(Settings2Icon, { className: "h-3 w-3 shrink-0" }),
983
+ /* @__PURE__ */ jsx4("span", { children: setting.error ? `Your "open details as" setting could not be read (${setting.error}) \u2014 details are opening as ${WORD[core.presentation]}.` : `Details open as ${WORD[effective]}.` }),
984
+ /* @__PURE__ */ jsx4("span", { className: "underline underline-offset-2", children: "Change" })
985
+ ]
986
+ }
987
+ );
988
+ }
989
+ return /* @__PURE__ */ jsxs3(
990
+ "section",
991
+ {
992
+ className: "space-y-2 rounded-md border border-border bg-muted/30 p-3",
993
+ "data-detail-presentation-pane": "open",
994
+ children: [
995
+ /* @__PURE__ */ jsxs3("h3", { className: "flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: [
996
+ /* @__PURE__ */ jsx4(Settings2Icon, { className: "h-3 w-3" }),
997
+ "How record details open"
998
+ ] }),
999
+ /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Open record details as", children: DETAIL_PRESENTATIONS.map((value) => /* @__PURE__ */ jsx4(
1000
+ "button",
1001
+ {
1002
+ type: "button",
1003
+ "aria-pressed": pending === value,
1004
+ onClick: () => {
1005
+ setChoice(value);
1006
+ setState({ status: "idle" });
1007
+ },
1008
+ className: cn2(
1009
+ "rounded-md px-2 py-1 text-xs transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:py-2",
1010
+ pending === value ? "bg-primary text-primary-foreground" : "bg-card text-muted-foreground hover:text-foreground"
1011
+ ),
1012
+ children: LABEL[value]
1013
+ },
1014
+ value
1015
+ )) }),
1016
+ hasTypeException ? /* @__PURE__ */ jsx4(
1017
+ "button",
1018
+ {
1019
+ type: "button",
1020
+ onClick: () => void clearForType(),
1021
+ disabled: state.status === "saving",
1022
+ className: "text-left text-[11px] text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground disabled:opacity-60",
1023
+ "data-detail-presentation-clear-type": true,
1024
+ children: `Use the default for ${core.typeLabel.toLowerCase()} records`
1025
+ }
1026
+ ) : null,
1027
+ /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1028
+ /* @__PURE__ */ jsx4(
1029
+ "input",
1030
+ {
1031
+ type: "checkbox",
1032
+ checked: forThisType,
1033
+ onChange: (event) => {
1034
+ setForThisType(event.target.checked);
1035
+ setState({ status: "idle" });
1036
+ },
1037
+ className: "h-3.5 w-3.5 accent-[var(--color-primary)]"
1038
+ }
1039
+ ),
1040
+ "Only for ",
1041
+ core.typeLabel.toLowerCase(),
1042
+ " records"
1043
+ ] }),
1044
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1045
+ /* @__PURE__ */ jsxs3(
1046
+ "button",
1047
+ {
1048
+ type: "button",
1049
+ onClick: () => void commit(),
1050
+ disabled: state.status === "saving",
1051
+ className: "inline-flex items-center gap-1 rounded-md bg-primary px-2.5 py-1 text-xs text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-60 pointer-coarse:py-2",
1052
+ children: [
1053
+ state.status === "saving" ? /* @__PURE__ */ jsx4(LoaderIcon, { className: "h-3 w-3 animate-spin" }) : state.status === "saved" ? /* @__PURE__ */ jsx4(CheckIcon, { className: "h-3 w-3" }) : null,
1054
+ "Save"
1055
+ ]
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsx4("span", { className: "text-[11px] text-muted-foreground", children: state.status === "saved" ? "Saved. It applies to the next record you open." : "\u2318/Ctrl + Enter" }),
1059
+ /* @__PURE__ */ jsx4(
1060
+ "button",
1061
+ {
1062
+ type: "button",
1063
+ onClick: () => setOpen(false),
1064
+ className: "ml-auto text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground",
1065
+ children: "Close"
1066
+ }
1067
+ )
1068
+ ] }),
1069
+ state.status === "refused" ? /* @__PURE__ */ jsx4("p", { className: "text-xs text-destructive", "data-detail-presentation-refusal": true, children: state.reason }) : null,
1070
+ setting.error ? /* @__PURE__ */ jsxs3("p", { className: "text-xs text-muted-foreground", children: [
1071
+ "The saved setting could not be read here (",
1072
+ setting.error,
1073
+ "), so this shows what this detail is actually doing."
1074
+ ] }) : null
1075
+ ]
1076
+ }
1077
+ );
1078
+ }
1079
+
1080
+ // src/react/DetailBody.tsx
1081
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1082
+ var SLOW_LOAD_NOTICE_MS = 6e3;
1083
+ function useSlowLoadNotice(active, key) {
1084
+ const [slow, setSlow] = useState5(false);
1085
+ useEffect7(() => {
1086
+ if (!active) {
1087
+ setSlow(false);
1088
+ return void 0;
1089
+ }
1090
+ setSlow(false);
1091
+ const timer = setTimeout(() => setSlow(true), SLOW_LOAD_NOTICE_MS);
1092
+ return () => clearTimeout(timer);
1093
+ }, [active, key]);
1094
+ return slow;
1095
+ }
1096
+ function SectionHeading({ icon, children }) {
1097
+ return /* @__PURE__ */ jsxs4("h3", { className: "flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: [
1098
+ icon,
1099
+ children
1100
+ ] });
1101
+ }
1102
+ function Notice({
1103
+ tone,
1104
+ children
1105
+ }) {
1106
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col items-center gap-2 py-8 text-center", children: [
1107
+ /* @__PURE__ */ jsx5(
1108
+ AlertCircleIcon,
1109
+ {
1110
+ className: cn3(
1111
+ "h-6 w-6",
1112
+ tone === "error" && "text-destructive",
1113
+ tone === "warn" && "text-primary",
1114
+ tone === "muted" && "text-muted-foreground/60"
1115
+ )
1116
+ }
1117
+ ),
1118
+ /* @__PURE__ */ jsx5("p", { className: "max-w-prose text-sm text-muted-foreground", children })
1119
+ ] });
1120
+ }
1121
+ var GRANT_WORD = {
1122
+ ok: "OK",
1123
+ expired: "Expired",
1124
+ revoked: "Revoked",
1125
+ missing: "Missing",
1126
+ unknown: "Unknown",
1127
+ blocked: "Blocked"
1128
+ };
1129
+ function deriveOpenAtSourceLabel(source) {
1130
+ const named = source.trim().replace(/\s+/g, " ");
1131
+ if (!named) return "Open at source";
1132
+ const listsSeveral = /[,&]/.test(named) || named.split(" ").length > 3;
1133
+ const [firstWord = ""] = named.split(" ");
1134
+ const place = listsSeveral ? firstWord.replace(/[,&]+$/, "") : named;
1135
+ return place ? `Open in ${place}` : "Open at source";
1136
+ }
1137
+ function HealthStrip({ health }) {
1138
+ const ok = health.grant === "ok";
1139
+ const [refreshing, setRefreshing] = useState5(false);
1140
+ const host = useDetailHost();
1141
+ const refresh = async () => {
1142
+ if (!health.onRefresh) return;
1143
+ setRefreshing(true);
1144
+ try {
1145
+ await health.onRefresh();
1146
+ } catch (error) {
1147
+ host.notify.error(
1148
+ `Refresh from ${health.source} failed: ${error instanceof Error ? error.message : String(error)}`
1149
+ );
1150
+ } finally {
1151
+ setRefreshing(false);
1152
+ }
1153
+ };
1154
+ return /* @__PURE__ */ jsxs4(
1155
+ "div",
1156
+ {
1157
+ className: cn3(
1158
+ "flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border/60 px-4 py-2 text-xs",
1159
+ ok ? "bg-muted/40 text-muted-foreground" : "bg-destructive/5 text-foreground"
1160
+ ),
1161
+ "data-detail-health": true,
1162
+ children: [
1163
+ ok ? /* @__PURE__ */ jsx5(ShieldCheckIcon, { className: "h-3.5 w-3.5 text-primary" }) : /* @__PURE__ */ jsx5(ShieldAlertIcon, { className: "h-3.5 w-3.5 text-destructive" }),
1164
+ /* @__PURE__ */ jsx5("span", { className: "font-medium", children: health.source }),
1165
+ ok ? null : /* @__PURE__ */ jsx5(
1166
+ "span",
1167
+ {
1168
+ className: "rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-destructive",
1169
+ "data-detail-health-state": health.grant,
1170
+ children: GRANT_WORD[health.grant]
1171
+ }
1172
+ ),
1173
+ health.lastRefreshedAt ? /* @__PURE__ */ jsxs4("span", { children: [
1174
+ "Refreshed ",
1175
+ formatWhen(health.lastRefreshedAt)
1176
+ ] }) : /* @__PURE__ */ jsx5("span", { children: "Never refreshed" }),
1177
+ health.grantDetail ? /* @__PURE__ */ jsx5("span", { children: health.grantDetail }) : null,
1178
+ /* @__PURE__ */ jsxs4("span", { className: "ml-auto flex items-center gap-1", children: [
1179
+ health.onRefresh ? /* @__PURE__ */ jsxs4(
1180
+ "button",
1181
+ {
1182
+ type: "button",
1183
+ onClick: () => void refresh(),
1184
+ disabled: refreshing,
1185
+ className: "inline-flex h-6 items-center gap-1 rounded px-1.5 hover:bg-accent hover:text-foreground disabled:opacity-50 pointer-coarse:h-10",
1186
+ children: [
1187
+ /* @__PURE__ */ jsx5(RefreshCwIcon, { className: cn3("h-3 w-3", refreshing && "animate-spin") }),
1188
+ "Refresh"
1189
+ ]
1190
+ }
1191
+ ) : null,
1192
+ !ok && health.onReconnect ? /* @__PURE__ */ jsx5(
1193
+ "button",
1194
+ {
1195
+ type: "button",
1196
+ onClick: health.onReconnect,
1197
+ className: "inline-flex h-6 items-center gap-1 rounded bg-primary px-2 text-primary-foreground hover:bg-primary/90 pointer-coarse:h-10",
1198
+ children: "Reconnect"
1199
+ }
1200
+ ) : null,
1201
+ health.openAtSourceHref ? /* @__PURE__ */ jsxs4(
1202
+ "a",
1203
+ {
1204
+ href: health.openAtSourceHref,
1205
+ target: "_blank",
1206
+ rel: "noreferrer",
1207
+ className: "inline-flex h-6 items-center gap-1 rounded px-1.5 hover:bg-accent hover:text-foreground pointer-coarse:h-10",
1208
+ children: [
1209
+ /* @__PURE__ */ jsx5(ExternalLinkIcon, { className: "h-3 w-3" }),
1210
+ health.openAtSourceLabel ?? deriveOpenAtSourceLabel(health.source)
1211
+ ]
1212
+ }
1213
+ ) : null
1214
+ ] })
1215
+ ]
1216
+ }
1217
+ );
1218
+ }
1219
+ function whatTheControlsDo(core) {
1220
+ return core.canOpenElsewhere ? "The controls above still open it where it lives, and copy its id." : "The controls above copy its id, so you can find it wherever it came from.";
1221
+ }
1222
+ function FieldsSection({ core }) {
1223
+ const host = useDetailHost();
1224
+ const RefCell = host.doors.RefCell;
1225
+ const label = core.recordType?.label.toLowerCase() ?? "record";
1226
+ if (!core.recordType) {
1227
+ return /* @__PURE__ */ jsx5(Notice, { tone: "warn", children: `This is a ${core.ref.type} record. Nothing more about it is stored here yet, so there is nothing else to show. ${whatTheControlsDo(core)}` });
1228
+ }
1229
+ if (core.status === "loading") {
1230
+ return /* @__PURE__ */ jsx5(LoadingFields, { core, label });
1231
+ }
1232
+ if (core.status === "not-found") {
1233
+ return /* @__PURE__ */ jsx5(Notice, { tone: "warn", children: `This ${label} couldn't be found \u2014 it may have been moved, deleted, or isn't shared with you.` });
1234
+ }
1235
+ if (core.status === "error") {
1236
+ return /* @__PURE__ */ jsx5(Notice, { tone: "error", children: `Couldn't load the details for this ${label}. ${core.errorMessage ?? ""}`.trim() });
1237
+ }
1238
+ if (core.status === "none") {
1239
+ return /* @__PURE__ */ jsx5(Notice, { tone: "muted", children: `Nothing more about this ${label} is stored here, so there is nothing else to show. ` + whatTheControlsDo(core) });
1240
+ }
1241
+ if (core.fields.length === 0) {
1242
+ return /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: "No additional fields to show." });
1243
+ }
1244
+ return /* @__PURE__ */ jsx5("dl", { className: "grid grid-cols-1 gap-x-4 gap-y-2.5", children: core.fields.map((field) => /* @__PURE__ */ jsxs4(
1245
+ "div",
1246
+ {
1247
+ className: "flex flex-col gap-0.5 border-b border-border/40 pb-2 last:border-b-0",
1248
+ children: [
1249
+ /* @__PURE__ */ jsx5("dt", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: field.label }),
1250
+ /* @__PURE__ */ jsx5(
1251
+ "dd",
1252
+ {
1253
+ className: cn3(
1254
+ "break-words text-sm text-foreground",
1255
+ !field.ref && field.mono && "whitespace-pre-wrap rounded-md bg-muted px-2 py-1 font-mono text-xs"
1256
+ ),
1257
+ children: field.ref ? (
1258
+ // 🚨 THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). This rendered
1259
+ // `field.ref.id` alone, so a field whose text was "Acme Robotics"
1260
+ // printed a truncated uuid. The ref's own `name` wins; otherwise the
1261
+ // field's text, unless that text IS the id and says nothing more.
1262
+ /* @__PURE__ */ jsx5(
1263
+ RefCell,
1264
+ {
1265
+ value: field.ref.id,
1266
+ label: field.label,
1267
+ token: field.ref.token,
1268
+ name: field.ref.name ?? (field.text === field.ref.id ? null : field.text)
1269
+ }
1270
+ )
1271
+ ) : field.text
1272
+ }
1273
+ )
1274
+ ]
1275
+ },
1276
+ field.key
1277
+ )) });
1278
+ }
1279
+ function LoadingFields({ core, label }) {
1280
+ const slow = useSlowLoadNotice(true, `${core.ref.type}:${core.ref.id}`);
1281
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-3", "aria-busy": "true", "aria-label": `Loading ${label} details`, children: [
1282
+ slow ? /* @__PURE__ */ jsxs4("p", { className: "text-xs text-muted-foreground", "data-detail-slow-load": true, children: [
1283
+ "Still loading this ",
1284
+ label,
1285
+ " \u2014 the read has been running for more than",
1286
+ " ",
1287
+ Math.round(SLOW_LOAD_NOTICE_MS / 1e3),
1288
+ " seconds. If nothing appears, the record may be unreachable from here; closing and reopening it starts a fresh read."
1289
+ ] }) : null,
1290
+ Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxs4("div", { className: "space-y-1.5", children: [
1291
+ /* @__PURE__ */ jsx5(Skeleton, { className: "h-2.5 w-20" }),
1292
+ /* @__PURE__ */ jsx5(Skeleton, { className: cn3("h-4", i % 2 ? "w-3/4" : "w-1/2") })
1293
+ ] }, i))
1294
+ ] });
1295
+ }
1296
+ function isAssociationTarget(token) {
1297
+ return ASSOCIATION_TARGET_TYPES.includes(token);
1298
+ }
1299
+ function AssociationsSection({ core }) {
1300
+ const host = useDetailHost();
1301
+ const token = core.entityToken;
1302
+ if (!token || !isAssociationTarget(token) || !host.associations.canAnchor(token)) return null;
1303
+ if (core.recordType?.associationTokens === null) return null;
1304
+ const wanted = core.recordType?.associationTokens ?? host.associations.defaultTokens;
1305
+ const tokens = wanted.filter((t) => t !== token).filter(isEntityTypeToken);
1306
+ if (tokens.length === 0) return null;
1307
+ return /* @__PURE__ */ jsxs4("section", { className: "space-y-2", "data-detail-section": "associations", children: [
1308
+ /* @__PURE__ */ jsx5(SectionHeading, { icon: /* @__PURE__ */ jsx5(Link2Icon, { className: "h-3 w-3" }), children: "Linked" }),
1309
+ /* @__PURE__ */ jsx5(PrimaryEntityProvider, { value: { type: token, id: core.ref.id, label: core.title }, children: /* @__PURE__ */ jsx5(AssociationCardGrid, { tokens }) })
1310
+ ] });
1311
+ }
1312
+ var ACTOR_COLUMNS = ["actor_id", "user_id", "person_id", "created_by_id"];
1313
+ function HistorySection({ core }) {
1314
+ const host = useDetailHost();
1315
+ const token = core.entityToken;
1316
+ const wanted = core.recordType?.history ?? Boolean(token);
1317
+ const [state, setState] = useState5({ status: "loading" });
1318
+ const lastKey = useRef6(null);
1319
+ const listHistory = host.history.list;
1320
+ const recordId = core.ref.id;
1321
+ useEffect7(() => {
1322
+ if (!wanted || !token) return void 0;
1323
+ const key = `${token}:${recordId}`;
1324
+ if (lastKey.current !== key) {
1325
+ lastKey.current = key;
1326
+ setState({ status: "loading" });
1327
+ }
1328
+ const controller = new AbortController();
1329
+ void listHistory(token, recordId, controller.signal).then((entries) => {
1330
+ if (!controller.signal.aborted) setState({ status: "ready", entries });
1331
+ }).catch((error) => {
1332
+ if (controller.signal.aborted) return;
1333
+ setState({
1334
+ status: "error",
1335
+ message: error instanceof Error ? error.message : String(error)
1336
+ });
1337
+ });
1338
+ return () => controller.abort();
1339
+ }, [listHistory, wanted, token, recordId]);
1340
+ if (!wanted || !token) return null;
1341
+ const ActorCell = host.doors.RefCell;
1342
+ const actorToken = ACTOR_COLUMNS.map((column) => host.doors.tokenFromColumnName(column)).find(
1343
+ (candidate) => Boolean(candidate)
1344
+ );
1345
+ const ActorName = host.history.ActorName ?? null;
1346
+ return /* @__PURE__ */ jsxs4("section", { className: "space-y-2", "data-detail-section": "history", children: [
1347
+ /* @__PURE__ */ jsx5(SectionHeading, { icon: /* @__PURE__ */ jsx5(HistoryIcon, { className: "h-3 w-3" }), children: "History" }),
1348
+ state.status === "loading" ? /* @__PURE__ */ jsxs4("div", { className: "space-y-1.5", "aria-busy": "true", "aria-label": "Loading history", children: [
1349
+ /* @__PURE__ */ jsx5(Skeleton, { className: "h-3.5 w-2/3" }),
1350
+ /* @__PURE__ */ jsx5(Skeleton, { className: "h-3.5 w-1/2" })
1351
+ ] }) : state.status === "error" ? /* @__PURE__ */ jsx5("p", { className: "text-xs text-muted-foreground", children: state.message.toLowerCase().includes("access denied") ? "History is only shown to people who can view this record." : `History could not be read: ${state.message}` }) : state.entries.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "text-xs text-muted-foreground", children: "No recorded changes yet." }) : /* @__PURE__ */ jsx5("ol", { className: "space-y-1", children: state.entries.map((entry) => /* @__PURE__ */ jsxs4(
1352
+ "li",
1353
+ {
1354
+ className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs",
1355
+ children: [
1356
+ /* @__PURE__ */ jsxs4("span", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground", children: [
1357
+ "v",
1358
+ entry.version
1359
+ ] }),
1360
+ /* @__PURE__ */ jsx5("span", { className: "capitalize text-foreground", children: entry.operation.toLowerCase() }),
1361
+ /* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: formatWhen(entry.occurredAt) }),
1362
+ entry.isCurrent ? /* @__PURE__ */ jsx5("span", { className: "text-[10px] uppercase tracking-wide text-primary", children: "current" }) : null,
1363
+ entry.actorId ? ActorName ? /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground", "data-detail-history-actor-person": true, children: [
1364
+ "Changed by ",
1365
+ /* @__PURE__ */ jsx5(ActorName, { actorId: entry.actorId, row: core.row })
1366
+ ] }) : actorToken ? /* @__PURE__ */ jsx5("span", { className: "inline-flex items-center", "data-detail-history-actor": true, children: /* @__PURE__ */ jsx5(ActorCell, { value: entry.actorId, label: "Changed by", token: actorToken }) }) : /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground", "data-detail-history-actor-raw": true, children: [
1367
+ "Changed by",
1368
+ " ",
1369
+ /* @__PURE__ */ jsx5(
1370
+ "span",
1371
+ {
1372
+ className: "font-mono text-[10px]",
1373
+ title: "We could not find the person behind this id \u2014 this host has bound no identity resolver.",
1374
+ children: entry.actorId
1375
+ }
1376
+ )
1377
+ ] }) : /* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", "data-detail-history-actor-absent": true, children: "No person recorded for this change" })
1378
+ ]
1379
+ },
1380
+ `${entry.version}-${entry.occurredAt}`
1381
+ )) })
1382
+ ] });
1383
+ }
1384
+ function DetailBody({ core }) {
1385
+ const extra = core.recordType?.extraSections && core.frameCtx ? core.recordType.extraSections(core.row, core.frameCtx) : [];
1386
+ const Frame = core.recordType?.Frame ?? null;
1387
+ const body = (
1388
+ // `min-h-full` so a Frame's right-click menu answers anywhere in the body,
1389
+ // not only on the rows — a short dossier otherwise leaves a dead band.
1390
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-h-full flex-col", children: [
1391
+ /* @__PURE__ */ jsx5(DetailRecordMeta, { core }),
1392
+ core.health ? /* @__PURE__ */ jsx5(HealthStrip, { health: core.health }) : null,
1393
+ core.about ? /* @__PURE__ */ jsx5("p", { className: "line-clamp-3 border-b border-border/60 p-4 text-xs leading-snug text-muted-foreground", children: core.about }) : null,
1394
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-6 p-4", children: [
1395
+ /* @__PURE__ */ jsx5(FieldsSection, { core }),
1396
+ core.status === "ready" || core.status === "none" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1397
+ /* @__PURE__ */ jsx5(AssociationsSection, { core }),
1398
+ /* @__PURE__ */ jsx5(HistorySection, { core }),
1399
+ extra.map((section) => /* @__PURE__ */ jsxs4("section", { className: "space-y-2", "data-detail-section": section.id, children: [
1400
+ /* @__PURE__ */ jsx5(SectionHeading, { children: section.label }),
1401
+ section.content
1402
+ ] }, section.id))
1403
+ ] }) : null,
1404
+ /* @__PURE__ */ jsx5(DetailPresentationPane, { core })
1405
+ ] })
1406
+ ] })
1407
+ );
1408
+ if (Frame && core.frameCtx) return /* @__PURE__ */ jsx5(Frame, { ctx: core.frameCtx, children: body });
1409
+ return body;
1410
+ }
1411
+
1412
+ // src/react/presentations.tsx
1413
+ import { useEffect as useEffect8, useRef as useRef7 } from "react";
1414
+
1415
+ // src/listContext.ts
1416
+ var PERCENT_ESCAPE_TOO = /[.!~*'()]/g;
1417
+ function encodeRefPart(value) {
1418
+ return encodeURIComponent(value).replace(PERCENT_ESCAPE_TOO, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%/g, "~");
1419
+ }
1420
+
1421
+ // src/presentation.ts
1422
+ function detailInstanceKey(ref) {
1423
+ return `${encodeRefPart(ref.type)}.${encodeRefPart(ref.id)}`;
1424
+ }
1425
+ var DEFAULT_PANEL_URL_RESERVE_BYTES = Math.floor(DETAIL_URL_BUDGET_BYTES / 2);
1426
+
1427
+ // src/react/presentations.tsx
1428
+ import { jsx as jsx6 } from "react/jsx-runtime";
1429
+ function KeyboardSlot({ core, children }) {
1430
+ return /* @__PURE__ */ jsx6(
1431
+ "div",
1432
+ {
1433
+ onKeyDownCapture: core.keyboard.rootProps.onKeyDownCapture,
1434
+ className: "flex min-w-0 flex-1 items-center",
1435
+ "data-detail-keyboard-slot": true,
1436
+ children
1437
+ }
1438
+ );
1439
+ }
1440
+ function KeyboardActionsSlot({ core, children }) {
1441
+ return /* @__PURE__ */ jsx6(
1442
+ "div",
1443
+ {
1444
+ onKeyDownCapture: core.keyboard.rootProps.onKeyDownCapture,
1445
+ className: "flex shrink-0 items-center",
1446
+ "data-detail-keyboard-slot": "actions",
1447
+ children
1448
+ }
1449
+ );
1450
+ }
1451
+ function KeyboardRoot({ core, children }) {
1452
+ const rootRef = useRef7(null);
1453
+ useEffect8(() => {
1454
+ const root = rootRef.current;
1455
+ if (!root) return;
1456
+ const active = document.activeElement;
1457
+ if (active instanceof HTMLElement && active !== document.body && root.contains(active)) return;
1458
+ root.focus({ preventScroll: true });
1459
+ }, [core.ref.id, core.ref.type]);
1460
+ return /* @__PURE__ */ jsx6(
1461
+ "div",
1462
+ {
1463
+ ref: rootRef,
1464
+ ...core.keyboard.rootProps,
1465
+ className: "flex min-h-full flex-1 flex-col outline-none",
1466
+ "data-detail-root": true,
1467
+ "data-detail-type": core.ref.type,
1468
+ "data-detail-presentation": core.presentation,
1469
+ children
1470
+ }
1471
+ );
1472
+ }
1473
+ function DetailWindowPresentation({ data, onClose }) {
1474
+ const host = useDetailHost();
1475
+ const core = useDetailCore(data, "window", { onClose });
1476
+ const Shell = requireShell(host.shells, "Window");
1477
+ return /* @__PURE__ */ jsx6(
1478
+ Shell,
1479
+ {
1480
+ instanceKey: detailInstanceKey(core.ref),
1481
+ target: core.ref,
1482
+ list: core.list.context,
1483
+ title: core.title,
1484
+ titleNode: /* @__PURE__ */ jsx6(KeyboardSlot, { core, children: /* @__PURE__ */ jsx6(DetailTitle, { core }) }),
1485
+ actions: /* @__PURE__ */ jsx6(KeyboardActionsSlot, { core, children: /* @__PURE__ */ jsx6(DetailActions, { core }) }),
1486
+ onClose: core.close,
1487
+ children: /* @__PURE__ */ jsx6(KeyboardRoot, { core, children: /* @__PURE__ */ jsx6(DetailBody, { core }) })
1488
+ }
1489
+ );
1490
+ }
1491
+ function DetailDockedPresentation({ data, onClose }) {
1492
+ const host = useDetailHost();
1493
+ const core = useDetailCore(data, "docked", { onClose });
1494
+ const Shell = requireShell(host.shells, "Docked");
1495
+ return /* @__PURE__ */ jsx6(
1496
+ Shell,
1497
+ {
1498
+ instanceKey: detailInstanceKey(core.ref),
1499
+ target: core.ref,
1500
+ title: core.title,
1501
+ titleNode: /* @__PURE__ */ jsx6(KeyboardSlot, { core, children: /* @__PURE__ */ jsx6(DetailTitle, { core }) }),
1502
+ actions: /* @__PURE__ */ jsx6(KeyboardActionsSlot, { core, children: /* @__PURE__ */ jsx6(DetailActions, { core }) }),
1503
+ onClose: core.close,
1504
+ children: /* @__PURE__ */ jsx6(KeyboardRoot, { core, children: /* @__PURE__ */ jsx6(DetailBody, { core }) })
1505
+ }
1506
+ );
1507
+ }
1508
+ function DetailPagePresentation({ data }) {
1509
+ const host = useDetailHost();
1510
+ const core = useDetailCore(data, "page", {});
1511
+ const Shell = requireShell(host.shells, "Page");
1512
+ return /* @__PURE__ */ jsx6(
1513
+ Shell,
1514
+ {
1515
+ target: core.ref,
1516
+ title: core.title,
1517
+ titleNode: /* @__PURE__ */ jsx6(KeyboardSlot, { core, children: /* @__PURE__ */ jsx6(DetailTitle, { core }) }),
1518
+ actions: /* @__PURE__ */ jsx6(KeyboardActionsSlot, { core, children: /* @__PURE__ */ jsx6(DetailActions, { core }) }),
1519
+ onClose: core.close,
1520
+ onBack: core.leave,
1521
+ children: /* @__PURE__ */ jsx6(KeyboardRoot, { core, children: /* @__PURE__ */ jsx6(DetailBody, { core }) })
1522
+ }
1523
+ );
1524
+ }
1525
+
1526
+ // src/react/headerGeometry.ts
1527
+ var DETAIL_HEADER_CONTAINER = "@container/window-header";
1528
+ var DETAIL_HEADER_COMPACT_BELOW = "@max-[26rem]/window-header";
1529
+ var DETAIL_TITLE_MIN_WIDTH_CLASS = "min-w-[6rem]";
1530
+ export {
1531
+ DETAIL_HEADER_COMPACT_BELOW,
1532
+ DETAIL_HEADER_CONTAINER,
1533
+ DETAIL_TITLE_MIN_WIDTH_CLASS,
1534
+ DetailActions,
1535
+ DetailBody,
1536
+ DetailDockedPresentation,
1537
+ DetailHostProvider,
1538
+ DetailPagePresentation,
1539
+ DetailPresentationPane,
1540
+ DetailRecordMeta,
1541
+ DetailTitle,
1542
+ DetailWindowPresentation,
1543
+ reconnectFor,
1544
+ requireResolveType,
1545
+ requireShell,
1546
+ useDetailCore,
1547
+ useDetailHealth,
1548
+ useDetailHost,
1549
+ useDetailKeyboard,
1550
+ useDetailRecord,
1551
+ useOpenDetail
1552
+ };
1553
+ //# sourceMappingURL=react.js.map