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:
MechaCat02
2026-07-06 19:31:58 +02:00
parent 200ab1c0a0
commit 34c1122e4e
5 changed files with 69 additions and 3 deletions

2
backend/Cargo.lock generated
View File

@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mangalord"
version = "0.110.0"
version = "0.111.0"
dependencies = [
"anyhow",
"argon2",

View File

@@ -1,6 +1,6 @@
[package]
name = "mangalord"
version = "0.110.0"
version = "0.111.0"
edition = "2021"
default-run = "mangalord"

View File

@@ -1,6 +1,6 @@
{
"name": "mangalord-frontend",
"version": "0.110.0",
"version": "0.111.0",
"private": true,
"type": "module",
"scripts": {

View 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>

View 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');
});
});