From 9d559091c580c8d56f9c37b3f1f0257b2e0d633c Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 11 Aug 2026 18:54:42 -0500 Subject: [PATCH] fix(client): give the portal row an icon, and assert every gated nav has one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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 --- client/src/entry.jsx | 4 ++-- client/src/icons.jsx | 28 ++++++++++++++++++++++++++++ client/test/registration.test.js | 18 +++++++++++++++--- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/client/src/entry.jsx b/client/src/entry.jsx index 3a8d55f..3b78cf0 100644 --- a/client/src/entry.jsx +++ b/client/src/entry.jsx @@ -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 ─────────────────────────────────────────────────────── diff --git a/client/src/icons.jsx b/client/src/icons.jsx index 76242eb..f833945 100644 --- a/client/src/icons.jsx +++ b/client/src/icons.jsx @@ -35,4 +35,32 @@ export const IconShard = () => ( ) +/** + * 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 = () => ( + +) + export default IconShard diff --git a/client/test/registration.test.js b/client/test/registration.test.js index 0a640ad..24e536f 100644 --- a/client/test/registration.test.js +++ b/client/test/registration.test.js @@ -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 `` 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`) + } } })