feat(transcription): add sticky review progress counter to TranscriptionEditView

Shows 'X / Y geprüft' with a brand-mint progress bar at the top of the
transcription panel. Derived from the blocks prop — no extra state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-13 13:59:35 +02:00
parent 33dc4654e5
commit 73229077be
2 changed files with 109 additions and 68 deletions

View File

@@ -50,6 +50,9 @@ let debounceTimers = new SvelteMap<string, ReturnType<typeof setTimeout>>();
let pendingTexts = new SvelteMap<string, string>(); let pendingTexts = new SvelteMap<string, string>();
let sortedBlocks = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder)); let sortedBlocks = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder));
let hasBlocks = $derived(blocks.length > 0); let hasBlocks = $derived(blocks.length > 0);
let reviewedCount = $derived(blocks.filter((b) => b.reviewed).length);
let totalCount = $derived(blocks.length);
let reviewProgress = $derived(totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0);
function getSaveState(blockId: string): SaveState { function getSaveState(blockId: string): SaveState {
return saveStates.get(blockId) ?? 'idle'; return saveStates.get(blockId) ?? 'idle';
@@ -263,8 +266,21 @@ $effect(() => {
}); });
</script> </script>
<div class="flex h-full flex-col overflow-y-auto bg-surface p-4"> <div class="flex h-full flex-col overflow-y-auto bg-surface">
{#if hasBlocks} {#if hasBlocks}
<!-- Sticky review progress header -->
<div class="sticky top-0 z-10 border-b border-line bg-surface px-4 pt-3 pb-2">
<p class="font-sans text-xs text-ink-2">
<span class="font-semibold text-ink">{reviewedCount} / {totalCount}</span> geprüft
</p>
<div class="bg-brand-sand mt-1.5 h-0.5 w-full overflow-hidden rounded-full">
<div
class="h-full rounded-full bg-brand-mint transition-all duration-300"
style="width: {reviewProgress}%"
></div>
</div>
</div>
<div class="p-4">
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="flex flex-col gap-3" class="flex flex-col gap-3"
@@ -336,6 +352,7 @@ $effect(() => {
</details> </details>
{/if} {/if}
</div> </div>
</div>
{:else} {:else}
<div class="flex flex-1 flex-col items-center justify-center px-6 py-12 text-center"> <div class="flex flex-1 flex-col items-center justify-center px-6 py-12 text-center">
<svg <svg

View File

@@ -13,7 +13,9 @@ const block1 = {
text: 'Block eins', text: 'Block eins',
label: null, label: null,
sortOrder: 0, sortOrder: 0,
version: 0 version: 0,
source: 'MANUAL' as const,
reviewed: false
}; };
const block2 = { const block2 = {
id: 'b2', id: 'b2',
@@ -22,7 +24,9 @@ const block2 = {
text: 'Block zwei', text: 'Block zwei',
label: null, label: null,
sortOrder: 1, sortOrder: 1,
version: 0 version: 0,
source: 'OCR' as const,
reviewed: true
}; };
function renderView(overrides: Record<string, unknown> = {}, service = createConfirmService()) { function renderView(overrides: Record<string, unknown> = {}, service = createConfirmService()) {
@@ -232,3 +236,23 @@ describe('TranscriptionEditView — delete block', () => {
expect(onDeleteBlock).not.toHaveBeenCalled(); expect(onDeleteBlock).not.toHaveBeenCalled();
}); });
}); });
// ─── Review progress counter ──────────────────────────────────────────────────
describe('TranscriptionEditView — review progress counter', () => {
it('shows reviewed count and total when blocks exist', async () => {
// block1: reviewed=false, block2: reviewed=true → "1 / 2 geprüft"
renderView();
await expect.element(page.getByText(/1 \/ 2 geprüft/)).toBeInTheDocument();
});
it('shows 0 reviewed when no blocks are reviewed', async () => {
renderView({ blocks: [block1] }); // block1.reviewed = false
await expect.element(page.getByText(/0 \/ 1 geprüft/)).toBeInTheDocument();
});
it('does not show progress counter when there are no blocks', async () => {
renderView({ blocks: [] });
await expect.element(page.getByText(/geprüft/)).not.toBeInTheDocument();
});
});