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]]
|
[[package]]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.110.0"
|
version = "0.111.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.110.0"
|
version = "0.111.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "mangalord"
|
default-run = "mangalord"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.110.0",
|
"version": "0.111.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"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