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:
@@ -3,22 +3,22 @@
|
||||
// Phase 2, PR 8 of docs/website/MODULE_SYSTEM.md §2.7 (§1.5 states the problem);
|
||||
// the contract is docs/website/MODULE_API.md §3.3.
|
||||
//
|
||||
// Ten of the sixteen rows in the public header carry a `feature`, and every one
|
||||
// of them is a shard surface an admin can disable or gate to a higher audience.
|
||||
// The provider that answers those questions — `useShardFeatures` — moves out
|
||||
// with the module, so core cannot keep calling it directly and still be a core.
|
||||
// It keeps a generic seam instead, and the module fills it.
|
||||
// Nine of the sixteen rows in the public header used to carry a `feature`, and
|
||||
// every one of them was a shard surface an admin can disable or gate to a higher
|
||||
// audience. The provider that answered those questions moved out with the module
|
||||
// in Phase 3 slice 3, and core cannot call it directly and still be a core. It
|
||||
// keeps this generic seam instead, and the module fills it.
|
||||
//
|
||||
// **The namespace comes from the registration, not from the string.** A row's
|
||||
// `feature` is resolved by the provider its OWN module registered, so a module
|
||||
// author writes `feature: 'status'` exactly as it reads today: nothing parses a
|
||||
// prefix, and a typo'd namespace is not a thing that can exist. Core's own rows
|
||||
// carry no `moduleId` and resolve against the owner id `core`, which is what
|
||||
// core registers `useShardFeatures` under until Phase 3 moves those rows into
|
||||
// the module and they arrive stamped `uo` instead.
|
||||
// carry no `moduleId` and resolve against the owner id `core` — which nothing
|
||||
// registers now that the shard rows are gone, and that is the correct resting
|
||||
// state rather than a gap: no core nav row carries a `feature`.
|
||||
//
|
||||
// Everything here fails OPEN, and that is deliberate and unchanged from
|
||||
// useShardFeatures' own posture: this is presentation, the gate is server-side
|
||||
// Everything here fails OPEN, and that is deliberate: this is presentation, the
|
||||
// gate is server-side
|
||||
// (a disabled feature 404s and an out-of-rung one 403s whether or not a link was
|
||||
// rendered), so an unknown answer shows the link rather than blanking the nav.
|
||||
// The one thing a UI mistake must never do here is hide a page from someone
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -37,7 +37,7 @@ import { Loading, ErrorState, EmptyState } from '../components/PageState.jsx'
|
||||
import { useAsync } from '../lib/useAsync.js'
|
||||
import { useAuth } from '../contexts/AuthContext.jsx'
|
||||
import { useSite } from '../contexts/SiteContext.jsx'
|
||||
import { request, ApiError } from '../api/client.js'
|
||||
import { request, ApiError, BASE } from '../api/client.js'
|
||||
|
||||
// The UI kit is CURATED AND CLOSED (§3.4), not a re-export of components/. These
|
||||
// seven are what the smallest UO page already needs beyond React and the router:
|
||||
@@ -66,11 +66,16 @@ const ui = {
|
||||
useSite,
|
||||
}
|
||||
|
||||
// The request PRIMITIVE, not the `api` object (§3.5). `api.atlas` and `api.shard`
|
||||
// are module bindings that only still live in core's client because Phase 3 has
|
||||
// not moved them; a module builds its own namespace over `request` and owns the
|
||||
// paths it calls — which is right, because it owns the routes at the other end.
|
||||
const api = { request, ApiError }
|
||||
// The request PRIMITIVE, not the `api` object (§3.5): a module builds its own
|
||||
// namespace over `request` and owns the paths it calls, which is right, because
|
||||
// it owns the routes at the other end.
|
||||
//
|
||||
// `BASE` was in §3.5 from the start and missing from this object until slice 3,
|
||||
// which is when something first needed it. `request` is fetch-only, so an
|
||||
// EventSource — the shard's live feed is two of them — has to build its own URL,
|
||||
// and the alternative is a module hardcoding `/api/v1`: an assertion about where
|
||||
// core mounts its API that core has never promised to keep.
|
||||
const api = { request, ApiError, BASE }
|
||||
|
||||
/**
|
||||
* Publish `window.__rg`. Called by main.jsx before it renders, and before any
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
// that the two files can drift, so a test asserts they agree
|
||||
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
|
||||
// both.
|
||||
// 1.3.0 — three additions, all from Phase 3 slice 3 needing them: a nav item may
|
||||
// carry an `icon` component (§3.3), core declares a third slot
|
||||
// `player.invite.accepted` (§3.7), and `window.__rg.api` gained `BASE`, which
|
||||
// §3.5 always documented and shared.js never published. Additive throughout: a
|
||||
// module written against 1.2.0 is unaffected. The server half is untouched and
|
||||
// bumps anyway, for the reason below.
|
||||
// 1.2.0 — `registry` gained `registerExtension` and core gained extension slots
|
||||
// (MODULE_API.md §3.7). The first change to window.__rg since 1.0.0, and an
|
||||
// addition: a module that never fills a slot is unaffected. The server half is
|
||||
@@ -20,4 +26,4 @@
|
||||
// but the two halves state ONE version: a module declares a single coreApi range
|
||||
// and is served one chunk, so a client that claimed 1.0.0 while the server
|
||||
// answered 1.1.0 would be two answers to one question.
|
||||
export const MODULE_API_VERSION = '1.2.0'
|
||||
export const MODULE_API_VERSION = '1.3.0'
|
||||
|
||||
Reference in New Issue
Block a user