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:
49
frontend/src/lib/components/ShelfSkeleton.svelte
Normal file
49
frontend/src/lib/components/ShelfSkeleton.svelte
Normal 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>
|
||||
22
frontend/src/lib/components/ShelfSkeleton.svelte.test.ts
Normal file
22
frontend/src/lib/components/ShelfSkeleton.svelte.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user