feat: add MangaGridSkeleton loading placeholder
Composes Skeleton into a cover-grid placeholder that reuses the shared .manga-grid / .manga-card layout, so it matches the real catalog grid at every breakpoint and content swaps in without a layout shift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mangalord"
|
||||
version = "0.110.0"
|
||||
version = "0.111.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mangalord"
|
||||
version = "0.110.0"
|
||||
version = "0.111.0"
|
||||
edition = "2021"
|
||||
default-run = "mangalord"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mangalord-frontend",
|
||||
"version": "0.110.0",
|
||||
"version": "0.111.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
35
frontend/src/lib/components/MangaGridSkeleton.svelte
Normal file
35
frontend/src/lib/components/MangaGridSkeleton.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
|
||||
/**
|
||||
* Placeholder for a manga cover grid while it loads. Reuses the global
|
||||
* `.manga-grid` / `.manga-card` shapes (single-sourced in tokens.css) so
|
||||
* it lines up pixel-for-pixel with the real grid at every breakpoint and
|
||||
* content swaps in without a layout shift.
|
||||
*/
|
||||
let { count = 12 }: { count?: number } = $props();
|
||||
|
||||
const cards = $derived(Array.from({ length: count }, (_, i) => i));
|
||||
</script>
|
||||
|
||||
<ul class="manga-grid" data-testid="manga-grid-skeleton" aria-hidden="true">
|
||||
{#each cards as i (i)}
|
||||
<li class="manga-card">
|
||||
<Skeleton height="0" radius="md" />
|
||||
<Skeleton variant="text" width="80%" radius="sm" />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
/* Reserve the 2/3 cover box the same way MangaCard does. `height: 0` on
|
||||
the primitive plus aspect-ratio here gives the cover its shape without
|
||||
hard-coding pixels. */
|
||||
.manga-card :global(.skeleton:first-child) {
|
||||
aspect-ratio: 2 / 3;
|
||||
}
|
||||
|
||||
.manga-card :global(.skeleton.text) {
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
</style>
|
||||
31
frontend/src/lib/components/MangaGridSkeleton.svelte.test.ts
Normal file
31
frontend/src/lib/components/MangaGridSkeleton.svelte.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/svelte';
|
||||
import MangaGridSkeleton from './MangaGridSkeleton.svelte';
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe('MangaGridSkeleton', () => {
|
||||
it('renders 12 card skeletons by default', () => {
|
||||
render(MangaGridSkeleton);
|
||||
const root = screen.getByTestId('manga-grid-skeleton');
|
||||
expect(root.querySelectorAll('.manga-card').length).toBe(12);
|
||||
});
|
||||
|
||||
it('honours the count prop', () => {
|
||||
render(MangaGridSkeleton, { props: { count: 4 } });
|
||||
const root = screen.getByTestId('manga-grid-skeleton');
|
||||
expect(root.querySelectorAll('.manga-card').length).toBe(4);
|
||||
});
|
||||
|
||||
it('reuses the shared .manga-grid layout class', () => {
|
||||
render(MangaGridSkeleton);
|
||||
expect(screen.getByTestId('manga-grid-skeleton').classList.contains('manga-grid')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('is decorative so the container announces loading', () => {
|
||||
render(MangaGridSkeleton);
|
||||
expect(screen.getByTestId('manga-grid-skeleton').getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user