Fuzzy: src/lib/fuzzy.js — editDistance + closeEnough (length-scaled typo
tolerance, 0 for names ≤4 chars). submitGuess() accepts a near-miss, so
"charizrd" counts. Plus inline ghost-text completion: once the typed
prefix matches exactly one Pokémon, the rest of the name shows greyed
after the caret; → (at end), Tab or End accepts it.
Generation filter: a "Pull from" select (All / Gen 1–9, built from the
snapshot's species.generation). Choice persists in pdx.whosThat.gen;
changing it reshuffles the pool (and the completion list) and resets the
streak.
7 new unit tests for fuzzy.js (48 total).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The near-white stage was too bright. Swap it for a radial gradient off
var(--accent): a pale centre (accent mixed 35% white) for the silhouette
to read against, through the full accent colour, to a soft dark rim.
Adapts to the chosen accent and works in every theme.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The stage used var(--surface), so a brightness(0) silhouette was
near-invisible on a dark theme. Make it a fixed light "TV screen"
(blue→white gradient) in every theme; move the caption/hint out onto the
page background so they stay readable; add a drop-shadow to the revealed
artwork.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
.nav__brand is display:none below 1024px, so "tap the wordmark" was
desktop-only. Add a shared onTapCode() helper (src/lib/tap-code.js) and
put the 7-tap shortcut on the dex feed's game-title (.feed-head__id),
which is visible on every screen size. Keeps the wordmark tap for desktop.
3 tests for onTapCode (count, window reset, unbind).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Guess the Pokémon from its silhouette (official artwork, filter:brightness(0)
until revealed). Streak + best + win% persisted in pdx.whosThat. Three
misses auto-reveal; hints (letter count, then first letter) after wrong
guesses; guess matching is punctuation/space/case-insensitive.
Not in the nav or any link. Reached by the Konami code (↑↑↓↓←→←→BA,
lib/konami.js wired in main.js), seven quick taps on the nav wordmark, or
the bare #/whos-that hash once you know it.
9 new unit tests for the konami matcher (sequence, case-insensitivity,
rewind, partial).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Gen 2 (GSC) is the one mainline dex that records which Unown letters
you've caught — a 26-byte ordered list right after the Seen flag array
(seen + 0x20). Parse it, map letters to the app's Form Dex slugs
('unown' for A, 'unown-b'…'unown-z'), and tick them via a new
markFormsCaught() on the formTracking store. A garbage/out-of-range list
is ignored rather than guessed at.
The import dialog and result line now surface "· N Unown letters".
Verified: 6 new unit tests (synthetic Gen 2 SRAM → slugs, empty list,
garbage rejection; markFormsCaught behaviour) + a real end-to-end import
of a synthetic Crystal save writing {unown, unown-c, unown-o, unown-z}
to pdx.formTracking.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- selection: clearAllGames() wipes seen/caught for every game but keeps
the global favorites/notes record.
- Progress view: a "Reset caught / seen…" action (custom dialog) that
calls clearAllGames — the soft, favorites-preserving reset. Settings →
"Clear tracking data" stays as the full wipe (now spells out that it
also drops favorites and notes, and points here for the softer one).
- Swept the remaining window.confirm / window.alert calls
(Settings clear-tracking, clear-cache, backup import result;
ShinyView delete-hunt) onto chooseDialog for a consistent look.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
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
31 tests in test/, run in a plain Node env with a tiny localStorage shim
(no jsdom — its Node-22.4 transitive-dep breakage isn't worth working
around for logic tests):
- type-chart: modern effectiveness, dual-type stacking, and the era rules
(no Fairy pre-6, Steel resists Ghost/Dark pre-6, the Gen 1 Ghost→Psychic
and Bug↔Poison quirks).
- damage-calc: nature multipliers, the Gen 3+ stat formula (Garchomp
reference values), STAB/effectiveness/immunity, pre/post-Gen-6 crit.
- savedex: a synthetic Gen 1 SRAM (bitfields + checksum) round-trips;
checksum-mismatch and size guards.
- selection: normalizeSelection v2→v3 migration, per-game isolation,
global favourite, stats vs statsNational.
- pokedex-resolver: prettify, dex resolution against a mini snapshot.
`npm test` wired into CI after format:check.
Also: docker-compose host port 8080 → 4753 (README updated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- ci.yml runs `npm run format:check` before the build.
- CONTRIBUTING / AGENTS: point at `npm run format` instead of the old
"no formatter" note.
- README: new "Built with AI assistance" section — the project is
developed with Claude / Claude Code, human-directed and reviewed;
visible in the Co-Authored-By trailers. CONTRIBUTING asks contributors
to disclose AI-assisted PRs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Pure formatting — no behaviour change. Verified: build passes, all 14
routes render, service worker active, no console errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
printWidth 100 to stay close to the existing hand-formatting; single
quotes; trailing commas. `npm run format` / `npm run format:check`.
The formatting pass itself is the next commit so it stays isolated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Vite 5 pulled esbuild 0.21.5 — the only source of the GHSA-67mh-4wv8-2f99
dev-server advisory. Vite 6.4 (esbuild 0.25) runs on the current Node
baseline and clears it: `npm audit` now reports 0 vulnerabilities.
- vite 5.4 → 6.4, vite-plugin-pwa 0.21 → 1.3 (needs Vite 6+).
- Removed `ws` from devDependencies — it was only used by throwaway test
scripts, never the app or build.
- Build, all 14 routes, and the service worker verified on Vite 6.
- SECURITY.md / AGENTS.md updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- .github/workflows/ci.yml — npm ci + npm run build on push/PR, with the
PokéAPI snapshot cached so only the first run pays the fetch cost.
- .github/dependabot.yml — weekly npm, monthly actions, grouped.
- .github/ISSUE_TEMPLATE/ — bug report, feature request, config with
discussion / upstream-data links.
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1 (contact method is a
placeholder to fill in).
- SECURITY.md — private reporting + the known dev-only Vite/esbuild advisory.
- AGENTS.md — machine-facing version of CONTRIBUTING for AI coding agents.
- docs/screenshots/ + a strip in the README (resized + pngquant'd, ~240 KB).
- public/og.png + Open Graph / Twitter card meta in index.html.
- .nvmrc (20); gitignore .claude/ .idea/ .vscode/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Rename the app from the working-title "Pokédex" to Pocketdex across the
nav brand, install toast, PWA manifest, index.html title/meta, and
package.json. Generic in-universe uses of "Pokédex" (sub-dex label, "all
Pokédex entries", save-import messages) are left as-is.
Docs for going public:
- README.md rewritten: quick start, full feature list, how-it-works
(data/storage split, per-game tracking, routing), deployment, legal
disclaimer, contributing pointer.
- CONTRIBUTING.md: ground rules (no copyrighted assets, framework-free,
offline-first), setup, project map, code style, common tasks, manual
test checklist, PR conventions.
- LICENSE: MIT (+ note that it covers source only, not Pokémon data).
- .editorconfig, .github/PULL_REQUEST_TEMPLATE.md.
- package.json: add "engines": node >=20.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Catching a Pokémon in Blue was counting it as caught in Gold, Emerald and
every other game, because the whole app ran on one caught record keyed by
National Dex id. Progress by game just intersected that set with each
game's species list, so the per-game bars were meaningless.
selection store is now:
pokemon: { [id]: { favorite, note } } -- global (describe the mon)
games: { [vg]: { [id]: { seen, caught } } } -- per game
- entry(id) / toggle(id, field) / stats(ids) default to the selected game;
favorite/note stay global. 'all' (All-games mode) reads as the union of
every game -- "caught anywhere" -- and is a real bucket you write to when
no specific game is selected.
- v2 stores migrate on load (normalizeSelection): favorite/note lift out,
seen/caught drop into the 'all' bucket so nothing is falsely attributed
to a game. Same path runs on JSON backup import.
- ProgressView: each game row counts its own bucket; National row =
statsNational (union). Intro text slimmed.
- Save import writes into one game bucket -- the selected game if it's a
candidate for that save, else the parser's first guess -- and the dialog
names which game it's importing to.
- DexGrid feed (ring, filter counts, caught-sort), PokemonDetail track
buttons, search dots, shiny "found it" all follow the selected game.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The reference links sat in their own strip under the header, reading as an
afterthought, and "Compare" appeared both there and as a Team mode.
- Reference links (Natures / Type chart / Breeding) now share one bar with
the Coverage/Compare/Calc switch — quiet text links on the right, so
they're still one tap away but clearly secondary chrome, not a pasted-on
row. Right-aligned on wide screens, wrapping under the switch on mobile.
- Removed the standalone "Compare" reference link. The Team's own Compare
mode now carries a "Compare any Pokémon →" link to #/compare in its
intro line, so the free-form compare tool is reachable in context and
only one "Compare" label is ever visible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- Unown's first slot is now "A" (its base sprite is the A shape), not
"Default". Other letter/pattern sets keep "Default".
- The grey-out for uncaught entries applied to the whole tile, dimming
the label text to ~0.55 opacity — unreadable in dark mode. Move the
grayscale/opacity to just the sprite; the label stays full-contrast.
- Sprites 40 -> 60px, grid cell min 68 -> 88px.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The Natures / Type chart / Compare / Breeding links under the Team header
were plain dot-separated text. Make them a row of pill chips with small
accent icon badges (.toollinks / .toollink).
Progress by game showed a caught total for all 32 version groups the
moment anything was marked, since the tracking model is one unified
caught record. Add a "played games" set (pdx.playedGames, vg keys),
auto-marked when a game is selected or a save is imported (savedex now
returns candidate versionGroups per generation). #/progress leads with a
"Your games" section and tucks the rest behind a "Show all other games"
toggle (persisted). Included in JSON export/import.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
savedex.js now also reads 512 KB DS saves:
- Gen 4 (D/P/Pt, HG/SS): dex struct at slot+0x12 — caught 0x40 bytes at
+0x16, four per-form "seen" copies at +0x56/0x96/0xD6/0x116 OR'd
together, 493 species.
- Gen 5 (B/W, B2/W2): dex block at slot+0x21600 (BW) / +0x21400 (B2W2) —
caught at +0x08, four "seen" copies at +0x5C/0xB0/0x104/0x158, 649
species; the offset that validates picks the game.
- Handles both save slots (picks the fuller one), a DeSmuME `.dsv`
footer, and oversized flash dumps.
No block CRC — validated structurally instead (nothing set past the last
species, every caught species also seen), so a wrong offset is rejected
rather than importing garbage. Offsets are from PKHeX; worth checking
against a real save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Button "Import from a game save" now sits on its own line with the
description "A .sav / .srm from a Gen 1–3 game." beneath it, instead of a
long inline sentence. The dialog already names the detected game and
offers merge vs replace.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- The save-import dialog body is now just "N seen · N caught in this
save." — the button labels carry the merge/replace meaning.
- "colour" → "color" throughout (the visible "Accent color" field, plus
comments and README for consistency).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- Save import now opens a small choice dialog (new lib/dialog.js) instead
of a confirm(): "Add to my Pokédex" (merge, keeps everything) or
"Replace mine with this". Replace resets seen/caught to exactly the
save but keeps favourites and notes (new selection.replaceFlags). The
dialog also explains that caught state is shared across games, so an
imported save only ever adds species that game has and they count
toward the National Dex too — no per-game caught flag, by design.
- The dex-feed progress ring is now an <a href="#/progress"> with a
hover/press nudge — tapping it opens the per-game breakdown.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
savedex.js now sniffs the file and parses whichever generation it is:
- Gen 1 (R/B/Y): 32 KB SRAM, fixed offsets — owned 0x25A3, seen 0x25B6
(151 species), name 0x2598, one-byte complement checksum.
- Gen 2 (G/S/C): tries Gold/Silver offsets (0x2A4C/0x2A6C, checksum
0x2D69) and Crystal (0x2A27/0x2A47, checksum 0x2D0D); the 16-bit
checksum picks which, with a caught⊆seen / no-overflow sanity fallback.
- Gen 3: unchanged (parseGen3Dex kept as an alias).
parseSaveDex(buffer) is the new entry point; the Settings picker relabels
to "Import from a game save". Verified against synthetic Gen 1 / GS /
Crystal saves and end-to-end through the UI (a Gen 1 save merged
11 caught / 84 seen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- onSwipe: before claiming a horizontal page-swipe, walk up from the
touch target for an overflow-x auto/scroll ancestor that can still
scroll that way (tab strip, moves list, coverage grid) and bail if
found — sliding those no longer flips to the next Pokémon.
- segThumb: retry placement until the seg is laid out, and observe it for
resizes, so the sliding pill is positioned on first paint instead of
only after the first click. Dropped the :has() selector for a
JS-applied .seg--thumbed class.
- New lib/sheet-drag.js: drag the game sheet / Pokémon picker down by its
header to dismiss (past ~110px or a flick), else it springs back.
Header gets touch-action:none + a grab cursor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
lib/savedex.js parses a Generation III save (Ruby/Sapphire/Emerald/
FireRed/LeafGreen): picks the newer of the two save slots, resolves the
rotated section order via each section's footer, and reads the seen /
owned bitfields out of `struct Pokedex` in section 0 (bit dexNo-1, 386
species). Also decodes the trainer name and detects the national-dex
magic byte. Soft checksum check.
Settings > Your data: "Import from a GBA save" — file picker → confirm →
selection.importFlags() merges seen/caught in (never removes). Verified
against synthetic saves (slot selection, rotation, FRLG vs RSE, bit
decode) and end-to-end through the Settings UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
New lib/anim.js (countUp, fadeSlideIn, segThumb), all reduced-motion
aware:
- Shared-element morph: tapping a card tags its art with
view-transition-name; the detail skeleton now renders the real sprite
at hero size (router passes the match, animates the skeleton mount), so
the sprite flies from the grid into the hero and the skeleton→content
swap is seamless.
- Segmented controls (Team / Search / Breeding) get a sliding pill that
measures each button and eases between them.
- Count-ups: the dex-feed progress number and the detail Stats total
tick up.
- Detail tab content fades/rises on switch.
- Global :active scale-press on buttons/chips/rows, plus soft
background/border transitions.
- Hero sprite has a slow idle bob.
- Router swallows the AbortError when a fast follow-up nav skips an
in-flight view transition.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
build-snapshot: a new pass fetches /pokemon/:id/encounters for every
species and records, per game, the versions it has wild encounters in —
when that's a non-empty strict subset of the game's versions,
species.exclusiveIn[vg] holds them. Wild-only, so gift/trade/evolution
exclusives are missed.
Card: with a specific game selected, shows a "<Version> only" pill
(e.g. Seedot -> "Ruby only", Lotad -> "Sapphire only"). 220 species
flagged across all games; nothing shown in National mode.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Team slots are now { id, form } instead of bare ids (old saves migrate
on read via slot()). The detail page's team button adds whatever
mechanical form is selected — "+ Mega X" / "✓ Mega X in team" — and
TeamView merges that form's baked snapshot types/stats/BST into the
member so Coverage, Compare and the Calc all analyse the form (Mega
Charizard X shows as Fire/Dragon, BST 634). Lineup shows the form name
and sprite; remove targets the exact slot. The Add picker still adds
base forms — Megas etc. come from their own page.
CompareTable takes an optional spriteId; addToTeam/removeFromTeam/inTeam
gain a form argument.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
New store + view (linked from Settings > Your data): one card per active
hunt with a big +1 counter, the method's odds, and the running cumulative
chance (1 - (1-p)^count). Nine methods with shiny-charm variants
(Full odds, Masuda, SOS, Radar, DexNav, Catch Combo, Dynamax Adventure,
Mass/Massive Outbreak). "Found it" marks the Pokémon caught and clears
the hunt; per-hunt notes field.
Also: JSON export/import now covers team, formTracking and shinyHunts
alongside settings/selection; openPokemonPicker gains a `closeAfterPick`
option (used by the shiny + breeding single-pick flows).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Two modes, linked from the Team header:
- Egg groups — browse every species in each of the 14 breedable egg
groups (offline, from the snapshot's eggGroups).
- Can breed with — pick a Pokémon and get its compatible partners
(shared egg group; Ditto pairs with everything; genderless pairs only
with Ditto; Undiscovered can't breed), plus its family's egg moves
(walks evolves-from to the base stage — Pikachu shows Pichu's), each
linking to the move page (which already lists who can learn it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Extracted the Team Compare table into components/CompareTable.js and
reused it for a new #/compare route: its own picker (up to 4 Pokémon,
persisted in ui.compareIds), gen-aware types, per-stat max highlight.
Linked from the Team header alongside Natures / Type chart. TeamView's
Compare tab now just calls CompareTable too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- Rasterised the SVG to pwa-192 / pwa-512 / pwa-maskable-512 (logo in the
safe zone, full-bleed background) and a real apple-touch-icon.png;
manifest now lists PNG "any" + a dedicated "maskable".
- Manifest shortcuts: Team, Search, Type chart.
- lib/install.js captures beforeinstallprompt so the app can offer
installation itself — a one-time toast, and an "Install app" button in
Settings (falls back to a "use your browser menu" hint where the event
never fires, e.g. iOS).
- index.html: apple-touch-icon PNG + apple-mobile-web-app meta.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
New #/types route (linked from the Team header alongside Natures): the
full 18x18 attack/defend grid, gen-aware from the selected game — drops
Fairy pre-Gen 6, Steel/Dark pre-Gen 2, and applies the Gen 1 / pre-Gen 6
quirks the type-chart module already models. Tap a type on either edge to
highlight its row+column and get an offensive + defensive breakdown as
type chips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
- AbilityDetail: "Pokémon with this ability" grid now gets a filter box
and the same game/generation narrowing as the move and item pages, plus
a note when the ability postdates the selected game (Libero in
Ruby/Sapphire -> "introduced in Gen 8").
- TeamView "At a glance": Fastest / Bulkiest names now link to their
Pokémon pages.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Two more spots where data was shown as a dead number/label:
- ItemDetail "Held by N Pokémon" -> a clickable sprite grid, same as the
move page. held_by_pokemon carries a per-version breakdown, so a
specific game filters exactly (Leftovers in SV lists only what holds it
there, not the all-games union).
- PokemonDetail "Held items" in the Training facts were plain text; now
each links to its item detail page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The "Learned by N Pokémon" fact was a dead number. Now the move page
renders that list as a clickable sprite grid (links to each Pokémon),
with a filter box once it's more than ~24 long.
PokéAPI's learned_by_pokemon is flat across all games, so for a specific
game it's narrowed to species that existed by that generation — a
"Crystal" view of Surf shows 69 Gen 1–2 Pokémon, not the full 231. Not
perfectly game-exact (a mon that only gained the move in a later gen can
slip through) but far better than guess-and-check. National ("all") shows
the unfiltered list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Gen 8+ games (Sword/Shield, Scarlet/Violet, Legends Arceus) dropped HMs
entirely, so "HMs only" there just zeroed the list with no way to see
why. buildFilters now checks whether the selected game actually has any
TM / HM machines and only renders the checkbox that applies; a filter
left stuck on from a previous game is cleared (and the "by number" sort
reverts) when it no longer has a control. SearchView also re-runs its
filters + results when the version group changes, so switching games
from the Games sheet updates it live.
"TMs only" + "HMs only" together already show the union (verified: 58 in
FRLG = 50 TMs + 8 HMs, ordered TM01…HM08).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
TM/HM numbers are now baked into the snapshot (build-snapshot resolves
every machine id -> "TM25" etc. in a post-pass, ~2400 lookups), so the
Search moves list no longer lazily fetches /machine per row — it shows
numbers synchronously and can sort by them.
- New "TM / HM number" sort option, shown only while a machine filter is
on, and dropped back to A–Z when neither is.
- "TMs only" now means strictly TMs; added a parallel "HMs only". Either
or both. In "All games" mode a move can be a TM in one game and an HM
in another, so machineFor picks the newest entry of a requested kind
rather than just the newest entry (fixes "HMs only" showing 0).
Removes the getMachine/mapLimit lazy-resolution path (and its now-unused
stale-token guard) from SearchView.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The learnset's "By TM / HM" group was alphabetical. TM numbers aren't in
the snapshot — they resolve lazily per move via /machine/{id}, which the
group's enrichers already fetch on expand. So: await those resolutions
(fillTm is now async and stashes the label on the move), and once the
whole group is enriched, re-append the rows in TM/HM/TR-then-number order
— TM01, TM02 … HM01 …, matching the in-game list. Verified against
Charizard in FireRed/LeafGreen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Four new preferences, all in settings.js / applied via applyTheme() so
boot and live changes go through one path:
- fontScale (small/default/large): scales root font-size (92/100/115%);
the whole stylesheet is already rem-based so this alone rescales
everything.
- reduceMotion: an explicit in-app override alongside the existing
`prefers-reduced-motion` OS check, via a new
:root[data-reduce-motion="true"] rule in tokens.css mirroring the
existing media-query kill-switch. New prefersReducedMotion() helper
(checks both) replaces the three hand-rolled matchMedia checks in
router.js/Card.js/PokemonDetail.js.
- defaultRoute (dex/team/search): redirects on a genuinely hash-less
launch only (bookmark, home-screen icon, bare URL) — explicit nav
(e.g. tapping "Dex") is never touched, since it's applied once in
main.js before initRouter runs.
- haptics: a short navigator.vibrate() pulse on the same catch/seen/
favorite toggles that already play the pop animation, via a new
lib/haptics.js buzz() (no-ops without Vibration API support, e.g. iOS
Safari).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
The selection store already had a note field and setNote() (from the
JSON export/import work) but nothing in the UI ever wrote to it. Adds:
- PokemonDetail: an auto-resizing textarea under the Seen/Caught/Team
row. Saves debounced (500ms) as you type, immediately on blur, and
flushes any unsaved keystrokes on teardown so navigating away right
after typing never loses them. A brief "Saved" flash confirms the
write.
- Card: a small 📝 mark on any card whose Pokémon has a note.
- DexGrid: a "Notes" filter chip (same pattern as Caught/Favorites) to
browse everything you've annotated.
Notes already ride along in the existing settings/selection JSON
export-import, so no changes needed there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Extends the existing filter drawer (type/gen) with four more:
- Ability — snapshot abilities already carry a reverse pokemon[] index,
so this is a plain lookup, no new data needed.
- Egg group — options derived from the species list's own eggGroups.
- Min BST — a number field; hard filter, distinct from sorting by BST.
- Fully evolved only — new species.evolvesFromId field (from
pokemon-species' evolves_from_species, already fetched during the
snapshot build, so no extra requests). "Fully evolved" is computed
client-side as "no other species points here via evolvesFromId" —
correctly handles branching lines (Eevee's evolutions all count as
fully evolved; Eevee itself doesn't).
All four persist via the ui store and count toward the filter badge, same
as the existing filters.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Every other reference page (Move/Item/Ability detail) has a '‹ back'
link; NaturesView never got one, so the only way out was the phone's
back gesture or the bottom nav. Add the same lookup__nav link, pointing
at #/team since that's the page's only current entry point.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
New "Calc" mode alongside Coverage/Compare. Pick (or quick-pick from your
current team) an attacker, a move, and a defender; get a damage range, %
of the defender's HP, hits-to-KO, and STAB/effectiveness badges.
- src/lib/damage-calc.js: the standard Gen 3+ stat and damage formulas
(statAt, calcDamage), evaluated across the 85–100% roll.
- Move data is era-accurate: picking a move fetches it live for
past_values (power/type/class changes), reusing MovesList's
forGeneration (now exported) via a snapshot-shaped shim so the instant
preview and the refined post-fetch result use the same code path.
- Level/nature/EV inputs per side (defaults: level 100, neutral nature,
0 EV, 31 IV assumed) plus a crit toggle.
- Explicitly out of scope and called out in the UI: items, abilities,
weather, terrain — this is a base-stats estimate, not a battle-exact
number.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Second thought on the previous commit: gating "<Species> Dex" to games
that include the species contradicted the app's own model. Caught/seen
tracking here is unified across games by design, and there's no
game-dependent data on this tab (unlike Locations/Moves, which really do
vary by game and are rightly gated) — it's the same fixed set of
letters/patterns/decorations regardless of which game is selected. Hiding
it just because you switched games to browse something else would make
already-tracked progress look lost. Show it whenever the species has
cosmetic forms, full stop.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
New tab on the detail page — "<Species> Dex" — for any species with more
than one cosmetic form: a checklist grid (sprite + label + tap-to-toggle)
tracking which appearances you've caught, separate from the species' own
caught flag. Persisted in a new pdx.formTracking store keyed by form slug.
Only offered when the species is actually in the currently selected
game's dex (reuses the page's existing prev/next row lookup), so Unown
gets its dex in HeartGold/SoulSilver but not in Sword/Shield, where it
was never released.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
onSwipe judged horizontal-vs-vertical from a single touchmove sample
compared against the start point. A fast scroll flick's first sample is
often slightly diagonal (thumb motion), so it could get claimed as a
page-swipe and every subsequent touchmove got preventDefault()'d — the
rest of the scroll silently died, so trying to reach off-screen content
in About/Stats/Evolution/etc. would instead flip to the next Pokémon.
Now vertical intent wins by default (bails, permanently, as soon as the
drag is even mildly more vertical than horizontal) and a horizontal
page-swipe only gets claimed once the drag is unambiguous
(dx > 20 && dx > 2*dy). Deliberate left/right swipes still work.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Coverage was hardcoded to Gen 9. Now it reads the selected game: only
that generation's types get rows (no Fairy pre-Gen 6, no Steel/Dark
pre-Gen 2), member typings are era-accurate via the snapshot's pastTypes,
and the type chart / STAB maths run at the game's generation. Compare's
Type row follows suit. The panel re-renders on a game switch.
New "Weak spots" block above the grid: types that hit 2+ members
super-effectively, or that a member is weak to and nobody resists (those
get a "0 resist" flag), each listing the exposed members. Plus a
"no resist to" line for types the whole team only takes neutrally but
can't wall. The grid itself swaps the plain "weak" count for a diverging
resist/weak bar and tints rows where 2+ members are weak.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu