Land Back on the opened card (clear of header/nav); card spacing
- Feed remembers the opened card (ui.feedAnchor) and, on Back, scrolls that exact card just below the sticky header via scrollIntoView + a computed scroll-margin-top; re-corrects on webfont load and as off-screen cards render their real height, but yields once the user scrolls - history.scrollRestoration = 'manual' so the browser doesn't fight it; per-session scroll memory cleared on cold launch - Cards: ghost number nudged off the top edge; sprite dropped 10px (flex column grows the card, so nothing overlaps); intrinsic size bumped to match Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b0e3655dbf
commit
23272b99a7
@ -6,9 +6,15 @@ import { el } from './lib/dom.js';
|
|||||||
import { initRouter, rerender } from './router.js';
|
import { initRouter, rerender } from './router.js';
|
||||||
import { Nav } from './components/Nav.js';
|
import { Nav } from './components/Nav.js';
|
||||||
import { settings, applyTheme } from './store/settings.js';
|
import { settings, applyTheme } from './store/settings.js';
|
||||||
|
import { ui } from './store/ui.js';
|
||||||
|
|
||||||
applyTheme();
|
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
|
// 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
|
// Pokémon detail page is built once per visit — rebuild it when the selected
|
||||||
// game changes so its data (flavour text, learnset, matchups, evolution,
|
// game changes so its data (flavour text, learnset, matchups, evolution,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const ui = createStore('pdx.ui', {
|
|||||||
searchScroll: 0,
|
searchScroll: 0,
|
||||||
searchTab: 'pokemon',
|
searchTab: 'pokemon',
|
||||||
feedScroll: 0,
|
feedScroll: 0,
|
||||||
|
feedAnchor: '',
|
||||||
sort: 'dex',
|
sort: 'dex',
|
||||||
sortDesc: false,
|
sortDesc: false,
|
||||||
filter: 'all',
|
filter: 'all',
|
||||||
|
|||||||
@ -416,7 +416,7 @@
|
|||||||
border: 1px solid color-mix(in srgb, var(--type-main) 20%, var(--border));
|
border: 1px solid color-mix(in srgb, var(--type-main) 20%, var(--border));
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
content-visibility: auto;
|
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;
|
transition: transform 0.14s var(--ease-spring), box-shadow 0.14s ease;
|
||||||
animation: cardIn 0.4s var(--ease-spring) backwards;
|
animation: cardIn 0.4s var(--ease-spring) backwards;
|
||||||
animation-delay: calc(var(--i, 0) * 18ms);
|
animation-delay: calc(var(--i, 0) * 18ms);
|
||||||
@ -431,8 +431,8 @@
|
|||||||
}
|
}
|
||||||
.card__ghost {
|
.card__ghost {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: 10px;
|
||||||
right: 6px;
|
right: 8px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@ -462,6 +462,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
|
margin-top: 10px; /* flex column grows the card — no overlap with the body */
|
||||||
}
|
}
|
||||||
.card__art .sprite {
|
.card__art .sprite {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -71,6 +71,13 @@ export async function DexGrid() {
|
|||||||
const snap = await loadSnapshot();
|
const snap = await loadSnapshot();
|
||||||
|
|
||||||
const grid = el('div', { class: 'grid' });
|
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 query = '';
|
||||||
let filterKey = ui.get().filter || 'all';
|
let filterKey = ui.get().filter || 'all';
|
||||||
let sortKey = ui.get().sort || 'dex';
|
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.
|
// Changing the list order/contents makes the old scroll offset meaningless.
|
||||||
function resetScroll() {
|
function resetScroll() {
|
||||||
ui.set({ feedScroll: 0 });
|
ui.set({ feedScroll: 0, feedAnchor: '' });
|
||||||
window.scrollTo(0, 0);
|
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(
|
view.append(
|
||||||
el(
|
el(
|
||||||
'header',
|
'header',
|
||||||
@ -274,12 +305,12 @@ export async function DexGrid() {
|
|||||||
let mountedVG = settings.get().versionGroup;
|
let mountedVG = settings.get().versionGroup;
|
||||||
rebuild();
|
rebuild();
|
||||||
// Return to where the user last scrolled (e.g. after viewing a Pokémon).
|
// 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) => {
|
const offSettings = settings.subscribe((s) => {
|
||||||
if (s.versionGroup !== mountedVG) {
|
if (s.versionGroup !== mountedVG) {
|
||||||
mountedVG = s.versionGroup;
|
mountedVG = s.versionGroup;
|
||||||
ui.set({ feedScroll: 0 });
|
ui.set({ feedScroll: 0, feedAnchor: '' });
|
||||||
}
|
}
|
||||||
rebuild();
|
rebuild();
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user