Progress: manage/reset played games; Lighthouse polish

Progress view "Your games" now has an Edit mode: tap a game to open a
dialog to **reset** its caught/seen (keeps it listed) or **remove** it
from the list (also clears its bucket). The currently-selected game can't
be removed (it'd re-add on next launch) — reset only. New
`clearGame(vgKey)` in the selection store.

Lighthouse pass (headless, local): SEO 100, a11y 96, best-practices 96.
- add public/robots.txt (SEO 92 → 100)
- ProgressRing: keep the aria-label in sync with the visible count
  (fixes label-content-name-mismatch)
- .type-chip: text-shadow for white-on-bright-type-colour legibility,
  0.68 → 0.7rem

Still open (design calls, see notes): type-chip colour contrast is
~2–3.7:1 for the lighter types (white on the canonical type colours);
some sub-12px labels; performance is dominated by the ~1 MB snapshot and
the ~1000-card grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
chris 2026-09-10 11:46:16 -04:00
parent 3a54cdcc53
commit edef6bf879
6 changed files with 111 additions and 11 deletions

View File

@ -125,7 +125,8 @@ selected game. Query, scroll, tab and filters persist.
Every game's seen/caught tally against its own dex, from its own per-game Every game's seen/caught tally against its own dex, from its own per-game
bucket. Leads with the games you've played (selected at least once, or bucket. Leads with the games you've played (selected at least once, or
imported a save for); the rest sit behind a toggle. The National Dex row is imported a save for); the rest sit behind a toggle. The National Dex row is
the union across all games. the union across all games. **Edit** the "Your games" list to reset a
game's caught/seen or drop it from the list.
### Shiny hunt tracker (`#/shiny`) ### Shiny hunt tracker (`#/shiny`)

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

@ -62,6 +62,9 @@ export function ProgressRing() {
String(CIRC * (1 - Math.max(seen, caught) / safeTotal)), String(CIRC * (1 - Math.max(seen, caught) / safeTotal)),
); );
tail.textContent = `/${total}`; tail.textContent = `/${total}`;
// Keep the accessible name in sync with the visible count (avoids an
// aria-label / visible-text mismatch).
node.setAttribute('aria-label', `Progress by game: ${caught} of ${total} caught`);
countUp(num, caught, { from: shown, duration: first ? 700 : 350 }); countUp(num, caught, { from: shown, duration: first ? 700 : 350 });
shown = caught; shown = caught;
}; };

View File

