fix(client): give the portal row an icon, and assert every gated nav has one
The §7.7 smoke, running the pair together: registering the player row without an `icon` blanked the whole portal with React error #130, because core's PlayerPortalLayout rendered `<n.icon />` unguarded. Core is guarded now (website), and this is the other half — the row had an icon before it moved and should have kept one. A second glyph rather than reusing IconShard: these two rows sit in different navs and each matched its neighbours before the extraction. The admin sidebar's UO rows were gems; the portal's Characters row was a person beside Appeals' shield and Account's gear. Matching the nav a row lands in is the whole reason `icon` is in the contract. `registration.test.js` now asserts it for admin AND player rows, which is the cheap place to catch the next one. The public header is text buttons and is deliberately excluded. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
// size and it is the one thing this seam cannot have.
|
// size and it is the one thing this seam cannot have.
|
||||||
|
|
||||||
import { registry, coreApiVersion } from './core.js'
|
import { registry, coreApiVersion } from './core.js'
|
||||||
import { IconShard } from './icons.jsx'
|
import { IconShard, IconUser } from './icons.jsx'
|
||||||
import { useShardFlags } from './lib/useShardFeatures.js'
|
import { useShardFlags } from './lib/useShardFeatures.js'
|
||||||
|
|
||||||
// Public pages — the twelve that used to live at /site/*.
|
// Public pages — the twelve that used to live at /site/*.
|
||||||
@@ -159,7 +159,7 @@ registry.registerNav(ID, {
|
|||||||
area: 'player',
|
area: 'player',
|
||||||
// Order 0: Characters is the portal's first row today, and with the module
|
// 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.
|
// 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 ───────────────────────────────────────────────────────
|
// ── Feature provider ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -35,4 +35,32 @@ export const IconShard = () => (
|
|||||||
</Icon>
|
</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
|
export default IconShard
|
||||||
|
|||||||
@@ -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', () => {
|
it('every admin and player nav row carries an icon', () => {
|
||||||
for (const row of registered.nav.admin) {
|
// Both of those navs render a glyph on every core row, so a row without one
|
||||||
assert.equal(typeof row.icon, 'function', `admin nav row "${row.label}" has no icon`)
|
// 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`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user