diff --git a/src/main.js b/src/main.js index 0a8392a..f69b34f 100644 --- a/src/main.js +++ b/src/main.js @@ -6,9 +6,15 @@ 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, feedAnchor: '', searchScroll: 0 }); + // 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, diff --git a/src/store/ui.js b/src/store/ui.js index ab8c17e..78704ef 100644 --- a/src/store/ui.js +++ b/src/store/ui.js @@ -10,6 +10,7 @@ export const ui = createStore('pdx.ui', { searchScroll: 0, searchTab: 'pokemon', feedScroll: 0, + feedAnchor: '', sort: 'dex', sortDesc: false, filter: 'all', diff --git a/src/styles/layout.css b/src/styles/layout.css index e9291a6..b58237a 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -416,7 +416,7 @@ border: 1px solid color-mix(in srgb, var(--type-main) 20%, var(--border)); box-shadow: var(--shadow); content-visibility: auto; - contain-intrinsic-size: auto 236px; + contain-intrinsic-size: auto 250px; transition: transform 0.14s var(--ease-spring), box-shadow 0.14s ease; animation: cardIn 0.4s var(--ease-spring) backwards; animation-delay: calc(var(--i, 0) * 18ms); @@ -431,8 +431,8 @@ } .card__ghost { position: absolute; - top: -4px; - right: 6px; + top: 10px; + right: 8px; z-index: 0; font-size: 3rem; font-weight: 800; @@ -462,6 +462,7 @@ z-index: 1; width: 120px; height: 120px; + margin-top: 10px; /* flex column grows the card — no overlap with the body */ } .card__art .sprite { width: 100%; diff --git a/src/views/DexGrid.js b/src/views/DexGrid.js index 7685ca4..be20040 100644 --- a/src/views/DexGrid.js +++ b/src/views/DexGrid.js @@ -71,6 +71,13 @@ export async function DexGrid() { const snap = await loadSnapshot(); const grid = el('div', { class: 'grid' }); + // Remember which card was opened so Back can land on it exactly. + grid.addEventListener('click', (e) => { + const card = e.target.closest('.card'); + if (card && !e.target.closest('.card__caught')) { + ui.set({ feedAnchor: card.getAttribute('href') }); + } + }); let query = ''; let filterKey = ui.get().filter || 'all'; let sortKey = ui.get().sort || 'dex'; @@ -153,10 +160,34 @@ export async function DexGrid() { // Changing the list order/contents makes the old scroll offset meaningless. function resetScroll() { - ui.set({ feedScroll: 0 }); + ui.set({ feedScroll: 0, feedAnchor: '' }); window.scrollTo(0, 0); } + // Prefer landing on the exact card that was opened — robust against the + // grid's estimated off-screen card heights and late header reflow (webfont + // load). Re-correct a few times unless the user has already scrolled away. + function restoreScroll() { + const anchor = ui.get().feedAnchor; + const target = anchor && grid.querySelector(`.card[href="${anchor}"]`); + if (!target) { + window.scrollTo(0, ui.get().feedScroll || 0); + return; + } + let applied = null; + const go = () => { + if (applied != null && Math.abs(window.scrollY - applied) > 8) return; // user moved + const headH = document.querySelector('.feed-head')?.getBoundingClientRect().height || 0; + target.style.scrollMarginTop = `${Math.round(headH) + 16}px`; + target.scrollIntoView({ block: 'start' }); + applied = Math.round(window.scrollY); + }; + go(); + setTimeout(go, 80); + setTimeout(go, 250); + if (document.fonts && document.fonts.ready) document.fonts.ready.then(go); + } + view.append( el( 'header', @@ -274,12 +305,12 @@ export async function DexGrid() { let mountedVG = settings.get().versionGroup; rebuild(); // Return to where the user last scrolled (e.g. after viewing a Pokémon). - requestAnimationFrame(() => window.scrollTo(0, ui.get().feedScroll || 0)); + requestAnimationFrame(restoreScroll); const offSettings = settings.subscribe((s) => { if (s.versionGroup !== mountedVG) { mountedVG = s.versionGroup; - ui.set({ feedScroll: 0 }); + ui.set({ feedScroll: 0, feedAnchor: '' }); } rebuild(); });