fix: address skeleton/nav-progress review findings

Correctness:
- MangaGridSkeleton: covers rendered 0px tall because height="0" is a
  definite height that defeats aspect-ratio. Skeleton gains an `aspectRatio`
  prop (height stays auto) so the 2/3 cover box is reserved and the grid no
  longer shifts when real covers load.
- Move `.manga-card` layout into global tokens.css (it was scoped to
  MangaCard, so the skeleton's cards inherited none of it). Both consumers
  now share one definition.
- NavProgress: rebuilt as an idle/loading/done state machine. It now
  completes to full and fades on settle (was snapping away instantly),
  restarts the trickle when one navigation supersedes another (was parking
  frozen near 90%), animates transform/opacity only with will-change gated to
  the active phase, and hides itself in reader fullscreen.
- Skeleton: text-variant height resolved in one place (no inline style
  shadowing a stylesheet rule); shimmer gradient/animation gated behind
  prefers-reduced-motion: no-preference so reduced motion shows a flat block.

A11y:
- Home loading region announces via visually-hidden text instead of an
  aria-label a live region won't reliably read.

Cleanup:
- Drop the fragile :first-child selector and index-keying ceremony in
  MangaGridSkeleton; simplify Skeleton's style construction.

Tests: adds regression guards for the 0px cover collapse, the aspect-ratio
box, text-height defaulting, and the nav-progress complete/restart phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-06 20:02:27 +02:00
parent 7bdbe3ce5b
commit 0d9505ce9f
13 changed files with 231 additions and 99 deletions

View File

@@ -132,16 +132,17 @@ test('shows the nav progress bar during a pending navigation and clears it after
const bar = page.getByTestId('nav-progress');
// Idle on a settled page.
await expect(bar).not.toHaveClass(/active/);
await expect(bar).toHaveAttribute('data-phase', 'idle');
// Client-side navigate into the (gated) detail page.
await page.locator(`a[href="/manga/${mangaId}"]`).first().click();
// Bar activates while the detail load is in flight.
await expect(bar).toHaveClass(/active/);
// Bar enters the loading phase while the detail load is in flight.
await expect(bar).toHaveAttribute('data-phase', 'loading');
// Let the load resolve; the detail renders and the bar goes idle again.
// Let the load resolve; the detail renders and the bar settles back to
// idle (via a brief done phase).
releaseDetail();
await expect(page.getByTestId('manga-title')).toHaveText('Berserk');
await expect(bar).not.toHaveClass(/active/);
await expect(bar).toHaveAttribute('data-phase', 'idle');
});