@ai-matrx/detail 0.1.67 → 0.1.69

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/index.ts","../src/react/host.tsx","../src/react/useDetailCore.ts","../src/react/useDetailHealth.ts","../src/react/useDetailKeyboard.ts","../src/react/useDetailRecord.ts","../src/react/useOpenDetail.ts","../src/types.ts","../src/react/DetailHeader.tsx","../src/react/icons.tsx","../src/react/DetailBody.tsx","../src/format.ts","../src/react/DetailPresentationPane.tsx","../src/react/presentations.tsx","../src/listContext.ts","../src/presentation.ts","../src/react/headerGeometry.ts"],"sourcesContent":["// @ai-matrx/detail/react — the React product.\n//\n// The ports and provider, the core, the keyboard model, the loader and the\n// health resolver, the one opener, the header halves, the body, the setting\n// pane, and the three presentations that hand the same core to a host's\n// window / docked / page shell.\n\nexport {\n DetailHostProvider,\n requireResolveType,\n requireShell,\n useDetailHost,\n} from \"./host\";\nexport type {\n DetailDockedShellProps,\n DetailEffectivePresentation,\n DetailHostPorts,\n DetailPageShellProps,\n DetailPresentationSetting,\n DetailShellSlots,\n DetailShells,\n DetailWindowShellProps,\n} from \"./host\";\n\nexport { useDetailCore } from \"./useDetailCore\";\nexport type { DetailCore } from \"./useDetailCore\";\n\nexport { useDetailKeyboard } from \"./useDetailKeyboard\";\nexport type { DetailKeyboard, DetailKeyboardHandlers } from \"./useDetailKeyboard\";\n\nexport { useDetailRecord } from \"./useDetailRecord\";\n\nexport { reconnectFor, useDetailHealth } from \"./useDetailHealth\";\nexport type { UseDetailHealthArgs } from \"./useDetailHealth\";\n\nexport { useOpenDetail } from \"./useOpenDetail\";\nexport type { OpenDetail } from \"./useOpenDetail\";\n\nexport { DetailActions, DetailRecordMeta, DetailTitle } from \"./DetailHeader\";\nexport { DetailBody } from \"./DetailBody\";\nexport { DetailPresentationPane } from \"./DetailPresentationPane\";\n\nexport {\n DetailDockedPresentation,\n DetailPagePresentation,\n DetailWindowPresentation,\n} from \"./presentations\";\nexport type { DetailPresentationProps } from \"./presentations\";\n\nexport {\n DETAIL_HEADER_COMPACT_BELOW,\n DETAIL_HEADER_CONTAINER,\n DETAIL_TITLE_MIN_WIDTH_CLASS,\n} from \"./headerGeometry\";\n","// @ai-matrx/detail — src/react/host.tsx\n//\n// THE SEAM. Everything this package needs from the app that hosts it arrives\n// here as ports — the same shape `@ai-matrx/associations` uses. The core and\n// the three presentations import nothing from any host; they ask the host for\n// the record-type map, the presentation setting, the shells (window / docked /\n// page), the doors, history, navigation and notifications.\n//\n// Providers NEST AND MERGE: a light provider at boot binds everything but the\n// shells (so `useOpenDetail` works anywhere), and each presentation's lazily\n// loaded entry binds its own shell right above the presentation — the window\n// shell parses the host's window manager and must never sit in a boot bundle.\n// A port that is missing when it is needed throws, naming itself.\n\n\"use client\";\n\nimport { createContext, useContext, type Context, type ComponentType, type ReactNode } from \"react\";\n\nimport type {\n DetailHistoryEntry,\n DetailInstanceData,\n DetailListContext,\n DetailPresentation,\n DetailRecordType,\n DetailRef,\n DetailRow,\n DetailSeed,\n} from \"../types\";\n\n/** Props every shell receives; the package fills the slots, the host owns the chrome. */\nexport interface DetailShellSlots {\n /** Accessible plain-text title (window chrome, drawer title, document title). */\n title: string;\n /** Rich title: icon · name · type chip · the record's doors. */\n titleNode: ReactNode;\n /** Presentation switcher · previous / next · copy id. Compact. */\n actions: ReactNode;\n children: ReactNode;\n onClose: () => void;\n}\n\nexport interface DetailWindowShellProps extends DetailShellSlots {\n /** `type.id` — the deep-link instance and the window manager's key. */\n instanceKey: string;\n target: DetailRef;\n /**\n * 🚨 NEW-15 — THE LIST TRAVELS INTO THE DEEP LINK. The window's `?panels=`\n * token used to carry the record and the presentation only, so a REFRESH lost\n * the previous / next controls and the counter without a word. The shell puts\n * this into the token the way the page presentation puts it into the query.\n */\n list: DetailListContext | null;\n}\n\nexport interface DetailDockedShellProps extends DetailShellSlots {\n instanceKey: string;\n target: DetailRef;\n}\n\nexport interface DetailPageShellProps extends DetailShellSlots {\n target: DetailRef;\n /** Back affordance — the page is the one presentation that left the URL. */\n onBack: () => void;\n}\n\nexport interface DetailEffectivePresentation {\n /** The effective presentation for this type, `undefined` when unresolved. */\n value: DetailPresentation | undefined;\n /** The per-record-type exception still in effect, from ANY rung. */\n forType: DetailPresentation | undefined;\n}\n\nexport interface DetailPresentationSetting {\n /** `undefined` while unresolved; `error` names the failure — never both silent. */\n value: DetailPresentation | undefined;\n error: string | null;\n /**\n * The PER-RECORD-TYPE exception in effect for this type, when there is one\n * (`undefined` = this type opens the way the default says). The pane offers\n * \"use the default for this type\" only then — an offer to remove something\n * that does not exist is a control that does nothing (NEW-2).\n */\n forType?: DetailPresentation | undefined;\n}\n\nexport interface DetailShells {\n Window?: ComponentType<DetailWindowShellProps>;\n Docked?: ComponentType<DetailDockedShellProps>;\n Page?: ComponentType<DetailPageShellProps>;\n}\n\nexport interface DetailHostPorts {\n /**\n * The ONE record-type map. `null` = the host has no registration for the\n * type. Bound by each presentation's entry (with its shell), not at boot —\n * the map pulls the registry and the frame into whatever imports it.\n */\n resolveType?: (type: string) => DetailRecordType | null;\n /** React face — the effective `ui.detail.default_presentation` for a type. A hook. */\n usePresentationSetting: (type: string) => DetailPresentationSetting;\n /** Awaited face for click handlers. Rejects with the resolver's error. */\n resolvePresentation: (type: string) => Promise<DetailPresentation>;\n /** Warm the cache so the click path is synchronous. */\n warmPresentation: (type: string) => void;\n /**\n * 🚨 NEW-10 (VERIFY-U-P1-R3) — RE-READ THE LADDER AFTER A WRITE, AND SAY WHAT\n * ANSWERS NOW. Removing your own per-type exception while your ORGANIZATION\n * holds one for the same type succeeded and the pane promised the records\n * \"now open the way you normally open records\" — they did not; the\n * organization's entry still won on the next open and nothing re-read the\n * ladder. A host binds this to a FRESH ladder read (cache invalidated), never\n * to the cached value the write just made stale. Optional: a host that cannot\n * re-read says so and the pane promises nothing.\n */\n reReadPresentation?: (type: string) => Promise<DetailEffectivePresentation>;\n /**\n * Write the person's presentation setting from inside a detail (the ONE\n * screen that writes `ui.detail.default_presentation`). `forType` non-null\n * writes the PER-RECORD-TYPE override instead of the default.\n *\n * `clear` with a `forType` REMOVES that record type's exception instead of\n * setting one, so an exception set from a record can be taken back from the\n * same place (NEW-2). It is the same map-entry write with the key absent — a\n * host never grows a second writer for it.\n *\n * Optional: a host that can only READ the setting binds nothing and the pane\n * is absent — never a disabled-looking control. A refusal comes back as\n * `{ ok: false, reason }` (the settings ladder's reasons are sentences and\n * the surface renders them), never as a thrown error.\n */\n savePresentation?: (args: {\n presentation: DetailPresentation;\n forType: string | null;\n clear?: boolean;\n }) => Promise<{ ok: true } | { ok: false; reason: string }>;\n /** Open an in-place presentation (window / docked). Page goes through `navigate`. */\n open: (args: {\n presentation: Exclude<DetailPresentation, \"page\">;\n data: DetailInstanceData;\n }) => void;\n close: (presentation: Exclude<DetailPresentation, \"page\">) => void;\n navigate: {\n pageHref: (ref: DetailRef, extra?: { list?: DetailListContext | null }) => string;\n toPage: (\n ref: DetailRef,\n extra?: {\n seed?: DetailSeed | null;\n list?: DetailListContext | null;\n /**\n * 🚨 NEW-14 (VERIFY-U-P1-R3) — MOVING INSIDE A LIST REPLACES, NEVER\n * PUSHES. Arrowing to a neighbour on the page pushed a history entry,\n * so twenty records meant twenty Backs and the chevron labelled \"Back\"\n * returned to the previous RECORD instead of the list. The record whose\n * history entry is being replaced, so the host can carry its \"this tab\n * pushed a detail page\" answer (`canGoBack`) forward to the record now\n * showing. Absent ⇒ a push, which is what opening a page IS.\n */\n replacing?: DetailRef | null;\n },\n ) => void;\n back: () => void;\n /**\n * 🚨 D1. Whether THIS tab reached the detail page from inside the app, so\n * `back()` lands on a screen the person was actually on. A deep link\n * opened in a fresh tab has NOTHING behind it: `back()` there leaves the\n * tab on `about:blank` with the whole app gone (measured 2026-09-17).\n * A host answers `false` whenever it is not certain.\n */\n canGoBack: (ref: DetailRef) => boolean;\n /**\n * Where the page goes when there is nothing behind it: the record's own\n * home (its canonical route), or the host's home when the type has none.\n * REPLACES the detail page — it is being left, not stacked on.\n */\n toRecordHome: (ref: DetailRef, entityToken: string | null) => void;\n };\n shells: DetailShells;\n doors: {\n /** The record's own doors (open / new tab / peek) — sibling of the title. */\n RecordDoors: ComponentType<{ token: string; id: string; name?: string | null }>;\n /** A reference inside the record — never a bare uuid. */\n /**\n * One id rendered as a door. `name` is the record's own name when the caller\n * has it — a door showing a truncated uuid is half a dead end (chair,\n * 2026-09-18), so a host that can name the record renders the name and keeps\n * the id for the copy control.\n */\n RefCell: ComponentType<{\n value: string;\n label: string;\n token: string;\n name?: string | null;\n }>;\n tokenFromColumnName: (column: string) => string | null;\n isUuidValue: (value: unknown) => value is string;\n /**\n * 🚨 NEW-17 (VERIFY-U-P1-R4) — WHETHER THIS RECORD REALLY OPENS ANYWHERE.\n * `RecordDoors` renders nothing when the token has no route and no peek, so\n * the honest absent state promised \"the controls above still open it where it\n * lives\" beside a header that carried no such control — for `session`, and for\n * every unregistered type. The body's sentence is derived from this answer,\n * never fixed.\n */\n hasDoor: (token: string, id: string) => boolean;\n };\n associations: {\n /** Tokens shown when a registration does not name its own. */\n defaultTokens: readonly string[];\n /** Whether a token may anchor an associations section at all. */\n canAnchor: (token: string) => boolean;\n };\n history: {\n list: (token: string, id: string, signal: AbortSignal) => Promise<DetailHistoryEntry[]>;\n /**\n * 🚨 NEW-23 (VERIFY-U-P1-R5) — WHO MADE THE CHANGE IS A PERSON, NOT AN ID.\n * Every history row printed a bare 36-character uuid under \"Changed by\",\n * because no `actor` or `user` token has a door in a host's registry: an\n * identity the UI names that opens nothing and says nothing (the\n * no-dead-ends class). A host binds its ONE existing identity resolver here\n * — never a second one — and the row shows the person's name. Optional: a\n * host that has no directory binds nothing and the row keeps the id under a\n * title that says what it is, which is honest rather than silent.\n *\n * It is given the record's own row too, because the directory a host may\n * legitimately read is usually the owning organization's members and the\n * row is where that organization is named.\n */\n ActorName?: ComponentType<{ actorId: string; row: DetailRow | null }>;\n };\n notify: {\n error: (message: string) => void;\n success: (message: string) => void;\n };\n copyText: (text: string) => Promise<boolean>;\n /**\n * 🚨 THE SAME RECONNECT THE CONNECTOR ROWS SHOW. A synced record whose grant\n * has expired, been revoked or lost a scope offers exactly the repair the\n * connector surface offers, from the record's own health strip, so a person\n * never has to go and find the connections screen. Optional: a host that\n * cannot open a consent flow binds nothing and the strip states the problem\n * without offering a control that would not help (law 4). A producer that\n * answers `onReconnect: null` overrides this for its record — see\n * `DetailSourceHealth.onReconnect`.\n */\n reconnectSource?: (ref: DetailRef, source: string) => void;\n /**\n * Where the developer goes to fix an unregistered or sourceless type — a\n * phrase for the console remedy the core prints once per type per tab (the\n * PERSON sees a plain sentence; the developer sees this). Optional: the\n * default names the `resolveType` port. A host names its own registry, e.g.\n * \"the item registry (features/item-presentation/registry.tsx)\".\n */\n remedy?: { typeMap: string };\n}\n\n// 🚨 MODULE-LEVEL STATE IS BANNED IN A DUAL-BUNDLE PACKAGE — the context lives\n// on `globalThis` under a `Symbol.for` slot (the kit `confirm/opener` shape), so\n// the ESM and CJS builds, and the /react and /testing entries, all share ONE\n// context. Two module instances with two contexts is exactly how a test seat's\n// provider stops being visible to the components it mounts.\nconst CONTEXT_SLOT = Symbol.for(\"ai-matrx.detail.host-context\");\ntype ContextSlot = { [CONTEXT_SLOT]?: Context<Partial<DetailHostPorts> | null> };\nconst DetailHostContext: Context<Partial<DetailHostPorts> | null> = ((globalThis as ContextSlot)[\n CONTEXT_SLOT\n] ??= createContext<Partial<DetailHostPorts> | null>(null));\n\n/**\n * Binds ports. Nested providers merge over their parent, shells included, so\n * a presentation entry can add only its own shell.\n */\nexport function DetailHostProvider({\n ports,\n children,\n}: {\n ports: Partial<DetailHostPorts>;\n children: ReactNode;\n}) {\n const parent = useContext(DetailHostContext);\n const merged: Partial<DetailHostPorts> = {\n ...(parent ?? {}),\n ...ports,\n shells: { ...(parent?.shells ?? {}), ...(ports.shells ?? {}) },\n };\n return <DetailHostContext.Provider value={merged}>{children}</DetailHostContext.Provider>;\n}\n\nconst REQUIRED_PORTS: (keyof DetailHostPorts)[] = [\n \"usePresentationSetting\",\n \"resolvePresentation\",\n \"warmPresentation\",\n \"open\",\n \"close\",\n \"navigate\",\n \"doors\",\n \"associations\",\n \"history\",\n \"notify\",\n \"copyText\",\n];\n\n/**\n * The bound ports. Throws with the remedy when a port is missing — a detail\n * that opened with no host would otherwise render every section empty and\n * look finished.\n */\nexport function useDetailHost(): DetailHostPorts {\n const ports = useContext(DetailHostContext);\n if (!ports) {\n throw new Error(\n \"[detail] No DetailHostProvider above this component. Mount the host binding — a \" +\n \"<DetailHostProvider ports={…}> carrying every required port (README → 'Binding the \" +\n \"ports') — around the tree that opens or renders record details.\",\n );\n }\n const missing = REQUIRED_PORTS.filter((key) => ports[key] === undefined);\n if (missing.length > 0) {\n throw new Error(\n `[detail] DetailHostProvider is missing the port(s): ${missing.join(\", \")}. ` +\n \"Bind them in the host binding (README → 'Binding the ports').\",\n );\n }\n // The context value itself (the provider always sets `shells`), so the\n // object a consumer depends on keeps its identity between renders.\n return ports as DetailHostPorts;\n}\n\n/** The record-type map, or an error naming which entry forgot to bind it. */\nexport function requireResolveType(\n host: DetailHostPorts,\n): NonNullable<DetailHostPorts[\"resolveType\"]> {\n if (!host.resolveType) {\n throw new Error(\n \"[detail] No resolveType port is bound. A presentation's entry must pass \" +\n \"`resolveType` (the host's ONE record-type map) beside its shell.\",\n );\n }\n return host.resolveType;\n}\n\n/** A presentation's shell, or an error naming which entry forgot to bind it. */\nexport function requireShell<K extends keyof DetailShells>(\n shells: DetailShells,\n which: K,\n): NonNullable<DetailShells[K]> {\n const shell = shells[which];\n if (!shell) {\n throw new Error(\n `[detail] No ${which} shell is bound. The ${which} presentation's entry must wrap itself in ` +\n `<DetailHostProvider ports={{ shells: { ${which} } }}> (README → 'The three shells').`,\n );\n }\n return shell;\n}\n","// @ai-matrx/detail — src/react/useDetailCore.ts\n//\n// THE core, as state: resolve the registration, load the row, derive title /\n// about / fields / health, and the list-navigation + presentation-switch\n// commands every presentation's header uses. The three presentations differ\n// only in which shell they hand this to.\n\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { requireResolveType, useDetailHost } from \"./host\";\nimport { useDetailHealth } from \"./useDetailHealth\";\nimport { useDetailKeyboard, type DetailKeyboard } from \"./useDetailKeyboard\";\nimport { useDetailRecord } from \"./useDetailRecord\";\nimport type {\n DetailField,\n DetailFrameContext,\n DetailInstanceData,\n DetailListContext,\n DetailPresentation,\n DetailRecordType,\n DetailRef,\n DetailRow,\n DetailSourceHealth,\n DetailStatus,\n} from \"../types\";\n\nexport interface DetailCore {\n ref: DetailRef;\n data: DetailInstanceData;\n presentation: DetailPresentation;\n /** `null` when the host has no registration for this type — the body says so. */\n recordType: DetailRecordType | null;\n status: DetailStatus;\n /** Set when `status === \"error\"`. */\n errorMessage: string | null;\n row: DetailRow | null;\n title: string;\n /** The title is a stand-in (loading / not found / failed), not a record's name. */\n titleIsStandIn: boolean;\n /**\n * 🚨 NEW-9 — WHETHER THE RECORD'S OWN DOORS MAY SHOW. A question about the\n * RECORD, not about its name: a record that exists but stores nothing more\n * here still opens in its home, and a title that could not be loaded has no\n * record behind it to open. The header hung the doors off `titleIsStandIn`,\n * which is why a type with no source became a named dead end.\n */\n doorsAvailable: boolean;\n /**\n * 🚨 NEW-17 — WHETHER A CONTROL THAT OPENS THIS RECORD SOMEWHERE ELSE IS\n * ACTUALLY ON THE SCREEN (the token resolves to a route or a peek). The body's\n * absent-state sentence is derived from this; it used to promise a door for\n * every type, including the ones whose header shows none.\n */\n canOpenElsewhere: boolean;\n /**\n * Human label for the record TYPE (\"File\") — for a statement about the type,\n * such as the presentation setting's \"Every file record now opens as a window\".\n */\n typeLabel: string;\n /**\n * What THIS ROW is (\"Company\" for a `crm.party` row whose `party_kind` is\n * `organization`, where `typeLabel` says \"Person\"). The header's type chip and\n * the stand-in titles use this; `typeLabel` when the registration cannot tell\n * the difference. See `DetailRecordType.labelForRow`.\n */\n recordLabel: string;\n about: string | null;\n fields: DetailField[];\n /**\n * The source health strip's content — `null` for a record the platform owns\n * outright, or while a producer that reads it from the host is still\n * answering. See `useDetailHealth`.\n */\n health: DetailSourceHealth | null;\n /** The entity token doors / associations / history key off; null = none. */\n entityToken: string | null;\n frameCtx: DetailFrameContext | null;\n keyboard: DetailKeyboard;\n list: {\n context: DetailListContext | null;\n hasPrev: boolean;\n hasNext: boolean;\n prev: () => void;\n next: () => void;\n };\n /** Re-open this record in another presentation and leave the current one. */\n switchTo: (presentation: DetailPresentation) => void;\n /**\n * 🚨 D1 — THE ONE EXIT FROM THE PAGE PRESENTATION. Back chevron, Escape and\n * a switch that leaves the route all come here; no host, route or shell may\n * call `back()` itself.\n */\n leave: () => void;\n close: () => void;\n}\n\nfunction entityTokenOf(recordType: DetailRecordType | null): string | null {\n return recordType?.entityToken ?? null;\n}\n\n// One console line per subject per tab — a remedy, never a per-render spam.\n// On `globalThis` under a `Symbol.for` slot, never module scope: the ESM and\n// CJS builds of this package are separate module instances, and a per-module\n// set would announce the same subject once per loader graph.\nconst ANNOUNCED_SLOT = Symbol.for(\"ai-matrx.detail.announced-remedies\");\ntype AnnouncedSlot = { [ANNOUNCED_SLOT]?: Set<string> };\nconst announced: Set<string> = ((globalThis as AnnouncedSlot)[ANNOUNCED_SLOT] ??= new Set<string>());\nfunction announceOnce(key: string, message: string): void {\n if (announced.has(key)) return;\n announced.add(key);\n console.warn(message);\n}\n\nfunction neighbour(list: DetailListContext | null, delta: 1 | -1): DetailRef | null {\n if (!list) return null;\n const target = list.index + delta;\n if (target < 0 || target >= list.items.length) return null;\n return list.items[target] ?? null;\n}\n\nconst DEFAULT_TYPE_MAP_REMEDY = \"the host's `resolveType` map (the DetailHostProvider's record-type port)\";\n\nexport function useDetailCore(\n data: DetailInstanceData,\n presentation: DetailPresentation,\n /**\n * `onClose` is how an IN-PLACE presentation is dismissed (the overlay's\n * close). The page presentation needs none: it is left through `leave`,\n * which is the guarded exit — see `close` below.\n */\n options: { onClose?: () => void },\n): DetailCore {\n const host = useDetailHost();\n const ref: DetailRef = { type: data.type, id: data.id };\n const recordType = requireResolveType(host)(data.type);\n const state = useDetailRecord(recordType, data.id);\n\n const row = state.status === \"ready\" ? state.row : null;\n const typeLabel = recordType?.label ?? data.type;\n const seedName = data.seed?.name?.trim() || null;\n // 🚨 D5 — A FAILED OR UNFINISHED LOAD NEVER CLAIMS A RECORD. The\n // registration's `title()` answers `Untitled <Label>` when it has no row,\n // which read as a real, unnamed record sitting above its own error notice\n // (VERIFY-U-P1, D5). A title is invented ONLY from something that exists:\n // the loaded row, or the name the opener already knew (the seed). With\n // neither, the header says what is actually happening and marks itself a\n // stand-in so the presentations can render it as one.\n //\n // 🚨 NEW-1 (VERIFY-U-P1-R2) — `none` IS NOT A LOADED ROW EITHER. A record\n // type with no `load` resolves to `status: \"none\"`, and treating that as a\n // successful load put the registration's invented `Untitled <Label>` in the\n // header, in foreground weight, with the record's DOORS beside it, above a\n // body saying no details were available. Every unregistered type — the\n // agent-emitted type a registry deliberately supports — landed there. Only\n // `ready` is a loaded row; `none` gets the honest absent state below.\n const loadedTitle =\n state.status === \"ready\" && recordType ? recordType.title(row, data.seed) : null;\n //\n // 🚨 NEW-9 (VERIFY-U-P1-R3) — AND THE `none` STAND-IN IS NEVER A SENTENCE.\n // Round 2's fix put \"No detail is registered for session records\" in the\n // header, which is where the record's NAME goes: the screen named a real\n // record with a developer's sentence, printed a repo path in the body, and\n // dropped the doors, so a record became a named dead end. A record that\n // exists but stores nothing more here is named from what is certain — its\n // type and its own id — the body says in one plain sentence that there is\n // nothing more, and the doors stay.\n const shortId = data.id.length > 8 ? data.id.slice(0, 8) : data.id;\n // 🚨 WHAT THIS ROW IS, not what its type is called (chair, 2026-09-18). One\n // registered type is regularly a family — `crm.party` is 1,432 companies and\n // 460 persons — so a type-level label says \"Person\" over a company, in the chip\n // and in every stand-in title (VERIFY-U-P1-R5, N6). A registration that can\n // tell from the row answers through `labelForRow`; nothing else changes, and\n // the presentation pane deliberately keeps `typeLabel`.\n const recordLabel = (row && recordType?.labelForRow?.(row)) || typeLabel;\n const standInTitle =\n state.status === \"not-found\"\n ? `This ${recordLabel.toLowerCase()} could not be found`\n : state.status === \"error\"\n ? `This ${recordLabel.toLowerCase()} could not be loaded`\n : state.status === \"none\"\n ? `${recordLabel} ${shortId}`\n : `Loading this ${recordLabel.toLowerCase()}…`;\n const titleIsStandIn = !loadedTitle && !seedName;\n const title = loadedTitle ?? seedName ?? standInTitle;\n // The record plausibly exists: only a failed or missing read says otherwise.\n const doorsAvailable =\n entityTokenOf(recordType) !== null &&\n state.status !== \"not-found\" &&\n state.status !== \"error\";\n // 🚨 NEW-17 — and a door the host can actually open. `RecordDoors` renders\n // nothing for a token with no route and no peek, so a screen that says \"the\n // controls above still open it where it lives\" must ask.\n const canOpenElsewhere =\n doorsAvailable &&\n entityTokenOf(recordType) !== null &&\n host.doors.hasDoor(entityTokenOf(recordType) as string, data.id);\n const about = data.seed?.about?.trim() || null;\n const fields: DetailField[] =\n row && recordType ? recordType.fields(row) : [];\n // 🚨 THE SOURCE HEALTH STRIP HAS A PRODUCER. The producer is a field on the\n // registration the HOST wires (it may read the connector's recorded\n // capability health, which is an async read), so it is resolved here through\n // one hook rather than called inline — and a producer that fails says so on\n // the strip instead of leaving the record silent.\n const health = useDetailHealth({\n recordType,\n row,\n ref,\n reconnect: host.reconnectSource ?? null,\n });\n const entityToken = entityTokenOf(recordType);\n\n const frameCtx: DetailFrameContext | null = recordType\n ? {\n ref,\n recordType,\n title,\n about,\n status: state.status,\n fields,\n presentation,\n }\n : null;\n\n // 🚨 NEW-9 — THE REMEDY GOES WHERE THE DEVELOPER IS. The person sees a plain\n // sentence; the registry instruction is a console warning, once per type per\n // tab, so it is impossible to miss in development and impossible to read on a\n // screen in production. WHERE the registry is comes from the host's `remedy`\n // port — this package cannot know a host's file paths.\n const typeMapRemedy = host.remedy?.typeMap ?? DEFAULT_TYPE_MAP_REMEDY;\n useEffect(() => {\n if (!recordType) {\n announceOnce(\n `no-type:${data.type}`,\n `[detail] Nothing is registered for the record type \"${data.type}\", so its detail shows ` +\n `the type, the id and the doors only. Remedy: add an entry for it in ${typeMapRemedy}.`,\n );\n return;\n }\n if (state.status === \"none\") {\n announceOnce(\n `no-source:${data.type}`,\n `[detail] The record type \"${data.type}\" has no \\`load\\` (no single canonical source), so ` +\n \"its detail can show nothing beyond what the opener already knew. Remedy: give the type \" +\n `a load in ${typeMapRemedy}, or leave it sourceless deliberately and say why there.`,\n );\n }\n }, [recordType, state.status, data.type, typeMapRemedy]);\n\n // Keep the setting warm for this type so a switch or a neighbour opens\n // without an awaited round-trip.\n const warmPresentation = host.warmPresentation;\n useEffect(() => {\n warmPresentation(data.type);\n }, [warmPresentation, data.type]);\n\n const openNeighbour = (delta: 1 | -1) => {\n const target = neighbour(data.list, delta);\n if (!target || !data.list) return;\n // 🚨 NEW-18 (VERIFY-U-P1-R4) — THE WHOLE LIST CONTEXT TRAVELS, `trimmedFrom`\n // INCLUDED. This rebuilt the context from `items` and `index` only, so the\n // FIRST arrow press turned \"stepping through 139 of the 500 records in the\n // list this was opened from\" into silence — in the window's payload and in\n // the page URL's `lt=` alike — and the next record presented the trimmed\n // window as the whole list. The arrows are the primary binding, so the\n // honesty NEW-7 and NEW-13 bought lasted exactly one keystroke.\n const list: DetailListContext = {\n ...data.list,\n items: data.list.items,\n index: data.list.index + delta,\n };\n const next: DetailInstanceData = { ...target, seed: null, list };\n if (presentation === \"page\") {\n // 🚨 NEW-14 — one history entry for the whole detail visit: moving to a\n // neighbour REPLACES the entry showing the current record, so Back leaves\n // to the list instead of walking back through every record arrowed past.\n host.navigate.toPage(target, { list, replacing: ref });\n } else {\n host.open({ presentation, data: next });\n }\n };\n\n /**\n * 🚨 D1 — LEAVING THE PAGE, ONCE, FOR A DESTINATION THAT EXISTS.\n * A detail page is reached by a shared deep link as often as by an in-app\n * push, and a tab opened straight onto it has NO history entry behind it:\n * `back()` there leaves the person on `about:blank` with the app gone\n * (VERIFY-U-P1 D1). Round 1 guarded the presentation SWITCH only, and\n * VERIFY-U-P1-R2 reproduced the same failure through the page header's Back\n * chevron and through Escape, because the route handed the presentation a raw\n * `router.back()`. The decision lives here now — the one place that knows the\n * record and its token — so every control that leaves the page inherits it.\n */\n const leave = () => {\n if (host.navigate.canGoBack(ref)) host.navigate.back();\n else host.navigate.toRecordHome(ref, entityToken);\n };\n\n const close = () => {\n if (presentation === \"page\") {\n leave();\n return;\n }\n if (!options.onClose) {\n throw new Error(\n \"[detail] The \" +\n presentation +\n \" presentation was mounted without an `onClose`, so nothing can dismiss it. \" +\n \"Pass the overlay's close from the presentation's entry.\",\n );\n }\n options.onClose();\n };\n\n const switchTo = (target: DetailPresentation) => {\n if (target === presentation) return;\n if (target === \"page\") {\n host.navigate.toPage(ref, { seed: data.seed, list: data.list });\n if (presentation !== \"page\") host.close(presentation);\n return;\n }\n host.open({ presentation: target, data });\n if (presentation !== \"page\") {\n host.close(presentation);\n return;\n }\n // The new presentation is already open by this line, so the page must go\n // somewhere that exists: the ONE guarded exit (D1).\n leave();\n };\n\n const hasPrev = neighbour(data.list, -1) !== null;\n const hasNext = neighbour(data.list, 1) !== null;\n\n const keyboard = useDetailKeyboard({\n onClose: close,\n onPrev: hasPrev ? () => openNeighbour(-1) : null,\n onNext: hasNext ? () => openNeighbour(1) : null,\n // 🚨 NEW-22 — Cmd+Enter is one of the three promised bindings; on a record\n // with no editor open it ANSWERS rather than doing nothing in silence.\n onSaveUnavailable: () =>\n host.notify.error(\n `There is nothing to save on this ${typeLabel.toLowerCase()} — nothing here is being ` +\n \"edited. Changes to the record are made where it lives.\",\n ),\n });\n\n return {\n ref,\n data,\n presentation,\n recordType,\n status: state.status,\n errorMessage: state.status === \"error\" ? state.message : null,\n row,\n title,\n titleIsStandIn,\n doorsAvailable,\n canOpenElsewhere,\n typeLabel,\n recordLabel,\n about,\n fields,\n health,\n entityToken,\n frameCtx,\n keyboard,\n list: {\n context: data.list,\n hasPrev,\n hasNext,\n prev: () => openNeighbour(-1),\n next: () => openNeighbour(1),\n },\n switchTo,\n leave,\n close,\n };\n}\n","// @ai-matrx/detail — src/react/useDetailHealth.ts\n//\n// 🚨 THE SOURCE HEALTH STRIP HAS A PRODUCER, AND A REFUSAL ANYWHERE REACHES\n// EVERY DEPENDENT RECORD.\n//\n// The strip itself has existed since the primitive's first commit and NO record\n// could ever render it: the registration's `health` field was never set by the\n// one producer of registrations in app code, so \"does the record tell the truth\n// from a live capability-health row?\" was unanswerable for four verification\n// rounds (VERIFY-U-P1-R4, § The health strip).\n//\n// What the contract needs is a producer the HOST wires, because the answer is not\n// in the row: whether the grant behind a synced record still works is the\n// connector's recorded per-capability health, read from the server. So the\n// producer may be ASYNC and it is resolved here — ONE hook, called for every\n// record type whether it produces health or not, so the hook order cannot change\n// when the person arrows from a synced record to an owned one.\n//\n// Nothing in this file knows what a provider is. It resolves a function the\n// registration carries and hands it the record, an abort signal and the host's\n// reconnect action — the same Reconnect the connector rows offer (\"A refusal\n// anywhere … shows on every dependent record's health strip with the same\n// Reconnect\").\n//\n// AND A PRODUCER THAT FAILS SAYS SO (law 4). A read that throws does not leave\n// the record looking healthy and does not leave the strip absent, which would\n// read as \"this record is not synced\": it renders with an unknown grant, the\n// failure in the person's language, and the Reconnect that is the only thing they\n// can do about it.\n//\n// 🚨 AND `onReconnect: null` MEANS \"OFFER NOTHING\" (Bugbot round 18, frontend\n// PR 228). The producer's decision is three-valued — a function (offer this),\n// `null` (offer nothing: a reconnect would not repair this refusal), or omitted\n// (defer to the host's `reconnectSource`) — and this hook used to merge the\n// host action back in with `??`, which reads `null` as \"unset\". A synced record\n// whose refusal a reconnect cannot fix (a share the owner must grant, a record\n// we own) therefore still showed Reconnect: a control that does nothing. Only an\n// OMITTED key defers to the host now.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport type {\n DetailRecordType,\n DetailRef,\n DetailRow,\n DetailSourceHealth,\n} from \"../types\";\n\nexport interface UseDetailHealthArgs {\n recordType: DetailRecordType | null;\n /** The loaded row — health is only ever produced for a record that loaded. */\n row: DetailRow | null;\n ref: DetailRef;\n /** The host's reconnect action for this record's source, when it has one. */\n reconnect: ((ref: DetailRef, source: string) => void) | null;\n}\n\n/**\n * The strip's Reconnect for a producer's answer: the producer's own action, its\n * explicit refusal (`null`), or — only when it said nothing — the host's.\n *\n * 🚨 AND `grant: \"blocked\"` NEVER GETS ONE (chair, 2026-09-18). The word means\n * the source refuses and nothing the person can click repairs it, so the refusal\n * is structural here rather than a promise each producer must remember to keep:\n * a host that binds `reconnectSource` cannot put the button back, and neither\n * can a producer that supplies an action by mistake.\n */\nexport function reconnectFor(\n value: DetailSourceHealth,\n hostReconnect: ((source: string) => void) | null,\n): (() => void) | null {\n if (value.grant === \"blocked\") return null;\n if (value.onReconnect !== undefined) return value.onReconnect;\n return hostReconnect ? () => hostReconnect(value.source) : null;\n}\n\nexport function useDetailHealth({\n recordType,\n row,\n ref,\n reconnect,\n}: UseDetailHealthArgs): DetailSourceHealth | null {\n const [health, setHealth] = useState<DetailSourceHealth | null>(null);\n const producer = recordType?.health ?? null;\n const key = `${ref.type}:${ref.id}`;\n\n // 🚨 N8 (VERIFY-U-P1-R5) — THE PRODUCER'S FUNCTION IDENTITY IS NOT A\n // DEPENDENCY, BECAUSE A HOST IS NOT REQUIRED TO CACHE IT.\n //\n // The effect used to key on `recordType.health` itself. `resolveType` is a\n // host PORT: a host whose map builds the registration per call hands a fresh\n // producer every render, and this effect's own `setHealth` causes the next\n // render — an unbounded loop that react-dom reports as \"Maximum update depth\n // exceeded\" from inside this file, with no remedy naming the cause. A verifier\n // hit it in minutes by registering the type inline.\n //\n // What actually identifies a producer is the RECORD TYPE it came from, which\n // is a string, plus whether there is one at all. The function and the host's\n // reconnect are read through refs, so a new identity is used on the next run\n // without scheduling one.\n const producerRef = useRef(producer);\n producerRef.current = producer;\n const reconnectRef = useRef(reconnect);\n reconnectRef.current = reconnect;\n const producerKey = producer ? `${recordType?.type ?? \"\"}:${key}` : null;\n\n useEffect(() => {\n const producer = producerRef.current;\n const reconnect = reconnectRef.current;\n if (!producer || !row) {\n setHealth(null);\n return undefined;\n }\n const controller = new AbortController();\n let settled = false;\n const onReconnect = reconnect ? (source: string) => reconnect(ref, source) : null;\n const publish = (value: DetailSourceHealth | null) => {\n if (controller.signal.aborted) return;\n settled = true;\n setHealth(value ? { ...value, onReconnect: reconnectFor(value, onReconnect) } : null);\n };\n try {\n const answer = producer(row, { ref, signal: controller.signal });\n if (answer && typeof (answer as Promise<unknown>).then === \"function\") {\n void (answer as Promise<DetailSourceHealth | null>).then(publish).catch(\n (error: unknown) => {\n if (controller.signal.aborted) return;\n publish(unreadable(error, onReconnect));\n },\n );\n } else {\n publish((answer as DetailSourceHealth | null) ?? null);\n }\n } catch (error: unknown) {\n publish(unreadable(error, onReconnect));\n }\n return () => {\n controller.abort();\n if (!settled) setHealth(null);\n };\n // `ref` is rebuilt every render; its identity is the key. `producerKey`\n // stands in for the producer (N8) and the host's reconnect is a ref.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [producerKey, row, key]);\n\n return health;\n}\n\n/** The honest strip for a source check that could not be read at all. */\nfunction unreadable(\n error: unknown,\n onReconnect: ((source: string) => void) | null,\n): DetailSourceHealth {\n const message = error instanceof Error ? error.message : String(error);\n const source = \"its source\";\n return {\n source: \"Source\",\n grant: \"unknown\",\n grantDetail:\n \"We could not check the connection this record refreshes through, so we cannot say \" +\n `whether it is still working (${message}). Reconnecting is safe and fixes it if the ` +\n \"connection is the problem.\",\n lastRefreshedAt: null,\n onReconnect: onReconnect ? () => onReconnect(source) : null,\n };\n}\n","// @ai-matrx/detail — src/react/useDetailKeyboard.ts\n//\n// The keyboard model, once, for all three presentations (Linear's bones):\n// Escape → close\n// ArrowUp / ArrowDown → previous / next record in the list it was\n// ArrowLeft / ArrowRight opened from, WHEN the body is not the scroll\n// target; `[` / `]` always move\n// Cmd/Ctrl + Enter → save, when a section registered a save handler\n//\n// ARROWS ARE THE PRIMARY BINDING (Arman, 2026-09-17: \"Escape closes, ARROWS\n// move between records, Cmd+Enter saves\"). `[` / `]` stay as aliases because\n// Linear and Notion both carry them and muscle memory is cheap to honour.\n// Every movement key is read only when focus is NOT in an editable — in a\n// field an arrow moves the caret, which is what the person meant.\n//\n// 🚨 NEW-5 — AND AN ARROW NEVER COSTS THE BODY ITS SCROLLING. With a list\n// context this hook `preventDefault`ed every ArrowUp/ArrowDown, so a\n// keyboard-only reader could not scroll a long dossier at all — the only scroll\n// left was PageDown/space (VERIFY-U-P1-R2, break attempt 2). Linear and Notion\n// bind the same keys in a peek and do NOT do that. The rule, ruled by the chair\n// 2026-09-17: the arrows move between records only when the detail body is not\n// the scroll target — when it can scroll, they scroll it, and `[` / `]` remain\n// the always-available record navigation.\n//\n// 🚨 NEW-6 — AND ESCAPE IS NOT SWALLOWED BY A CHECKBOX. `isTypingTarget` is the\n// right rule for an ARROW (a caret belongs to the field) and the wrong one for\n// Escape: every INPUT counted, so Escape pressed on the presentation pane's\n// \"Only for file records\" checkbox closed nothing. Escape closes from any\n// control inside the detail except a menu/select that may be open and a text\n// field with uncommitted edits, where it means \"undo what I typed\".\n//\n// Keys are read on the presentation's own root (capture phase), so two open\n// details never answer one keystroke and a key typed into a field inside the\n// detail is left alone — except Cmd/Ctrl+Enter, which is the point of a field.\n\n\"use client\";\n\nimport { useCallback, useRef, type KeyboardEvent as ReactKeyboardEvent } from \"react\";\n\nexport interface DetailKeyboardHandlers {\n onClose?: (() => void) | null;\n onPrev?: (() => void) | null;\n onNext?: (() => void) | null;\n /**\n * 🚨 NEW-22 (VERIFY-U-P1-R4) — CMD+ENTER NEVER VANISHES. The chord is one of\n * the three bindings the primitive promises, and nothing is registered to save\n * unless a section with an editor is open, so the keystroke was swallowed in\n * silence on every read-only record. A deliberate keystroke gets an answer.\n */\n onSaveUnavailable?: (() => void) | null;\n}\n\nexport interface DetailKeyboard {\n /** Spread onto the presentation root. */\n rootProps: {\n tabIndex: -1;\n onKeyDownCapture: (event: ReactKeyboardEvent<HTMLElement>) => void;\n };\n /** A section with an editor registers its save; returns the unregister. */\n registerSave: (save: () => void | Promise<void>) => () => void;\n /** Whether anything is registered — the switcher shows the hint only then. */\n hasSave: () => boolean;\n}\n\n/** Previous record, when an arrow may move at all. Brackets always may. */\nconst PREV_ARROWS: ReadonlySet<string> = new Set([\"ArrowUp\", \"ArrowLeft\"]);\nconst NEXT_ARROWS: ReadonlySet<string> = new Set([\"ArrowDown\", \"ArrowRight\"]);\nconst PREV_BRACKET = \"[\";\nconst NEXT_BRACKET = \"]\";\n\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Input types that hold typed text — the only ones an arrow or Escape serves. */\nconst NON_TEXT_INPUT_TYPES: ReadonlySet<string> = new Set([\n \"checkbox\",\n \"radio\",\n \"button\",\n \"submit\",\n \"reset\",\n \"range\",\n \"color\",\n \"file\",\n \"image\",\n \"hidden\",\n]);\n\n/**\n * The controls that ACTUALLY own Escape when they are open over the detail.\n *\n * A menu, a select's list, a combobox's list and a popover all render inside the\n * floating layer (`[data-radix-popper-content-wrapper]` in this repo's Radix\n * primitives), and an open menu's items carry the listbox/menu/tree/grid roles.\n * A Drawer's or Dialog's content is deliberately NOT here: the docked and window\n * presentations ARE a Drawer on a phone, and Escape there means \"close this\n * detail\" — which is the whole binding.\n */\nconst ESCAPE_OWNING_ANCESTORS =\n '[data-radix-popper-content-wrapper], [role=\"menu\"], [role=\"menubar\"], ' +\n '[role=\"listbox\"], [role=\"tree\"], [role=\"grid\"]';\n\n/** Whether this element is itself the trigger of something that is OPEN. */\nfunction isOpenTrigger(element: HTMLElement): boolean {\n if (element.getAttribute(\"aria-expanded\") !== \"true\") return false;\n const role = element.getAttribute(\"role\");\n if (role === \"combobox\") return true;\n const haspopup = element.getAttribute(\"aria-haspopup\");\n return haspopup !== null && haspopup !== \"false\" && haspopup !== \"dialog\";\n}\n\n/**\n * Whether Escape belongs to the focused control rather than to the detail\n * (NEW-6). Two cases, and only two: something that IS OPEN over the detail\n * (a select's list, a menu, a combobox, a popover — or the focused trigger of\n * one), and a text field holding edits the person has not committed, where\n * Escape means \"put it back\".\n *\n * 🚨 NEW-11 (VERIFY-U-P1-R3) — AND NOT ANYTHING MERELY EXPANDED. This used to\n * match any ANCESTOR carrying `data-state=\"open\"` or `aria-expanded=\"true\"`,\n * which is every Radix Collapsible and Accordion section a record type puts in\n * its own body, every Drawer's and Dialog's content (both presentations on a\n * phone) and a closed combobox's input. Reproduced with one `data-state=\"open\"`\n * wrapper: Escape stopped working for everything inside it while the same key\n * from the root left at once. The chair's ruling was \"an open menu/select or a\n * text field with uncommitted edits\" — these two cases, and they are narrow on\n * purpose: over-claiming Escape breaks the detail's only keyboard exit.\n */\nfunction escapeBelongsToTheControl(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.tagName === \"SELECT\") return true;\n if (isOpenTrigger(target)) return true;\n if (target.closest(ESCAPE_OWNING_ANCESTORS)) return true;\n if (target.isContentEditable) return true;\n if (target instanceof HTMLTextAreaElement) return target.value !== target.defaultValue;\n if (target instanceof HTMLInputElement) {\n if (NON_TEXT_INPUT_TYPES.has(target.type)) return false;\n return target.value !== target.defaultValue;\n }\n return false;\n}\n\n/** Whether an element is a real vertical scroll target with room to move. */\nfunction isScrollTarget(element: HTMLElement): boolean {\n if (element.scrollHeight - element.clientHeight <= 1) return false;\n const overflowY =\n typeof window === \"undefined\" ? \"\" : window.getComputedStyle(element).overflowY;\n return overflowY === \"auto\" || overflowY === \"scroll\" || overflowY === \"overlay\";\n}\n\n/**\n * Whether the detail the keystroke landed in is SCROLLABLE — in which case the\n * arrows are the reader's, not the list's (NEW-5). The walk starts at the\n * keystroke's target and goes up past the presentation root, because every shell\n * puts its own scroll area AROUND the root (the page's body, the docked panel's\n * column, the window's body).\n */\nfunction bodyIsTheScrollTarget(target: EventTarget | null): boolean {\n let node: HTMLElement | null = target instanceof HTMLElement ? target : null;\n while (node) {\n if (isScrollTarget(node)) return true;\n node = node.parentElement;\n }\n return false;\n}\n\nexport function useDetailKeyboard(handlers: DetailKeyboardHandlers): DetailKeyboard {\n const saveRef = useRef<(() => void | Promise<void>) | null>(null);\n const handlersRef = useRef(handlers);\n handlersRef.current = handlers;\n\n const onKeyDownCapture = useCallback((event: ReactKeyboardEvent<HTMLElement>) => {\n const h = handlersRef.current;\n const meta = event.metaKey || event.ctrlKey;\n if (meta && event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n if (saveRef.current) void saveRef.current();\n else h.onSaveUnavailable?.();\n return;\n }\n if (event.key === \"Escape\") {\n if (!h.onClose || escapeBelongsToTheControl(event.target)) return;\n event.preventDefault();\n event.stopPropagation();\n h.onClose();\n return;\n }\n if (isTypingTarget(event.target)) return;\n // A modified arrow or bracket belongs to the browser (word jump, history, zoom).\n if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;\n if (event.key === PREV_BRACKET && h.onPrev) {\n event.preventDefault();\n h.onPrev();\n return;\n }\n if (event.key === NEXT_BRACKET && h.onNext) {\n event.preventDefault();\n h.onNext();\n return;\n }\n const prev = PREV_ARROWS.has(event.key);\n const next = NEXT_ARROWS.has(event.key);\n if (!prev && !next) return;\n // The reader is reading: the arrows scroll, and `[` / `]` still move.\n if (bodyIsTheScrollTarget(event.target)) return;\n if (prev && h.onPrev) {\n event.preventDefault();\n h.onPrev();\n return;\n }\n if (next && h.onNext) {\n event.preventDefault();\n h.onNext();\n }\n }, []);\n\n const registerSave = useCallback((save: () => void | Promise<void>) => {\n saveRef.current = save;\n return () => {\n if (saveRef.current === save) saveRef.current = null;\n };\n }, []);\n\n const hasSave = useCallback(() => saveRef.current !== null, []);\n\n return {\n rootProps: { tabIndex: -1, onKeyDownCapture },\n registerSave,\n hasSave,\n };\n}\n","// @ai-matrx/detail — src/react/useDetailRecord.ts\n//\n// Loads ONE record through its registration's `load`, with an abort on every\n// change and a structured state for every outcome. Nothing throws out of\n// here and nothing is swallowed: an error carries its message to the screen.\n//\n// 🚨 N8 (VERIFY-U-P1-R5) — THE REGISTRATION'S FUNCTION IDENTITY IS NOT A\n// DEPENDENCY. `resolveType` is a host PORT and nothing in the contract says its\n// answer must be cached: a host whose map builds the registration per call hands\n// a fresh `load` every render, and this effect's own `setState` causes the next\n// render — an endless reload of the same record, at the server. `requestKey`\n// (the type, the id and whether there is a source at all) is what identifies a\n// read; the closure is read through a ref.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport type { DetailLoadState, DetailRecordType, DetailRow } from \"../types\";\n\nexport function useDetailRecord(\n recordType: DetailRecordType | null,\n id: string | null,\n): DetailLoadState<DetailRow> {\n const load = recordType?.load ?? null;\n const [state, setState] = useState<DetailLoadState<DetailRow>>(() =>\n load && id ? { status: \"loading\" } : { status: \"none\" },\n );\n const requestKey = `${recordType?.type ?? \"\"}|${id ?? \"\"}|${load ? \"src\" : \"none\"}`;\n const [seenKey, setSeenKey] = useState(requestKey);\n if (seenKey !== requestKey) {\n // Mask the previous record synchronously — an effect-only reset shows the\n // last record's fields under the next record's title for a frame.\n setSeenKey(requestKey);\n setState(load && id ? { status: \"loading\" } : { status: \"none\" });\n }\n\n const loadRef = useRef(load);\n loadRef.current = load;\n\n useEffect(() => {\n const load = loadRef.current;\n if (!load || !id) return undefined;\n const controller = new AbortController();\n void (async () => {\n try {\n const result = await load(id, controller.signal);\n if (controller.signal.aborted) return;\n if (\"notFound\" in result) {\n setState({ status: \"not-found\" });\n } else {\n setState({ status: \"ready\", row: result.row });\n }\n } catch (error: unknown) {\n if (controller.signal.aborted) return;\n const message = error instanceof Error ? error.message : String(error);\n console.error(\"[detail] record load failed\", { type: recordType?.type, id, error });\n setState({ status: \"error\", message });\n }\n })();\n return () => controller.abort();\n // `requestKey` stands in for `load` (N8): a host is not required to cache it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [requestKey, id]);\n\n return state;\n}\n","// @ai-matrx/detail — src/react/useOpenDetail.ts\n//\n// THE ONE OPENER. Every surface that names a record and wants its detail\n// calls this; nothing else decides how a record opens. It resolves the\n// person's setting (`ui.detail.default_presentation`, per record type), opens\n// in place, and never navigates away — `page` is the only presentation that\n// changes the URL, and it offers \"open as window\" back.\n\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { useDetailHost } from \"./host\";\nimport {\n DETAIL_PRESENTATION_KNOB,\n type DetailInstanceData,\n type DetailOpenRequest,\n type DetailPresentation,\n} from \"../types\";\n\nexport type OpenDetail = (request: DetailOpenRequest) => Promise<DetailPresentation>;\n\n/**\n * @param warmType a record type this surface will open — its setting is\n * resolved on mount so the click is synchronous.\n */\nexport function useOpenDetail(warmType?: string | null): OpenDetail {\n const host = useDetailHost();\n\n const warmPresentation = host.warmPresentation;\n useEffect(() => {\n if (warmType) warmPresentation(warmType);\n }, [warmPresentation, warmType]);\n\n return async (request) => {\n const data: DetailInstanceData = {\n type: request.type,\n id: request.id,\n seed: request.seed ?? null,\n list: request.list ?? null,\n };\n let presentation = request.presentation;\n if (!presentation) {\n try {\n presentation = await host.resolvePresentation(request.type);\n } catch (error: unknown) {\n // The knob resolver RAISES for an unregistered key by design. The\n // stand-in announces itself with the remedy and uses the platform\n // default the knob would have carried.\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[detail] ${DETAIL_PRESENTATION_KNOB} could not be resolved`, error);\n host.notify.error(\n `Your detail presentation setting could not be read (${DETAIL_PRESENTATION_KNOB}: ${message}). ` +\n \"Opening as a window until it can be read; an administrator can register or repair the setting.\",\n );\n presentation = \"window\";\n }\n }\n if (presentation === \"page\") {\n host.navigate.toPage({ type: data.type, id: data.id }, { seed: data.seed, list: data.list });\n } else {\n host.open({ presentation, data });\n }\n return presentation;\n };\n}\n","// @ai-matrx/detail — src/types.ts\n//\n// The Detail primitive's contract. ONE core per record type; the wrapper\n// yields three presentations (window | docked | page) from that one\n// registration. Nothing in this file knows how a record is fetched, how a\n// window is drawn, or which app hosts it — every app-specific capability\n// arrives through `DetailHostPorts` (see react/host.tsx). This module imports\n// React TYPES only and is legal inside a React Server Component.\n\nimport type { ComponentType, ReactNode } from \"react\";\n\n/** The three ways a record detail can show. `window` is the platform default. */\nexport const DETAIL_PRESENTATIONS = [\"window\", \"docked\", \"page\"] as const;\nexport type DetailPresentation = (typeof DETAIL_PRESENTATIONS)[number];\n\n/**\n * The knob every presentation decision reads (`platform.feature_knob`,\n * feature `ui.detail`, key `default_presentation`; seeded by matrx-frontend's\n * `migrations/detail_presentation_knob.sql`). Enum of DETAIL_PRESENTATIONS,\n * overridable by organization and user. The package never reads the settings\n * ladder itself — the host resolves this key and answers through the\n * `usePresentationSetting` / `resolvePresentation` ports.\n */\nexport const DETAIL_PRESENTATION_KNOB = \"ui.detail.default_presentation\";\n\n/**\n * The PER-RECORD-TYPE override of the setting above (`platform.feature_knob`,\n * feature `ui.detail`, key `presentation_by_type`): a json object keyed by\n * record type token, e.g. `{\"file\": \"docked\"}`. Nearest wins over\n * `default_presentation` for the type it names, and the same organization →\n * user ladder resolves it, so an organization can set \"contracts open as a\n * page\" and a person can still choose otherwise for themselves.\n *\n * WHY A SECOND KEY AND NOT THE `table` RUNG. The platform's per-table rung\n * (DD-131, precedence 50) is keyed by a `platform.entity_types` ROW ID, and\n * that table is admin-only by a restrictive policy — a browser cannot read the\n * id, so a `table` rung would be a setting the screen offers and the reader\n * never honours. One json key on the ladder the client already has is the\n * override the settings platform supports today.\n */\nexport const DETAIL_PRESENTATION_BY_TYPE_KNOB = \"ui.detail.presentation_by_type\";\n\n/**\n * How many records of the list a detail was opened FROM may ride the page\n * presentation's URL (`platform.feature_knob`, feature `ui.detail`, key\n * `list_context_max_ids`; seeded by matrx-frontend's\n * `migrations/detail_list_context_max_knob.sql`). Organization-overridable.\n *\n * Why a knob and not a constant: it is a ceiling, and every ceiling here is a\n * row an admin owns (`common-docs/policies/limits-are-knobs-agents-set-them.md`).\n * Why it exists at all: uncapped, a 500-row list produced a >20 KB href no\n * server accepts (NEW-7, VERIFY-U-P1-R2). Beyond the cap the URL carries the\n * window around the current record and the detail says the list was trimmed.\n */\nexport const DETAIL_LIST_CONTEXT_MAX_KNOB = \"ui.detail.list_context_max_ids\";\n\n/**\n * The cap when the knob has not answered yet (a cold cache, a signed-out\n * render, a host that binds no settings ladder). 100 `type.id` pairs with uuid\n * ids is the most the strictest presentation's FINAL URL can carry inside the\n * budget below, and it is far more neighbours than a person arrows through in\n * one sitting.\n */\nexport const DEFAULT_DETAIL_LIST_CONTEXT_MAX = 100;\n\n/**\n * 🚨 NEW-19 (VERIFY-U-P1-R4) — THE BUDGET BELONGS TO THE FINAL URL, ONCE.\n *\n * It used to be 6,000 characters measured on the list VALUE, one escaping layer\n * too early, and nothing bounded the whole address:\n *\n * * a detail window's deep link measured 5,992 against that 6,000 and reached\n * the address bar at 7,416, because the host's panel-URL writer re-serializes\n * the `?panels=` token through `new URLSearchParams(...).toString()`, which\n * re-escapes every `%` the token's own escaping already produced (a uuid's\n * `-` goes `-` → `%2D` → `%252D`);\n * * a detail PAGE that also carried an open window was 13,433 characters —\n * its own `?l=` plus the merged `panels=` — past the 8 KB request line, so\n * the edge answers 414 and the link is dead (VERIFY-U-P1-R4, NEW-19).\n *\n * 8,000 characters is the strictest edge in front of this platform (an 8 KB\n * request line), and the method, the path, every other parameter and the\n * token's own non-list args share it: a caller passes what the rest of the URL\n * already costs as `reservedBytes` and `trimListContext` measures the FINAL\n * serialized form. No caller may skip it, and the record SAYS what was cut\n * (`trimmedFrom`).\n */\nexport const DETAIL_URL_BUDGET_BYTES = 8000;\n\n/**\n * The hard ceiling on the knob — the number of records the final URL can really\n * carry in EVERY presentation, so a value below it always changes what travels.\n *\n * 🚨 NEW-20 (VERIFY-U-P1-R4). The ceiling was 500 while the byte budget stopped\n * a uuid list at 139, so every value an administrator could set from 139 to 500\n * behaved identically and the knob's own live basis text told them 200 records\n * would travel. Measured against the final URL: 100 uuid entries under a short\n * type token cost ~6.7 KB in the window's `?panels=` token (the strictest\n * spelling) and ~4.3 KB in the page query, both inside the budget above; 150\n * costs ~10 KB in the token and does not fit. matrx-frontend's\n * `migrations/detail_list_context_max_deliverable.sql` lowers the knob's own\n * `max_value` and default to the same number and rewrites its basis text, so\n * the settings screen cannot offer records the URL can never carry. A longer\n * type token still trims below the cap — and still says so.\n */\nexport const DETAIL_LIST_CONTEXT_MAX_IDS_CEILING = 100;\n\n/**\n * The knob's value as a usable cap; the default for anything that is not one,\n * and never more than the ceiling the byte budget can honour (NEW-12). The ONE\n * place the knob is turned into a number — the page href builder, the window's\n * deep-link token and the trim itself all read it through here.\n */\nexport function detailListContextMax(raw: unknown): number {\n const value = typeof raw === \"number\" ? raw : Number(raw);\n const asked = Number.isFinite(value) && value >= 1\n ? Math.floor(value)\n : DEFAULT_DETAIL_LIST_CONTEXT_MAX;\n return Math.min(asked, DETAIL_LIST_CONTEXT_MAX_IDS_CEILING);\n}\n\n/** The per-type entry for `type`, when the map holds a usable one. */\nexport function presentationForTypeFromMap(\n raw: unknown,\n type: string,\n): DetailPresentation | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n const value = (raw as Record<string, unknown>)[type];\n return isDetailPresentation(value) ? value : undefined;\n}\n\nexport function isDetailPresentation(value: unknown): value is DetailPresentation {\n return (\n typeof value === \"string\" &&\n (DETAIL_PRESENTATIONS as readonly string[]).includes(value)\n );\n}\n\n/** A record, named the way the platform names records: type token + id. */\nexport interface DetailRef {\n type: string;\n id: string;\n}\n\n/** What the opener already knows, shown instantly until the row loads. */\nexport interface DetailSeed {\n name?: string | null;\n about?: string | null;\n}\n\n/**\n * The list the record was opened FROM, so `[` / `]` move to the previous /\n * next record without going back to the list. Plain data — it travels through\n * the host's state store and the URL.\n */\nexport interface DetailListContext {\n items: DetailRef[];\n index: number;\n /**\n * Set when this list is a WINDOW cut out of a longer one (the page\n * presentation carries the list in the URL and it is capped —\n * `ui.detail.list_context_max_ids`): the length of the list it was cut from,\n * so the detail can say the neighbours beyond it are not reachable from here\n * instead of presenting the window as the whole list (NEW-7).\n */\n trimmedFrom?: number;\n}\n\n/** What `useOpenDetail` accepts. */\nexport interface DetailOpenRequest extends DetailRef {\n /** Skip the setting and open this way. Omit to honour the user's setting. */\n presentation?: DetailPresentation;\n seed?: DetailSeed | null;\n list?: DetailListContext | null;\n}\n\n/** The payload a presentation receives — everything an open request carried. */\nexport interface DetailInstanceData extends DetailRef {\n seed: DetailSeed | null;\n list: DetailListContext | null;\n}\n\n// ─── The record-type contract ───────────────────────────────────────────────\n\nexport type DetailRow = Record<string, unknown>;\n\nexport type DetailLoadResult<Row extends DetailRow = DetailRow> =\n | { row: Row }\n | { notFound: true };\n\n/** One rendered field. `ref` makes the value a door instead of text. */\nexport interface DetailField {\n key: string;\n label: string;\n text: string;\n /** Render in a monospace block (JSON, long identifiers). */\n mono?: boolean;\n /**\n * This value names another record — render it as a door.\n *\n * 🚨 AND THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). The body used to\n * render the door from `ref.id` alone, so a field whose `text` was\n * \"Brightwater Robotics\" printed a truncated uuid instead: a door nobody can read is\n * half a dead end. `name` is the record's own name for the host's `RefCell` to\n * render; when it is omitted the body falls back to `text` (unless `text` is\n * the id itself, in which case there is nothing better to say).\n */\n ref?: { token: string; id: string; name?: string | null } | null;\n}\n\n/**\n * The \"source health strip\" for a synced record — where it came from, when it\n * was last refreshed, whether the grant behind it still works, and how to open\n * it at the source. Absent for records the platform owns outright.\n */\nexport interface DetailSourceHealth {\n /** Human name of the source (\"Google Drive\", \"Search Console\"). */\n source: string;\n lastRefreshedAt?: string | null;\n /**\n * 🚨 THE GRANT-STATE VOCABULARY. Five of the six say what is wrong with the\n * CONNECTION; `blocked` says the record cannot be refreshed and NOTHING the\n * person can click repairs it (chair, 2026-09-18).\n *\n * - `ok` → the grant behind this record works;\n * - `expired` → it lapsed and a reconnect renews it;\n * - `revoked` → someone withdrew it and a reconnect re-grants it;\n * - `missing` → we never had it, or the record is not shared with us;\n * - `unknown` → we could not check (a read failed) — reconnecting is safe;\n * - `blocked` → the source itself refuses, and no control repairs it: a\n * provider outage, a quota, a platform-configuration fault,\n * a connection the server reports as unavailable. The strip\n * says BLOCKED, states the reason, and offers NO Reconnect —\n * which `reconnectFor` enforces for this word whatever the\n * producer or the host supplies, because a button that\n * cannot work is a control that does nothing (law 4).\n *\n * `unknown` used to carry both meanings, so a blocked record was offered the\n * consent window it could not be repaired by (VERIFY-U-P1-R5, N1's class).\n */\n grant: \"ok\" | \"expired\" | \"revoked\" | \"missing\" | \"unknown\" | \"blocked\";\n /** One sentence for the grant state, in the person's language. */\n grantDetail?: string | null;\n openAtSourceHref?: string | null;\n /**\n * The per-record label for the \"open at source\" control, when the\n * registration knows it precisely (chair, 2026-09-18). `DetailBody` derives\n * a label from `source` on its own (see `deriveOpenAtSourceLabel`) for a\n * producer that has no opinion, but that derivation guesses from a family\n * name (\"Google Docs, Sheets & Drive files\" → \"Open in Google\") — a\n * registration that knows the row's exact kind (a Doc vs. a Sheet vs. a\n * plain Drive file; a Calendar event) sets this instead and it wins.\n */\n openAtSourceLabel?: string;\n /**\n * The repair offered when the grant is not `ok`. 🚨 THREE-VALUED, AND THE\n * THREE MEAN DIFFERENT THINGS (Bugbot round 18 on frontend PR 228):\n *\n * - a function → offer THIS action;\n * - `null` → offer NOTHING: the producer has decided a reconnect\n * would not repair this refusal (a share the owner must\n * grant, a record we own, an unclassified failure) and a\n * Reconnect button would be a control that does nothing;\n * - omitted → the producer has no opinion; the host's `reconnectSource`\n * port supplies the action when it has one.\n *\n * `useDetailHealth` honours `null` explicitly — it used to fill the host\n * action back in with `??`, which reads `null` as \"unset\", so a synced record\n * whose refusal a reconnect cannot fix still showed Reconnect.\n */\n onReconnect?: (() => void) | null;\n /** Offered when the record can be refreshed on demand. */\n onRefresh?: (() => void | Promise<void>) | null;\n}\n\n/** What a `health` producer is told about the record it is answering for. */\nexport interface DetailHealthContext {\n ref: DetailRef;\n /** Aborted when the record changes or the detail closes. */\n signal: AbortSignal;\n}\n\n/**\n * The health producer: sync or async, and free to answer `null` for a record\n * that is not synced. A throw or a rejection is rendered as an honest \"we could\n * not check the connection\" strip, never as silence (`useDetailHealth`).\n */\nexport type DetailHealthProducer<Row extends DetailRow = DetailRow> = (\n row: Row,\n ctx: DetailHealthContext,\n) => DetailSourceHealth | null | Promise<DetailSourceHealth | null>;\n\n/** An extra section a record type adds under the fixed ones. */\nexport interface DetailSection {\n id: string;\n label: string;\n content: ReactNode;\n}\n\n/** What a registration's optional `Frame` and sections receive. */\nexport interface DetailFrameContext {\n ref: DetailRef;\n recordType: DetailRecordType;\n title: string;\n about: string | null;\n status: DetailStatus;\n fields: DetailField[];\n presentation: DetailPresentation;\n}\n\n/**\n * ONE registration per record type. The core component (header + fixed\n * sections) is derived from this; a record type never writes presentation\n * code. Everything optional degrades to \"section absent\", never to a dead\n * or disabled-looking control.\n */\nexport interface DetailRecordType<Row extends DetailRow = DetailRow> {\n /** The type token this registration answers for (as opened). */\n type: string;\n /** Human label (\"Task\", \"File\"). */\n label: string;\n icon: ComponentType<{ className?: string }>;\n /** Accent classes for the icon and type chip; semantic tokens only. */\n accent?: { text: string; bg: string; ring: string } | null;\n /**\n * The canonical entity-registry token for doors, associations and history.\n * `null` when the type is not a registry entity — the header then offers\n * copy-id only, and the associations / history sections are absent.\n */\n entityToken: string | null;\n /**\n * Load the full row. `null` when the type has no single canonical source\n * (the detail then renders from the seed alone and says so).\n */\n load: ((id: string, signal: AbortSignal) => Promise<DetailLoadResult<Row>>) | null;\n /** The title to show; receives the loaded row (or null) and the seed. */\n title: (row: Row | null, seed: DetailSeed | null) => string;\n /**\n * 🚨 WHAT THIS ROW IS, WHEN THE TYPE CANNOT SAY (chair, 2026-09-18). `label` is\n * per TYPE, and a registered type is regularly a family: `crm.party` holds\n * 1,432 companies and 460 persons, so a type labelled \"Person\" told a person\n * that a company is a person — twice on screen, above a field reading\n * `organization` (VERIFY-U-P1-R5, N6). A registration that can tell the\n * difference from the row answers here.\n *\n * It names the RECORD, so it is used for the header's type chip and for the\n * stand-in titles (\"Company 3f2a1b9c\") only. The presentation pane keeps the\n * type-level `label`, because \"Every contact record now opens as a window\" is a\n * statement about the TYPE's setting, not about the row on screen.\n *\n * `null` (or omitted) falls back to `label`, then to the raw type token.\n */\n labelForRow?: (row: Row | null) => string | null;\n /** The field list for the fields section. */\n fields: (row: Row) => DetailField[];\n /**\n * 🚨 THE SOURCE HEALTH STRIP'S PRODUCER. Source health for a SYNCED record;\n * omit it, or answer `null`, for a record the platform owns outright.\n *\n * It may be ASYNC, because whether the grant behind a synced record still works\n * is not in the row — it is the connector's recorded per-capability health, read\n * from the server. The host wires this field when it registers the record type\n * (never a second reader of the connector's health), and `useDetailHealth`\n * resolves it. Until 2026-09-17 nothing set it, so the strip could not render on\n * any record and the promise that a refusal anywhere shows on every dependent\n * record with the same Reconnect had no witness (VERIFY-U-P1-R4).\n */\n health?: DetailHealthProducer<Row> | null;\n /**\n * Tokens the associations section shows. `null` hides the section.\n * Omitted → the host's default set minus this record's own token.\n */\n associationTokens?: string[] | null;\n /** Show the history section (row versions). Default: when `entityToken` is set. */\n history?: boolean;\n /** Extra sections under the fixed ones. */\n extraSections?: ((row: Row | null, ctx: DetailFrameContext) => DetailSection[]) | null;\n /**\n * Wraps the whole body (surface runtime, right-click menu, …). Receives the\n * live context so a host can expose the dossier to its agents. Optional.\n */\n Frame?: ComponentType<{ ctx: DetailFrameContext; children: ReactNode }> | null;\n}\n\nexport type DetailStatus =\n | \"loading\"\n | \"ready\"\n | \"not-found\"\n | \"error\"\n /** The type has no `load` — rendered from the seed alone. */\n | \"none\";\n\nexport type DetailLoadState<Row extends DetailRow = DetailRow> =\n | { status: \"loading\" }\n | { status: \"ready\"; row: Row }\n | { status: \"not-found\" }\n | { status: \"error\"; message: string }\n | { status: \"none\" };\n\n/** One row of the history section. */\nexport interface DetailHistoryEntry {\n version: number;\n operation: string;\n actorId: string | null;\n occurredAt: string;\n isCurrent: boolean;\n}\n","// @ai-matrx/detail — src/react/DetailHeader.tsx\n//\n// The header's two halves, filled into whichever shell is showing the record:\n// <DetailTitle> icon · name · type chip · the record's own doors\n// <DetailActions> previous / next · open-as (window / docked / page) · copy id\n//\n// 🚨 TWO RULES THIS FILE EXISTS TO HOLD (VERIFY-U-P1, D2 and D4):\n//\n// 1. THE NAME IS THE ONE THING THE HEADER ALWAYS SHOWS. At 390px the page\n// header used to reduce to icon + type chip + icons, with the record's name\n// gone and the id chip colliding with the shell's own right-hand cluster —\n// a person could not tell WHICH record they were looking at. The type chip\n// and the record's doors now step aside below `sm` and reappear in the\n// body's meta line (`DetailRecordMeta`), which is where \"below or into\n// overflow\" lands. The name never steps aside.\n//\n// 2. THE ACTION CLUSTER IS ICONS ONLY, AT EVERY WIDTH. A `WindowPanel` centres\n// its title across the whole bar, so a wide action cluster is drawn on top\n// of it. The old copy control carried the full uuid behind a `sm:` class —\n// and `sm:` answers the VIEWPORT, not the 540px window, so on a 1440px\n// screen the id text was always there and always overlapped. The id lives\n// in the body meta line instead; up here it is one copy icon.\n//\n// 3. A stand-in title (loading / not found / failed) is rendered AS a stand-in:\n// muted, with no doors beside it, because there is no record to open. A\n// record that exists but stores nothing more here is NOT that case: it is\n// named from its type and id and keeps its doors (NEW-9).\n//\n// 4. THE NAME HAS A FLOOR AND THE CLUSTER COLLAPSES FIRST (NEW-8). At the\n// window's own 360px minimum the cluster left the name ~60px. The name now\n// carries a minimum width and the optional controls (open-as, copy) move into\n// an overflow menu below a CONTAINER width — never a `sm:` viewport class,\n// which cannot know a window's size. See `headerGeometry.ts`.\n\n\"use client\";\n\nimport { useEffect, useRef, useState, type ComponentType } from \"react\";\nimport {\n AppWindowIcon,\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n CopyIcon,\n ExpandIcon,\n MoreHorizontalIcon,\n PanelRightIcon,\n type DetailIconProps,\n} from \"./icons\";\nimport { cn } from \"@ai-matrx/design-system\";\n\nimport { useDetailHost } from \"./host\";\nimport type { DetailPresentation } from \"../types\";\nimport type { DetailCore } from \"./useDetailCore\";\n\nconst ICON_BUTTON =\n \"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors \" +\n \"hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:opacity-40 disabled:hover:bg-transparent pointer-coarse:h-10 pointer-coarse:w-10\";\n\nconst MENU_ITEM =\n \"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-muted-foreground \" +\n \"transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 \" +\n \"focus-visible:ring-ring pointer-coarse:py-2.5\";\n\nconst TYPE_CHIP =\n \"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide ring-1 ring-inset\";\n\nconst PRESENTATIONS: {\n value: DetailPresentation;\n label: string;\n Icon: ComponentType<DetailIconProps>;\n}[] = [\n { value: \"window\", label: \"Open as window\", Icon: AppWindowIcon },\n { value: \"docked\", label: \"Open docked to the side\", Icon: PanelRightIcon },\n { value: \"page\", label: \"Open as page\", Icon: ExpandIcon },\n];\n\nfunction TypeChip({ core, className }: { core: DetailCore; className?: string }) {\n const accent = core.recordType?.accent ?? null;\n return (\n <span\n className={cn(\n TYPE_CHIP,\n accent?.bg ?? \"bg-muted\",\n accent?.text ?? \"text-muted-foreground\",\n accent?.ring ?? \"ring-border\",\n className,\n )}\n data-detail-type-chip\n >\n {/* What THIS ROW is (chair, 2026-09-18): `recordLabel`, not the type's own\n label — one registered type is regularly a family, and the chip is the\n place a company was called a Person. */}\n {core.recordLabel}\n </span>\n );\n}\n\nexport function DetailTitle({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const Icon = core.recordType?.icon ?? null;\n const accent = core.recordType?.accent ?? null;\n const RecordDoors = host.doors.RecordDoors;\n return (\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {Icon ? <Icon className={cn(\"h-4 w-4 shrink-0\", accent?.text)} /> : null}\n {/* The name. Never hidden, never truncated away to nothing. */}\n <span\n /* `min-w-[6rem]` is DETAIL_TITLE_MIN_WIDTH_CLASS, written out so\n Tailwind compiles it (NEW-8). */\n className={cn(\n \"flex-1 truncate text-sm font-medium min-w-[6rem]\",\n core.titleIsStandIn ? \"text-muted-foreground\" : \"text-foreground\",\n )}\n data-detail-title\n data-detail-title-standin={core.titleIsStandIn ? \"true\" : undefined}\n >\n {core.title}\n </span>\n {/* Type chip: desktop only — `DetailRecordMeta` carries it on a phone. */}\n <TypeChip core={core} className=\"hidden sm:inline-block\" />\n {/* The record's own doors — a SIBLING of the title, never inside a button.\n Absent while the title is a stand-in: there is no record to open yet.\n The host's control renders nothing when the token has no route and no\n peek, so an unregistered type keeps the name and nothing else. */}\n {core.entityToken && core.doorsAvailable ? (\n <span className=\"hidden shrink-0 sm:inline-flex\">\n <RecordDoors token={core.entityToken} id={core.ref.id} name={core.title} />\n </span>\n ) : null}\n </div>\n );\n}\n\n/**\n * The type, the id and the record's doors, in the BODY — where the header\n * hands them off below `sm`, and where the id lives at every width now that\n * the header cluster is icons only.\n */\nexport function DetailRecordMeta({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const RecordDoors = host.doors.RecordDoors;\n const trimmedFrom = core.list.context?.trimmedFrom;\n return (\n <div\n className=\"flex flex-wrap items-center gap-2 border-b border-border/60 px-4 py-2\"\n data-detail-record-meta\n >\n <TypeChip core={core} className=\"sm:hidden\" />\n {core.entityToken && core.doorsAvailable ? (\n <span className=\"inline-flex sm:hidden\">\n <RecordDoors token={core.entityToken} id={core.ref.id} name={core.title} />\n </span>\n ) : null}\n <span className=\"truncate font-mono text-[10px] text-muted-foreground\" data-detail-id>\n {core.ref.id}\n </span>\n {/* 🚨 NEW-7 — A TRIMMED LIST SAYS SO. The page presentation carries its\n list in the URL, capped by `ui.detail.list_context_max_ids`; beyond the\n cap it holds the window around this record. Presenting that window as\n the whole list would be the screen quietly lying about what the arrows\n can reach. */}\n {trimmedFrom ? (\n <span className=\"w-full text-[10px] text-muted-foreground\" data-detail-list-trimmed>\n {`Stepping through ${core.list.context?.items.length} of the ${trimmedFrom} records in the list this was opened from — a link can only carry that many, so the rest are not reachable from here. Open the list again to reach them.`}\n </span>\n ) : null}\n </div>\n );\n}\n\nexport function DetailActions({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const [copied, setCopied] = useState(false);\n const [menuOpen, setMenuOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n\n const copyId = async () => {\n const ok = await host.copyText(core.ref.id);\n if (!ok) {\n host.notify.error(\"Could not copy the record id to the clipboard.\");\n return;\n }\n setCopied(true);\n window.setTimeout(() => setCopied(false), 1500);\n };\n\n // A click anywhere else, or Escape, closes the overflow menu. Escape is left\n // to the menu by the keyboard model precisely because it is a `role=\"menu\"`\n // (NEW-11), so the menu must be the thing that closes on it.\n useEffect(() => {\n if (!menuOpen) return undefined;\n const onDown = (event: MouseEvent) => {\n if (!(event.target instanceof Node)) return;\n if (menuRef.current && !menuRef.current.contains(event.target)) setMenuOpen(false);\n };\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") setMenuOpen(false);\n };\n document.addEventListener(\"mousedown\", onDown, true);\n document.addEventListener(\"keydown\", onKey, true);\n return () => {\n document.removeEventListener(\"mousedown\", onDown, true);\n document.removeEventListener(\"keydown\", onKey, true);\n };\n }, [menuOpen]);\n\n const list = core.list;\n const others = PRESENTATIONS.filter((p) => p.value !== core.presentation);\n\n return (\n <div className=\"flex shrink-0 items-center gap-0.5\" data-detail-actions>\n {/* Previous / next stay in the bar at every width: they are what the\n person is DOING while stepping through a list. */}\n {list.context ? (\n <>\n <button\n type=\"button\"\n className={ICON_BUTTON}\n onClick={list.prev}\n disabled={!list.hasPrev}\n aria-label=\"Previous record (Up arrow, or left bracket)\"\n title=\"Previous record ↑ or [\"\n >\n <ChevronLeftIcon className=\"h-4 w-4\" />\n </button>\n <span className=\"px-1 text-[11px] tabular-nums text-muted-foreground\">\n {list.context.index + 1}/{list.context.items.length}\n </span>\n <button\n type=\"button\"\n className={ICON_BUTTON}\n onClick={list.next}\n disabled={!list.hasNext}\n aria-label=\"Next record (Down arrow, or right bracket)\"\n title=\"Next record ↓ or ]\"\n >\n <ChevronRightIcon className=\"h-4 w-4\" />\n </button>\n <span className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n </>\n ) : null}\n\n {/* 🚨 NEW-8 — THE OPTIONAL CONTROLS YIELD BEFORE THE NAME DOES. Below the\n window header's container width these become one overflow button; the\n classes are the literals `headerGeometry.ts` names, written out so\n Tailwind compiles them, and the guard asserts they agree. */}\n <span\n className=\"flex items-center gap-0.5 @max-[26rem]/window-header:hidden\"\n data-detail-actions-collapsible\n >\n {others.map(({ value, label, Icon }) => (\n <button\n key={value}\n type=\"button\"\n className={ICON_BUTTON}\n onClick={() => core.switchTo(value)}\n aria-label={label}\n title={label}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n {/* Icons only: the id itself is in the body meta line (see rule 2). */}\n <button\n type=\"button\"\n onClick={() => void copyId()}\n className={ICON_BUTTON}\n aria-label=\"Copy record id\"\n title=\"Copy record id\"\n data-detail-copy-id\n >\n {copied ? (\n <CheckIcon className=\"h-3.5 w-3.5 text-primary\" />\n ) : (\n <CopyIcon className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </span>\n\n <span className=\"relative hidden @max-[26rem]/window-header:inline-flex\" ref={menuRef}>\n <button\n type=\"button\"\n className={cn(ICON_BUTTON, \"hidden @max-[26rem]/window-header:inline-flex\")}\n onClick={() => setMenuOpen((wasOpen) => !wasOpen)}\n aria-label=\"More record actions\"\n title=\"More record actions\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n data-detail-actions-overflow\n >\n <MoreHorizontalIcon className=\"h-4 w-4\" />\n </button>\n {menuOpen ? (\n <div\n role=\"menu\"\n aria-label=\"Record actions\"\n className=\"absolute right-0 top-full z-50 mt-1 min-w-44 rounded-md border border-border bg-card p-1 shadow-md\"\n data-detail-actions-menu\n >\n {others.map(({ value, label, Icon }) => (\n <button\n key={value}\n type=\"button\"\n role=\"menuitem\"\n className={MENU_ITEM}\n onClick={() => {\n setMenuOpen(false);\n core.switchTo(value);\n }}\n >\n <Icon className=\"h-3.5 w-3.5 shrink-0\" />\n {label}\n </button>\n ))}\n <button\n type=\"button\"\n role=\"menuitem\"\n className={MENU_ITEM}\n onClick={() => {\n setMenuOpen(false);\n void copyId();\n }}\n >\n <CopyIcon className=\"h-3.5 w-3.5 shrink-0\" />\n Copy record id\n </button>\n </div>\n ) : null}\n </span>\n </div>\n );\n}\n","// @ai-matrx/detail — src/react/icons.tsx\n//\n// THE PACKAGE SHIPS ITS OWN GLYPHS (C19, typescript-package-standard: \"no\n// icon-library dependency, ever\"). These seventeen are the Lucide glyphs the\n// primitive draws — copied from Lucide (ISC license, https://lucide.dev) with\n// Lucide's default stroke attributes — so a consumer never inherits a\n// lucide-react version, and a host that draws no Lucide at all still gets the\n// same header, strip and pane. The design-system `data-table/icons.tsx`\n// precedent, at the same fraction-of-a-percent cost.\n//\n// A record type's OWN icon (`DetailRecordType.icon`) is host data and arrives\n// through the registration; nothing here decides it.\n\nimport type { ReactNode, SVGProps } from \"react\";\n\nexport type DetailIconProps = SVGProps<SVGSVGElement>;\n\nfunction glyph(name: string, children: ReactNode) {\n function Icon(props: DetailIconProps) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={24}\n height={24}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n data-detail-icon={name}\n {...props}\n >\n {children}\n </svg>\n );\n }\n Icon.displayName = `DetailIcon(${name})`;\n return Icon;\n}\n\nexport const AlertCircleIcon = glyph(\n \"alert-circle\",\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" />\n <line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />\n </>,\n);\n\nexport const ExternalLinkIcon = glyph(\n \"external-link\",\n <>\n <path d=\"M15 3h6v6\" />\n <path d=\"M10 14 21 3\" />\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n </>,\n);\n\nexport const HistoryIcon = glyph(\n \"history\",\n <>\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n <path d=\"M12 7v5l4 2\" />\n </>,\n);\n\nexport const Link2Icon = glyph(\n \"link-2\",\n <>\n <path d=\"M9 17H7A5 5 0 0 1 7 7h2\" />\n <path d=\"M15 7h2a5 5 0 1 1 0 10h-2\" />\n <line x1=\"8\" x2=\"16\" y1=\"12\" y2=\"12\" />\n </>,\n);\n\nexport const RefreshCwIcon = glyph(\n \"refresh-cw\",\n <>\n <path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" />\n <path d=\"M21 3v5h-5\" />\n <path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" />\n <path d=\"M8 16H3v5\" />\n </>,\n);\n\nconst SHIELD =\n \"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\";\n\nexport const ShieldAlertIcon = glyph(\n \"shield-alert\",\n <>\n <path d={SHIELD} />\n <path d=\"M12 8v4\" />\n <path d=\"M12 16h.01\" />\n </>,\n);\n\nexport const ShieldCheckIcon = glyph(\n \"shield-check\",\n <>\n <path d={SHIELD} />\n <path d=\"m9 12 2 2 4-4\" />\n </>,\n);\n\nexport const AppWindowIcon = glyph(\n \"app-window\",\n <>\n <rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\" />\n <path d=\"M10 4v4\" />\n <path d=\"M2 8h20\" />\n <path d=\"M6 4v4\" />\n </>,\n);\n\nexport const CheckIcon = glyph(\"check\", <path d=\"M20 6 9 17l-5-5\" />);\n\nexport const ChevronLeftIcon = glyph(\"chevron-left\", <path d=\"m15 18-6-6 6-6\" />);\n\nexport const ChevronRightIcon = glyph(\"chevron-right\", <path d=\"m9 18 6-6-6-6\" />);\n\nexport const CopyIcon = glyph(\n \"copy\",\n <>\n <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </>,\n);\n\nexport const ExpandIcon = glyph(\n \"expand\",\n <>\n <path d=\"m15 15 6 6\" />\n <path d=\"m15 9 6-6\" />\n <path d=\"M21 16v5h-5\" />\n <path d=\"M21 8V3h-5\" />\n <path d=\"M3 16v5h5\" />\n <path d=\"m3 21 6-6\" />\n <path d=\"M3 8V3h5\" />\n <path d=\"M9 9 3 3\" />\n </>,\n);\n\nexport const MoreHorizontalIcon = glyph(\n \"more-horizontal\",\n <>\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n </>,\n);\n\nexport const PanelRightIcon = glyph(\n \"panel-right\",\n <>\n <rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />\n <path d=\"M15 3v18\" />\n </>,\n);\n\nexport const LoaderIcon = glyph(\"loader\", <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />);\n\nexport const Settings2Icon = glyph(\n \"settings-2\",\n <>\n <path d=\"M14 17H5\" />\n <path d=\"M19 7h-9\" />\n <circle cx=\"17\" cy=\"17\" r=\"3\" />\n <circle cx=\"7\" cy=\"7\" r=\"3\" />\n </>,\n);\n","// @ai-matrx/detail — src/react/DetailBody.tsx\n//\n// The fixed sections, in the order every record shows them:\n// source health strip (synced records only) · about · fields · associations\n// · history · a registration's extra sections\n// A section that does not apply is ABSENT — never an empty box, never a\n// disabled-looking control. Every load state says what happened.\n\n\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn, ErrorActions, Skeleton } from \"@ai-matrx/design-system\";\nimport {\n ASSOCIATION_TARGET_TYPES,\n isEntityTypeToken,\n type AssociationTargetType,\n} from \"@ai-matrx/associations\";\nimport { AssociationCardGrid, PrimaryEntityProvider } from \"@ai-matrx/associations/react\";\nimport { formatDurationMs } from \"@ai-matrx/kit/format\";\n\nimport { formatWhen } from \"../format\";\nimport type { DetailHistoryEntry, DetailSection, DetailSourceHealth } from \"../types\";\nimport { DetailRecordMeta } from \"./DetailHeader\";\nimport { DetailPresentationPane } from \"./DetailPresentationPane\";\nimport { useDetailHost } from \"./host\";\nimport {\n AlertCircleIcon,\n ExternalLinkIcon,\n HistoryIcon,\n Link2Icon,\n RefreshCwIcon,\n ShieldAlertIcon,\n ShieldCheckIcon,\n} from \"./icons\";\nimport type { DetailCore } from \"./useDetailCore\";\n\n/**\n * 🚨 D5 — THE SKELETON HAS A BOUNDED WAIT. A load that never answers used to\n * shimmer for ~20 seconds and then drop an error, with nothing in between\n * saying anything was wrong. After this many milliseconds the skeleton says,\n * in plain words, that it is still waiting — the stand-in announcing itself\n * (law 4) rather than pretending progress. A patience threshold, not a ceiling\n * or a quota: it buys nothing and spends nothing, so it is a constant here and\n * not a knob.\n */\nconst SLOW_LOAD_NOTICE_MS = 6000;\n\n/** True once a load has been running longer than a person expects to wait. */\nfunction useSlowLoadNotice(active: boolean, key: string): boolean {\n const [slow, setSlow] = useState(false);\n useEffect(() => {\n if (!active) {\n setSlow(false);\n return undefined;\n }\n setSlow(false);\n const timer = setTimeout(() => setSlow(true), SLOW_LOAD_NOTICE_MS);\n return () => clearTimeout(timer);\n }, [active, key]);\n return slow;\n}\n\nfunction SectionHeading({ icon, children }: { icon?: ReactNode; children: ReactNode }) {\n return (\n <h3 className=\"flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n {icon}\n {children}\n </h3>\n );\n}\n\nfunction Notice({\n tone,\n children,\n}: {\n tone: \"muted\" | \"warn\" | \"error\";\n children: ReactNode;\n}) {\n return (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <AlertCircleIcon\n className={cn(\n \"h-6 w-6\",\n tone === \"error\" && \"text-destructive\",\n tone === \"warn\" && \"text-primary\",\n tone === \"muted\" && \"text-muted-foreground/60\",\n )}\n />\n <p className=\"max-w-prose text-sm text-muted-foreground\">{children}</p>\n </div>\n );\n}\n\n// ─── Source health strip ────────────────────────────────────────────────────\n\n/**\n * The state in one word, for every grant but `ok`. `blocked` is the word for\n * \"the source refuses and nothing you can click repairs it\" (chair, 2026-09-18):\n * the strip says it, states the reason, and shows no Reconnect.\n */\nconst GRANT_WORD: Record<DetailSourceHealth[\"grant\"], string> = {\n ok: \"OK\",\n expired: \"Expired\",\n revoked: \"Revoked\",\n missing: \"Missing\",\n unknown: \"Unknown\",\n blocked: \"Blocked\",\n};\n\n/**\n * 🚨 N14 (VERIFY-U-W1-U-W2) — THE CONTROL THAT LEAVES FOR THE SOURCE NAMES IT.\n * The strip already prints `health.source` beside it, so \"Open at source\" threw\n * away the only word the person recognises. One producer often answers for a\n * whole FAMILY, though (\"Google Docs, Sheets & Drive files\" covers Docs, Sheets\n * and Slides), and \"Open in Google Docs\" on a spreadsheet would be a lie — so a\n * source that lists several things is cut back to the provider word, a short\n * source is used whole, and no source at all keeps the generic phrase rather than\n * letting the screen guess.\n *\n * A registration that knows the row's exact kind sets `health.openAtSourceLabel`\n * instead (a Doc vs. a Sheet vs. a plain Drive file; \"Open in Google Calendar\"\n * for an event) — that field wins over this derivation below, in `HealthStrip`.\n */\nfunction deriveOpenAtSourceLabel(source: string): string {\n const named = source.trim().replace(/\\s+/g, \" \");\n if (!named) return \"Open at source\";\n const listsSeveral = /[,&]/.test(named) || named.split(\" \").length > 3;\n const [firstWord = \"\"] = named.split(\" \");\n const place = listsSeveral ? firstWord.replace(/[,&]+$/, \"\") : named;\n return place ? `Open in ${place}` : \"Open at source\";\n}\n\nfunction HealthStrip({ health }: { health: DetailSourceHealth }) {\n const ok = health.grant === \"ok\";\n const [refreshing, setRefreshing] = useState(false);\n const host = useDetailHost();\n const refresh = async () => {\n if (!health.onRefresh) return;\n setRefreshing(true);\n try {\n await health.onRefresh();\n } catch (error: unknown) {\n host.notify.error(\n `Refresh from ${health.source} failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n } finally {\n setRefreshing(false);\n }\n };\n return (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border/60 px-4 py-2 text-xs\",\n ok ? \"bg-muted/40 text-muted-foreground\" : \"bg-destructive/5 text-foreground\",\n )}\n data-detail-health\n >\n {ok ? (\n <ShieldCheckIcon className=\"h-3.5 w-3.5 text-primary\" />\n ) : (\n <ShieldAlertIcon className=\"h-3.5 w-3.5 text-destructive\" />\n )}\n <span className=\"font-medium\">{health.source}</span>\n {ok ? null : (\n <span\n className=\"rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-destructive\"\n data-detail-health-state={health.grant}\n >\n {GRANT_WORD[health.grant]}\n </span>\n )}\n {health.lastRefreshedAt ? (\n <span>Refreshed {formatWhen(health.lastRefreshedAt)}</span>\n ) : (\n <span>Never refreshed</span>\n )}\n {health.grantDetail ? <span>{health.grantDetail}</span> : null}\n <span className=\"ml-auto flex items-center gap-1\">\n {health.onRefresh ? (\n <button\n type=\"button\"\n onClick={() => void refresh()}\n disabled={refreshing}\n 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\"\n >\n <RefreshCwIcon className={cn(\"h-3 w-3\", refreshing && \"animate-spin\")} />\n Refresh\n </button>\n ) : null}\n {!ok && health.onReconnect ? (\n <button\n type=\"button\"\n onClick={health.onReconnect}\n 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\"\n >\n Reconnect\n </button>\n ) : null}\n {health.openAtSourceHref ? (\n <a\n href={health.openAtSourceHref}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex h-6 items-center gap-1 rounded px-1.5 hover:bg-accent hover:text-foreground pointer-coarse:h-10\"\n >\n <ExternalLinkIcon className=\"h-3 w-3\" />\n {health.openAtSourceLabel ?? deriveOpenAtSourceLabel(health.source)}\n </a>\n ) : null}\n </span>\n </div>\n );\n}\n\n// ─── Fields ─────────────────────────────────────────────────────────────────\n\n/**\n * 🚨 NEW-17 (VERIFY-U-P1-R4) — THE SENTENCE IS DERIVED FROM THE CONTROLS THAT\n * RENDERED, NEVER A FIXED STRING. \"The controls above still open it where it\n * lives, and copy its id\" was printed for `session` — a registered type with no\n * `entityToken`, whose header therefore shows no door at all — and for every\n * unregistered type. The copy control is unconditional in `DetailActions`, so\n * that half is always true; the open half is claimed only when the record really\n * has a door (`core.canOpenElsewhere`).\n */\nfunction whatTheControlsDo(core: DetailCore): string {\n return core.canOpenElsewhere\n ? \"The controls above still open it where it lives, and copy its id.\"\n : \"The controls above copy its id, so you can find it wherever it came from.\";\n}\n\nfunction FieldsSection({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const RefCell = host.doors.RefCell;\n const label = core.recordType?.label.toLowerCase() ?? \"record\";\n\n if (!core.recordType) {\n // 🚨 NEW-9 — THE PERSON'S LANGUAGE, NEVER A REPO PATH. This sentence named a\n // source file and told a brilliant non-technical expert to edit a registry.\n // The remedy is a console warning from the core, where the developer is.\n return (\n <Notice tone=\"warn\">\n {`This is a ${core.ref.type} record. Nothing more about it is stored here yet, so there is ` +\n `nothing else to show. ${whatTheControlsDo(core)}`}\n </Notice>\n );\n }\n if (core.status === \"loading\") {\n return <LoadingFields core={core} label={label} />;\n }\n if (core.status === \"not-found\") {\n // 🚨 THE ABSENT RECORD CARRIES THE HOST'S ERROR ACTIONS TOO. A record that\n // cannot be read (moved, deleted, or invisible under row-level access) is\n // the state a person most needs to hand to someone — so it gets the same\n // host slot every package error gets (`ErrorActions`, bound by the host's\n // `ErrorActionsProvider`; matrx-frontend fills it with its Alchemy menu).\n // It carries the sentence and the record, so the host's copy names WHICH\n // record could not be found. With no provider it renders nothing.\n const sentence = `This ${label} couldn't be found — it may have been moved, deleted, or isn't shared with you.`;\n return (\n <Notice tone=\"warn\">\n {sentence}\n <ErrorActions\n origin=\"detail/DetailBody\"\n operation={`Open this ${label}`}\n message={sentence}\n records={[{ type: core.ref.type, id: core.ref.id }]}\n />\n </Notice>\n );\n }\n if (core.status === \"error\") {\n return (\n <Notice tone=\"error\">\n {`Couldn't load the details for this ${label}. ${core.errorMessage ?? \"\"}`.trim()}\n <ErrorActions origin=\"detail/DetailBody\" operation={`Load this ${label}`} error={core.errorMessage} />\n </Notice>\n );\n }\n if (core.status === \"none\") {\n // 🚨 NEW-1 — THE ABSENT STATE NAMES THE TYPE AND THE REMEDY. This branch is\n // \"nothing is registered to LOAD a record of this type\", which is not the\n // same as \"this record has no extra fields\", and it used to read as the\n // latter under an invented title (VERIFY-U-P1-R2).\n return (\n <Notice tone=\"muted\">\n {`Nothing more about this ${label} is stored here, so there is nothing else to show. ` +\n whatTheControlsDo(core)}\n </Notice>\n );\n }\n if (core.fields.length === 0) {\n return <p className=\"text-sm text-muted-foreground\">No additional fields to show.</p>;\n }\n return (\n <dl className=\"grid grid-cols-1 gap-x-4 gap-y-2.5\">\n {core.fields.map((field) => (\n <div\n key={field.key}\n className=\"flex flex-col gap-0.5 border-b border-border/40 pb-2 last:border-b-0\"\n >\n <dt className=\"text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n {field.label}\n </dt>\n <dd\n className={cn(\n \"break-words text-sm text-foreground\",\n !field.ref &&\n field.mono &&\n \"whitespace-pre-wrap rounded-md bg-muted px-2 py-1 font-mono text-xs\",\n )}\n >\n {field.ref ? (\n // 🚨 THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). This rendered\n // `field.ref.id` alone, so a field whose text was \"Brightwater Robotics\"\n // printed a truncated uuid. The ref's own `name` wins; otherwise the\n // field's text, unless that text IS the id and says nothing more.\n <RefCell\n value={field.ref.id}\n label={field.label}\n token={field.ref.token}\n name={field.ref.name ?? (field.text === field.ref.id ? null : field.text)}\n />\n ) : (\n field.text\n )}\n </dd>\n </div>\n ))}\n </dl>\n );\n}\n\n/** The skeleton, plus the honest line once the wait has gone long (D5). */\nfunction LoadingFields({ core, label }: { core: DetailCore; label: string }) {\n const slow = useSlowLoadNotice(true, `${core.ref.type}:${core.ref.id}`);\n return (\n <div className=\"space-y-3\" aria-busy=\"true\" aria-label={`Loading ${label} details`}>\n {slow ? (\n <p className=\"text-xs text-muted-foreground\" data-detail-slow-load>\n Still loading this {label} — the read has been running for more than{\" \"}\n {formatDurationMs(SLOW_LOAD_NOTICE_MS, { style: \"long\" })}. If nothing appears, the\n record may be unreachable from here; closing and reopening it starts a fresh read.\n </p>\n ) : null}\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className=\"space-y-1.5\">\n <Skeleton className=\"h-2.5 w-20\" />\n <Skeleton className={cn(\"h-4\", i % 2 ? \"w-3/4\" : \"w-1/2\")} />\n </div>\n ))}\n </div>\n );\n}\n\n// ─── Associations ───────────────────────────────────────────────────────────\n\nfunction isAssociationTarget(token: string): token is AssociationTargetType {\n return (ASSOCIATION_TARGET_TYPES as readonly string[]).includes(token);\n}\n\nfunction AssociationsSection({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const token = core.entityToken;\n if (!token || !isAssociationTarget(token) || !host.associations.canAnchor(token)) return null;\n if (core.recordType?.associationTokens === null) return null;\n const wanted = core.recordType?.associationTokens ?? host.associations.defaultTokens;\n const tokens = wanted.filter((t) => t !== token).filter(isEntityTypeToken);\n if (tokens.length === 0) return null;\n return (\n <section className=\"space-y-2\" data-detail-section=\"associations\">\n <SectionHeading icon={<Link2Icon className=\"h-3 w-3\" />}>Linked</SectionHeading>\n <PrimaryEntityProvider value={{ type: token, id: core.ref.id, label: core.title }}>\n <AssociationCardGrid tokens={tokens} />\n </PrimaryEntityProvider>\n </section>\n );\n}\n\n// ─── History ────────────────────────────────────────────────────────────────\n\n/**\n * The columns a change's author is spelled as, in the order the map is asked.\n * `version_list` returns `actor_id`; the others are what sibling history shapes\n * on this platform carry.\n */\nconst ACTOR_COLUMNS = [\"actor_id\", \"user_id\", \"person_id\", \"created_by_id\"] as const;\n\ntype HistoryState =\n | { status: \"loading\" }\n | { status: \"ready\"; entries: DetailHistoryEntry[] }\n | { status: \"error\"; message: string };\n\nfunction HistorySection({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const token = core.entityToken;\n const wanted = core.recordType?.history ?? Boolean(token);\n const [state, setState] = useState<HistoryState>({ status: \"loading\" });\n const lastKey = useRef<string | null>(null);\n // The port function, not the host object: the effect must re-run for a new\n // record, never for a re-render.\n const listHistory = host.history.list;\n const recordId = core.ref.id;\n\n useEffect(() => {\n if (!wanted || !token) return undefined;\n const key = `${token}:${recordId}`;\n if (lastKey.current !== key) {\n lastKey.current = key;\n setState({ status: \"loading\" });\n }\n const controller = new AbortController();\n void listHistory(token, recordId, controller.signal)\n .then((entries) => {\n if (!controller.signal.aborted) setState({ status: \"ready\", entries });\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n setState({\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n });\n return () => controller.abort();\n }, [listHistory, wanted, token, recordId]);\n\n if (!wanted || !token) return null;\n const ActorCell = host.doors.RefCell;\n // 🚨 NEW-23 (VERIFY-U-P1-R4) — WHO MADE THE CHANGE IS NEVER DROPPED. The row\n // was rendered only when `tokenFromColumnName(\"actor_id\")` named a token, and\n // it names none (no `actor` or `user` token has a door in this platform's\n // registry today), so every history row silently threw away the id\n // `version_list` hands over. The map is asked for each column an actor is\n // spelled as — the first that has a door wins — and when none does the value is\n // shown as itself under an honest label, because an identity the data names is\n // never nothing (law 4 + the no-dead-ends class).\n const actorToken = ACTOR_COLUMNS.map((column) => host.doors.tokenFromColumnName(column)).find(\n (candidate): candidate is string => Boolean(candidate),\n );\n // 🚨 NEW-23 (VERIFY-U-P1-R5) — AND THE HONEST FALLBACK WAS STILL A BARE UUID\n // ON EVERY ROW. Nothing was silent, but no host registry gives `actor_id` a\n // door, so \"Changed by 8f3c…-…\" was what a brilliant non-technical expert read\n // on every change of every record. The host's ONE identity resolver answers\n // first now (`history.ActorName`); the door is next; the id itself is last and\n // says, in its title, that we could not find the person.\n const ActorName = host.history.ActorName ?? null;\n\n return (\n <section className=\"space-y-2\" data-detail-section=\"history\">\n <SectionHeading icon={<HistoryIcon className=\"h-3 w-3\" />}>History</SectionHeading>\n {state.status === \"loading\" ? (\n <div className=\"space-y-1.5\" aria-busy=\"true\" aria-label=\"Loading history\">\n <Skeleton className=\"h-3.5 w-2/3\" />\n <Skeleton className=\"h-3.5 w-1/2\" />\n </div>\n ) : state.status === \"error\" ? (\n <p className=\"text-xs text-muted-foreground\">\n {state.message.toLowerCase().includes(\"access denied\")\n ? \"History is only shown to people who can view this record.\"\n : `History could not be read: ${state.message}`}\n <ErrorActions origin=\"detail/DetailBody\" operation=\"Read the history\" error={state.message} />\n </p>\n ) : state.entries.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">No recorded changes yet.</p>\n ) : (\n <ol className=\"space-y-1\">\n {state.entries.map((entry) => (\n <li\n key={`${entry.version}-${entry.occurredAt}`}\n className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs\"\n >\n <span className=\"rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground\">\n v{entry.version}\n </span>\n <span className=\"capitalize text-foreground\">{entry.operation.toLowerCase()}</span>\n <span className=\"text-muted-foreground\">{formatWhen(entry.occurredAt)}</span>\n {entry.isCurrent ? (\n <span className=\"text-[10px] uppercase tracking-wide text-primary\">current</span>\n ) : null}\n {entry.actorId ? (\n ActorName ? (\n <span className=\"text-muted-foreground\" data-detail-history-actor-person>\n Changed by <ActorName actorId={entry.actorId} row={core.row} />\n </span>\n ) : actorToken ? (\n <span className=\"inline-flex items-center\" data-detail-history-actor>\n <ActorCell value={entry.actorId} label=\"Changed by\" token={actorToken} />\n </span>\n ) : (\n <span className=\"text-muted-foreground\" data-detail-history-actor-raw>\n Changed by{\" \"}\n <span\n className=\"font-mono text-[10px]\"\n title=\"We could not find the person behind this id — this host has bound no identity resolver.\"\n >\n {entry.actorId}\n </span>\n </span>\n )\n ) : (\n <span className=\"text-muted-foreground\" data-detail-history-actor-absent>\n No person recorded for this change\n </span>\n )}\n </li>\n ))}\n </ol>\n )}\n </section>\n );\n}\n\n// ─── Body ───────────────────────────────────────────────────────────────────\n\nexport function DetailBody({ core }: { core: DetailCore }) {\n const extra: DetailSection[] =\n core.recordType?.extraSections && core.frameCtx\n ? core.recordType.extraSections(core.row, core.frameCtx)\n : [];\n const Frame = core.recordType?.Frame ?? null;\n\n const body = (\n // `min-h-full` so a Frame's right-click menu answers anywhere in the body,\n // not only on the rows — a short dossier otherwise leaves a dead band.\n <div className=\"flex min-h-full flex-col\">\n <DetailRecordMeta core={core} />\n {core.health ? <HealthStrip health={core.health} /> : null}\n {core.about ? (\n <p className=\"line-clamp-3 border-b border-border/60 p-4 text-xs leading-snug text-muted-foreground\">\n {core.about}\n </p>\n ) : null}\n <div className=\"space-y-6 p-4\">\n <FieldsSection core={core} />\n {core.status === \"ready\" || core.status === \"none\" ? (\n <>\n <AssociationsSection core={core} />\n <HistorySection core={core} />\n {extra.map((section) => (\n <section key={section.id} className=\"space-y-2\" data-detail-section={section.id}>\n <SectionHeading>{section.label}</SectionHeading>\n {section.content}\n </section>\n ))}\n </>\n ) : null}\n {/* Last, quiet, and on every load state: how details open is a setting\n the person can change from right here (and the one caller of the\n keyboard model's `registerSave`). */}\n <DetailPresentationPane core={core} />\n </div>\n </div>\n );\n\n if (Frame && core.frameCtx) return <Frame ctx={core.frameCtx}>{body}</Frame>;\n return body;\n}\n","// @ai-matrx/detail — src/format.ts\n//\n// Generic row → fields formatting, lifted verbatim from the item detail\n// window it replaced so every registry-known record renders exactly as it did.\n\nimport type { DetailField } from \"./types\";\nimport { humanizeIdentifier } from \"@ai-matrx/kit/text-case\";\n\n/** Columns that are pure plumbing — hidden from a formatted field list. */\nexport const HIDDEN_FIELDS = new Set([\n \"id\",\n \"user_id\",\n \"organization_id\",\n \"created_by\",\n \"updated_by\",\n \"embedding\",\n \"search_vector\",\n \"tsv\",\n]);\n\nconst ISO_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n\nexport function formatValue(value: unknown): { text: string; mono?: boolean } | null {\n if (value === null || value === undefined) return null;\n if (typeof value === \"boolean\") return { text: value ? \"Yes\" : \"No\" };\n if (typeof value === \"number\") {\n return { text: Number.isFinite(value) ? value.toLocaleString() : String(value) };\n }\n if (typeof value === \"string\") {\n const t = value.trim();\n if (!t) return null;\n if (ISO_RE.test(t)) {\n const d = new Date(t);\n if (!Number.isNaN(d.getTime())) {\n return { text: d.toLocaleString([], { dateStyle: \"medium\", timeStyle: \"short\" }) };\n }\n }\n return { text: t };\n }\n try {\n const json = JSON.stringify(value, null, 2);\n if (!json || json === \"{}\" || json === \"[]\") return null;\n return { text: json, mono: true };\n } catch {\n return { text: String(value), mono: true };\n }\n}\n\n/**\n * Every populated scalar column of a row as a field; a `<token>_id` column\n * holding a real uuid becomes a door (the host decides which token names a\n * registry entity — a wrong link is worse than no link).\n */\nexport function fieldsFromRow(\n row: Record<string, unknown>,\n doors: {\n tokenFromColumnName: (column: string) => string | null;\n isUuidValue: (value: unknown) => value is string;\n },\n): DetailField[] {\n const out: DetailField[] = [];\n for (const [key, raw] of Object.entries(row)) {\n if (HIDDEN_FIELDS.has(key)) continue;\n const formatted = formatValue(raw);\n if (!formatted) continue;\n const token = doors.isUuidValue(raw) ? doors.tokenFromColumnName(key) : null;\n out.push({\n key,\n label: humanizeIdentifier(key) || key,\n text: formatted.text,\n ...(formatted.mono ? { mono: true } : {}),\n ref: token ? { token, id: raw as string } : null,\n });\n }\n return out;\n}\n\nexport function formatWhen(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleString([], { dateStyle: \"medium\", timeStyle: \"short\" });\n}\n","// @ai-matrx/detail — src/react/DetailPresentationPane.tsx\n//\n// \"How record details open\" — the ONE screen that WRITES\n// `ui.detail.default_presentation`, and the per-record-type override beside\n// it. It sits at the foot of every detail, quiet and collapsed, the way\n// Notion carries \"Open pages in\" inside the peek itself rather than in a\n// settings app three clicks away.\n//\n// 🚨 IT IS ALSO THE SURFACE THAT EXERCISES Cmd/Ctrl+Enter. The keyboard model\n// has always offered `registerSave`, and until this pane existed NOTHING in\n// the repo registered one, so the chord saved nothing anywhere (VERIFY-U-P1).\n// No record type on this branch has an editable body, so the contract is held\n// by the one editable thing a detail owns: this setting. The moment a record\n// type ships an editor it registers its own save the same way and this pane\n// stops being the only caller.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn, ErrorActions } from \"@ai-matrx/design-system\";\n\nimport { DETAIL_PRESENTATIONS, type DetailPresentation } from \"../types\";\nimport { useDetailHost } from \"./host\";\nimport { CheckIcon, LoaderIcon, Settings2Icon } from \"./icons\";\nimport type { DetailCore } from \"./useDetailCore\";\n\nconst WORD: Record<DetailPresentation, string> = {\n window: \"a window\",\n docked: \"a docked panel\",\n page: \"a full page\",\n};\n\nconst LABEL: Record<DetailPresentation, string> = {\n window: \"Window\",\n docked: \"Docked\",\n page: \"Page\",\n};\n\ntype SaveState =\n | { status: \"idle\" }\n | { status: \"saving\" }\n | { status: \"saved\" }\n | { status: \"refused\"; reason: string };\n\nexport function DetailPresentationPane({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const save = host.savePresentation;\n const setting = host.usePresentationSetting(core.ref.type);\n const [choice, setChoice] = useState<DetailPresentation | null>(null);\n const [forThisType, setForThisType] = useState(false);\n const [open, setOpen] = useState(false);\n const [state, setState] = useState<SaveState>({ status: \"idle\" });\n\n const effective = setting.value ?? core.presentation;\n const pending = choice ?? effective;\n const registerSave = core.keyboard.registerSave;\n /** This record type has its own exception — so it can be taken back (NEW-2). */\n const hasTypeException = setting.forType !== undefined;\n\n const commit = async () => {\n if (!save) return;\n setState({ status: \"saving\" });\n const result = await save({\n presentation: pending,\n forType: forThisType ? core.ref.type : null,\n });\n if (result.ok) {\n setState({ status: \"saved\" });\n host.notify.success(\n forThisType\n ? `Every ${core.typeLabel.toLowerCase()} now opens as ${WORD[pending]} for you.`\n : `Record details now open as ${WORD[pending]} for you.`,\n );\n } else {\n setState({ status: \"refused\", reason: result.reason });\n }\n };\n\n /**\n * After a REMOVAL, what governs now — read from the ladder, not from the\n * cached value this write just made stale (NEW-10). The sentence names the\n * rung that actually answers, or says plainly that it could not be read.\n */\n const sentenceAfterRemoval = async (): Promise<string> => {\n const reRead = host.reReadPresentation;\n if (!reRead) {\n return (\n `Your own exception for ${core.typeLabel.toLowerCase()} records is gone. How they open now is ` +\n \"whatever your organization and your own default say — open one to see.\"\n );\n }\n try {\n const effective = await reRead(core.ref.type);\n if (effective.forType) {\n return (\n `Your own exception is gone, but ${core.typeLabel.toLowerCase()} records still open as ` +\n `${WORD[effective.forType]} because your organization sets that for everyone.`\n );\n }\n if (effective.value) {\n return `${core.typeLabel} records now open as ${WORD[effective.value]}, the way you normally open records.`;\n }\n return (\n `Your own exception for ${core.typeLabel.toLowerCase()} records is gone. The setting that ` +\n \"governs them now could not be read, so open one to see how it opens.\"\n );\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n return (\n `Your own exception for ${core.typeLabel.toLowerCase()} records is gone, but what governs them ` +\n `now could not be read (${message}), so open one to see how it opens.`\n );\n }\n };\n\n /**\n * 🚨 NEW-2 — TAKING THE EXCEPTION BACK. Setting \"only for file records\" wrote\n * one entry of a json map and nothing in the product removed it: the only\n * escape was editing that json in the generic settings row. This clears the\n * entry through the same write, so the type goes back to opening the way the\n * person (or their organization) normally opens records.\n */\n const clearForType = async () => {\n if (!save) return;\n setState({ status: \"saving\" });\n const result = await save({ presentation: pending, forType: core.ref.type, clear: true });\n if (result.ok) {\n setState({ status: \"saved\" });\n // 🚨 NEW-10 — NEVER \"they now open your way\" ON THIS WRITE'S OWN WORD. With\n // an ORGANIZATION exception for the same type still in place, the removal\n // succeeds and changes nothing a person can see: the organization's entry\n // wins on the next open. The ladder is re-read and the sentence names\n // whichever rung answers now.\n host.notify.success(await sentenceAfterRemoval());\n } else {\n setState({ status: \"refused\", reason: result.reason });\n }\n };\n\n // Cmd/Ctrl+Enter saves whatever is pending here, from anywhere in the detail.\n // The registration is a stable function reading the LATEST commit through a\n // ref (the same shape `useDetailKeyboard` uses for its handlers), so moving\n // the choice does not re-register, and the chord is registered only while\n // this pane is open — a record type's own editor always wins when one exists.\n const commitRef = useRef(commit);\n commitRef.current = commit;\n useEffect(() => {\n if (!open || !save) return undefined;\n return registerSave(() => commitRef.current());\n }, [open, save, registerSave]);\n\n // The host has no writable setting bound (a client that reads the platform\n // setting but cannot write it). Absent, never a disabled-looking control.\n if (!save) return null;\n\n if (!open) {\n // An unread setting is an error sentence, not a button label: it is said in\n // its own line with the host's actions inside it (never a control inside\n // the row's button), and the row keeps only \"Change\".\n return (\n <div className=\"w-full\">\n {setting.error ? (\n <p className=\"px-1 pt-1 text-[11px] text-muted-foreground\">\n {`Your \"open details as\" setting could not be read (${setting.error}) — details are opening as ${WORD[core.presentation]}.`}\n <ErrorActions origin=\"detail/DetailPresentationPane\" operation=\"Read the open-details-as setting\" error={setting.error} />\n </p>\n ) : null}\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n 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\"\n data-detail-presentation-pane=\"collapsed\"\n >\n <Settings2Icon className=\"h-3 w-3 shrink-0\" />\n {setting.error ? null : <span>{`Details open as ${WORD[effective]}.`}</span>}\n <span className=\"underline underline-offset-2\">Change</span>\n </button>\n </div>\n );\n }\n\n return (\n <section\n className=\"space-y-2 rounded-md border border-border bg-muted/30 p-3\"\n data-detail-presentation-pane=\"open\"\n >\n <h3 className=\"flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n <Settings2Icon className=\"h-3 w-3\" />\n How record details open\n </h3>\n <div className=\"flex flex-wrap gap-1\" role=\"group\" aria-label=\"Open record details as\">\n {DETAIL_PRESENTATIONS.map((value) => (\n <button\n key={value}\n type=\"button\"\n aria-pressed={pending === value}\n onClick={() => {\n setChoice(value);\n setState({ status: \"idle\" });\n }}\n className={cn(\n \"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\",\n pending === value\n ? \"bg-primary text-primary-foreground\"\n : \"bg-card text-muted-foreground hover:text-foreground\",\n )}\n >\n {LABEL[value]}\n </button>\n ))}\n </div>\n {hasTypeException ? (\n <button\n type=\"button\"\n onClick={() => void clearForType()}\n disabled={state.status === \"saving\"}\n className=\"text-left text-[11px] text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground disabled:opacity-60\"\n data-detail-presentation-clear-type\n >\n {`Use the default for ${core.typeLabel.toLowerCase()} records`}\n </button>\n ) : null}\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={forThisType}\n onChange={(event) => {\n setForThisType(event.target.checked);\n setState({ status: \"idle\" });\n }}\n className=\"h-3.5 w-3.5 accent-[var(--color-primary)]\"\n />\n Only for {core.typeLabel.toLowerCase()} records\n </label>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => void commit()}\n disabled={state.status === \"saving\"}\n 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\"\n >\n {state.status === \"saving\" ? (\n <LoaderIcon className=\"h-3 w-3 animate-spin\" />\n ) : state.status === \"saved\" ? (\n <CheckIcon className=\"h-3 w-3\" />\n ) : null}\n Save\n </button>\n <span className=\"text-[11px] text-muted-foreground\">\n {state.status === \"saved\" ? \"Saved. It applies to the next record you open.\" : \"⌘/Ctrl + Enter\"}\n </span>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"ml-auto text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground\"\n >\n Close\n </button>\n </div>\n {state.status === \"refused\" ? (\n <p className=\"text-xs text-destructive\" data-detail-presentation-refusal>\n {state.reason}\n <ErrorActions origin=\"detail/DetailPresentationPane\" operation=\"Change how details open\" message={state.reason} />\n </p>\n ) : null}\n {setting.error ? (\n <p className=\"text-xs text-muted-foreground\">\n The saved setting could not be read here ({setting.error}), so this shows what this detail\n is actually doing.\n <ErrorActions origin=\"detail/DetailPresentationPane\" operation=\"Read the open-details-as setting\" error={setting.error} />\n </p>\n ) : null}\n </section>\n );\n}\n","// @ai-matrx/detail — src/react/presentations.tsx\n//\n// The wrapper: ONE core, three presentations. Each component hands the same\n// header halves and the same body to a different host shell. Nothing here\n// draws chrome — the shells are the host's window manager, its docked side\n// panel and its route header; this file only decides what goes in each slot.\n\n\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\n\nimport { DetailBody } from \"./DetailBody\";\nimport { DetailActions, DetailTitle } from \"./DetailHeader\";\nimport { useDetailCore, type DetailCore } from \"./useDetailCore\";\nimport { requireShell, useDetailHost } from \"./host\";\nimport { detailInstanceKey } from \"../presentation\";\nimport type { DetailInstanceData } from \"../types\";\n\n/**\n * 🚨 NEW-22 (VERIFY-U-P1-R4) — THE KEYBOARD MODEL IS BOUND ON EVERY SLOT THE\n * PRESENTATION FILLS, NOT ONLY THE BODY.\n *\n * `titleNode` and `actions` are handed to the SHELL and render outside the body's\n * root, and no shell in this app handles a keystroke: with focus on the copy-id\n * button, a presentation icon, a previous/next chevron or the overflow trigger,\n * Escape closed nothing in all three desktop presentations (reproduced in each).\n * On a phone the docked and window presentations are a `vaul` Drawer, which\n * handles Escape itself — which is why it hid. The header is part of the detail,\n * so the same handler answers from it.\n *\n * Capture phase on each slot, so two open details never answer one keystroke.\n */\nfunction KeyboardSlot({ core, children }: { core: DetailCore; children: ReactNode }) {\n return (\n <div\n onKeyDownCapture={core.keyboard.rootProps.onKeyDownCapture}\n className=\"flex min-w-0 flex-1 items-center\"\n data-detail-keyboard-slot\n >\n {children}\n </div>\n );\n}\n\n/** The same, for the actions cluster — which must not grow or stretch. */\nfunction KeyboardActionsSlot({ core, children }: { core: DetailCore; children: ReactNode }) {\n return (\n <div\n onKeyDownCapture={core.keyboard.rootProps.onKeyDownCapture}\n className=\"flex shrink-0 items-center\"\n data-detail-keyboard-slot=\"actions\"\n >\n {children}\n </div>\n );\n}\n\n/**\n * The keyboard-owning root. Focus lands here on open (and on every record\n * change) so Escape / [ / ] answer immediately, the way a peek does in Linear.\n */\nfunction KeyboardRoot({ core, children }: { core: DetailCore; children: ReactNode }) {\n const rootRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n // Do not steal focus from a field the person is already typing in.\n const active = document.activeElement;\n if (active instanceof HTMLElement && active !== document.body && root.contains(active)) return;\n root.focus({ preventScroll: true });\n }, [core.ref.id, core.ref.type]);\n return (\n <div\n ref={rootRef}\n {...core.keyboard.rootProps}\n className=\"flex min-h-full flex-1 flex-col outline-none\"\n data-detail-root\n data-detail-type={core.ref.type}\n data-detail-presentation={core.presentation}\n >\n {children}\n </div>\n );\n}\n\nexport interface DetailPresentationProps {\n data: DetailInstanceData;\n onClose: () => void;\n}\n\nexport function DetailWindowPresentation({ data, onClose }: DetailPresentationProps) {\n const host = useDetailHost();\n const core = useDetailCore(data, \"window\", { onClose });\n const Shell = requireShell(host.shells, \"Window\");\n return (\n <Shell\n instanceKey={detailInstanceKey(core.ref)}\n target={core.ref}\n list={core.list.context}\n title={core.title}\n titleNode={\n <KeyboardSlot core={core}>\n <DetailTitle core={core} />\n </KeyboardSlot>\n }\n actions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} />\n </KeyboardActionsSlot>\n }\n onClose={core.close}\n >\n <KeyboardRoot core={core}>\n <DetailBody core={core} />\n </KeyboardRoot>\n </Shell>\n );\n}\n\nexport function DetailDockedPresentation({ data, onClose }: DetailPresentationProps) {\n const host = useDetailHost();\n const core = useDetailCore(data, \"docked\", { onClose });\n const Shell = requireShell(host.shells, \"Docked\");\n return (\n <Shell\n instanceKey={detailInstanceKey(core.ref)}\n target={core.ref}\n title={core.title}\n titleNode={\n <KeyboardSlot core={core}>\n <DetailTitle core={core} />\n </KeyboardSlot>\n }\n actions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} />\n </KeyboardActionsSlot>\n }\n onClose={core.close}\n >\n <KeyboardRoot core={core}>\n <DetailBody core={core} />\n </KeyboardRoot>\n </Shell>\n );\n}\n\n/**\n * 🚨 D1 — THE PAGE OWNS ITS OWN EXIT. It accepts no `onBack` FROM ITS CALLER: the\n * route passes none, and this component is what fills the shell's `onBack` slot,\n * with `core.leave` — so the back chevron, Escape and a presentation switch all\n * leave through the one guarded exit (`canGoBack` → `back()`, otherwise the\n * record's own home). A route that passed its own `router.back()` in here is\n * exactly how a pasted or bookmarked detail link still landed the tab on\n * `about:blank` after round 1's fix (VERIFY-U-P1-R2, D1). N9 (VERIFY-U-P1-R5):\n * this sentence used to read \"It takes no `onBack`\" directly above the line that\n * passes one, which is the sentence a future agent trusts.\n */\nexport function DetailPagePresentation({ data }: { data: DetailInstanceData }) {\n const host = useDetailHost();\n const core = useDetailCore(data, \"page\", {});\n const Shell = requireShell(host.shells, \"Page\");\n return (\n <Shell\n target={core.ref}\n title={core.title}\n titleNode={\n <KeyboardSlot core={core}>\n <DetailTitle core={core} />\n </KeyboardSlot>\n }\n actions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} />\n </KeyboardActionsSlot>\n }\n onClose={core.close}\n onBack={core.leave}\n >\n <KeyboardRoot core={core}>\n <DetailBody core={core} />\n </KeyboardRoot>\n </Shell>\n );\n}\n","// @ai-matrx/detail — src/listContext.ts\n//\n// 🚨 NEW-7 / NEW-12 — THE LIST A RECORD WAS OPENED FROM IS BOUNDED BEFORE IT\n// RIDES ANY URL, BY RECORDS *AND* BY BYTES, IN ONE PLACE.\n//\n// The page presentation carries its list context in the query string\n// (`?l=type.id,…&i=<n>`) and a detail WINDOW carries the same list in its\n// `?panels=detail:…` token, so the arrows and `[` / `]` keep working after a\n// record is opened as a page and after that window is refreshed (NEW-15).\n// Uncapped, a 500-row list produced a >20 KB href — past every practical\n// request-line limit (VERIFY-U-P1-R2, break attempt 5).\n//\n// The record cap is a KNOB, not a constant: every ceiling on this platform is a\n// row an admin can change (`ui.detail.list_context_max_ids`, organization-\n// overridable). But a record count is not a length: at 2000 — the value the\n// knob's own `max_value` used to permit — the query was 84 KB, and even at 200 a\n// 30-character type token gave 13.6 KB (VERIFY-U-P1-R3, NEW-12). So the trim\n// obeys BOTH: the knob's record cap, and `DETAIL_URL_BUDGET_BYTES` measured on\n// the FINAL SERIALIZED URL. Whichever bites first, the result carries the WINDOW\n// AROUND THE CURRENT RECORD — the far ends of a long list are not what the arrows\n// are for — and says the list was trimmed (`trimmedFrom`) rather than presenting\n// the window as the whole thing.\n//\n// 🚨 NEW-19 (VERIFY-U-P1-R4) — AND THE BUDGET IS THE WHOLE URL'S, AFTER THE LAST\n// ESCAPING. It used to be a 6,000-character allowance for the list VALUE alone,\n// measured before `URLSearchParams` re-escaped it: a window deep link that\n// measured 5,992 arrived in the address bar at 7,416, and a detail PAGE that also\n// carried an open window was 13,433 characters — past the 8 KB request line, so\n// the edge answers 414 and the link is dead. A caller now passes what the rest of\n// the address already costs (`reservedBytes`) and a `measure` that returns the\n// FINAL serialized length of the list as that URL will carry it.\n//\n// Nothing here talks to the settings ladder: the host resolves the knob and\n// passes the number in, so the module stays package-shaped.\n\nimport {\n DETAIL_URL_BUDGET_BYTES,\n type DetailListContext,\n type DetailRef,\n} from \"./types\";\n\n/**\n * 🚨 NEW-25 (VERIFY-U-P1-R4) / N4 (VERIFY-U-P1-R5) — THE SEPARATORS ARE ESCAPED\n * WITHOUT A PERCENT, SO NO LATER URL DECODE CAN PUT ONE BACK.\n *\n * `encodeURIComponent` leaves `.` alone and every spelling of a record in a URL\n * splits on the FIRST dot, so `{type: \"gr.ant\", id: \"x.y\"}` came back as\n * `{type: \"gr\", id: \"ant.x.y\"}` — a DIFFERENT record, silently. Round 4 escaped\n * `.` `:` `,` to `%2E` `%3A` `%2C` here, which fixed the `?panels=` token (three\n * escaping layers, three decodes) and did NOT fix the page presentation's `?l=`\n * query: an ordinary query parse — `new URL(...).searchParams`, which is the\n * algorithm the page route's `searchParams` come from — percent-decodes the\n * value BEFORE `decodeListItems` splits it, so `%2E` was a literal `.` again and\n * the dotted token still retargeted the record. An id containing a comma was\n * truncated the same way (VERIFY-U-P1-R5, N4).\n *\n * So the escape carries no `%` at all. A ref part is percent-encoding spelled\n * with `~` as the escape character, over the alphabet `A-Za-z0-9-_~`: it holds\n * no `.`, `:`, `,` or `%`, which makes it invariant under any number of\n * percent-decodes and the split unambiguous on every layer. `~` itself is\n * escaped first (`%7E` → `~7E`), so the grammar is closed.\n */\nconst PERCENT_ESCAPE_TOO = /[.!~*'()]/g;\n\n/** One half of a `type.id` pair, carrying no character the decoders split on. */\nexport function encodeRefPart(value: string): string {\n return encodeURIComponent(value)\n .replace(PERCENT_ESCAPE_TOO, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)\n .replace(/%/g, \"~\");\n}\n\n/** Inverse of `encodeRefPart`; a hand-edited value comes back as its own text. */\nexport function decodeRefPart(value: string): string {\n try {\n return decodeURIComponent(value.replace(/~/g, \"%\"));\n } catch {\n return value;\n }\n}\n\n/** `type.id,type.id` — the ONE spelling of a list in a URL, page or panel token. */\nexport function encodeListItems(items: readonly DetailRef[]): string {\n return items.map((r) => `${encodeRefPart(r.type)}.${encodeRefPart(r.id)}`).join(\",\");\n}\n\n/** Inverse of `encodeListItems`; entries that do not name a record are dropped. */\nexport function decodeListItems(value: string | null | undefined): DetailRef[] {\n if (!value) return [];\n const items: DetailRef[] = [];\n for (const key of value.split(\",\")) {\n const dot = key.indexOf(\".\");\n if (dot <= 0 || dot === key.length - 1) continue;\n items.push({\n type: decodeRefPart(key.slice(0, dot)),\n id: decodeRefPart(key.slice(dot + 1)),\n });\n }\n return items;\n}\n\n/** How many characters these records cost in a URL — what the budget measures. */\nexport function listQueryBytes(items: readonly DetailRef[]): number {\n return encodeListItems(items).length;\n}\n\nexport interface TrimListContextOptions {\n /**\n * The character budget for the whole URL. Defaults to the platform's request\n * line (`DETAIL_URL_BUDGET_BYTES`).\n */\n budgetBytes?: number;\n /**\n * What the rest of the FINAL address already costs — the path, the query it is\n * being merged into, the token's own non-list args. The list gets what is\n * left, never a private allowance of its own (NEW-19).\n */\n reservedBytes?: number;\n /**\n * How many characters these records cost IN THE FINAL URL. The `?panels=`\n * token escapes every separator and `URLSearchParams` escapes the result\n * again, so the same records cost about 1.6× there what they cost in the page\n * query (NEW-15, NEW-19).\n */\n measure?: (items: readonly DetailRef[]) => number;\n}\n\n/**\n * The list as it may travel, plus what was left behind. Returns the same list\n * when it already fits both limits, and `null` for no list at all.\n */\nexport function trimListContext(\n list: DetailListContext | null | undefined,\n max: number,\n options: TrimListContextOptions = {},\n): DetailListContext | null {\n if (!list || list.items.length === 0) return null;\n const budget =\n (options.budgetBytes ?? DETAIL_URL_BUDGET_BYTES) - (options.reservedBytes ?? 0);\n const measure = options.measure ?? listQueryBytes;\n const total = list.items.length;\n const index = list.index >= 0 && list.index < total ? list.index : 0;\n const idCap = Number.isFinite(max) && max >= 1 ? Math.floor(max) : 1;\n\n /** The widest window of `size` records that still contains the current one. */\n const windowOf = (size: number): { items: DetailRef[]; index: number } => {\n const half = Math.floor(size / 2);\n const start = Math.min(Math.max(index - half, 0), Math.max(total - size, 0));\n return { items: list.items.slice(start, start + size), index: index - start };\n };\n\n let size = Math.min(idCap, total);\n // Shrink until the encoded value fits the byte budget. One record always\n // travels: the record being shown is the list's own current position, and a\n // single entry is ~41 characters against a 6000-character budget.\n while (size > 1 && measure(windowOf(size).items) > budget) {\n const fitted = Math.max(\n 1,\n Math.floor((size * budget) / Math.max(measure(windowOf(size).items), 1)),\n );\n size = fitted < size ? fitted : size - 1;\n }\n const cut = windowOf(size);\n const trimmedFrom = size < total ? total : list.trimmedFrom;\n return {\n items: cut.items,\n index: cut.index,\n ...(trimmedFrom && trimmedFrom > cut.items.length ? { trimmedFrom } : {}),\n };\n}\n","// @ai-matrx/detail — src/presentation.ts\n//\n// Pure helpers: the deep-link spelling of every presentation, and the in-URL\n// instance key. Deep links ride the host's ONE panel URL mechanism (`?panels=`\n// in matrx-frontend), under the type key `detail`, so every client that\n// already opens panels from a URL reaches every presentation with no new code:\n//\n// ?panels=detail:<type>.<id>:as-window\n// ?panels=detail:<type>.<id>:as-docked\n//\n// `page` is a route (the host's `navigate.pageHref`), never a `?panels=` token\n// — it is the one presentation that changes the URL. Its list context rides\n// the query (`?l=type.id,…&i=<n>&lt=<total>`); that spelling lives HERE too,\n// beside the token's, so both are cut by the ONE trim and the ONE budget.\n\nimport {\n decodeListItems,\n decodeRefPart,\n encodeListItems,\n encodeRefPart,\n trimListContext,\n} from \"./listContext\";\nimport {\n DEFAULT_DETAIL_LIST_CONTEXT_MAX,\n DETAIL_URL_BUDGET_BYTES,\n type DetailListContext,\n type DetailPresentation,\n type DetailRef,\n} from \"./types\";\n\nexport const DETAIL_URL_TYPE_KEY = \"detail\";\nexport const DETAIL_URL_AS_ARG = \"as\";\n/** The list the window was opened from, so a REFRESH keeps it (NEW-15). */\nexport const DETAIL_URL_LIST_ARG = \"l\";\nexport const DETAIL_URL_INDEX_ARG = \"i\";\nexport const DETAIL_URL_LIST_TOTAL_ARG = \"lt\";\n\n/**\n * `type.id` — the panel URL splits tokens on `:` and args on `-`/`_`, never on\n * `.`, so the two halves carry no `.` `:` or `,` of their own.\n *\n * 🚨 NEW-25 (VERIFY-U-P1-R4) — the halves used to be raw, so a type token or an\n * id containing a dot came back as a DIFFERENT record: `{type: \"gr.ant\", id:\n * \"x.y\"}` parsed as `{type: \"gr\", id: \"ant.x.y\"}`. One encoder, one decoder\n * (`encodeRefPart` / `decodeRefPart`), the same pair the list spelling uses.\n */\nexport function detailInstanceKey(ref: DetailRef): string {\n return `${encodeRefPart(ref.type)}.${encodeRefPart(ref.id)}`;\n}\n\n/** Inverse of `detailInstanceKey`; `null` when the key is not `type.id`. */\nexport function parseDetailInstanceKey(key: string | null | undefined): DetailRef | null {\n if (!key) return null;\n const dot = key.indexOf(\".\");\n if (dot <= 0 || dot === key.length - 1) return null;\n return { type: decodeRefPart(key.slice(0, dot)), id: decodeRefPart(key.slice(dot + 1)) };\n}\n\n/** Which in-place presentation a URL arg names; `window` when unspecified. */\nexport function presentationFromUrlArg(\n value: string | undefined,\n): Exclude<DetailPresentation, \"page\"> {\n return value === \"docked\" ? \"docked\" : \"window\";\n}\n\n// ─── The list context inside a `?panels=` token (NEW-15) ────────────────────\n//\n// 🚨 NEW-15 (VERIFY-U-P1-R3) — A REFRESHED DETAIL WINDOW KEEPS ITS LIST. The\n// window's token used to carry `as` and nothing else, and the hydrator passed\n// `list: null`, so a reload silently lost the previous / next controls and the\n// \"3/40\" counter. The page URL already carried the list; the token carries the\n// same three values, under the same cap and the same byte budget.\n//\n// THE TOKEN'S GRAMMAR IS THE CONSTRAINT. `?panels=` splits panels on `,`, a\n// panel's args off on `:`, arg pairs on `_` and each pair on its first `-`. A\n// uuid is full of hyphens and a list is full of commas, so a list value is\n// escaped into a form holding none of those four characters: `encodeURIComponent`\n// plus the unreserved characters it leaves alone. The result is percent-escaped\n// again by `URLSearchParams` on the way into the address bar and decoded once on\n// the way back out, which is exactly how the page query's comma already travels.\n\nconst EXTRA_ESCAPES = /[-_.!~*'()]/g;\n\n/** A value safe to carry as a `?panels=` arg — no `,` `:` `_` or `-`. */\nexport function encodePanelArgValue(value: string): string {\n return encodeURIComponent(value).replace(\n EXTRA_ESCAPES,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\n/** Inverse of `encodePanelArgValue`. */\nexport function decodePanelArgValue(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n // A hand-edited link. Better the raw text than a thrown hydrator.\n return value;\n }\n}\n\n/**\n * 🚨 NEW-19 (VERIFY-U-P1-R4) — MEASURE THE VALUE AS THE ADDRESS BAR CARRIES IT.\n * The token's own escaping is only the first layer: the host's panel-URL writer\n * serializes the whole `?panels=` value through `new URLSearchParams(...).toString()`,\n * which escapes every `%` again (a uuid's `-` goes `-` → `%2D` → `%252D`, three\n * characters becoming five). Measuring the token's spelling said 5,992 for a URL\n * the browser carried at 7,416.\n */\nexport function panelListValueBytes(items: readonly DetailRef[]): number {\n return encodeURIComponent(encodePanelArgValue(encodeListItems(items))).length;\n}\n\n/**\n * The length of the FINAL address a detail window's deep link produces: the path\n * and query it is merged into, plus the `panels` parameter as\n * `URLSearchParams` serializes it. What the budget is measured against, and what\n * a guard can assert.\n */\nexport function finalPanelUrlLength(\n pathAndQuery: string,\n tokenWithoutListArgs: string,\n listArgs: Record<string, string>,\n): number {\n const args = Object.entries(listArgs)\n .map(([key, value]) => `${key}-${value}`)\n .join(\"_\");\n const token = args ? `${tokenWithoutListArgs}_${args}` : tokenWithoutListArgs;\n const [path = \"\", existingQuery = \"\"] = pathAndQuery.split(\"?\");\n const params = new URLSearchParams(existingQuery);\n params.set(\"panels\", token);\n return `${path}?${params.toString()}`.length;\n}\n\n/**\n * What the address a panel token is written INTO really costs, after\n * `URLSearchParams` re-serializes it — a detail page's own `?l=` list carries raw\n * commas that become `%2C` the moment a `panels` parameter is merged beside it,\n * so the page's query grows by ~2× its comma count at the instant the window\n * opens. Reserving the pre-serialization length is how a page + window address\n * still overran the budget by 70 characters in the guard that found this.\n */\nexport function panelUrlReserveBytes(pathAndQuery: string): number {\n const [path = \"\", query = \"\"] = pathAndQuery.split(\"?\");\n const serialized = new URLSearchParams(query).toString();\n return path.length + (serialized ? serialized.length + 1 : 0);\n}\n\n/**\n * What the token costs beside the list: `panels=detail:<type>.<id>:as-window`\n * plus the `_i-<n>_lt-<n>` args and their escaping. Measured generously so the\n * budget is never overspent by the fixed part.\n */\nconst PANEL_TOKEN_FIXED_BYTES = 160;\n\n/**\n * The reserve for a caller that cannot see the address it writes into. A detail\n * PAGE carrying its own capped list is the longest address a window is opened\n * from, so the reserve is that: the budget's own share for a page query. Without\n * it, a window opened from a detail page produced a 13,433-character URL the edge\n * refuses (NEW-19).\n */\nconst DEFAULT_PANEL_URL_RESERVE_BYTES = Math.floor(DETAIL_URL_BUDGET_BYTES / 2);\n\n/**\n * The window's deep-link args for a list context — `{}` when there is no list.\n * `max` is the resolved `ui.detail.list_context_max_ids`; the budget is the\n * platform's REQUEST LINE, measured on the final serialized value, with whatever\n * the rest of the address already costs reserved (NEW-19). A caller that knows\n * the address it is writing into passes its length; one that does not gets the\n * conservative reserve above, which is what a long detail-page query costs.\n */\nexport function detailListToUrlArgs(\n list: DetailListContext | null | undefined,\n max: number,\n options: { reservedBytes?: number } = {},\n): Record<string, string> {\n const reserved =\n options.reservedBytes ?? DEFAULT_PANEL_URL_RESERVE_BYTES;\n const capped = trimListContext(list, max, {\n measure: panelListValueBytes,\n reservedBytes: reserved + PANEL_TOKEN_FIXED_BYTES,\n });\n if (!capped) return {};\n const args: Record<string, string> = {\n [DETAIL_URL_LIST_ARG]: encodePanelArgValue(encodeListItems(capped.items)),\n [DETAIL_URL_INDEX_ARG]: String(capped.index),\n };\n if (capped.trimmedFrom) args[DETAIL_URL_LIST_TOTAL_ARG] = String(capped.trimmedFrom);\n return args;\n}\n\n/** The list a `?panels=detail:` token carries; `null` when it carries none. */\nexport function detailListFromUrlArgs(\n args: Record<string, string> | null | undefined,\n): DetailListContext | null {\n const raw = args?.[DETAIL_URL_LIST_ARG];\n if (!raw) return null;\n const items = decodeListItems(decodePanelArgValue(raw));\n if (items.length === 0) return null;\n const index = Number.parseInt(args?.[DETAIL_URL_INDEX_ARG] ?? \"\", 10);\n const total = Number.parseInt(args?.[DETAIL_URL_LIST_TOTAL_ARG] ?? \"\", 10);\n return {\n items,\n index: Number.isFinite(index) && index >= 0 && index < items.length ? index : 0,\n ...(Number.isFinite(total) && total > items.length ? { trimmedFrom: total } : {}),\n };\n}\n\n// ─── The list context in the PAGE presentation's query ──────────────────────\n//\n// `/detail/<type>/<id>?l=type.id,type.id&i=<index>&lt=<total>` — the same\n// `type.id` instance key the `?panels=detail:` deep link uses. `lt` is present\n// only when the list was TRIMMED to fit the URL (NEW-7): it is the length of the\n// list the window was cut from, so the detail can say so. The host owns the\n// PATH (`navigate.pageHref`) and appends what `encodeListQuery` returns.\n\n/** `?l=`, `&i=`, `&lt=` and their values — what the query costs beside the list. */\nconst PAGE_QUERY_FIXED_BYTES = 40;\n\n/**\n * 🚨 NEW-7 — CAPPED, ALWAYS. `max` is the resolved\n * `ui.detail.list_context_max_ids` knob; the default is used when the host has\n * no answer yet, never \"no cap\". A 500-row list uncapped produced a >20 KB href\n * no server accepts (VERIFY-U-P1-R2).\n *\n * 🚨 NEW-19 (VERIFY-U-P1-R4) — AND THE BUDGET IS THE WHOLE URL'S. The path this\n * query hangs off is part of the request line, so the caller passes what it\n * costs (`reservedBytes`) and the list gets what is left. Measuring the list\n * alone is how a detail page reached 8,464 characters while the code claimed a\n * 2 KB margin.\n */\nexport function encodeListQuery(\n list: DetailListContext | null | undefined,\n max: number = DEFAULT_DETAIL_LIST_CONTEXT_MAX,\n options: { reservedBytes?: number } = {},\n): string {\n const capped = trimListContext(list, max, {\n reservedBytes: (options.reservedBytes ?? 0) + PAGE_QUERY_FIXED_BYTES,\n });\n if (!capped) return \"\";\n // Each `type.id` is encoded, the separators are not: a comma is legal in a\n // query value, and `%2C` × 200 was 400 bytes of nothing.\n const l = encodeListItems(capped.items);\n const trimmed = capped.trimmedFrom ? `&lt=${capped.trimmedFrom}` : \"\";\n return `?l=${l}&i=${capped.index}${trimmed}`;\n}\n\n/** Inverse of `encodeListQuery`, from the three query values. */\nexport function decodeListQuery(\n l: string | null | undefined,\n i: string | null | undefined,\n /** `lt` — the length of the list this window was cut from, when it was. */\n lt?: string | null | undefined,\n): DetailListContext | null {\n if (!l) return null;\n const items = decodeListItems(l);\n if (items.length === 0) return null;\n const index = Number.parseInt(i ?? \"\", 10);\n const total = Number.parseInt(lt ?? \"\", 10);\n return {\n items,\n index: Number.isFinite(index) && index >= 0 && index < items.length ? index : 0,\n ...(Number.isFinite(total) && total > items.length ? { trimmedFrom: total } : {}),\n };\n}\n","// @ai-matrx/detail — src/react/headerGeometry.ts\n//\n// 🚨 NEW-8 (VERIFY-U-P1-R3) — THE ONE PLACE THE DETAIL HEADER'S GEOMETRY IS\n// NAMED, so a guard can hold the host and the primitive to the same words.\n//\n// D4's fix put the window title in flow between the two action zones, which\n// made overlap impossible — and traded it for a squeeze: at the Detail window's\n// own `minWidth={360}` the traffic-light spacer is 96px and the cluster\n// (previous / counter / next / two presentation icons / copy / pop-out) is about\n// 200px, leaving the record's NAME roughly 60px — four characters and an\n// ellipsis. D2's law (\"the name is the one thing the header always shows\")\n// therefore failed in the window at small sizes, where round 1 measured the page.\n//\n// The rule, and the order it holds in: the name has a FLOOR, and the action\n// cluster collapses into an overflow menu BEFORE the name gives way. The\n// breakpoint is a CONTAINER query on the window header, never a `sm:` viewport\n// breakpoint — a window's width is not the screen's, which is the mistake that\n// made D4 invisible on a 1440px screen. In the page and docked presentations no\n// such container exists above the header, the max-width query never matches, and\n// the full cluster shows: those shells are already wide or already wrap.\n//\n// 🚨 NO SCREEN HAS BEEN SEEN for any of this. The numbers above are arithmetic\n// over the declared widths and the classes, not pixels: this container cannot\n// sign in to any Matrx host. The structural guard is\n// the host's rendered-header guard (matrx-frontend `features/window-panels/detail/__tests__/the-title-never-runs-under-the-actions.test.tsx`).\n//\n// THE LITERALS ARE WRITTEN OUT IN THE JSX AS WELL, because Tailwind generates a\n// utility only for a class it can SEE in source — a class assembled from these\n// constants at run time would never be compiled. The guard asserts the rendered\n// className against these constants, so the two cannot drift apart silently.\n\n/** The container the detail header's breakpoint reads — declared by the window chrome. */\nexport const DETAIL_HEADER_CONTAINER = \"@container/window-header\";\n\n/** Below this container width the optional actions move into the overflow menu. */\nexport const DETAIL_HEADER_COMPACT_BELOW = \"@max-[26rem]/window-header\";\n\n/** The floor under the record's name: ~96px, enough for a readable fragment. */\nexport const DETAIL_TITLE_MIN_WIDTH_CLASS = \"min-w-[6rem]\";\n"],"mappings":"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","names":["import_react","import_react","producer","reconnect","import_react","import_react","load","import_react","import_react","import_jsx_runtime","import_jsx_runtime","import_react","import_design_system","import_react","import_design_system","import_jsx_runtime","effective","import_jsx_runtime","import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/react/index.ts","../src/react/host.tsx","../src/react/useDetailCore.ts","../src/react/useDetailHealth.ts","../src/react/useDetailKeyboard.ts","../src/react/useDetailRecord.ts","../src/react/useOpenDetail.ts","../src/types.ts","../src/react/DetailHeader.tsx","../src/react/icons.tsx","../src/react/DetailBody.tsx","../src/format.ts","../src/react/DetailPresentationPane.tsx","../src/react/presentations.tsx","../src/listContext.ts","../src/presentation.ts","../src/react/headerGeometry.ts"],"sourcesContent":["// @ai-matrx/detail/react — the React product.\n//\n// The ports and provider, the core, the keyboard model, the loader and the\n// health resolver, the one opener, the header halves, the body, the setting\n// pane, and the three presentations that hand the same core to a host's\n// window / docked / page shell.\n\nexport {\n DetailHostProvider,\n requireResolveType,\n requireShell,\n useDetailHost,\n} from \"./host\";\nexport type {\n DetailDockedShellProps,\n DetailEffectivePresentation,\n DetailHostPorts,\n DetailPageShellProps,\n DetailPresentationSetting,\n DetailShellSlots,\n DetailMenuAction,\n DetailShells,\n DetailWindowShellProps,\n} from \"./host\";\n\nexport { useDetailCore } from \"./useDetailCore\";\nexport type { DetailCore } from \"./useDetailCore\";\n\nexport { useDetailKeyboard } from \"./useDetailKeyboard\";\nexport type { DetailKeyboard, DetailKeyboardHandlers } from \"./useDetailKeyboard\";\n\nexport { useDetailRecord } from \"./useDetailRecord\";\n\nexport { reconnectFor, useDetailHealth } from \"./useDetailHealth\";\nexport type { UseDetailHealthArgs } from \"./useDetailHealth\";\n\nexport { useOpenDetail } from \"./useOpenDetail\";\nexport type { OpenDetail } from \"./useOpenDetail\";\n\nexport { DetailActions, DetailRecordMeta, DetailTitle, useDetailMenuActions } from \"./DetailHeader\";\nexport { DetailBody } from \"./DetailBody\";\nexport { DetailPresentationPane } from \"./DetailPresentationPane\";\n\nexport {\n DetailDockedPresentation,\n DetailPagePresentation,\n DetailWindowPresentation,\n} from \"./presentations\";\nexport type { DetailPresentationProps } from \"./presentations\";\n\nexport {\n DETAIL_HEADER_COMPACT_BELOW,\n DETAIL_HEADER_CONTAINER,\n DETAIL_TITLE_MIN_WIDTH_CLASS,\n} from \"./headerGeometry\";\n","// @ai-matrx/detail — src/react/host.tsx\n//\n// THE SEAM. Everything this package needs from the app that hosts it arrives\n// here as ports — the same shape `@ai-matrx/associations` uses. The core and\n// the three presentations import nothing from any host; they ask the host for\n// the record-type map, the presentation setting, the shells (window / docked /\n// page), the doors, history, navigation and notifications.\n//\n// Providers NEST AND MERGE: a light provider at boot binds everything but the\n// shells (so `useOpenDetail` works anywhere), and each presentation's lazily\n// loaded entry binds its own shell right above the presentation — the window\n// shell parses the host's window manager and must never sit in a boot bundle.\n// A port that is missing when it is needed throws, naming itself.\n\n\"use client\";\n\nimport { createContext, useContext, type Context, type ComponentType, type ReactNode } from \"react\";\n\nimport type {\n DetailHistoryEntry,\n DetailInstanceData,\n DetailListContext,\n DetailPresentation,\n DetailRecordType,\n DetailRef,\n DetailRow,\n DetailSeed,\n} from \"../types\";\n\n/** Props every shell receives; the package fills the slots, the host owns the chrome. */\n/** One of the record's optional controls as data (a shell's menu draws it). */\nexport interface DetailMenuAction {\n readonly id: string;\n readonly label: string;\n readonly onSelect: () => void;\n}\n\nexport interface DetailShellSlots {\n /** Accessible plain-text title (window chrome, drawer title, document title). */\n title: string;\n /** Rich title: icon · name · type chip · the record's doors. */\n titleNode: ReactNode;\n /** Presentation switcher · previous / next · copy id. Compact. */\n actions: ReactNode;\n /**\n * Previous / next only — for a shell whose chrome has its OWN menu: it puts\n * `menuActions` there instead of a second overflow button (\"…\" beside \"⋮\").\n */\n navActions: ReactNode;\n /** The optional controls (open as window / docked / page, copy id) as data, for a shell's own menu. */\n menuActions: readonly DetailMenuAction[];\n children: ReactNode;\n onClose: () => void;\n}\n\nexport interface DetailWindowShellProps extends DetailShellSlots {\n /** `type.id` — the deep-link instance and the window manager's key. */\n instanceKey: string;\n target: DetailRef;\n /**\n * 🚨 NEW-15 — THE LIST TRAVELS INTO THE DEEP LINK. The window's `?panels=`\n * token used to carry the record and the presentation only, so a REFRESH lost\n * the previous / next controls and the counter without a word. The shell puts\n * this into the token the way the page presentation puts it into the query.\n */\n list: DetailListContext | null;\n}\n\nexport interface DetailDockedShellProps extends DetailShellSlots {\n instanceKey: string;\n target: DetailRef;\n}\n\nexport interface DetailPageShellProps extends DetailShellSlots {\n target: DetailRef;\n /** Back affordance — the page is the one presentation that left the URL. */\n onBack: () => void;\n}\n\nexport interface DetailEffectivePresentation {\n /** The effective presentation for this type, `undefined` when unresolved. */\n value: DetailPresentation | undefined;\n /** The per-record-type exception still in effect, from ANY rung. */\n forType: DetailPresentation | undefined;\n}\n\nexport interface DetailPresentationSetting {\n /** `undefined` while unresolved; `error` names the failure — never both silent. */\n value: DetailPresentation | undefined;\n error: string | null;\n /**\n * The PER-RECORD-TYPE exception in effect for this type, when there is one\n * (`undefined` = this type opens the way the default says). The pane offers\n * \"use the default for this type\" only then — an offer to remove something\n * that does not exist is a control that does nothing (NEW-2).\n */\n forType?: DetailPresentation | undefined;\n}\n\nexport interface DetailShells {\n Window?: ComponentType<DetailWindowShellProps>;\n Docked?: ComponentType<DetailDockedShellProps>;\n Page?: ComponentType<DetailPageShellProps>;\n}\n\nexport interface DetailHostPorts {\n /**\n * The ONE record-type map. `null` = the host has no registration for the\n * type. Bound by each presentation's entry (with its shell), not at boot —\n * the map pulls the registry and the frame into whatever imports it.\n */\n resolveType?: (type: string) => DetailRecordType | null;\n /** React face — the effective `ui.detail.default_presentation` for a type. A hook. */\n usePresentationSetting: (type: string) => DetailPresentationSetting;\n /** Awaited face for click handlers. Rejects with the resolver's error. */\n resolvePresentation: (type: string) => Promise<DetailPresentation>;\n /** Warm the cache so the click path is synchronous. */\n warmPresentation: (type: string) => void;\n /**\n * 🚨 NEW-10 (VERIFY-U-P1-R3) — RE-READ THE LADDER AFTER A WRITE, AND SAY WHAT\n * ANSWERS NOW. Removing your own per-type exception while your ORGANIZATION\n * holds one for the same type succeeded and the pane promised the records\n * \"now open the way you normally open records\" — they did not; the\n * organization's entry still won on the next open and nothing re-read the\n * ladder. A host binds this to a FRESH ladder read (cache invalidated), never\n * to the cached value the write just made stale. Optional: a host that cannot\n * re-read says so and the pane promises nothing.\n */\n reReadPresentation?: (type: string) => Promise<DetailEffectivePresentation>;\n /**\n * Write the person's presentation setting from inside a detail (the ONE\n * screen that writes `ui.detail.default_presentation`). `forType` non-null\n * writes the PER-RECORD-TYPE override instead of the default.\n *\n * `clear` with a `forType` REMOVES that record type's exception instead of\n * setting one, so an exception set from a record can be taken back from the\n * same place (NEW-2). It is the same map-entry write with the key absent — a\n * host never grows a second writer for it.\n *\n * Optional: a host that can only READ the setting binds nothing and the pane\n * is absent — never a disabled-looking control. A refusal comes back as\n * `{ ok: false, reason }` (the settings ladder's reasons are sentences and\n * the surface renders them), never as a thrown error.\n */\n savePresentation?: (args: {\n presentation: DetailPresentation;\n forType: string | null;\n clear?: boolean;\n }) => Promise<{ ok: true } | { ok: false; reason: string }>;\n /** Open an in-place presentation (window / docked). Page goes through `navigate`. */\n open: (args: {\n presentation: Exclude<DetailPresentation, \"page\">;\n data: DetailInstanceData;\n }) => void;\n close: (presentation: Exclude<DetailPresentation, \"page\">) => void;\n navigate: {\n pageHref: (ref: DetailRef, extra?: { list?: DetailListContext | null }) => string;\n toPage: (\n ref: DetailRef,\n extra?: {\n seed?: DetailSeed | null;\n list?: DetailListContext | null;\n /**\n * 🚨 NEW-14 (VERIFY-U-P1-R3) — MOVING INSIDE A LIST REPLACES, NEVER\n * PUSHES. Arrowing to a neighbour on the page pushed a history entry,\n * so twenty records meant twenty Backs and the chevron labelled \"Back\"\n * returned to the previous RECORD instead of the list. The record whose\n * history entry is being replaced, so the host can carry its \"this tab\n * pushed a detail page\" answer (`canGoBack`) forward to the record now\n * showing. Absent ⇒ a push, which is what opening a page IS.\n */\n replacing?: DetailRef | null;\n },\n ) => void;\n back: () => void;\n /**\n * 🚨 D1. Whether THIS tab reached the detail page from inside the app, so\n * `back()` lands on a screen the person was actually on. A deep link\n * opened in a fresh tab has NOTHING behind it: `back()` there leaves the\n * tab on `about:blank` with the whole app gone (measured 2026-09-17).\n * A host answers `false` whenever it is not certain.\n */\n canGoBack: (ref: DetailRef) => boolean;\n /**\n * Where the page goes when there is nothing behind it: the record's own\n * home (its canonical route), or the host's home when the type has none.\n * REPLACES the detail page — it is being left, not stacked on.\n */\n toRecordHome: (ref: DetailRef, entityToken: string | null) => void;\n };\n shells: DetailShells;\n doors: {\n /** The record's own doors (open / new tab / peek) — sibling of the title. */\n RecordDoors: ComponentType<{ token: string; id: string; name?: string | null }>;\n /** A reference inside the record — never a bare uuid. */\n /**\n * One id rendered as a door. `name` is the record's own name when the caller\n * has it — a door showing a truncated uuid is half a dead end (chair,\n * 2026-09-18), so a host that can name the record renders the name and keeps\n * the id for the copy control.\n */\n RefCell: ComponentType<{\n value: string;\n label: string;\n token: string;\n name?: string | null;\n }>;\n tokenFromColumnName: (column: string) => string | null;\n isUuidValue: (value: unknown) => value is string;\n /**\n * 🚨 NEW-17 (VERIFY-U-P1-R4) — WHETHER THIS RECORD REALLY OPENS ANYWHERE.\n * `RecordDoors` renders nothing when the token has no route and no peek, so\n * the honest absent state promised \"the controls above still open it where it\n * lives\" beside a header that carried no such control — for `session`, and for\n * every unregistered type. The body's sentence is derived from this answer,\n * never fixed.\n */\n hasDoor: (token: string, id: string) => boolean;\n };\n associations: {\n /** Tokens shown when a registration does not name its own. */\n defaultTokens: readonly string[];\n /** Whether a token may anchor an associations section at all. */\n canAnchor: (token: string) => boolean;\n };\n history: {\n list: (token: string, id: string, signal: AbortSignal) => Promise<DetailHistoryEntry[]>;\n /**\n * 🚨 NEW-23 (VERIFY-U-P1-R5) — WHO MADE THE CHANGE IS A PERSON, NOT AN ID.\n * Every history row printed a bare 36-character uuid under \"Changed by\",\n * because no `actor` or `user` token has a door in a host's registry: an\n * identity the UI names that opens nothing and says nothing (the\n * no-dead-ends class). A host binds its ONE existing identity resolver here\n * — never a second one — and the row shows the person's name. Optional: a\n * host that has no directory binds nothing and the row keeps the id under a\n * title that says what it is, which is honest rather than silent.\n *\n * It is given the record's own row too, because the directory a host may\n * legitimately read is usually the owning organization's members and the\n * row is where that organization is named.\n */\n ActorName?: ComponentType<{ actorId: string; row: DetailRow | null }>;\n };\n /**\n * 🚨 CUSTOM FIELDS ON EVERY RECORD VIEW (lane 7 STANDARD-TABLES W5). The ONE\n * place a window, a docked panel, the `/detail` page and a record peek get the\n * organization's own fields for a standard record: the host binds its section\n * once, and the body renders it after Fields whenever the record has an\n * `entityToken`. The section is given only the token and the id — it reads the\n * row's organization itself (never the active organization) and says in one\n * line why it shows nothing, so it is never silently absent. A peek binds a\n * compact section through a nested provider (providers merge). Optional: a\n * host without custom fields binds nothing and the section is absent.\n */\n customFields?: {\n Section: ComponentType<{ token: string; id: string }>;\n };\n notify: {\n error: (message: string) => void;\n success: (message: string) => void;\n };\n copyText: (text: string) => Promise<boolean>;\n /**\n * 🚨 THE SAME RECONNECT THE CONNECTOR ROWS SHOW. A synced record whose grant\n * has expired, been revoked or lost a scope offers exactly the repair the\n * connector surface offers, from the record's own health strip, so a person\n * never has to go and find the connections screen. Optional: a host that\n * cannot open a consent flow binds nothing and the strip states the problem\n * without offering a control that would not help (law 4). A producer that\n * answers `onReconnect: null` overrides this for its record — see\n * `DetailSourceHealth.onReconnect`.\n */\n reconnectSource?: (ref: DetailRef, source: string) => void;\n /**\n * Where the developer goes to fix an unregistered or sourceless type — a\n * phrase for the console remedy the core prints once per type per tab (the\n * PERSON sees a plain sentence; the developer sees this). Optional: the\n * default names the `resolveType` port. A host names its own registry, e.g.\n * \"the item registry (features/item-presentation/registry.tsx)\".\n */\n remedy?: { typeMap: string };\n}\n\n// 🚨 MODULE-LEVEL STATE IS BANNED IN A DUAL-BUNDLE PACKAGE — the context lives\n// on `globalThis` under a `Symbol.for` slot (the kit `confirm/opener` shape), so\n// the ESM and CJS builds, and the /react and /testing entries, all share ONE\n// context. Two module instances with two contexts is exactly how a test seat's\n// provider stops being visible to the components it mounts.\nconst CONTEXT_SLOT = Symbol.for(\"ai-matrx.detail.host-context\");\ntype ContextSlot = { [CONTEXT_SLOT]?: Context<Partial<DetailHostPorts> | null> };\nconst DetailHostContext: Context<Partial<DetailHostPorts> | null> = ((globalThis as ContextSlot)[\n CONTEXT_SLOT\n] ??= createContext<Partial<DetailHostPorts> | null>(null));\n\n/**\n * Binds ports. Nested providers merge over their parent, shells included, so\n * a presentation entry can add only its own shell.\n */\nexport function DetailHostProvider({\n ports,\n children,\n}: {\n ports: Partial<DetailHostPorts>;\n children: ReactNode;\n}) {\n const parent = useContext(DetailHostContext);\n const merged: Partial<DetailHostPorts> = {\n ...(parent ?? {}),\n ...ports,\n shells: { ...(parent?.shells ?? {}), ...(ports.shells ?? {}) },\n };\n return <DetailHostContext.Provider value={merged}>{children}</DetailHostContext.Provider>;\n}\n\nconst REQUIRED_PORTS: (keyof DetailHostPorts)[] = [\n \"usePresentationSetting\",\n \"resolvePresentation\",\n \"warmPresentation\",\n \"open\",\n \"close\",\n \"navigate\",\n \"doors\",\n \"associations\",\n \"history\",\n \"notify\",\n \"copyText\",\n];\n\n/**\n * The bound ports. Throws with the remedy when a port is missing — a detail\n * that opened with no host would otherwise render every section empty and\n * look finished.\n */\nexport function useDetailHost(): DetailHostPorts {\n const ports = useContext(DetailHostContext);\n if (!ports) {\n throw new Error(\n \"[detail] No DetailHostProvider above this component. Mount the host binding — a \" +\n \"<DetailHostProvider ports={…}> carrying every required port (README → 'Binding the \" +\n \"ports') — around the tree that opens or renders record details.\",\n );\n }\n const missing = REQUIRED_PORTS.filter((key) => ports[key] === undefined);\n if (missing.length > 0) {\n throw new Error(\n `[detail] DetailHostProvider is missing the port(s): ${missing.join(\", \")}. ` +\n \"Bind them in the host binding (README → 'Binding the ports').\",\n );\n }\n // The context value itself (the provider always sets `shells`), so the\n // object a consumer depends on keeps its identity between renders.\n return ports as DetailHostPorts;\n}\n\n/** The record-type map, or an error naming which entry forgot to bind it. */\nexport function requireResolveType(\n host: DetailHostPorts,\n): NonNullable<DetailHostPorts[\"resolveType\"]> {\n if (!host.resolveType) {\n throw new Error(\n \"[detail] No resolveType port is bound. A presentation's entry must pass \" +\n \"`resolveType` (the host's ONE record-type map) beside its shell.\",\n );\n }\n return host.resolveType;\n}\n\n/** A presentation's shell, or an error naming which entry forgot to bind it. */\nexport function requireShell<K extends keyof DetailShells>(\n shells: DetailShells,\n which: K,\n): NonNullable<DetailShells[K]> {\n const shell = shells[which];\n if (!shell) {\n throw new Error(\n `[detail] No ${which} shell is bound. The ${which} presentation's entry must wrap itself in ` +\n `<DetailHostProvider ports={{ shells: { ${which} } }}> (README → 'The three shells').`,\n );\n }\n return shell;\n}\n","// @ai-matrx/detail — src/react/useDetailCore.ts\n//\n// THE core, as state: resolve the registration, load the row, derive title /\n// about / fields / health, and the list-navigation + presentation-switch\n// commands every presentation's header uses. The three presentations differ\n// only in which shell they hand this to.\n\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { requireResolveType, useDetailHost } from \"./host\";\nimport { useDetailHealth } from \"./useDetailHealth\";\nimport { useDetailKeyboard, type DetailKeyboard } from \"./useDetailKeyboard\";\nimport { useDetailRecord } from \"./useDetailRecord\";\nimport type {\n DetailField,\n DetailFrameContext,\n DetailInstanceData,\n DetailListContext,\n DetailPresentation,\n DetailRecordType,\n DetailRef,\n DetailRow,\n DetailSourceHealth,\n DetailStatus,\n} from \"../types\";\n\nexport interface DetailCore {\n ref: DetailRef;\n data: DetailInstanceData;\n presentation: DetailPresentation;\n /** `null` when the host has no registration for this type — the body says so. */\n recordType: DetailRecordType | null;\n status: DetailStatus;\n /** Set when `status === \"error\"`. */\n errorMessage: string | null;\n row: DetailRow | null;\n title: string;\n /** The title is a stand-in (loading / not found / failed), not a record's name. */\n titleIsStandIn: boolean;\n /**\n * 🚨 NEW-9 — WHETHER THE RECORD'S OWN DOORS MAY SHOW. A question about the\n * RECORD, not about its name: a record that exists but stores nothing more\n * here still opens in its home, and a title that could not be loaded has no\n * record behind it to open. The header hung the doors off `titleIsStandIn`,\n * which is why a type with no source became a named dead end.\n */\n doorsAvailable: boolean;\n /**\n * 🚨 NEW-17 — WHETHER A CONTROL THAT OPENS THIS RECORD SOMEWHERE ELSE IS\n * ACTUALLY ON THE SCREEN (the token resolves to a route or a peek). The body's\n * absent-state sentence is derived from this; it used to promise a door for\n * every type, including the ones whose header shows none.\n */\n canOpenElsewhere: boolean;\n /**\n * Human label for the record TYPE (\"File\") — for a statement about the type,\n * such as the presentation setting's \"Every file record now opens as a window\".\n */\n typeLabel: string;\n /**\n * What THIS ROW is (\"Company\" for a `crm.party` row whose `party_kind` is\n * `organization`, where `typeLabel` says \"Person\"). The header's type chip and\n * the stand-in titles use this; `typeLabel` when the registration cannot tell\n * the difference. See `DetailRecordType.labelForRow`.\n */\n recordLabel: string;\n about: string | null;\n fields: DetailField[];\n /**\n * The source health strip's content — `null` for a record the platform owns\n * outright, or while a producer that reads it from the host is still\n * answering. See `useDetailHealth`.\n */\n health: DetailSourceHealth | null;\n /** The entity token doors / associations / history key off; null = none. */\n entityToken: string | null;\n frameCtx: DetailFrameContext | null;\n keyboard: DetailKeyboard;\n list: {\n context: DetailListContext | null;\n hasPrev: boolean;\n hasNext: boolean;\n prev: () => void;\n next: () => void;\n };\n /** Re-open this record in another presentation and leave the current one. */\n switchTo: (presentation: DetailPresentation) => void;\n /**\n * 🚨 D1 — THE ONE EXIT FROM THE PAGE PRESENTATION. Back chevron, Escape and\n * a switch that leaves the route all come here; no host, route or shell may\n * call `back()` itself.\n */\n leave: () => void;\n close: () => void;\n}\n\nfunction entityTokenOf(recordType: DetailRecordType | null): string | null {\n return recordType?.entityToken ?? null;\n}\n\n// One console line per subject per tab — a remedy, never a per-render spam.\n// On `globalThis` under a `Symbol.for` slot, never module scope: the ESM and\n// CJS builds of this package are separate module instances, and a per-module\n// set would announce the same subject once per loader graph.\nconst ANNOUNCED_SLOT = Symbol.for(\"ai-matrx.detail.announced-remedies\");\ntype AnnouncedSlot = { [ANNOUNCED_SLOT]?: Set<string> };\nconst announced: Set<string> = ((globalThis as AnnouncedSlot)[ANNOUNCED_SLOT] ??= new Set<string>());\nfunction announceOnce(key: string, message: string): void {\n if (announced.has(key)) return;\n announced.add(key);\n console.warn(message);\n}\n\nfunction neighbour(list: DetailListContext | null, delta: 1 | -1): DetailRef | null {\n if (!list) return null;\n const target = list.index + delta;\n if (target < 0 || target >= list.items.length) return null;\n return list.items[target] ?? null;\n}\n\nconst DEFAULT_TYPE_MAP_REMEDY = \"the host's `resolveType` map (the DetailHostProvider's record-type port)\";\n\nexport function useDetailCore(\n data: DetailInstanceData,\n presentation: DetailPresentation,\n /**\n * `onClose` is how an IN-PLACE presentation is dismissed (the overlay's\n * close). The page presentation needs none: it is left through `leave`,\n * which is the guarded exit — see `close` below.\n */\n options: { onClose?: () => void },\n): DetailCore {\n const host = useDetailHost();\n const ref: DetailRef = { type: data.type, id: data.id };\n const recordType = requireResolveType(host)(data.type);\n const state = useDetailRecord(recordType, data.id);\n\n const row = state.status === \"ready\" ? state.row : null;\n const typeLabel = recordType?.label ?? data.type;\n const seedName = data.seed?.name?.trim() || null;\n // 🚨 D5 — A FAILED OR UNFINISHED LOAD NEVER CLAIMS A RECORD. The\n // registration's `title()` answers `Untitled <Label>` when it has no row,\n // which read as a real, unnamed record sitting above its own error notice\n // (VERIFY-U-P1, D5). A title is invented ONLY from something that exists:\n // the loaded row, or the name the opener already knew (the seed). With\n // neither, the header says what is actually happening and marks itself a\n // stand-in so the presentations can render it as one.\n //\n // 🚨 NEW-1 (VERIFY-U-P1-R2) — `none` IS NOT A LOADED ROW EITHER. A record\n // type with no `load` resolves to `status: \"none\"`, and treating that as a\n // successful load put the registration's invented `Untitled <Label>` in the\n // header, in foreground weight, with the record's DOORS beside it, above a\n // body saying no details were available. Every unregistered type — the\n // agent-emitted type a registry deliberately supports — landed there. Only\n // `ready` is a loaded row; `none` gets the honest absent state below.\n const loadedTitle =\n state.status === \"ready\" && recordType ? recordType.title(row, data.seed) : null;\n //\n // 🚨 NEW-9 (VERIFY-U-P1-R3) — AND THE `none` STAND-IN IS NEVER A SENTENCE.\n // Round 2's fix put \"No detail is registered for session records\" in the\n // header, which is where the record's NAME goes: the screen named a real\n // record with a developer's sentence, printed a repo path in the body, and\n // dropped the doors, so a record became a named dead end. A record that\n // exists but stores nothing more here is named from what is certain — its\n // type and its own id — the body says in one plain sentence that there is\n // nothing more, and the doors stay.\n const shortId = data.id.length > 8 ? data.id.slice(0, 8) : data.id;\n // 🚨 WHAT THIS ROW IS, not what its type is called (chair, 2026-09-18). One\n // registered type is regularly a family — `crm.party` is 1,432 companies and\n // 460 persons — so a type-level label says \"Person\" over a company, in the chip\n // and in every stand-in title (VERIFY-U-P1-R5, N6). A registration that can\n // tell from the row answers through `labelForRow`; nothing else changes, and\n // the presentation pane deliberately keeps `typeLabel`.\n const recordLabel = (row && recordType?.labelForRow?.(row)) || typeLabel;\n const standInTitle =\n state.status === \"not-found\"\n ? `This ${recordLabel.toLowerCase()} could not be found`\n : state.status === \"error\"\n ? `This ${recordLabel.toLowerCase()} could not be loaded`\n : state.status === \"none\"\n ? `${recordLabel} ${shortId}`\n : `Loading this ${recordLabel.toLowerCase()}…`;\n const titleIsStandIn = !loadedTitle && !seedName;\n const title = loadedTitle ?? seedName ?? standInTitle;\n // The record plausibly exists: only a failed or missing read says otherwise.\n const doorsAvailable =\n entityTokenOf(recordType) !== null &&\n state.status !== \"not-found\" &&\n state.status !== \"error\";\n // 🚨 NEW-17 — and a door the host can actually open. `RecordDoors` renders\n // nothing for a token with no route and no peek, so a screen that says \"the\n // controls above still open it where it lives\" must ask.\n const canOpenElsewhere =\n doorsAvailable &&\n entityTokenOf(recordType) !== null &&\n host.doors.hasDoor(entityTokenOf(recordType) as string, data.id);\n const about = data.seed?.about?.trim() || null;\n const fields: DetailField[] =\n row && recordType ? recordType.fields(row) : [];\n // 🚨 THE SOURCE HEALTH STRIP HAS A PRODUCER. The producer is a field on the\n // registration the HOST wires (it may read the connector's recorded\n // capability health, which is an async read), so it is resolved here through\n // one hook rather than called inline — and a producer that fails says so on\n // the strip instead of leaving the record silent.\n const health = useDetailHealth({\n recordType,\n row,\n ref,\n reconnect: host.reconnectSource ?? null,\n });\n const entityToken = entityTokenOf(recordType);\n\n const frameCtx: DetailFrameContext | null = recordType\n ? {\n ref,\n recordType,\n title,\n about,\n status: state.status,\n fields,\n presentation,\n }\n : null;\n\n // 🚨 NEW-9 — THE REMEDY GOES WHERE THE DEVELOPER IS. The person sees a plain\n // sentence; the registry instruction is a console warning, once per type per\n // tab, so it is impossible to miss in development and impossible to read on a\n // screen in production. WHERE the registry is comes from the host's `remedy`\n // port — this package cannot know a host's file paths.\n const typeMapRemedy = host.remedy?.typeMap ?? DEFAULT_TYPE_MAP_REMEDY;\n useEffect(() => {\n if (!recordType) {\n announceOnce(\n `no-type:${data.type}`,\n `[detail] Nothing is registered for the record type \"${data.type}\", so its detail shows ` +\n `the type, the id and the doors only. Remedy: add an entry for it in ${typeMapRemedy}.`,\n );\n return;\n }\n if (state.status === \"none\") {\n announceOnce(\n `no-source:${data.type}`,\n `[detail] The record type \"${data.type}\" has no \\`load\\` (no single canonical source), so ` +\n \"its detail can show nothing beyond what the opener already knew. Remedy: give the type \" +\n `a load in ${typeMapRemedy}, or leave it sourceless deliberately and say why there.`,\n );\n }\n }, [recordType, state.status, data.type, typeMapRemedy]);\n\n // Keep the setting warm for this type so a switch or a neighbour opens\n // without an awaited round-trip.\n const warmPresentation = host.warmPresentation;\n useEffect(() => {\n warmPresentation(data.type);\n }, [warmPresentation, data.type]);\n\n const openNeighbour = (delta: 1 | -1) => {\n const target = neighbour(data.list, delta);\n if (!target || !data.list) return;\n // 🚨 NEW-18 (VERIFY-U-P1-R4) — THE WHOLE LIST CONTEXT TRAVELS, `trimmedFrom`\n // INCLUDED. This rebuilt the context from `items` and `index` only, so the\n // FIRST arrow press turned \"stepping through 139 of the 500 records in the\n // list this was opened from\" into silence — in the window's payload and in\n // the page URL's `lt=` alike — and the next record presented the trimmed\n // window as the whole list. The arrows are the primary binding, so the\n // honesty NEW-7 and NEW-13 bought lasted exactly one keystroke.\n const list: DetailListContext = {\n ...data.list,\n items: data.list.items,\n index: data.list.index + delta,\n };\n const next: DetailInstanceData = { ...target, seed: null, list };\n if (presentation === \"page\") {\n // 🚨 NEW-14 — one history entry for the whole detail visit: moving to a\n // neighbour REPLACES the entry showing the current record, so Back leaves\n // to the list instead of walking back through every record arrowed past.\n host.navigate.toPage(target, { list, replacing: ref });\n } else {\n host.open({ presentation, data: next });\n }\n };\n\n /**\n * 🚨 D1 — LEAVING THE PAGE, ONCE, FOR A DESTINATION THAT EXISTS.\n * A detail page is reached by a shared deep link as often as by an in-app\n * push, and a tab opened straight onto it has NO history entry behind it:\n * `back()` there leaves the person on `about:blank` with the app gone\n * (VERIFY-U-P1 D1). Round 1 guarded the presentation SWITCH only, and\n * VERIFY-U-P1-R2 reproduced the same failure through the page header's Back\n * chevron and through Escape, because the route handed the presentation a raw\n * `router.back()`. The decision lives here now — the one place that knows the\n * record and its token — so every control that leaves the page inherits it.\n */\n const leave = () => {\n if (host.navigate.canGoBack(ref)) host.navigate.back();\n else host.navigate.toRecordHome(ref, entityToken);\n };\n\n const close = () => {\n if (presentation === \"page\") {\n leave();\n return;\n }\n if (!options.onClose) {\n throw new Error(\n \"[detail] The \" +\n presentation +\n \" presentation was mounted without an `onClose`, so nothing can dismiss it. \" +\n \"Pass the overlay's close from the presentation's entry.\",\n );\n }\n options.onClose();\n };\n\n const switchTo = (target: DetailPresentation) => {\n if (target === presentation) return;\n if (target === \"page\") {\n host.navigate.toPage(ref, { seed: data.seed, list: data.list });\n if (presentation !== \"page\") host.close(presentation);\n return;\n }\n host.open({ presentation: target, data });\n if (presentation !== \"page\") {\n host.close(presentation);\n return;\n }\n // The new presentation is already open by this line, so the page must go\n // somewhere that exists: the ONE guarded exit (D1).\n leave();\n };\n\n const hasPrev = neighbour(data.list, -1) !== null;\n const hasNext = neighbour(data.list, 1) !== null;\n\n const keyboard = useDetailKeyboard({\n onClose: close,\n onPrev: hasPrev ? () => openNeighbour(-1) : null,\n onNext: hasNext ? () => openNeighbour(1) : null,\n // 🚨 NEW-22 — Cmd+Enter is one of the three promised bindings; on a record\n // with no editor open it ANSWERS rather than doing nothing in silence.\n onSaveUnavailable: () =>\n host.notify.error(\n `There is nothing to save on this ${typeLabel.toLowerCase()} — nothing here is being ` +\n \"edited. Changes to the record are made where it lives.\",\n ),\n });\n\n return {\n ref,\n data,\n presentation,\n recordType,\n status: state.status,\n errorMessage: state.status === \"error\" ? state.message : null,\n row,\n title,\n titleIsStandIn,\n doorsAvailable,\n canOpenElsewhere,\n typeLabel,\n recordLabel,\n about,\n fields,\n health,\n entityToken,\n frameCtx,\n keyboard,\n list: {\n context: data.list,\n hasPrev,\n hasNext,\n prev: () => openNeighbour(-1),\n next: () => openNeighbour(1),\n },\n switchTo,\n leave,\n close,\n };\n}\n","// @ai-matrx/detail — src/react/useDetailHealth.ts\n//\n// 🚨 THE SOURCE HEALTH STRIP HAS A PRODUCER, AND A REFUSAL ANYWHERE REACHES\n// EVERY DEPENDENT RECORD.\n//\n// The strip itself has existed since the primitive's first commit and NO record\n// could ever render it: the registration's `health` field was never set by the\n// one producer of registrations in app code, so \"does the record tell the truth\n// from a live capability-health row?\" was unanswerable for four verification\n// rounds (VERIFY-U-P1-R4, § The health strip).\n//\n// What the contract needs is a producer the HOST wires, because the answer is not\n// in the row: whether the grant behind a synced record still works is the\n// connector's recorded per-capability health, read from the server. So the\n// producer may be ASYNC and it is resolved here — ONE hook, called for every\n// record type whether it produces health or not, so the hook order cannot change\n// when the person arrows from a synced record to an owned one.\n//\n// Nothing in this file knows what a provider is. It resolves a function the\n// registration carries and hands it the record, an abort signal and the host's\n// reconnect action — the same Reconnect the connector rows offer (\"A refusal\n// anywhere … shows on every dependent record's health strip with the same\n// Reconnect\").\n//\n// AND A PRODUCER THAT FAILS SAYS SO (law 4). A read that throws does not leave\n// the record looking healthy and does not leave the strip absent, which would\n// read as \"this record is not synced\": it renders with an unknown grant, the\n// failure in the person's language, and the Reconnect that is the only thing they\n// can do about it.\n//\n// 🚨 AND `onReconnect: null` MEANS \"OFFER NOTHING\" (Bugbot round 18, frontend\n// PR 228). The producer's decision is three-valued — a function (offer this),\n// `null` (offer nothing: a reconnect would not repair this refusal), or omitted\n// (defer to the host's `reconnectSource`) — and this hook used to merge the\n// host action back in with `??`, which reads `null` as \"unset\". A synced record\n// whose refusal a reconnect cannot fix (a share the owner must grant, a record\n// we own) therefore still showed Reconnect: a control that does nothing. Only an\n// OMITTED key defers to the host now.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport type {\n DetailRecordType,\n DetailRef,\n DetailRow,\n DetailSourceHealth,\n} from \"../types\";\n\nexport interface UseDetailHealthArgs {\n recordType: DetailRecordType | null;\n /** The loaded row — health is only ever produced for a record that loaded. */\n row: DetailRow | null;\n ref: DetailRef;\n /** The host's reconnect action for this record's source, when it has one. */\n reconnect: ((ref: DetailRef, source: string) => void) | null;\n}\n\n/**\n * The strip's Reconnect for a producer's answer: the producer's own action, its\n * explicit refusal (`null`), or — only when it said nothing — the host's.\n *\n * 🚨 AND `grant: \"blocked\"` NEVER GETS ONE (chair, 2026-09-18). The word means\n * the source refuses and nothing the person can click repairs it, so the refusal\n * is structural here rather than a promise each producer must remember to keep:\n * a host that binds `reconnectSource` cannot put the button back, and neither\n * can a producer that supplies an action by mistake.\n */\nexport function reconnectFor(\n value: DetailSourceHealth,\n hostReconnect: ((source: string) => void) | null,\n): (() => void) | null {\n if (value.grant === \"blocked\") return null;\n if (value.onReconnect !== undefined) return value.onReconnect;\n return hostReconnect ? () => hostReconnect(value.source) : null;\n}\n\nexport function useDetailHealth({\n recordType,\n row,\n ref,\n reconnect,\n}: UseDetailHealthArgs): DetailSourceHealth | null {\n const [health, setHealth] = useState<DetailSourceHealth | null>(null);\n const producer = recordType?.health ?? null;\n const key = `${ref.type}:${ref.id}`;\n\n // 🚨 N8 (VERIFY-U-P1-R5) — THE PRODUCER'S FUNCTION IDENTITY IS NOT A\n // DEPENDENCY, BECAUSE A HOST IS NOT REQUIRED TO CACHE IT.\n //\n // The effect used to key on `recordType.health` itself. `resolveType` is a\n // host PORT: a host whose map builds the registration per call hands a fresh\n // producer every render, and this effect's own `setHealth` causes the next\n // render — an unbounded loop that react-dom reports as \"Maximum update depth\n // exceeded\" from inside this file, with no remedy naming the cause. A verifier\n // hit it in minutes by registering the type inline.\n //\n // What actually identifies a producer is the RECORD TYPE it came from, which\n // is a string, plus whether there is one at all. The function and the host's\n // reconnect are read through refs, so a new identity is used on the next run\n // without scheduling one.\n const producerRef = useRef(producer);\n producerRef.current = producer;\n const reconnectRef = useRef(reconnect);\n reconnectRef.current = reconnect;\n const producerKey = producer ? `${recordType?.type ?? \"\"}:${key}` : null;\n\n useEffect(() => {\n const producer = producerRef.current;\n const reconnect = reconnectRef.current;\n if (!producer || !row) {\n setHealth(null);\n return undefined;\n }\n const controller = new AbortController();\n let settled = false;\n const onReconnect = reconnect ? (source: string) => reconnect(ref, source) : null;\n const publish = (value: DetailSourceHealth | null) => {\n if (controller.signal.aborted) return;\n settled = true;\n setHealth(value ? { ...value, onReconnect: reconnectFor(value, onReconnect) } : null);\n };\n try {\n const answer = producer(row, { ref, signal: controller.signal });\n if (answer && typeof (answer as Promise<unknown>).then === \"function\") {\n void (answer as Promise<DetailSourceHealth | null>).then(publish).catch(\n (error: unknown) => {\n if (controller.signal.aborted) return;\n publish(unreadable(error, onReconnect));\n },\n );\n } else {\n publish((answer as DetailSourceHealth | null) ?? null);\n }\n } catch (error: unknown) {\n publish(unreadable(error, onReconnect));\n }\n return () => {\n controller.abort();\n if (!settled) setHealth(null);\n };\n // `ref` is rebuilt every render; its identity is the key. `producerKey`\n // stands in for the producer (N8) and the host's reconnect is a ref.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [producerKey, row, key]);\n\n return health;\n}\n\n/** The honest strip for a source check that could not be read at all. */\nfunction unreadable(\n error: unknown,\n onReconnect: ((source: string) => void) | null,\n): DetailSourceHealth {\n const message = error instanceof Error ? error.message : String(error);\n const source = \"its source\";\n return {\n source: \"Source\",\n grant: \"unknown\",\n grantDetail:\n \"We could not check the connection this record refreshes through, so we cannot say \" +\n `whether it is still working (${message}). Reconnecting is safe and fixes it if the ` +\n \"connection is the problem.\",\n lastRefreshedAt: null,\n onReconnect: onReconnect ? () => onReconnect(source) : null,\n };\n}\n","// @ai-matrx/detail — src/react/useDetailKeyboard.ts\n//\n// The keyboard model, once, for all three presentations (Linear's bones):\n// Escape → close\n// ArrowUp / ArrowDown → previous / next record in the list it was\n// ArrowLeft / ArrowRight opened from, WHEN the body is not the scroll\n// target; `[` / `]` always move\n// Cmd/Ctrl + Enter → save, when a section registered a save handler\n//\n// ARROWS ARE THE PRIMARY BINDING (Arman, 2026-09-17: \"Escape closes, ARROWS\n// move between records, Cmd+Enter saves\"). `[` / `]` stay as aliases because\n// Linear and Notion both carry them and muscle memory is cheap to honour.\n// Every movement key is read only when focus is NOT in an editable — in a\n// field an arrow moves the caret, which is what the person meant.\n//\n// 🚨 NEW-5 — AND AN ARROW NEVER COSTS THE BODY ITS SCROLLING. With a list\n// context this hook `preventDefault`ed every ArrowUp/ArrowDown, so a\n// keyboard-only reader could not scroll a long dossier at all — the only scroll\n// left was PageDown/space (VERIFY-U-P1-R2, break attempt 2). Linear and Notion\n// bind the same keys in a peek and do NOT do that. The rule, ruled by the chair\n// 2026-09-17: the arrows move between records only when the detail body is not\n// the scroll target — when it can scroll, they scroll it, and `[` / `]` remain\n// the always-available record navigation.\n//\n// 🚨 NEW-6 — AND ESCAPE IS NOT SWALLOWED BY A CHECKBOX. `isTypingTarget` is the\n// right rule for an ARROW (a caret belongs to the field) and the wrong one for\n// Escape: every INPUT counted, so Escape pressed on the presentation pane's\n// \"Only for file records\" checkbox closed nothing. Escape closes from any\n// control inside the detail except a menu/select that may be open and a text\n// field with uncommitted edits, where it means \"undo what I typed\".\n//\n// Keys are read on the presentation's own root (capture phase), so two open\n// details never answer one keystroke and a key typed into a field inside the\n// detail is left alone — except Cmd/Ctrl+Enter, which is the point of a field.\n\n\"use client\";\n\nimport { useCallback, useRef, type KeyboardEvent as ReactKeyboardEvent } from \"react\";\n\nexport interface DetailKeyboardHandlers {\n onClose?: (() => void) | null;\n onPrev?: (() => void) | null;\n onNext?: (() => void) | null;\n /**\n * 🚨 NEW-22 (VERIFY-U-P1-R4) — CMD+ENTER NEVER VANISHES. The chord is one of\n * the three bindings the primitive promises, and nothing is registered to save\n * unless a section with an editor is open, so the keystroke was swallowed in\n * silence on every read-only record. A deliberate keystroke gets an answer.\n */\n onSaveUnavailable?: (() => void) | null;\n}\n\nexport interface DetailKeyboard {\n /** Spread onto the presentation root. */\n rootProps: {\n tabIndex: -1;\n onKeyDownCapture: (event: ReactKeyboardEvent<HTMLElement>) => void;\n };\n /** A section with an editor registers its save; returns the unregister. */\n registerSave: (save: () => void | Promise<void>) => () => void;\n /** Whether anything is registered — the switcher shows the hint only then. */\n hasSave: () => boolean;\n}\n\n/** Previous record, when an arrow may move at all. Brackets always may. */\nconst PREV_ARROWS: ReadonlySet<string> = new Set([\"ArrowUp\", \"ArrowLeft\"]);\nconst NEXT_ARROWS: ReadonlySet<string> = new Set([\"ArrowDown\", \"ArrowRight\"]);\nconst PREV_BRACKET = \"[\";\nconst NEXT_BRACKET = \"]\";\n\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Input types that hold typed text — the only ones an arrow or Escape serves. */\nconst NON_TEXT_INPUT_TYPES: ReadonlySet<string> = new Set([\n \"checkbox\",\n \"radio\",\n \"button\",\n \"submit\",\n \"reset\",\n \"range\",\n \"color\",\n \"file\",\n \"image\",\n \"hidden\",\n]);\n\n/**\n * The controls that ACTUALLY own Escape when they are open over the detail.\n *\n * A menu, a select's list, a combobox's list and a popover all render inside the\n * floating layer (`[data-radix-popper-content-wrapper]` in this repo's Radix\n * primitives), and an open menu's items carry the listbox/menu/tree/grid roles.\n * A Drawer's or Dialog's content is deliberately NOT here: the docked and window\n * presentations ARE a Drawer on a phone, and Escape there means \"close this\n * detail\" — which is the whole binding.\n */\nconst ESCAPE_OWNING_ANCESTORS =\n '[data-radix-popper-content-wrapper], [role=\"menu\"], [role=\"menubar\"], ' +\n '[role=\"listbox\"], [role=\"tree\"], [role=\"grid\"]';\n\n/** Whether this element is itself the trigger of something that is OPEN. */\nfunction isOpenTrigger(element: HTMLElement): boolean {\n if (element.getAttribute(\"aria-expanded\") !== \"true\") return false;\n const role = element.getAttribute(\"role\");\n if (role === \"combobox\") return true;\n const haspopup = element.getAttribute(\"aria-haspopup\");\n return haspopup !== null && haspopup !== \"false\" && haspopup !== \"dialog\";\n}\n\n/**\n * Whether Escape belongs to the focused control rather than to the detail\n * (NEW-6). Two cases, and only two: something that IS OPEN over the detail\n * (a select's list, a menu, a combobox, a popover — or the focused trigger of\n * one), and a text field holding edits the person has not committed, where\n * Escape means \"put it back\".\n *\n * 🚨 NEW-11 (VERIFY-U-P1-R3) — AND NOT ANYTHING MERELY EXPANDED. This used to\n * match any ANCESTOR carrying `data-state=\"open\"` or `aria-expanded=\"true\"`,\n * which is every Radix Collapsible and Accordion section a record type puts in\n * its own body, every Drawer's and Dialog's content (both presentations on a\n * phone) and a closed combobox's input. Reproduced with one `data-state=\"open\"`\n * wrapper: Escape stopped working for everything inside it while the same key\n * from the root left at once. The chair's ruling was \"an open menu/select or a\n * text field with uncommitted edits\" — these two cases, and they are narrow on\n * purpose: over-claiming Escape breaks the detail's only keyboard exit.\n */\nfunction escapeBelongsToTheControl(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.tagName === \"SELECT\") return true;\n if (isOpenTrigger(target)) return true;\n if (target.closest(ESCAPE_OWNING_ANCESTORS)) return true;\n if (target.isContentEditable) return true;\n if (target instanceof HTMLTextAreaElement) return target.value !== target.defaultValue;\n if (target instanceof HTMLInputElement) {\n if (NON_TEXT_INPUT_TYPES.has(target.type)) return false;\n return target.value !== target.defaultValue;\n }\n return false;\n}\n\n/** Whether an element is a real vertical scroll target with room to move. */\nfunction isScrollTarget(element: HTMLElement): boolean {\n if (element.scrollHeight - element.clientHeight <= 1) return false;\n const overflowY =\n typeof window === \"undefined\" ? \"\" : window.getComputedStyle(element).overflowY;\n return overflowY === \"auto\" || overflowY === \"scroll\" || overflowY === \"overlay\";\n}\n\n/**\n * Whether the detail the keystroke landed in is SCROLLABLE — in which case the\n * arrows are the reader's, not the list's (NEW-5). The walk starts at the\n * keystroke's target and goes up past the presentation root, because every shell\n * puts its own scroll area AROUND the root (the page's body, the docked panel's\n * column, the window's body).\n */\nfunction bodyIsTheScrollTarget(target: EventTarget | null): boolean {\n let node: HTMLElement | null = target instanceof HTMLElement ? target : null;\n while (node) {\n if (isScrollTarget(node)) return true;\n node = node.parentElement;\n }\n return false;\n}\n\nexport function useDetailKeyboard(handlers: DetailKeyboardHandlers): DetailKeyboard {\n const saveRef = useRef<(() => void | Promise<void>) | null>(null);\n const handlersRef = useRef(handlers);\n handlersRef.current = handlers;\n\n const onKeyDownCapture = useCallback((event: ReactKeyboardEvent<HTMLElement>) => {\n const h = handlersRef.current;\n const meta = event.metaKey || event.ctrlKey;\n if (meta && event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n if (saveRef.current) void saveRef.current();\n else h.onSaveUnavailable?.();\n return;\n }\n if (event.key === \"Escape\") {\n if (!h.onClose || escapeBelongsToTheControl(event.target)) return;\n event.preventDefault();\n event.stopPropagation();\n h.onClose();\n return;\n }\n if (isTypingTarget(event.target)) return;\n // A modified arrow or bracket belongs to the browser (word jump, history, zoom).\n if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;\n if (event.key === PREV_BRACKET && h.onPrev) {\n event.preventDefault();\n h.onPrev();\n return;\n }\n if (event.key === NEXT_BRACKET && h.onNext) {\n event.preventDefault();\n h.onNext();\n return;\n }\n const prev = PREV_ARROWS.has(event.key);\n const next = NEXT_ARROWS.has(event.key);\n if (!prev && !next) return;\n // The reader is reading: the arrows scroll, and `[` / `]` still move.\n if (bodyIsTheScrollTarget(event.target)) return;\n if (prev && h.onPrev) {\n event.preventDefault();\n h.onPrev();\n return;\n }\n if (next && h.onNext) {\n event.preventDefault();\n h.onNext();\n }\n }, []);\n\n const registerSave = useCallback((save: () => void | Promise<void>) => {\n saveRef.current = save;\n return () => {\n if (saveRef.current === save) saveRef.current = null;\n };\n }, []);\n\n const hasSave = useCallback(() => saveRef.current !== null, []);\n\n return {\n rootProps: { tabIndex: -1, onKeyDownCapture },\n registerSave,\n hasSave,\n };\n}\n","// @ai-matrx/detail — src/react/useDetailRecord.ts\n//\n// Loads ONE record through its registration's `load`, with an abort on every\n// change and a structured state for every outcome. Nothing throws out of\n// here and nothing is swallowed: an error carries its message to the screen.\n//\n// 🚨 N8 (VERIFY-U-P1-R5) — THE REGISTRATION'S FUNCTION IDENTITY IS NOT A\n// DEPENDENCY. `resolveType` is a host PORT and nothing in the contract says its\n// answer must be cached: a host whose map builds the registration per call hands\n// a fresh `load` every render, and this effect's own `setState` causes the next\n// render — an endless reload of the same record, at the server. `requestKey`\n// (the type, the id and whether there is a source at all) is what identifies a\n// read; the closure is read through a ref.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport type { DetailLoadState, DetailRecordType, DetailRow } from \"../types\";\n\nexport function useDetailRecord(\n recordType: DetailRecordType | null,\n id: string | null,\n): DetailLoadState<DetailRow> {\n const load = recordType?.load ?? null;\n const [state, setState] = useState<DetailLoadState<DetailRow>>(() =>\n load && id ? { status: \"loading\" } : { status: \"none\" },\n );\n const requestKey = `${recordType?.type ?? \"\"}|${id ?? \"\"}|${load ? \"src\" : \"none\"}`;\n const [seenKey, setSeenKey] = useState(requestKey);\n if (seenKey !== requestKey) {\n // Mask the previous record synchronously — an effect-only reset shows the\n // last record's fields under the next record's title for a frame.\n setSeenKey(requestKey);\n setState(load && id ? { status: \"loading\" } : { status: \"none\" });\n }\n\n const loadRef = useRef(load);\n loadRef.current = load;\n\n useEffect(() => {\n const load = loadRef.current;\n if (!load || !id) return undefined;\n const controller = new AbortController();\n void (async () => {\n try {\n const result = await load(id, controller.signal);\n if (controller.signal.aborted) return;\n if (\"notFound\" in result) {\n setState({ status: \"not-found\" });\n } else {\n setState({ status: \"ready\", row: result.row });\n }\n } catch (error: unknown) {\n if (controller.signal.aborted) return;\n const message = error instanceof Error ? error.message : String(error);\n console.error(\"[detail] record load failed\", { type: recordType?.type, id, error });\n setState({ status: \"error\", message });\n }\n })();\n return () => controller.abort();\n // `requestKey` stands in for `load` (N8): a host is not required to cache it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [requestKey, id]);\n\n return state;\n}\n","// @ai-matrx/detail — src/react/useOpenDetail.ts\n//\n// THE ONE OPENER. Every surface that names a record and wants its detail\n// calls this; nothing else decides how a record opens. It resolves the\n// person's setting (`ui.detail.default_presentation`, per record type), opens\n// in place, and never navigates away — `page` is the only presentation that\n// changes the URL, and it offers \"open as window\" back.\n\n\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { useDetailHost } from \"./host\";\nimport {\n DETAIL_PRESENTATION_KNOB,\n type DetailInstanceData,\n type DetailOpenRequest,\n type DetailPresentation,\n} from \"../types\";\n\nexport type OpenDetail = (request: DetailOpenRequest) => Promise<DetailPresentation>;\n\n/**\n * @param warmType a record type this surface will open — its setting is\n * resolved on mount so the click is synchronous.\n */\nexport function useOpenDetail(warmType?: string | null): OpenDetail {\n const host = useDetailHost();\n\n const warmPresentation = host.warmPresentation;\n useEffect(() => {\n if (warmType) warmPresentation(warmType);\n }, [warmPresentation, warmType]);\n\n return async (request) => {\n const data: DetailInstanceData = {\n type: request.type,\n id: request.id,\n seed: request.seed ?? null,\n list: request.list ?? null,\n };\n let presentation = request.presentation;\n if (!presentation) {\n try {\n presentation = await host.resolvePresentation(request.type);\n } catch (error: unknown) {\n // The knob resolver RAISES for an unregistered key by design. The\n // stand-in announces itself with the remedy and uses the platform\n // default the knob would have carried.\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[detail] ${DETAIL_PRESENTATION_KNOB} could not be resolved`, error);\n host.notify.error(\n `Your detail presentation setting could not be read (${DETAIL_PRESENTATION_KNOB}: ${message}). ` +\n \"Opening as a window until it can be read; an administrator can register or repair the setting.\",\n );\n presentation = \"window\";\n }\n }\n if (presentation === \"page\") {\n host.navigate.toPage({ type: data.type, id: data.id }, { seed: data.seed, list: data.list });\n } else {\n host.open({ presentation, data });\n }\n return presentation;\n };\n}\n","// @ai-matrx/detail — src/types.ts\n//\n// The Detail primitive's contract. ONE core per record type; the wrapper\n// yields three presentations (window | docked | page) from that one\n// registration. Nothing in this file knows how a record is fetched, how a\n// window is drawn, or which app hosts it — every app-specific capability\n// arrives through `DetailHostPorts` (see react/host.tsx). This module imports\n// React TYPES only and is legal inside a React Server Component.\n\nimport type { ComponentType, ReactNode } from \"react\";\n\n/** The three ways a record detail can show. `window` is the platform default. */\nexport const DETAIL_PRESENTATIONS = [\"window\", \"docked\", \"page\"] as const;\nexport type DetailPresentation = (typeof DETAIL_PRESENTATIONS)[number];\n\n/**\n * The knob every presentation decision reads (`platform.feature_knob`,\n * feature `ui.detail`, key `default_presentation`; seeded by matrx-frontend's\n * `migrations/detail_presentation_knob.sql`). Enum of DETAIL_PRESENTATIONS,\n * overridable by organization and user. The package never reads the settings\n * ladder itself — the host resolves this key and answers through the\n * `usePresentationSetting` / `resolvePresentation` ports.\n */\nexport const DETAIL_PRESENTATION_KNOB = \"ui.detail.default_presentation\";\n\n/**\n * The PER-RECORD-TYPE override of the setting above (`platform.feature_knob`,\n * feature `ui.detail`, key `presentation_by_type`): a json object keyed by\n * record type token, e.g. `{\"file\": \"docked\"}`. Nearest wins over\n * `default_presentation` for the type it names, and the same organization →\n * user ladder resolves it, so an organization can set \"contracts open as a\n * page\" and a person can still choose otherwise for themselves.\n *\n * WHY A SECOND KEY AND NOT THE `table` RUNG. The platform's per-table rung\n * (DD-131, precedence 50) is keyed by a `platform.entity_types` ROW ID, and\n * that table is admin-only by a restrictive policy — a browser cannot read the\n * id, so a `table` rung would be a setting the screen offers and the reader\n * never honours. One json key on the ladder the client already has is the\n * override the settings platform supports today.\n */\nexport const DETAIL_PRESENTATION_BY_TYPE_KNOB = \"ui.detail.presentation_by_type\";\n\n/**\n * How many records of the list a detail was opened FROM may ride the page\n * presentation's URL (`platform.feature_knob`, feature `ui.detail`, key\n * `list_context_max_ids`; seeded by matrx-frontend's\n * `migrations/detail_list_context_max_knob.sql`). Organization-overridable.\n *\n * Why a knob and not a constant: it is a ceiling, and every ceiling here is a\n * row an admin owns (`common-docs/policies/limits-are-knobs-agents-set-them.md`).\n * Why it exists at all: uncapped, a 500-row list produced a >20 KB href no\n * server accepts (NEW-7, VERIFY-U-P1-R2). Beyond the cap the URL carries the\n * window around the current record and the detail says the list was trimmed.\n */\nexport const DETAIL_LIST_CONTEXT_MAX_KNOB = \"ui.detail.list_context_max_ids\";\n\n/**\n * The cap when the knob has not answered yet (a cold cache, a signed-out\n * render, a host that binds no settings ladder). 100 `type.id` pairs with uuid\n * ids is the most the strictest presentation's FINAL URL can carry inside the\n * budget below, and it is far more neighbours than a person arrows through in\n * one sitting.\n */\nexport const DEFAULT_DETAIL_LIST_CONTEXT_MAX = 100;\n\n/**\n * 🚨 NEW-19 (VERIFY-U-P1-R4) — THE BUDGET BELONGS TO THE FINAL URL, ONCE.\n *\n * It used to be 6,000 characters measured on the list VALUE, one escaping layer\n * too early, and nothing bounded the whole address:\n *\n * * a detail window's deep link measured 5,992 against that 6,000 and reached\n * the address bar at 7,416, because the host's panel-URL writer re-serializes\n * the `?panels=` token through `new URLSearchParams(...).toString()`, which\n * re-escapes every `%` the token's own escaping already produced (a uuid's\n * `-` goes `-` → `%2D` → `%252D`);\n * * a detail PAGE that also carried an open window was 13,433 characters —\n * its own `?l=` plus the merged `panels=` — past the 8 KB request line, so\n * the edge answers 414 and the link is dead (VERIFY-U-P1-R4, NEW-19).\n *\n * 8,000 characters is the strictest edge in front of this platform (an 8 KB\n * request line), and the method, the path, every other parameter and the\n * token's own non-list args share it: a caller passes what the rest of the URL\n * already costs as `reservedBytes` and `trimListContext` measures the FINAL\n * serialized form. No caller may skip it, and the record SAYS what was cut\n * (`trimmedFrom`).\n */\nexport const DETAIL_URL_BUDGET_BYTES = 8000;\n\n/**\n * The hard ceiling on the knob — the number of records the final URL can really\n * carry in EVERY presentation, so a value below it always changes what travels.\n *\n * 🚨 NEW-20 (VERIFY-U-P1-R4). The ceiling was 500 while the byte budget stopped\n * a uuid list at 139, so every value an administrator could set from 139 to 500\n * behaved identically and the knob's own live basis text told them 200 records\n * would travel. Measured against the final URL: 100 uuid entries under a short\n * type token cost ~6.7 KB in the window's `?panels=` token (the strictest\n * spelling) and ~4.3 KB in the page query, both inside the budget above; 150\n * costs ~10 KB in the token and does not fit. matrx-frontend's\n * `migrations/detail_list_context_max_deliverable.sql` lowers the knob's own\n * `max_value` and default to the same number and rewrites its basis text, so\n * the settings screen cannot offer records the URL can never carry. A longer\n * type token still trims below the cap — and still says so.\n */\nexport const DETAIL_LIST_CONTEXT_MAX_IDS_CEILING = 100;\n\n/**\n * The knob's value as a usable cap; the default for anything that is not one,\n * and never more than the ceiling the byte budget can honour (NEW-12). The ONE\n * place the knob is turned into a number — the page href builder, the window's\n * deep-link token and the trim itself all read it through here.\n */\nexport function detailListContextMax(raw: unknown): number {\n const value = typeof raw === \"number\" ? raw : Number(raw);\n const asked = Number.isFinite(value) && value >= 1\n ? Math.floor(value)\n : DEFAULT_DETAIL_LIST_CONTEXT_MAX;\n return Math.min(asked, DETAIL_LIST_CONTEXT_MAX_IDS_CEILING);\n}\n\n/** The per-type entry for `type`, when the map holds a usable one. */\nexport function presentationForTypeFromMap(\n raw: unknown,\n type: string,\n): DetailPresentation | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n const value = (raw as Record<string, unknown>)[type];\n return isDetailPresentation(value) ? value : undefined;\n}\n\nexport function isDetailPresentation(value: unknown): value is DetailPresentation {\n return (\n typeof value === \"string\" &&\n (DETAIL_PRESENTATIONS as readonly string[]).includes(value)\n );\n}\n\n/** A record, named the way the platform names records: type token + id. */\nexport interface DetailRef {\n type: string;\n id: string;\n}\n\n/** What the opener already knows, shown instantly until the row loads. */\nexport interface DetailSeed {\n name?: string | null;\n about?: string | null;\n}\n\n/**\n * The list the record was opened FROM, so `[` / `]` move to the previous /\n * next record without going back to the list. Plain data — it travels through\n * the host's state store and the URL.\n */\nexport interface DetailListContext {\n items: DetailRef[];\n index: number;\n /**\n * Set when this list is a WINDOW cut out of a longer one (the page\n * presentation carries the list in the URL and it is capped —\n * `ui.detail.list_context_max_ids`): the length of the list it was cut from,\n * so the detail can say the neighbours beyond it are not reachable from here\n * instead of presenting the window as the whole list (NEW-7).\n */\n trimmedFrom?: number;\n}\n\n/** What `useOpenDetail` accepts. */\nexport interface DetailOpenRequest extends DetailRef {\n /** Skip the setting and open this way. Omit to honour the user's setting. */\n presentation?: DetailPresentation;\n seed?: DetailSeed | null;\n list?: DetailListContext | null;\n}\n\n/** The payload a presentation receives — everything an open request carried. */\nexport interface DetailInstanceData extends DetailRef {\n seed: DetailSeed | null;\n list: DetailListContext | null;\n}\n\n// ─── The record-type contract ───────────────────────────────────────────────\n\nexport type DetailRow = Record<string, unknown>;\n\nexport type DetailLoadResult<Row extends DetailRow = DetailRow> =\n | { row: Row }\n | { notFound: true };\n\n/** One rendered field. `ref` makes the value a door instead of text. */\nexport interface DetailField {\n key: string;\n label: string;\n text: string;\n /** Render in a monospace block (JSON, long identifiers). */\n mono?: boolean;\n /**\n * This value names another record — render it as a door.\n *\n * 🚨 AND THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). The body used to\n * render the door from `ref.id` alone, so a field whose `text` was\n * \"Brightwater Robotics\" printed a truncated uuid instead: a door nobody can read is\n * half a dead end. `name` is the record's own name for the host's `RefCell` to\n * render; when it is omitted the body falls back to `text` (unless `text` is\n * the id itself, in which case there is nothing better to say).\n */\n ref?: { token: string; id: string; name?: string | null } | null;\n}\n\n/**\n * The \"source health strip\" for a synced record — where it came from, when it\n * was last refreshed, whether the grant behind it still works, and how to open\n * it at the source. Absent for records the platform owns outright.\n */\nexport interface DetailSourceHealth {\n /** Human name of the source (\"Google Drive\", \"Search Console\"). */\n source: string;\n lastRefreshedAt?: string | null;\n /**\n * 🚨 THE GRANT-STATE VOCABULARY. Five of the six say what is wrong with the\n * CONNECTION; `blocked` says the record cannot be refreshed and NOTHING the\n * person can click repairs it (chair, 2026-09-18).\n *\n * - `ok` → the grant behind this record works;\n * - `expired` → it lapsed and a reconnect renews it;\n * - `revoked` → someone withdrew it and a reconnect re-grants it;\n * - `missing` → we never had it, or the record is not shared with us;\n * - `unknown` → we could not check (a read failed) — reconnecting is safe;\n * - `blocked` → the source itself refuses, and no control repairs it: a\n * provider outage, a quota, a platform-configuration fault,\n * a connection the server reports as unavailable. The strip\n * says BLOCKED, states the reason, and offers NO Reconnect —\n * which `reconnectFor` enforces for this word whatever the\n * producer or the host supplies, because a button that\n * cannot work is a control that does nothing (law 4).\n *\n * `unknown` used to carry both meanings, so a blocked record was offered the\n * consent window it could not be repaired by (VERIFY-U-P1-R5, N1's class).\n */\n grant: \"ok\" | \"expired\" | \"revoked\" | \"missing\" | \"unknown\" | \"blocked\";\n /** One sentence for the grant state, in the person's language. */\n grantDetail?: string | null;\n openAtSourceHref?: string | null;\n /**\n * The per-record label for the \"open at source\" control, when the\n * registration knows it precisely (chair, 2026-09-18). `DetailBody` derives\n * a label from `source` on its own (see `deriveOpenAtSourceLabel`) for a\n * producer that has no opinion, but that derivation guesses from a family\n * name (\"Google Docs, Sheets & Drive files\" → \"Open in Google\") — a\n * registration that knows the row's exact kind (a Doc vs. a Sheet vs. a\n * plain Drive file; a Calendar event) sets this instead and it wins.\n */\n openAtSourceLabel?: string;\n /**\n * The repair offered when the grant is not `ok`. 🚨 THREE-VALUED, AND THE\n * THREE MEAN DIFFERENT THINGS (Bugbot round 18 on frontend PR 228):\n *\n * - a function → offer THIS action;\n * - `null` → offer NOTHING: the producer has decided a reconnect\n * would not repair this refusal (a share the owner must\n * grant, a record we own, an unclassified failure) and a\n * Reconnect button would be a control that does nothing;\n * - omitted → the producer has no opinion; the host's `reconnectSource`\n * port supplies the action when it has one.\n *\n * `useDetailHealth` honours `null` explicitly — it used to fill the host\n * action back in with `??`, which reads `null` as \"unset\", so a synced record\n * whose refusal a reconnect cannot fix still showed Reconnect.\n */\n onReconnect?: (() => void) | null;\n /** Offered when the record can be refreshed on demand. */\n onRefresh?: (() => void | Promise<void>) | null;\n}\n\n/** What a `health` producer is told about the record it is answering for. */\nexport interface DetailHealthContext {\n ref: DetailRef;\n /** Aborted when the record changes or the detail closes. */\n signal: AbortSignal;\n}\n\n/**\n * The health producer: sync or async, and free to answer `null` for a record\n * that is not synced. A throw or a rejection is rendered as an honest \"we could\n * not check the connection\" strip, never as silence (`useDetailHealth`).\n */\nexport type DetailHealthProducer<Row extends DetailRow = DetailRow> = (\n row: Row,\n ctx: DetailHealthContext,\n) => DetailSourceHealth | null | Promise<DetailSourceHealth | null>;\n\n/** An extra section a record type adds under the fixed ones. */\nexport interface DetailSection {\n id: string;\n label: string;\n content: ReactNode;\n}\n\n/** What a registration's optional `Frame` and sections receive. */\nexport interface DetailFrameContext {\n ref: DetailRef;\n recordType: DetailRecordType;\n title: string;\n about: string | null;\n status: DetailStatus;\n fields: DetailField[];\n presentation: DetailPresentation;\n}\n\n/**\n * ONE registration per record type. The core component (header + fixed\n * sections) is derived from this; a record type never writes presentation\n * code. Everything optional degrades to \"section absent\", never to a dead\n * or disabled-looking control.\n */\nexport interface DetailRecordType<Row extends DetailRow = DetailRow> {\n /** The type token this registration answers for (as opened). */\n type: string;\n /** Human label (\"Task\", \"File\"). */\n label: string;\n icon: ComponentType<{ className?: string }>;\n /** Accent classes for the icon and type chip; semantic tokens only. */\n accent?: { text: string; bg: string; ring: string } | null;\n /**\n * The canonical entity-registry token for doors, associations and history.\n * `null` when the type is not a registry entity — the header then offers\n * copy-id only, and the associations / history sections are absent.\n */\n entityToken: string | null;\n /**\n * Load the full row. `null` when the type has no single canonical source\n * (the detail then renders from the seed alone and says so).\n */\n load: ((id: string, signal: AbortSignal) => Promise<DetailLoadResult<Row>>) | null;\n /** The title to show; receives the loaded row (or null) and the seed. */\n title: (row: Row | null, seed: DetailSeed | null) => string;\n /**\n * 🚨 WHAT THIS ROW IS, WHEN THE TYPE CANNOT SAY (chair, 2026-09-18). `label` is\n * per TYPE, and a registered type is regularly a family: `crm.party` holds\n * 1,432 companies and 460 persons, so a type labelled \"Person\" told a person\n * that a company is a person — twice on screen, above a field reading\n * `organization` (VERIFY-U-P1-R5, N6). A registration that can tell the\n * difference from the row answers here.\n *\n * It names the RECORD, so it is used for the header's type chip and for the\n * stand-in titles (\"Company 3f2a1b9c\") only. The presentation pane keeps the\n * type-level `label`, because \"Every contact record now opens as a window\" is a\n * statement about the TYPE's setting, not about the row on screen.\n *\n * `null` (or omitted) falls back to `label`, then to the raw type token.\n */\n labelForRow?: (row: Row | null) => string | null;\n /** The field list for the fields section. */\n fields: (row: Row) => DetailField[];\n /**\n * 🚨 THE SOURCE HEALTH STRIP'S PRODUCER. Source health for a SYNCED record;\n * omit it, or answer `null`, for a record the platform owns outright.\n *\n * It may be ASYNC, because whether the grant behind a synced record still works\n * is not in the row — it is the connector's recorded per-capability health, read\n * from the server. The host wires this field when it registers the record type\n * (never a second reader of the connector's health), and `useDetailHealth`\n * resolves it. Until 2026-09-17 nothing set it, so the strip could not render on\n * any record and the promise that a refusal anywhere shows on every dependent\n * record with the same Reconnect had no witness (VERIFY-U-P1-R4).\n */\n health?: DetailHealthProducer<Row> | null;\n /**\n * Tokens the associations section shows. `null` hides the section.\n * Omitted → the host's default set minus this record's own token.\n */\n associationTokens?: string[] | null;\n /** Show the history section (row versions). Default: when `entityToken` is set. */\n history?: boolean;\n /** Extra sections under the fixed ones. */\n extraSections?: ((row: Row | null, ctx: DetailFrameContext) => DetailSection[]) | null;\n /**\n * Wraps the whole body (surface runtime, right-click menu, …). Receives the\n * live context so a host can expose the dossier to its agents. Optional.\n */\n Frame?: ComponentType<{ ctx: DetailFrameContext; children: ReactNode }> | null;\n}\n\nexport type DetailStatus =\n | \"loading\"\n | \"ready\"\n | \"not-found\"\n | \"error\"\n /** The type has no `load` — rendered from the seed alone. */\n | \"none\";\n\nexport type DetailLoadState<Row extends DetailRow = DetailRow> =\n | { status: \"loading\" }\n | { status: \"ready\"; row: Row }\n | { status: \"not-found\" }\n | { status: \"error\"; message: string }\n | { status: \"none\" };\n\n/** One row of the history section. */\nexport interface DetailHistoryEntry {\n version: number;\n operation: string;\n actorId: string | null;\n occurredAt: string;\n isCurrent: boolean;\n}\n","// @ai-matrx/detail — src/react/DetailHeader.tsx\n//\n// The header's two halves, filled into whichever shell is showing the record:\n// <DetailTitle> icon · name · type chip · the record's own doors\n// <DetailActions> previous / next · open-as (window / docked / page) · copy id\n//\n// 🚨 TWO RULES THIS FILE EXISTS TO HOLD (VERIFY-U-P1, D2 and D4):\n//\n// 1. THE NAME IS THE ONE THING THE HEADER ALWAYS SHOWS. At 390px the page\n// header used to reduce to icon + type chip + icons, with the record's name\n// gone and the id chip colliding with the shell's own right-hand cluster —\n// a person could not tell WHICH record they were looking at. The type chip\n// and the record's doors now step aside below `sm` and reappear in the\n// body's meta line (`DetailRecordMeta`), which is where \"below or into\n// overflow\" lands. The name never steps aside.\n//\n// 2. THE ACTION CLUSTER IS ICONS ONLY, AT EVERY WIDTH. A `WindowPanel` centres\n// its title across the whole bar, so a wide action cluster is drawn on top\n// of it. The old copy control carried the full uuid behind a `sm:` class —\n// and `sm:` answers the VIEWPORT, not the 540px window, so on a 1440px\n// screen the id text was always there and always overlapped. The id lives\n// in the body meta line instead; up here it is one copy icon.\n//\n// 3. A stand-in title (loading / not found / failed) is rendered AS a stand-in:\n// muted, with no doors beside it, because there is no record to open. A\n// record that exists but stores nothing more here is NOT that case: it is\n// named from its type and id and keeps its doors (NEW-9).\n//\n// 4. THE NAME HAS A FLOOR AND THE CLUSTER COLLAPSES FIRST (NEW-8). At the\n// window's own 360px minimum the cluster left the name ~60px. The name now\n// carries a minimum width and the optional controls (open-as, copy) move into\n// an overflow menu below a CONTAINER width — never a `sm:` viewport class,\n// which cannot know a window's size. See `headerGeometry.ts`.\n\n\"use client\";\n\nimport { useEffect, useRef, useState, type ComponentType } from \"react\";\nimport {\n AppWindowIcon,\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n CopyIcon,\n ExpandIcon,\n MoreHorizontalIcon,\n PanelRightIcon,\n type DetailIconProps,\n} from \"./icons\";\nimport { cn } from \"@ai-matrx/design-system\";\n\nimport { useDetailHost, type DetailMenuAction } from \"./host\";\nimport type { DetailPresentation } from \"../types\";\nimport type { DetailCore } from \"./useDetailCore\";\n\nconst ICON_BUTTON =\n \"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors \" +\n \"hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:opacity-40 disabled:hover:bg-transparent pointer-coarse:h-10 pointer-coarse:w-10\";\n\nconst MENU_ITEM =\n \"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-muted-foreground \" +\n \"transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 \" +\n \"focus-visible:ring-ring pointer-coarse:py-2.5\";\n\nconst TYPE_CHIP =\n \"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide ring-1 ring-inset\";\n\nconst PRESENTATIONS: {\n value: DetailPresentation;\n label: string;\n Icon: ComponentType<DetailIconProps>;\n}[] = [\n { value: \"window\", label: \"Open as window\", Icon: AppWindowIcon },\n { value: \"docked\", label: \"Open docked to the side\", Icon: PanelRightIcon },\n { value: \"page\", label: \"Open as page\", Icon: ExpandIcon },\n];\n\nfunction TypeChip({ core, className }: { core: DetailCore; className?: string }) {\n const accent = core.recordType?.accent ?? null;\n return (\n <span\n className={cn(\n TYPE_CHIP,\n accent?.bg ?? \"bg-muted\",\n accent?.text ?? \"text-muted-foreground\",\n accent?.ring ?? \"ring-border\",\n className,\n )}\n data-detail-type-chip\n >\n {/* What THIS ROW is (chair, 2026-09-18): `recordLabel`, not the type's own\n label — one registered type is regularly a family, and the chip is the\n place a company was called a Person. */}\n {core.recordLabel}\n </span>\n );\n}\n\nexport function DetailTitle({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const Icon = core.recordType?.icon ?? null;\n const accent = core.recordType?.accent ?? null;\n const RecordDoors = host.doors.RecordDoors;\n return (\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {Icon ? <Icon className={cn(\"h-4 w-4 shrink-0\", accent?.text)} /> : null}\n {/* The name. Never hidden, never truncated away to nothing. */}\n <span\n /* `min-w-[6rem]` is DETAIL_TITLE_MIN_WIDTH_CLASS, written out so\n Tailwind compiles it (NEW-8). */\n className={cn(\n \"flex-1 truncate text-sm font-medium min-w-[6rem]\",\n core.titleIsStandIn ? \"text-muted-foreground\" : \"text-foreground\",\n )}\n data-detail-title\n data-detail-title-standin={core.titleIsStandIn ? \"true\" : undefined}\n >\n {core.title}\n </span>\n {/* Type chip: desktop only — `DetailRecordMeta` carries it on a phone. */}\n <TypeChip core={core} className=\"hidden sm:inline-block\" />\n {/* The record's own doors — a SIBLING of the title, never inside a button.\n Absent while the title is a stand-in: there is no record to open yet.\n The host's control renders nothing when the token has no route and no\n peek, so an unregistered type keeps the name and nothing else. */}\n {core.entityToken && core.doorsAvailable ? (\n <span className=\"hidden shrink-0 sm:inline-flex\">\n <RecordDoors token={core.entityToken} id={core.ref.id} name={core.title} />\n </span>\n ) : null}\n </div>\n );\n}\n\n/**\n * The type, the id and the record's doors, in the BODY — where the header\n * hands them off below `sm`, and where the id lives at every width now that\n * the header cluster is icons only.\n */\nexport function DetailRecordMeta({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const RecordDoors = host.doors.RecordDoors;\n const trimmedFrom = core.list.context?.trimmedFrom;\n return (\n <div\n className=\"flex flex-wrap items-center gap-2 border-b border-border/60 px-4 py-2\"\n data-detail-record-meta\n >\n <TypeChip core={core} className=\"sm:hidden\" />\n {core.entityToken && core.doorsAvailable ? (\n <span className=\"inline-flex sm:hidden\">\n <RecordDoors token={core.entityToken} id={core.ref.id} name={core.title} />\n </span>\n ) : null}\n <span className=\"truncate font-mono text-[10px] text-muted-foreground\" data-detail-id>\n {core.ref.id}\n </span>\n {/* 🚨 NEW-7 — A TRIMMED LIST SAYS SO. The page presentation carries its\n list in the URL, capped by `ui.detail.list_context_max_ids`; beyond the\n cap it holds the window around this record. Presenting that window as\n the whole list would be the screen quietly lying about what the arrows\n can reach. */}\n {trimmedFrom ? (\n <span className=\"w-full text-[10px] text-muted-foreground\" data-detail-list-trimmed>\n {`Stepping through ${core.list.context?.items.length} of the ${trimmedFrom} records in the list this was opened from — a link can only carry that many, so the rest are not reachable from here. Open the list again to reach them.`}\n </span>\n ) : null}\n </div>\n );\n}\n\n/**\n * The optional controls as data — the same actions the overflow menu offers —\n * for a shell whose chrome has its own menu (a canvas tab's one \"⋮\").\n */\nexport function useDetailMenuActions(core: DetailCore): DetailMenuAction[] {\n const host = useDetailHost();\n return [\n ...PRESENTATIONS.filter((p) => p.value !== core.presentation).map(({ value, label }) => ({\n id: `detail-open-${value}`,\n label,\n onSelect: () => core.switchTo(value),\n })),\n {\n id: \"detail-copy-id\",\n label: \"Copy record id\",\n onSelect: () => {\n void host.copyText(core.ref.id).then((ok) => {\n if (ok) host.notify.success(\"Record id copied\");\n else host.notify.error(\"Could not copy the record id to the clipboard.\");\n });\n },\n },\n ];\n}\n\n/**\n * Previous / next, the counter, and — unless `optional=\"none\"` (the shell\n * offers `menuActions` in its own menu) — the optional controls, which fold\n * into one overflow button in a narrow header.\n */\nexport function DetailActions({ core, optional = \"inline\" }: { core: DetailCore; optional?: \"inline\" | \"none\" }) {\n const host = useDetailHost();\n const [copied, setCopied] = useState(false);\n const [menuOpen, setMenuOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n\n const copyId = async () => {\n const ok = await host.copyText(core.ref.id);\n if (!ok) {\n host.notify.error(\"Could not copy the record id to the clipboard.\");\n return;\n }\n setCopied(true);\n window.setTimeout(() => setCopied(false), 1500);\n };\n\n // A click anywhere else, or Escape, closes the overflow menu. Escape is left\n // to the menu by the keyboard model precisely because it is a `role=\"menu\"`\n // (NEW-11), so the menu must be the thing that closes on it.\n useEffect(() => {\n if (!menuOpen) return undefined;\n const onDown = (event: MouseEvent) => {\n if (!(event.target instanceof Node)) return;\n if (menuRef.current && !menuRef.current.contains(event.target)) setMenuOpen(false);\n };\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") setMenuOpen(false);\n };\n document.addEventListener(\"mousedown\", onDown, true);\n document.addEventListener(\"keydown\", onKey, true);\n return () => {\n document.removeEventListener(\"mousedown\", onDown, true);\n document.removeEventListener(\"keydown\", onKey, true);\n };\n }, [menuOpen]);\n\n const list = core.list;\n const others = PRESENTATIONS.filter((p) => p.value !== core.presentation);\n\n return (\n <div className=\"flex shrink-0 items-center gap-0.5\" data-detail-actions>\n {/* Previous / next stay in the bar at every width: they are what the\n person is DOING while stepping through a list. */}\n {list.context ? (\n <>\n <button\n type=\"button\"\n className={ICON_BUTTON}\n onClick={list.prev}\n disabled={!list.hasPrev}\n aria-label=\"Previous record (Up arrow, or left bracket)\"\n title=\"Previous record ↑ or [\"\n >\n <ChevronLeftIcon className=\"h-4 w-4\" />\n </button>\n <span className=\"px-1 text-[11px] tabular-nums text-muted-foreground\">\n {list.context.index + 1}/{list.context.items.length}\n </span>\n <button\n type=\"button\"\n className={ICON_BUTTON}\n onClick={list.next}\n disabled={!list.hasNext}\n aria-label=\"Next record (Down arrow, or right bracket)\"\n title=\"Next record ↓ or ]\"\n >\n <ChevronRightIcon className=\"h-4 w-4\" />\n </button>\n <span className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n </>\n ) : null}\n\n {/* 🚨 NEW-8 — THE OPTIONAL CONTROLS YIELD BEFORE THE NAME DOES. Below the\n window header's container width these become one overflow button; the\n classes are the literals `headerGeometry.ts` names, written out so\n Tailwind compiles them, and the guard asserts they agree. */}\n {optional === \"none\" ? null : (\n <>\n <span\n className=\"flex items-center gap-0.5 @max-[26rem]/window-header:hidden\"\n data-detail-actions-collapsible\n >\n {others.map(({ value, label, Icon }) => (\n <button\n key={value}\n type=\"button\"\n className={ICON_BUTTON}\n onClick={() => core.switchTo(value)}\n aria-label={label}\n title={label}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n {/* Icons only: the id itself is in the body meta line (see rule 2). */}\n <button\n type=\"button\"\n onClick={() => void copyId()}\n className={ICON_BUTTON}\n aria-label=\"Copy record id\"\n title=\"Copy record id\"\n data-detail-copy-id\n >\n {copied ? (\n <CheckIcon className=\"h-3.5 w-3.5 text-primary\" />\n ) : (\n <CopyIcon className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </span>\n\n <span className=\"relative hidden @max-[26rem]/window-header:inline-flex\" ref={menuRef}>\n <button\n type=\"button\"\n className={cn(ICON_BUTTON, \"hidden @max-[26rem]/window-header:inline-flex\")}\n onClick={() => setMenuOpen((wasOpen) => !wasOpen)}\n aria-label=\"More record actions\"\n title=\"More record actions\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n data-detail-actions-overflow\n >\n <MoreHorizontalIcon className=\"h-4 w-4\" />\n </button>\n {menuOpen ? (\n <div\n role=\"menu\"\n aria-label=\"Record actions\"\n className=\"absolute right-0 top-full z-50 mt-1 min-w-44 rounded-md border border-border bg-card p-1 shadow-md\"\n data-detail-actions-menu\n >\n {others.map(({ value, label, Icon }) => (\n <button\n key={value}\n type=\"button\"\n role=\"menuitem\"\n className={MENU_ITEM}\n onClick={() => {\n setMenuOpen(false);\n core.switchTo(value);\n }}\n >\n <Icon className=\"h-3.5 w-3.5 shrink-0\" />\n {label}\n </button>\n ))}\n <button\n type=\"button\"\n role=\"menuitem\"\n className={MENU_ITEM}\n onClick={() => {\n setMenuOpen(false);\n void copyId();\n }}\n >\n <CopyIcon className=\"h-3.5 w-3.5 shrink-0\" />\n Copy record id\n </button>\n </div>\n ) : null}\n </span>\n </>\n )}\n </div>\n );\n}\n","// @ai-matrx/detail — src/react/icons.tsx\n//\n// THE PACKAGE SHIPS ITS OWN GLYPHS (C19, typescript-package-standard: \"no\n// icon-library dependency, ever\"). These seventeen are the Lucide glyphs the\n// primitive draws — copied from Lucide (ISC license, https://lucide.dev) with\n// Lucide's default stroke attributes — so a consumer never inherits a\n// lucide-react version, and a host that draws no Lucide at all still gets the\n// same header, strip and pane. The design-system `data-table/icons.tsx`\n// precedent, at the same fraction-of-a-percent cost.\n//\n// A record type's OWN icon (`DetailRecordType.icon`) is host data and arrives\n// through the registration; nothing here decides it.\n\nimport type { ReactNode, SVGProps } from \"react\";\n\nexport type DetailIconProps = SVGProps<SVGSVGElement>;\n\nfunction glyph(name: string, children: ReactNode) {\n function Icon(props: DetailIconProps) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={24}\n height={24}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n data-detail-icon={name}\n {...props}\n >\n {children}\n </svg>\n );\n }\n Icon.displayName = `DetailIcon(${name})`;\n return Icon;\n}\n\nexport const AlertCircleIcon = glyph(\n \"alert-circle\",\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" />\n <line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />\n </>,\n);\n\nexport const ExternalLinkIcon = glyph(\n \"external-link\",\n <>\n <path d=\"M15 3h6v6\" />\n <path d=\"M10 14 21 3\" />\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n </>,\n);\n\nexport const HistoryIcon = glyph(\n \"history\",\n <>\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n <path d=\"M12 7v5l4 2\" />\n </>,\n);\n\nexport const Link2Icon = glyph(\n \"link-2\",\n <>\n <path d=\"M9 17H7A5 5 0 0 1 7 7h2\" />\n <path d=\"M15 7h2a5 5 0 1 1 0 10h-2\" />\n <line x1=\"8\" x2=\"16\" y1=\"12\" y2=\"12\" />\n </>,\n);\n\nexport const RefreshCwIcon = glyph(\n \"refresh-cw\",\n <>\n <path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" />\n <path d=\"M21 3v5h-5\" />\n <path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" />\n <path d=\"M8 16H3v5\" />\n </>,\n);\n\nconst SHIELD =\n \"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\";\n\nexport const ShieldAlertIcon = glyph(\n \"shield-alert\",\n <>\n <path d={SHIELD} />\n <path d=\"M12 8v4\" />\n <path d=\"M12 16h.01\" />\n </>,\n);\n\nexport const ShieldCheckIcon = glyph(\n \"shield-check\",\n <>\n <path d={SHIELD} />\n <path d=\"m9 12 2 2 4-4\" />\n </>,\n);\n\nexport const AppWindowIcon = glyph(\n \"app-window\",\n <>\n <rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\" />\n <path d=\"M10 4v4\" />\n <path d=\"M2 8h20\" />\n <path d=\"M6 4v4\" />\n </>,\n);\n\nexport const CheckIcon = glyph(\"check\", <path d=\"M20 6 9 17l-5-5\" />);\n\nexport const ChevronLeftIcon = glyph(\"chevron-left\", <path d=\"m15 18-6-6 6-6\" />);\n\nexport const ChevronRightIcon = glyph(\"chevron-right\", <path d=\"m9 18 6-6-6-6\" />);\n\nexport const CopyIcon = glyph(\n \"copy\",\n <>\n <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </>,\n);\n\nexport const ExpandIcon = glyph(\n \"expand\",\n <>\n <path d=\"m15 15 6 6\" />\n <path d=\"m15 9 6-6\" />\n <path d=\"M21 16v5h-5\" />\n <path d=\"M21 8V3h-5\" />\n <path d=\"M3 16v5h5\" />\n <path d=\"m3 21 6-6\" />\n <path d=\"M3 8V3h5\" />\n <path d=\"M9 9 3 3\" />\n </>,\n);\n\nexport const MoreHorizontalIcon = glyph(\n \"more-horizontal\",\n <>\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n </>,\n);\n\nexport const PanelRightIcon = glyph(\n \"panel-right\",\n <>\n <rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />\n <path d=\"M15 3v18\" />\n </>,\n);\n\nexport const LoaderIcon = glyph(\"loader\", <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />);\n\nexport const Settings2Icon = glyph(\n \"settings-2\",\n <>\n <path d=\"M14 17H5\" />\n <path d=\"M19 7h-9\" />\n <circle cx=\"17\" cy=\"17\" r=\"3\" />\n <circle cx=\"7\" cy=\"7\" r=\"3\" />\n </>,\n);\n","// @ai-matrx/detail — src/react/DetailBody.tsx\n//\n// The fixed sections, in the order every record shows them:\n// source health strip (synced records only) · about · fields · custom fields\n// (the host's port, standard records only) · associations · history · a\n// registration's extra sections\n// A section that does not apply is ABSENT — never an empty box, never a\n// disabled-looking control. Every load state says what happened.\n\n\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn, ErrorActions, Skeleton } from \"@ai-matrx/design-system\";\nimport {\n ASSOCIATION_TARGET_TYPES,\n isEntityTypeToken,\n type AssociationTargetType,\n} from \"@ai-matrx/associations\";\nimport { AssociationCardGrid, PrimaryEntityProvider } from \"@ai-matrx/associations/react\";\nimport { formatDurationMs } from \"@ai-matrx/kit/format\";\n\nimport { formatWhen } from \"../format\";\nimport type { DetailHistoryEntry, DetailSection, DetailSourceHealth } from \"../types\";\nimport { DetailRecordMeta } from \"./DetailHeader\";\nimport { DetailPresentationPane } from \"./DetailPresentationPane\";\nimport { useDetailHost } from \"./host\";\nimport {\n AlertCircleIcon,\n ExternalLinkIcon,\n HistoryIcon,\n Link2Icon,\n RefreshCwIcon,\n ShieldAlertIcon,\n ShieldCheckIcon,\n} from \"./icons\";\nimport type { DetailCore } from \"./useDetailCore\";\n\n/**\n * 🚨 D5 — THE SKELETON HAS A BOUNDED WAIT. A load that never answers used to\n * shimmer for ~20 seconds and then drop an error, with nothing in between\n * saying anything was wrong. After this many milliseconds the skeleton says,\n * in plain words, that it is still waiting — the stand-in announcing itself\n * (law 4) rather than pretending progress. A patience threshold, not a ceiling\n * or a quota: it buys nothing and spends nothing, so it is a constant here and\n * not a knob.\n */\nconst SLOW_LOAD_NOTICE_MS = 6000;\n\n/** True once a load has been running longer than a person expects to wait. */\nfunction useSlowLoadNotice(active: boolean, key: string): boolean {\n const [slow, setSlow] = useState(false);\n useEffect(() => {\n if (!active) {\n setSlow(false);\n return undefined;\n }\n setSlow(false);\n const timer = setTimeout(() => setSlow(true), SLOW_LOAD_NOTICE_MS);\n return () => clearTimeout(timer);\n }, [active, key]);\n return slow;\n}\n\nfunction SectionHeading({ icon, children }: { icon?: ReactNode; children: ReactNode }) {\n return (\n <h3 className=\"flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n {icon}\n {children}\n </h3>\n );\n}\n\nfunction Notice({\n tone,\n children,\n}: {\n tone: \"muted\" | \"warn\" | \"error\";\n children: ReactNode;\n}) {\n return (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <AlertCircleIcon\n className={cn(\n \"h-6 w-6\",\n tone === \"error\" && \"text-destructive\",\n tone === \"warn\" && \"text-primary\",\n tone === \"muted\" && \"text-muted-foreground/60\",\n )}\n />\n <p className=\"max-w-prose text-sm text-muted-foreground\">{children}</p>\n </div>\n );\n}\n\n// ─── Source health strip ────────────────────────────────────────────────────\n\n/**\n * The state in one word, for every grant but `ok`. `blocked` is the word for\n * \"the source refuses and nothing you can click repairs it\" (chair, 2026-09-18):\n * the strip says it, states the reason, and shows no Reconnect.\n */\nconst GRANT_WORD: Record<DetailSourceHealth[\"grant\"], string> = {\n ok: \"OK\",\n expired: \"Expired\",\n revoked: \"Revoked\",\n missing: \"Missing\",\n unknown: \"Unknown\",\n blocked: \"Blocked\",\n};\n\n/**\n * 🚨 N14 (VERIFY-U-W1-U-W2) — THE CONTROL THAT LEAVES FOR THE SOURCE NAMES IT.\n * The strip already prints `health.source` beside it, so \"Open at source\" threw\n * away the only word the person recognises. One producer often answers for a\n * whole FAMILY, though (\"Google Docs, Sheets & Drive files\" covers Docs, Sheets\n * and Slides), and \"Open in Google Docs\" on a spreadsheet would be a lie — so a\n * source that lists several things is cut back to the provider word, a short\n * source is used whole, and no source at all keeps the generic phrase rather than\n * letting the screen guess.\n *\n * A registration that knows the row's exact kind sets `health.openAtSourceLabel`\n * instead (a Doc vs. a Sheet vs. a plain Drive file; \"Open in Google Calendar\"\n * for an event) — that field wins over this derivation below, in `HealthStrip`.\n */\nfunction deriveOpenAtSourceLabel(source: string): string {\n const named = source.trim().replace(/\\s+/g, \" \");\n if (!named) return \"Open at source\";\n const listsSeveral = /[,&]/.test(named) || named.split(\" \").length > 3;\n const [firstWord = \"\"] = named.split(\" \");\n const place = listsSeveral ? firstWord.replace(/[,&]+$/, \"\") : named;\n return place ? `Open in ${place}` : \"Open at source\";\n}\n\nfunction HealthStrip({ health }: { health: DetailSourceHealth }) {\n const ok = health.grant === \"ok\";\n const [refreshing, setRefreshing] = useState(false);\n const host = useDetailHost();\n const refresh = async () => {\n if (!health.onRefresh) return;\n setRefreshing(true);\n try {\n await health.onRefresh();\n } catch (error: unknown) {\n host.notify.error(\n `Refresh from ${health.source} failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n } finally {\n setRefreshing(false);\n }\n };\n return (\n <div\n className={cn(\n \"flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border/60 px-4 py-2 text-xs\",\n ok ? \"bg-muted/40 text-muted-foreground\" : \"bg-destructive/5 text-foreground\",\n )}\n data-detail-health\n >\n {ok ? (\n <ShieldCheckIcon className=\"h-3.5 w-3.5 text-primary\" />\n ) : (\n <ShieldAlertIcon className=\"h-3.5 w-3.5 text-destructive\" />\n )}\n <span className=\"font-medium\">{health.source}</span>\n {ok ? null : (\n <span\n className=\"rounded bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-destructive\"\n data-detail-health-state={health.grant}\n >\n {GRANT_WORD[health.grant]}\n </span>\n )}\n {health.lastRefreshedAt ? (\n <span>Refreshed {formatWhen(health.lastRefreshedAt)}</span>\n ) : (\n <span>Never refreshed</span>\n )}\n {health.grantDetail ? <span>{health.grantDetail}</span> : null}\n <span className=\"ml-auto flex items-center gap-1\">\n {health.onRefresh ? (\n <button\n type=\"button\"\n onClick={() => void refresh()}\n disabled={refreshing}\n 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\"\n >\n <RefreshCwIcon className={cn(\"h-3 w-3\", refreshing && \"animate-spin\")} />\n Refresh\n </button>\n ) : null}\n {!ok && health.onReconnect ? (\n <button\n type=\"button\"\n onClick={health.onReconnect}\n 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\"\n >\n Reconnect\n </button>\n ) : null}\n {health.openAtSourceHref ? (\n <a\n href={health.openAtSourceHref}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex h-6 items-center gap-1 rounded px-1.5 hover:bg-accent hover:text-foreground pointer-coarse:h-10\"\n >\n <ExternalLinkIcon className=\"h-3 w-3\" />\n {health.openAtSourceLabel ?? deriveOpenAtSourceLabel(health.source)}\n </a>\n ) : null}\n </span>\n </div>\n );\n}\n\n// ─── Fields ─────────────────────────────────────────────────────────────────\n\n/**\n * 🚨 NEW-17 (VERIFY-U-P1-R4) — THE SENTENCE IS DERIVED FROM THE CONTROLS THAT\n * RENDERED, NEVER A FIXED STRING. \"The controls above still open it where it\n * lives, and copy its id\" was printed for `session` — a registered type with no\n * `entityToken`, whose header therefore shows no door at all — and for every\n * unregistered type. The copy control is unconditional in `DetailActions`, so\n * that half is always true; the open half is claimed only when the record really\n * has a door (`core.canOpenElsewhere`).\n */\nfunction whatTheControlsDo(core: DetailCore): string {\n return core.canOpenElsewhere\n ? \"The controls above still open it where it lives, and copy its id.\"\n : \"The controls above copy its id, so you can find it wherever it came from.\";\n}\n\nfunction FieldsSection({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const RefCell = host.doors.RefCell;\n const label = core.recordType?.label.toLowerCase() ?? \"record\";\n\n if (!core.recordType) {\n // 🚨 NEW-9 — THE PERSON'S LANGUAGE, NEVER A REPO PATH. This sentence named a\n // source file and told a brilliant non-technical expert to edit a registry.\n // The remedy is a console warning from the core, where the developer is.\n return (\n <Notice tone=\"warn\">\n {`This is a ${core.ref.type} record. Nothing more about it is stored here yet, so there is ` +\n `nothing else to show. ${whatTheControlsDo(core)}`}\n </Notice>\n );\n }\n if (core.status === \"loading\") {\n return <LoadingFields core={core} label={label} />;\n }\n if (core.status === \"not-found\") {\n // 🚨 THE ABSENT RECORD CARRIES THE HOST'S ERROR ACTIONS TOO. A record that\n // cannot be read (moved, deleted, or invisible under row-level access) is\n // the state a person most needs to hand to someone — so it gets the same\n // host slot every package error gets (`ErrorActions`, bound by the host's\n // `ErrorActionsProvider`; matrx-frontend fills it with its Alchemy menu).\n // It carries the sentence and the record, so the host's copy names WHICH\n // record could not be found. With no provider it renders nothing.\n const sentence = `This ${label} couldn't be found — it may have been moved, deleted, or isn't shared with you.`;\n return (\n <Notice tone=\"warn\">\n {sentence}\n <ErrorActions\n origin=\"detail/DetailBody\"\n operation={`Open this ${label}`}\n message={sentence}\n records={[{ type: core.ref.type, id: core.ref.id }]}\n />\n </Notice>\n );\n }\n if (core.status === \"error\") {\n return (\n <Notice tone=\"error\">\n {`Couldn't load the details for this ${label}. ${core.errorMessage ?? \"\"}`.trim()}\n <ErrorActions origin=\"detail/DetailBody\" operation={`Load this ${label}`} error={core.errorMessage} />\n </Notice>\n );\n }\n if (core.status === \"none\") {\n // 🚨 NEW-1 — THE ABSENT STATE NAMES THE TYPE AND THE REMEDY. This branch is\n // \"nothing is registered to LOAD a record of this type\", which is not the\n // same as \"this record has no extra fields\", and it used to read as the\n // latter under an invented title (VERIFY-U-P1-R2).\n return (\n <Notice tone=\"muted\">\n {`Nothing more about this ${label} is stored here, so there is nothing else to show. ` +\n whatTheControlsDo(core)}\n </Notice>\n );\n }\n if (core.fields.length === 0) {\n return <p className=\"text-sm text-muted-foreground\">No additional fields to show.</p>;\n }\n return (\n <dl className=\"grid grid-cols-1 gap-x-4 gap-y-2.5\">\n {core.fields.map((field) => (\n <div\n key={field.key}\n className=\"flex flex-col gap-0.5 border-b border-border/40 pb-2 last:border-b-0\"\n >\n <dt className=\"text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n {field.label}\n </dt>\n <dd\n className={cn(\n \"break-words text-sm text-foreground\",\n !field.ref &&\n field.mono &&\n \"whitespace-pre-wrap rounded-md bg-muted px-2 py-1 font-mono text-xs\",\n )}\n >\n {field.ref ? (\n // 🚨 THE DOOR SAYS WHAT IT OPENS (chair, 2026-09-18). This rendered\n // `field.ref.id` alone, so a field whose text was \"Brightwater Robotics\"\n // printed a truncated uuid. The ref's own `name` wins; otherwise the\n // field's text, unless that text IS the id and says nothing more.\n <RefCell\n value={field.ref.id}\n label={field.label}\n token={field.ref.token}\n name={field.ref.name ?? (field.text === field.ref.id ? null : field.text)}\n />\n ) : (\n field.text\n )}\n </dd>\n </div>\n ))}\n </dl>\n );\n}\n\n/** The skeleton, plus the honest line once the wait has gone long (D5). */\nfunction LoadingFields({ core, label }: { core: DetailCore; label: string }) {\n const slow = useSlowLoadNotice(true, `${core.ref.type}:${core.ref.id}`);\n return (\n <div className=\"space-y-3\" aria-busy=\"true\" aria-label={`Loading ${label} details`}>\n {slow ? (\n <p className=\"text-xs text-muted-foreground\" data-detail-slow-load>\n Still loading this {label} — the read has been running for more than{\" \"}\n {formatDurationMs(SLOW_LOAD_NOTICE_MS, { style: \"long\" })}. If nothing appears, the\n record may be unreachable from here; closing and reopening it starts a fresh read.\n </p>\n ) : null}\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className=\"space-y-1.5\">\n <Skeleton className=\"h-2.5 w-20\" />\n <Skeleton className={cn(\"h-4\", i % 2 ? \"w-3/4\" : \"w-1/2\")} />\n </div>\n ))}\n </div>\n );\n}\n\n// ─── Associations ───────────────────────────────────────────────────────────\n\nfunction isAssociationTarget(token: string): token is AssociationTargetType {\n return (ASSOCIATION_TARGET_TYPES as readonly string[]).includes(token);\n}\n\nfunction AssociationsSection({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const token = core.entityToken;\n if (!token || !isAssociationTarget(token) || !host.associations.canAnchor(token)) return null;\n if (core.recordType?.associationTokens === null) return null;\n const wanted = core.recordType?.associationTokens ?? host.associations.defaultTokens;\n const tokens = wanted.filter((t) => t !== token).filter(isEntityTypeToken);\n if (tokens.length === 0) return null;\n return (\n <section className=\"space-y-2\" data-detail-section=\"associations\">\n <SectionHeading icon={<Link2Icon className=\"h-3 w-3\" />}>Linked</SectionHeading>\n <PrimaryEntityProvider value={{ type: token, id: core.ref.id, label: core.title }}>\n <AssociationCardGrid tokens={tokens} />\n </PrimaryEntityProvider>\n </section>\n );\n}\n\n// ─── History ────────────────────────────────────────────────────────────────\n\n/**\n * The columns a change's author is spelled as, in the order the map is asked.\n * `version_list` returns `actor_id`; the others are what sibling history shapes\n * on this platform carry.\n */\nconst ACTOR_COLUMNS = [\"actor_id\", \"user_id\", \"person_id\", \"created_by_id\"] as const;\n\ntype HistoryState =\n | { status: \"loading\" }\n | { status: \"ready\"; entries: DetailHistoryEntry[] }\n | { status: \"error\"; message: string };\n\nfunction HistorySection({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const token = core.entityToken;\n const wanted = core.recordType?.history ?? Boolean(token);\n const [state, setState] = useState<HistoryState>({ status: \"loading\" });\n const lastKey = useRef<string | null>(null);\n // The port function, not the host object: the effect must re-run for a new\n // record, never for a re-render.\n const listHistory = host.history.list;\n const recordId = core.ref.id;\n\n useEffect(() => {\n if (!wanted || !token) return undefined;\n const key = `${token}:${recordId}`;\n if (lastKey.current !== key) {\n lastKey.current = key;\n setState({ status: \"loading\" });\n }\n const controller = new AbortController();\n void listHistory(token, recordId, controller.signal)\n .then((entries) => {\n if (!controller.signal.aborted) setState({ status: \"ready\", entries });\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n setState({\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n });\n return () => controller.abort();\n }, [listHistory, wanted, token, recordId]);\n\n if (!wanted || !token) return null;\n const ActorCell = host.doors.RefCell;\n // 🚨 NEW-23 (VERIFY-U-P1-R4) — WHO MADE THE CHANGE IS NEVER DROPPED. The row\n // was rendered only when `tokenFromColumnName(\"actor_id\")` named a token, and\n // it names none (no `actor` or `user` token has a door in this platform's\n // registry today), so every history row silently threw away the id\n // `version_list` hands over. The map is asked for each column an actor is\n // spelled as — the first that has a door wins — and when none does the value is\n // shown as itself under an honest label, because an identity the data names is\n // never nothing (law 4 + the no-dead-ends class).\n const actorToken = ACTOR_COLUMNS.map((column) => host.doors.tokenFromColumnName(column)).find(\n (candidate): candidate is string => Boolean(candidate),\n );\n // 🚨 NEW-23 (VERIFY-U-P1-R5) — AND THE HONEST FALLBACK WAS STILL A BARE UUID\n // ON EVERY ROW. Nothing was silent, but no host registry gives `actor_id` a\n // door, so \"Changed by 8f3c…-…\" was what a brilliant non-technical expert read\n // on every change of every record. The host's ONE identity resolver answers\n // first now (`history.ActorName`); the door is next; the id itself is last and\n // says, in its title, that we could not find the person.\n const ActorName = host.history.ActorName ?? null;\n\n return (\n <section className=\"space-y-2\" data-detail-section=\"history\">\n <SectionHeading icon={<HistoryIcon className=\"h-3 w-3\" />}>History</SectionHeading>\n {state.status === \"loading\" ? (\n <div className=\"space-y-1.5\" aria-busy=\"true\" aria-label=\"Loading history\">\n <Skeleton className=\"h-3.5 w-2/3\" />\n <Skeleton className=\"h-3.5 w-1/2\" />\n </div>\n ) : state.status === \"error\" ? (\n <p className=\"text-xs text-muted-foreground\">\n {state.message.toLowerCase().includes(\"access denied\")\n ? \"History is only shown to people who can view this record.\"\n : `History could not be read: ${state.message}`}\n <ErrorActions origin=\"detail/DetailBody\" operation=\"Read the history\" error={state.message} />\n </p>\n ) : state.entries.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">No recorded changes yet.</p>\n ) : (\n <ol className=\"space-y-1\">\n {state.entries.map((entry) => (\n <li\n key={`${entry.version}-${entry.occurredAt}`}\n className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs\"\n >\n <span className=\"rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground\">\n v{entry.version}\n </span>\n <span className=\"capitalize text-foreground\">{entry.operation.toLowerCase()}</span>\n <span className=\"text-muted-foreground\">{formatWhen(entry.occurredAt)}</span>\n {entry.isCurrent ? (\n <span className=\"text-[10px] uppercase tracking-wide text-primary\">current</span>\n ) : null}\n {entry.actorId ? (\n ActorName ? (\n <span className=\"text-muted-foreground\" data-detail-history-actor-person>\n Changed by <ActorName actorId={entry.actorId} row={core.row} />\n </span>\n ) : actorToken ? (\n <span className=\"inline-flex items-center\" data-detail-history-actor>\n <ActorCell value={entry.actorId} label=\"Changed by\" token={actorToken} />\n </span>\n ) : (\n <span className=\"text-muted-foreground\" data-detail-history-actor-raw>\n Changed by{\" \"}\n <span\n className=\"font-mono text-[10px]\"\n title=\"We could not find the person behind this id — this host has bound no identity resolver.\"\n >\n {entry.actorId}\n </span>\n </span>\n )\n ) : (\n <span className=\"text-muted-foreground\" data-detail-history-actor-absent>\n No person recorded for this change\n </span>\n )}\n </li>\n ))}\n </ol>\n )}\n </section>\n );\n}\n\n// ─── Custom fields (the host's port) ───────────────────────────────────────\n\n/**\n * The organization's own fields on a standard record, through the ONE port the\n * host binds (`customFields.Section`). Absent when the record has no entity\n * token or the host binds no section — a record type that takes no custom\n * fields shows nothing rather than an empty box; the section itself says why\n * when a standard record cannot show any.\n */\nfunction CustomFieldsPort({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const Section = host.customFields?.Section;\n const token = core.entityToken;\n if (!Section || !token) return null;\n return <Section token={token} id={core.ref.id} />;\n}\n\n// ─── Body ───────────────────────────────────────────────────────────────────\n\nexport function DetailBody({ core }: { core: DetailCore }) {\n const extra: DetailSection[] =\n core.recordType?.extraSections && core.frameCtx\n ? core.recordType.extraSections(core.row, core.frameCtx)\n : [];\n const Frame = core.recordType?.Frame ?? null;\n\n const body = (\n // `min-h-full` so a Frame's right-click menu answers anywhere in the body,\n // not only on the rows — a short dossier otherwise leaves a dead band.\n <div className=\"flex min-h-full flex-col\">\n <DetailRecordMeta core={core} />\n {core.health ? <HealthStrip health={core.health} /> : null}\n {core.about ? (\n <p className=\"line-clamp-3 border-b border-border/60 p-4 text-xs leading-snug text-muted-foreground\">\n {core.about}\n </p>\n ) : null}\n <div className=\"space-y-6 p-4\">\n <FieldsSection core={core} />\n {core.status === \"ready\" || core.status === \"none\" ? (\n <>\n <CustomFieldsPort core={core} />\n <AssociationsSection core={core} />\n <HistorySection core={core} />\n {extra.map((section) => (\n <section key={section.id} className=\"space-y-2\" data-detail-section={section.id}>\n <SectionHeading>{section.label}</SectionHeading>\n {section.content}\n </section>\n ))}\n </>\n ) : null}\n {/* Last, quiet, and on every load state: how details open is a setting\n the person can change from right here (and the one caller of the\n keyboard model's `registerSave`). */}\n <DetailPresentationPane core={core} />\n </div>\n </div>\n );\n\n if (Frame && core.frameCtx) return <Frame ctx={core.frameCtx}>{body}</Frame>;\n return body;\n}\n","// @ai-matrx/detail — src/format.ts\n//\n// Generic row → fields formatting, lifted verbatim from the item detail\n// window it replaced so every registry-known record renders exactly as it did.\n\nimport type { DetailField } from \"./types\";\nimport { humanizeIdentifier } from \"@ai-matrx/kit/text-case\";\n\n/** Columns that are pure plumbing — hidden from a formatted field list. */\nexport const HIDDEN_FIELDS = new Set([\n \"id\",\n \"user_id\",\n \"organization_id\",\n \"created_by\",\n \"updated_by\",\n \"embedding\",\n \"search_vector\",\n \"tsv\",\n]);\n\nconst ISO_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n\nexport function formatValue(value: unknown): { text: string; mono?: boolean } | null {\n if (value === null || value === undefined) return null;\n if (typeof value === \"boolean\") return { text: value ? \"Yes\" : \"No\" };\n if (typeof value === \"number\") {\n return { text: Number.isFinite(value) ? value.toLocaleString() : String(value) };\n }\n if (typeof value === \"string\") {\n const t = value.trim();\n if (!t) return null;\n if (ISO_RE.test(t)) {\n const d = new Date(t);\n if (!Number.isNaN(d.getTime())) {\n return { text: d.toLocaleString([], { dateStyle: \"medium\", timeStyle: \"short\" }) };\n }\n }\n return { text: t };\n }\n try {\n const json = JSON.stringify(value, null, 2);\n if (!json || json === \"{}\" || json === \"[]\") return null;\n return { text: json, mono: true };\n } catch {\n return { text: String(value), mono: true };\n }\n}\n\n/**\n * Every populated scalar column of a row as a field; a `<token>_id` column\n * holding a real uuid becomes a door (the host decides which token names a\n * registry entity — a wrong link is worse than no link).\n */\nexport function fieldsFromRow(\n row: Record<string, unknown>,\n doors: {\n tokenFromColumnName: (column: string) => string | null;\n isUuidValue: (value: unknown) => value is string;\n },\n): DetailField[] {\n const out: DetailField[] = [];\n for (const [key, raw] of Object.entries(row)) {\n if (HIDDEN_FIELDS.has(key)) continue;\n const formatted = formatValue(raw);\n if (!formatted) continue;\n const token = doors.isUuidValue(raw) ? doors.tokenFromColumnName(key) : null;\n out.push({\n key,\n label: humanizeIdentifier(key) || key,\n text: formatted.text,\n ...(formatted.mono ? { mono: true } : {}),\n ref: token ? { token, id: raw as string } : null,\n });\n }\n return out;\n}\n\nexport function formatWhen(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleString([], { dateStyle: \"medium\", timeStyle: \"short\" });\n}\n","// @ai-matrx/detail — src/react/DetailPresentationPane.tsx\n//\n// \"How record details open\" — the ONE screen that WRITES\n// `ui.detail.default_presentation`, and the per-record-type override beside\n// it. It sits at the foot of every detail, quiet and collapsed, the way\n// Notion carries \"Open pages in\" inside the peek itself rather than in a\n// settings app three clicks away.\n//\n// 🚨 IT IS ALSO THE SURFACE THAT EXERCISES Cmd/Ctrl+Enter. The keyboard model\n// has always offered `registerSave`, and until this pane existed NOTHING in\n// the repo registered one, so the chord saved nothing anywhere (VERIFY-U-P1).\n// No record type on this branch has an editable body, so the contract is held\n// by the one editable thing a detail owns: this setting. The moment a record\n// type ships an editor it registers its own save the same way and this pane\n// stops being the only caller.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn, ErrorActions } from \"@ai-matrx/design-system\";\n\nimport { DETAIL_PRESENTATIONS, type DetailPresentation } from \"../types\";\nimport { useDetailHost } from \"./host\";\nimport { CheckIcon, LoaderIcon, Settings2Icon } from \"./icons\";\nimport type { DetailCore } from \"./useDetailCore\";\n\nconst WORD: Record<DetailPresentation, string> = {\n window: \"a window\",\n docked: \"a docked panel\",\n page: \"a full page\",\n};\n\nconst LABEL: Record<DetailPresentation, string> = {\n window: \"Window\",\n docked: \"Docked\",\n page: \"Page\",\n};\n\ntype SaveState =\n | { status: \"idle\" }\n | { status: \"saving\" }\n | { status: \"saved\" }\n | { status: \"refused\"; reason: string };\n\nexport function DetailPresentationPane({ core }: { core: DetailCore }) {\n const host = useDetailHost();\n const save = host.savePresentation;\n const setting = host.usePresentationSetting(core.ref.type);\n const [choice, setChoice] = useState<DetailPresentation | null>(null);\n const [forThisType, setForThisType] = useState(false);\n const [open, setOpen] = useState(false);\n const [state, setState] = useState<SaveState>({ status: \"idle\" });\n\n const effective = setting.value ?? core.presentation;\n const pending = choice ?? effective;\n const registerSave = core.keyboard.registerSave;\n /** This record type has its own exception — so it can be taken back (NEW-2). */\n const hasTypeException = setting.forType !== undefined;\n\n const commit = async () => {\n if (!save) return;\n setState({ status: \"saving\" });\n const result = await save({\n presentation: pending,\n forType: forThisType ? core.ref.type : null,\n });\n if (result.ok) {\n setState({ status: \"saved\" });\n host.notify.success(\n forThisType\n ? `Every ${core.typeLabel.toLowerCase()} now opens as ${WORD[pending]} for you.`\n : `Record details now open as ${WORD[pending]} for you.`,\n );\n } else {\n setState({ status: \"refused\", reason: result.reason });\n }\n };\n\n /**\n * After a REMOVAL, what governs now — read from the ladder, not from the\n * cached value this write just made stale (NEW-10). The sentence names the\n * rung that actually answers, or says plainly that it could not be read.\n */\n const sentenceAfterRemoval = async (): Promise<string> => {\n const reRead = host.reReadPresentation;\n if (!reRead) {\n return (\n `Your own exception for ${core.typeLabel.toLowerCase()} records is gone. How they open now is ` +\n \"whatever your organization and your own default say — open one to see.\"\n );\n }\n try {\n const effective = await reRead(core.ref.type);\n if (effective.forType) {\n return (\n `Your own exception is gone, but ${core.typeLabel.toLowerCase()} records still open as ` +\n `${WORD[effective.forType]} because your organization sets that for everyone.`\n );\n }\n if (effective.value) {\n return `${core.typeLabel} records now open as ${WORD[effective.value]}, the way you normally open records.`;\n }\n return (\n `Your own exception for ${core.typeLabel.toLowerCase()} records is gone. The setting that ` +\n \"governs them now could not be read, so open one to see how it opens.\"\n );\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n return (\n `Your own exception for ${core.typeLabel.toLowerCase()} records is gone, but what governs them ` +\n `now could not be read (${message}), so open one to see how it opens.`\n );\n }\n };\n\n /**\n * 🚨 NEW-2 — TAKING THE EXCEPTION BACK. Setting \"only for file records\" wrote\n * one entry of a json map and nothing in the product removed it: the only\n * escape was editing that json in the generic settings row. This clears the\n * entry through the same write, so the type goes back to opening the way the\n * person (or their organization) normally opens records.\n */\n const clearForType = async () => {\n if (!save) return;\n setState({ status: \"saving\" });\n const result = await save({ presentation: pending, forType: core.ref.type, clear: true });\n if (result.ok) {\n setState({ status: \"saved\" });\n // 🚨 NEW-10 — NEVER \"they now open your way\" ON THIS WRITE'S OWN WORD. With\n // an ORGANIZATION exception for the same type still in place, the removal\n // succeeds and changes nothing a person can see: the organization's entry\n // wins on the next open. The ladder is re-read and the sentence names\n // whichever rung answers now.\n host.notify.success(await sentenceAfterRemoval());\n } else {\n setState({ status: \"refused\", reason: result.reason });\n }\n };\n\n // Cmd/Ctrl+Enter saves whatever is pending here, from anywhere in the detail.\n // The registration is a stable function reading the LATEST commit through a\n // ref (the same shape `useDetailKeyboard` uses for its handlers), so moving\n // the choice does not re-register, and the chord is registered only while\n // this pane is open — a record type's own editor always wins when one exists.\n const commitRef = useRef(commit);\n commitRef.current = commit;\n useEffect(() => {\n if (!open || !save) return undefined;\n return registerSave(() => commitRef.current());\n }, [open, save, registerSave]);\n\n // The host has no writable setting bound (a client that reads the platform\n // setting but cannot write it). Absent, never a disabled-looking control.\n if (!save) return null;\n\n if (!open) {\n // An unread setting is an error sentence, not a button label: it is said in\n // its own line with the host's actions inside it (never a control inside\n // the row's button), and the row keeps only \"Change\".\n return (\n <div className=\"w-full\">\n {setting.error ? (\n <p className=\"px-1 pt-1 text-[11px] text-muted-foreground\">\n {`Your \"open details as\" setting could not be read (${setting.error}) — details are opening as ${WORD[core.presentation]}.`}\n <ErrorActions origin=\"detail/DetailPresentationPane\" operation=\"Read the open-details-as setting\" error={setting.error} />\n </p>\n ) : null}\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n 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\"\n data-detail-presentation-pane=\"collapsed\"\n >\n <Settings2Icon className=\"h-3 w-3 shrink-0\" />\n {setting.error ? null : <span>{`Details open as ${WORD[effective]}.`}</span>}\n <span className=\"underline underline-offset-2\">Change</span>\n </button>\n </div>\n );\n }\n\n return (\n <section\n className=\"space-y-2 rounded-md border border-border bg-muted/30 p-3\"\n data-detail-presentation-pane=\"open\"\n >\n <h3 className=\"flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n <Settings2Icon className=\"h-3 w-3\" />\n How record details open\n </h3>\n <div className=\"flex flex-wrap gap-1\" role=\"group\" aria-label=\"Open record details as\">\n {DETAIL_PRESENTATIONS.map((value) => (\n <button\n key={value}\n type=\"button\"\n aria-pressed={pending === value}\n onClick={() => {\n setChoice(value);\n setState({ status: \"idle\" });\n }}\n className={cn(\n \"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\",\n pending === value\n ? \"bg-primary text-primary-foreground\"\n : \"bg-card text-muted-foreground hover:text-foreground\",\n )}\n >\n {LABEL[value]}\n </button>\n ))}\n </div>\n {hasTypeException ? (\n <button\n type=\"button\"\n onClick={() => void clearForType()}\n disabled={state.status === \"saving\"}\n className=\"text-left text-[11px] text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground disabled:opacity-60\"\n data-detail-presentation-clear-type\n >\n {`Use the default for ${core.typeLabel.toLowerCase()} records`}\n </button>\n ) : null}\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={forThisType}\n onChange={(event) => {\n setForThisType(event.target.checked);\n setState({ status: \"idle\" });\n }}\n className=\"h-3.5 w-3.5 accent-[var(--color-primary)]\"\n />\n Only for {core.typeLabel.toLowerCase()} records\n </label>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => void commit()}\n disabled={state.status === \"saving\"}\n 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\"\n >\n {state.status === \"saving\" ? (\n <LoaderIcon className=\"h-3 w-3 animate-spin\" />\n ) : state.status === \"saved\" ? (\n <CheckIcon className=\"h-3 w-3\" />\n ) : null}\n Save\n </button>\n <span className=\"text-[11px] text-muted-foreground\">\n {state.status === \"saved\" ? \"Saved. It applies to the next record you open.\" : \"⌘/Ctrl + Enter\"}\n </span>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"ml-auto text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground\"\n >\n Close\n </button>\n </div>\n {state.status === \"refused\" ? (\n <p className=\"text-xs text-destructive\" data-detail-presentation-refusal>\n {state.reason}\n <ErrorActions origin=\"detail/DetailPresentationPane\" operation=\"Change how details open\" message={state.reason} />\n </p>\n ) : null}\n {setting.error ? (\n <p className=\"text-xs text-muted-foreground\">\n The saved setting could not be read here ({setting.error}), so this shows what this detail\n is actually doing.\n <ErrorActions origin=\"detail/DetailPresentationPane\" operation=\"Read the open-details-as setting\" error={setting.error} />\n </p>\n ) : null}\n </section>\n );\n}\n","// @ai-matrx/detail — src/react/presentations.tsx\n//\n// The wrapper: ONE core, three presentations. Each component hands the same\n// header halves and the same body to a different host shell. Nothing here\n// draws chrome — the shells are the host's window manager, its docked side\n// panel and its route header; this file only decides what goes in each slot.\n\n\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\n\nimport { DetailBody } from \"./DetailBody\";\nimport { DetailActions, DetailTitle, useDetailMenuActions } from \"./DetailHeader\";\nimport { useDetailCore, type DetailCore } from \"./useDetailCore\";\nimport { requireShell, useDetailHost } from \"./host\";\nimport { detailInstanceKey } from \"../presentation\";\nimport type { DetailInstanceData } from \"../types\";\n\n/**\n * 🚨 NEW-22 (VERIFY-U-P1-R4) — THE KEYBOARD MODEL IS BOUND ON EVERY SLOT THE\n * PRESENTATION FILLS, NOT ONLY THE BODY.\n *\n * `titleNode` and `actions` are handed to the SHELL and render outside the body's\n * root, and no shell in this app handles a keystroke: with focus on the copy-id\n * button, a presentation icon, a previous/next chevron or the overflow trigger,\n * Escape closed nothing in all three desktop presentations (reproduced in each).\n * On a phone the docked and window presentations are a `vaul` Drawer, which\n * handles Escape itself — which is why it hid. The header is part of the detail,\n * so the same handler answers from it.\n *\n * Capture phase on each slot, so two open details never answer one keystroke.\n */\nfunction KeyboardSlot({ core, children }: { core: DetailCore; children: ReactNode }) {\n return (\n <div\n onKeyDownCapture={core.keyboard.rootProps.onKeyDownCapture}\n className=\"flex min-w-0 flex-1 items-center\"\n data-detail-keyboard-slot\n >\n {children}\n </div>\n );\n}\n\n/** The same, for the actions cluster — which must not grow or stretch. */\nfunction KeyboardActionsSlot({ core, children }: { core: DetailCore; children: ReactNode }) {\n return (\n <div\n onKeyDownCapture={core.keyboard.rootProps.onKeyDownCapture}\n className=\"flex shrink-0 items-center\"\n data-detail-keyboard-slot=\"actions\"\n >\n {children}\n </div>\n );\n}\n\n/**\n * The keyboard-owning root. Focus lands here on open (and on every record\n * change) so Escape / [ / ] answer immediately, the way a peek does in Linear.\n */\nfunction KeyboardRoot({ core, children }: { core: DetailCore; children: ReactNode }) {\n const rootRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n // Do not steal focus from a field the person is already typing in.\n const active = document.activeElement;\n if (active instanceof HTMLElement && active !== document.body && root.contains(active)) return;\n root.focus({ preventScroll: true });\n }, [core.ref.id, core.ref.type]);\n return (\n <div\n ref={rootRef}\n {...core.keyboard.rootProps}\n className=\"flex min-h-full flex-1 flex-col outline-none\"\n data-detail-root\n data-detail-type={core.ref.type}\n data-detail-presentation={core.presentation}\n >\n {children}\n </div>\n );\n}\n\nexport interface DetailPresentationProps {\n data: DetailInstanceData;\n onClose: () => void;\n}\n\nexport function DetailWindowPresentation({ data, onClose }: DetailPresentationProps) {\n const host = useDetailHost();\n const core = useDetailCore(data, \"window\", { onClose });\n const menuActions = useDetailMenuActions(core);\n const Shell = requireShell(host.shells, \"Window\");\n return (\n <Shell\n instanceKey={detailInstanceKey(core.ref)}\n target={core.ref}\n list={core.list.context}\n title={core.title}\n titleNode={\n <KeyboardSlot core={core}>\n <DetailTitle core={core} />\n </KeyboardSlot>\n }\n actions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} />\n </KeyboardActionsSlot>\n }\n navActions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} optional=\"none\" />\n </KeyboardActionsSlot>\n }\n menuActions={menuActions}\n onClose={core.close}\n >\n <KeyboardRoot core={core}>\n <DetailBody core={core} />\n </KeyboardRoot>\n </Shell>\n );\n}\n\nexport function DetailDockedPresentation({ data, onClose }: DetailPresentationProps) {\n const host = useDetailHost();\n const core = useDetailCore(data, \"docked\", { onClose });\n const menuActions = useDetailMenuActions(core);\n const Shell = requireShell(host.shells, \"Docked\");\n return (\n <Shell\n instanceKey={detailInstanceKey(core.ref)}\n target={core.ref}\n title={core.title}\n titleNode={\n <KeyboardSlot core={core}>\n <DetailTitle core={core} />\n </KeyboardSlot>\n }\n actions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} />\n </KeyboardActionsSlot>\n }\n navActions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} optional=\"none\" />\n </KeyboardActionsSlot>\n }\n menuActions={menuActions}\n onClose={core.close}\n >\n <KeyboardRoot core={core}>\n <DetailBody core={core} />\n </KeyboardRoot>\n </Shell>\n );\n}\n\n/**\n * 🚨 D1 — THE PAGE OWNS ITS OWN EXIT. It accepts no `onBack` FROM ITS CALLER: the\n * route passes none, and this component is what fills the shell's `onBack` slot,\n * with `core.leave` — so the back chevron, Escape and a presentation switch all\n * leave through the one guarded exit (`canGoBack` → `back()`, otherwise the\n * record's own home). A route that passed its own `router.back()` in here is\n * exactly how a pasted or bookmarked detail link still landed the tab on\n * `about:blank` after round 1's fix (VERIFY-U-P1-R2, D1). N9 (VERIFY-U-P1-R5):\n * this sentence used to read \"It takes no `onBack`\" directly above the line that\n * passes one, which is the sentence a future agent trusts.\n */\nexport function DetailPagePresentation({ data }: { data: DetailInstanceData }) {\n const host = useDetailHost();\n const core = useDetailCore(data, \"page\", {});\n const menuActions = useDetailMenuActions(core);\n const Shell = requireShell(host.shells, \"Page\");\n return (\n <Shell\n target={core.ref}\n title={core.title}\n titleNode={\n <KeyboardSlot core={core}>\n <DetailTitle core={core} />\n </KeyboardSlot>\n }\n actions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} />\n </KeyboardActionsSlot>\n }\n navActions={\n <KeyboardActionsSlot core={core}>\n <DetailActions core={core} optional=\"none\" />\n </KeyboardActionsSlot>\n }\n menuActions={menuActions}\n onClose={core.close}\n onBack={core.leave}\n >\n <KeyboardRoot core={core}>\n <DetailBody core={core} />\n </KeyboardRoot>\n </Shell>\n );\n}\n","// @ai-matrx/detail — src/listContext.ts\n//\n// 🚨 NEW-7 / NEW-12 — THE LIST A RECORD WAS OPENED FROM IS BOUNDED BEFORE IT\n// RIDES ANY URL, BY RECORDS *AND* BY BYTES, IN ONE PLACE.\n//\n// The page presentation carries its list context in the query string\n// (`?l=type.id,…&i=<n>`) and a detail WINDOW carries the same list in its\n// `?panels=detail:…` token, so the arrows and `[` / `]` keep working after a\n// record is opened as a page and after that window is refreshed (NEW-15).\n// Uncapped, a 500-row list produced a >20 KB href — past every practical\n// request-line limit (VERIFY-U-P1-R2, break attempt 5).\n//\n// The record cap is a KNOB, not a constant: every ceiling on this platform is a\n// row an admin can change (`ui.detail.list_context_max_ids`, organization-\n// overridable). But a record count is not a length: at 2000 — the value the\n// knob's own `max_value` used to permit — the query was 84 KB, and even at 200 a\n// 30-character type token gave 13.6 KB (VERIFY-U-P1-R3, NEW-12). So the trim\n// obeys BOTH: the knob's record cap, and `DETAIL_URL_BUDGET_BYTES` measured on\n// the FINAL SERIALIZED URL. Whichever bites first, the result carries the WINDOW\n// AROUND THE CURRENT RECORD — the far ends of a long list are not what the arrows\n// are for — and says the list was trimmed (`trimmedFrom`) rather than presenting\n// the window as the whole thing.\n//\n// 🚨 NEW-19 (VERIFY-U-P1-R4) — AND THE BUDGET IS THE WHOLE URL'S, AFTER THE LAST\n// ESCAPING. It used to be a 6,000-character allowance for the list VALUE alone,\n// measured before `URLSearchParams` re-escaped it: a window deep link that\n// measured 5,992 arrived in the address bar at 7,416, and a detail PAGE that also\n// carried an open window was 13,433 characters — past the 8 KB request line, so\n// the edge answers 414 and the link is dead. A caller now passes what the rest of\n// the address already costs (`reservedBytes`) and a `measure` that returns the\n// FINAL serialized length of the list as that URL will carry it.\n//\n// Nothing here talks to the settings ladder: the host resolves the knob and\n// passes the number in, so the module stays package-shaped.\n\nimport {\n DETAIL_URL_BUDGET_BYTES,\n type DetailListContext,\n type DetailRef,\n} from \"./types\";\n\n/**\n * 🚨 NEW-25 (VERIFY-U-P1-R4) / N4 (VERIFY-U-P1-R5) — THE SEPARATORS ARE ESCAPED\n * WITHOUT A PERCENT, SO NO LATER URL DECODE CAN PUT ONE BACK.\n *\n * `encodeURIComponent` leaves `.` alone and every spelling of a record in a URL\n * splits on the FIRST dot, so `{type: \"gr.ant\", id: \"x.y\"}` came back as\n * `{type: \"gr\", id: \"ant.x.y\"}` — a DIFFERENT record, silently. Round 4 escaped\n * `.` `:` `,` to `%2E` `%3A` `%2C` here, which fixed the `?panels=` token (three\n * escaping layers, three decodes) and did NOT fix the page presentation's `?l=`\n * query: an ordinary query parse — `new URL(...).searchParams`, which is the\n * algorithm the page route's `searchParams` come from — percent-decodes the\n * value BEFORE `decodeListItems` splits it, so `%2E` was a literal `.` again and\n * the dotted token still retargeted the record. An id containing a comma was\n * truncated the same way (VERIFY-U-P1-R5, N4).\n *\n * So the escape carries no `%` at all. A ref part is percent-encoding spelled\n * with `~` as the escape character, over the alphabet `A-Za-z0-9-_~`: it holds\n * no `.`, `:`, `,` or `%`, which makes it invariant under any number of\n * percent-decodes and the split unambiguous on every layer. `~` itself is\n * escaped first (`%7E` → `~7E`), so the grammar is closed.\n */\nconst PERCENT_ESCAPE_TOO = /[.!~*'()]/g;\n\n/** One half of a `type.id` pair, carrying no character the decoders split on. */\nexport function encodeRefPart(value: string): string {\n return encodeURIComponent(value)\n .replace(PERCENT_ESCAPE_TOO, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)\n .replace(/%/g, \"~\");\n}\n\n/** Inverse of `encodeRefPart`; a hand-edited value comes back as its own text. */\nexport function decodeRefPart(value: string): string {\n try {\n return decodeURIComponent(value.replace(/~/g, \"%\"));\n } catch {\n return value;\n }\n}\n\n/** `type.id,type.id` — the ONE spelling of a list in a URL, page or panel token. */\nexport function encodeListItems(items: readonly DetailRef[]): string {\n return items.map((r) => `${encodeRefPart(r.type)}.${encodeRefPart(r.id)}`).join(\",\");\n}\n\n/** Inverse of `encodeListItems`; entries that do not name a record are dropped. */\nexport function decodeListItems(value: string | null | undefined): DetailRef[] {\n if (!value) return [];\n const items: DetailRef[] = [];\n for (const key of value.split(\",\")) {\n const dot = key.indexOf(\".\");\n if (dot <= 0 || dot === key.length - 1) continue;\n items.push({\n type: decodeRefPart(key.slice(0, dot)),\n id: decodeRefPart(key.slice(dot + 1)),\n });\n }\n return items;\n}\n\n/** How many characters these records cost in a URL — what the budget measures. */\nexport function listQueryBytes(items: readonly DetailRef[]): number {\n return encodeListItems(items).length;\n}\n\nexport interface TrimListContextOptions {\n /**\n * The character budget for the whole URL. Defaults to the platform's request\n * line (`DETAIL_URL_BUDGET_BYTES`).\n */\n budgetBytes?: number;\n /**\n * What the rest of the FINAL address already costs — the path, the query it is\n * being merged into, the token's own non-list args. The list gets what is\n * left, never a private allowance of its own (NEW-19).\n */\n reservedBytes?: number;\n /**\n * How many characters these records cost IN THE FINAL URL. The `?panels=`\n * token escapes every separator and `URLSearchParams` escapes the result\n * again, so the same records cost about 1.6× there what they cost in the page\n * query (NEW-15, NEW-19).\n */\n measure?: (items: readonly DetailRef[]) => number;\n}\n\n/**\n * The list as it may travel, plus what was left behind. Returns the same list\n * when it already fits both limits, and `null` for no list at all.\n */\nexport function trimListContext(\n list: DetailListContext | null | undefined,\n max: number,\n options: TrimListContextOptions = {},\n): DetailListContext | null {\n if (!list || list.items.length === 0) return null;\n const budget =\n (options.budgetBytes ?? DETAIL_URL_BUDGET_BYTES) - (options.reservedBytes ?? 0);\n const measure = options.measure ?? listQueryBytes;\n const total = list.items.length;\n const index = list.index >= 0 && list.index < total ? list.index : 0;\n const idCap = Number.isFinite(max) && max >= 1 ? Math.floor(max) : 1;\n\n /** The widest window of `size` records that still contains the current one. */\n const windowOf = (size: number): { items: DetailRef[]; index: number } => {\n const half = Math.floor(size / 2);\n const start = Math.min(Math.max(index - half, 0), Math.max(total - size, 0));\n return { items: list.items.slice(start, start + size), index: index - start };\n };\n\n let size = Math.min(idCap, total);\n // Shrink until the encoded value fits the byte budget. One record always\n // travels: the record being shown is the list's own current position, and a\n // single entry is ~41 characters against a 6000-character budget.\n while (size > 1 && measure(windowOf(size).items) > budget) {\n const fitted = Math.max(\n 1,\n Math.floor((size * budget) / Math.max(measure(windowOf(size).items), 1)),\n );\n size = fitted < size ? fitted : size - 1;\n }\n const cut = windowOf(size);\n const trimmedFrom = size < total ? total : list.trimmedFrom;\n return {\n items: cut.items,\n index: cut.index,\n ...(trimmedFrom && trimmedFrom > cut.items.length ? { trimmedFrom } : {}),\n };\n}\n","// @ai-matrx/detail — src/presentation.ts\n//\n// Pure helpers: the deep-link spelling of every presentation, and the in-URL\n// instance key. Deep links ride the host's ONE panel URL mechanism (`?panels=`\n// in matrx-frontend), under the type key `detail`, so every client that\n// already opens panels from a URL reaches every presentation with no new code:\n//\n// ?panels=detail:<type>.<id>:as-window\n// ?panels=detail:<type>.<id>:as-docked\n//\n// `page` is a route (the host's `navigate.pageHref`), never a `?panels=` token\n// — it is the one presentation that changes the URL. Its list context rides\n// the query (`?l=type.id,…&i=<n>&lt=<total>`); that spelling lives HERE too,\n// beside the token's, so both are cut by the ONE trim and the ONE budget.\n\nimport {\n decodeListItems,\n decodeRefPart,\n encodeListItems,\n encodeRefPart,\n trimListContext,\n} from \"./listContext\";\nimport {\n DEFAULT_DETAIL_LIST_CONTEXT_MAX,\n DETAIL_URL_BUDGET_BYTES,\n type DetailListContext,\n type DetailPresentation,\n type DetailRef,\n} from \"./types\";\n\nexport const DETAIL_URL_TYPE_KEY = \"detail\";\nexport const DETAIL_URL_AS_ARG = \"as\";\n/** The list the window was opened from, so a REFRESH keeps it (NEW-15). */\nexport const DETAIL_URL_LIST_ARG = \"l\";\nexport const DETAIL_URL_INDEX_ARG = \"i\";\nexport const DETAIL_URL_LIST_TOTAL_ARG = \"lt\";\n\n/**\n * `type.id` — the panel URL splits tokens on `:` and args on `-`/`_`, never on\n * `.`, so the two halves carry no `.` `:` or `,` of their own.\n *\n * 🚨 NEW-25 (VERIFY-U-P1-R4) — the halves used to be raw, so a type token or an\n * id containing a dot came back as a DIFFERENT record: `{type: \"gr.ant\", id:\n * \"x.y\"}` parsed as `{type: \"gr\", id: \"ant.x.y\"}`. One encoder, one decoder\n * (`encodeRefPart` / `decodeRefPart`), the same pair the list spelling uses.\n */\nexport function detailInstanceKey(ref: DetailRef): string {\n return `${encodeRefPart(ref.type)}.${encodeRefPart(ref.id)}`;\n}\n\n/** Inverse of `detailInstanceKey`; `null` when the key is not `type.id`. */\nexport function parseDetailInstanceKey(key: string | null | undefined): DetailRef | null {\n if (!key) return null;\n const dot = key.indexOf(\".\");\n if (dot <= 0 || dot === key.length - 1) return null;\n return { type: decodeRefPart(key.slice(0, dot)), id: decodeRefPart(key.slice(dot + 1)) };\n}\n\n/** Which in-place presentation a URL arg names; `window` when unspecified. */\nexport function presentationFromUrlArg(\n value: string | undefined,\n): Exclude<DetailPresentation, \"page\"> {\n return value === \"docked\" ? \"docked\" : \"window\";\n}\n\n// ─── The list context inside a `?panels=` token (NEW-15) ────────────────────\n//\n// 🚨 NEW-15 (VERIFY-U-P1-R3) — A REFRESHED DETAIL WINDOW KEEPS ITS LIST. The\n// window's token used to carry `as` and nothing else, and the hydrator passed\n// `list: null`, so a reload silently lost the previous / next controls and the\n// \"3/40\" counter. The page URL already carried the list; the token carries the\n// same three values, under the same cap and the same byte budget.\n//\n// THE TOKEN'S GRAMMAR IS THE CONSTRAINT. `?panels=` splits panels on `,`, a\n// panel's args off on `:`, arg pairs on `_` and each pair on its first `-`. A\n// uuid is full of hyphens and a list is full of commas, so a list value is\n// escaped into a form holding none of those four characters: `encodeURIComponent`\n// plus the unreserved characters it leaves alone. The result is percent-escaped\n// again by `URLSearchParams` on the way into the address bar and decoded once on\n// the way back out, which is exactly how the page query's comma already travels.\n\nconst EXTRA_ESCAPES = /[-_.!~*'()]/g;\n\n/** A value safe to carry as a `?panels=` arg — no `,` `:` `_` or `-`. */\nexport function encodePanelArgValue(value: string): string {\n return encodeURIComponent(value).replace(\n EXTRA_ESCAPES,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\n/** Inverse of `encodePanelArgValue`. */\nexport function decodePanelArgValue(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n // A hand-edited link. Better the raw text than a thrown hydrator.\n return value;\n }\n}\n\n/**\n * 🚨 NEW-19 (VERIFY-U-P1-R4) — MEASURE THE VALUE AS THE ADDRESS BAR CARRIES IT.\n * The token's own escaping is only the first layer: the host's panel-URL writer\n * serializes the whole `?panels=` value through `new URLSearchParams(...).toString()`,\n * which escapes every `%` again (a uuid's `-` goes `-` → `%2D` → `%252D`, three\n * characters becoming five). Measuring the token's spelling said 5,992 for a URL\n * the browser carried at 7,416.\n */\nexport function panelListValueBytes(items: readonly DetailRef[]): number {\n return encodeURIComponent(encodePanelArgValue(encodeListItems(items))).length;\n}\n\n/**\n * The length of the FINAL address a detail window's deep link produces: the path\n * and query it is merged into, plus the `panels` parameter as\n * `URLSearchParams` serializes it. What the budget is measured against, and what\n * a guard can assert.\n */\nexport function finalPanelUrlLength(\n pathAndQuery: string,\n tokenWithoutListArgs: string,\n listArgs: Record<string, string>,\n): number {\n const args = Object.entries(listArgs)\n .map(([key, value]) => `${key}-${value}`)\n .join(\"_\");\n const token = args ? `${tokenWithoutListArgs}_${args}` : tokenWithoutListArgs;\n const [path = \"\", existingQuery = \"\"] = pathAndQuery.split(\"?\");\n const params = new URLSearchParams(existingQuery);\n params.set(\"panels\", token);\n return `${path}?${params.toString()}`.length;\n}\n\n/**\n * What the address a panel token is written INTO really costs, after\n * `URLSearchParams` re-serializes it — a detail page's own `?l=` list carries raw\n * commas that become `%2C` the moment a `panels` parameter is merged beside it,\n * so the page's query grows by ~2× its comma count at the instant the window\n * opens. Reserving the pre-serialization length is how a page + window address\n * still overran the budget by 70 characters in the guard that found this.\n */\nexport function panelUrlReserveBytes(pathAndQuery: string): number {\n const [path = \"\", query = \"\"] = pathAndQuery.split(\"?\");\n const serialized = new URLSearchParams(query).toString();\n return path.length + (serialized ? serialized.length + 1 : 0);\n}\n\n/**\n * What the token costs beside the list: `panels=detail:<type>.<id>:as-window`\n * plus the `_i-<n>_lt-<n>` args and their escaping. Measured generously so the\n * budget is never overspent by the fixed part.\n */\nconst PANEL_TOKEN_FIXED_BYTES = 160;\n\n/**\n * The reserve for a caller that cannot see the address it writes into. A detail\n * PAGE carrying its own capped list is the longest address a window is opened\n * from, so the reserve is that: the budget's own share for a page query. Without\n * it, a window opened from a detail page produced a 13,433-character URL the edge\n * refuses (NEW-19).\n */\nconst DEFAULT_PANEL_URL_RESERVE_BYTES = Math.floor(DETAIL_URL_BUDGET_BYTES / 2);\n\n/**\n * The window's deep-link args for a list context — `{}` when there is no list.\n * `max` is the resolved `ui.detail.list_context_max_ids`; the budget is the\n * platform's REQUEST LINE, measured on the final serialized value, with whatever\n * the rest of the address already costs reserved (NEW-19). A caller that knows\n * the address it is writing into passes its length; one that does not gets the\n * conservative reserve above, which is what a long detail-page query costs.\n */\nexport function detailListToUrlArgs(\n list: DetailListContext | null | undefined,\n max: number,\n options: { reservedBytes?: number } = {},\n): Record<string, string> {\n const reserved =\n options.reservedBytes ?? DEFAULT_PANEL_URL_RESERVE_BYTES;\n const capped = trimListContext(list, max, {\n measure: panelListValueBytes,\n reservedBytes: reserved + PANEL_TOKEN_FIXED_BYTES,\n });\n if (!capped) return {};\n const args: Record<string, string> = {\n [DETAIL_URL_LIST_ARG]: encodePanelArgValue(encodeListItems(capped.items)),\n [DETAIL_URL_INDEX_ARG]: String(capped.index),\n };\n if (capped.trimmedFrom) args[DETAIL_URL_LIST_TOTAL_ARG] = String(capped.trimmedFrom);\n return args;\n}\n\n/** The list a `?panels=detail:` token carries; `null` when it carries none. */\nexport function detailListFromUrlArgs(\n args: Record<string, string> | null | undefined,\n): DetailListContext | null {\n const raw = args?.[DETAIL_URL_LIST_ARG];\n if (!raw) return null;\n const items = decodeListItems(decodePanelArgValue(raw));\n if (items.length === 0) return null;\n const index = Number.parseInt(args?.[DETAIL_URL_INDEX_ARG] ?? \"\", 10);\n const total = Number.parseInt(args?.[DETAIL_URL_LIST_TOTAL_ARG] ?? \"\", 10);\n return {\n items,\n index: Number.isFinite(index) && index >= 0 && index < items.length ? index : 0,\n ...(Number.isFinite(total) && total > items.length ? { trimmedFrom: total } : {}),\n };\n}\n\n// ─── The list context in the PAGE presentation's query ──────────────────────\n//\n// `/detail/<type>/<id>?l=type.id,type.id&i=<index>&lt=<total>` — the same\n// `type.id` instance key the `?panels=detail:` deep link uses. `lt` is present\n// only when the list was TRIMMED to fit the URL (NEW-7): it is the length of the\n// list the window was cut from, so the detail can say so. The host owns the\n// PATH (`navigate.pageHref`) and appends what `encodeListQuery` returns.\n\n/** `?l=`, `&i=`, `&lt=` and their values — what the query costs beside the list. */\nconst PAGE_QUERY_FIXED_BYTES = 40;\n\n/**\n * 🚨 NEW-7 — CAPPED, ALWAYS. `max` is the resolved\n * `ui.detail.list_context_max_ids` knob; the default is used when the host has\n * no answer yet, never \"no cap\". A 500-row list uncapped produced a >20 KB href\n * no server accepts (VERIFY-U-P1-R2).\n *\n * 🚨 NEW-19 (VERIFY-U-P1-R4) — AND THE BUDGET IS THE WHOLE URL'S. The path this\n * query hangs off is part of the request line, so the caller passes what it\n * costs (`reservedBytes`) and the list gets what is left. Measuring the list\n * alone is how a detail page reached 8,464 characters while the code claimed a\n * 2 KB margin.\n */\nexport function encodeListQuery(\n list: DetailListContext | null | undefined,\n max: number = DEFAULT_DETAIL_LIST_CONTEXT_MAX,\n options: { reservedBytes?: number } = {},\n): string {\n const capped = trimListContext(list, max, {\n reservedBytes: (options.reservedBytes ?? 0) + PAGE_QUERY_FIXED_BYTES,\n });\n if (!capped) return \"\";\n // Each `type.id` is encoded, the separators are not: a comma is legal in a\n // query value, and `%2C` × 200 was 400 bytes of nothing.\n const l = encodeListItems(capped.items);\n const trimmed = capped.trimmedFrom ? `&lt=${capped.trimmedFrom}` : \"\";\n return `?l=${l}&i=${capped.index}${trimmed}`;\n}\n\n/** Inverse of `encodeListQuery`, from the three query values. */\nexport function decodeListQuery(\n l: string | null | undefined,\n i: string | null | undefined,\n /** `lt` — the length of the list this window was cut from, when it was. */\n lt?: string | null | undefined,\n): DetailListContext | null {\n if (!l) return null;\n const items = decodeListItems(l);\n if (items.length === 0) return null;\n const index = Number.parseInt(i ?? \"\", 10);\n const total = Number.parseInt(lt ?? \"\", 10);\n return {\n items,\n index: Number.isFinite(index) && index >= 0 && index < items.length ? index : 0,\n ...(Number.isFinite(total) && total > items.length ? { trimmedFrom: total } : {}),\n };\n}\n","// @ai-matrx/detail — src/react/headerGeometry.ts\n//\n// 🚨 NEW-8 (VERIFY-U-P1-R3) — THE ONE PLACE THE DETAIL HEADER'S GEOMETRY IS\n// NAMED, so a guard can hold the host and the primitive to the same words.\n//\n// D4's fix put the window title in flow between the two action zones, which\n// made overlap impossible — and traded it for a squeeze: at the Detail window's\n// own `minWidth={360}` the traffic-light spacer is 96px and the cluster\n// (previous / counter / next / two presentation icons / copy / pop-out) is about\n// 200px, leaving the record's NAME roughly 60px — four characters and an\n// ellipsis. D2's law (\"the name is the one thing the header always shows\")\n// therefore failed in the window at small sizes, where round 1 measured the page.\n//\n// The rule, and the order it holds in: the name has a FLOOR, and the action\n// cluster collapses into an overflow menu BEFORE the name gives way. The\n// breakpoint is a CONTAINER query on the window header, never a `sm:` viewport\n// breakpoint — a window's width is not the screen's, which is the mistake that\n// made D4 invisible on a 1440px screen. In the page and docked presentations no\n// such container exists above the header, the max-width query never matches, and\n// the full cluster shows: those shells are already wide or already wrap.\n//\n// 🚨 NO SCREEN HAS BEEN SEEN for any of this. The numbers above are arithmetic\n// over the declared widths and the classes, not pixels: this container cannot\n// sign in to any Matrx host. The structural guard is\n// the host's rendered-header guard (matrx-frontend `features/window-panels/detail/__tests__/the-title-never-runs-under-the-actions.test.tsx`).\n//\n// THE LITERALS ARE WRITTEN OUT IN THE JSX AS WELL, because Tailwind generates a\n// utility only for a class it can SEE in source — a class assembled from these\n// constants at run time would never be compiled. The guard asserts the rendered\n// className against these constants, so the two cannot drift apart silently.\n\n/** The container the detail header's breakpoint reads — declared by the window chrome. */\nexport const DETAIL_HEADER_CONTAINER = \"@container/window-header\";\n\n/** Below this container width the optional actions move into the overflow menu. */\nexport const DETAIL_HEADER_COMPACT_BELOW = \"@max-[26rem]/window-header\";\n\n/** The floor under the record's name: ~96px, enough for a readable fragment. */\nexport const DETAIL_TITLE_MIN_WIDTH_CLASS = \"min-w-[6rem]\";\n"],"mappings":"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","names":["import_react","import_react","producer","reconnect","import_react","import_react","load","import_react","import_react","import_jsx_runtime","import_jsx_runtime","import_react","import_design_system","import_react","import_design_system","import_jsx_runtime","effective","import_jsx_runtime","import_react","import_jsx_runtime"]}