Both-version flavour text; refresh detail page on game switch

- 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 <noreply@anthropic.com>
This commit is contained in:
chris 2026-08-27 12:54:11 -04:00
parent 59ff8d6b68
commit 0665d0f2ef
5 changed files with 143 additions and 14 deletions

View File

@ -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);
}

View File

@ -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());

View File

@ -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();
}

View File

@ -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;

View File

@ -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' },