Four new preferences, all in settings.js / applied via applyTheme() so boot and live changes go through one path: - fontScale (small/default/large): scales root font-size (92/100/115%); the whole stylesheet is already rem-based so this alone rescales everything. - reduceMotion: an explicit in-app override alongside the existing `prefers-reduced-motion` OS check, via a new :root[data-reduce-motion="true"] rule in tokens.css mirroring the existing media-query kill-switch. New prefersReducedMotion() helper (checks both) replaces the three hand-rolled matchMedia checks in router.js/Card.js/PokemonDetail.js. - defaultRoute (dex/team/search): redirects on a genuinely hash-less launch only (bookmark, home-screen icon, bare URL) — explicit nav (e.g. tapping "Dex") is never touched, since it's applied once in main.js before initRouter runs. - haptics: a short navigator.vibrate() pulse on the same catch/seen/ favorite toggles that already play the pop animation, via a new lib/haptics.js buzz() (no-ops without Vibration API support, e.g. iOS Safari). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
95 lines
3.1 KiB
JavaScript
95 lines
3.1 KiB
JavaScript
import { el } from './lib/dom.js';
|
|
import { DexGrid } from './views/DexGrid.js';
|
|
import { PokemonDetail } from './views/PokemonDetail.js';
|
|
import { SearchView } from './views/SearchView.js';
|
|
import { SettingsView } from './views/SettingsView.js';
|
|
import { MoveDetail } from './views/MoveDetail.js';
|
|
import { ItemDetail } from './views/ItemDetail.js';
|
|
import { AbilityDetail } from './views/AbilityDetail.js';
|
|
import { TeamView } from './views/TeamView.js';
|
|
import { NaturesView } from './views/NaturesView.js';
|
|
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
|
import { prefersReducedMotion } from './store/settings.js';
|
|
|
|
const routes = [
|
|
{ pattern: /^#?\/?$/, view: () => DexGrid() },
|
|
{ pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])), skeleton: detailSkeleton },
|
|
{ pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton },
|
|
{ pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton },
|
|
{ pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton },
|
|
{ pattern: /^#\/team$/, view: () => TeamView() },
|
|
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
|
{ pattern: /^#\/search$/, view: () => SearchView() },
|
|
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
|
];
|
|
|
|
function resolve(hash) {
|
|
for (const route of routes) {
|
|
const match = hash.match(route.pattern);
|
|
if (match) return { route, match };
|
|
}
|
|
return { route: routes[0], match: [] };
|
|
}
|
|
|
|
const canAnimate = () =>
|
|
typeof document.startViewTransition === 'function' && !prefersReducedMotion();
|
|
|
|
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;
|
|
|
|
function mount(node, animate) {
|
|
if (current) current.dispatchEvent(new CustomEvent('view:teardown'));
|
|
current = node;
|
|
if (animate && canAnimate()) {
|
|
document.startViewTransition(() => host.replaceChildren(node));
|
|
} else {
|
|
host.replaceChildren(node);
|
|
}
|
|
}
|
|
|
|
async function render() {
|
|
const hash = location.hash || '#/';
|
|
const { route, match } = resolve(hash);
|
|
|
|
// Instant feedback: a shaped skeleton for detail pages, a light
|
|
// placeholder otherwise.
|
|
mount(
|
|
route.skeleton
|
|
? route.skeleton()
|
|
: el('div', { class: 'view view--loading' }, 'Loading…'),
|
|
false,
|
|
);
|
|
window.scrollTo(0, 0);
|
|
|
|
try {
|
|
const node = await route.view(match);
|
|
if ((location.hash || '#/') !== hash) return; // superseded
|
|
// Skeleton/placeholder -> content morphs via the View Transitions API.
|
|
mount(node, true);
|
|
} catch (err) {
|
|
console.error(err);
|
|
mount(
|
|
el(
|
|
'div',
|
|
{ class: 'view view--error' },
|
|
el('h1', {}, 'Something went wrong'),
|
|
el('p', {}, err.message || String(err)),
|
|
el('a', { href: '#/', class: 'button' }, 'Back to the dex'),
|
|
),
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
|
|
rerenderCurrent = render;
|
|
window.addEventListener('hashchange', render);
|
|
render();
|
|
}
|