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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user