refactor(modules): move core's UO page content behind the two slots
All checks were successful
PR Checks / client-build (pull_request) Successful in 26s
PR Checks / server-tests (pull_request) Successful in 29s
PR Checks / bot-install (pull_request) Successful in 8m46s

Core declares site.footer.status and admin.users.detail in main.jsx and fills
both itself, under owner id `core` -- the client twin of registries.registerCore()
and the same trick useShardFlags already uses. The rendered page is unchanged;
what changes is that the content now arrives the way a module's will.

The footer's Shard Status link becomes ShardStatusLink.jsx, and UserDetail's six
UO sections become UserShardSections.jsx. Both are files rather than inline
markup so that the client half of phase 3 deletes a registration and a file
instead of editing a core page under extraction pressure -- which is also what
proves the mechanism before anything depends on it.

The user-detail slot is handed userId and not scope. api.admin.userShard is a UO
binding that leaves core with the client half, so a slot passing it would hand a
module something core is about to delete; an extension builds its own client for
the routes it registered at the other end. Core's own fill now does exactly what
the module will.

Verified in a browser against a real chunk (MODULE_API.md 7.7): a throwaway
module fills both slots and renders its own label and target in the footer with
core's linkStyle, and receives userId on the admin page; a deliberate render
failure is contained to that one spot with the slot named in the console; core's
own fills leave the pages byte-identical to before; and with no module installed
both slots render nothing. Zero CSP reports throughout.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 16:45:22 -05:00
parent 1d1350558b
commit d667565ae7
5 changed files with 247 additions and 140 deletions

View File

@@ -0,0 +1,24 @@
// ── Core's fill for the `site.footer.status` extension slot ────────────────
//
// Phase 3, slice 2 of docs/website/MODULE_SYSTEM.md §2.7.1; the contract is
// MODULE_API.md §3.7.
//
// This is the whole of what used to be four lines inline in SiteFooter.jsx, and
// it is a file now for one reason: `/site/shard` is a UO page, so the link goes
// when the client half goes, and core should be deleting a registration rather
// than editing its footer under extraction pressure.
//
// Note what core kept and what it handed over. Core owns the position in the row
// and the separator around it, and passes `linkStyle` so the row stays visually
// one row. The label, the destination, and the decision to render at all are
// this file's — which is exactly the division a module inherits.
import { Link } from 'react-router-dom'
export default function ShardStatusLink({ linkStyle }) {
return (
<Link to="/site/shard" style={linkStyle}>
Shard Status
</Link>
)
}

View File

@@ -1,5 +1,14 @@
import { Link } from 'react-router-dom'
import { useSite } from '../contexts/SiteContext.jsx'
import Slot from '../modules/Slot.jsx'
const FOOTER_SLOT = 'site.footer.status'
// Handed to the extension rather than left for it to guess. A module rendering
// its own link in this row should look like the row, and the alternative is
// every module restating core's colours and then drifting from them the next
// time this footer is themed.
const LINK_STYLE = { color: 'var(--accent)', textDecoration: 'none' }
export default function SiteFooter() {
const { contactEmail, siteTitle } = useSite()
@@ -36,10 +45,14 @@ export default function SiteFooter() {
<a href={`mailto:${contactEmail}`} style={{ color: 'var(--accent)', textDecoration: 'none' }}>
{contactEmail}
</a>
&nbsp;·&nbsp;
<Link to="/site/shard" style={{ color: 'var(--accent)', textDecoration: 'none' }}>
Shard Status
</Link>
{/* A module's spot in the footer, and core supplies only the
position and the styling: the label, the target and whether
anything renders at all are the module's (MODULE_API.md §3.7).
The separator goes through `wrap` rather than sitting beside the
slot, so it shares the extension's fate — no module installed and
a module whose link throws both render nothing here, rather than
the second leaving a stray middot behind. */}
<Slot name={FOOTER_SLOT} linkStyle={LINK_STYLE} wrap={(link) => <>&nbsp;·&nbsp;{link}</>} />
&nbsp;·&nbsp;
<Link to="/admin/login" style={{ color: '#5d6b7d', textDecoration: 'none' }}>
Admin