Vertical evolution tree; swipe to page through the dex
- Evolution chain is now a vertical tree (stage, then '↓ trigger' step, then the next stage) that indents by branch depth. Never overflows a phone; branching chains (Eevee, Wurmple) stay grouped and readable. - Swipe left/right on the Pokémon detail page navigates prev/next in the current dex (src/lib/swipe.js). Claims horizontal drags via preventDefault so page scroll and the browser's edge back-gesture don't steal them; vertical drags pass through. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
1d35f2d433
commit
8ebdb9d197
@ -18,11 +18,11 @@ function describe(details) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function node(stage, style, versionGroup) {
|
||||
function nodeEl(stage, style, versionGroup, depth) {
|
||||
return el(
|
||||
'a',
|
||||
{ class: 'evo__node', href: `#/pokemon/${stage.id}` },
|
||||
Sprite(stage.id, { style, versionGroup, alt: stage.name, size: 72 }),
|
||||
{ class: 'evo__node', href: `#/pokemon/${stage.id}`, style: `--d:${depth}` },
|
||||
Sprite(stage.id, { style, versionGroup, alt: stage.name, size: 56 }),
|
||||
el('span', { class: 'evo__name' }, stage.name.replace(/-/g, ' ')),
|
||||
);
|
||||
}
|
||||
@ -72,35 +72,27 @@ export function EvolutionChain(chain, { style = 'default', versionGroup, genOf =
|
||||
}
|
||||
|
||||
const roots = [...nodes.values()].filter((n) => n.parent == null || !nodes.has(n.parent));
|
||||
const rows = [];
|
||||
const walkPaths = (n, acc) => {
|
||||
const path = [...acc, n];
|
||||
if (!n.kids.length) rows.push(path);
|
||||
else for (const k of n.kids) walkPaths(nodes.get(k), path);
|
||||
};
|
||||
roots.forEach((r) => walkPaths(r, []));
|
||||
const wrap = el('div', { class: 'evo' });
|
||||
|
||||
const wrap = el(
|
||||
'div',
|
||||
{ class: 'evo' },
|
||||
...rows.map((path) => {
|
||||
const row = el('div', { class: 'evo__row' });
|
||||
path.forEach((stage, i) => {
|
||||
if (i > 0) {
|
||||
row.append(
|
||||
el(
|
||||
'span',
|
||||
{ class: 'evo__arrow' },
|
||||
'→',
|
||||
stage.via ? el('span', { class: 'evo__via' }, stage.via) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
row.append(node(stage, style, versionGroup));
|
||||
});
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
// Vertical tree: a stage, then for each branch a "↓ trigger" step and the
|
||||
// stage it leads to. Depth adds indentation so mid-chain splits (Wurmple)
|
||||
// stay grouped without overflowing on a phone.
|
||||
const renderNode = (n, depth) => {
|
||||
wrap.append(nodeEl(n, style, versionGroup, depth));
|
||||
for (const kidId of n.kids) {
|
||||
const kid = nodes.get(kidId);
|
||||
wrap.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'evo__step', style: `--d:${depth + 1}` },
|
||||
el('span', { class: 'evo__arrow', 'aria-hidden': 'true' }, '↓'),
|
||||
el('span', { class: 'evo__via' }, kid.via || 'evolves'),
|
||||
),
|
||||
);
|
||||
renderNode(kid, depth + 1);
|
||||
}
|
||||
};
|
||||
roots.forEach((r) => renderNode(r, 0));
|
||||
|
||||
if (pruned) {
|
||||
wrap.append(
|
||||
|
||||
62
src/lib/swipe.js
Normal file
62
src/lib/swipe.js
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Detect a deliberate horizontal swipe on a touch device.
|
||||
*
|
||||
* Once the drag reads as horizontal it calls `preventDefault()` on
|
||||
* touchmove, which both stops the page scrolling sideways and stops the
|
||||
* browser's own edge back/forward gesture from stealing the swipe. Vertical
|
||||
* drags (scrolling), multi-touch and slow drags are ignored.
|
||||
*
|
||||
* onSwipe(node, { onLeft, onRight }) -> cleanup fn
|
||||
*/
|
||||
export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) {
|
||||
let x0 = 0;
|
||||
let y0 = 0;
|
||||
let t0 = 0;
|
||||
let active = false;
|
||||
let claimed = false;
|
||||
|
||||
const start = (e) => {
|
||||
if (e.touches.length !== 1) {
|
||||
active = false;
|
||||
return;
|
||||
}
|
||||
x0 = e.touches[0].clientX;
|
||||
y0 = e.touches[0].clientY;
|
||||
t0 = Date.now();
|
||||
active = true;
|
||||
claimed = false;
|
||||
};
|
||||
|
||||
const move = (e) => {
|
||||
if (!active || e.touches.length !== 1) return;
|
||||
const dx = e.touches[0].clientX - x0;
|
||||
const dy = e.touches[0].clientY - y0;
|
||||
if (!claimed) {
|
||||
if (Math.abs(dy) > 12 && Math.abs(dy) > Math.abs(dx)) {
|
||||
active = false; // it's a vertical scroll — let it be
|
||||
return;
|
||||
}
|
||||
if (Math.abs(dx) > 10 && Math.abs(dx) > Math.abs(dy)) claimed = true;
|
||||
}
|
||||
if (claimed) e.preventDefault();
|
||||
};
|
||||
|
||||
const end = (e) => {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
if (!claimed) return;
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - x0;
|
||||
if (Date.now() - t0 > 700 || Math.abs(dx) < threshold) return;
|
||||
(dx < 0 ? onLeft : onRight)?.();
|
||||
};
|
||||
|
||||
target.addEventListener('touchstart', start, { passive: true });
|
||||
target.addEventListener('touchmove', move, { passive: false });
|
||||
target.addEventListener('touchend', end, { passive: true });
|
||||
return () => {
|
||||
target.removeEventListener('touchstart', start);
|
||||
target.removeEventListener('touchmove', move);
|
||||
target.removeEventListener('touchend', end);
|
||||
};
|
||||
}
|
||||
@ -1124,52 +1124,52 @@
|
||||
}
|
||||
|
||||
/* ---------- Evolution chain ----------------------------------- */
|
||||
/* Vertical evolution tree — never overflows a phone; indents by depth. */
|
||||
.evo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.evo__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 3px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.evo__node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
margin-left: calc(var(--d, 0) * 18px);
|
||||
}
|
||||
.evo__node:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.evo__node .sprite {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
flex: none;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.evo__name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.evo__step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.74rem;
|
||||
margin-left: calc(var(--d, 0) * 18px + 16px);
|
||||
}
|
||||
.evo__arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.95rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.evo__via {
|
||||
font-size: 0.66rem;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---------- Type matchups ---------------------------------- */
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { el, clear, onTeardown } from '../lib/dom.js';
|
||||
import { onSwipe } from '../lib/swipe.js';
|
||||
import { loadSnapshot } from '../data/snapshot.js';
|
||||
import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
|
||||
import { getPokemon, getSpecies, getEvolutionChain, getEncounters } from '../data/api.js';
|
||||
@ -337,8 +338,17 @@ export async function PokemonDetail(nationalId) {
|
||||
|
||||
selectTab(ui.get().detailTab);
|
||||
|
||||
// Swipe left/right to page through the current dex.
|
||||
const offSwipe = onSwipe(view, {
|
||||
onLeft: () => next && (location.hash = `#/pokemon/${next.species.id}`),
|
||||
onRight: () => prev && (location.hash = `#/pokemon/${prev.species.id}`),
|
||||
});
|
||||
|
||||
const off = settings.subscribe(() => syncTrack());
|
||||
onTeardown(view, off);
|
||||
onTeardown(view, () => {
|
||||
off();
|
||||
offSwipe();
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user