Align ability page with move/item; link Team at-a-glance names

- 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
This commit is contained in:
chris 2026-09-09 17:50:54 -04:00
parent 9810eb0996
commit 262e1a454e
3 changed files with 76 additions and 20 deletions

View File

@ -102,9 +102,11 @@ Working app with a type-themed UI:
/ "TMs only" / "HMs only" for the selected game and sort by power /
accuracy / recency — or, with a machine filter on, by TM/HM number
(numbers are baked into the snapshot). Items filter by category. Each
has its own detail page — the move page lists every Pokémon that can
learn it (filterable, narrowed to species that existed by the selected
game's generation). Query, scroll, tab and filters persist.
has its own detail page; the move, ability and item pages list the
Pokémon connected to them (learners / users / wild holders) as a
filterable grid, narrowed to the selected game (by generation, or
exactly by version for held items). Query, scroll, tab and filters
persist.
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
summary (types that hit 2+ members, or that someone's weak to and nobody
resists), then a full matchup grid with a diverging resist◀│▶weak bar per

View File

@ -1,6 +1,7 @@
import { el, clear } from '../lib/dom.js';
import { loadSnapshot } from '../data/snapshot.js';
import { settings } from '../store/settings.js';
import { prettify } from '../data/pokedex-resolver.js';
import { Sprite } from '../components/Sprite.js';
export async function AbilityDetail(id) {
@ -21,9 +22,51 @@ export async function AbilityDetail(id) {
}
const style = settings.get().spriteStyle;
const mons = a.pokemon.map((pid) => snap.speciesById.get(pid)).filter(Boolean);
const vg = settings.get().versionGroup;
const gen = snap.versionGroupByKey.get(vg)?.generation ?? 9;
// All-games: every species with the ability. A specific game: only those
// that existed by its generation.
let mons = a.pokemon.map((pid) => snap.speciesById.get(pid)).filter(Boolean);
if (vg !== 'all') mons = mons.filter((sp) => sp.generation <= gen);
mons.sort((x, y) => x.id - y.id);
const CAP = 400;
const grid = el('div', { class: 'ab-mons' });
function paint(q) {
const qq = (q || '').trim().toLowerCase().replace(/\s+/g, '-');
const shown = qq ? mons.filter((sp) => sp.name.includes(qq)) : mons;
clear(grid);
if (!shown.length) {
grid.append(el('p', { class: 'detail__muted' }, 'No matches.'));
return;
}
for (const sp of shown.slice(0, CAP)) {
grid.append(
el(
'a',
{ class: 'ab-mon', href: `#/pokemon/${sp.id}` },
Sprite(sp.id, { style, alt: sp.name, size: 48 }),
el('span', {}, sp.name.replace(/-/g, ' ')),
),
);
}
if (shown.length > CAP) {
grid.append(
el('p', { class: 'detail__muted' }, `+${shown.length - CAP} more — filter to narrow.`),
);
}
}
paint('');
const filterInput = el('input', {
type: 'search',
class: 'search__input learners__filter',
placeholder: 'Filter Pokémon…',
oninput: (e) => paint(e.target.value),
});
const predatesGame = vg !== 'all' && a.generation > gen;
clear(view).append(
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, ' Lookup')),
el(
@ -45,20 +88,21 @@ export async function AbilityDetail(id) {
el(
'section',
{ class: 'detail__section' },
el('h2', {}, `Pokémon with this ability (${mons.length})`),
mons.length
el(
'h2',
{},
`Pokémon with this ability${vg === 'all' ? '' : ` in ${prettify(vg)}`} (${mons.length})`,
),
predatesGame
? el(
'div',
{ class: 'ab-mons' },
...mons.map((sp) =>
el(
'a',
{ class: 'ab-mon', href: `#/pokemon/${sp.id}` },
Sprite(sp.id, { style, alt: sp.name, size: 48 }),
el('span', {}, sp.name.replace(/-/g, ' ')),
),
),
'p',
{ class: 'detail__muted' },
`Not in ${prettify(vg)} — this ability was introduced in Gen ${a.generation}.`,
)
: null,
mons.length > 24 ? filterInput : null,
mons.length
? grid
: el('p', { class: 'detail__muted' }, 'None on record.'),
),
);

View File

@ -309,17 +309,27 @@ export async function TeamView() {
'dl',
{ class: 'pfacts' },
fact('Average BST', String(avgBst)),
fact('Fastest', `${prettify(fastest.name)} (${fastest.stats?.spe ?? '?'} Spe)`),
fact('Fastest', monLink(fastest, `${fastest.stats?.spe ?? '?'} Spe`)),
fact(
'Bulkiest',
`${prettify(bulkiest.name)} (${
(bulkiest.stats?.hp || 0) + (bulkiest.stats?.def || 0) + (bulkiest.stats?.spd || 0)
} HP+Def+SpD)`,
monLink(
bulkiest,
`${(bulkiest.stats?.hp || 0) + (bulkiest.stats?.def || 0) + (bulkiest.stats?.spd || 0)} HP+Def+SpD`,
),
),
),
);
}
function monLink(sp, tail) {
return el(
'span',
{},
el('a', { class: 'link', href: `#/pokemon/${sp.id}` }, prettify(sp.name)),
` (${tail})`,
);
}
function compare(mem) {
const vg = snap.versionGroupByKey.get(settings.get().versionGroup);
const gen = vg ? vg.generation : 9;