All-games mode, full flavour list, persistent search
- FlavorText: 'All Pokédex entries' disclosure listing every distinct English entry across all games (newest first, labelled by game) — not just the selected game's one line - Search remembers its query and scroll position in the ui store, so Back or re-opening the tab restores the results; header explains the scope (matches all species; opens for the selected game / all games) - GameSheet 'All games' option: National Dex, newest data, no generation limits — abilities/shiny ungated, types untrimmed, evolution unpruned; Moves/Locations prompt to pick a game Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0665d0f2ef
commit
01269fd35a
@ -1,36 +1,49 @@
|
||||
import { el } from '../lib/dom.js';
|
||||
import { prettify } from '../data/pokedex-resolver.js';
|
||||
|
||||
const clean = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
|
||||
function label(names) {
|
||||
const arr = [...names].map(prettify);
|
||||
return arr.length <= 3 ? arr.join(', ') : `${arr.slice(0, 3).join(', ')} +${arr.length - 3}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Pokédex flavour text. The primary block shows the selected game's
|
||||
* entry/entries (with a Sword/Shield-style pill toggle when they differ);
|
||||
* an "All Pokédex entries" disclosure lists every distinct entry across
|
||||
* every game, newest first.
|
||||
*/
|
||||
export function FlavorText(species, vg) {
|
||||
const versions = vg ? vg.versions : [];
|
||||
const english = species.flavor_text_entries.filter((f) => f.language.name === 'en');
|
||||
if (!english.length) return null;
|
||||
|
||||
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, ' '));
|
||||
// distinct text -> set of version names, in newest-first order
|
||||
const order = [];
|
||||
const versionsByText = new Map();
|
||||
for (let i = english.length - 1; i >= 0; i--) {
|
||||
const text = clean(english[i].flavor_text);
|
||||
if (!versionsByText.has(text)) {
|
||||
versionsByText.set(text, new Set());
|
||||
order.push(text);
|
||||
}
|
||||
versionsByText.get(text).add(english[i].version.name);
|
||||
}
|
||||
|
||||
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 }];
|
||||
// The selected game's distinct entries.
|
||||
const wanted = vg ? vg.versions : [];
|
||||
let primary = [];
|
||||
for (const v of wanted) {
|
||||
const hit = english.find((e) => e.version.name === v);
|
||||
if (!hit) continue;
|
||||
const text = clean(hit.flavor_text);
|
||||
if (!primary.some((p) => p.text === text)) primary.push({ version: v, text });
|
||||
}
|
||||
let fallback = false;
|
||||
if (!primary.length) {
|
||||
primary = [{ version: [...versionsByText.get(order[0])][0], text: order[0] }];
|
||||
fallback = true;
|
||||
}
|
||||
if (!picks.length) return null;
|
||||
|
||||
const quote = el('p', { class: 'ppanel__flavor' });
|
||||
const caption = el('span', { class: 'flavor__cap' });
|
||||
@ -38,20 +51,19 @@ export function FlavorText(species, vg) {
|
||||
let index = 0;
|
||||
|
||||
function paint() {
|
||||
const p = picks[index];
|
||||
const p = primary[index];
|
||||
quote.textContent = p.text;
|
||||
quote.style.animation = 'none';
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
quote.offsetWidth;
|
||||
quote.offsetWidth; // reflow so the slide-in re-runs
|
||||
quote.style.animation = '';
|
||||
caption.textContent = p.fallback
|
||||
caption.textContent = 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) =>
|
||||
if (primary.length > 1) {
|
||||
primary.forEach((p, i) =>
|
||||
toggle.append(
|
||||
el(
|
||||
'button',
|
||||
@ -68,7 +80,35 @@ export function FlavorText(species, vg) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
paint();
|
||||
return el('div', { class: 'flavor' }, picks.length > 1 ? toggle : null, quote, caption);
|
||||
|
||||
const allEntries =
|
||||
order.length > 1
|
||||
? el(
|
||||
'details',
|
||||
{ class: 'flavor__all' },
|
||||
el('summary', {}, `All Pokédex entries (${order.length})`),
|
||||
el(
|
||||
'ul',
|
||||
{ class: 'flavor__list' },
|
||||
...order.map((text) =>
|
||||
el(
|
||||
'li',
|
||||
{},
|
||||
el('span', { class: 'flavor__list-ver' }, label(versionsByText.get(text))),
|
||||
el('span', {}, text),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
return el(
|
||||
'div',
|
||||
{ class: 'flavor' },
|
||||
primary.length > 1 ? toggle : null,
|
||||
quote,
|
||||
caption,
|
||||
allEntries,
|
||||
);
|
||||
}
|
||||
|
||||
@ -76,6 +76,23 @@ export async function openGameSheet() {
|
||||
function renderGames() {
|
||||
const st = settings.get();
|
||||
clear(list);
|
||||
|
||||
list.append(
|
||||
el(
|
||||
'button',
|
||||
{
|
||||
class: `gamecard gamecard--all${st.versionGroup === 'all' ? ' is-active' : ''}`,
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
settings.set({ versionGroup: 'all', pokedex: null });
|
||||
close();
|
||||
},
|
||||
},
|
||||
el('span', { class: 'gamecard__name' }, 'All games'),
|
||||
el('span', { class: 'gamecard__meta' }, 'National Dex · newest data, no generation limits'),
|
||||
),
|
||||
);
|
||||
|
||||
for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) {
|
||||
list.append(
|
||||
el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`),
|
||||
|
||||
@ -6,4 +6,6 @@ import { createStore } from './createStore.js';
|
||||
*/
|
||||
export const ui = createStore('pdx.ui', {
|
||||
detailTab: 'about',
|
||||
searchQuery: '',
|
||||
searchScroll: 0,
|
||||
});
|
||||
|
||||
@ -904,6 +904,38 @@
|
||||
color: var(--text-dim);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.flavor__all {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
.flavor__all summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.flavor__list {
|
||||
list-style: none;
|
||||
margin: 10px 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.flavor__list li {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.flavor__list-ver {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--type-main, var(--accent));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.flavor .ppanel__flavor {
|
||||
animation: none;
|
||||
@ -1434,3 +1466,11 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.gamecard--all {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
border-style: dashed;
|
||||
}
|
||||
.gamecard--all.is-active {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
@ -158,7 +158,8 @@ export async function DexGrid() {
|
||||
ids = rows.map((r) => r.species.id);
|
||||
|
||||
title.textContent = prettify(dex.name || dex.key);
|
||||
gamePill.firstChild.textContent = prettify(st.versionGroup);
|
||||
gamePill.firstChild.textContent =
|
||||
st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
||||
refreshMeta();
|
||||
paintGrid();
|
||||
}
|
||||
|
||||
@ -202,20 +202,28 @@ export async function PokemonDetail(nationalId) {
|
||||
const evoPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…'));
|
||||
loadEvolution(species, st, snap, vgGen).then((node) => evoPanel.replaceChildren(node));
|
||||
|
||||
const allGames = st.versionGroup === 'all';
|
||||
|
||||
// ---- Moves panel ----------------------------------------
|
||||
const movesPanel = el(
|
||||
'div',
|
||||
{ class: 'ppanel' },
|
||||
MovesList(pokemon.moves, {
|
||||
versionGroupKey: st.versionGroup,
|
||||
gen: vgGen,
|
||||
genOfVg: (name) => snap.versionGroupByKey.get(name)?.generation ?? 9,
|
||||
}),
|
||||
allGames
|
||||
? el('p', { class: 'detail__muted' }, 'Pick a game to see its learnset.')
|
||||
: MovesList(pokemon.moves, {
|
||||
versionGroupKey: st.versionGroup,
|
||||
gen: vgGen,
|
||||
genOfVg: (name) => snap.versionGroupByKey.get(name)?.generation ?? 9,
|
||||
}),
|
||||
);
|
||||
|
||||
// ---- Locations panel ----------------------------------
|
||||
const locPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…'));
|
||||
if (NO_ENCOUNTER_DATA.has(st.versionGroup)) {
|
||||
if (allGames) {
|
||||
locPanel.replaceChildren(
|
||||
el('p', { class: 'detail__muted' }, 'Pick a game to see where to catch it.'),
|
||||
);
|
||||
} else if (NO_ENCOUNTER_DATA.has(st.versionGroup)) {
|
||||
locPanel.replaceChildren(
|
||||
el('p', { class: 'detail__muted' }, "PokéAPI doesn't have wild-encounter data for this game yet."),
|
||||
);
|
||||
|
||||
@ -1,25 +1,43 @@
|
||||
import { el, clear, onTeardown } from '../lib/dom.js';
|
||||
import { loadSnapshot } from '../data/snapshot.js';
|
||||
import { settings } from '../store/settings.js';
|
||||
import { ui } from '../store/ui.js';
|
||||
import { entry } from '../store/selection.js';
|
||||
import { prettify } from '../data/pokedex-resolver.js';
|
||||
import { Sprite } from '../components/Sprite.js';
|
||||
import { TypeChip } from '../components/TypeChip.js';
|
||||
|
||||
/**
|
||||
* Global search across every Pokémon, not just the current dex. Runs
|
||||
* entirely off the bundled snapshot, so it works offline.
|
||||
* Global search across every Pokémon, independent of the selected dex. Runs
|
||||
* off the bundled snapshot (offline-capable). The query and scroll position
|
||||
* are kept in the `ui` store so going back or re-opening the tab lands you
|
||||
* exactly where you left off.
|
||||
*
|
||||
* Results are game-agnostic — matching is on name / national number / type.
|
||||
* Opening a result shows that Pokémon for whichever game is currently
|
||||
* selected (or all games), so its stats, learnset and dex text follow that
|
||||
* choice, not the search.
|
||||
*/
|
||||
export async function SearchView() {
|
||||
const snap = await loadSnapshot();
|
||||
const view = el('section', { class: 'view search' });
|
||||
|
||||
const st = settings.get();
|
||||
const scope =
|
||||
st.versionGroup === 'all'
|
||||
? 'Opening one shows it across all games.'
|
||||
: `Opening one shows it for ${prettify(st.versionGroup)}.`;
|
||||
|
||||
const results = el('ul', { class: 'search__results' });
|
||||
const input = el('input', {
|
||||
class: 'search__input',
|
||||
type: 'search',
|
||||
placeholder: 'Name, number or type…',
|
||||
autofocus: true,
|
||||
oninput: (e) => run(e.target.value),
|
||||
value: ui.get().searchQuery || '',
|
||||
oninput: (e) => {
|
||||
ui.set({ searchQuery: e.target.value });
|
||||
run(e.target.value);
|
||||
},
|
||||
});
|
||||
|
||||
view.append(
|
||||
@ -27,7 +45,7 @@ export async function SearchView() {
|
||||
'header',
|
||||
{ class: 'view__header' },
|
||||
el('h1', {}, 'Search all Pokémon'),
|
||||
el('p', {}, 'Look up any Pokémon from any game — not limited to your selected Pokédex.'),
|
||||
el('p', {}, `Every Pokémon, any game. ${scope}`),
|
||||
),
|
||||
input,
|
||||
results,
|
||||
@ -72,6 +90,10 @@ export async function SearchView() {
|
||||
}
|
||||
}
|
||||
|
||||
onTeardown(view, () => {});
|
||||
// Restore query + scroll on entry; remember scroll on the way out.
|
||||
run(input.value);
|
||||
requestAnimationFrame(() => window.scrollTo(0, ui.get().searchScroll || 0));
|
||||
onTeardown(view, () => ui.set({ searchScroll: window.scrollY }));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user