feat(client): the whole client half (phase 3, slice 3) #4

Merged
whitlocktech merged 4 commits from feature/module-extract-client into main 2026-08-12 00:35:35 +00:00
3 changed files with 45 additions and 5 deletions
Showing only changes of commit 9d559091c5 - Show all commits

View File

@@ -18,7 +18,7 @@
// size and it is the one thing this seam cannot have.
import { registry, coreApiVersion } from './core.js'
import { IconShard } from './icons.jsx'
import { IconShard, IconUser } from './icons.jsx'
import { useShardFlags } from './lib/useShardFeatures.js'
// Public pages — the twelve that used to live at /site/*.
@@ -159,7 +159,7 @@ registry.registerNav(ID, {
area: 'player',
// Order 0: Characters is the portal's first row today, and with the module
// installed it is also what core's `/player` index resolves to.
items: [{ label: 'Characters', to: '/player/uo/characters', order: 0 }],
items: [{ label: 'Characters', to: '/player/uo/characters', icon: IconUser, order: 0 }],
})
// ── Feature provider ───────────────────────────────────────────────────────

View File

@@ -35,4 +35,32 @@ export const IconShard = () => (
</Icon>
)
/**
* A figure — the glyph core used for the portal's "Characters" row.
*
* A second icon rather than reusing IconShard, because these two rows sit in
* different navs and each matched its neighbours before the extraction: the
* admin sidebar's UO rows were all gems, and the portal's Characters row was a
* person beside Appeals' shield and Account's gear. Copied from core's
* PlayerPortalLayout, which uses a 16px frame and a heavier stroke than the
* admin one — matching the nav a row lands in is the whole reason `icon` exists.
*/
export const IconUser = () => (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<circle cx="12" cy="8" r="4" />
<path d="M4 21a8 8 0 0 1 16 0" />
</svg>
)
export default IconShard

View File

@@ -139,9 +139,21 @@ it('every nav row points at a route this module actually registered', () => {
}
})
it('admin nav rows carry an icon; core rows all have one and a text-only row reads as breakage', () => {
for (const row of registered.nav.admin) {
assert.equal(typeof row.icon, 'function', `admin nav row "${row.label}" has no icon`)
it('every admin and player nav row carries an icon', () => {
// Both of those navs render a glyph on every core row, so a row without one
// reads as breakage rather than as a design. The PUBLIC header is text
// buttons and is deliberately excluded.
//
// The player half of this assertion is not symmetry for its own sake. Core's
// PlayerPortalLayout rendered `<n.icon />` UNGUARDED — fine for as long as
// every row in it was core's own and had one, and React error #130 with a
// blank portal the moment a module registered one without. Core is guarded
// now, but a missing icon there is still a visible defect and this is the
// cheap place to catch it.
for (const area of ['admin', 'player']) {
for (const row of registered.nav[area]) {
assert.equal(typeof row.icon, 'function', `${area} nav row "${row.label}" has no icon`)
}
}
})