feat(modules): a third slot, nav icons, and api.BASE (phase 3, slice 3)

The three things core owes the client half before it can leave, all additive,
all MODULE_API 1.2.0 → 1.3.0.

`player.invite.accepted` is the third extension slot. Core's invite page owned a
UO game-account step — it read a `gameAccountSignup` flag out of core's own
settings and posted to a shard route — and an invite is a core concept that
staff receive too, so the page stays and its optional next step becomes a slot.
Named for the place, like the other two. Whether there is a step at all is the
filling module's call, made from data core does not have; core keeps the shell,
the skip control and the destination.

`icon` on a nav item, because without it the six extracted UO rows would have
been the only text-only entries in a sidebar where every other row has a glyph.
Core supplies no fallback — an invented one is core making a presentation choice
for content it knows nothing about. `icon` was already among the fields an
override may not touch, so the concept predates a module being able to send one.

`api.BASE` was in §3.5 from the first draft and never actually published.
`request` is fetch-only, so an EventSource builds its own URL, and the shard's
live feed is two of them; the alternative is a module hardcoding `/api/v1`,
which asserts something about core that core has not promised.

`AcceptInvite` is the one legitimate reader of `extensionFor` outside Slot.jsx:
the answer decides a NAVIGATION, not a decoration. Decoration goes inside
`<Slot wrap>`, which is why `hasExtension` stayed deleted.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:39:36 -05:00
parent 91964c8898
commit 5b5006c365
9 changed files with 142 additions and 68 deletions

View File

@@ -79,8 +79,18 @@ export function registerRoutes(id, byArea) {
* core group, `order` sorts within it, and an unknown group name appends rather
* than dropping the item — a mis-typed group must cost a position, never a link.
*
* `icon` is a component core renders exactly as it renders its own rows' icons
* (1.3.0). It exists because without it the six UO rows would have extracted as
* the only text-only entries in a sidebar where every other row has a glyph,
* which reads as breakage rather than as a design. Core does not supply a
* fallback: a module that omits it gets no icon, the same as a core row that
* omits it, and inventing one would be core making a presentation choice for
* content it knows nothing about. Note that `icon` is already among the fields
* an override may not touch (lib/navOverrides.js) — the concept predates a
* module being able to supply one.
*
* @param {string} id
* @param {{area: string, items: Array<{label, to, group?, order?, roles?, feature?}>}} spec
* @param {{area: string, items: Array<{label, to, group?, order?, roles?, feature?, icon?}>}} spec
*/
export function registerNav(id, spec) {
const { area, items } = spec || {}
@@ -92,10 +102,10 @@ export function registerNav(id, spec) {
/**
* The hook that answers "which of this module's features may this viewer see".
*
* Core keeps a generic flag context and owns none of the semantics — `uo` fills
* its namespace with today's `useShardFeatures` (MODULE_SYSTEM.md §1.5). With no
* module installed the nav filter is a correct no-op, because no core nav item
* carries a `feature` today.
* Core keeps a generic flag context and owns none of the semantics
* (MODULE_SYSTEM.md §1.5). With no module installed the nav filter is a correct
* no-op, because no core nav item carries a `feature` — which has been literally
* true since Phase 3 slice 3 took the nine shard-gated rows out.
*/
export function registerFeatureProvider(id, namespace, hook) {
providers.set(namespace, { id, hook })