diff --git a/frontend/src/lib/components/ProgressRing.svelte b/frontend/src/lib/components/ProgressRing.svelte
new file mode 100644
index 00000000..4b4c5d89
--- /dev/null
+++ b/frontend/src/lib/components/ProgressRing.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+
+ {percentage}%
+
+
diff --git a/frontend/src/lib/components/ProgressRing.svelte.spec.ts b/frontend/src/lib/components/ProgressRing.svelte.spec.ts
new file mode 100644
index 00000000..8efc36e5
--- /dev/null
+++ b/frontend/src/lib/components/ProgressRing.svelte.spec.ts
@@ -0,0 +1,44 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import ProgressRing from './ProgressRing.svelte';
+
+afterEach(cleanup);
+
+describe('ProgressRing', () => {
+ it('renders the correct stroke-dasharray for 75%', async () => {
+ render(ProgressRing, { percentage: 75 });
+ const arc = document.querySelector('circle.fill-arc') as SVGCircleElement | null;
+ expect(arc).not.toBeNull();
+ // circumference = 2 * π * 7 ≈ 43.98; 75% of that ≈ 32.99
+ const dasharray = arc!.getAttribute('stroke-dasharray') ?? '';
+ const filled = parseFloat(dasharray.split(' ')[0]);
+ expect(filled).toBeCloseTo(32.99, 1);
+ });
+
+ it('renders a gray label when percentage is 0', async () => {
+ render(ProgressRing, { percentage: 0 });
+ const label = page.getByText('0%');
+ await expect.element(label).toBeInTheDocument();
+ // Label should carry the gray class, not the mint class
+ const el = (await label.element()) as HTMLElement;
+ expect(el.className).toContain('text-gray-400');
+ });
+
+ it('renders a mint-colored label when percentage is > 0', async () => {
+ render(ProgressRing, { percentage: 75 });
+ const label = page.getByText('75%');
+ await expect.element(label).toBeInTheDocument();
+ const el = (await label.element()) as HTMLElement;
+ expect(el.className).toContain('text-accent');
+ });
+
+ it('renders a fully filled arc for 100%', async () => {
+ render(ProgressRing, { percentage: 100 });
+ const arc = document.querySelector('circle.fill-arc') as SVGCircleElement | null;
+ expect(arc).not.toBeNull();
+ const dasharray = arc!.getAttribute('stroke-dasharray') ?? '';
+ const filled = parseFloat(dasharray.split(' ')[0]);
+ expect(filled).toBeCloseTo(43.98, 1);
+ });
+});