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
98 lines
3.0 KiB
JavaScript
98 lines
3.0 KiB
JavaScript
import './styles/tokens.css';
|
|
import './styles/layout.css';
|
|
|
|
import { registerSW } from 'virtual:pwa-register';
|
|
import { el } from './lib/dom.js';
|
|
import { initRouter, rerender } from './router.js';
|
|
import { Nav } from './components/Nav.js';
|
|
import { settings, applyTheme } from './store/settings.js';
|
|
import { ui } from './store/ui.js';
|
|
|
|
applyTheme();
|
|
|
|
// Own the scroll position ourselves (views restore it deliberately).
|
|
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
|
|
// Navigation scroll memory is per-session — start a cold launch at the top.
|
|
ui.set({ feedScroll: 0, searchScroll: 0 });
|
|
|
|
// Land on the configured default screen for a fresh launch (no hash yet at
|
|
// all — a bookmark, the home-screen icon, or typing the bare URL). Explicit
|
|
// navigation, like tapping "Dex" in the nav, is never redirected — that
|
|
// only ever touches location.hash after this point.
|
|
if (!location.hash) {
|
|
const DEFAULT_ROUTES = { dex: '#/', team: '#/team', search: '#/search' };
|
|
location.hash = DEFAULT_ROUTES[settings.get().defaultRoute] || '#/';
|
|
}
|
|
|
|
// React to preference changes. The dex feed updates itself in place, but the
|
|
// Pokémon detail page is built once per visit — rebuild it when the selected
|
|
// game changes so its data (flavour text, learnset, matchups, evolution,
|
|
// abilities, locations, era sprites) matches the new game.
|
|
let lastVersionGroup = settings.get().versionGroup;
|
|
settings.subscribe((s) => {
|
|
applyTheme();
|
|
if (s.versionGroup !== lastVersionGroup) {
|
|
lastVersionGroup = s.versionGroup;
|
|
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
|
}
|
|
});
|
|
window
|
|
.matchMedia('(prefers-color-scheme: dark)')
|
|
.addEventListener('change', () => applyTheme());
|
|
|
|
const app = document.getElementById('app');
|
|
app.replaceChildren();
|
|
|
|
const viewHost = el('main', { id: 'view', class: 'view-host' });
|
|
app.append(Nav(), viewHost);
|
|
|
|
initRouter(viewHost);
|
|
|
|
// --- Service worker: prompt to refresh rather than silently swapping ------
|
|
const updateSW = registerSW({
|
|
onNeedRefresh() {
|
|
showToast('A new version is available.', 'Reload', () => updateSW(true));
|
|
},
|
|
onOfflineReady() {
|
|
showToast('Ready to use offline.', 'Dismiss');
|
|
},
|
|
});
|
|
|
|
// --- Offline indicator -------------------------------------------------
|
|
const netPill = el('div', { class: 'netpill', role: 'status' }, 'Offline');
|
|
function syncNet() {
|
|
if (navigator.onLine) {
|
|
netPill.remove();
|
|
} else if (!netPill.isConnected) {
|
|
document.body.append(netPill);
|
|
}
|
|
}
|
|
window.addEventListener('offline', syncNet);
|
|
window.addEventListener('online', () => {
|
|
syncNet();
|
|
showToast('Back online.', 'Dismiss');
|
|
});
|
|
syncNet();
|
|
|
|
function showToast(message, actionLabel, onAction) {
|
|
const toast = el(
|
|
'div',
|
|
{ class: 'toast', role: 'status' },
|
|
el('span', {}, message),
|
|
el(
|
|
'button',
|
|
{
|
|
class: 'toast__action',
|
|
type: 'button',
|
|
onclick: () => {
|
|
toast.remove();
|
|
onAction?.();
|
|
},
|
|
},
|
|
actionLabel,
|
|
),
|
|
);
|
|
document.body.append(toast);
|
|
if (!onAction) setTimeout(() => toast.remove(), 5000);
|
|
}
|