From 200ab1c0a035d3c1ddee5d3abf9031d82f4550f7 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 6 Jul 2026 19:28:00 +0200 Subject: [PATCH] feat: add Skeleton loading-placeholder primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reusable, token-driven shimmer block with configurable width/height, radius, and variant. Decorative (aria-hidden) — loading state is announced by the container. Freezes to a solid muted block under the global prefers-reduced-motion override. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- frontend/package.json | 2 +- frontend/src/lib/components/Skeleton.svelte | 85 +++++++++++++++++++ .../lib/components/Skeleton.svelte.test.ts | 49 +++++++++++ 5 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/components/Skeleton.svelte create mode 100644 frontend/src/lib/components/Skeleton.svelte.test.ts diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 3561d6a..fdd3f4e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.109.1" +version = "0.110.0" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e9973ba..3342068 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.109.1" +version = "0.110.0" edition = "2021" default-run = "mangalord" diff --git a/frontend/package.json b/frontend/package.json index 2da7b92..12b924f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.109.1", + "version": "0.110.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/components/Skeleton.svelte b/frontend/src/lib/components/Skeleton.svelte new file mode 100644 index 0000000..1bed695 --- /dev/null +++ b/frontend/src/lib/components/Skeleton.svelte @@ -0,0 +1,85 @@ + + + + + diff --git a/frontend/src/lib/components/Skeleton.svelte.test.ts b/frontend/src/lib/components/Skeleton.svelte.test.ts new file mode 100644 index 0000000..9545c08 --- /dev/null +++ b/frontend/src/lib/components/Skeleton.svelte.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/svelte'; +import Skeleton from './Skeleton.svelte'; + +afterEach(() => cleanup()); + +describe('Skeleton primitive', () => { + it('renders an element carrying the given testid', () => { + render(Skeleton, { props: { testid: 'sk' } }); + expect(screen.getByTestId('sk')).toBeTruthy(); + }); + + it('is decorative (aria-hidden) so screen readers ignore it', () => { + render(Skeleton, { props: { testid: 'sk' } }); + expect(screen.getByTestId('sk').getAttribute('aria-hidden')).toBe('true'); + }); + + it('applies width and height as inline styles', () => { + render(Skeleton, { props: { testid: 'sk', width: '120px', height: '2rem' } }); + const el = screen.getByTestId('sk') as HTMLElement; + expect(el.style.width).toBe('120px'); + expect(el.style.height).toBe('2rem'); + }); + + it('defaults width to 100% when omitted', () => { + render(Skeleton, { props: { testid: 'sk' } }); + expect((screen.getByTestId('sk') as HTMLElement).style.width).toBe('100%'); + }); + + it('maps the radius prop to a radius class', () => { + render(Skeleton, { props: { testid: 'sk', radius: 'lg' } }); + expect(screen.getByTestId('sk').classList.contains('radius-lg')).toBe(true); + }); + + it('defaults to the md radius', () => { + render(Skeleton, { props: { testid: 'sk' } }); + expect(screen.getByTestId('sk').classList.contains('radius-md')).toBe(true); + }); + + it('renders a pill radius for the circle variant', () => { + render(Skeleton, { props: { testid: 'sk', variant: 'circle' } }); + expect(screen.getByTestId('sk').classList.contains('radius-pill')).toBe(true); + }); + + it('carries the skeleton base class for the shimmer', () => { + render(Skeleton, { props: { testid: 'sk' } }); + expect(screen.getByTestId('sk').classList.contains('skeleton')).toBe(true); + }); +});