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); }