From 163b0a7493e606a788511ae8bf0cd87bcbfdee77 Mon Sep 17 00:00:00 2001 From: Seanathon Date: Fri, 26 Jun 2026 23:19:55 -0700 Subject: [PATCH] fix(ui): composed empty-state copy was shadowed by inherited board type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A composed grid board inherits type "inspiration" for its card chrome, and emptyVoice() checked `type === 'inspiration'` BEFORE the descriptor.empty_state branch — so a board like 'Wish List' showed the seeded Inspiration copy ('Nothing pinned yet.') instead of its own composer-written copy. Move the empty_state check to the top of emptyVoice so composer-written copy wins for any board that carries it; the seeded boards have no empty_state and keep their hardcoded, AI-aware voice. Regression test uses type:'inspiration' + empty_state to lock the ordering. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/collections-ui.js | 14 +++++++++----- src/collections-ui.test.ts | 11 +++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/collections-ui.js b/src/collections-ui.js index f2acc70..e1ab5a4 100644 --- a/src/collections-ui.js +++ b/src/collections-ui.js @@ -184,6 +184,13 @@ export function boardPurpose(collection) { function emptyVoice(collection, aiOn) { const id = collection && collection.id; const type = collection && collection.type; + // Composer-written copy wins for ANY board that carries it. This MUST come before the + // type-based branches below: a composed grid board inherits type "inspiration" (for + // card chrome), which would otherwise shadow its bespoke empty_state with the seeded + // Inspiration copy. The seeded boards have no empty_state, so they skip this and keep + // their hardcoded, AI-aware voice. + const es = collection && collection.descriptor && collection.descriptor.empty_state; + if (es && es.head && es.body) return { head: es.head, body: es.body }; if (id === "inbox") { return { head: "Inbox zero.", @@ -206,11 +213,8 @@ function emptyVoice(collection, aiOn) { : "Library is for things worth reading twice. Save a link and it is kept as a clean, readable bookmark.", }; } - // Composed / custom board: lead with the bespoke empty-state copy the composer wrote - // for it (a moment of delight in the board's own voice, Story C). Guardrails guarantee - // both halves are non-empty when present; otherwise fall back to the generic stance. - const es = collection && collection.descriptor && collection.descriptor.empty_state; - if (es && es.head && es.body) return { head: es.head, body: es.body }; + // Composed / custom board with no bespoke copy (e.g. created with AI off): fall back + // to the generic stance + purpose line. return { head: "This board is ready.", body: boardPurpose(collection) }; } diff --git a/src/collections-ui.test.ts b/src/collections-ui.test.ts index 30db26a..72a1c32 100644 --- a/src/collections-ui.test.ts +++ b/src/collections-ui.test.ts @@ -387,12 +387,15 @@ test("renderEmptyState: composed board leads with its descriptor purpose", () => }); test("renderEmptyState: composed board prefers descriptor.empty_state copy (Story C delight)", () => { + // type:"inspiration" is the regression: a composed grid board inherits that type for + // card chrome, and it MUST NOT shadow the board's bespoke empty_state copy. const html = renderEmptyState({ - id: "moodboard", name: "Mood", view: "grid", - descriptor: { empty_state: { head: "Nothing in the mood yet.", body: "Drop a reference and it joins the wall." } }, + id: "wish-list-sblv", name: "Wish List", type: "inspiration", view: "grid", + descriptor: { empty_state: { head: "Nothing wanted yet.", body: "Drop the first product you have your eye on." } }, }); - assert.ok(html.includes("Nothing in the mood yet."), "uses the composed head, not the generic 'This board is ready.'"); - assert.ok(html.includes("Drop a reference and it joins the wall."), "uses the composed body"); + assert.ok(html.includes("Nothing wanted yet."), "uses the composed head, not the inherited Inspiration copy"); + assert.ok(html.includes("Drop the first product you have your eye on."), "uses the composed body"); + assert.ok(!html.includes("Nothing pinned yet."), "the type=inspiration seeded copy must not leak through"); assert.ok(!html.includes("This board is ready."), "generic fallback suppressed when empty_state present"); });