Restore feed scroll position on Back; enlarge the detail back button

- The dex feed remembers scrollY on teardown (ui.feedScroll) and restores
  it on re-mount, so viewing a Pokémon and going back lands where you were.
  Reset to top when the filter, sort, direction or game changes.
- Router no longer force-scrolls after mount; it scrolls to top on the
  loading placeholder and each view manages its own position (detail pages
  land at top, feed and search restore).
- .phero__back is now a rounded pill with a real 40px tap target.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
chris 2026-08-27 17:40:13 -04:00
parent 0d95f9c93a
commit b0e3655dbf
4 changed files with 39 additions and 2 deletions

View File

@ -45,6 +45,9 @@ export function initRouter(host) {
const pending = el('div', { class: 'view view--loading' }, 'Loading…');
host.replaceChildren(pending);
// Views manage their own scroll: detail pages jump to the top, the feed
// and search restore where the user left off.
window.scrollTo(0, 0);
try {
const node = await route.view(match);
@ -64,7 +67,6 @@ export function initRouter(host) {
),
);
}
window.scrollTo(0, 0);
}
rerenderCurrent = render;

View File

@ -9,6 +9,7 @@ export const ui = createStore('pdx.ui', {
searchQuery: '',
searchScroll: 0,
searchTab: 'pokemon',
feedScroll: 0,
sort: 'dex',
sortDesc: false,
filter: 'all',

View File

@ -761,6 +761,21 @@
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
font-size: 0.9rem;
}
.phero__back {
display: inline-flex;
align-items: center;
gap: 4px;
flex: none;
padding: 8px 14px 8px 11px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.2);
font-size: 1rem;
font-weight: 700;
text-shadow: none;
}
.phero__back:hover {
background: rgba(255, 255, 255, 0.32);
}
.phero__num {
flex: 1;
min-width: 0;

View File

@ -108,6 +108,7 @@ export async function DexGrid() {
ui.set({ filter: f.key });
chipEls.forEach((c, i) => c.btn.classList.toggle('is-active', FILTERS[i].key === f.key));
paintGrid();
resetScroll();
},
},
el('span', {}, f.label),
@ -127,6 +128,7 @@ export async function DexGrid() {
ui.set({ sort: sortKey, sortDesc });
syncDirBtn();
paintGrid();
resetScroll();
},
},
...SORTS.map((s) => el('option', { value: s.key, selected: s.key === sortKey }, s.label)),
@ -140,6 +142,7 @@ export async function DexGrid() {
ui.set({ sortDesc });
syncDirBtn();
paintGrid();
resetScroll();
},
});
function syncDirBtn() {
@ -148,6 +151,12 @@ export async function DexGrid() {
}
syncDirBtn();
// Changing the list order/contents makes the old scroll offset meaningless.
function resetScroll() {
ui.set({ feedScroll: 0 });
window.scrollTo(0, 0);
}
view.append(
el(
'header',
@ -262,15 +271,25 @@ export async function DexGrid() {
paintGrid();
}
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));
const offSettings = settings.subscribe(rebuild);
const offSettings = settings.subscribe((s) => {
if (s.versionGroup !== mountedVG) {
mountedVG = s.versionGroup;
ui.set({ feedScroll: 0 });
}
rebuild();
});
const offSelection = selection.subscribe(() => {
refreshMeta();
if (filterKey !== 'all' && filterKey !== 'legendary') paintGrid();
else if (sortKey === 'caught') paintGrid();
});
onTeardown(view, () => {
ui.set({ feedScroll: window.scrollY });
offSettings();
offSelection();
});