From 0665d0f2efaf8510e53c195f12a7e7c010399da8 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 27 Aug 2026 12:54:11 -0400 Subject: [PATCH] Both-version flavour text; refresh detail page on game switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FlavorText: shows a version pill toggle (Sword/Shield, Scarlet/Violet…) when the two entries differ, sliding between them; collapses identical entries; falls back to the newest English entry, captioned - Switching games while viewing a Pokémon now rebuilds the detail page (router.rerender wired from a settings subscription in main.js) so flavour text, learnset, matchups, evolution, abilities, locations and era sprites all follow the newly selected game Co-Authored-By: Claude Sonnet 5 --- src/components/FlavorText.js | 74 ++++++++++++++++++++++++++++++++++++ src/main.js | 16 +++++++- src/router.js | 8 ++++ src/styles/layout.css | 46 +++++++++++++++++++++- src/views/PokemonDetail.js | 13 +------ 5 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 src/components/FlavorText.js diff --git a/src/components/FlavorText.js b/src/components/FlavorText.js new file mode 100644 index 0000000..0e27887 --- /dev/null +++ b/src/components/FlavorText.js @@ -0,0 +1,74 @@ +import { el } from '../lib/dom.js'; +import { prettify } from '../data/pokedex-resolver.js'; + +/** + * Pokédex flavour text for the selected game. Most games ship two versions + * (Scarlet/Violet, Sword/Shield…) with different entries — show a pill + * toggle and slide between them. Falls back to the most recent English + * entry when the game itself has none. + */ +export function FlavorText(species, vg) { + const versions = vg ? vg.versions : []; + const english = species.flavor_text_entries.filter((f) => f.language.name === 'en'); + + const byVersion = new Map(); + for (const f of english) { + if (!byVersion.has(f.version.name)) { + byVersion.set(f.version.name, f.flavor_text.replace(/\s+/g, ' ')); + } + } + + let picks = versions + .filter((v) => byVersion.has(v)) + .map((v) => ({ version: v, text: byVersion.get(v) })); + + // Collapse identical entries (both versions often share the same text). + const seen = new Set(); + picks = picks.filter((p) => (seen.has(p.text) ? false : seen.add(p.text))); + + if (!picks.length && english.length) { + const last = english[english.length - 1]; + picks = [{ version: last.version.name, text: last.flavor_text.replace(/\s+/g, ' '), fallback: true }]; + } + if (!picks.length) return null; + + const quote = el('p', { class: 'ppanel__flavor' }); + const caption = el('span', { class: 'flavor__cap' }); + const toggle = el('div', { class: 'flavor__toggle' }); + let index = 0; + + function paint() { + const p = picks[index]; + quote.textContent = p.text; + quote.style.animation = 'none'; + // eslint-disable-next-line no-unused-expressions + quote.offsetWidth; + quote.style.animation = ''; + caption.textContent = p.fallback + ? `— ${prettify(p.version)} (most recent)` + : `— Pokémon ${prettify(p.version)}`; + [...toggle.children].forEach((b, i) => b.classList.toggle('is-active', i === index)); + } + + if (picks.length > 1) { + picks.forEach((p, i) => + toggle.append( + el( + 'button', + { + class: 'flavor__pill', + type: 'button', + onclick: () => { + index = i; + paint(); + }, + }, + prettify(p.version), + ), + ), + ); + } + + paint(); + return el('div', { class: 'flavor' }, picks.length > 1 ? toggle : null, quote, caption); +} diff --git a/src/main.js b/src/main.js index 3bad6d6..0a8392a 100644 --- a/src/main.js +++ b/src/main.js @@ -3,12 +3,24 @@ import './styles/layout.css'; import { registerSW } from 'virtual:pwa-register'; import { el } from './lib/dom.js'; -import { initRouter } from './router.js'; +import { initRouter, rerender } from './router.js'; import { Nav } from './components/Nav.js'; import { settings, applyTheme } from './store/settings.js'; applyTheme(); -settings.subscribe(() => applyTheme()); + +// React to preference changes. The dex feed updates itself in place, but the +// Pokémon detail page is built once per visit — rebuild it when the selected +// game changes so its data (flavour text, learnset, matchups, evolution, +// abilities, locations, era sprites) matches the new game. +let lastVersionGroup = settings.get().versionGroup; +settings.subscribe((s) => { + applyTheme(); + if (s.versionGroup !== lastVersionGroup) { + lastVersionGroup = s.versionGroup; + if ((location.hash || '').startsWith('#/pokemon/')) rerender(); + } +}); window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => applyTheme()); diff --git a/src/router.js b/src/router.js index f6320b5..edd37cb 100644 --- a/src/router.js +++ b/src/router.js @@ -19,6 +19,13 @@ function resolve(hash) { return { route: routes[0], match: [] }; } +let rerenderCurrent = null; + +/** Re-run the active route (e.g. after the selected game changed). */ +export function rerender() { + if (rerenderCurrent) rerenderCurrent(); +} + export function initRouter(host) { let current = null; @@ -56,6 +63,7 @@ export function initRouter(host) { window.scrollTo(0, 0); } + rerenderCurrent = render; window.addEventListener('hashchange', render); render(); } diff --git a/src/styles/layout.css b/src/styles/layout.css index 6c6b3ba..06aa03d 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -862,9 +862,53 @@ .ppanel__flavor { font-size: 1rem; line-height: 1.6; - margin: 0 0 18px; + margin: 0 0 6px; overflow-wrap: anywhere; } +.flavor { + margin-bottom: 18px; +} +.flavor__toggle { + display: flex; + gap: 6px; + margin-bottom: 10px; +} +.flavor__pill { + appearance: none; + border: 1.5px solid var(--border); + background: var(--surface); + color: var(--text-dim); + font: inherit; + font-size: 0.76rem; + font-weight: 600; + padding: 5px 13px; + border-radius: 999px; + cursor: pointer; + text-transform: capitalize; +} +.flavor__pill.is-active { + background: var(--type-main, var(--accent)); + border-color: var(--type-main, var(--accent)); + color: #fff; +} +.flavor .ppanel__flavor { + animation: flavorSlide 0.28s var(--ease-spring); +} +@keyframes flavorSlide { + from { opacity: 0; transform: translateX(14px); } + to { opacity: 1; transform: none; } +} +.flavor__cap { + display: block; + font-size: 0.76rem; + color: var(--text-dim); + text-transform: capitalize; +} +@media (prefers-reduced-motion: reduce) { + .flavor .ppanel__flavor { + animation: none; + } +} .ppanel__sub { font-size: 0.8rem; text-transform: uppercase; diff --git a/src/views/PokemonDetail.js b/src/views/PokemonDetail.js index 2a2372d..fbcd324 100644 --- a/src/views/PokemonDetail.js +++ b/src/views/PokemonDetail.js @@ -12,6 +12,7 @@ import { EvolutionChain } from '../components/EvolutionChain.js'; import { MovesList } from '../components/MovesList.js'; import { TypeMatchups } from '../components/TypeMatchups.js'; import { Locations } from '../components/Locations.js'; +import { FlavorText } from '../components/FlavorText.js'; import { typeHex } from '../lib/type-color.js'; const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop()); @@ -153,14 +154,6 @@ export async function PokemonDetail(nationalId) { syncTrack(); // ---- About panel ----------------------------------------------- - const wantedVersions = new Set(vg ? vg.versions : []); - const englishFlavor = species.flavor_text_entries.filter( - (f) => f.language.name === 'en', - ); - const flavor = - englishFlavor.find((f) => wantedVersions.has(f.version.name)) || - englishFlavor[englishFlavor.length - 1]; - const abilities = pokemon.abilities .filter((a) => !a.is_hidden || vgGen >= 5) .map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : '')); @@ -168,9 +161,7 @@ export async function PokemonDetail(nationalId) { const aboutPanel = el( 'div', { class: 'ppanel' }, - flavor - ? el('p', { class: 'ppanel__flavor' }, flavor.flavor_text.replace(/\s+/g, ' ')) - : null, + FlavorText(species, vg), el( 'dl', { class: 'pfacts' },