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>
66 lines
2.5 KiB
JavaScript
66 lines
2.5 KiB
JavaScript
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()
|
|
return (
|
|
<footer
|
|
className="sans site-footer"
|
|
style={{
|
|
borderTop: '1px solid var(--line)',
|
|
padding: '28px 16px',
|
|
color: 'var(--muted)',
|
|
fontSize: '0.9rem',
|
|
background: 'rgba(9,13,18,0.6)',
|
|
}}
|
|
>
|
|
<div className="site-footer-inner">
|
|
<div className="site-footer-badge">
|
|
<img src="/assets/img/runic-emblem.png" alt="" aria-hidden="true" />
|
|
<span>
|
|
Powered by
|
|
<br />
|
|
<a
|
|
href="https://gitea.whitlocktech.com/RunicGateway"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="site-footer-brand-link"
|
|
>
|
|
<strong>Runic Gateway</strong>
|
|
</a>
|
|
</span>
|
|
</div>
|
|
<div className="site-footer-info">
|
|
<span>{siteTitle} is an independent private shard project.</span>
|
|
<span style={{ color: 'var(--dim)', fontSize: '0.84rem' }}>
|
|
<a href={`mailto:${contactEmail}`} style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
|
{contactEmail}
|
|
</a>
|
|
{/* 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) => <> · {link}</>} />
|
|
·
|
|
<Link to="/admin/login" style={{ color: '#5d6b7d', textDecoration: 'none' }}>
|
|
Admin
|
|
</Link>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
)
|
|
}
|