feat(transcribe): guided empty state + Kurrent primer for first-time transcribers #320

Closed
opened 2026-04-24 13:23:11 +02:00 by marcel · 11 comments
Owner

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:

  1. They must click-and-drag on the image to define a region.
  2. What a "region" is (one word? one line? a paragraph?).
  3. What the Read vs Edit toggle does.
  4. What "mark for training" does, why it matters, and why it's off-limits when they have nothing to mark.

Evidence: /tmp/fa-audit/G2-transcribe-panel-open.png from 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

  • No changes to transcription engine or region model.
  • Not a forced modal — coaching must sit inline and be dismissible.
  • Not a full help center — the Kurrent primer is a one-page reference, not a treatise.

Proposed design

Replace the icon + single sentence with a three-step coach card plus a link to a Kurrent primer:

┌────────────────────────────────────────────────────────────┐
│ Erste Transkription? So geht's:                            │
│                                                            │
│ 1 ▸ Klicken und ziehen Sie auf dem Bild, um einen         │
│     Textbereich zu markieren.                              │
│ 2 ▸ Geben Sie den Text ein, den Sie im markierten Bereich │
│     sehen. → Kurrent-Hilfe                                 │
│ 3 ▸ Speichern. Ihre Transkription verbessert das Kurrent- │
│     Modell, wenn Sie "Für Training vormerken" aktivieren.  │
│                                                            │
│ [ Ich kenne das schon — Hinweis ausblenden ]               │
└────────────────────────────────────────────────────────────┘
  • Hide the "Für Training vormerken" footer when sections.length === 0 — the user has nothing to mark.
  • Dismissal is per-user, persistent (localStorage v1; server-side via AppUser.preferences JSONB in a follow-up if we want it to cross devices).
  • After the first region is drawn, the coach card auto-hides (future loads respect the stored dismissal).

Kurrent primer

Static help page at /help/kurrent (new route):

  • One-page reference, no tabs.
  • 6–10 common Kurrent letterforms with their modern equivalents (e / n / m / u / long s / capital H / ch ligature). Each shown as inline SVG or PNG with a label.
  • 2–3 tips for novices ("Long s looks like an f without the crossbar", "The u often has a small u-shaped mark above it for clarity").
  • Link back to the Transcribe panel.

Source images can be drawn from Wikipedia Kurrent pages (CC-BY-SA) with attribution, OR commissioned as simple inline SVGs.

Implementation plan

Frontend

  • New frontend/src/lib/components/TranscribeCoachEmptyState.svelte. Renders when sections.length === 0 && !dismissed. Three cards + dismissal button + link to /help/kurrent.
  • Update frontend/src/lib/components/TranscribePanel.svelte:
    • Conditional render of the coach vs. the existing empty state.
    • Hide the training footer when sections.length === 0.
  • New route frontend/src/routes/help/kurrent/+page.svelte — static content component.
  • Dismissal via localStorage.getItem('transcribe-coach-dismissed'). Store set in onMount after 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:

  • Extend AppUser with preferences JSONB NOT NULL DEFAULT '{}'.
  • PATCH /api/users/me/preferences (self-serve).
  • Coach reads from server preference first, falls back to localStorage.

Tests

  • Component: TranscribeCoachEmptyState renders when sections empty; hidden when sections non-empty; hidden when dismissal flag set.
  • Component: TranscribePanel hides training footer when sections empty.
  • E2E: login, open a document with zero regions, assert coach card visible with all three steps; assert training footer NOT visible; click dismiss; reload page; assert coach is gone.
  • E2E: login, open a document with ≥ 1 region, assert coach is NOT visible.
  • Visual: snapshot of the coach card at desktop and tablet viewports.

Verification

Manual walkthrough as a fresh user:

  1. Sign in, find an untranscribed document.
  2. Open Transcribe panel → coach card visible.
  3. Read steps; click Kurrent link → primer opens, back-link works.
  4. Draw a region on the image → coach card disappears; training footer now appears beside the new region.
  5. Dismiss coach manually on a second untranscribed document; reload → coach stays dismissed.

Acceptance criteria

  • Coach card renders iff sections.length === 0 && !dismissed
  • Training footer hidden iff sections.length === 0
  • Dismissal is persistent (localStorage minimum)
  • Kurrent primer route exists at /help/kurrent with ≥ 6 sample letters
  • i18n complete for de/en/es (all new keys present in each)
  • No regression on documents that already have regions
  • axe-core passes on both the panel empty state and the primer page

Critical files

frontend/src/lib/components/TranscribeCoachEmptyState.svelte   (new)
frontend/src/lib/components/TranscribePanel.svelte             (wire conditional)
frontend/src/routes/help/kurrent/+page.svelte                  (new)
frontend/src/routes/help/kurrent/kurrent-letters.svg           (new, or PNG set)
frontend/messages/de.json
frontend/messages/en.json
frontend/messages/es.json
  • #321 (transcription progress indicator) — the coach complements the progress readout.
  • #327 (keyboard shortcuts) — coach should mention ? for the cheatsheet once #327 merges.
  • #310 (AI summary / tag suggestion) — adjacent transcriber-QoL work; not a dependency.
