Compare commits
No commits in common. "1d35f2d433674e80a2ce9293cd2d68eb18a94640" and "23272b99a7257348e661abd85b41e8ac57bd9e41" have entirely different histories.
1d35f2d433
...
23272b99a7
@ -28,12 +28,6 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
|||||||
const next = entry(species.id);
|
const next = entry(species.id);
|
||||||
caughtBtn.setAttribute('aria-pressed', String(next.caught));
|
caughtBtn.setAttribute('aria-pressed', String(next.caught));
|
||||||
card.classList.toggle('is-caught', next.caught);
|
card.classList.toggle('is-caught', next.caught);
|
||||||
if (next.caught && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
||||||
caughtBtn.animate(
|
|
||||||
[{ transform: 'scale(1)' }, { transform: 'scale(1.4)' }, { transform: 'scale(1)' }],
|
|
||||||
{ duration: 260, easing: 'cubic-bezier(.34,1.4,.64,1)' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -37,25 +37,11 @@ export function ProgressRing() {
|
|||||||
const value = el('span', { class: 'ring__value' });
|
const value = el('span', { class: 'ring__value' });
|
||||||
const node = el('div', { class: 'ring' }, svg, el('div', { class: 'ring__label' }, value));
|
const node = el('div', { class: 'ring' }, svg, el('div', { class: 'ring__label' }, value));
|
||||||
|
|
||||||
let first = true;
|
function update({ caught = 0, seen = 0, total = 1 }) {
|
||||||
function update(stats) {
|
const safeTotal = Math.max(total, 1);
|
||||||
const draw = () => {
|
caughtArc.setAttribute('stroke-dashoffset', String(CIRC * (1 - caught / safeTotal)));
|
||||||
const { caught = 0, seen = 0, total = 1 } = stats;
|
seenArc.setAttribute('stroke-dashoffset', String(CIRC * (1 - Math.max(seen, caught) / safeTotal)));
|
||||||
const safeTotal = Math.max(total, 1);
|
value.replaceChildren(String(caught), el('small', {}, `/${total}`));
|
||||||
caughtArc.setAttribute('stroke-dashoffset', String(CIRC * (1 - caught / safeTotal)));
|
|
||||||
seenArc.setAttribute(
|
|
||||||
'stroke-dashoffset',
|
|
||||||
String(CIRC * (1 - Math.max(seen, caught) / safeTotal)),
|
|
||||||
);
|
|
||||||
value.replaceChildren(String(caught), el('small', {}, `/${total}`));
|
|
||||||
};
|
|
||||||
// Defer the first draw a frame so the arc animates in from empty.
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
requestAnimationFrame(draw);
|
|
||||||
} else {
|
|
||||||
draw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { node, update };
|
return { node, update };
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
import { el } from '../lib/dom.js';
|
|
||||||
|
|
||||||
const bar = (w, h = 14) =>
|
|
||||||
el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` });
|
|
||||||
|
|
||||||
const facts = () =>
|
|
||||||
el(
|
|
||||||
'div',
|
|
||||||
{ class: 'pfacts' },
|
|
||||||
...Array.from({ length: 6 }, () =>
|
|
||||||
el('div', { class: 'fact' }, bar('42px', 9), bar('72px', 15)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Placeholder shown while a Pokémon detail page loads. */
|
|
||||||
export function detailSkeleton() {
|
|
||||||
return el(
|
|
||||||
'section',
|
|
||||||
{ class: 'view pdetail sk' },
|
|
||||||
el(
|
|
||||||
'div',
|
|
||||||
{ class: 'phero sk-hero' },
|
|
||||||
el('div', { class: 'phero__top' }, bar('72px', 34), bar('130px', 14), bar('34px', 34)),
|
|
||||||
el('div', { class: 'phero__head' }, bar('55%', 30)),
|
|
||||||
el('div', { class: 'sk-art' }),
|
|
||||||
),
|
|
||||||
el(
|
|
||||||
'div',
|
|
||||||
{ class: 'psheet' },
|
|
||||||
el(
|
|
||||||
'div',
|
|
||||||
{ class: 'psheet__tabs' },
|
|
||||||
...Array.from({ length: 5 }, () => bar('58px', 18)),
|
|
||||||
),
|
|
||||||
el('div', { class: 'ppanel' }, bar('92%'), bar('84%'), bar('66%'), facts()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Placeholder for move / item detail pages. */
|
|
||||||
export function lookupSkeleton() {
|
|
||||||
return el(
|
|
||||||
'section',
|
|
||||||
{ class: 'view lookup sk' },
|
|
||||||
bar('84px', 16),
|
|
||||||
el('div', { class: 'sk-hero sk-hero--sm' }),
|
|
||||||
facts(),
|
|
||||||
el('div', { class: 'detail__section' }, bar('90px', 16), bar('96%'), bar('70%')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
18
src/main.js
18
src/main.js
@ -13,7 +13,7 @@ applyTheme();
|
|||||||
// Own the scroll position ourselves (views restore it deliberately).
|
// Own the scroll position ourselves (views restore it deliberately).
|
||||||
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
|
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
|
||||||
// Navigation scroll memory is per-session — start a cold launch at the top.
|
// Navigation scroll memory is per-session — start a cold launch at the top.
|
||||||
ui.set({ feedScroll: 0, searchScroll: 0 });
|
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
|
||||||
@ -49,22 +49,6 @@ const updateSW = registerSW({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 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) {
|
function showToast(message, actionLabel, onAction) {
|
||||||
const toast = el(
|
const toast = el(
|
||||||
'div',
|
'div',
|
||||||
|
|||||||
@ -5,13 +5,12 @@ import { SearchView } from './views/SearchView.js';
|
|||||||
import { SettingsView } from './views/SettingsView.js';
|
import { SettingsView } from './views/SettingsView.js';
|
||||||
import { MoveDetail } from './views/MoveDetail.js';
|
import { MoveDetail } from './views/MoveDetail.js';
|
||||||
import { ItemDetail } from './views/ItemDetail.js';
|
import { ItemDetail } from './views/ItemDetail.js';
|
||||||
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ pattern: /^#?\/?$/, view: () => DexGrid() },
|
{ pattern: /^#?\/?$/, view: () => DexGrid() },
|
||||||
{ pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])), skeleton: detailSkeleton },
|
{ pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])) },
|
||||||
{ pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton },
|
{ pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])) },
|
||||||
{ pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton },
|
{ pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])) },
|
||||||
{ pattern: /^#\/search$/, view: () => SearchView() },
|
{ pattern: /^#\/search$/, view: () => SearchView() },
|
||||||
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
||||||
];
|
];
|
||||||
@ -24,10 +23,6 @@ function resolve(hash) {
|
|||||||
return { route: routes[0], match: [] };
|
return { route: routes[0], match: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const canAnimate = () =>
|
|
||||||
typeof document.startViewTransition === 'function' &&
|
|
||||||
!window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
|
|
||||||
let rerenderCurrent = null;
|
let rerenderCurrent = null;
|
||||||
|
|
||||||
/** Re-run the active route (e.g. after the selected game changed). */
|
/** Re-run the active route (e.g. after the selected game changed). */
|
||||||
@ -38,38 +33,31 @@ export function rerender() {
|
|||||||
export function initRouter(host) {
|
export function initRouter(host) {
|
||||||
let current = null;
|
let current = null;
|
||||||
|
|
||||||
function mount(node, animate) {
|
|
||||||
if (current) current.dispatchEvent(new CustomEvent('view:teardown'));
|
|
||||||
current = node;
|
|
||||||
if (animate && canAnimate()) {
|
|
||||||
document.startViewTransition(() => host.replaceChildren(node));
|
|
||||||
} else {
|
|
||||||
host.replaceChildren(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function render() {
|
async function render() {
|
||||||
const hash = location.hash || '#/';
|
const hash = location.hash || '#/';
|
||||||
const { route, match } = resolve(hash);
|
const { route, match } = resolve(hash);
|
||||||
|
|
||||||
// Instant feedback: a shaped skeleton for detail pages, a light
|
if (current) {
|
||||||
// placeholder otherwise.
|
current.dispatchEvent(new CustomEvent('view:teardown'));
|
||||||
mount(
|
current.remove();
|
||||||
route.skeleton
|
current = null;
|
||||||
? route.skeleton()
|
}
|
||||||
: el('div', { class: 'view view--loading' }, 'Loading…'),
|
|
||||||
false,
|
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);
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const node = await route.view(match);
|
const node = await route.view(match);
|
||||||
if ((location.hash || '#/') !== hash) return; // superseded
|
// Guard against a fast second navigation while awaiting.
|
||||||
// Skeleton/placeholder -> content morphs via the View Transitions API.
|
if ((location.hash || '#/') !== hash) return;
|
||||||
mount(node, true);
|
current = node;
|
||||||
|
host.replaceChildren(node);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
mount(
|
host.replaceChildren(
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'view view--error' },
|
{ class: 'view view--error' },
|
||||||
@ -77,7 +65,6 @@ export function initRouter(host) {
|
|||||||
el('p', {}, err.message || String(err)),
|
el('p', {}, err.message || String(err)),
|
||||||
el('a', { href: '#/', class: 'button' }, 'Back to the dex'),
|
el('a', { href: '#/', class: 'button' }, 'Back to the dex'),
|
||||||
),
|
),
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -408,12 +408,12 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
170deg,
|
180deg,
|
||||||
color-mix(in srgb, var(--type-main) 55%, var(--surface)) 0%,
|
color-mix(in srgb, var(--type-main) 30%, var(--surface)) 0%,
|
||||||
color-mix(in srgb, var(--type-main) 26%, var(--surface)) 55%,
|
color-mix(in srgb, var(--type-main) 9%, var(--surface)) 52%,
|
||||||
color-mix(in srgb, var(--type-main) 15%, var(--surface)) 100%
|
var(--surface) 100%
|
||||||
);
|
);
|
||||||
border: 1px solid color-mix(in srgb, var(--type-main) 38%, 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 250px;
|
contain-intrinsic-size: auto 250px;
|
||||||
@ -431,7 +431,7 @@
|
|||||||
}
|
}
|
||||||
.card__ghost {
|
.card__ghost {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 10px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
@ -444,7 +444,7 @@
|
|||||||
}
|
}
|
||||||
.card__spot {
|
.card__spot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 48px;
|
top: 14px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 128px;
|
width: 128px;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
@ -460,15 +460,13 @@
|
|||||||
.card__art {
|
.card__art {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex: 1 1 auto;
|
width: 120px;
|
||||||
min-height: 152px;
|
height: 120px;
|
||||||
padding-top: 36px; /* drop the sprite clear of the ghost number */
|
margin-top: 10px; /* flex column grows the card — no overlap with the body */
|
||||||
display: grid;
|
|
||||||
place-items: center; /* centre the sprite in the space that's left */
|
|
||||||
}
|
}
|
||||||
.card__art .sprite {
|
.card__art .sprite {
|
||||||
width: 116px;
|
width: 100%;
|
||||||
height: 116px;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28));
|
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28));
|
||||||
}
|
}
|
||||||
@ -476,19 +474,21 @@
|
|||||||
.card[data-sprite="game"] .card__art .sprite {
|
.card[data-sprite="game"] .card__art .sprite {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
.card[data-sprite="game"] .card__art .sprite {
|
.card[data-sprite="game"] .card__art {
|
||||||
width: 92px;
|
width: 96px;
|
||||||
height: 92px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
/* Gen 1–2 game sprites ship with an opaque white background — show them as
|
/* Gen 1–2 game sprites ship with an opaque white background — show them as
|
||||||
a small framed tile rather than a box that swallows the card. */
|
a small framed tile rather than a box that swallows the card. */
|
||||||
.card[data-boxed] .card__art .sprite {
|
.card[data-boxed] .card__art {
|
||||||
width: 74px;
|
width: 78px;
|
||||||
height: 74px;
|
height: 78px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #f7f7f5;
|
background: #f7f7f5;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.14);
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
.card[data-boxed] .card__art .sprite {
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
.card__body {
|
.card__body {
|
||||||
@ -1718,110 +1718,3 @@
|
|||||||
.gamecard--all.is-active {
|
.gamecard--all.is-active {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= Motion: transitions, skeletons, indicators ======== */
|
|
||||||
.view:not(.sk):not(.view--loading) {
|
|
||||||
animation: viewIn 0.26s ease both;
|
|
||||||
}
|
|
||||||
@keyframes viewIn {
|
|
||||||
from { opacity: 0; transform: translateY(6px); }
|
|
||||||
to { opacity: 1; transform: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
::view-transition-old(root),
|
|
||||||
::view-transition-new(root) {
|
|
||||||
animation-duration: 0.26s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Skeletons ---------------------------------------------- */
|
|
||||||
.sk-bar {
|
|
||||||
display: block;
|
|
||||||
border-radius: 7px;
|
|
||||||
margin: 7px 0;
|
|
||||||
background-color: var(--surface-2);
|
|
||||||
background-image: linear-gradient(
|
|
||||||
100deg,
|
|
||||||
transparent 20%,
|
|
||||||
color-mix(in srgb, var(--text) 9%, transparent) 45%,
|
|
||||||
transparent 70%
|
|
||||||
);
|
|
||||||
background-size: 220% 100%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
animation: shimmer 1.25s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes shimmer {
|
|
||||||
from { background-position: 220% 0; }
|
|
||||||
to { background-position: -120% 0; }
|
|
||||||
}
|
|
||||||
.sk-hero {
|
|
||||||
background: var(--surface-2);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 16px 20px 44px;
|
|
||||||
}
|
|
||||||
.sk-hero--sm {
|
|
||||||
min-height: 100px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.sk-art {
|
|
||||||
width: min(230px, 56vw);
|
|
||||||
height: min(230px, 56vw);
|
|
||||||
margin: 16px auto 0;
|
|
||||||
border-radius: 26px;
|
|
||||||
background: color-mix(in srgb, var(--text) 6%, var(--surface-2));
|
|
||||||
}
|
|
||||||
.sk .psheet {
|
|
||||||
margin-top: -28px;
|
|
||||||
}
|
|
||||||
.sk .psheet__tabs {
|
|
||||||
gap: 12px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.sk .ppanel .sk-bar {
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
.sk .pfacts .sk-bar {
|
|
||||||
margin: 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Online / offline indicator ------------------------------ */
|
|
||||||
.netpill {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
bottom: calc(var(--nav-size) + env(safe-area-inset-bottom) + 12px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 45;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
padding: 7px 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
background: #333842;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
animation: viewIn 0.25s ease both;
|
|
||||||
}
|
|
||||||
.netpill::before {
|
|
||||||
content: "";
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #ffb454;
|
|
||||||
}
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.netpill {
|
|
||||||
bottom: 20px;
|
|
||||||
left: calc(50% + 110px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.view:not(.sk):not(.view--loading),
|
|
||||||
.netpill {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
.sk-bar {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,20 +160,26 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return to the saved scroll offset. Retry a few times — the grid's height
|
// Prefer landing on the exact card that was opened — robust against the
|
||||||
// isn't final until the webfont loads and off-screen cards settle — but
|
// grid's estimated off-screen card heights and late header reflow (webfont
|
||||||
// stop once we've reached the target or the user starts scrolling.
|
// load). Re-correct a few times unless the user has already scrolled away.
|
||||||
function restoreScroll() {
|
function restoreScroll() {
|
||||||
const y = ui.get().feedScroll || 0;
|
const anchor = ui.get().feedAnchor;
|
||||||
if (!y) return;
|
const target = anchor && grid.querySelector(`.card[href="${anchor}"]`);
|
||||||
|
if (!target) {
|
||||||
|
window.scrollTo(0, ui.get().feedScroll || 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let applied = null;
|
let applied = null;
|
||||||
const go = () => {
|
const go = () => {
|
||||||
if (applied != null && Math.abs(window.scrollY - applied) > 8) return; // user moved
|
if (applied != null && Math.abs(window.scrollY - applied) > 8) return; // user moved
|
||||||
window.scrollTo(0, y);
|
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);
|
applied = Math.round(window.scrollY);
|
||||||
};
|
};
|
||||||
go();
|
go();
|
||||||
@ -297,7 +310,7 @@ export async function DexGrid() {
|
|||||||
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();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -135,12 +135,6 @@ export async function PokemonDetail(nationalId) {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
toggle(nationalId, field);
|
toggle(nationalId, field);
|
||||||
syncTrack();
|
syncTrack();
|
||||||
if (entry(nationalId)[field] && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
||||||
b.animate(
|
|
||||||
[{ transform: 'scale(1)' }, { transform: 'scale(1.12)' }, { transform: 'scale(1)' }],
|
|
||||||
{ duration: 240, easing: 'cubic-bezier(.34,1.4,.64,1)' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.append(label);
|
b.append(label);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user