feat: reserve home shelf space while it loads (fixes CLS)

The Continue-reading / Recommended shelves were fetched after the catalogue
and rendered only once populated, so they popped in late and shoved the grid
down on every authenticated home visit. Now the shelf fetch fires
concurrently with the catalogue and a reserved ShelfSkeleton holds the space
for logged-in users until it resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-06 21:53:03 +02:00
parent bd6ae86a85
commit ec73c6e001
6 changed files with 115 additions and 27 deletions

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import Skeleton from './Skeleton.svelte';
/**
* Placeholder for a horizontal home shelf (Continue reading / Recommended)
* while it loads. Reserves the shelf's vertical space so the catalog below
* doesn't get shoved down when the real shelf pops in after its late fetch.
* Dimensions mirror ContinueReadingShelf's 108×162 cards.
*/
let { count = 6, testid = 'shelf-skeleton' }: { count?: number; testid?: string } = $props();
</script>
<section class="shelf" data-testid={testid} aria-hidden="true">
<Skeleton variant="text" width="10rem" height="1.1rem" radius="sm" />
<ul class="track">
{#each Array(count) as _}
<li class="card">
<Skeleton width="108px" height="162px" radius="sm" />
<Skeleton variant="text" width="80%" radius="sm" />
</li>
{/each}
</ul>
</section>
<style>
.shelf {
margin: 0 0 var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.track {
list-style: none;
display: flex;
gap: var(--space-3);
padding: 0 0 var(--space-2);
margin: 0;
overflow: hidden;
}
.card {
display: flex;
flex-direction: column;
gap: var(--space-1);
width: 108px;
flex: 0 0 auto;
}
</style>

View File

@@ -0,0 +1,22 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/svelte';
import ShelfSkeleton from './ShelfSkeleton.svelte';
afterEach(() => cleanup());
describe('ShelfSkeleton', () => {
it('renders the requested number of card placeholders', () => {
render(ShelfSkeleton, { props: { count: 4, testid: 'sk' } });
expect(screen.getByTestId('sk').querySelectorAll('.card').length).toBe(4);
});
it('defaults to 6 cards', () => {
render(ShelfSkeleton, { props: { testid: 'sk' } });
expect(screen.getByTestId('sk').querySelectorAll('.card').length).toBe(6);
});
it('is decorative (aria-hidden)', () => {
render(ShelfSkeleton, { props: { testid: 'sk' } });
expect(screen.getByTestId('sk').getAttribute('aria-hidden')).toBe('true');
});
});