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