feat(transcribe): guided empty state + Kurrent primer for first-time transcribers #320
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
When a transcriber opens the Transcribe panel on a document with zero regions, the panel currently shows only a document icon + the single sentence "Draw regions on the document to start transcribing." The "Für Training vormerken" footer is already visible even though nothing can be marked for training yet, which is confusing.
First-time transcribers — especially the 60+ Kurrent-literate family members we're counting on for the crowd workflow — cannot discover from this empty state:
Evidence:
/tmp/fa-audit/G2-transcribe-panel-open.pngfrom Phase B2 audit on 2026-04-24.This is the onramp for the primary user journey of the whole app (the crowd transcription flywheel that feeds
/admin/ocr). Every new volunteer hits this first; if it stalls them, we lose them. Kurrent literacy is a finite, aging resource — we can't afford drop-off.Non-goals
Proposed design
Replace the icon + single sentence with a three-step coach card plus a link to a Kurrent primer:
sections.length === 0— the user has nothing to mark.AppUser.preferencesJSONB in a follow-up if we want it to cross devices).Kurrent primer
Static help page at
/help/kurrent(new route):e/n/m/u/ long s / capital H /chligature). Each shown as inline SVG or PNG with a label.Source images can be drawn from Wikipedia Kurrent pages (CC-BY-SA) with attribution, OR commissioned as simple inline SVGs.
Implementation plan
Frontend
frontend/src/lib/components/TranscribeCoachEmptyState.svelte. Renders whensections.length === 0 && !dismissed. Three cards + dismissal button + link to/help/kurrent.frontend/src/lib/components/TranscribePanel.svelte:sections.length === 0.frontend/src/routes/help/kurrent/+page.svelte— static content component.localStorage.getItem('transcribe-coach-dismissed'). Store set inonMountafter click.i18n
10–12 new Paraglide keys in
frontend/messages/{de,en,es}.json:transcribe_coach_title,transcribe_coach_step_1,transcribe_coach_step_2,transcribe_coach_step_3,transcribe_coach_dismiss,transcribe_coach_kurrent_link,help_kurrent_title,help_kurrent_intro,help_kurrent_tip_1,help_kurrent_tip_2,help_kurrent_tip_3.Backend (optional, deferred)
Cross-device dismissal is out of scope for v1. If later desired:
AppUserwithpreferences JSONB NOT NULL DEFAULT '{}'.PATCH /api/users/me/preferences(self-serve).Tests
Verification
Manual walkthrough as a fresh user:
Acceptance criteria
sections.length === 0 && !dismissedsections.length === 0/help/kurrentwith ≥ 6 sample lettersCritical files
Related
?for the cheatsheet once #327 merges.👋 Leonie Voss — UI/UX & Accessibility
Walked through the proposal from a brand + accessibility + senior-UX lens. Captures the agreed changes to scope.
Added to scope
Animated SVG drag demo next to step 1. Pure inline SVG + SMIL (~5 KB), brand-navy dashed frame with mint press ripple and navy snap-confirmation check.
prefers-reduced-motionfreezes at the final frame. Proof-of-concept approved.(?) help chip next to the Read/Edit toggle in
TranscriptionPanelHeader.svelte— closes the "what does Read vs Edit do" gap the issue flagged as a first-time confusion point. Keyboard-accessible popover, focus-visible ring, two new i18n keys (label + description).Copy pass: markieren → einrahmen / "Rahmen ziehen" across the Transcribe panel. Markieren reads as "highlight with yellow marker" to the 60+ audience; einrahmen primes the correct mental model (draw a frame). Audit covers step 1,
transcription_empty_draw_hint,transcription_next_block_cta, and any tooltip labels using the old verb.Drop the Kurrent alphabet primer; build
/hilfe/transkription(Transkriptions-Richtlinien) instead. Wikipedia already does the alphabet reference for free — duplicating it adds nothing except maintenance. Project-specific editorial conventions are what a family archive actually needs.v1 content (uncontroversial, ship-ready):
[unleserlich][durchgestrichen: Wort][Name?]"Noch in Klärung" section flags open family decisions: Abkürzungen, Datumsformate, Zeilenumbrüche, Groß-/Kleinschreibung.
Top of page: one-line framing + external link to Wikipedia's Kurrent article for alphabet reference.
@media printstyles baked in (hide chrome, black-on-white body,break-inside: avoidper section,@page { margin: 1.5cm }) — seniors print reference material, and sharing as PDF is likely.Link to Richtlinien opens in a new tab (
target="_blank" rel="noopener noreferrer") with a visible "Öffnet in neuem Tab" annotation next to the link, so screen-reader and keyboard users aren't surprised.Expanded test plan:
Removed from scope / simplified
transcribe_coach_dismissdrops from the list). Cleaner proposal than my original critique of item 6 — credit to Marcel for the simplification.Flagged as a separate micro-issue (not #320)
When
blocks.length === 1 && block1.text === '', the dashed "Block 2 hinzufügen" CTA reads as premature — the user hasn't finished block 1 yet. Worth its own ticket for a copy/conditional tweak; out of scope here.Overall read
The floor proposed in the issue was already a real improvement over the single-sentence empty state. The final scope is sharper: a short, gesture-first coach that disappears the moment the user starts working; a help chip closing the Read/Edit confusion at the point of the control; and a Richtlinien page that answers the questions Wikipedia can't. Small, bounded, demo-day shippable, and it serves the hardest constraint (60+ first-timer on a laptop) without bloating the millennial-dense-info path.
👋 Leonie Voss — Follow-up: final coach copy & layout
After a second iteration round (prompted by user-testing feedback and a jargon check), the coach card landed a little different from what my first comment described. This supersedes the step-3 copy and animation scope there.
Two drivers for the change
Final structure — text-hero, animation as accent
Final copy
Title:
Preamble (expectation-setter):
Steps:
— + 5-s drawing animation (SVG + SMIL, with
prefers-reduced-motionfallback)Footer:
Why this copy works
What this changes from the earlier comment
transcribe_coach_preamble).Modellanywhere in user-facing copy.Unchanged from the earlier comment
/hilfe/transkriptionTranskriptions-Richtlinien page with@media printstylesi18n key count (updated)
Roughly 9 new keys for the coach + Richtlinien page (down from the original ~12):
transcribe_coach_title,transcribe_coach_preamble,transcribe_coach_step_1_title,transcribe_coach_step_1_body,transcribe_coach_step_2_title,transcribe_coach_step_2_body,transcribe_coach_step_3_title,transcribe_coach_footer_kurrent,transcribe_coach_footer_richtlinien— plus Richtlinien-page keys and the (?) Read/Edit help-chip keys as previously scoped.👨💻 Felix Brandt — Senior Fullstack Developer
Observations
TranscribeCoachEmptyState.svelte(new),HelpPopover.svelte(new, reused for Read/Edit help chip),/hilfe/transkription/+page.svelte(new). Each is nameable in one word.TranscriptionEditView.svelte:233-252— the coach replaces that block. Sameelsebranch, samehasBlocksguard.prefers-reduced-motionpattern already exists in 4 places (TranscriptionReadView.svelte:52,ChronikFuerDichBox.svelte:144,AnnotationShape.svelte:136,documents/[id]/+page.svelte:49— the latter usesmatchMedia). No invention needed, just follow precedent.TranscriptionBlock.svelte:229-239renderssaving/saved/fading/errorstates. Step 3's "Speichert automatisch." aligns with actual current behavior — zero backend or new UI for that step.coach_*i18n keys exist yet (grepcame up empty) — clean greenfield naming.Recommendations
lib/assets/transcribe-drag-demo.svgloaded via<img>+aria-label, or a dedicatedTranscribeDragDemo.svelte. Inline SMIL in the parent bloats the component past 60 lines and hides the animation from review diffs. My pick: dedicated Svelte component soprefers-reduced-motioncan be handled in the same file as the animation.TranscribeCoachEmptyState.svelte.spec.ts— renders preamble + 3 steps + animation region whenblocks.length === 0; rendering is suppressed whenblocks.length > 0.TranscriptionPanelHeader.svelte.test.ts— add tests for the (?) help chip: opens on click, opens on Enter/Space, closes on Esc, focus returns to the trigger on close, hasaria-expanded/aria-controls./hilfe/transkription/+page.svelte.spec.ts— all five v1 conventions present; Wikipedia link hastarget="_blank" rel="noopener noreferrer"; print stylesheet rules applied under@media print.$derivedfor visibility, never$effect.const showCoach = $derived(blocks.length === 0 && mode === 'edit');— themode === 'edit'guard matters (see Markus's note).+page.server.ts, addexport const prerender = true;so it's built to static HTML at build time. No SSR cost, no API surface.de.json/en.json/es.jsonin the same commit — partial translations leak English fallbacks to de users.lib/by accident from the Marcel-Leonie scratch file at/tmp/transcribe-coach-demo.html.Open Decisions
None.
🏛️ Markus Keller — Application Architect
Observations
TranscriptionEditView.sveltehandles edit;TranscriptionReadView.sveltehandles read. Any logic that puts the coach into the Read branch leaks across a module boundary that's already correctly drawn./hilfe/transkriptionis a new top-level route. No need to nest it under/adminor/documents— it's a general help resource, its own feature folder is correct.HelpPopover.svelte. Writing a bespoke popover per feature is coupling by copy-paste.Recommendations
HelpPopover.svelteas a reusable primitive, not a Read/Edit-specific chip. Props:label,content(slot),placement. First consumer is the Read/Edit toggle inTranscriptionPanelHeader.svelte; second consumer is likely the Transkriptions-Richtlinien page for per-convention "Warum?" explanations. One component, two initial uses, room to grow. Place it inlib/components/.TranscribeCoachEmptyState.sveltescoped to the empty state only. Drive visibility from the parent (TranscriptionEditView.svelte) with{#if !hasBlocks}<TranscribeCoachEmptyState ... />{/if}. Do not add mode-switching logic inside the coach — the parent owns themodeprop and decides whether the edit-mode empty branch renders at all.loadfunction, no backend call, no cache-control headaches. Its content updates ship via git commits, not runtime fetches — exactly right for a family archive's editorial conventions that change on the order of months.TranscribeCoachManager.svelteorTranscribeCoachService.ts. If the component has no state beyond theblocks.length === 0check and nothing to orchestrate, the Svelte component is the full logical unit. Services are for cross-component flows, not for feature scaffolding.Open Decisions
None. The scope is contained, the module boundaries are clear, and the transport/storage choices are all already made by existing precedent.
🛠️ Tobias Wendt — DevOps & Platform
Observations
frontend/CLAUDE.md— the 9 new keys × 3 languages flow through the existing pipeline automatically.Recommendations
export const prerender = true;). It's static content; prerendering means a reverse-proxy HIT on a file, not a Node process handling the request. Future-proof for when we put Caddy in front.main. If the 320/768/1440 × 2-theme screenshots land in the repo, that's 6 PNGs per page × N pages — manageable for the Richtlinien page (one page), potentially noisy if coach snapshots follow the same pattern. My pick: commit initial baselines, and prune any stale baselines in the same PR that renames or restyles the components. Considerlfsonly if the set grows past ~50 images.Open Decisions
None.
🔐 Nora "NullX" Steiner — Application Security
Observations
target="_blank" rel="noopener noreferrer"is already established inPdfViewer.svelte:169-170. Reuse it for the Wikipedia and Richtlinien links — not new territory.{@html}, no user-controlled content rendered into the DOM. XSS surface is zero.SecurityConfig); adding/hilfe/transkriptionwith no explicitpermitAll()inherits that default. Architecture decision needed — see Open Decisions.Recommendations
target="_blank" rel="noopener noreferrer"is correct. Additionally addreferrerpolicy="no-referrer"on that specific anchor — Wikipedia doesn't need to know the Familienarchiv referrer, and the policy is a one-attribute free privacy win. Belt-and-braces, no code cost.::after) so assistive tech announces it. Use an actual<span>inside the<a>, or pair the link with an adjacent visually-hidden<span class="sr-only">if the visible text would otherwise look noisy.<animate>elements since they're part of the bundled asset. If the app adds a stricter CSP later, SMIL is same-origin and does not needunsafe-inline.@RequirePermissionchanges, no backend work. Security footprint of this issue is basically "don't regress the external-link pattern."Open Decisions
/hilfe/transkription— see the consolidated Decision Queue.🧪 Sara Holt — QA & Test Strategist
Observations
@axe-core/playwrightis already integrated — three existing specs use it (header.spec.ts,korrespondenz.spec.ts,notification-deep-link.spec.ts).korrespondenz.spec.ts:4-5already has abuildAxe(page)helper withwithTags(['wcag2a', 'wcag2aa'])— reuse it.ThemeToggle.svelte:25setsdata-themeondocumentElement;+layout.svelte:89mounts it. The dark-mode axe test has real infrastructure to drive — no new harness needed.TranscriptionBlock.svelte.spec.tsexists). Step 3's "Speichert automatisch." aligns with existing save states; no new auto-save assertions.frontend/e2e/fixtures— E2E will need to create one or use a freshly-uploaded document in setup.Recommendations
prefers-reduced-motion: reduceper test:prefers-reduced-motionhandler Felix adds to the animation component, snapshots capture the frozen final frame deterministically.ThemeTogglebutton, don't directly mutatedata-theme. Two benefits: (a) smoke-tests the toggle itself, (b) one less thing to stub. Pattern:vitest-browser-svelte): coach renders/unrenders based onblocks; help chip keyboard interactions (Enter/Space open, Esc close, focus return); Richtlinien page renders all v1 convention sections.TranscriptionEditView.spec.tsalready exists — add cases for empty→non-empty coach transition.frontend/e2e/helpersthat uploads a document and returns its ID with zero blocks. Tests call it inbeforeEach. Keeps E2E independent of ordering.reducedMotion: 'reduce'the project-wide default inplaywright.config.tsfor non-animation-specific tests.page.emulateMedia({ media: 'print' })+ screenshot compare. One test, catches future regressions of the@media printblock.Open Decisions
None. All of this is within standard test-strategy calls I'm comfortable making.
👋 Leonie Voss — UI/UX Design Lead
No fresh concerns from my angle. I've already posted two comments on this issue — #issuecomment-4330 (full scope review) and #issuecomment-4331 (final copy + structure after user-testing round). The landed design, copy, and accessibility scope are what I'd push for, so no new recommendations here.
What I checked for this review pass: brand-token compliance, WCAG 2.1 AA coverage in both themes, touch-target and focus-ring discipline, senior-UX copy choices, responsive behavior, animation respect for
prefers-reduced-motion. All addressed in the earlier two comments.Open Decisions
None.
📋 Elicit — Requirements Engineer
Observations
Coach card renders iff sections.length === 0 && !dismissed). With the dismissal mechanism removed and the final copy locked per #issuecomment-4331, those criteria need a rewrite before implementation so testers can verify against them.Recommendations
de.json,en.json, andes.jsonbefore merge.Open Decisions
None. The open question about post-merge user testing is a plan, not a blocker.
🗳️ Decision Queue — Action Required
1 decision needs your input before implementation starts.
Security / Access Control
/hilfe/transkription— should the Transkriptions-Richtlinien page require login, or be publicly accessible?anyRequest().authenticated()): consistent with every other route; audience is family members only anyway; zero implementation cost (default behavior).permitAll()for this path): discoverable and shareable (family member on a phone, no login session, wants to check a convention); page contains no sensitive data — it's editorial style rules, not archive content. Cost: one extra line inSecurityConfigand a conscious decision that this path is intentionally public.📄 Spec committed —
/hilfe/transkriptionFinal UI/UX spec for the Transkriptions-Richtlinien page is now on
main:→
docs/specs/transkriptions-richtlinien-spec.html(commit1d5219ea)What it covers
@media printstylesOpen the file locally or via the Gitea link above to see the full rendering at multiple viewports and themes.