Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/collections-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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) };
}

Expand Down
11 changes: 7 additions & 4 deletions src/collections-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down
Loading