feat: loading skeletons on the bookmarks and collections pages

Both loaders now stream the whole result (the single fetch is also the auth
gate) so the page shows a skeleton while it loads instead of the global nav
bar only: a ListRowSkeleton on bookmarks and a MangaGridSkeleton on the
collections grid, resolving to the sign-in / error / empty / list branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-07 07:18:43 +02:00
parent 5b51bcf056
commit 16cdec051a
10 changed files with 212 additions and 63 deletions

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import Skeleton from './Skeleton.svelte';
/**
* Placeholder for a cover + two-line row list (bookmarks, history, tagged
* pages). Reserves the list height so rows swap in without a shift.
*/
let {
count = 6,
coverWidth = '64px',
testid = 'list-row-skeleton'
}: { count?: number; coverWidth?: string; testid?: string } = $props();
</script>
<ul class="list" data-testid={testid} aria-hidden="true">
{#each Array(count) as _}
<li class="row" style="grid-template-columns: {coverWidth} 1fr;">
<Skeleton width={coverWidth} aspectRatio="2 / 3" radius="sm" />
<div class="meta">
<Skeleton variant="text" width="55%" radius="sm" />
<Skeleton variant="text" width="35%" radius="sm" />
</div>
</li>
{/each}
</ul>
<style>
.list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.row {
display: grid;
gap: var(--space-3);
align-items: center;
}
.meta {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-width: 0;
}
</style>

View File

@@ -0,0 +1,25 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/svelte';
import ListRowSkeleton from './ListRowSkeleton.svelte';
afterEach(() => cleanup());
describe('ListRowSkeleton', () => {
it('renders the requested number of rows', () => {
render(ListRowSkeleton, { props: { count: 5, testid: 'sk' } });
expect(screen.getByTestId('sk').querySelectorAll('.row').length).toBe(5);
});
it('defaults to 6 rows and is decorative', () => {
render(ListRowSkeleton, { props: { testid: 'sk' } });
const el = screen.getByTestId('sk');
expect(el.querySelectorAll('.row').length).toBe(6);
expect(el.getAttribute('aria-hidden')).toBe('true');
});
it('applies the cover width to the row template', () => {
render(ListRowSkeleton, { props: { coverWidth: '80px', testid: 'sk' } });
const row = screen.getByTestId('sk').querySelector('.row') as HTMLElement;
expect(row.style.gridTemplateColumns).toBe('80px 1fr');
});
});