openvibers/openvibe.quest Commits
Quest cards: the rewards as one row of small chips, each quest's own mark (#283)
Changes
@@@ -265,3 +265,29 @@ p.lede { font-size: 1.05rem; color: var(--text-secondary, #96a7c2); max-width: 7/* Under a site's own heading a card does not repeat the site (say it once); the chip stays wherever a card standsalone, on a quest's page and in the quest log. */.site-group .quest .q-site-chip { display: none; }++++/* ── Quest cards, condensed (Opus, 2026-11-11; asks 61-62) ──++ The medal is the quest's own mark on its colour; the rewards are one row of small chips (the badge's mark and name,++ the item's emoji and name, the coin and amount), with "Earned by N" at the end of the same row. Hover lifts the card++ and turns the medal a little; a tap gives the same press. Phones: a 44 px medal and tighter padding. */++.quest { gap: 12px; padding: 14px; }++.quest .medal { width: 48px; height: 48px; transition: transform .25s cubic-bezier(.2, .8, .3, 1.3); }++.quest:hover .medal, .quest:focus-within .medal { transform: rotate(-8deg) scale(1.06); }++.quest:active { transform: scale(.995); }++.quest-why { margin: 3px 0 8px; }++.quest-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px; margin: 0; }++.reward { gap: 6px; }++.reward-badge, .reward-coins, .reward-item { display: inline-flex; align-items: center; gap: 5px; min-height: 26px; padding: 2px 10px 2px 7px; font-size: .78rem; line-height: 1.2; }++.reward-badge .q-glyph, .reward-badge .ov-icon { color: hsl(var(--h, 220) 80% 72%); }++.quest .reward-badge { border-color: color-mix(in srgb, var(--text-primary, #e6edf7) 16%, transparent); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); }++.reward-coins strong { color: inherit; }++.quest-count { display: inline-flex; align-items: center; gap: 5px; margin: 0; font-size: .8rem; }++.quest-count .q-glyph { color: inherit; }++.quest .quest-steps { margin-bottom: 8px; }++/* A step's words stay beside its mark and wrap there (they dropped to their own line under it). */++.quest-steps li > span:not([class]) { flex: 1 1 12ch; min-width: 0; }++@media (max-width: 520px) {++ .quest { grid-template-columns: 44px minmax(0, 1fr); padding: 12px; gap: 10px; }++ .quest .medal { width: 44px; height: 44px; }++}++@media (prefers-reduced-motion: reduce) { .quest .medal { transition: none; } .quest:hover .medal, .quest:focus-within .medal { transform: none; } .quest:active { transform: none; } }@@@ -12,19 +12,21 @@ const icons = require('./quest-icons');const { html } = require('./html');/**-- * What a quest pays: its badge (a text label), for some an item (OpenVibe.Inventory) and its OpenCoins. Every chip-- * says its type and what it is for, and the coin chip keeps its amount even while OpenCoins are off, so the catalog's-- * promise is visible rather than replaced by "soon". Never an "up to": the catalog's amount is the whole amount.++ * What a quest pays, as one row of small chips (Opus, 2026-11-11: "turn the ugly badge text list into condensed icons"):++ * the badge with its own mark and name, the item with its emoji and name, the OpenCoins with the coin and the amount.++ * What each one is ("a badge on your profile", "a hat for your inventory", "start soon") is in the chip's title and in++ * text a screen reader reads, so nothing is lost; the coin chip keeps its amount even while OpenCoins are off (dashed),++ * so the catalog's promise is visible rather than replaced by "soon". Never an "up to".*/function reward(quest, { coinsEnabled = false } = {}) {const badge = quest.reward.badge.name;-- const badgeChip = html`<span class="reward-badge">Badge: <strong>${badge}</strong> <small>on your profile</small></span>`;++ const badgeChip = html`<span class="reward-badge" title="A badge on your profile">${icons.glyph(icons.badge(quest.reward.badge.id, quest), 14)}<span class="sr-only">Badge: </span><strong>${badge}</strong><span class="sr-only"> on your profile</span></span>`;// An item for the person's OpenVibe.Inventory (server/quests/items.js), worn in chat and on their profile.const it = quest.reward.item;-- const itemChip = it ? html`<span class="reward-item" title="${it.kind} for your OpenVibe inventory"><span aria-hidden="true">${it.emoji}</span> <strong>${it.name}</strong> <small>${it.kind} for your inventory</small></span>` : '';++ const itemChip = it ? html`<span class="reward-item" title="${it.kind} for your OpenVibe inventory"><span aria-hidden="true">${it.emoji}</span> <strong>${it.name}</strong><span class="sr-only">, ${it.kind} for your inventory</span></span>` : '';if (quest.reward.coins <= 0) return html`<span class="reward">${badgeChip}${itemChip}</span>`;-- if (!coinsEnabled) return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins soon"><strong>${quest.reward.coins}</strong> OpenCoins, <small>start soon</small></span></span>`;-- return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins"><strong>${quest.reward.coins}</strong> OpenCoins</span></span>`;++ if (!coinsEnabled) return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins soon" title="OpenCoins start soon">${icons.glyph('coin', 14)}<strong>${quest.reward.coins}</strong> OpenCoins<span class="sr-only">, start soon</span></span></span>`;++ return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins" title="${quest.reward.coins} OpenCoins">${icons.glyph('coin', 14)}<strong>${quest.reward.coins}</strong> OpenCoins</span></span>`;}/**@@@ -120,7 +122,7 @@ const seriesSvg = (series) => art.svg({ hue: hueOf(series.id), mark: String(seri/** "Earned by 3 people": how many have a quest's badge (a count, never who), or nothing at 0. */function earnedBy(n) {-- return n > 0 ? html`<p class="quest-count">Earned by ${n} ${n === 1 ? 'person' : 'people'}</p>` : '';++ return n > 0 ? html`<span class="quest-count">${icons.glyph('star', 13)}Earned by ${n} ${n === 1 ? 'person' : 'people'}</span>` : '';}/**@@@ -157,15 +159,14 @@ function card(quest, { coinsEnabled = false, earned = false, counts = null, seenconst date = completedAt ? String(completedAt).slice(0, 10) : '';// A completion means every step reached its target; show that even if the progress rows were not the reason.const stepSeen = done && seen ? new Map(quest.steps.map((s, i) => [i, s.count])) : seen;-- return html`<li class="quest${done ? ' is-completed' : ''}">++ return html`<li class="quest${done ? ' is-completed' : ''}" style="--h:${hueOf(quest.id)}">${medal(quest, { earned: done })}<div class="quest-main"><a class="quest-title" href="/quests/${quest.id}">${quest.title}</a> <span class="q-site-chip">${icons.glyph(icons.site(quest.site), 16)}${shortSite(quest.site)}</span> ${stateChip(done)}<p class="quest-why">${quest.why}</p><ul class="quest-steps">${quest.steps.map((s, i) => stepLine(s, i, stepSeen))}</ul>-- <p class="quest-reward">${reward(quest, { coinsEnabled })}</p>++ <div class="quest-foot"><p class="quest-reward">${reward(quest, { coinsEnabled })}</p>${counts ? earnedBy(counts.get(quest.id) || 0) : ''}</div>${done && completedAt ? html`<p class="quest-earned">Completed <time datetime="${completedAt}">${date}</time> · Badge: <strong>${quest.reward.badge.name}</strong></p>` : ''}-- ${counts ? earnedBy(counts.get(quest.id) || 0) : ''}</div></li>`;}@@@ -181,7 +182,7 @@ function seriesCard(series, { coinsEnabled = false, counts = null } = {}) {<a class="quest-title" href="/series/${series.id}">${series.title}</a><p class="quest-why">${series.why}</p><p class="quest-reward"><span class="reward"><span class="reward-badge"><strong>${n}</strong> quests, a badge each</span>${coins > 0 ? html`<span class="reward-coins${coinsEnabled ? '' : ' soon'}"><strong>${coins}</strong> OpenCoins in all${coinsEnabled ? '' : html`, <small>start soon</small>`}</span>` : ''}</span></p>-- ${done > 0 ? html`<p class="quest-count">${done} ${done === 1 ? 'badge' : 'badges'} earned in this series</p>` : ''}++ ${done > 0 ? html`<p class="quest-foot"><span class="quest-count">${icons.glyph('star', 13)}${done} ${done === 1 ? 'badge' : 'badges'} earned in this series</span></p>` : ''}</div></li>`;}@@@ -196,7 +197,7 @@ function appCard(q, { seen = null, completedAt = null } = {}) {<a class="quest-title" href="/apps/quests/${q.id}">${q.title}</a> ${stateChip(done)}${q.why ? html`<p class="quest-why">${q.why}</p>` : ''}<ul class="quest-steps">${q.steps.map((s, i) => stepLine(s, i, stepSeen))}</ul>-- <p class="quest-reward"><span class="reward"><span class="reward-badge">Badge: <strong>${q.reward.badge.name}</strong> <small>on your profile</small></span></span></p>++ <p class="quest-reward"><span class="reward"><span class="reward-badge" title="A badge on your profile">${icons.glyph(icons.badge(q.reward.badge.id, q), 14)}<span class="sr-only">Badge: </span><strong>${q.reward.badge.name}</strong><span class="sr-only"> on your profile</span></span></span></p>${done && completedAt ? html`<p class="quest-earned">Completed <time datetime="${completedAt}">${String(completedAt).slice(0, 10)}</time> · Badge: <strong>${q.reward.badge.name}</strong></p>` : ''}${q.url ? html`<p class="quest-count"><a href="${q.url}" rel="nofollow noopener">Where to do it</a></p>` : ''}</div>@@@ -41,7 +41,7 @@ const NOW = '2026-10-01T10:00:00.000Z';assert.ok(card.includes('Three broadcasts is a habit'), 'the one-line description');assert.ok(card.includes('Start a stream') && card.includes('1/3 streams'), 'what it counts, with progress and its unit');assert.ok(card.includes('role="progressbar"') && card.includes('aria-valuenow="1"') && card.includes('aria-valuemax="3"'), 'the bar and its values');-- assert.ok(card.includes('Badge: <strong>Regular Broadcaster</strong>'), 'the badge reward, named');++ assert.ok(card.includes('<span class="sr-only">Badge: </span><strong>Regular Broadcaster</strong>'), 'the badge reward, named');assert.ok(card.includes('25</strong> OpenCoins'), 'the coin amount, even while OpenCoins are off');assert.ok(card.includes('start soon'), 'and that they do not move yet');assert.ok(!card.includes('up to'), 'never an up-to figure');@@@ -54,7 +54,7 @@ const NOW = '2026-10-01T10:00:00.000Z';assert.ok(card.includes('class="quest is-completed"'), 'the card is dimmed by its class');assert.ok(card.includes('Completed'), 'the state as text');assert.ok(card.includes(`<time datetime="${NOW}">2026-10-01</time>`), 'the date it was earned');-- assert.ok(card.includes('Badge: <strong>First Broadcast</strong>'), 'the badge');++ assert.ok(card.includes('<span class="sr-only">Badge: </span><strong>First Broadcast</strong>'), 'the badge');assert.ok(card.includes('1/1'), 'a completed step reads 1/1, not the missing progress row');const open = render(renderQuests.card(q, { coinsEnabled: false, seen: new Map() }));assert.ok(!open.includes('Completed <time'), 'not completed: the completion line is left out');
@@ -265,3 +265,29 @@ p.lede { font-size: 1.05rem; color: var(--text-secondary, #96a7c2); max-width: 7
@@ -265,3 +265,29 @@ p.lede { font-size: 1.05rem; color: var(--text-secondary, #96a7c2); max-width: 7
/* Under a site's own heading a card does not repeat the site (say it once); the chip stays wherever a card stands
/* Under a site's own heading a card does not repeat the site (say it once); the chip stays wherever a card stands
alone, on a quest's page and in the quest log. */
alone, on a quest's page and in the quest log. */
.site-group .quest .q-site-chip { display: none; }
.site-group .quest .q-site-chip { display: none; }
+
+/* ── Quest cards, condensed (Opus, 2026-11-11; asks 61-62) ──
+ The medal is the quest's own mark on its colour; the rewards are one row of small chips (the badge's mark and name,
+ the item's emoji and name, the coin and amount), with "Earned by N" at the end of the same row. Hover lifts the card
+ and turns the medal a little; a tap gives the same press. Phones: a 44 px medal and tighter padding. */
+.quest { gap: 12px; padding: 14px; }
+.quest .medal { width: 48px; height: 48px; transition: transform .25s cubic-bezier(.2, .8, .3, 1.3); }
+.quest:hover .medal, .quest:focus-within .medal { transform: rotate(-8deg) scale(1.06); }
+.quest:active { transform: scale(.995); }
+.quest-why { margin: 3px 0 8px; }
+.quest-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px; margin: 0; }
+.reward { gap: 6px; }
+.reward-badge, .reward-coins, .reward-item { display: inline-flex; align-items: center; gap: 5px; min-height: 26px; padding: 2px 10px 2px 7px; font-size: .78rem; line-height: 1.2; }
+.reward-badge .q-glyph, .reward-badge .ov-icon { color: hsl(var(--h, 220) 80% 72%); }
+.quest .reward-badge { border-color: color-mix(in srgb, var(--text-primary, #e6edf7) 16%, transparent); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); }
+.reward-coins strong { color: inherit; }
+.quest-count { display: inline-flex; align-items: center; gap: 5px; margin: 0; font-size: .8rem; }
+.quest-count .q-glyph { color: inherit; }
+.quest .quest-steps { margin-bottom: 8px; }
+/* A step's words stay beside its mark and wrap there (they dropped to their own line under it). */
+.quest-steps li > span:not([class]) { flex: 1 1 12ch; min-width: 0; }
+@media (max-width: 520px) {
+ .quest { grid-template-columns: 44px minmax(0, 1fr); padding: 12px; gap: 10px; }
+ .quest .medal { width: 44px; height: 44px; }
+}
+@media (prefers-reduced-motion: reduce) { .quest .medal { transition: none; } .quest:hover .medal, .quest:focus-within .medal { transform: none; } .quest:active { transform: none; } }
@@ -12,19 +12,21 @@ const icons = require('./quest-icons');
@@ -12,19 +12,21 @@ const icons = require('./quest-icons');
const { html } = require('./html');
const { html } = require('./html');
/**
/**
- * What a quest pays: its badge (a text label), for some an item (OpenVibe.Inventory) and its OpenCoins. Every chip
- * says its type and what it is for, and the coin chip keeps its amount even while OpenCoins are off, so the catalog's
- * promise is visible rather than replaced by "soon". Never an "up to": the catalog's amount is the whole amount.
+ * What a quest pays, as one row of small chips (Opus, 2026-11-11: "turn the ugly badge text list into condensed icons"):
+ * the badge with its own mark and name, the item with its emoji and name, the OpenCoins with the coin and the amount.
+ * What each one is ("a badge on your profile", "a hat for your inventory", "start soon") is in the chip's title and in
+ * text a screen reader reads, so nothing is lost; the coin chip keeps its amount even while OpenCoins are off (dashed),
+ * so the catalog's promise is visible rather than replaced by "soon". Never an "up to".
*/
*/
function reward(quest, { coinsEnabled = false } = {}) {
function reward(quest, { coinsEnabled = false } = {}) {
const badge = quest.reward.badge.name;
const badge = quest.reward.badge.name;
- const badgeChip = html`<span class="reward-badge">Badge: <strong>${badge}</strong> <small>on your profile</small></span>`;
+ const badgeChip = html`<span class="reward-badge" title="A badge on your profile">${icons.glyph(icons.badge(quest.reward.badge.id, quest), 14)}<span class="sr-only">Badge: </span><strong>${badge}</strong><span class="sr-only"> on your profile</span></span>`;
// An item for the person's OpenVibe.Inventory (server/quests/items.js), worn in chat and on their profile.
// An item for the person's OpenVibe.Inventory (server/quests/items.js), worn in chat and on their profile.
const it = quest.reward.item;
const it = quest.reward.item;
- const itemChip = it ? html`<span class="reward-item" title="${it.kind} for your OpenVibe inventory"><span aria-hidden="true">${it.emoji}</span> <strong>${it.name}</strong> <small>${it.kind} for your inventory</small></span>` : '';
+ const itemChip = it ? html`<span class="reward-item" title="${it.kind} for your OpenVibe inventory"><span aria-hidden="true">${it.emoji}</span> <strong>${it.name}</strong><span class="sr-only">, ${it.kind} for your inventory</span></span>` : '';
if (quest.reward.coins <= 0) return html`<span class="reward">${badgeChip}${itemChip}</span>`;
if (quest.reward.coins <= 0) return html`<span class="reward">${badgeChip}${itemChip}</span>`;
- if (!coinsEnabled) return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins soon"><strong>${quest.reward.coins}</strong> OpenCoins, <small>start soon</small></span></span>`;
- return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins"><strong>${quest.reward.coins}</strong> OpenCoins</span></span>`;
+ if (!coinsEnabled) return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins soon" title="OpenCoins start soon">${icons.glyph('coin', 14)}<strong>${quest.reward.coins}</strong> OpenCoins<span class="sr-only">, start soon</span></span></span>`;
+ return html`<span class="reward">${badgeChip}${itemChip}<span class="reward-coins" title="${quest.reward.coins} OpenCoins">${icons.glyph('coin', 14)}<strong>${quest.reward.coins}</strong> OpenCoins</span></span>`;
}
}
/**
/**
@@ -120,7 +122,7 @@ const seriesSvg = (series) => art.svg({ hue: hueOf(series.id), mark: String(seri
@@ -120,7 +122,7 @@ const seriesSvg = (series) => art.svg({ hue: hueOf(series.id), mark: String(seri
/** "Earned by 3 people": how many have a quest's badge (a count, never who), or nothing at 0. */
/** "Earned by 3 people": how many have a quest's badge (a count, never who), or nothing at 0. */
function earnedBy(n) {
function earnedBy(n) {
- return n > 0 ? html`<p class="quest-count">Earned by ${n} ${n === 1 ? 'person' : 'people'}</p>` : '';
+ return n > 0 ? html`<span class="quest-count">${icons.glyph('star', 13)}Earned by ${n} ${n === 1 ? 'person' : 'people'}</span>` : '';
}
}
/**
/**
@@ -157,15 +159,14 @@ function card(quest, { coinsEnabled = false, earned = false, counts = null, seen
@@ -157,15 +159,14 @@ function card(quest, { coinsEnabled = false, earned = false, counts = null, seen
const date = completedAt ? String(completedAt).slice(0, 10) : '';
const date = completedAt ? String(completedAt).slice(0, 10) : '';
// A completion means every step reached its target; show that even if the progress rows were not the reason.
// A completion means every step reached its target; show that even if the progress rows were not the reason.
const stepSeen = done && seen ? new Map(quest.steps.map((s, i) => [i, s.count])) : seen;
const stepSeen = done && seen ? new Map(quest.steps.map((s, i) => [i, s.count])) : seen;
- return html`<li class="quest${done ? ' is-completed' : ''}">
+ return html`<li class="quest${done ? ' is-completed' : ''}" style="--h:${hueOf(quest.id)}">
${medal(quest, { earned: done })}
${medal(quest, { earned: done })}
<div class="quest-main">
<div class="quest-main">
<a class="quest-title" href="/quests/${quest.id}">${quest.title}</a> <span class="q-site-chip">${icons.glyph(icons.site(quest.site), 16)}${shortSite(quest.site)}</span> ${stateChip(done)}
<a class="quest-title" href="/quests/${quest.id}">${quest.title}</a> <span class="q-site-chip">${icons.glyph(icons.site(quest.site), 16)}${shortSite(quest.site)}</span> ${stateChip(done)}
<p class="quest-why">${quest.why}</p>
<p class="quest-why">${quest.why}</p>
<ul class="quest-steps">${quest.steps.map((s, i) => stepLine(s, i, stepSeen))}</ul>
<ul class="quest-steps">${quest.steps.map((s, i) => stepLine(s, i, stepSeen))}</ul>
- <p class="quest-reward">${reward(quest, { coinsEnabled })}</p>
+ <div class="quest-foot"><p class="quest-reward">${reward(quest, { coinsEnabled })}</p>${counts ? earnedBy(counts.get(quest.id) || 0) : ''}</div>
${done && completedAt ? html`<p class="quest-earned">Completed <time datetime="${completedAt}">${date}</time> · Badge: <strong>${quest.reward.badge.name}</strong></p>` : ''}
${done && completedAt ? html`<p class="quest-earned">Completed <time datetime="${completedAt}">${date}</time> · Badge: <strong>${quest.reward.badge.name}</strong></p>` : ''}
- ${counts ? earnedBy(counts.get(quest.id) || 0) : ''}
</div>
</div>
</li>`;
</li>`;
}
}
@@ -181,7 +182,7 @@ function seriesCard(series, { coinsEnabled = false, counts = null } = {}) {
@@ -181,7 +182,7 @@ function seriesCard(series, { coinsEnabled = false, counts = null } = {}) {
<a class="quest-title" href="/series/${series.id}">${series.title}</a>
<a class="quest-title" href="/series/${series.id}">${series.title}</a>
<p class="quest-why">${series.why}</p>
<p class="quest-why">${series.why}</p>
<p class="quest-reward"><span class="reward"><span class="reward-badge"><strong>${n}</strong> quests, a badge each</span>${coins > 0 ? html`<span class="reward-coins${coinsEnabled ? '' : ' soon'}"><strong>${coins}</strong> OpenCoins in all${coinsEnabled ? '' : html`, <small>start soon</small>`}</span>` : ''}</span></p>
<p class="quest-reward"><span class="reward"><span class="reward-badge"><strong>${n}</strong> quests, a badge each</span>${coins > 0 ? html`<span class="reward-coins${coinsEnabled ? '' : ' soon'}"><strong>${coins}</strong> OpenCoins in all${coinsEnabled ? '' : html`, <small>start soon</small>`}</span>` : ''}</span></p>
- ${done > 0 ? html`<p class="quest-count">${done} ${done === 1 ? 'badge' : 'badges'} earned in this series</p>` : ''}
+ ${done > 0 ? html`<p class="quest-foot"><span class="quest-count">${icons.glyph('star', 13)}${done} ${done === 1 ? 'badge' : 'badges'} earned in this series</span></p>` : ''}
</div>
</div>
</li>`;
</li>`;
}
}
@@ -196,7 +197,7 @@ function appCard(q, { seen = null, completedAt = null } = {}) {
@@ -196,7 +197,7 @@ function appCard(q, { seen = null, completedAt = null } = {}) {
<a class="quest-title" href="/apps/quests/${q.id}">${q.title}</a> ${stateChip(done)}
<a class="quest-title" href="/apps/quests/${q.id}">${q.title}</a> ${stateChip(done)}
${q.why ? html`<p class="quest-why">${q.why}</p>` : ''}
${q.why ? html`<p class="quest-why">${q.why}</p>` : ''}
<ul class="quest-steps">${q.steps.map((s, i) => stepLine(s, i, stepSeen))}</ul>
<ul class="quest-steps">${q.steps.map((s, i) => stepLine(s, i, stepSeen))}</ul>
- <p class="quest-reward"><span class="reward"><span class="reward-badge">Badge: <strong>${q.reward.badge.name}</strong> <small>on your profile</small></span></span></p>
+ <p class="quest-reward"><span class="reward"><span class="reward-badge" title="A badge on your profile">${icons.glyph(icons.badge(q.reward.badge.id, q), 14)}<span class="sr-only">Badge: </span><strong>${q.reward.badge.name}</strong><span class="sr-only"> on your profile</span></span></span></p>
${done && completedAt ? html`<p class="quest-earned">Completed <time datetime="${completedAt}">${String(completedAt).slice(0, 10)}</time> · Badge: <strong>${q.reward.badge.name}</strong></p>` : ''}
${done && completedAt ? html`<p class="quest-earned">Completed <time datetime="${completedAt}">${String(completedAt).slice(0, 10)}</time> · Badge: <strong>${q.reward.badge.name}</strong></p>` : ''}
${q.url ? html`<p class="quest-count"><a href="${q.url}" rel="nofollow noopener">Where to do it</a></p>` : ''}
${q.url ? html`<p class="quest-count"><a href="${q.url}" rel="nofollow noopener">Where to do it</a></p>` : ''}
</div>
</div>
@@ -41,7 +41,7 @@ const NOW = '2026-10-01T10:00:00.000Z';
@@ -41,7 +41,7 @@ const NOW = '2026-10-01T10:00:00.000Z';
assert.ok(card.includes('Three broadcasts is a habit'), 'the one-line description');
assert.ok(card.includes('Three broadcasts is a habit'), 'the one-line description');
assert.ok(card.includes('Start a stream') && card.includes('1/3 streams'), 'what it counts, with progress and its unit');
assert.ok(card.includes('Start a stream') && card.includes('1/3 streams'), 'what it counts, with progress and its unit');
assert.ok(card.includes('role="progressbar"') && card.includes('aria-valuenow="1"') && card.includes('aria-valuemax="3"'), 'the bar and its values');
assert.ok(card.includes('role="progressbar"') && card.includes('aria-valuenow="1"') && card.includes('aria-valuemax="3"'), 'the bar and its values');
- assert.ok(card.includes('Badge: <strong>Regular Broadcaster</strong>'), 'the badge reward, named');
+ assert.ok(card.includes('<span class="sr-only">Badge: </span><strong>Regular Broadcaster</strong>'), 'the badge reward, named');
assert.ok(card.includes('25</strong> OpenCoins'), 'the coin amount, even while OpenCoins are off');
assert.ok(card.includes('25</strong> OpenCoins'), 'the coin amount, even while OpenCoins are off');
assert.ok(card.includes('start soon'), 'and that they do not move yet');
assert.ok(card.includes('start soon'), 'and that they do not move yet');
assert.ok(!card.includes('up to'), 'never an up-to figure');
assert.ok(!card.includes('up to'), 'never an up-to figure');
@@ -54,7 +54,7 @@ const NOW = '2026-10-01T10:00:00.000Z';
@@ -54,7 +54,7 @@ const NOW = '2026-10-01T10:00:00.000Z';
assert.ok(card.includes('class="quest is-completed"'), 'the card is dimmed by its class');
assert.ok(card.includes('class="quest is-completed"'), 'the card is dimmed by its class');
assert.ok(card.includes('Completed'), 'the state as text');
assert.ok(card.includes('Completed'), 'the state as text');
assert.ok(card.includes(`<time datetime="${NOW}">2026-10-01</time>`), 'the date it was earned');
assert.ok(card.includes(`<time datetime="${NOW}">2026-10-01</time>`), 'the date it was earned');
- assert.ok(card.includes('Badge: <strong>First Broadcast</strong>'), 'the badge');
+ assert.ok(card.includes('<span class="sr-only">Badge: </span><strong>First Broadcast</strong>'), 'the badge');
assert.ok(card.includes('1/1'), 'a completed step reads 1/1, not the missing progress row');
assert.ok(card.includes('1/1'), 'a completed step reads 1/1, not the missing progress row');
const open = render(renderQuests.card(q, { coinsEnabled: false, seen: new Map() }));
const open = render(renderQuests.card(q, { coinsEnabled: false, seen: new Map() }));
assert.ok(!open.includes('Completed <time'), 'not completed: the completion line is left out');
assert.ok(!open.includes('Completed <time'), 'not completed: the completion line is left out');