From 9d0a1970089e4e4afaa860a96f4a97a91e54f977 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 18 Aug 2026 07:24:37 -0500 Subject: [PATCH] feat(guilds): declare a second place on the guild page, for core's forum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mirror of the activity feed, one phase later. Core owns the Team forum — membership, manual grants and the member/guest split are all core's rules, and a module reimplementing any of them would be reimplementing a security boundary — but core publishes no Team page, because it does not own the word "guild". So this module declares the place and core puts the forum in it. TWO declarations rather than one, and that is the interesting part. A slot holds one component and the first fill wins, so folding the forum into `uo.guild.detail` alongside the feed would hand core the decision about where each of its two contributions sits on a page this module owns. Separate slots also keep them independent: with the forum switched off, the feed renders exactly as before. The registration test now asserts the set of declared slots and that EVERY one of them is rendered by the page that owns it, rather than naming a single slot twice. A slot nothing renders is a slot core fills into the void. Co-Authored-By: Claude --- client/src/entry.jsx | 8 ++++++++ client/src/routes/public/Guild.jsx | 5 +++++ client/test/registration.test.js | 18 ++++++++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/client/src/entry.jsx b/client/src/entry.jsx index 1c10dc5..ef9177d 100644 --- a/client/src/entry.jsx +++ b/client/src/entry.jsx @@ -194,6 +194,14 @@ registry.registerExtension(ID, 'player.invite.accepted', InviteGameAccountStep) // enough; on a core that knows nothing of Teams it simply stays empty. registry.declareModuleSlot(ID, 'uo.guild.detail') +// A SECOND place on the same page, for core's Team forum (TEAMS.md Part 5). Two +// declarations rather than one, because a slot holds one component and this module +// wants to decide where each of core's two contributions sits on its own page — +// the feed reads as part of the guild's story, the forum is a room you go into. +// Neither knows the other exists, and a core that fills only one leaves the other +// empty. +registry.declareModuleSlot(ID, 'uo.guild.forum') + // `module.json`'s `coreApi` range is checked by the loader before this file is // ever served, so there is nothing to re-check here. It is logged because a // mismatch between the core that validated the manifest and the core that diff --git a/client/src/routes/public/Guild.jsx b/client/src/routes/public/Guild.jsx index 91ce62c..945cc21 100644 --- a/client/src/routes/public/Guild.jsx +++ b/client/src/routes/public/Guild.jsx @@ -102,6 +102,11 @@ export default function Guild() { that does not fill it, or when there is nothing to show. The guild is named in OUR terms — core maps its own Team from these two. */} + + {/* And the Team forum, in its own place below the feed. Core resolves + who may read it — membership and manual grants are core's rules — + so this module renders the room and never its door policy. */} + )} diff --git a/client/test/registration.test.js b/client/test/registration.test.js index f3a124c..5378bea 100644 --- a/client/test/registration.test.js +++ b/client/test/registration.test.js @@ -209,16 +209,22 @@ it('registers under exactly one module id, matching the manifest', () => { assert.deepEqual([...owners], [manifest.id]) }) -it('declares its own guild-detail slot, for core to fill', () => { +it('declares its own guild slots, for core to fill', () => { // The inverted direction (TEAMS.md Part 3). Teams are a core primitive with no - // core page: core owns the activity feed and this module owns the word "guild", - // so this module declares the place and core puts the feed in it. - assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail']) + // core page: core owns the activity feed and the forum, this module owns the + // word "guild", so this module declares the places and core puts them in. + // + // TWO slots rather than one because a slot holds one component: stacking the + // feed and the forum into a single fill would take away this module's ability + // to place them separately on its own page. + assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail', 'uo.guild.forum']) }) -it('the declared slot is rendered by the page that owns it', () => { +it('every declared slot is rendered by the page that owns it', () => { // A slot nothing renders is a slot core fills into the void. Asserted against // the source rather than the chunk, since the chunk is minified. const page = fs.readFileSync(path.resolve(HERE, '..', 'src', 'routes', 'public', 'Guild.jsx'), 'utf8') - assert.match(page, /name="uo\.guild\.detail"/) + for (const name of registered.declaredSlots) { + assert.match(page, new RegExp(`name="${name.replace(/\./g, '\.')}"`)) + } })