A focused reading experience for completed transcriptions. Uses the clean split layout: PDF scan on the left, flowing prose on the right. All editing chrome is stripped — no block borders, no comment threads, no toolbars. The text reads like a letter, not like an editing interface.
Transcribe mode is for editing. But most visits to a completed transcription are for reading — comparing the handwriting with the typed text, sharing with family, or just revisiting a letter. Read mode strips away all editing chrome and presents the transcription as flowing prose alongside the original scan.
The clean split was chosen over the full-page reader (PDF hidden) and the interleaved view (cropped PDF per block) because it preserves the familiar side-by-side layout from transcribe mode while dramatically reducing visual noise. Users can switch between reading and editing without re-learning the spatial layout.
/* Same side-by-side layout as transcribe mode, but the right panel renders * the transcription as continuous flowing prose instead of block cards. * * Key differences from transcribe mode: * - No block borders, headers, footers, or numbered badges * - No contenteditable — text is plain rendered HTML * - No comment threads, no "Kommentieren" buttons * - No presence dots, no hint strip, no auto-save indicator * - Annotation rects on PDF are dimmed (opacity ~0.3, no badges) * - Still clickable for scroll-sync * - Status bar shows: "4 Abschnitte · Zuletzt bearbeitet: Oma Inge, 14:23" * * Scroll sync: * - Click paragraph → matching PDF annotation flashes turquoise (1.5s fade) * - Click PDF annotation → matching paragraph gets subtle bg highlight (1.5s fade) * - PDF auto-scrolls to center the annotation in the viewport */
| Element | Value | Notes |
|---|---|---|
| Text panel | ||
| Font | Tinos (serif), 16px, line-height 1.85 | Generous reading typography |
| Padding | 24px 32px | Comfortable margins like a book page |
| Paragraphs | One <p> per transcription block | mb-4 between paragraphs |
| [unleserlich] | italic, text-ink-2, font-size: 0.9em | Subtle but readable |
| Hover | Subtle turquoise bg at 6% opacity | Hint that paragraphs are clickable |
| PDF panel | ||
| Annotations | Dimmed: border-opacity 0.3, bg-opacity 0.04 | Still clickable for scroll-sync |
| Badges | Hidden | No numbered circles in read mode |
| Scroll sync | Click para → PDF scrolls, flash 1.5s | Turquoise tint at 18% → fade to 4% |
| Status bar | ||
| Content | "N Abschnitte · Zuletzt bearbeitet: Name, HH:mm" | Uses most recent updated_at across blocks |
| Height | 28px, sand background | Same as transcribe mode status bar |
/* Bidirectional scroll sync with visual feedback. * * Text → PDF: * 1. User clicks a paragraph * 2. Paragraph gets .highlighted class (turquoise bg at 10%) * 3. Matching annotation rect gets .highlight-flash class * 4. PDF viewport scrolls to center the annotation * 5. Both highlights fade over 1.5s via CSS animation * * PDF → Text: * 1. User clicks a dimmed annotation rect * 2. Annotation flashes (same .highlight-flash) * 3. Matching paragraph gets .highlighted * 4. Text panel scrolls to center the paragraph * 5. Both fade over 1.5s * * Implementation: each paragraph has data-block-id matching the * transcription block's annotation_id. The annotation rects already * have annotation IDs from transcribe mode. */
| Element | Value | Notes |
|---|---|---|
| Highlight animation | ||
| Paragraph bg | rgba(0,199,177,.10) | Turquoise at 10%, fades to 0 |
| Annotation flash | rgba(0,199,177,.18) → .04 | Border returns to .3 opacity |
| Duration | 1.5s ease-out | CSS animation, no JS timers needed |
| Scroll behavior | smooth, block: center | scrollIntoView({ behavior: 'smooth', block: 'center' }) |
| Data binding | ||
| Paragraph attr | data-block-id="{annotation_id}" | Links text to PDF annotation |
| Annotation attr | data-annotation-id="{id}" | Already exists from transcribe mode |
/* When transcription_blocks count is 0: * - Mode switcher defaults to "Bearbeiten" * - "Lesen" tab is disabled: opacity 0.35, cursor: not-allowed, not clickable * - Right panel shows empty state with pencil icon, title, and description * - No status bar (nothing to show) * * As soon as the first block is saved, "Lesen" becomes enabled. * The mode does NOT auto-switch — user stays in Bearbeiten. */
| Element | Value | Notes |
|---|---|---|
| Empty state | ||
| Icon | Pencil in 48px sand circle | Centered vertically in panel |
| Title | "Noch keine Transkription" | i18n key: transcription_empty_title |
| Description | "Zeichne Bereiche auf dem Scan…" | i18n key: transcription_empty_desc |
| Mode switcher | ||
| Lesen tab | Disabled: opacity .35, not-allowed | Enabled when block count > 0 |
| Default | "Bearbeiten" active | Enters transcribe mode directly |
Liebe Martha,
ich schreibe Dir heute aus dem Lazarett in Breslau. Mach Dir keine Sorgen, es geht mir den Umständen entsprechend gut. Der Arzt sagt [unleserlich] Wochen noch dauern wird.
Die Kinder sollen wissen, dass ich an sie denke. Sag dem kleinen Fritz, er soll auf seine Mutter aufpassen. Und Lotte soll weiter so fleißig in der Schule sein.
In ewiger Liebe,
Dein Heinrich
Liebe Martha,
ich schreibe Dir heute aus dem Lazarett in Breslau…
/* On viewports < 768px, the side-by-side split becomes vertical: * - PDF scan strip at top (collapsed: 70px, expanded: ~50vh) * - Flowing text below, full-width * - Tap PDF strip to toggle expand/collapse * - Expand hint text: "▲ Scan vergrößern" / "▼ Scan verkleinern" * * Mode switcher abbreviates: "Lesen | Bearb." * Scroll-sync: tapping a paragraph briefly highlights the matching * region in the expanded PDF. If PDF is collapsed, it auto-expands * first, then scrolls to the annotation. * * Same status bar at the bottom, same flowing prose styling. */
| Element | Value | Notes |
|---|---|---|
| PDF strip | ||
| Collapsed height | 70px | Shows miniature scan preview |
| Expanded height | ~50vh or max 300px | Smooth CSS transition (300ms ease) |
| Toggle | Tap anywhere on strip | Hint text in bottom-right corner |
| Text area | ||
| Font | Tinos, 15px, line-height 1.9 | Slightly larger than desktop for touch |
| Padding | 16px | Full-width, no wasted space |
| Mode switcher | ||
| Labels | "Lesen | Bearb." | Abbreviated to fit mobile topbar |
| Font size | 10px | Compact but readable |
| Scroll sync on mobile | ||
| Tap paragraph | Expand PDF if collapsed, then highlight | Auto-expand + scroll + flash |
| Tap annotation | Collapse PDF, scroll text to paragraph | Smart collapse after showing match |
/* Three mutually exclusive modes: * * 1. Lesen (read) — this spec. Flowing prose, dimmed annotations, no editing. * 2. Bearbeiten (edit) — annotation-transcription-final-spec. Block cards, contenteditable. * 3. Annotieren — yellow comment annotations on PDF. Separate button, not in segmented control. * * The segmented control only contains Lesen + Bearbeiten. * Annotieren is a separate button that, when active, deselects both Lesen and Bearbeiten * (both appear deselected/dimmed in the segmented control). * * When the user clicks Annotieren while in read/transcribe mode: * → Enter annotate mode, both segmented items dim * When the user clicks a segmented item while in annotate mode: * → Exit annotate mode, enter the selected mode * * State: let mode: 'read' | 'transcribe' | 'annotate' = $state(...) * Default: 'read' if blocks.length > 0, else 'transcribe' * The "Annotieren" button is hidden if !canAnnotate || !isPdf */
| Element | Value | Notes |
|---|---|---|
| Segmented control | ||
| Items | "Lesen" | "Bearbeiten" | Mobile: "Lesen" | "Bearb." |
| Active style | bg:navy, color:#fff | Rounded within the pill |
| Inactive style | bg:transparent, color:muted | Hover: bg:sand |
| Dimmed style | Both items at opacity .5 | Only when annotate mode is active |
| Disabled (Lesen) | opacity .35, cursor not-allowed | When no transcription blocks exist |
| Annotieren button | ||
| Default | Ghost style (border:muted) | Same as current topbar button |
| Active | bg:navy, color:#fff | Filled state when annotate mode on |
| Visibility | canAnnotate && isPdf | Hidden for non-PDF documents |
| Accessibility | ||
| Segmented | role="tablist", children role="tab" | aria-selected on active tab |
| Annotieren | aria-pressed={annotateMode} | Toggle button semantics |
| Disabled tab | aria-disabled="true", tabindex="-1" | Not focusable when no blocks |
Read mode is the default view for documents that have transcription blocks. It reuses the same side-by-side split layout as transcribe mode but replaces the editable block cards with flowing serif prose. The goal is a distraction-free reading experience that still lets users compare handwriting with typed text.
The document detail page manages a single mode state that governs the entire view:
let mode: 'read' | 'transcribe' | 'annotate' = $state( blocks.length > 0 ? 'read' : 'transcribe' );
mode === 'read' → this spec (flowing prose, dimmed annotations, no editing)mode === 'transcribe' → annotation-transcription-final-spec (block cards, contenteditable)mode === 'annotate' → yellow comment annotations on PDF'read' and 'transcribe'.'annotate' on/off. When entering annotate mode, the previous mode (read or transcribe) is stored so the user returns to it when exiting.| Component | Purpose |
|---|---|
TranscriptionReadView.svelte | Right panel content in read mode. Renders transcription blocks as flowing prose (<article> with <p> per block). Handles scroll-sync click handlers. |
ModeSwitcher.svelte | Segmented control (Lesen | Bearbeiten). Props: mode (bindable), hasBlocks (disables Lesen when false). Emits mode changes. |
| Component | Change |
|---|---|
DocumentTopBar.svelte | Replace the Transkribieren button with ModeSwitcher. Keep the Annotieren button separate. Add mode bindable prop. |
[id]/+page.svelte | Add mode state. Conditionally render TranscriptionReadView vs TranscriptionEditView in the right panel based on mode. |
PdfAnnotationLayer.svelte | Accept dimmed prop. When true: annotation rects get opacity 0.3, no numbered badges, but remain clickable for scroll-sync. |
GET /api/documents/{id}/transcription-blocks (same endpoint as transcribe mode).<p data-block-id="{block.annotation_id}"> inside an <article> element.font-family: Tinos, Georgia, serif; font-size: 16px; line-height: 1.85.24px 32px for comfortable reading margins.[unleserlich] markers: detect via regex /\[unleserlich\]/g and wrap in <em class="text-ink-2 italic text-[0.9em]">.highlight-annotation event with the annotation_id.flash-fade, 1.5s ease-out).highlight-paragraph with the annotation_id. The text panel scrolls the matching paragraph into view and applies a background highlight that fades.scrollIntoView({ behavior: 'smooth', block: 'center' }) for both directions.background rgba(0,199,177,.10) → transparent over 1.5s..ann-num elements are hidden via display: none)."{n} Abschnitte · Zuletzt bearbeitet: {userName}, {HH:mm}"updated_at across all transcription blocks for this document.updated_by field of that most recently updated block.transcription_status_sections, transcription_status_last_edited.70px. Shows a miniature scan preview.mode_edit_short).15px (slightly larger than desktop) with line-height: 1.9.transcription_blocks count is 0, “Lesen” tab is disabled (opacity: 0.35, cursor: not-allowed, aria-disabled="true").'transcribe'.| Key | de | en |
|---|---|---|
mode_read | Lesen | Read |
mode_edit | Bearbeiten | Edit |
mode_edit_short | Bearb. | Edit |
transcription_status_sections | {n} Abschnitte | {n} sections |
transcription_status_last_edited | Zuletzt bearbeitet: {name}, {time} | Last edited: {name}, {time} |
transcription_empty_title | Noch keine Transkription | No transcription yet |
transcription_empty_desc | Zeichne Bereiche auf dem Scan und tippe den Text ab, um eine Transkription zu erstellen. | Draw regions on the scan and type the text to create a transcription. |
scan_expand | Scan vergrößern | Expand scan |
scan_collapse | Scan verkleinern | Collapse scan |
role="tablist" with role="tab" children, aria-selected on active tab.aria-disabled="true", tabindex="-1".<article> wrapping <p> elements. No contenteditable.role="button", tabindex="0", aria-label="Abschnitt N — klicken um Scan-Position anzuzeigen".role="button", aria-expanded="{expanded}", aria-label="Scan {expanded ? 'verkleinern' : 'vergrößern'}".prefers-reduced-motion: skip the 1.5s fade, apply instant highlight that disappears after 200ms.