From edef6bf879a7b7df2740a4480e9d7ae48a3d4ba2 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 10 Sep 2026 11:46:16 -0400 Subject: [PATCH] Progress: manage/reset played games; Lighthouse polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu --- README.md | 3 +- public/robots.txt | 2 + src/components/ProgressRing.js | 3 ++ src/store/selection.js | 10 +++++ src/styles/layout.css | 30 +++++++++++++- src/views/ProgressView.js | 74 +++++++++++++++++++++++++++++----- 6 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 public/robots.txt diff --git a/README.md b/README.md index 9c302a4..53c267a 100644 --- a/README.md +++ b/README.md @@ -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 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 -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`) diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/src/components/ProgressRing.js b/src/components/ProgressRing.js index aae46a6..5bc2d20 100644 --- a/src/components/ProgressRing.js +++ b/src/components/ProgressRing.js @@ -62,6 +62,9 @@ export function ProgressRing() { String(CIRC * (1 - Math.max(seen, caught) / safeTotal)), ); 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 }); shown = caught; }; diff --git a/src/store/selection.js b/src/store/selection.js index f2daa63..3e73b3c 100644 --- a/src/store/selection.js +++ b/src/store/selection.js @@ -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'). */ export function stats(speciesIds, vgKey = curGame()) { const get = vgKey === 'all' ? unionEntry : (id) => selection.get().games[vgKey]?.[id] || GBLANK; diff --git a/src/styles/layout.css b/src/styles/layout.css index 138442c..a9700f5 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -221,10 +221,13 @@ display: inline-block; padding: 2px 8px; border-radius: 999px; - font-size: 0.68rem; + font-size: 0.7rem; font-weight: 700; text-transform: capitalize; 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); letter-spacing: 0.02em; } @@ -3275,6 +3278,31 @@ color: var(--text-dim); 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 { display: grid; grid-template-columns: 8.5rem 1fr 3.6rem; diff --git a/src/views/ProgressView.js b/src/views/ProgressView.js index 1dc23ca..3508aa5 100644 --- a/src/views/ProgressView.js +++ b/src/views/ProgressView.js @@ -1,10 +1,11 @@ import { el, clear, onTeardown } from '../lib/dom.js'; import { loadSnapshot } from '../data/snapshot.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 { playedGames, isPlayed } from '../store/playedGames.js'; +import { playedGames, isPlayed, unmarkPlayed } from '../store/playedGames.js'; import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js'; +import { chooseDialog } from '../lib/dialog.js'; function bar(seen, caught, total) { const pc = total ? (caught / total) * 100 : 0; @@ -30,21 +31,56 @@ export async function ProgressView() { 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 }) { const s = stats(speciesIdsFor(vg.key), vg.key); + const manage = played && editing; return el( 'button', { type: 'button', - class: `prog__row${played ? ' prog__row--played' : ''}`, - onclick: () => { - settings.set({ versionGroup: vg.key, pokedex: null }); - location.hash = '#/'; - }, + class: `prog__row${played ? ' prog__row--played' : ''}${manage ? ' prog__row--manage' : ''}`, + onclick: manage + ? () => manageGame(vg) + : () => { + settings.set({ versionGroup: vg.key, pokedex: null }); + location.hash = '#/'; + }, }, el('span', { class: 'prog__name' }, vg.name), 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). 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 vg of versionGroups) { if (isPlayed(vg.key)) list.append(gameRow(vg, { played: true })); } } + } else { + editing = false; } // Everything else — behind a toggle, since a single unified caught