@ -139,6 +139,16 @@ export function replaceFlags({ seen = [], caught = [], game } = {}) {
}); });
} }
/** Drop one game's seen/caught bucket entirely (its own progress reset). */
export function clearGame(vgKey) {
selection.set((s) => {
if (!s.games[vgKey]) return s;
const games = { ...s.games };
delete games[vgKey];
return { ...s, games };
});
}
/** Aggregate seen/caught over a species list for one game (union when 'all'). */ /** Aggregate seen/caught over a species list for one game (union when 'all'). */
export function stats(speciesIds, vgKey = curGame()) { export function stats(speciesIds, vgKey = curGame()) {
const get = vgKey === 'all' ? unionEntry : (id) => selection.get().games[vgKey]?.[id] || GBLANK; const get = vgKey === 'all' ? unionEntry : (id) => selection.get().games[vgKey]?.[id] || GBLANK;

View File

@ -221,10 +221,13 @@
display: inline-block; display: inline-block;
padding: 2px 8px; padding: 2px 8px;
border-radius: 999px; border-radius: 999px;
font-size: 0.68rem; font-size: 0.7rem;
font-weight: 700; font-weight: 700;
text-transform: capitalize; text-transform: capitalize;
color: #fff; color: #fff;
/* The bright type colours are light; a soft shadow keeps the white text
readable on fire / grass / electric etc. */
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
background: var(--type-normal); background: var(--type-normal);
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
@ -3275,6 +3278,31 @@
color: var(--text-dim); color: var(--text-dim);
margin: 12px 0 2px; margin: 12px 0 2px;
} }
.prog__genrow {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.prog__edit {
border: none;
background: none;
font: inherit;
font-size: 0.78rem;
font-weight: 600;
color: var(--accent);
cursor: pointer;
padding: 4px;
}
.prog__row--manage {
box-shadow: inset 0 0 0 1px var(--border);
}
.prog__manage {
text-align: right;
font-size: 1rem;
font-weight: 700;
color: var(--text-dim);
}
.prog__row { .prog__row {
display: grid; display: grid;
grid-template-columns: 8.5rem 1fr 3.6rem; grid-template-columns: 8.5rem 1fr 3.6rem;

View File

@ -1,10 +1,11 @@
import { el, clear, onTeardown } from '../lib/dom.js'; import { el, clear, onTeardown } from '../lib/dom.js';
import { loadSnapshot } from '../data/snapshot.js'; import { loadSnapshot } from '../data/snapshot.js';
import { settings } from '../store/settings.js'; import { settings } from '../store/settings.js';
import { selection, stats, statsNational } from '../store/selection.js'; import { selection, stats, statsNational, clearGame } from '../store/selection.js';
import { ui } from '../store/ui.js'; import { ui } from '../store/ui.js';
import { playedGames, isPlayed } from '../store/playedGames.js'; import { playedGames, isPlayed, unmarkPlayed } from '../store/playedGames.js';
import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js'; import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js';
import { chooseDialog } from '../lib/dialog.js';
function bar(seen, caught, total) { function bar(seen, caught, total) {
const pc = total ? (caught / total) * 100 : 0; const pc = total ? (caught / total) * 100 : 0;
@ -30,21 +31,56 @@ export async function ProgressView() {
return [...ids]; return [...ids];
}; };
let editing = false;
async function manageGame(vg) {
const g = stats(speciesIdsFor(vg.key), vg.key);
const isActive = settings.get().versionGroup === vg.key;
const choices = [{ key: 'reset', label: 'Reset caught / seen for this game' }];
if (!isActive) {
choices.push({ key: 'remove', label: 'Remove from your games', class: 'button--danger' });
}
choices.push({ key: null, label: 'Cancel', class: 'button--ghost' });
const pick = await chooseDialog({
title: vg.name,
body:
g.caught || g.seen
? `${g.caught} caught · ${g.seen} seen tracked here.` +
(isActive ? ' This is your selected game.' : '')
: 'Nothing tracked here yet.' + (isActive ? ' This is your selected game.' : ''),
choices,
});
if (pick === 'reset') {
clearGame(vg.key);
render();
} else if (pick === 'remove') {
clearGame(vg.key);
unmarkPlayed(vg.key);
render();
}
}
function gameRow(vg, { played }) { function gameRow(vg, { played }) {
const s = stats(speciesIdsFor(vg.key), vg.key); const s = stats(speciesIdsFor(vg.key), vg.key);
const manage = played && editing;
return el( return el(
'button', 'button',
{ {
type: 'button', type: 'button',
class: `prog__row${played ? ' prog__row--played' : ''}`, class: `prog__row${played ? ' prog__row--played' : ''}${manage ? ' prog__row--manage' : ''}`,
onclick: () => { onclick: manage
settings.set({ versionGroup: vg.key, pokedex: null }); ? () => manageGame(vg)
location.hash = '#/'; : () => {
}, settings.set({ versionGroup: vg.key, pokedex: null });
location.hash = '#/';
},
}, },
el('span', { class: 'prog__name' }, vg.name), el('span', { class: 'prog__name' }, vg.name),
bar(s.seen, s.caught, s.total), bar(s.seen, s.caught, s.total),
el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`), manage
? el('span', { class: 'prog__manage', 'aria-hidden': 'true' }, '⋯')
: el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`),
); );
} }
@ -72,12 +108,32 @@ export async function ProgressView() {
// Games you've played (imported a save for, or opened at least once). // Games you've played (imported a save for, or opened at least once).
if (anyPlayed) { if (anyPlayed) {
list.append(el('h3', { class: 'prog__gen' }, 'Your games')); list.append(
el(
'div',
{ class: 'prog__genrow' },
el('h3', { class: 'prog__gen' }, 'Your games'),
el(
'button',
{
type: 'button',
class: 'prog__edit',
onclick: () => {
editing = !editing;
render();
},
},
editing ? 'Done' : 'Edit',
),
),
);
for (const { versionGroups } of gens) { for (const { versionGroups } of gens) {
for (const vg of versionGroups) { for (const vg of versionGroups) {
if (isPlayed(vg.key)) list.append(gameRow(vg, { played: true })); if (isPlayed(vg.key)) list.append(gameRow(vg, { played: true }));
} }
} }
} else {
editing = false;
} }
// Everything else — behind a toggle, since a single unified caught // Everything else — behind a toggle, since a single unified caught