feat(web): add LibrarySkeleton with list/grid/album variants

Shimmer placeholders that mirror the layout of each library page so
the real content slides in without shifting.
This commit is contained in:
2026-04-23 18:41:32 -04:00
parent 12d1cb739b
commit 9ed8e261d4
2 changed files with 71 additions and 0 deletions
@@ -0,0 +1,48 @@
<script lang="ts">
type Variant = 'list' | 'grid' | 'album';
let { variant, count = 12 }: { variant: Variant; count?: number } = $props();
// Common shimmer class applied to every placeholder block.
const shimmer = 'animate-pulse bg-surface';
</script>
{#if variant === 'list'}
<div data-testid="skeleton-list" class="divide-y divide-border">
{#each Array.from({ length: count }) as _, i (i)}
<div data-skeleton-row class="flex items-center gap-3 px-3 py-3">
<span class="h-10 w-10 rounded-full {shimmer}"></span>
<span class="h-4 flex-1 rounded {shimmer}"></span>
<span class="h-3 w-20 rounded {shimmer}"></span>
</div>
{/each}
</div>
{:else if variant === 'grid'}
<div
data-testid="skeleton-grid"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"
>
{#each Array.from({ length: count }) as _, i (i)}
<div data-skeleton-card>
<div class="aspect-square w-full rounded {shimmer}"></div>
<div class="mt-2 h-4 w-3/4 rounded {shimmer}"></div>
<div class="mt-1 h-3 w-1/3 rounded {shimmer}"></div>
</div>
{/each}
</div>
{:else}
<div data-testid="skeleton-album" class="space-y-6">
<div class="flex flex-col gap-4 md:flex-row md:items-end">
<div data-testid="skeleton-album-cover" class="h-60 w-60 rounded {shimmer}"></div>
<div class="flex-1 space-y-3">
<div class="h-8 w-3/4 rounded {shimmer}"></div>
<div class="h-4 w-1/3 rounded {shimmer}"></div>
<div class="h-4 w-1/4 rounded {shimmer}"></div>
</div>
</div>
<div class="space-y-2">
{#each Array.from({ length: 10 }) as _, i (i)}
<div class="h-6 w-full rounded {shimmer}"></div>
{/each}
</div>
</div>
{/if}
@@ -0,0 +1,23 @@
import { describe, expect, test } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import LibrarySkeleton from './LibrarySkeleton.svelte';
describe('LibrarySkeleton', () => {
test('variant="list" renders count placeholder rows', () => {
render(LibrarySkeleton, { props: { variant: 'list', count: 5 } });
const container = screen.getByTestId('skeleton-list');
expect(container.querySelectorAll('[data-skeleton-row]').length).toBe(5);
});
test('variant="grid" renders count placeholder cards', () => {
render(LibrarySkeleton, { props: { variant: 'grid', count: 6 } });
const container = screen.getByTestId('skeleton-grid');
expect(container.querySelectorAll('[data-skeleton-card]').length).toBe(6);
});
test('variant="album" renders a hero placeholder with cover block + bars', () => {
render(LibrarySkeleton, { props: { variant: 'album' } });
expect(screen.getByTestId('skeleton-album')).toBeInTheDocument();
expect(screen.getByTestId('skeleton-album-cover')).toBeInTheDocument();
});
});