dex/src/components/skeletons.js
chris a437f539fc Animation pass: shared-element morph, sliding tabs, count-ups
New lib/anim.js (countUp, fadeSlideIn, segThumb), all reduced-motion
aware:

- Shared-element morph: tapping a card tags its art with
  view-transition-name; the detail skeleton now renders the real sprite
  at hero size (router passes the match, animates the skeleton mount), so
  the sprite flies from the grid into the hero and the skeleton→content
  swap is seamless.
- Segmented controls (Team / Search / Breeding) get a sliding pill that
  measures each button and eases between them.
- Count-ups: the dex-feed progress number and the detail Stats total
  tick up.
- Detail tab content fades/rises on switch.
- Global :active scale-press on buttons/chips/rows, plus soft
  background/border transitions.
- Hero sprite has a slow idle bob.
- Router swallows the AbortError when a fast follow-up nav skips an
  in-flight view transition.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 09:22:08 -04:00

67 lines
1.9 KiB
JavaScript

import { el } from '../lib/dom.js';
import { Sprite } from './Sprite.js';
import { settings } from '../store/settings.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(match) {
const id = match && Number(match[1]);
// Real sprite in the hero spot (cheap, usually cached) so the tapped
// card's art can morph straight into it.
const art = id
? el(
'div',
{ class: 'sk-art sk-art--live', style: 'view-transition-name: pkmn-sprite' },
Sprite(id, {
style: settings.get().spriteStyle === 'default' ? 'official' : settings.get().spriteStyle,
alt: '',
size: 220,
}),
)
: el('div', { class: 'sk-art' });
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)),
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%')),
);
}