## 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: 1. They must click-and-drag on the image to define a region. 2. What a "region" is (one word? one line? a paragraph?). 3. What the **Read** vs **Edit** toggle does. 4. What "mark for training" does, why it matters, and why it's off-limits when they have nothing to mark. Evidence: `/tmp/fa-audit/G2-transcribe-panel-open.png` from 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 - No changes to transcription engine or region model. - Not a forced modal — coaching must sit inline and be dismissible. - Not a full help center — the Kurrent primer is a one-page reference, not a treatise. ## Proposed design Replace the icon + single sentence with a **three-step coach card** plus a link to a Kurrent primer: ``` ┌────────────────────────────────────────────────────────────┐ │ Erste Transkription? So geht's: │ │ │ │ 1 ▸ Klicken und ziehen Sie auf dem Bild, um einen │ │ Textbereich zu markieren. │ │ 2 ▸ Geben Sie den Text ein, den Sie im markierten Bereich │ │ sehen. → Kurrent-Hilfe │ │ 3 ▸ Speichern. Ihre Transkription verbessert das Kurrent- │ │ Modell, wenn Sie "Für Training vormerken" aktivieren. │ │ │ │ [ Ich kenne das schon — Hinweis ausblenden ] │ └────────────────────────────────────────────────────────────┘ ``` - Hide the "Für Training vormerken" footer when `sections.length === 0` — the user has nothing to mark. - Dismissal is per-user, persistent (localStorage v1; server-side via `AppUser.preferences` JSONB in a follow-up if we want it to cross devices). - After the first region is drawn, the coach card auto-hides (future loads respect the stored dismissal). ### Kurrent primer Static help page at `/help/kurrent` (new route): - One-page reference, no tabs. - 6–10 common Kurrent letterforms with their modern equivalents (`e` / `n` / `m` / `u` / long s / capital H / `ch` ligature). Each shown as inline SVG or PNG with a label. - 2–3 tips for novices ("Long s looks like an f without the crossbar", "The u often has a small u-shaped mark above it for clarity"). - Link back to the Transcribe panel. Source images can be drawn from Wikipedia Kurrent pages (CC-BY-SA) with attribution, OR commissioned as simple inline SVGs. ## Implementation plan ### Frontend - New `frontend/src/lib/components/TranscribeCoachEmptyState.svelte`. Renders when `sections.length === 0 && !dismissed`. Three cards + dismissal button + link to `/help/kurrent`. - Update `frontend/src/lib/components/TranscribePanel.svelte`: - Conditional render of the coach vs. the existing empty state. - Hide the training footer when `sections.length === 0`. - New route `frontend/src/routes/help/kurrent/+page.svelte` — static content component. - Dismissal via `localStorage.getItem('transcribe-coach-dismissed')`. Store set in `onMount` after 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: - Extend `AppUser` with `preferences JSONB NOT NULL DEFAULT '{}'`. - `PATCH /api/users/me/preferences` (self-serve). - Coach reads from server preference first, falls back to localStorage. ## Tests - **Component:** TranscribeCoachEmptyState renders when sections empty; hidden when sections non-empty; hidden when dismissal flag set. - **Component:** TranscribePanel hides training footer when sections empty. - **E2E:** login, open a document with zero regions, assert coach card visible with all three steps; assert training footer NOT visible; click dismiss; reload page; assert coach is gone. - **E2E:** login, open a document with ≥ 1 region, assert coach is NOT visible. - **Visual:** snapshot of the coach card at desktop and tablet viewports. ## Verification Manual walkthrough as a fresh user: 1. Sign in, find an untranscribed document. 2. Open Transcribe panel → coach card visible. 3. Read steps; click Kurrent link → primer opens, back-link works. 4. Draw a region on the image → coach card disappears; training footer now appears beside the new region. 5. Dismiss coach manually on a second untranscribed document; reload → coach stays dismissed. ## Acceptance criteria - [ ] Coach card renders iff `sections.length === 0 && !dismissed` - [ ] Training footer hidden iff `sections.length === 0` - [ ] Dismissal is persistent (localStorage minimum) - [ ] Kurrent primer route exists at `/help/kurrent` with ≥ 6 sample letters - [ ] i18n complete for de/en/es (all new keys present in each) - [ ] No regression on documents that already have regions - [ ] axe-core passes on both the panel empty state and the primer page ## Critical files ``` frontend/src/lib/components/TranscribeCoachEmptyState.svelte (new) frontend/src/lib/components/TranscribePanel.svelte (wire conditional) frontend/src/routes/help/kurrent/+page.svelte (new) frontend/src/routes/help/kurrent/kurrent-letters.svg (new, or PNG set) frontend/messages/de.json frontend/messages/en.json frontend/messages/es.json ``` ## Related - #321 (transcription progress indicator) — the coach complements the progress readout. - #327 (keyboard shortcuts) — coach should mention `?` for the cheatsheet once #327 merges. - #310 (AI summary / tag suggestion) — adjacent transcriber-QoL work; not a dependency.
marcel added this to the (deleted) milestone 2026-04-24 13:23:11 +02:00
marcel added the P1-highfeatureui labels 2026-04-24 13:28:07 +02:00
marcel modified the milestone from (deleted) to (deleted) 2026-04-24 13:35:14 +02:00
Author
Owner

👋 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-motion freezes 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: markiereneinrahmen / "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):

    • Nicht lesbare Wörter → [unleserlich]
    • Durchgestrichene Wörter → [durchgestrichen: Wort]
    • Das lange s (ſ) → als s schreiben
    • Unsichere Namen → [Name?]
    • Dialekt / Fremdwörter / fremde Zitate → wörtlich übernehmen

    "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 print styles baked in (hide chrome, black-on-white body, break-inside: avoid per 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:

    • axe-core runs in light AND dark mode on the empty-state panel (dark mode remaps every token — contrast ratios must be verified independently).
    • axe-core on the non-empty panel state covering the Read/Edit help chip (keyboard-reachable, SR-labeled).
    • Richtlinien page: axe + visual regression at 320px, 768px, 1440px in both themes — this page is likely opened on a phone by family members referencing conventions mid-session.

