From 53f4f4381daeaed015a079221a32775a2796ec68 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 9 Sep 2026 18:52:01 -0400 Subject: [PATCH] Per-game caught breakdown (#/progress) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New view (linked from Settings > Your data): the unified caught record counted against every game's dex at once — National total on top, then each version group grouped by generation with a seen/caught bar and X / Y count. Tapping a game switches the active version group and jumps to the dex. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu --- README.md | 2 +- src/router.js | 2 + src/styles/layout.css | 85 +++++++++++++++++++++++++++++++++++ src/views/ProgressView.js | 94 +++++++++++++++++++++++++++++++++++++++ src/views/SettingsView.js | 6 +++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/views/ProgressView.js diff --git a/README.md b/README.md index 9ae196b..6f3d250 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ framework. | Feature | How it works | | --- | --- | | **Game series selection** | Pick a *game* (PokéAPI version group, e.g. Scarlet & Violet). If it has more than one regional dex (Paldea / Kitakami / Blueberry) a sub-dex switcher appears. The choice drives which species show, their regional numbering, and which version's flavor text the detail page uses. Stored in `localStorage`. | -| **Unified Pokémon selection** | One tracking record per Pokémon, keyed by **National Dex id**. Mark Pikachu once and every game's view reflects it — each dex computes its own "Seen / Caught" totals by intersecting its species list with that single map. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). | +| **Unified Pokémon selection** | One tracking record per Pokémon, keyed by **National Dex id**. Mark Pikachu once and every game's view reflects it — each dex computes its own "Seen / Caught" totals by intersecting its species list with that single map. `#/progress` breaks that down for every game at once. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). | ## Data & storage split diff --git a/src/router.js b/src/router.js index 9232ff2..1d77ff7 100644 --- a/src/router.js +++ b/src/router.js @@ -10,6 +10,7 @@ import { TeamView } from './views/TeamView.js'; import { NaturesView } from './views/NaturesView.js'; import { TypeChartView } from './views/TypeChartView.js'; import { CompareView } from './views/CompareView.js'; +import { ProgressView } from './views/ProgressView.js'; import { detailSkeleton, lookupSkeleton } from './components/skeletons.js'; import { prefersReducedMotion } from './store/settings.js'; @@ -23,6 +24,7 @@ const routes = [ { pattern: /^#\/natures$/, view: () => NaturesView() }, { pattern: /^#\/types$/, view: () => TypeChartView() }, { pattern: /^#\/compare$/, view: () => CompareView() }, + { pattern: /^#\/progress$/, view: () => ProgressView() }, { pattern: /^#\/search$/, view: () => SearchView() }, { pattern: /^#\/settings$/, view: () => SettingsView() }, ]; diff --git a/src/styles/layout.css b/src/styles/layout.css index 38dcbc1..373dd19 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -3014,3 +3014,88 @@ .compareview__body { margin-top: 16px; } + +/* ---- Progress by game -------------------------------------- */ +.prog { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 12px; +} +.prog__gen { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-dim); + margin: 12px 0 2px; +} +.prog__row { + display: grid; + grid-template-columns: 8.5rem 1fr 3.6rem; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + border: none; + border-radius: 10px; + background: var(--surface-2); + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.prog__row--nat { + background: color-mix(in srgb, var(--accent) 14%, var(--surface-2)); + cursor: default; + font-weight: 700; +} +.prog__name { + font-size: 0.82rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.prog__track { + position: relative; + height: 8px; + border-radius: 999px; + background: var(--surface); + overflow: hidden; +} +.prog__seen, +.prog__caught { + position: absolute; + inset: 0 auto 0 0; + border-radius: 999px; +} +.prog__seen { + background: color-mix(in srgb, var(--accent) 35%, transparent); +} +.prog__caught { + background: var(--good); +} +.prog__count { + font-size: 0.74rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--text-dim); + text-align: right; +} +.prog__legend { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.75rem; + color: var(--text-dim); +} +.prog__key { + display: inline-block; + width: 1.1rem; + height: 0.6rem; + border-radius: 999px; + vertical-align: middle; +} +.prog__key--caught { background: var(--good); } +.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); } diff --git a/src/views/ProgressView.js b/src/views/ProgressView.js new file mode 100644 index 0000000..d0157de --- /dev/null +++ b/src/views/ProgressView.js @@ -0,0 +1,94 @@ +import { el, clear, onTeardown } from '../lib/dom.js'; +import { loadSnapshot } from '../data/snapshot.js'; +import { settings } from '../store/settings.js'; +import { selection, stats } from '../store/selection.js'; +import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js'; + +function bar(seen, caught, total) { + const pc = total ? (caught / total) * 100 : 0; + const ps = total ? (seen / total) * 100 : 0; + const track = el('div', { class: 'prog__track' }); + const seenEl = el('div', { class: 'prog__seen' }); + const caughtEl = el('div', { class: 'prog__caught' }); + seenEl.style.width = `${ps}%`; + caughtEl.style.width = `${pc}%`; + track.append(seenEl, caughtEl); + return track; +} + +export async function ProgressView() { + const view = el('section', { class: 'view lookup progressview' }); + const snap = await loadSnapshot(); + + const speciesIdsFor = (vgKey) => { + const ids = new Set(); + for (const dex of dexesForVersionGroup(snap, vgKey)) { + for (const [sid] of dex.entries) ids.add(sid); + } + return [...ids]; + }; + + const list = el('div', { class: 'prog' }); + function render() { + clear(list); + + // National total + const nat = stats(snap.species.map((s) => s.id)); + list.append( + el( + 'div', + { class: 'prog__row prog__row--nat' }, + el('span', { class: 'prog__name' }, 'National Dex'), + bar(nat.seen, nat.caught, nat.total), + el('span', { class: 'prog__count' }, `${nat.caught} / ${nat.total}`), + ), + ); + + for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) { + list.append(el('h3', { class: 'prog__gen' }, `Gen ${generation.id} · ${generation.name}`)); + for (const vg of versionGroups) { + const s = stats(speciesIdsFor(vg.key)); + list.append( + el( + 'button', + { + type: 'button', + class: 'prog__row', + onclick: () => { + 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}`), + ), + ); + } + } + } + render(); + + clear(view).append( + el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, '‹ Settings')), + el( + 'header', + { class: 'view__header' }, + el('h1', {}, 'Progress by game'), + el('p', {}, 'Your one unified caught record, counted against each game’s dex. Tap a game to switch to it.'), + ), + el( + 'p', + { class: 'prog__legend' }, + el('span', { class: 'prog__key prog__key--caught' }), + ' caught ', + el('span', { class: 'prog__key prog__key--seen' }), + ' seen', + ), + list, + ); + + const off = selection.subscribe(render); + onTeardown(view, () => off()); + return view; +} diff --git a/src/views/SettingsView.js b/src/views/SettingsView.js index bcdc499..fa1153f 100644 --- a/src/views/SettingsView.js +++ b/src/views/SettingsView.js @@ -182,6 +182,12 @@ export async function SettingsView() { ), el('h2', {}, 'Your data'), + el( + 'p', + { class: 'settings__note' }, + el('a', { class: 'link', href: '#/progress' }, 'Progress by game'), + ' — caught totals against every game’s dex.', + ), storageNote, el('div', { class: 'settings__actions' }, el('button', {