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);
+ });
+});