@ai-matrx/detail 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/host-eKqulRpB.d.cts +524 -0
- package/dist/host-eKqulRpB.d.ts +524 -0
- package/dist/index.cjs +308 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +505 -0
- package/dist/index.d.ts +505 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1572 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +191 -0
- package/dist/react.d.ts +191 -0
- package/dist/react.js +1553 -0
- package/dist/react.js.map +1 -0
- package/dist/testing.cjs +162 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +54 -0
- package/dist/testing.d.ts +54 -0
- package/dist/testing.js +142 -0
- package/dist/testing.js.map +1 -0
- package/package.json +115 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Changelog — @ai-matrx/detail
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
Not published: 0.1.0 is still not on npm, so matrx-frontend's `lib/detail/**` remains the LIVE
|
|
6
|
+
code and this `src/` remains what it must say (README → "Adoption status"). These fixes were made
|
|
7
|
+
in BOTH copies in the same session, and the frontend now carries a drift guard that fails by name
|
|
8
|
+
when they diverge.
|
|
9
|
+
|
|
10
|
+
- **`Consumer action`: none for an installer** — nothing here changes an installed API's shape
|
|
11
|
+
except the two ADDITIONS below, both optional. A host already on 0.1.0's source copy must take
|
|
12
|
+
all of it, because the copies are diffed byte for byte.
|
|
13
|
+
- **Fix — the page presentation's list query opened a DIFFERENT record** (VERIFY-U-P1-R5, N4).
|
|
14
|
+
`encodeRefPart` escaped `.` `:` `,` to `%2E` `%3A` `%2C`, and an ordinary query parse
|
|
15
|
+
(`new URL(...).searchParams`, which is where the page route's `searchParams` come from)
|
|
16
|
+
percent-decodes those back to bare separators BEFORE `decodeListItems` splits, so a dotted type
|
|
17
|
+
token resolved to `{type: <before the dot>}` and an id containing a comma was truncated. The
|
|
18
|
+
`?panels=` token survived only because it has three escaping layers. The grammar now carries no
|
|
19
|
+
`%` at all — percent-encoding spelled with `~` over `A-Za-z0-9-_~` — so it is invariant under
|
|
20
|
+
any number of percent-decodes. New test leg: the round trip THROUGH a query parse.
|
|
21
|
+
- **Fix — an uncached `resolveType` looped forever** (N8). `useDetailHealth` and
|
|
22
|
+
`useDetailRecord` keyed their effects on the registration's FUNCTION identity, so a host whose
|
|
23
|
+
map builds the registration per call re-ran them every render (their own `setState` causing the
|
|
24
|
+
next) — "Maximum update depth exceeded" out of the package, and an endless reload of the same
|
|
25
|
+
record at the server. Both now key on the record type plus the id and read the closure through
|
|
26
|
+
a ref. `the-health-effect-does-not-loop.test.tsx` does not terminate against the old deps.
|
|
27
|
+
- **Fix — a comment contradicted the line beneath it** (N9). `DetailPagePresentation` was headed
|
|
28
|
+
"It takes no `onBack`" directly above `onBack={core.leave}`; the behaviour is right (the one
|
|
29
|
+
guarded exit) and the sentence now says what it means: no `onBack` from its CALLER.
|
|
30
|
+
- **New grant word `blocked`** (chair, 2026-09-18). `DetailSourceHealth.grant` gains a sixth
|
|
31
|
+
value for "the source refuses and nothing the person can click repairs it" — a provider outage,
|
|
32
|
+
a quota, a platform-configuration fault, a connection a server reports as unavailable. `unknown`
|
|
33
|
+
was carrying that meaning and it means "we could not check", so those records were offered a
|
|
34
|
+
consent window that repairs none of them. The strip says BLOCKED, states the reason, and
|
|
35
|
+
`reconnectFor` returns `null` for the word whatever the producer or the host supplies — the
|
|
36
|
+
refusal is structural, not a promise. The strip now shows the state word for every non-`ok`
|
|
37
|
+
grant.
|
|
38
|
+
- **New optional `DetailRecordType.labelForRow`** (chair, 2026-09-18). `label` is per TYPE and a
|
|
39
|
+
registered type is regularly a family: `crm.party` holds 1,432 companies and 460 persons under
|
|
40
|
+
one type labelled "Person", so the header chip and every stand-in title called a company a
|
|
41
|
+
Person, twice on screen, above a field reading `organization` (VERIFY-U-P1-R5, N6). A
|
|
42
|
+
registration that can tell from the row answers here, and `DetailCore` gains `recordLabel`
|
|
43
|
+
beside `typeLabel`. Only the chip and the stand-in titles use it: the presentation pane keeps
|
|
44
|
+
the TYPE's label, because "Every contact record now opens as a window" is a statement about the
|
|
45
|
+
type's setting.
|
|
46
|
+
- **`DetailField.ref` may carry a `name`, and the `doors.RefCell` port receives it.** The body
|
|
47
|
+
rendered the door from `ref.id` alone and threw `field.text` away, so a field whose value was
|
|
48
|
+
"Acme Robotics" printed a truncated uuid — a door nobody can read is half a dead end. The ref's
|
|
49
|
+
`name` wins; otherwise the field's text, unless that text IS the id.
|
|
50
|
+
- **New optional port `history.ActorName`** (NEW-23, round 5). Every history row printed a bare
|
|
51
|
+
36-character uuid under "Changed by", because no host registry gives `actor_id` a door: an
|
|
52
|
+
identity the UI names that opens nothing. A host binds its ONE existing identity resolver here
|
|
53
|
+
and the row names the person; unbound, the id stays under a title that says we could not find
|
|
54
|
+
them.
|
|
55
|
+
|
|
56
|
+
## 0.1.0
|
|
57
|
+
|
|
58
|
+
The Detail primitive becomes a package. Extracted from matrx-frontend's `lib/detail` (the
|
|
59
|
+
folder four verification rounds called "still not a PACKAGE" — VERIFY-U-P1-R4's first
|
|
60
|
+
disqualifier), behaviour ported VERBATIM: the record-type contract, the presentation setting
|
|
61
|
+
shape and its per-type override, the list-context trim with the final-URL byte budget
|
|
62
|
+
(NEW-19 / NEW-20), the deep-link spelling with the escaped ref parts (NEW-25), the keyboard
|
|
63
|
+
model bound on every slot (NEW-22), the honest absent states (NEW-9 / NEW-17), the history row
|
|
64
|
+
that never drops who (NEW-23), the one guarded page exit (D1), and the source health strip
|
|
65
|
+
with its async producer. 126 tests ride along, ported from jest to vitest, plus a mechanical
|
|
66
|
+
boundary guard that had never existed (the README stated the boundary in prose).
|
|
67
|
+
|
|
68
|
+
What changed on the way in, and why:
|
|
69
|
+
|
|
70
|
+
- **The page presentation's list-context query spelling (`encodeListQuery` /
|
|
71
|
+
`decodeListQuery`) moved INTO the package** beside the panel-token spelling. It lived in the
|
|
72
|
+
host's overlay-payload module while every other spelling was already here; the Redux payload
|
|
73
|
+
shape stays in the host.
|
|
74
|
+
- **The seventeen icons are inlined SVGs** (C19: no icon-library dependency). A record type's
|
|
75
|
+
own icon is unchanged host data.
|
|
76
|
+
- **Host-shaped strings left the package.** The console remedy for an unregistered or
|
|
77
|
+
sourceless type named a matrx-frontend file; it now names the host's `remedy.typeMap` port
|
|
78
|
+
(default: the `resolveType` port), and the throw messages point at the README instead of a
|
|
79
|
+
host path.
|
|
80
|
+
- **Module-level state moved to `globalThis` `Symbol.for` slots** (the host context and the
|
|
81
|
+
once-per-tab remedy set), per the dual-bundle rule — the ESM and CJS builds, and the `/react`
|
|
82
|
+
and `/testing` entries, share one context.
|
|
83
|
+
- **Fix (Bugbot round 18 on frontend PR 228, comment 4041985944): `onReconnect: null` means
|
|
84
|
+
"offer nothing".** `useDetailHealth` filled the host's Reconnect back in with `??`, which
|
|
85
|
+
reads a producer's explicit `null` as "unset", so a synced record whose refusal a reconnect
|
|
86
|
+
cannot repair (a share the owner must grant, a record we own, an unclassified failure) still
|
|
87
|
+
showed Reconnect — a control that does nothing. The contract is now three-valued and
|
|
88
|
+
documented on `DetailSourceHealth.onReconnect` (a function → that action; `null` → none;
|
|
89
|
+
omitted → the host's), resolved by `reconnectFor`. Proven red (2 of 4 failing with the `??`
|
|
90
|
+
body) then green in `the-strip-honours-a-producers-refusal.test.tsx`.
|
|
91
|
+
- **A `/testing` entry ships the stub-port seat** (`makePortsWith(fn)` takes the runner's mock
|
|
92
|
+
factory), so a host tests its binding against the same seat the package proves itself with —
|
|
93
|
+
one harness, no twin in the consumer.
|
|
94
|
+
|
|
95
|
+
### Consumer action — matrx-frontend
|
|
96
|
+
|
|
97
|
+
1. `package.json`: add `"@ai-matrx/detail": "latest"` beside the other `@ai-matrx/*` entries,
|
|
98
|
+
then `pnpm install` and `pnpm check:matrx-packages`.
|
|
99
|
+
2. `app/globals.css`: add `@source "../node_modules/@ai-matrx/detail/dist";` (the components are
|
|
100
|
+
Tailwind-utility styled; the consumer's scan must see the built package).
|
|
101
|
+
3. Import from the package: types, spelling helpers, `trimListContext`, `encodeListQuery` /
|
|
102
|
+
`decodeListQuery`, `fieldsFromRow` from `@ai-matrx/detail`; `DetailHostProvider`,
|
|
103
|
+
`useDetailHost`, `useDetailCore`, `useOpenDetail`, the presentations, header/body/pane
|
|
104
|
+
and the geometry constants from `@ai-matrx/detail/react`; the test seat from
|
|
105
|
+
`@ai-matrx/detail/testing` with `makePortsWith(jest.fn)`.
|
|
106
|
+
4. Delete `lib/detail/**` source and tests (they are here now); keep its `FEATURE.md` as the
|
|
107
|
+
host-wiring pointer.
|
|
108
|
+
5. Bind the new optional port `remedy: { typeMap: "…" }` in the host binding so the console
|
|
109
|
+
remedy names the item registry.
|
|
110
|
+
6. Any health producer that decides a reconnect would not help must answer
|
|
111
|
+
`onReconnect: null` (not omit the key) — omitting it now means "defer to the host".
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Matrix Engine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @ai-matrx/detail — the Detail primitive
|
|
2
|
+
|
|
3
|
+
> Arman, 2026-09-17: *"a core component that then shows up as a Page, a flexible drawer, and a
|
|
4
|
+
> window panel. The default is the window. … you just make a core component with some core
|
|
5
|
+
> rules, then wrap it in this reusable thing from the package, which then instantly gives you
|
|
6
|
+
> all 3 for free."*
|
|
7
|
+
|
|
8
|
+
ONE registration per record type → three presentations for free, with the same content in all
|
|
9
|
+
three. Keyboard: Escape closes, the arrows move between records (`[` / `]` are aliases), and
|
|
10
|
+
Cmd/Ctrl+Enter saves.
|
|
11
|
+
|
|
12
|
+
| Presentation | What it is | What the host binds |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `window` (**the default**) | Floating, draggable, minimizable; a bottom sheet on phones | `shells.Window` — the host's window manager |
|
|
15
|
+
| `docked` | A resizable side panel docked to the right edge, no backdrop; a bottom sheet on phones | `shells.Docked` — the host's side-panel surface |
|
|
16
|
+
| `page` | A full route body under the app header — the only presentation that changes the URL; offers the window back | `shells.Page` — the host's route header + scroll body |
|
|
17
|
+
|
|
18
|
+
Champions: Notion (side peek / center peek / full page, per-database "open pages in") and Linear
|
|
19
|
+
(peek, keyboard-first). Bones matched, skin ours.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pnpm add @ai-matrx/detail # every Matrx repo declares it "latest" (THE LATEST LAW)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 🚨 Adoption status — nothing installs this yet, and matrx-frontend carries a COPY
|
|
26
|
+
|
|
27
|
+
The `mv` has been performed and the three gates pass here, but **0.1.0 is not on npm**. The
|
|
28
|
+
frontend adopted it (`ec7ce701`) and the adoption was **reverted** (`32ce9170`, ruling R21)
|
|
29
|
+
because a `"latest"` spec for a package the registry does not have kills the ONE
|
|
30
|
+
`pnpm install --frozen-lockfile` every workspace project shares. The publish is a human step.
|
|
31
|
+
|
|
32
|
+
So until it lands: **matrx-frontend's `lib/detail/**` is the LIVE code and this `src/` is the
|
|
33
|
+
source of truth for what it should say.** A fix goes in BOTH, in the same session. What keeps
|
|
34
|
+
them honest is the frontend's own drift guard,
|
|
35
|
+
`lib/detail/__tests__/the-in-repo-copy-matches-the-package.test.ts`, which diffs all fifteen
|
|
36
|
+
module pairs against this `src/` and fails by name on any difference outside the module header
|
|
37
|
+
and relative import specifiers. It FAILS AS UNMEASURED when this checkout is absent, never
|
|
38
|
+
passes quietly. The fifteen days when nobody had that guard are exactly when the copies
|
|
39
|
+
diverged: this package fixed a reviewed defect (`onReconnect: null` still got a Reconnect
|
|
40
|
+
button) and the revert un-fixed it in the frontend, where a person could feel it
|
|
41
|
+
(VERIFY-U-P1-R5, N1 / N2). One file is deliberately different and says so: `react/icons.tsx`
|
|
42
|
+
inlines its glyphs here (no icon-library dependency), and the host's `core/icons.ts` re-exports
|
|
43
|
+
Lucide's `*Icon` aliases under the same names.
|
|
44
|
+
|
|
45
|
+
| Entry | Ships | `"use client"` |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `@ai-matrx/detail` | the record-type contract, the presentation setting shape and its per-type override, the list-context trim with the final-URL byte budget, the deep-link spelling of every presentation, the row → fields formatter | no — pure, legal in a Server Component |
|
|
48
|
+
| `@ai-matrx/detail/react` | the ports and provider, the core, the keyboard model, the loader and the health resolver, the one opener, the header halves, the body, the setting pane, the three presentations, the header geometry constants | yes |
|
|
49
|
+
| `@ai-matrx/detail/testing` | the stub-port seat: `makePortsWith(fn)`, `mount`, `instance`, `clickByLabel`, three stub shells, `FILE_TYPE` — the seat the package proves itself with, for a host to prove its binding with | yes |
|
|
50
|
+
|
|
51
|
+
## The contract — one registration per record type
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import type { DetailRecordType } from "@ai-matrx/detail";
|
|
55
|
+
|
|
56
|
+
const task: DetailRecordType = {
|
|
57
|
+
type: "task", label: "Task", icon: TaskIcon, accent: null,
|
|
58
|
+
entityToken: "task", // doors · associations · history key off this; null = none
|
|
59
|
+
load: (id, signal) => …, // null = no single source → renders from the seed, and says so
|
|
60
|
+
title: (row, seed) => …,
|
|
61
|
+
fields: (row) => […],
|
|
62
|
+
health?: (row, ctx) => …, // the source health strip for a SYNCED record (sync or async)
|
|
63
|
+
associationTokens?: […], // null hides the section; omitted → the host default minus self
|
|
64
|
+
history?: true, // default: when entityToken is set
|
|
65
|
+
extraSections?: (row, ctx) => […],
|
|
66
|
+
Frame?: ({ ctx, children }) => …, // wraps the body: surface runtime, right-click menu, …
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Fixed sections, in order: **source health strip** (synced records only) · about · **fields** ·
|
|
71
|
+
**associations** (`AssociationCardGrid` under a `PrimaryEntityProvider`, from
|
|
72
|
+
`@ai-matrx/associations`) · **history** (row versions) · extras · the "how details open" pane.
|
|
73
|
+
A section that does not apply is absent, never an empty box.
|
|
74
|
+
|
|
75
|
+
**The one opener:** `useOpenDetail()({ type, id, presentation?, seed?, list? })`. Without
|
|
76
|
+
`presentation` it asks the host's `resolvePresentation(type)` — the person's
|
|
77
|
+
`ui.detail.default_presentation`, with `ui.detail.presentation_by_type` read first for that
|
|
78
|
+
type. A resolver that raises is announced (the host's `notify.error`, naming the key) and the
|
|
79
|
+
window opens. Nothing navigates away except `page`.
|
|
80
|
+
|
|
81
|
+
## Binding the ports
|
|
82
|
+
|
|
83
|
+
Everything app-shaped arrives through `DetailHostPorts` (`@ai-matrx/detail/react`). Providers
|
|
84
|
+
nest and merge: bind the light ports ONCE at boot so `useOpenDetail` works everywhere, and let
|
|
85
|
+
each presentation's lazily loaded entry add its own shell and the record-type map right above
|
|
86
|
+
the presentation, so the window shell never reaches a boot bundle. A missing port throws,
|
|
87
|
+
naming itself.
|
|
88
|
+
|
|
89
|
+
| Port | What the host injects | Required |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `resolveType(type)` | THE record-type map — one registry, never a second | per presentation entry |
|
|
92
|
+
| `usePresentationSetting(type)` · `resolvePresentation(type)` · `warmPresentation(type)` | the settings ladder's answer for `ui.detail.default_presentation` and `ui.detail.presentation_by_type` (React face, awaited face, cache warmer) | yes |
|
|
93
|
+
| `reReadPresentation(type)` | a FRESH ladder read after a write, naming the rung that answers now | optional |
|
|
94
|
+
| `savePresentation({ presentation, forType, clear? })` | the ONE writer of the setting and its per-type map entry; a refusal comes back as `{ ok: false, reason }` | optional — absent ⇒ the pane is absent |
|
|
95
|
+
| `open` · `close` | the in-place presentations (window / docked) on the host's overlay system | yes |
|
|
96
|
+
| `navigate.pageHref` · `toPage` · `back` · `canGoBack` · `toRecordHome` | the page route; `canGoBack` answers "did THIS tab push the page" and `toRecordHome` replaces the page with the record's home — never a raw `back()` into an empty history | yes |
|
|
97
|
+
| `shells.Window` · `Docked` · `Page` | the chrome; the package fills `title`, `titleNode`, `actions`, `children`, `onClose` (+ `onBack` for the page, + `list` for the window's deep link) | per presentation entry |
|
|
98
|
+
| `doors.RecordDoors` · `RefCell` · `tokenFromColumnName` · `isUuidValue` · `hasDoor` | the host's entity doors (open / new tab / peek); `hasDoor` decides whether the absent state may claim "the controls above still open it" | yes |
|
|
99
|
+
| `associations.defaultTokens` · `canAnchor` | which tokens the associations section shows and which may anchor one | yes |
|
|
100
|
+
| `history.list(token, id, signal)` | row versions | yes |
|
|
101
|
+
| `notify.error` · `success` · `copyText` | the host's toast and clipboard | yes |
|
|
102
|
+
| `reconnectSource(ref, source)` | the same Reconnect the connector rows offer, for a synced record's strip | optional |
|
|
103
|
+
| `remedy.typeMap` | where the developer goes to register a type — the console remedy names it | optional |
|
|
104
|
+
|
|
105
|
+
The knobs the package READS THROUGH PORTS and never resolves itself: `ui.detail.default_presentation`
|
|
106
|
+
(enum `window|docked|page`, organization → user), `ui.detail.presentation_by_type` (json map,
|
|
107
|
+
same ladder), `ui.detail.list_context_max_ids` (organization; the host resolves it and passes
|
|
108
|
+
the number into `encodeListQuery` / `detailListToUrlArgs`, which clamp it to
|
|
109
|
+
`DETAIL_LIST_CONTEXT_MAX_IDS_CEILING`). Their rows are seeded by matrx-frontend's migrations.
|
|
110
|
+
|
|
111
|
+
## Deep links
|
|
112
|
+
|
|
113
|
+
Window and docked deep links ride the host's ONE panel-URL mechanism under the type key
|
|
114
|
+
`detail`, instance `type.id`, arg `as`: `?panels=detail:task.<uuid>:as-window` / `:as-docked`,
|
|
115
|
+
carrying the list the record was opened from (`l` / `i` / `lt`). The page is the host's route
|
|
116
|
+
(`/detail/<type>/<id>` in matrx-frontend) with `?l=type.id,…&i=<n><=<total>`. Both spellings
|
|
117
|
+
are cut by ONE trim (`trimListContext`) against ONE budget — 8,000 characters of the FINAL
|
|
118
|
+
serialized URL, with what the rest of the address costs reserved — and the record says when the
|
|
119
|
+
list was trimmed. `.` `:` `,` inside a type token or an id are escaped, so an external id such
|
|
120
|
+
as `sc-domain:example.com` round-trips as the same record.
|
|
121
|
+
|
|
122
|
+
## Keyboard
|
|
123
|
+
|
|
124
|
+
`useDetailKeyboard`, bound on every slot the presentation fills (header and body): Escape closes
|
|
125
|
+
from any control except an open menu / select / combobox and a text field with uncommitted
|
|
126
|
+
edits; ArrowUp/Down/Left/Right move between records only when the body is not the scroll target,
|
|
127
|
+
`[` / `]` always move; Cmd/Ctrl+Enter saves what a section registered — and says there is nothing
|
|
128
|
+
to save when nothing did.
|
|
129
|
+
|
|
130
|
+
## Testing a host binding
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { makePortsWith, mount, instance, StubPageShell } from "@ai-matrx/detail/testing";
|
|
134
|
+
const makePorts = makePortsWith(jest.fn); // or vi.fn — the seat takes the runner's factory
|
|
135
|
+
const ports = makePorts({ shells: { Page: StubPageShell } });
|
|
136
|
+
const m = mount(<DetailPagePresentation data={instance()} />, ports);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Every port is a mock, so a test asserts what the core ASKED THE HOST TO DO — which is where every
|
|
140
|
+
recorded defect of this primitive lived.
|
|
141
|
+
|
|
142
|
+
## What is deliberately NOT here
|
|
143
|
+
|
|
144
|
+
- The host's window manager, side panel and route header — they are shells, bound as ports.
|
|
145
|
+
- The settings ladder, the toast, the clipboard, the router, the entity registry — ports.
|
|
146
|
+
- An icon library: the seventeen glyphs the primitive draws are inlined SVGs (Lucide-derived,
|
|
147
|
+
ISC). A record type's own icon is host data on its registration.
|
|
148
|
+
- Tailwind CSS generation: the components are styled with utility classes. Register the built
|
|
149
|
+
package with the consumer's Tailwind scan (`@source "../node_modules/@ai-matrx/detail/dist";`
|
|
150
|
+
in matrx-frontend's `globals.css`, the design-system precedent).
|
|
151
|
+
|
|
152
|
+
Implementation truth, file map and change history: `FEATURE.md` beside this file. Packaging
|
|
153
|
+
law: `common-docs/policies/typescript-package-standard.md`.
|