Removed from scope / simplified

  • On-image overlay hint dropped. A dashed "↓ Hier einen Rahmen ziehen" overlay on the document image would fight the Kurrent strokes for attention and read as noise rather than signal. The side-card animation carries the gesture teaching on its own.
  • Progressive three-step disclosure dropped. The panel's non-empty state has no natural third slot — the sticky review-progress header takes the top, the "draw another" dashed CTA takes the bottom, and each block already is its own affordance (empty textarea labeled "Block N" asks for input without being told). The issue's original design — coach lives only in the empty state — is correct.
  • Dismissal mechanism removed entirely. The coach is the empty state, not an overlay on top of it, so there's nothing to dismiss. Drawing the first frame removes it automatically. No "Ich kenne das schon" button, no localStorage persistence, no recovery path needed, one fewer i18n key (transcribe_coach_dismiss drops 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 — 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-motion` freezes 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):** - Nicht lesbare Wörter → `[unleserlich]` - Durchgestrichene Wörter → `[durchgestrichen: Wort]` - Das lange s (ſ) → als *s* schreiben - Unsichere Namen → `[Name?]` - Dialekt / Fremdwörter / fremde Zitate → wörtlich übernehmen **"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 print` styles** baked in (hide chrome, black-on-white body, `break-inside: avoid` per 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:** - axe-core runs in **light AND dark mode** on the empty-state panel (dark mode remaps every token — contrast ratios must be verified independently). - axe-core on the **non-empty panel state** covering the Read/Edit help chip (keyboard-reachable, SR-labeled). - Richtlinien page: axe + visual regression at **320px, 768px, 1440px** in **both themes** — this page is likely opened on a phone by family members referencing conventions mid-session. ### Removed from scope / simplified - **On-image overlay hint dropped.** A dashed "↓ Hier einen Rahmen ziehen" overlay on the document image would fight the Kurrent strokes for attention and read as noise rather than signal. The side-card animation carries the gesture teaching on its own. - **Progressive three-step disclosure dropped.** The panel's non-empty state has no natural third slot — the sticky review-progress header takes the top, the "draw another" dashed CTA takes the bottom, and each block already *is* its own affordance (empty textarea labeled "Block N" asks for input without being told). The issue's original design — coach lives only in the empty state — is correct. - **Dismissal mechanism removed entirely.** The coach *is* the empty state, not an overlay on top of it, so there's nothing to dismiss. Drawing the first frame removes it automatically. No "Ich kenne das schon" button, no localStorage persistence, no recovery path needed, one fewer i18n key (`transcribe_coach_dismiss` drops 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.
Author
Owner

👋 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

  1. User testing surfaced the auto-OCR expectation. People from Google-Lens / Adobe-Scan land on the panel and assume drawing a rectangle auto-fills the text. When it doesn't, they think the app is broken. The coach needs to set the expectation before step 1: no auto-OCR yet, manual typing is what teaches the recognizer.
  2. "Modell" is ML jargon that doesn't land with a 60+ audience (reads as fashion model or scale model). Step 3 originally tried to explain the training opt-in with phrases like "bringen Sie den Block dem Modell bei" — confusing.

Final structure — text-hero, animation as accent

  • Title + preamble sit at the top (hero text, user reads at their own pace — no forced animation pacing).
  • Three numbered steps as prose. User scans freely, jumps to whichever step they need.
  • Drawing animation (5-s loop) lives inside step 1 only — the "pop" reinforces the one gesture prose genuinely can't convey. Typing + auto-save are NOT animated; the block UI and existing save indicator carry that job once the user is in flow.
  • Footer links: Kurrent help (Wikipedia) and Transkriptions-Richtlinien.

Final copy

Title:

Erste Transkription?

Preamble (expectation-setter):

Unser Kurrent-Erkenner lernt noch. Jede Transkription, die Sie zum Training freigeben, bringt ihm die Schrift bei — so funktioniert's:

Steps:

  1. Rahmen ziehen. Klicken und ziehen Sie mit der Maus einen Rahmen um den Text, den Sie transkribieren möchten.
    + 5-s drawing animation (SVG + SMIL, with prefers-reduced-motion fallback)
  2. Text eingeben. Geben Sie den Text, den Sie im Rahmen sehen, in das neue Textfeld ein.
  3. Speichert automatisch.

Footer:

Hilfe zu Kurrent ↗ · Transkriptions-Richtlinien ↗

Why this copy works

  • "zum Training freigeben" in the preamble is everyday German (freigeben = release/enable) — no ML jargon, and it previews the "Für Training vormerken" chip that sits visible in the panel footer below the coach. User connects the dots unprompted.
  • "Kurrent-Erkenner" is a descriptive compound noun ("a thing that recognizes Kurrent") that any German speaker can parse without technical context.
  • Step 3 is three words. Auto-save is a non-action; giving it more copy than it deserves falsely elevates its importance.
  • No training-opt-in explanation in step 3 — the preamble already carries it, and the actual Für Training vormerken button in the panel footer is self-explanatory given the preamble's framing.

What this changes from the earlier comment

  • Preamble is new scope. Adds ~1 i18n key (transcribe_coach_preamble).
  • Step 3 copy shortens from two sentences to three words. Drops a translation key.
  • Full-flow animation (typing + save indicator) dropped. Only the 5-s drawing loop ships. Animations force pacing; text-scanners felt punished by the 10-s full-flow loop. Animation earns its keep only for the gesture, which prose genuinely can't replace.
  • No Modell anywhere in user-facing copy.

Unchanged from the earlier comment

  • (?) help chip next to the Read/Edit toggle
  • Markieren → einrahmen copy pass across the panel
  • /hilfe/transkription Transkriptions-Richtlinien page with @media print styles
  • Link to Richtlinien opens in new tab with visible "Öffnet in neuem Tab" annotation
  • Expanded test plan (axe in dark mode, non-empty panel, Richtlinien at 320/768/1440 in both themes)
  • Coach lives and dies with the empty state — no dismiss button
  • On-image overlay dropped
  • Progressive disclosure dropped

i18n 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.

## 👋 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 1. **User testing surfaced the auto-OCR expectation.** People from Google-Lens / Adobe-Scan land on the panel and assume drawing a rectangle auto-fills the text. When it doesn't, they think the app is broken. The coach needs to **set the expectation before step 1**: no auto-OCR yet, manual typing is what teaches the recognizer. 2. **"Modell" is ML jargon** that doesn't land with a 60+ audience (reads as fashion model or scale model). Step 3 originally tried to explain the training opt-in with phrases like *"bringen Sie den Block dem Modell bei"* — confusing. ### Final structure — text-hero, animation as accent - **Title + preamble** sit at the top (hero text, user reads at their own pace — no forced animation pacing). - **Three numbered steps as prose.** User scans freely, jumps to whichever step they need. - **Drawing animation (5-s loop) lives inside step 1 only** — the "pop" reinforces the one gesture prose genuinely can't convey. Typing + auto-save are NOT animated; the block UI and existing save indicator carry that job once the user is in flow. - **Footer links:** Kurrent help (Wikipedia) and Transkriptions-Richtlinien. ### Final copy **Title:** > Erste Transkription? **Preamble (expectation-setter):** > Unser Kurrent-Erkenner lernt noch. Jede Transkription, die Sie zum Training freigeben, bringt ihm die Schrift bei — so funktioniert's: **Steps:** 1. **Rahmen ziehen.** Klicken und ziehen Sie mit der Maus einen Rahmen um den Text, den Sie transkribieren möchten. — _+ 5-s drawing animation (SVG + SMIL, with `prefers-reduced-motion` fallback)_ 2. **Text eingeben.** Geben Sie den Text, den Sie im Rahmen sehen, in das neue Textfeld ein. 3. **Speichert automatisch.** **Footer:** > Hilfe zu Kurrent ↗ · Transkriptions-Richtlinien ↗ ### Why this copy works - *"zum Training freigeben"* in the preamble is everyday German (freigeben = release/enable) — no ML jargon, and it previews the *"Für Training vormerken"* chip that sits visible in the panel footer below the coach. User connects the dots unprompted. - *"Kurrent-Erkenner"* is a descriptive compound noun ("a thing that recognizes Kurrent") that any German speaker can parse without technical context. - Step 3 is three words. Auto-save is a non-action; giving it more copy than it deserves falsely elevates its importance. - No training-opt-in explanation in step 3 — the preamble already carries it, and the actual *Für Training vormerken* button in the panel footer is self-explanatory given the preamble's framing. ### What this changes from the earlier comment - **Preamble is new scope.** Adds ~1 i18n key (`transcribe_coach_preamble`). - **Step 3 copy shortens** from two sentences to three words. Drops a translation key. - **Full-flow animation (typing + save indicator) dropped.** Only the 5-s drawing loop ships. Animations force pacing; text-scanners felt punished by the 10-s full-flow loop. Animation earns its keep only for the gesture, which prose genuinely can't replace. - **No `Modell` anywhere in user-facing copy.** ### Unchanged from the earlier comment - (?) help chip next to the Read/Edit toggle - *Markieren → einrahmen* copy pass across the panel - `/hilfe/transkription` Transkriptions-Richtlinien page with `@media print` styles - Link to Richtlinien opens in new tab with visible "Öffnet in neuem Tab" annotation - Expanded test plan (axe in dark mode, non-empty panel, Richtlinien at 320/768/1440 in both themes) - Coach lives and dies with the empty state — no dismiss button - On-image overlay dropped - Progressive disclosure dropped ### i18n 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.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • Visual regions map cleanly: TranscribeCoachEmptyState.svelte (new), HelpPopover.svelte (new, reused for Read/Edit help chip), /hilfe/transkription/+page.svelte (new). Each is nameable in one word.
  • Empty state currently lives at TranscriptionEditView.svelte:233-252 — the coach replaces that block. Same else branch, same hasBlocks guard.
  • prefers-reduced-motion pattern already exists in 4 places (TranscriptionReadView.svelte:52, ChronikFuerDichBox.svelte:144, AnnotationShape.svelte:136, documents/[id]/+page.svelte:49 — the latter uses matchMedia). No invention needed, just follow precedent.
  • Auto-save UI already exists: TranscriptionBlock.svelte:229-239 renders saving / saved / fading / error states. Step 3's "Speichert automatisch." aligns with actual current behavior — zero backend or new UI for that step.
  • No coach_* i18n keys exist yet (grep came up empty) — clean greenfield naming.

Recommendations

  • Extract the SVG animation into its own file, not inlined as ~130 lines of XML in the coach component. Options: lib/assets/transcribe-drag-demo.svg loaded via <img> + aria-label, or a dedicated TranscribeDragDemo.svelte. Inline SMIL in the parent bloats the component past 60 lines and hides the animation from review diffs. My pick: dedicated Svelte component so prefers-reduced-motion can be handled in the same file as the animation.
  • TDD order:
    1. TranscribeCoachEmptyState.svelte.spec.ts — renders preamble + 3 steps + animation region when blocks.length === 0; rendering is suppressed when blocks.length > 0.
    2. 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, has aria-expanded / aria-controls.
    3. /hilfe/transkription/+page.svelte.spec.ts — all five v1 conventions present; Wikipedia link has target="_blank" rel="noopener noreferrer"; print stylesheet rules applied under @media print.
    4. E2E covers the transition: open empty doc → see coach → draw first region → coach unmounts.
  • Use $derived for visibility, never $effect. const showCoach = $derived(blocks.length === 0 && mode === 'edit'); — the mode === 'edit' guard matters (see Markus's note).
  • Richtlinien page as prerendered static: no +page.server.ts, add export const prerender = true; so it's built to static HTML at build time. No SSR cost, no API surface.
  • i18n discipline: the ~9 new coach keys + Richtlinien page keys × 3 languages (de/en/es) = non-trivial translation load. Land every key in de.json / en.json / es.json in the same commit — partial translations leak English fallbacks to de users.
  • Drop the bigger-animation Variant B even from the scratch demo; the final design is the 5-s drawing loop only. Don't let the typing-animation code leak into lib/ by accident from the Marcel-Leonie scratch file at /tmp/transcribe-coach-demo.html.

Open Decisions

None.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - Visual regions map cleanly: `TranscribeCoachEmptyState.svelte` (new), `HelpPopover.svelte` (new, reused for Read/Edit help chip), `/hilfe/transkription/+page.svelte` (new). Each is nameable in one word. - Empty state currently lives at `TranscriptionEditView.svelte:233-252` — the coach replaces that block. Same `else` branch, same `hasBlocks` guard. - `prefers-reduced-motion` pattern already exists in 4 places (`TranscriptionReadView.svelte:52`, `ChronikFuerDichBox.svelte:144`, `AnnotationShape.svelte:136`, `documents/[id]/+page.svelte:49` — the latter uses `matchMedia`). No invention needed, just follow precedent. - Auto-save UI already exists: `TranscriptionBlock.svelte:229-239` renders `saving` / `saved` / `fading` / `error` states. Step 3's "*Speichert automatisch.*" aligns with actual current behavior — zero backend or new UI for that step. - No `coach_*` i18n keys exist yet (`grep` came up empty) — clean greenfield naming. ### Recommendations - **Extract the SVG animation into its own file**, not inlined as ~130 lines of XML in the coach component. Options: `lib/assets/transcribe-drag-demo.svg` loaded via `<img>` + `aria-label`, **or** a dedicated `TranscribeDragDemo.svelte`. Inline SMIL in the parent bloats the component past 60 lines and hides the animation from review diffs. My pick: dedicated Svelte component so `prefers-reduced-motion` can be handled in the same file as the animation. - **TDD order:** 1. `TranscribeCoachEmptyState.svelte.spec.ts` — renders preamble + 3 steps + animation region when `blocks.length === 0`; rendering is suppressed when `blocks.length > 0`. 2. `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, has `aria-expanded` / `aria-controls`. 3. `/hilfe/transkription/+page.svelte.spec.ts` — all five v1 conventions present; Wikipedia link has `target="_blank" rel="noopener noreferrer"`; print stylesheet rules applied under `@media print`. 4. E2E covers the transition: open empty doc → see coach → draw first region → coach unmounts. - **Use `$derived` for visibility, never `$effect`.** `const showCoach = $derived(blocks.length === 0 && mode === 'edit');` — the `mode === 'edit'` guard matters (see Markus's note). - **Richtlinien page as prerendered static:** no `+page.server.ts`, add `export const prerender = true;` so it's built to static HTML at build time. No SSR cost, no API surface. - **i18n discipline:** the ~9 new coach keys + Richtlinien page keys × 3 languages (de/en/es) = non-trivial translation load. Land every key in `de.json` / `en.json` / `es.json` **in the same commit** — partial translations leak English fallbacks to de users. - **Drop the bigger-animation Variant B** even from the scratch demo; the final design is the 5-s drawing loop only. Don't let the typing-animation code leak into `lib/` by accident from the Marcel-Leonie scratch file at `/tmp/transcribe-coach-demo.html`. ### Open Decisions _None._
Author
Owner

🏛️ Markus Keller — Application Architect

Observations

  • Pure frontend content scope. No schema changes, no new services, no cross-domain coupling, no ADR required. Architecture surface is minimal — this is content + components.
  • The coach belongs exclusively to the Edit mode flow. TranscriptionEditView.svelte handles edit; TranscriptionReadView.svelte handles read. Any logic that puts the coach into the Read branch leaks across a module boundary that's already correctly drawn.
  • /hilfe/transkription is a new top-level route. No need to nest it under /admin or /documents — it's a general help resource, its own feature folder is correct.
  • The Read/Edit help chip (item 4 from Leonie) and any future inline help tooltips should share one component — HelpPopover.svelte. Writing a bespoke popover per feature is coupling by copy-paste.

Recommendations

  • Build HelpPopover.svelte as a reusable primitive, not a Read/Edit-specific chip. Props: label, content (slot), placement. First consumer is the Read/Edit toggle in TranscriptionPanelHeader.svelte; second consumer is likely the Transkriptions-Richtlinien page for per-convention "Warum?" explanations. One component, two initial uses, room to grow. Place it in lib/components/.
  • Keep TranscribeCoachEmptyState.svelte scoped 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 the mode prop and decides whether the edit-mode empty branch renders at all.
  • Richtlinien page is prerendered static content. No SvelteKit load function, 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.
  • Do not create a TranscribeCoachManager.svelte or TranscribeCoachService.ts. If the component has no state beyond the blocks.length === 0 check 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.

## 🏛️ Markus Keller — Application Architect ### Observations - Pure frontend content scope. No schema changes, no new services, no cross-domain coupling, no ADR required. Architecture surface is minimal — this is content + components. - The coach belongs exclusively to the Edit mode flow. `TranscriptionEditView.svelte` handles edit; `TranscriptionReadView.svelte` handles read. Any logic that puts the coach into the Read branch leaks across a module boundary that's already correctly drawn. - `/hilfe/transkription` is a new top-level route. No need to nest it under `/admin` or `/documents` — it's a general help resource, its own feature folder is correct. - The Read/Edit help chip (item 4 from Leonie) and any future inline help tooltips should share one component — `HelpPopover.svelte`. Writing a bespoke popover per feature is coupling by copy-paste. ### Recommendations - **Build `HelpPopover.svelte` as a reusable primitive**, not a Read/Edit-specific chip. Props: `label`, `content` (slot), `placement`. First consumer is the Read/Edit toggle in `TranscriptionPanelHeader.svelte`; second consumer is likely the Transkriptions-Richtlinien page for per-convention "Warum?" explanations. One component, two initial uses, room to grow. Place it in `lib/components/`. - **Keep `TranscribeCoachEmptyState.svelte` scoped 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 the `mode` prop and decides whether the edit-mode empty branch renders at all. - **Richtlinien page is prerendered static content.** No SvelteKit `load` function, 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. - **Do not create a `TranscribeCoachManager.svelte` or `TranscribeCoachService.ts`.** If the component has no state beyond the `blocks.length === 0` check 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.
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform

Observations

  • Zero infrastructure changes. No new service, no new port, no new env var, no Compose changes, no Caddy changes. Pure frontend content.
  • Paraglide compile is already wired into the Vite plugin per frontend/CLAUDE.md — the 9 new keys × 3 languages flow through the existing pipeline automatically.
  • Inline SVG + SMIL at ~5 KB adds negligible bundle weight to the Transcribe panel route. No optimization concern.
  • The Richtlinien page as a static route is cheap to serve — it costs nothing on the VPS, prerendered or not.

Recommendations

  • Prerender the Richtlinien page (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.
  • Ensure visual-regression snapshots aren't stored as binary noise in 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. Consider lfs only if the set grows past ~50 images.
  • No other notes. This ships without me touching anything.

Open Decisions

None.

## 🛠️ Tobias Wendt — DevOps & Platform ### Observations - Zero infrastructure changes. No new service, no new port, no new env var, no Compose changes, no Caddy changes. Pure frontend content. - Paraglide compile is already wired into the Vite plugin per `frontend/CLAUDE.md` — the 9 new keys × 3 languages flow through the existing pipeline automatically. - Inline SVG + SMIL at ~5 KB adds negligible bundle weight to the Transcribe panel route. No optimization concern. - The Richtlinien page as a static route is cheap to serve — it costs nothing on the VPS, prerendered or not. ### Recommendations - **Prerender the Richtlinien page** (`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. - **Ensure visual-regression snapshots aren't stored as binary noise in `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. Consider `lfs` only if the set grows past ~50 images. - **No other notes.** This ships without me touching anything. ### Open Decisions _None._
Author
Owner

🔐 Nora "NullX" Steiner — Application Security

Observations

  • No auth surface change. No new backend endpoint, no schema, no token flow.
  • External link pattern target="_blank" rel="noopener noreferrer" is already established in PdfViewer.svelte:169-170. Reuse it for the Wikipedia and Richtlinien links — not new territory.
  • The coach + Richtlinien content is fully i18n static strings and inline SVG. No {@html}, no user-controlled content rendered into the DOM. XSS surface is zero.
  • Familienarchiv is auth-required by default (see SecurityConfig); adding /hilfe/transkription with no explicit permitAll() inherits that default. Architecture decision needed — see Open Decisions.

Recommendations

  • Wikipedia external link: target="_blank" rel="noopener noreferrer" is correct. Additionally add referrerpolicy="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.
  • Richtlinien page "Öffnet in neuem Tab" annotation: good for screen readers. Confirm the annotation is a real DOM text node (not CSS ::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.
  • SMIL animation and SVG safety: the SVG is static content baked at build time (not user-generated). No CSP concerns with <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 need unsafe-inline.
  • No new permissions, no @RequirePermission changes, no backend work. Security footprint of this issue is basically "don't regress the external-link pattern."

Open Decisions

  • Auth boundary on /hilfe/transkription — see the consolidated Decision Queue.
## 🔐 Nora "NullX" Steiner — Application Security ### Observations - No auth surface change. No new backend endpoint, no schema, no token flow. - External link pattern `target="_blank" rel="noopener noreferrer"` is already established in `PdfViewer.svelte:169-170`. Reuse it for the Wikipedia and Richtlinien links — not new territory. - The coach + Richtlinien content is fully i18n static strings and inline SVG. No `{@html}`, no user-controlled content rendered into the DOM. XSS surface is zero. - Familienarchiv is auth-required by default (see `SecurityConfig`); adding `/hilfe/transkription` with no explicit `permitAll()` inherits that default. **Architecture decision needed** — see Open Decisions. ### Recommendations - **Wikipedia external link:** `target="_blank" rel="noopener noreferrer"` is correct. Additionally add `referrerpolicy="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. - **Richtlinien page "Öffnet in neuem Tab" annotation:** good for screen readers. Confirm the annotation is a real DOM text node (not CSS `::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. - **SMIL animation and SVG safety:** the SVG is static content baked at build time (not user-generated). No CSP concerns with `<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 need `unsafe-inline`. - **No new permissions, no `@RequirePermission` changes, no backend work.** Security footprint of this issue is basically "don't regress the external-link pattern." ### Open Decisions - **Auth boundary on `/hilfe/transkription`** — see the consolidated Decision Queue.
Author
Owner

🧪 Sara Holt — QA & Test Strategist

Observations

  • @axe-core/playwright is already integrated — three existing specs use it (header.spec.ts, korrespondenz.spec.ts, notification-deep-link.spec.ts). korrespondenz.spec.ts:4-5 already has a buildAxe(page) helper with withTags(['wcag2a', 'wcag2aa']) — reuse it.
  • Dark mode IS toggleable in the app: ThemeToggle.svelte:25 sets data-theme on documentElement; +layout.svelte:89 mounts it. The dark-mode axe test has real infrastructure to drive — no new harness needed.
  • Auto-save behavior exists and is already testable (TranscriptionBlock.svelte.spec.ts exists). Step 3's "Speichert automatisch." aligns with existing save states; no new auto-save assertions.
  • No existing fixtures for "document with zero transcription blocks" that I can find in frontend/e2e/fixtures — E2E will need to create one or use a freshly-uploaded document in setup.

Recommendations

  • Disable animations during visual snapshots to kill snapshot flake from the SMIL loop. Set prefers-reduced-motion: reduce per test:
    await page.emulateMedia({ reducedMotion: 'reduce' });
    
    With the prefers-reduced-motion handler Felix adds to the animation component, snapshots capture the frozen final frame deterministically.
  • Theme-switching in axe tests: click the real ThemeToggle button, don't directly mutate data-theme. Two benefits: (a) smoke-tests the toggle itself, (b) one less thing to stub. Pattern:
    for (const theme of ['light', 'dark']) {
      test(`transcribe empty-state a11y (${theme})`, async ({ page }) => {
        if (theme === 'dark') await page.getByRole('button', { name: /theme/i }).click();
        await page.goto(`/documents/${emptyDocId}`);
        const results = await buildAxe(page).analyze();
        expect(results.violations).toEqual([]);
      });
    }
    
  • Test pyramid distribution:
    • Unit (Vitest + vitest-browser-svelte): coach renders/unrenders based on blocks; help chip keyboard interactions (Enter/Space open, Esc close, focus return); Richtlinien page renders all v1 convention sections.
    • Integration: TranscriptionEditView.spec.ts already exists — add cases for empty→non-empty coach transition.
    • E2E: one journey — login → open empty doc → see coach → draw first frame → coach gone → type → auto-save confirms. Plus dark-mode axe at the empty-state moment. Plus Richtlinien page visit at 320/768/1440 × 2 themes. Don't explode E2E to cover all permutations — the 8-minute budget matters.
  • Seed fixture for "empty Transcribe panel": add a helper in frontend/e2e/helpers that uploads a document and returns its ID with zero blocks. Tests call it in beforeEach. Keeps E2E independent of ordering.
  • Flakiness pre-mortem: the SMIL animation has ~5 keyframes running continuously. Any Playwright assertion that times too close to a keyframe transition without reduced-motion applied will flake. Make reducedMotion: 'reduce' the project-wide default in playwright.config.ts for non-animation-specific tests.
  • Richtlinien print styles: testable via page.emulateMedia({ media: 'print' }) + screenshot compare. One test, catches future regressions of the @media print block.

Open Decisions

None. All of this is within standard test-strategy calls I'm comfortable making.

## 🧪 Sara Holt — QA & Test Strategist ### Observations - `@axe-core/playwright` is already integrated — three existing specs use it (`header.spec.ts`, `korrespondenz.spec.ts`, `notification-deep-link.spec.ts`). `korrespondenz.spec.ts:4-5` already has a `buildAxe(page)` helper with `withTags(['wcag2a', 'wcag2aa'])` — reuse it. - Dark mode IS toggleable in the app: `ThemeToggle.svelte:25` sets `data-theme` on `documentElement`; `+layout.svelte:89` mounts it. The dark-mode axe test has real infrastructure to drive — no new harness needed. - Auto-save behavior exists and is already testable (`TranscriptionBlock.svelte.spec.ts` exists). Step 3's "Speichert automatisch." aligns with existing save states; no new auto-save assertions. - No existing fixtures for "document with zero transcription blocks" that I can find in `frontend/e2e/fixtures` — E2E will need to create one or use a freshly-uploaded document in setup. ### Recommendations - **Disable animations during visual snapshots** to kill snapshot flake from the SMIL loop. Set `prefers-reduced-motion: reduce` per test: ```typescript await page.emulateMedia({ reducedMotion: 'reduce' }); ``` With the `prefers-reduced-motion` handler Felix adds to the animation component, snapshots capture the frozen final frame deterministically. - **Theme-switching in axe tests: click the real `ThemeToggle` button**, don't directly mutate `data-theme`. Two benefits: (a) smoke-tests the toggle itself, (b) one less thing to stub. Pattern: ```typescript for (const theme of ['light', 'dark']) { test(`transcribe empty-state a11y (${theme})`, async ({ page }) => { if (theme === 'dark') await page.getByRole('button', { name: /theme/i }).click(); await page.goto(`/documents/${emptyDocId}`); const results = await buildAxe(page).analyze(); expect(results.violations).toEqual([]); }); } ``` - **Test pyramid distribution:** - **Unit** (Vitest + `vitest-browser-svelte`): coach renders/unrenders based on `blocks`; help chip keyboard interactions (Enter/Space open, Esc close, focus return); Richtlinien page renders all v1 convention sections. - **Integration**: `TranscriptionEditView.spec.ts` already exists — add cases for empty→non-empty coach transition. - **E2E**: *one* journey — login → open empty doc → see coach → draw first frame → coach gone → type → auto-save confirms. Plus dark-mode axe at the empty-state moment. Plus Richtlinien page visit at 320/768/1440 × 2 themes. **Don't explode E2E to cover all permutations** — the 8-minute budget matters. - **Seed fixture for "empty Transcribe panel":** add a helper in `frontend/e2e/helpers` that uploads a document and returns its ID with zero blocks. Tests call it in `beforeEach`. Keeps E2E independent of ordering. - **Flakiness pre-mortem:** the SMIL animation has ~5 keyframes running continuously. Any Playwright assertion that times too close to a keyframe transition without reduced-motion applied will flake. Make `reducedMotion: 'reduce'` the project-wide default in `playwright.config.ts` for non-animation-specific tests. - **Richtlinien print styles:** testable via `page.emulateMedia({ media: 'print' })` + screenshot compare. One test, catches future regressions of the `@media print` block. ### Open Decisions _None._ All of this is within standard test-strategy calls I'm comfortable making.
Author
Owner

👋 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.

## 👋 Leonie Voss — UI/UX Design Lead No fresh concerns from my angle. I've already posted two comments on this issue — [#issuecomment-4330](http://heim-nas:3005/marcel/familienarchiv/issues/320#issuecomment-4330) (full scope review) and [#issuecomment-4331](http://heim-nas:3005/marcel/familienarchiv/issues/320#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._
Author
Owner

📋 Elicit — Requirements Engineer

Observations

  • The acceptance criteria in the issue body are phrased in implementation terms (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.
  • The driving JTBD is clear but not explicit in the issue: "When I open a document to transcribe for the first time, I want to know what to do without feeling lost, so that I don't give up before contributing my first transcription." Primary persona: 60+ Kurrent-literate family volunteer on laptop/tablet.
  • NFR checklist is partially covered (accessibility via axe) but other NFRs (i18n parity, responsive coverage of the Richtlinien page, bundle-size impact) are implicit rather than stated.
  • One tacit assumption worth surfacing: the preamble "zum Training freigeben" is being treated as a validated fix for the auto-OCR expectation, but the only user-testing data point so far is the one that flagged the problem. The fix has not been tested.

Recommendations

  • Rewrite acceptance criteria in Given-When-Then so every one is verifiable from outside the code:
Given an authenticated user in Edit mode on a document with zero transcription blocks,
when the Transcribe panel renders,
then they see the title, preamble, three numbered steps, and the drawing animation.

Given an authenticated user in Edit mode on a document with one or more transcription blocks,
when the Transcribe panel renders,
then the coach is not present.

Given the user opens the Transcribe panel in Read mode,
when the panel renders regardless of block count,
then the coach is not present.

Given a user with prefers-reduced-motion enabled,
when the coach renders,
then the drawing animation is shown as a static final-frame illustration.

Given dark mode is active,
when axe-core analyzes the panel empty state,
then no WCAG 2.1 AA violations are reported.

Given the user clicks the "Transkriptions-Richtlinien" link,
when the link is activated,
then the Richtlinien page opens in a new browser tab with the five v1 conventions visible.

Given the user prints the Richtlinien page via the browser print dialog,
when the preview renders,
then app chrome is hidden and each convention section does not break across pages.
  • Make NFRs explicit in the acceptance-criteria section:
    • Internationalization: every new Paraglide key must exist in de.json, en.json, and es.json before merge.
    • Accessibility: WCAG 2.1 AA in both themes; keyboard-reachable help chip with Esc-close and focus return.
    • Performance: Transcribe panel bundle adds ≤ 10 KB gzipped.
    • Responsive: Richtlinien page verified at 320/768/1440 in both themes; Transcribe panel coach not tested below 768 (transcribers are on laptop/tablet per project memory).
    • Observability: no new analytics or logging required.
  • Traceability matrix: Goal (onboard first-time transcribers) → Persona (60+ Kurrent volunteer) → JTBD (statement above) → User stories (coach empty state; Read/Edit help chip; Richtlinien page; copy audit) → Acceptance criteria above.
  • Flag for post-merge user test: "Does the preamble mitigate the auto-OCR expectation?" — Plan a 3-person test with fresh family volunteers within two weeks of ship. Success signal: no tester asks "where is the text going to appear?" after reading the preamble but before drawing their first frame. If even one tester still confuses the flow, the preamble copy needs another round.
  • Add a Definition of Done checkbox: "Validated in production with at least one 60+ family member session within 14 days of ship."

Open Decisions

None. The open question about post-merge user testing is a plan, not a blocker.

## 📋 Elicit — Requirements Engineer ### Observations - The acceptance criteria in the issue body are phrased in implementation terms (`Coach card renders iff sections.length === 0 && !dismissed`). With the dismissal mechanism removed and the final copy locked per [#issuecomment-4331](http://heim-nas:3005/marcel/familienarchiv/issues/320#issuecomment-4331), those criteria need a rewrite before implementation so testers can verify against them. - The driving JTBD is clear but not explicit in the issue: *"When I open a document to transcribe for the first time, I want to know what to do without feeling lost, so that I don't give up before contributing my first transcription."* Primary persona: 60+ Kurrent-literate family volunteer on laptop/tablet. - NFR checklist is partially covered (accessibility via axe) but other NFRs (i18n parity, responsive coverage of the Richtlinien page, bundle-size impact) are implicit rather than stated. - One tacit assumption worth surfacing: the preamble *"zum Training freigeben"* is being treated as a validated fix for the auto-OCR expectation, but the only user-testing data point so far is the one that flagged the problem. The *fix* has not been tested. ### Recommendations - **Rewrite acceptance criteria in Given-When-Then** so every one is verifiable from outside the code: ``` Given an authenticated user in Edit mode on a document with zero transcription blocks, when the Transcribe panel renders, then they see the title, preamble, three numbered steps, and the drawing animation. Given an authenticated user in Edit mode on a document with one or more transcription blocks, when the Transcribe panel renders, then the coach is not present. Given the user opens the Transcribe panel in Read mode, when the panel renders regardless of block count, then the coach is not present. Given a user with prefers-reduced-motion enabled, when the coach renders, then the drawing animation is shown as a static final-frame illustration. Given dark mode is active, when axe-core analyzes the panel empty state, then no WCAG 2.1 AA violations are reported. Given the user clicks the "Transkriptions-Richtlinien" link, when the link is activated, then the Richtlinien page opens in a new browser tab with the five v1 conventions visible. Given the user prints the Richtlinien page via the browser print dialog, when the preview renders, then app chrome is hidden and each convention section does not break across pages. ``` - **Make NFRs explicit in the acceptance-criteria section:** - *Internationalization:* every new Paraglide key must exist in `de.json`, `en.json`, and `es.json` before merge. - *Accessibility:* WCAG 2.1 AA in both themes; keyboard-reachable help chip with Esc-close and focus return. - *Performance:* Transcribe panel bundle adds ≤ 10 KB gzipped. - *Responsive:* Richtlinien page verified at 320/768/1440 in both themes; Transcribe panel coach not tested below 768 (transcribers are on laptop/tablet per project memory). - *Observability:* no new analytics or logging required. - **Traceability matrix:** Goal (onboard first-time transcribers) → Persona (60+ Kurrent volunteer) → JTBD (statement above) → User stories (coach empty state; Read/Edit help chip; Richtlinien page; copy audit) → Acceptance criteria above. - **Flag for post-merge user test:** *"Does the preamble mitigate the auto-OCR expectation?"* — Plan a 3-person test with fresh family volunteers within two weeks of ship. Success signal: no tester asks *"where is the text going to appear?"* after reading the preamble but before drawing their first frame. If even one tester still confuses the flow, the preamble copy needs another round. - **Add a Definition of Done checkbox:** *"Validated in production with at least one 60+ family member session within 14 days of ship."* ### Open Decisions _None._ The open question about post-merge user testing is a plan, not a blocker.
Author
Owner

🗳️ Decision Queue — Action Required

1 decision needs your input before implementation starts.

Security / Access Control

  • Auth boundary on /hilfe/transkription — should the Transkriptions-Richtlinien page require login, or be publicly accessible?
    • (a) Auth-required (inherits the app's default anyRequest().authenticated()): consistent with every other route; audience is family members only anyway; zero implementation cost (default behavior).
    • (b) Public (explicit 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 in SecurityConfig and a conscious decision that this path is intentionally public.
    • Raised by: Nora.
## 🗳️ Decision Queue — Action Required _1 decision needs your input before implementation starts._ ### Security / Access Control - **Auth boundary on `/hilfe/transkription`** — should the Transkriptions-Richtlinien page require login, or be publicly accessible? - **(a) Auth-required** (inherits the app's default `anyRequest().authenticated()`): consistent with every other route; audience is family members only anyway; zero implementation cost (default behavior). - **(b) Public** (explicit `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 in `SecurityConfig` and a conscious decision that this path is intentionally public. - _Raised by: Nora._
Author
Owner

📄 Spec committed — /hilfe/transkription

Final UI/UX spec for the Transkriptions-Richtlinien page is now on main:

docs/specs/transkriptions-richtlinien-spec.html (commit 1d5219ea)

What it covers

  • Masthead + layout-overview diagram (5 sections, top to bottom)
  • Rendered mockups at desktop light, desktop dark, tablet light, mobile light, print
  • Annotation callouts for the six key regions (header tone, Wikipedia info-card, 3-col grid, Beispiel boxes, "Noch in Klärung" strip, closing invitation)
  • Impl-ref tables with Tailwind classes + exact px values for every element: page shell, header/intro, Wikipedia info-card, section labels, rule card grid, Beispiel box, per-rule Beispiel visuals, "Noch in Klärung" strip, closing invitation, @media print styles
  • Paraglide keys — 23 new keys in de/en/es with full translations drafted
  • Interaction + behaviour notes (static, prerender, external-link behaviour, growth model)
  • Edge cases + a11y (WCAG 2.1 AA contrast numbers both themes, heading structure, decorative SVG markup, colour-alone compliance for chips, long-word handling at 320px)
  • Component tree (new Svelte components + reuse candidates)
  • Gherkin acceptance criteria for the page
  • Out-of-scope list (anchor nav, interactive chips, per-rule "Warum?" expandables, Kurrent webfont, search)

Open the file locally or via the Gitea link above to see the full rendering at multiple viewports and themes.

## 📄 Spec committed — `/hilfe/transkription` Final UI/UX spec for the Transkriptions-Richtlinien page is now on `main`: **→ [`docs/specs/transkriptions-richtlinien-spec.html`](http://heim-nas:3005/marcel/familienarchiv/src/branch/main/docs/specs/transkriptions-richtlinien-spec.html)** (commit [`1d5219ea`](http://heim-nas:3005/marcel/familienarchiv/commit/1d5219ea)) ### What it covers - **Masthead** + layout-overview diagram (5 sections, top to bottom) - **Rendered mockups** at desktop light, desktop dark, tablet light, mobile light, print - **Annotation callouts** for the six key regions (header tone, Wikipedia info-card, 3-col grid, Beispiel boxes, "Noch in Klärung" strip, closing invitation) - **Impl-ref tables** with Tailwind classes + exact px values for every element: page shell, header/intro, Wikipedia info-card, section labels, rule card grid, Beispiel box, per-rule Beispiel visuals, "Noch in Klärung" strip, closing invitation, `@media print` styles - **Paraglide keys** — 23 new keys in de/en/es with full translations drafted - **Interaction + behaviour** notes (static, prerender, external-link behaviour, growth model) - **Edge cases + a11y** (WCAG 2.1 AA contrast numbers both themes, heading structure, decorative SVG markup, colour-alone compliance for chips, long-word handling at 320px) - **Component tree** (new Svelte components + reuse candidates) - **Gherkin acceptance criteria** for the page - **Out-of-scope list** (anchor nav, interactive chips, per-rule "Warum?" expandables, Kurrent webfont, search) Open the file locally or via the Gitea link above to see the full rendering at multiple viewports and themes.
Sign in to join this conversation.
No Label P1-high feature ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#320