docs(website): client extension slots, and the client half is one slice (phase 3, slice 2) #137

Merged
whitlocktech merged 2 commits from docs/module-client-slots into main 2026-08-11 16:54:09 -05:00
2 changed files with 50 additions and 3 deletions
Showing only changes of commit 96b364b0bc - Show all commits

View File

@@ -782,6 +782,19 @@ those props — or **nothing at all** when the slot is unfilled. An instance wit
therefore renders exactly what it renders today, which is the same "untouched path" guarantee
`withModuleNav` makes for nav.
**Core decorates a slot with `<Slot wrap>`, never by asking whether it is filled.** `wrap` is called
with the extension's element and rendered *inside* the boundary, so core's own markup around an
extension — a separator, a heading, a rule — shares the extension's fate:
```jsx
<Slot name="site.footer.status" linkStyle={LINK_STYLE} wrap={(link) => <>&nbsp;·&nbsp;{link}</>} />
```
There is deliberately no `hasExtension` for a layout to branch on. Branching is right about the
unfilled case and wrong about the failed one: the slot *is* filled, so the separator renders, and
the component then throws into the boundary and leaves the separator behind on its own. Found in a
browser with this exact footer separator, which is the only place it could have been found.
The slots in 1.2.0:
| Slot | Rendered in | Props core passes |
@@ -830,10 +843,15 @@ already uses for the feature seam. It means the mechanism is exercised by core's
moment it lands, and the extraction becomes a deletion rather than a rewrite made under extraction
pressure.
`declareSlot` and `hasExtension` are module exports of `modules/registry.js` and deliberately **not**
**While core fills a slot, no module can** — first fill wins, and core registers first. In 1.2.0 that
means both slots are occupied and a module's fill is rejected, loudly, naming `core`. This is
scaffolding, not the steady state: core's two fills and the two files behind them are deleted by the
client half of Phase 3, in the same change that registers the module's. Neither slot is one core
intends to fill permanently, and a slot core *does* intend to fill is a slot that should not exist.
`declareSlot` and `extensionFor` are module exports of `modules/registry.js` and deliberately **not**
members of the `registry` object handed to modules, for the same reason `featureProviders()` is not:
declaring is core's, and a module asks about a slot it knows the name of rather than enumerating what
everyone else filled.
declaring is core's, and so is reading back who filled what.
---

View File

@@ -882,6 +882,35 @@ Two decisions inside it, both settled with the org lead 2026-08-11:
`registries.registerCore()` already use — so the rendered page is unchanged and the mechanism is
proved by core's own content before a line of it moves.
#### Slice 2 — the slots (website#138, 2026-08-11)
`modules/registry.js` grows `declareSlot` / `registerExtension` / `extensionFor`; `modules/Slot.jsx`
is the read side and core's only error boundary. `SiteFooter`'s link becomes `ShardStatusLink.jsx`
and `UserDetail`'s six UO sections become `UserShardSections.jsx`, both registered by `main.jsx`
under owner id `core`. 616 server + **169 client** tests (+9), manifest still 158, swagger
byte-identical.
**The §7.7 browser smoke earned its place again, and this time by finding something no test would
have.** A throwaway module filled both slots: the footer rendered the module's own label and target
in core's `linkStyle`, the admin page received `userId`, and a deliberate render failure was
contained to its own spot with the slot named in the console — but the footer was left showing
`email · · Admin`. The separator was rendered *beside* the slot, guarded on `hasExtension`, which is
right when nothing is installed and wrong when something is installed and fails. Core now decorates
through `<Slot wrap>`, inside the boundary, and `hasExtension` is gone rather than kept as a trap for
the next caller. Every unit test passed both before and after.
Two more things the smoke settled, neither of them a defect:
- **Core's own fill occupies the slot, so a module cannot fill it** — first fill wins and core
registers first, so the smoke module's call was rejected naming `core`. That is correct and
temporary: the client half deletes core's two fills in the change that registers the module's. It
is worth stating in the contract, because a module written against 1.2.0 today cannot use either
slot.
- **Core's UO sections on the user-detail page now fail to load, and that is slice 1's doing, not
this slice's** — core stopped serving `/admin/users/:id/shard/*` when the server half left, and
with no module installed there is nothing at the other end. Identical before and after this change;
the client half is what puts a live module behind it.
**Criterion 1 is a grep over code, not over prose** — see [API §5.2](MODULE_API.md#52-zero-uo-identifiers-in-core-ci-website-repo)
for what that means precisely. Core's marketing copy says "shard" in a dozen places, and a literal
word grep would have made every one of them a CI failure while proving nothing about the boundary.