Phase 3's acceptance criterion 1, made real. Three things, one review: **The dead bindings.** `client/src/api/client.js` still carried ~190 lines of UO namespaces — `shard`, `atlas`, the two SSE URLs, `admin.shard/shardOps/atlas/ userShard`, the uo-link and town-crier calls, `player.shard` — with zero core consumers since slice 3 deleted the views. module-uo vendors its own bindings. The five assertions core's `apiClient.test.js` made about those URLs moved with them (Module-uo#5); the encoding test that used `governorHistory` now uses a core route. **The copy.** Core is the platform, not one game's site, so its words are game-neutral now: `About`, `Screenshots`, `Website`'s cards, `Status` (which was never about a game server at all — it reports site mode), `Wiki`, `SiteFooter`, the default hero, `brand.js`'s tagline and description, the seeded wiki categories, and two user-visible NavEditor strings that named a module's admin screen by its proper name. Which game an instance is for is the operator's to say — BRAND_* vars, the hero editor, CMS pages — and every real instance already does: `.env.uomysticmoon.example` sets both brand strings explicitly, so nothing live changes wording. Wiki page SLUGS are untouched: `seedDefault*` only inserts what is absent, so renaming one adds a duplicate page to every install. Also gone: an orphan comment block in `schema.sql` describing the spawn-atlas tables slice 1 took away, and the two settings rows core seeded for a module (`game_account_signup`, `uo_link_protocol_3_migrated`). The second was a live defect — see Module-uo#5, which takes ownership of both and repairs the one-shot migration core's ordering had disabled. **The check.** `scripts/checkModuleIdentifiers.js` + `npm run check:modules`, first step of the server-tests job because it needs no dependencies. It reads CODE, not prose — file names, import specifiers, route path literals, declared identifiers and property names — per §5.2, so core's English may still say "shard" where saying it is worth more than the word costs. Two things it gets right only because getting them wrong was tried first: it matches WHOLE WORDS (a substring pass flags `defaultImage`, which contains "ultIma", four times in this repo), and it strips comments and string bodies in one character walk (a comment contains quotes, a string contains `//`) — the `checkImports.js` lesson. It has its own 17-test suite, because a boundary check that silently stops checking is worse than none. The three §6.5 grandfathering allowlists are exempt by name, and an exemption that stops matching fails the build rather than lingering. BREAKING CHANGE: core no longer seeds `game_account_signup` or `uo_link_protocol_3_migrated`; module-uo's schema fragment does. An install running core without module-uo keeps whatever rows it already has and gains no new ones — nothing in core reads either key. Deferred to slice 5, deliberately: README.md's 48 UO mentions, including a `## Shard integration (uo-link)` section and the architecture diagram. That is documentation, which §5.2 does not cover, and it belongs with the phase-closing docs pass rather than half-done here. Co-Authored-By: Claude <noreply@anthropic.com>
124 lines
5.2 KiB
JavaScript
124 lines
5.2 KiB
JavaScript
import { useMemo } from 'react'
|
|
import { Link, NavLink } from 'react-router-dom'
|
|
import MoonDot from './MoonDot.jsx'
|
|
import BrandLogo from './BrandLogo.jsx'
|
|
import { useAuth } from '../contexts/AuthContext.jsx'
|
|
import { useSite } from '../contexts/SiteContext.jsx'
|
|
import NavDropdown from './NavDropdown.jsx'
|
|
import { buildPublicNav, pruneNav } from '../lib/navOverrides.js'
|
|
import { parseJsonSetting } from '../lib/settingsJson.js'
|
|
import { withModuleNav } from '../modules/nav.js'
|
|
import { useFeatureGate } from '../modules/features.jsx'
|
|
|
|
// One consistent top nav for the whole public site. Every page gets the same
|
|
// main links plus an auth-aware entry on the right (Sign in / My Account / Admin).
|
|
//
|
|
// A row may carry a `feature`, naming a surface an installed module can disable
|
|
// or gate to a higher audience; it is hidden when this viewer cannot reach it,
|
|
// so we never render a link that would 403. No CORE row carries one today — the
|
|
// nine that did were UO and left with the client half in slice 3 — but the gate
|
|
// is not dead code: a module's rows join this list and bring their own flags,
|
|
// resolved by the module that registered them (modules/featureGate.js).
|
|
//
|
|
// Exported because Admin -> Navigation edits this list. It stays declared here,
|
|
// with this component as its owner: the editor may only relabel, reorder and
|
|
// hide what it finds, and `to`/`feature` are never its to change (§7). An
|
|
// installed module's rows join it in `withModuleNav` below — before the override
|
|
// merge, so an admin can edit those rows exactly as they edit these.
|
|
export const NAV = [
|
|
{ label: 'Home', to: '/', end: true },
|
|
{ label: 'News', to: '/site/news' },
|
|
{ label: 'Screenshots', to: '/site/screenshots' },
|
|
{ label: 'Five on Friday', to: '/site/five-on-friday' },
|
|
{ label: 'Newsletter', to: '/site/newsletter' },
|
|
{ label: 'Wiki', to: '/wiki' },
|
|
{ label: 'About', to: '/site/about' },
|
|
]
|
|
|
|
const linkStyle = ({ isActive }) => ({
|
|
background: isActive ? 'var(--accent)' : undefined,
|
|
color: isActive ? 'var(--bg-deep)' : undefined,
|
|
borderColor: isActive ? 'var(--accent)' : undefined,
|
|
})
|
|
|
|
export default function SiteHeader() {
|
|
const { user, loading } = useAuth()
|
|
const { siteTitle, settings } = useSite()
|
|
const isVisible = useFeatureGate()
|
|
|
|
// Core's rows plus every installed module's. Computed once: the registry is
|
|
// fixed before the first render and there is no unregistering, so this cannot
|
|
// change during a session (modules/nav.js).
|
|
const baseNav = useMemo(() => withModuleNav(NAV, 'public'), [])
|
|
|
|
// An admin may relabel, reorder and hide these entries from Admin →
|
|
// Navigation, and may group them into dropdown sections alongside links of
|
|
// their own (THEMING_AND_NAV.md §7). Two things about the order here:
|
|
//
|
|
// • the override merge runs FIRST and the feature filter after it, so the
|
|
// filter stays the boundary — an override cannot un-hide a surface this
|
|
// viewer may not see, whatever it says. `pruneNav` applies the same
|
|
// check inside a section and drops one it leaves empty, so a dropdown
|
|
// never opens onto nothing;
|
|
// • with no stored row this is the coded NAV, in code order, so an
|
|
// untouched instance renders exactly what it renders today.
|
|
const nav = useMemo(() => {
|
|
const tree = buildPublicNav(baseNav, parseJsonSetting(settings.nav_public))
|
|
return pruneNav(tree, isVisible)
|
|
}, [baseNav, settings.nav_public, isVisible])
|
|
|
|
// Where the auth entry points: staff → admin, player → portal, else sign in.
|
|
let account
|
|
if (user && user.role && user.role !== 'player') account = { label: 'Admin', to: '/admin' }
|
|
else if (user) account = { label: 'My Account', to: '/player' }
|
|
else account = { label: 'Sign in', to: '/account/login' }
|
|
|
|
return (
|
|
<header
|
|
style={{
|
|
borderBottom: '1px solid var(--line)',
|
|
background: 'rgba(9,13,18,0.86)',
|
|
backdropFilter: 'blur(8px)',
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 30,
|
|
}}
|
|
>
|
|
<div
|
|
className="shell"
|
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 20, padding: '14px 0', flexWrap: 'wrap' }}
|
|
>
|
|
<Link
|
|
to="/"
|
|
className="display"
|
|
style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: '1.2rem', letterSpacing: '0.05em', color: 'var(--accent-bright)', textDecoration: 'none', fontWeight: 600 }}
|
|
>
|
|
<BrandLogo height={22} />
|
|
<MoonDot />
|
|
{siteTitle}
|
|
</Link>
|
|
<nav style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
|
{nav.map((l) =>
|
|
l.kind === 'section' ? (
|
|
<NavDropdown key={l.id} label={l.label} items={l.items} linkStyle={linkStyle} />
|
|
) : (
|
|
<NavLink key={l.kind === 'link' ? l.id : l.to} to={l.to} end={l.end} className="pill" style={linkStyle}>
|
|
{l.label}
|
|
</NavLink>
|
|
),
|
|
)}
|
|
{!loading && (
|
|
<NavLink
|
|
to={account.to}
|
|
className="pill"
|
|
style={{ marginLeft: 6, borderColor: 'var(--accent)', color: 'var(--accent-bright)' }}
|
|
>
|
|
{account.label}
|
|
</NavLink>
|
|
)}
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|