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 '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function node(stage, style, versionGroup) {
|
function nodeEl(stage, style, versionGroup, depth) {
|
||||||
return el(
|
return el(
|
||||||
'a',
|
'a',
|
||||||
{ class: 'evo__node', href: `#/pokemon/${stage.id}` },
|
{ class: 'evo__node', href: `#/pokemon/${stage.id}`, style: `--d:${depth}` },
|
||||||
Sprite(stage.id, { style, versionGroup, alt: stage.name, size: 72 }),
|
Sprite(stage.id, { style, versionGroup, alt: stage.name, size: 56 }),
|
||||||
el('span', { class: 'evo__name' }, stage.name.replace(/-/g, ' ')),
|
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 roots = [...nodes.values()].filter((n) => n.parent == null || !nodes.has(n.parent));
|
||||||
const rows = [];
|
const wrap = el('div', { class: 'evo' });
|
||||||
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(
|
// Vertical tree: a stage, then for each branch a "↓ trigger" step and the
|
||||||
'div',
|
// stage it leads to. Depth adds indentation so mid-chain splits (Wurmple)
|
||||||
{ class: 'evo' },
|
// stay grouped without overflowing on a phone.
|
||||||
...rows.map((path) => {
|
const renderNode = (n, depth) => {
|
||||||
const row = el('div', { class: 'evo__row' });
|
wrap.append(nodeEl(n, style, versionGroup, depth));
|
||||||
path.forEach((stage, i) => {
|
for (const kidId of n.kids) {
|
||||||
if (i > 0) {
|
const kid = nodes.get(kidId);
|
||||||
row.append(
|
wrap.append(
|
||||||
el(
|
el(
|
||||||
'span',
|
'div',
|
||||||
{ class: 'evo__arrow' },
|
{ class: 'evo__step', style: `--d:${depth + 1}` },
|
||||||
'→',
|
el('span', { class: 'evo__arrow', 'aria-hidden': 'true' }, '↓'),
|
||||||
stage.via ? el('span', { class: 'evo__via' }, stage.via) : null,
|
el('span', { class: 'evo__via' }, kid.via || 'evolves'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
renderNode(kid, depth + 1);
|
||||||
}
|
}
|
||||||
row.append(node(stage, style, versionGroup));
|
};
|
||||||
});
|
roots.forEach((r) => renderNode(r, 0));
|
||||||
return row;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pruned) {
|
if (pruned) {
|
||||||
wrap.append(
|
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 ----------------------------------- */
|
/* ---------- Evolution chain ----------------------------------- */
|
||||||
|
/* Vertical evolution tree — never overflows a phone; indents by depth. */
|
||||||
.evo {
|
.evo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 3px;
|
||||||
}
|
align-items: flex-start;
|
||||||
.evo__row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
.evo__node {
|
.evo__node {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 10px;
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
margin-left: calc(var(--d, 0) * 18px);
|
||||||
}
|
}
|
||||||
.evo__node:hover {
|
.evo__node:hover {
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
}
|
}
|
||||||
.evo__node .sprite {
|
.evo__node .sprite {
|
||||||
width: 72px;
|
width: 54px;
|
||||||
height: 72px;
|
height: 54px;
|
||||||
|
flex: none;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
.evo__name {
|
.evo__name {
|
||||||
font-size: 0.8rem;
|
font-size: 0.92rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
text-transform: capitalize;
|
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 {
|
.evo__arrow {
|
||||||
display: flex;
|
font-size: 1rem;
|
||||||
flex-direction: column;
|
line-height: 1;
|
||||||
align-items: center;
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
}
|
||||||
.evo__via {
|
.evo__via {
|
||||||
font-size: 0.66rem;
|
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Type matchups ---------------------------------- */
|
/* ---------- Type matchups ---------------------------------- */
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { el, clear, onTeardown } from '../lib/dom.js';
|
import { el, clear, onTeardown } from '../lib/dom.js';
|
||||||
|
import { onSwipe } from '../lib/swipe.js';
|
||||||
import { loadSnapshot } from '../data/snapshot.js';
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
|
import { resolvePokedex, dexRows, prettify } from '../data/pokedex-resolver.js';
|
||||||
import { getPokemon, getSpecies, getEvolutionChain, getEncounters } from '../data/api.js';
|
import { getPokemon, getSpecies, getEvolutionChain, getEncounters } from '../data/api.js';
|
||||||
@ -337,8 +338,17 @@ export async function PokemonDetail(nationalId) {
|
|||||||
|
|
||||||
selectTab(ui.get().detailTab);
|
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());
|
const off = settings.subscribe(() => syncTrack());
|
||||||
onTeardown(view, off);
|
onTeardown(view, () => {
|
||||||
|
off();
|
||||||
|
offSwipe();
|
||||||
|
});
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user