feat(ocr): add OcrProgressBar component with page-based ARIA semantics

Progress bar shows brand-mint fill on brand-sand background with
smooth transition. Displays page counter with tabular-nums and
skipped-pages warning in amber when applicable. Only renders when
totalPages > 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-13 10:13:57 +02:00
parent ddec64fc79
commit 035f9768bd
2 changed files with 82 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<script lang="ts">
let {
currentPage,
totalPages,
skippedPages = 0
}: {
currentPage: number;
totalPages: number;
skippedPages?: number;
} = $props();
let percentage = $derived((currentPage / totalPages) * 100);
</script>
{#if totalPages > 0}
<div class="flex flex-col items-center">
<div
class="bg-brand-sand mx-auto mt-4 h-2 w-full max-w-xs rounded-full"
role="progressbar"
aria-valuenow={currentPage}
aria-valuemax={totalPages}
aria-label="OCR progress"
>
<div
class="h-full rounded-full bg-brand-mint transition-all duration-500"
data-testid="progress-fill"
style="width: {percentage}%"
></div>
</div>
<span class="mt-1 text-xs text-gray-400 tabular-nums">
{currentPage} / {totalPages}
</span>
{#if skippedPages > 0}
<span class="mt-1 text-xs text-amber-600" data-testid="skipped-warning">
{skippedPages} Seiten übersprungen
</span>
{/if}
</div>
{/if}

View File

@@ -0,0 +1,43 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import OcrProgressBar from './OcrProgressBar.svelte';
afterEach(cleanup);
describe('OcrProgressBar', () => {
it('renders progress bar with correct ARIA attributes', async () => {
render(OcrProgressBar, { currentPage: 2, totalPages: 5 });
const bar = page.getByRole('progressbar');
await expect.element(bar).toHaveAttribute('aria-valuenow', '2');
await expect.element(bar).toHaveAttribute('aria-valuemax', '5');
});
it('hides progress bar when totalPages is zero', async () => {
render(OcrProgressBar, { currentPage: 0, totalPages: 0 });
await expect.element(page.getByRole('progressbar')).not.toBeInTheDocument();
});
it('fills to 100 percent when current equals total', async () => {
render(OcrProgressBar, { currentPage: 5, totalPages: 5 });
const fill = page.getByTestId('progress-fill');
await expect.element(fill).toBeInTheDocument();
const el = fill.element() as HTMLElement;
expect(el.style.width).toBe('100%');
});
it('shows page counter text', async () => {
render(OcrProgressBar, { currentPage: 3, totalPages: 7 });
await expect.element(page.getByText('3 / 7')).toBeInTheDocument();
});
it('shows skipped pages warning when skippedPages > 0', async () => {
render(OcrProgressBar, { currentPage: 5, totalPages: 5, skippedPages: 2 });
await expect.element(page.getByTestId('skipped-warning')).toBeInTheDocument();
});
it('does not show warning when skippedPages is 0', async () => {
render(OcrProgressBar, { currentPage: 3, totalPages: 5, skippedPages: 0 });
await expect.element(page.getByTestId('skipped-warning')).not.toBeInTheDocument();
});
});