From 46d64f50a5a6da0efaa73846314280d0d096c902 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 5 Apr 2026 09:27:22 +0200 Subject: [PATCH] docs(specs): add final specs for transcription feature Three final UI/UX specs for the collaborative transcription system: - expandable-metadata-header-spec: labeled "Details" toggle with drawer - annotation-transcription-final-spec: annotation-backed transcription with block-level comment threads - transcription-read-mode-final-spec: clean split read mode with flowing prose and scroll sync Co-Authored-By: Claude Sonnet 4.6 --- .../annotation-transcription-final-spec.html | 963 ++++++++++++++++++ .../expandable-metadata-header-spec.html | 700 +++++++++++++ .../transcription-read-mode-final-spec.html | 804 +++++++++++++++ 3 files changed, 2467 insertions(+) create mode 100644 docs/specs/annotation-transcription-final-spec.html create mode 100644 docs/specs/expandable-metadata-header-spec.html create mode 100644 docs/specs/transcription-read-mode-final-spec.html diff --git a/docs/specs/annotation-transcription-final-spec.html b/docs/specs/annotation-transcription-final-spec.html new file mode 100644 index 00000000..dded5517 --- /dev/null +++ b/docs/specs/annotation-transcription-final-spec.html @@ -0,0 +1,963 @@ + + + + + + Annotation-Backed Transcription — Final Spec + + + + +
+ +
+
+

Annotation-Backed Transcription

+

Final spec for the collaborative inline transcription system. Draw turquoise rectangles on the scanned letter → numbered transcript blocks appear in a side panel → type what you read. Block-level comment threads with quoted selections for discussion. History in the transcript toolbar. No bottom panel.

+
+
+ Familienarchiv
+ Final spec
+ 2026-04-04 · @leonievoss +
+
+ + + +
+
Core concept — Draw-to-Transcribe
+

Today, annotations are rectangles on the PDF that open a comment thread in the side panel. By adding a type field to DocumentAnnotation, the same draw-a-rectangle gesture can create a transcription annotation (turquoise). A transcription annotation links a PDF region to an editable text block in the right panel.

+

Comments live inside transcript blocks as block-level threads. Users can select a word or phrase before commenting — the selection is auto-quoted into the comment message (e.g. > “Breslau”) rather than structurally anchored to character offsets. This avoids fragile offset tracking that breaks when text is edited. The quote is a display hint, not a structural anchor. Yellow comment annotations are disabled in transcribe mode — only turquoise transcription rectangles appear on the PDF.

+
+ +
+
T
+

Draw-to-transcribe workflow

Draw a rectangle around a passage on the scan. A transcript block appears in the editor, linked to that region. Type what you read. Rinse and repeat down the page. Others can join and work on different blocks simultaneously.

Reuses: AnnotationLayer + PdfViewer + CommentThread · New: TranscriptBlock + TranscriptEditor + type:transcription
+
+ + + +
+
What stays, what changes, what’s new
+
+
+
Reused as-is
+
    +
  • AnnotationLayer — draw rects on PDF
  • +
  • PdfViewer — render, zoom, page nav
  • +
  • CommentThread — threaded replies, mentions
  • +
  • DocumentAnnotation model — add type field
  • +
  • DocumentComment model — unchanged
  • +
+
+
+
Repurposed
+
    +
  • AnnotationSidePanel slot → becomes the transcript editor panel
  • +
  • annotateMode state → split into annotateMode + transcribeMode
  • +
  • Annotation color → turquoise only in transcribe mode, yellow only in annotate mode (mutually exclusive)
  • +
  • AnnotateHintStrip → new copy for transcribe mode
  • +
+
+
+
New
+
    +
  • transcription_blocks table
  • +
  • Transcript editor component (right panel)
  • +
  • Block-level comment threads (quoted selections)
  • +
  • type column on document_annotations
  • +
  • History in transcript toolbar
  • +
  • Bottom panel removed (all modes)
  • +
+
+
+
+ + + +
+

Desktop — transcribe mode active

S1
+
Two users are collaborating. Only turquoise transcription rectangles appear on the PDF — no yellow comment annotations in transcribe mode. One user (Oma Inge, purple) is editing Block 2. The current user (blue) is editing Block 3. Block 2 has a comment thread where Oma Inge quoted “Breslau” to discuss the reading. Each block has a “Kommentieren” button in its footer. The transcript toolbar shows “Verlauf” (history). No bottom panel.
+ +
+
+
Desktop · 1040px
+
+
+ + + +
MR
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+
Du
+
Oma Inge
+
+
Details ▼
+
+
✎ Transkribieren
+
Annotieren
+
+ + +
+ Transkribieren + — Markiere eine Textpassage im Scan, um einen Transkriptions-Block anzulegen +
+ +
+ +
+
+
+
Liebe Martha,
+
+
+
+
+
Dein Heinrich
+ + +
+
1
+
+
+
2
+
+
+
3
+
+
+
4
+
+ + +
+
+
+ +
+ + +
+ +
+ 4 Blöcke +
+
☰ Sortieren
+
🕑 Verlauf
+ ✓ Gespeichert +
+ + +
+ + +
+
1
Anrede
+
Liebe Martha,
+
+ + +
+
+
2
Hauptteil +
Oma Inge
+
+
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.
+ + +
+
💬 2 Kommentare
+
+
OI
+
+ Oma Inge · vor 12 Min. +
“Breslau”
+
Ich bin sicher, das ist “Breslau” — Heinrich war dort im Lazarett.
+
+
+
+
DU
+
+ Du · vor 8 Min. +
Stimmt, danke! Lass ich so.
+
+
+
+ +
✓ Lösen
+
+
+ + + +
+ + +
+
+
3
Familie +
Du
+
+
Die Kinder sollen wissen, dass ich an sie denke. Sag dem kleinen Fritz, er soll auf seine Mutter aufpassen.
+ +
+ + +
+
4
Schluss
+
In ewiger Liebe,
Dein Heinrich
+
+ + +
+ Markiere eine weitere Passage im Scan, um Block 5 anzulegen +
+
+ +
+ Block 3 aktiv + Oma Inge · Block 2 + 1 offene Diskussion +
+
+
+ + +
+
+
+
+ + + +
+

Comment flow — select, quote, discuss

S2
+
The user has selected “[unleserlich]” in Block 2 and clicked “Kommentieren”. The comment input opens with the selection auto-quoted. After posting, the comment appears in the thread with the quote displayed as an indented blockquote. This shows the full lifecycle: selection → quoted input → posted comment.
+
Block-level threads + quoted selections — no char-offset anchoring, no fragile highlights. The quote is frozen text in the message body.
+ +
+
+
Desktop · comment input open with auto-quote
+
+
+ + + +
MR
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+
Details ▼
+
+
✎ Transkribieren
+
Annotieren
+
+ +
+
+
+
+
Liebe Martha,
+
+
+
2
+
+
+
+
+ +
+
+ 4 Blöcke +
+ ✓ Gespeichert +
+ +
+ +
+
2
Hauptteil
+
ich schreibe Dir heute aus dem Lazarett in Breslau. Mach Dir keine Sorgen. Der Arzt sagt [unleserlich] Wochen noch dauern wird.
+ + +
+
Neuer Kommentar zu Block 2
+ +
+ > “[unleserlich]” + ✕ Zitat entfernen +
+
+ + +
+
+ + +
+ + +
+
3
Familie
+
Die Kinder sollen wissen, dass ich an sie denke.
+ + +
+
💬 1 Kommentar
+
+
OI
+
+ Oma Inge · vor 5 Min. +
“Die Kinder”
+
Fritz und Lotte. Fritz war damals 4, Lotte 7.
+
+
+
+ +
+
+ + +
+
+ +
Block 2 aktiv2 Kommentare
+
+
+
+
+
+ +
+

Comment flow · Select → Quote → Discuss

+
/* Comment flow for block-level threads with quoted selections:
+ *
+ * 1. TRIGGER: user clicks "Kommentieren" in block footer.
+ *    Alternatively: Ctrl+Shift+K when block is focused.
+ *
+ * 2. AUTO-QUOTE: if text is selected in the block body (via mouse or keyboard),
+ *    the selection is captured and pre-filled as a blockquote in the comment input:
+ *      > "[unleserlich]"
+ *    The user can edit or remove the quote before sending (× button).
+ *    If no text was selected → input opens empty (general block comment).
+ *
+ * 3. STORAGE: the quote is stored as part of the comment `content` field.
+ *    Markdown blockquote syntax: "> \"Breslau\"\nI think this is Breslau."
+ *    The block_id FK on DocumentComment links the comment to its block.
+ *    NO char_offset_start/end columns. The quote is just text.
+ *
+ * 4. DISPLAY: the quote renders as an indented italic line with a left border,
+ *    above the comment text. It's visually distinct but structurally just content.
+ *
+ * 5. RESILIENCE: if the transcription text changes after quoting, nothing breaks.
+ *    The quote is a frozen snapshot. The discussion context is preserved.
+ *    Compare to char-offset anchoring where an edit would shift all offsets
+ *    and potentially point to the wrong text.
+ *
+ * 6. THREAD: replies to a quoted comment don't need their own quotes —
+ *    the parent comment provides context. Standard CommentThread reply flow.
+ *
+ * 7. MOBILE: "Kommentieren" button always visible in footer.
+ *    Selecting text → auto-quote works the same via touch selection.
+ *    Thread collapsed to "N Kommentare" row, tap to expand. */
+ + + + + + + + + + + + + + +
ElementValueNotes
Comment input
Containerborder:orange, bg:white, radius:4px, mx:8pxAppears below block body, above footer
Quote displayleft-border:2px line, italic, 7px mutedEditable — user can modify or remove
Remove quote"× Zitat entfernen" link, 5px, top-right of quoteConverts to general block comment
Input fieldflex:1, 7px, border:line, bg:page, radius:3pxAuto-focuses when opened
Send button"Senden", 6px/600, navy bg, white textEnter to send, Shift+Enter for newline
Posted comment with quote
Quote in threadleft-border:2px line, italic, 7px mutedRead-only — frozen snapshot of selected text
Message below8px normal text, below the quoteStandard CommentThread message styling
Data model
block_idUUID FK → transcription_blocks (nullable)Links comment to its block
contentTEXT with markdown blockquote> "quoted text"\nComment message
No char offsetsIntentional. See spec rationale.
+
+
+ + + +
+

Desktop — history panel open

S3
+
Clicking “Verlauf” in the transcript toolbar opens a collapsible history panel between the toolbar and the block list. Shows recent changes with word-level diffs, just like the existing PanelHistory component but embedded in the transcript panel instead of the bottom panel.
+ +
+
+
Desktop · 1040px · history open
+
+
+ + + +
MR
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+
Details ▼
+
+
✎ Transkribieren
+
Annotieren
+
+ +
+ Transkribieren + — Markiere eine Textpassage im Scan +
+ +
+
+
+
+
Liebe Martha,
+
+
+
+
1
+
2
+
3
+
4
+
+
+
+ +
+ +
+ +
+ 4 Blöcke +
+
☰ Sortieren
+
🕑 Verlauf
+ ✓ Gespeichert +
+ + +
+
+ Letzte Änderungen + Alle anzeigen → +
+
+ 14:23 + Oma Inge + Block 2: ...Lazarett in BreslauBresla... +
+
+ 14:18 + Du + Block 3: Die Kinder sollen wissen, dass ich an sie denke. +
+
+ 14:12 + Oma Inge + Block 2: ich schreibe Dir heute aus dem Lazarett +
+
+ 14:05 + Du + Block 1: Liebe Martha, +
+
+ + +
+
+
1
Anrede
+
Liebe Martha,
+
+
+
2
Hauptteil
+
ich schreibe Dir heute aus dem Lazarett in Breslau...
+
+
+
3
Familie
+
Die Kinder sollen wissen...
+
+
+
4
Schluss
+
In ewiger Liebe,
Dein Heinrich
+
+
+ +
Block 3 aktiv✓ Gespeichert
+
+
+
+
+
+
+ + + +
+

Mobile — transcribe mode

S4
+
On mobile, the PDF collapses to a 90px strip at the top. Annotation rectangles are visible as thin outlines. Transcript blocks stack vertically below. The history button is in the toolbar above the blocks. Inline threads expand in-place.
+ +
+
+
Mobile · 320px
+
+
14:23••• WiFi 🔋
+
+
+ + Brief von Heinrich, 14.05.1943 + Transkr. +
+ +
+
+
Liebe Martha,
+
+
+
+
+
+
+
+ +
+ 4 Blöcke +
+
🕑 Verlauf
+ +
+ +
+
+
1
Anrede
+
Liebe Martha,
+
+
+
2
Hauptteil Oma Inge
+
ich schreibe Dir heute aus dem Lazarett in Breslau...
+ +
+ 💬 + 1 Diskussion · “Breslau” + +
+
+
+
3
Familie Du
+
Die Kinder sollen wissen...
+
+
+
4
Schluss
+
In ewiger Liebe,
Dein Heinrich
+
+
+
+
+
+
+
+ + + +
+

Annotation-backed transcription · Core implementation spec

+
/* Core flow: enter transcribe mode → crosshair cursor on PDF → draw rect → creates:
+ *   1. DocumentAnnotation(type:"transcription", turquoise) in the DB
+ *   2. TranscriptionBlock(annotation_id, text:"", sort_order:N) in the DB
+ *   3. Editable block in the right panel, linked to the annotation
+ * Clicking an annotation rect on PDF scrolls to + highlights the matching block.
+ * Clicking a block header highlights the matching rect on PDF.
+ *
+ * COMMENTS: block-level threads with quoted selections.
+ *   - Each block has a "Kommentieren" button in its footer.
+ *   - If text is selected when clicking "Kommentieren", the selection is auto-quoted
+ *     into the comment (> "Breslau"). The quote is plain text in the message body,
+ *     NOT a structural char-offset anchor. It doesn't break when text changes.
+ *   - Threads are anchored to block_id only (no char offsets).
+ *   - Yellow comment annotations are DISABLED in transcribe mode.
+ *     Only turquoise transcription rects on the PDF. One annotation type per mode.
+ *
+ * History: "Verlauf" button in transcript toolbar toggles a collapsible panel
+ *   showing recent changes with word-level diffs per block.
+ * Auto-save: debounced PATCH to /api/transcription-blocks/{blockId} (500ms).
+ * Bottom panel: removed entirely (all modes). Metadata → topbar drawer. */
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementValueNotes
Annotation reuse
Draw gestureExisting AnnotationLayer.onDraw(rect)Same pointer events. crosshair cursor.
Annotation colorturquoise (#00C7B1) for transcriptionYellow annotations disabled in transcribe mode
Annotation typeNew column: type VARCHAR "transcription"|"comment"Default "comment" for backward compat
Number badge16px navy circle, top-left of rectSort order number, matches block number
Transcript blocks (right panel)
Block cardborder:1px line, radius:5px, active: turquoise glowHeader: number + label + presence. Body: contenteditable.
Block labelEditable text, defaults: Anrede, Hauptteil, SchlussDouble-click to rename
Empty stateDashed border, "noch leer" italic textFocus to start typing
Add block CTADashed card: "Markiere eine Passage im Scan..."Not clickable — directs user to draw on PDF
Block-level comment threads
Trigger"Kommentieren" button in block footerAlways visible — no hover-reveal
Quoted selectionIf text selected → auto-quoted into comment bodyPlain text quote (> "Breslau"), NOT char-offset anchor
Quote displayLeft border + italic, above the comment textDecorative only — doesn't link to text range
Thread UIorange left-border, orange-tint bg, below block bodyBlock-level anchor (block_id). Reuses CommentThread.
Footer hint"Text markieren für Zitat" in 5px muted textOnly shown when block is active/focused
Resolve"✓ Lösen" button collapses threadResolved threads hidden by default, toggle to show
MobileThreads collapsed to "2 Kommentare" row, tap to expandSaves vertical space on small screens
Yellow annotations in transcribe mode
StatusDisabled — draw gesture only creates turquoise rectsExisting yellow annotations still visible (read-only)
Annotate modeStill available via topbar "Annotieren" buttonExits transcribe mode, enters annotate mode (yellow)
History (transcript toolbar)
Toggle"🕗 Verlauf" button in transcript toolbarActive state: navy bg, white text
PanelCollapsible, between toolbar and block listbg:color-page, border:line, radius:5px
EntriesTime + user + block ref + word-level diffReuses diffWords from 'diff' library
"Alle anzeigen"Link to full history view (reuses PanelHistory)Opens in a modal or replaces block list temporarily
Interaction
Click rect → blockscrollIntoView + active state on blockTurquoise glow on both rect and block
Click block → rectPDF scrolls/zooms to show the annotationIf multi-page: switches page
Delete blockDeletes annotation + block + threadsConfirm dialog if threads exist
Reorder blocksDrag handle in block headerUpdates sort_order via PATCH
Presence (collaborative)
Dots in topbarColored dot + user name, flex rowMax 3 shown, "+N" overflow
Block-level presenceColored dot + name in block headerLeft border color matches user
ImplementationWebSocket presence via Y.js (future)MVP: polling-based, 5s interval
Auto-save
Debounce500ms after last keystrokePATCH /api/transcription-blocks/{blockId}
Status"✓ Gespeichert" in toolbar, fades after 3s"Speichern..." while request in-flight
ConflictLast-write-wins for MVPY.js CRDT for future collaborative editing
+
+ + + +
+

Implementation Guide — Annotation-Backed Transcription

+ +

1. Data Model Changes

+ +

Flyway migration: document_annotations

+
    +
  • Add type VARCHAR(20) NOT NULL DEFAULT 'comment'.
  • +
  • Values: 'comment' (existing behavior) or 'transcription'.
  • +
  • Backward compatible — all existing annotations default to 'comment'.
  • +
+ +

New table: transcription_blocks

+ + + + + + + + + + + + + + +
ColumnTypeNotes
idUUID PKGenerated
annotation_idUUID FK → document_annotationsLinks block to its PDF rectangle
document_idUUID FK → documentsDenormalized for efficient queries
textTEXTThe transcription content
labelVARCHAR(100)"Anrede", "Hauptteil", etc.
sort_orderINTDisplay order in the editor
created_byUUID FK → app_users
updated_byUUID FK → app_users
created_atTIMESTAMP@CreationTimestamp
updated_atTIMESTAMP@UpdateTimestamp
+ +

Block-level comments: document_comments

+
    +
  • Add block_id UUID FK → transcription_blocks (nullable).
  • +
  • No char_offset columns. Quoted selections are stored as plain text in the comment content field using blockquote markdown syntax (> “Breslau”). This is intentional — char offsets break when text is edited and require OT/CRDT to maintain. Quotes are a display hint, not a structural anchor.
  • +
  • Backward compatible — block_id is nullable, existing comments unaffected.
  • +
+ +

Backward compatibility: Document.transcription

+

The existing transcription TEXT field becomes a computed read-only view: SELECT string_agg(text, E'\n\n' ORDER BY sort_order) FROM transcription_blocks WHERE document_id = ?. Write operations go through the block API. This keeps search indexing, export, and the read-only PanelTranscription working without changes.

+ +

2. Annotation Color Convention & Mode Exclusivity

+ + + + + + +
TypeColorHexOn clickWhen active
CommentYellow#FFC800Opens AnnotationSidePanel (existing)Annotate mode only
TranscriptionTurquoise#00C7B1Highlights matching block in transcript editorTranscribe mode only
+

Mode exclusivity: In transcribe mode, only turquoise rects can be drawn. Existing yellow comment annotations from annotate mode are still visible on the PDF (read-only, dimmed) but cannot be created or interacted with. The “Annotieren” button exits transcribe mode and enters annotate mode (and vice versa). This prevents overlapping annotation types and avoids user confusion about which comment system to use.

+ +

3. Component Architecture

+ + + + + + + + + + + + + + +
ComponentChange
AnnotationLayer.sveltePass type to onDraw callback. Render turquoise vs yellow based on annotation type. Add number badges for transcription annotations.
PdfViewer.svelteSplit handleAnnotationDraw into two paths (annotate vs transcribe). Route handleAnnotationClick to either side panel or transcript editor.
AnnotationSidePanel.svelteNo change — still handles comment-type annotations in annotate mode. Hidden in transcribe mode.
TranscriptEditor.svelte (new)Right panel. Renders transcript toolbar + block list. Manages block CRUD, auto-save, block-level comment threads.
TranscriptBlock.svelte (new)Single block card. contenteditable body, header with number/label/presence, footer with “Kommentieren” button, thread slot below body.
BlockCommentThread.svelte (new)Comment thread anchored to a block. Shows quoted selections as blockquotes. Reuses CommentThread internally for replies/mentions.
TranscriptToolbar.svelte (new)Block count, sort button, history toggle, save status.
TranscriptHistory.svelte (new)Collapsible panel. Reuses diffWords from the diff library. Shows recent changes per block.
DocumentBottomPanel.svelteRemoved entirely. Metadata lives in the topbar drawer (see companion spec). Discussion, transcription, and history are all inline.
documents/[id]/+page.svelteAdd transcribeMode state. Conditionally render TranscriptEditor vs bottom panel.
+ +

4. API Endpoints

+ + + + + + + + + +
MethodPathNotes
POST/api/documents/{id}/annotationsExisting, but now accepts type field. If type="transcription", also creates a TranscriptionBlock.
GET/api/documents/{id}/transcription-blocksReturns all blocks ordered by sort_order.
PATCH/api/transcription-blocks/{blockId}Update text, label, or sort_order. Auto-save target.
DELETE/api/transcription-blocks/{blockId}Deletes block + its annotation + any anchored comments.
PATCH/api/transcription-blocks/reorderBulk update sort_order for drag-and-drop reordering.
+ +

5. Draw-to-Transcribe Workflow

+
    +
  1. User enters Transcribe mode (topbar button, turquoise). Hint strip appears. Yellow comment annotations become read-only/dimmed. Only turquoise rects can be drawn.
  2. +
  3. Crosshair cursor on PDF (same as annotate mode). User draws a rectangle around a handwriting passage.
  4. +
  5. AnnotationLayer.onDraw(rect) fires. PdfViewer calls POST /api/documents/{id}/annotations with type: "transcription".
  6. +
  7. Backend creates DocumentAnnotation + TranscriptionBlock (empty text, next sort_order).
  8. +
  9. Frontend receives the created annotation + block. The transcript editor scrolls to the new empty block and focuses it.
  10. +
  11. User types the transcription. Auto-save debounces to PATCH /api/transcription-blocks/{blockId}.
  12. +
  13. Repeat: draw next rectangle, type next block.
  14. +
+ +

6. Comment Flow — Block-Level Threads with Quoted Selections

+

Comments are anchored to blocks, not character offsets. This is a deliberate simplification:

+ +

Why not char-offset anchoring?

+
    +
  • When someone edits the transcription text, all character offsets downstream shift.
  • +
  • Keeping offsets in sync requires operational transforms (OT) or CRDT — that’s the Y.js future work, not MVP.
  • +
  • A stale offset pointing to the wrong word is worse than a quoted snippet that no longer matches but still shows what was discussed.
  • +
+ +

How it works

+
    +
  1. User clicks “Kommentieren” in a block footer.
  2. +
  3. If text is selected in the block body, the selection is auto-quoted into the comment input: > “Breslau”. The user can edit or remove the quote before sending.
  4. +
  5. If no text is selected, the comment input opens empty — a general block-level comment.
  6. +
  7. The comment is saved as a DocumentComment with block_id set. The quoted text is part of the content field (markdown blockquote syntax).
  8. +
  9. The thread renders below the block body with an orange left-border. Quoted text appears as an indented italic blockquote above the comment message.
  10. +
  11. Replies work the same as existing CommentThread — no changes needed.
  12. +
+ +

What happens when text changes after quoting?

+

Nothing breaks. The quote is a frozen snapshot of what the user selected. If “Bresla” was later corrected to “Breslau”, the original quote still reads > “Bresla” with Oma Inge’s comment “I think this is Breslau.” The context is preserved. No orphaned anchors, no broken highlights.

+ +

Footer hint

+

When a block is focused/active, the footer shows a subtle hint: “Text markieren für Zitat” (select text for a quote). This teaches the quoted-selection pattern without requiring documentation.

+ +

7. History in Transcript Toolbar

+
    +
  • The “Verlauf” button in the toolbar toggles TranscriptHistory.svelte.
  • +
  • The panel renders between the toolbar and the block list (pushes blocks down).
  • +
  • It shows recent changes per block, using diffWords from the diff library (same as existing PanelHistory).
  • +
  • Each entry: timestamp, user name, block reference (e.g. “Block 2”), and a word-level diff snippet.
  • +
  • “Alle anzeigen” opens a full history view — can reuse the existing PanelHistory component in a modal.
  • +
  • Data source: the existing document version history API, filtered/grouped by block.
  • +
+ +

8. Accessibility

+
    +
  • Transcription blocks: role="region" with aria-label="Transkriptions-Block N: [label]"
  • +
  • Block body: contenteditable with aria-multiline="true"
  • +
  • Number badges on PDF: aria-label="Transkriptions-Bereich N"
  • +
  • Comment button: aria-label="Block N kommentieren"
  • +
  • History toggle: aria-expanded, aria-controls="transcript-history"
  • +
  • Focus order: topbar → hint strip → PDF (for drawing) → transcript blocks (in sort order) → comment button → status bar
  • +
  • Keyboard: Tab between blocks, Enter to edit, Escape to deselect. Ctrl+Shift+N to prompt draw on PDF. Ctrl+Shift+K to open comment on focused block.
  • +
+ +

9. Companion Spec

+

The expandable metadata header (labeled “Details ▼” toggle) is specified separately in expandable-metadata-header-spec.html. Together, these two specs fully eliminate the bottom panel in all modes: metadata → header drawer, transcription → inline split view, discussion → inline threads, history → transcript toolbar. One consistent pattern — no mode-dependent UI structure.

+
+ +
+ + diff --git a/docs/specs/expandable-metadata-header-spec.html b/docs/specs/expandable-metadata-header-spec.html new file mode 100644 index 00000000..72a57536 --- /dev/null +++ b/docs/specs/expandable-metadata-header-spec.html @@ -0,0 +1,700 @@ + + + + + + Expandable Metadata Header — Final Spec + + + + +
+ +
+
+

Expandable Metadata Header

+

The document topbar gains a labeled toggle button (“Details ▼”) that opens a full-width metadata drawer below the main row. This replaces the bottom panel’s Metadata tab in transcribe mode, keeping all interactive elements (person links, conversation links, tag filters) accessible without consuming permanent viewport space.

+
+
+ Familienarchiv
+ Final spec
+ 2026-04-04 · @leonievoss +
+
+ + + +
+
Why a labeled toggle, not just a chevron
+

User interviews include family members aged 60+. A bare 12–16px chevron icon is easy to miss or misinterpret as decorative. A labeled button — “Details ▼” — is self-explanatory, provides a larger click target (min 44×28px), and follows the progressive disclosure pattern: key facts (title, date, person chips) are always visible in the topbar; the toggle reveals the full metadata only when needed.

+
+ +
+
What lives where
+
+
+
Always visible in topbar
+
    +
  • Document title (truncated)
  • +
  • Date (compact format)
  • +
  • Sender & receiver chips (abbreviated)
  • +
  • Action buttons (Edit, Annotate, Download)
  • +
  • “Details” toggle button
  • +
+
+
+
Revealed in drawer
+
    +
  • Full date (long format)
  • +
  • Creation location (e.g. “Breslau”)
  • +
  • Archive location (e.g. “Ordner A3, Schublade 2”)
  • +
  • Tags (clickable → filter documents)
  • +
  • Full person cards with avatar, name, alias
  • +
  • Person detail links (/persons/{id})
  • +
  • Conversation links (/korrespondenz?...)
  • +
+
+
+
+ + + +
+

Desktop — collapsed (default)

S1
+
The topbar looks identical to today except for the “Details” button between the person chips and the action buttons. The split view gets the full remaining viewport.
+ +
+
+
Desktop · 1040px · collapsed
+
+
+ + + +
MR
+
+
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+ + + +
+ +
Details
+
+
✎ Transkribieren
+
Annotieren
+
+
+ + +
+
+
+
+
Liebe Martha,
+
+
+
+
1
+
2
+
3
+
+
+
+
+
+
+ 3 Blöcke +
+ ✓ Gespeichert +
+
+
1
Anrede
Liebe Martha,
+
2
Hauptteil
ich schreibe Dir heute aus dem Lazarett in Breslau...
+
3
Familie
noch leer
+
+
Block 2 aktiv
+
+
+
+
+
+
+ + + +
+

Desktop — expanded

S2
+
Clicking “Details” slides open a full-width drawer below the topbar. Three-column grid: details (date, location, archive), persons (sender & receiver cards with conversation links), and tags. The drawer pushes content down — it is part of the document flow, not an overlay. No clipping, no z-index issues.
+ +
+
+
Desktop · 1040px · expanded
+
+
+ + + +
MR
+
+
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+ + + +
+ +
Details
+
+
✎ Transkribieren
+
Annotieren
+
+ + +
+ +
+
Details
+
+
+
📅
+
14. Mai 1943
Datum
+
+
+
📍
+
Breslau
Entstehungsort
+
+
+
📁
+
Ordner A3, Schublade 2
Archivstandort
+
+
+
+ + +
+
Personen
+
+
Absender
+
+
HR
+
Heinrich Raddatz
Opa Heinrich
+
💬
+
+
Empfänger
+
+
MR
+
Martha Raddatz
Oma Martha
+
💬
+
+
+
+ + +
+
Schlagwörter
+
+
Feldpost
+
2. Weltkrieg
+
Lazarett
+
Breslau
+
+
+
+
+
+ + +
+
+
+
+
Liebe Martha,
+
+
+
+
+
+
+
+
+
1
Anrede
Liebe Martha,
+
2
Hauptteil
ich schreibe Dir heute...
+
+
Block 2 aktiv
+
+
+
+
+
+
+ + + +
+

Mobile — collapsed

S3
+
On mobile, the topbar shows the title, a compact “Details” toggle, and the transcribe mode pill. Person chips are hidden (shown in drawer instead). The toggle provides a 44px tap target.
+ +
+
+
Mobile · 320px · collapsed
+
+
14:23••• WiFi 🔋
+
+
+
+ + Brief von Heinrich, 14.05.1943 +
Details
+ Transkr. +
+
+ +
+
+
Liebe Martha,
+
+
+
+
+
+ +
+
+
1
Anrede
+
Liebe Martha,
+
+
+
2
Hauptteil
+
ich schreibe Dir heute aus dem Lazarett in Breslau...
+
+
+
3
Familie
+
noch leer
+
+
+
+
+
+
+
+ + + +
+

Mobile — expanded

S4
+
The drawer opens as a single-column stack below the topbar. Person cards are full-width with 44px minimum touch targets. Conversation links are always visible (no hover-reveal on touch). Tags wrap naturally. The PDF strip and transcript blocks are pushed down.
+ +
+
+
Mobile · 320px · expanded
+
+
14:23••• WiFi 🔋
+
+
+
+ + Brief von Heinrich, 14.05.1943 +
Details
+ Transkr. +
+
+ +
+
+
+
Datum
+
14. Mai 1943
+
+
+
Ort
+
Breslau
+
+
+
+
+
Archivstandort
+
Ordner A3, Schublade 2
+
+
+
Absender
+
+
HR
+
Heinrich Raddatz
Opa Heinrich
+
💬
+
+
Empfänger
+
+
MR
+
Martha Raddatz
Oma Martha
+
💬
+
+
Schlagwörter
+
+
Feldpost
+
2. Weltkrieg
+
Lazarett
+
Breslau
+
+
+ +
+
+
+
+
+
+ +
+
+
1
Anrede
+
Liebe Martha,
+
+
+
2
Hauptteil
+
ich schreibe Dir heute...
+
+
+
+
+
+
+
+ + + +
+

Non-transcribe mode — standard document view

S5
+
Outside of transcribe mode, the document detail page uses the same “Details” drawer pattern. No bottom panel. The PDF gets the full remaining viewport. Discussion and transcription are accessible via dedicated buttons (Transkribieren enters split mode, Annotieren enters annotation mode). One consistent pattern everywhere — no mode-dependent UI structure.
+ +
+
+
Desktop · non-transcribe · collapsed
+
+
+ + + +
MR
+
+
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+ + + +
+
Details
+
+
✎ Transkribieren
+
Annotieren
+
+ +
+
+
+
+ + +
+
+
Liebe Martha,
+
+
+
+
+
+
+
+
+
+ + + +
+

Expandable metadata header · Implementation spec

+
/* The topbar gains a labeled "Details ▼" toggle button that opens a full-width metadata
+ * drawer below the main topbar row.
+ *
+ * Collapsed (default): topbar looks like today + a "Details ▼" button between
+ *   the person chips and the action buttons.
+ * Expanded: a new row slides down with a 3-column grid (desktop):
+ *   Col 1: date (long format), location, archive location — icon + value + label
+ *   Col 2: sender card + receiver cards — clickable, links to /persons/{id}
+ *           conversation icon links to /korrespondenz?senderId=X&receiverId=Y
+ *   Col 3: tag chips — clickable, link to /?tag=X
+ *
+ * The drawer PUSHES content down (document flow, not overlay).
+ * Background: color-page (sand) to visually separate from white topbar.
+ * Animation: Svelte slide transition or max-height + overflow:hidden, 200ms ease.
+ *
+ * KEY DECISION: "Details ▼" labeled toggle instead of bare chevron icon.
+ * Reason: 60+ year old users in user interviews — bare icons are easy to miss.
+ * The label makes the interaction self-explanatory and provides a 44×28px min tap target.
+ *
+ * Mobile: single-column stack, person cards full-width with 44px min-height,
+ *   conversation links always visible (no hover-reveal on touch). */
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementValueNotes
Toggle button
Label"Details" + ▼ chevroni18n key: topbar_details_toggle
Sizemin 44×28px tap target, text-xs font-semiboldWCAG 2.5.5 compliant target size
Inactive styleborder border-line, text-ink-2, bg-transparentSubtle, doesn't compete with action buttons
Active stylebg-primary, text-primary-fg, border-primaryClear open state — matches annotate button pattern
Chevron▼ (U+25BC), rotates 180deg when openCSS transition transform 200ms
Ariaaria-expanded, aria-controls="metadata-drawer"Button role implicit
KeyboardCtrl+M toggles, Escape closesCtrl+M matches "M for metadata"
Drawer (expanded)
Layoutgrid 3-col desktop (1fr 1fr 1fr), 1-col mobilebg:color-page, border-top:line, p:12px 16px
AnimationSvelte slide transition, 200msOr CSS max-height 0↔auto with overflow:hidden
Push behaviorIn document flow, pushes split view downNot absolute/overlay — no clipping
IDid="metadata-drawer"role="region", aria-label="Dokumentmetadaten"
Drawer content — Details column
DateLong format (14. Mai 1943), icon 📅Uses existing formatDate utility
LocationText, icon 📍Only shown if doc.creationLocation exists
ArchiveText, icon 📁Only shown if doc.archiveLocation exists
Drawer content — Persons column
Person cardborder:line, radius:5px, bg:page, hover:accent-bgEntire card is a link to /persons/{id}
Card content18px avatar + full name + aliasAlias from person.alias field
Conversation icon💬 appears on hover (desktop), always visible (mobile)Links to /korrespondenz?senderId=X&receiverId=Y
Mobile card heightmin-height 44pxWCAG touch target compliance
Drawer content — Tags column
Chiptext-[10px]/600, sand bg, uppercase, radius:3pxClick → navigate to /?tag=X
Hoverbg-primary, text-primary-fgVisual feedback that chips are interactive
Non-transcribe mode
Toggle shown?Yes — always present in topbarConsistent UX across all modes
Bottom panelRemoved entirely — all modesDrawer is the single metadata pattern everywhere
+
+ + + +
+

Implementation Guide — Expandable Metadata Header

+ +

1. Scope

+

Add a labeled “Details” toggle button and a collapsible metadata drawer to DocumentTopBar.svelte. This spec covers only the header expansion — the transcription split view, inline comments, and history toolbar are covered in the companion spec (annotation-transcription-final-spec.html).

+ +

2. State

+
    +
  • let metadataOpen = $state(false) in DocumentTopBar.svelte.
  • +
  • Toggle on button click. Close on Escape key. Toggle on Ctrl+M.
  • +
  • State is local — not persisted. Defaults to closed on every page load.
  • +
+ +

3. Component Changes

+ + + + + + + + +
ComponentChange
DocumentTopBar.svelteAdd metadataOpen state, toggle button, and conditional drawer div. New props needed: doc.creationLocation, doc.archiveLocation, doc.tags, full sender/receiver objects with aliases.
MetadataDrawer.svelte (new)Extracted child component. Receives the doc object. Renders the 3-column grid (desktop) or 1-column stack (mobile). Contains person cards, tag chips, and metadata fields.
PersonChipRow.svelteNo change. Still renders the abbreviated chips in the main topbar row.
DocumentBottomPanel.svelteRemove entirely. The metadata drawer replaces the Metadata tab. Transcription, Discussion, and History move to inline UI (see companion transcription spec). No bottom panel in any mode.
+ +

4. Toggle Button Placement

+

In the topbar’s flex row, the button goes after the person chips divider and before the action buttons divider:

+

← | Title | chips → | Details ▼ | Transkribieren | Annotieren | Edit | Download

+

On mobile (<375px), person chips are hidden. The toggle sits after the title, before the transcribe pill.

+ +

5. Drawer Markup

+
    +
  • Use Svelte slide transition: {#if metadataOpen}<div transition:slide={{ duration: 200 }}>
  • +
  • The drawer is a direct child of the topbar wrapper, below the main flex row.
  • +
  • Desktop: grid grid-cols-3 gap-4 p-3 sm:p-4 bg-canvas border-t border-line
  • +
  • Mobile: grid grid-cols-1 gap-3 p-3 bg-canvas border-t border-line
  • +
  • Breakpoint for 3-col: md:grid-cols-3 (768px+).
  • +
+ +

6. Person Cards in Drawer

+
    +
  • Each card: avatar (using personAvatarColor), full name (font-serif), alias (text-xs text-ink-2).
  • +
  • Card wraps an <a href="/persons/{id}">.
  • +
  • Conversation icon: separate <a> inside the card, absolute-positioned or flex-end. Links to /korrespondenz?senderId={sender.id}&receiverId={receiver.id}.
  • +
  • On mobile: min-h-[44px] for touch targets. Conversation icon always visible (opacity-100 instead of opacity-0 group-hover:opacity-100).
  • +
+ +

7. Tag Chips in Drawer

+
    +
  • Each tag: <a href="/?tag={tag.name}"> with text-[10px] font-semibold uppercase bg-muted rounded px-2 py-0.5 hover:bg-primary hover:text-primary-fg transition-colors.
  • +
  • aria-label="Dokumente mit Schlagwort {tag.name} filtern".
  • +
+ +

8. Accessibility

+
    +
  • Toggle button: aria-expanded={metadataOpen}, aria-controls="metadata-drawer".
  • +
  • Drawer: id="metadata-drawer", role="region", aria-label="Dokumentmetadaten".
  • +
  • Person cards: accessible name includes full name + “Zur Personenseite”.
  • +
  • Conversation link: aria-label="Korrespondenz zwischen {sender} und {receiver} anzeigen".
  • +
  • Tab order: toggle button → drawer contents (when open) → action buttons.
  • +
  • Escape closes drawer and returns focus to the toggle button.
  • +
+ +

9. i18n Keys

+ + + + + + + + + + + + +
Keydeen
topbar_details_toggleDetailsDetails
topbar_details_dateDatumDate
topbar_details_locationEntstehungsortLocation
topbar_details_archiveArchivstandortArchive location
topbar_details_senderAbsenderSender
topbar_details_receiversEmpfängerReceivers
topbar_details_tagsSchlagwörterTags
topbar_details_conversationKorrespondenz anzeigenView correspondence
+
+ +
+ + diff --git a/docs/specs/transcription-read-mode-final-spec.html b/docs/specs/transcription-read-mode-final-spec.html new file mode 100644 index 00000000..9fd310bb --- /dev/null +++ b/docs/specs/transcription-read-mode-final-spec.html @@ -0,0 +1,804 @@ + + + + + + Transcription Read Mode — Final Spec (Clean Split) + + + + +
+ +
+
+

Transcription Read Mode — Final Spec

+

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.

+
+
+ Familienarchiv
+ Final
+ 2026-04-05 · @leonievoss +
+
+ + + +
+
Design rationale
+

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.

+ +
+
+
Kept
+
    +
  • Transcription text (flowing prose)
  • +
  • PDF scan viewer (same position)
  • +
  • Topbar (title, Details toggle, person chips)
  • +
  • Mode switcher (Lesen / Bearbeiten)
  • +
  • Resizable split handle
  • +
  • Scroll sync (click paragraph ↔ PDF)
  • +
+
+
+
Removed
+
    +
  • Block borders & numbered badges
  • +
  • Contenteditable / cursor
  • +
  • Comment threads & “Kommentieren” buttons
  • +
  • Presence dots & user indicators
  • +
  • Hint strip (“Markiere eine Passage…”)
  • +
  • Drag handles, sort controls
  • +
  • Auto-save status indicator
  • +
  • Add-block CTA
  • +
  • History / “Verlauf” button
  • +
+
+
+
+ + + +
+

S1 — Desktop read mode

S1
+
Side-by-side split: PDF scan on the left with dimmed annotation outlines, flowing serif prose on the right. The mode switcher shows “Lesen” as active. Clicking a paragraph briefly highlights the matching PDF region (turquoise flash, 1.5s fade).
+
Primary reading state — the default view when a transcription exists.
+ +
+
+
Desktop · 1040px
+
+
+ + + +
MR
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+
Heinrich R.
+ +
MR
Martha R.
+
+
Details ▼
+
+ +
LesenBearbeiten
+
+ +
+ +
+
+
+
Liebe Martha,
+
+
+
+
+
Dein Heinrich
+ + +
+
+
+
+
+
+
+ +
+ + +
+
+
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
+
+
+ 4 Abschnitte + Zuletzt bearbeitet: Oma Inge, 14:23 +
+
+
+
+
+
+ +
+

S1 · Desktop read mode

+
/* 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 */
+ + + + + + + + + + + + + + +
ElementValueNotes
Text panel
FontTinos (serif), 16px, line-height 1.85Generous reading typography
Padding24px 32pxComfortable margins like a book page
ParagraphsOne <p> per transcription blockmb-4 between paragraphs
[unleserlich]italic, text-ink-2, font-size: 0.9emSubtle but readable
HoverSubtle turquoise bg at 6% opacityHint that paragraphs are clickable
PDF panel
AnnotationsDimmed: border-opacity 0.3, bg-opacity 0.04Still clickable for scroll-sync
BadgesHiddenNo numbered circles in read mode
Scroll syncClick para → PDF scrolls, flash 1.5sTurquoise tint at 18% → fade to 4%
Status bar
Content"N Abschnitte · Zuletzt bearbeitet: Name, HH:mm"Uses most recent updated_at across blocks
Height28px, sand backgroundSame as transcribe mode status bar
+
+
+ + + +
+

S2 — Scroll sync highlight

S2
+
The user clicked the second paragraph. The matching PDF annotation flashes with a turquoise highlight that fades over 1.5 seconds. The paragraph itself gets a subtle background tint. This is the only interactive element in read mode — no editing, no comments.
+
Click-to-highlight interaction — bidirectional scroll sync between text and scan.
+ +
+
+
Desktop · 1040px
+
+
+ + + +
MR
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+
Heinrich R.
+ +
MR
Martha R.
+
+
Details ▼
+
+
LesenBearbeiten
+
+ +
+
+
+
+
Liebe Martha,
+
+
+
+
+
Dein Heinrich
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
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
+
+
+ 4 Abschnitte + Zuletzt bearbeitet: Oma Inge, 14:23 +
+
+
+
+
+
+ +
+

S2 · Scroll sync highlight

+
/* 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. */
+ + + + + + + + + +
ElementValueNotes
Highlight animation
Paragraph bgrgba(0,199,177,.10)Turquoise at 10%, fades to 0
Annotation flashrgba(0,199,177,.18) → .04Border returns to .3 opacity
Duration1.5s ease-outCSS animation, no JS timers needed
Scroll behaviorsmooth, block: centerscrollIntoView({ behavior: 'smooth', block: 'center' })
Data binding
Paragraph attrdata-block-id="{annotation_id}"Links text to PDF annotation
Annotation attrdata-annotation-id="{id}"Already exists from transcribe mode
+
+
+ + + +
+

S3 — No transcription (empty state)

S3
+
When no transcription blocks exist, the mode switcher defaults to “Bearbeiten” and the right panel shows an empty state encouraging the user to start transcribing. The “Lesen” tab is disabled (greyed out).
+
Empty state — no read mode available until at least one block exists.
+ +
+
+
Desktop · 1040px
+
+
+ + + +
MR
+
+
+
+
Brief von Heinrich an Martha, 14. Mai 1943
+
+
Details ▼
+
+ +
LesenBearbeiten
+
+ +
+
+
+
+
Liebe Martha,
+
+
+
Dein Heinrich
+
+
+
+ +
+ +
+
+ +
+
Noch keine Transkription
+
Zeichne Bereiche auf dem Scan und tippe den Text ab, um eine Transkription zu erstellen.
+
+
+
+
+
+ +
+

S3 · Empty state

+
/* 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. */
+ + + + + + + + +
ElementValueNotes
Empty state
IconPencil in 48px sand circleCentered 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 tabDisabled: opacity .35, not-allowedEnabled when block count > 0
Default"Bearbeiten" activeEnters transcribe mode directly
+
+
+ + + +
+

S4 — Mobile read mode

S4
+
On mobile, the split view becomes vertical: a collapsible PDF strip (70px) at the top, flowing text below. The mode switcher abbreviates “Bearbeiten” to “Bearb.” to fit. Tapping the PDF strip expands it; tapping again collapses. Paragraphs are still tappable for scroll-sync.
+
Mobile layout — stacked vertical with collapsible scan strip.
+ +
+
+
Mobile · 320px
+
+
14:23••• WiFi 🔋
+
+
+ + Brief von Heinrich, 14.05.1943 +
LesenBearb.
+
+ +
+
+
Liebe Martha,
+
+
+
+ +
▲ Scan vergrößern
+
+ +
+
+

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

+
+
+ +
+ 4 Abschnitte + Oma Inge, 14:23 +
+
+
+
+ + +
+
Mobile · 320px · scan expanded
+
+
14:23••• WiFi 🔋
+
+
+ + Brief von Heinrich, 14.05.1943 +
LesenBearb.
+
+ +
+
+
Liebe Martha,
+
+
+
+
Dein Heinrich
+ +
+
+
+ +
▼ Scan verkleinern
+
+ +
+
+

Liebe Martha,

+

ich schreibe Dir heute aus dem Lazarett in Breslau…

+
+
+
+
+
+
+ +
+

S4 · Mobile read mode

+
/* 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. */
+ + + + + + + + + + + + + + +
ElementValueNotes
PDF strip
Collapsed height70pxShows miniature scan preview
Expanded height~50vh or max 300pxSmooth CSS transition (300ms ease)
ToggleTap anywhere on stripHint text in bottom-right corner
Text area
FontTinos, 15px, line-height 1.9Slightly larger than desktop for touch
Padding16pxFull-width, no wasted space
Mode switcher
Labels"Lesen | Bearb."Abbreviated to fit mobile topbar
Font size10pxCompact but readable
Scroll sync on mobile
Tap paragraphExpand PDF if collapsed, then highlightAuto-expand + scroll + flash
Tap annotationCollapse PDF, scroll text to paragraphSmart collapse after showing match
+
+
+ + + +
+

S5 — Mode switcher states

S5
+
The mode switcher is a segmented control in the topbar that replaces the previous “Transkribieren” turquoise button. It governs three visual states: Lesen (read mode, this spec), Bearbeiten (transcribe/edit mode), and the existing Annotieren button (yellow comment annotations). The modes are mutually exclusive.
+
Segmented control — replacing the turquoise “Transkribieren” button.
+ +
+ +
+
Lesen active
+
+
LesenBearbeiten
+
+
✎ Annotieren
+
+
+ + +
+
Bearbeiten active
+
+
LesenBearbeiten
+
+
✎ Annotieren
+
+
+ + +
+
Annotieren active (separate button)
+
+
LesenBearbeiten
+
+
✎ Annotieren
+
+
+ + +
+
No transcription blocks
+
+
LesenBearbeiten
+
+
✎ Annotieren
+
+
+
+ +
+

S5 · Mode switcher states

+
/* 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 */
+ + + + + + + + + + + + + + + +
ElementValueNotes
Segmented control
Items"Lesen" | "Bearbeiten"Mobile: "Lesen" | "Bearb."
Active stylebg:navy, color:#fffRounded within the pill
Inactive stylebg:transparent, color:mutedHover: bg:sand
Dimmed styleBoth items at opacity .5Only when annotate mode is active
Disabled (Lesen)opacity .35, cursor not-allowedWhen no transcription blocks exist
Annotieren button
DefaultGhost style (border:muted)Same as current topbar button
Activebg:navy, color:#fffFilled state when annotate mode on
VisibilitycanAnnotate && isPdfHidden for non-PDF documents
Accessibility
Segmentedrole="tablist", children role="tab"aria-selected on active tab
Annotierenaria-pressed={annotateMode}Toggle button semantics
Disabled tabaria-disabled="true", tabindex="-1"Not focusable when no blocks
+
+
+ + + +
+

Implementation Guide — Transcription Read Mode (Clean Split)

+ +

1. Overview

+

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.

+ +

2. Mode State Management

+

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
  • +
  • The segmented control in the topbar toggles between 'read' and 'transcribe'.
  • +
  • The “Annotieren” button toggles 'annotate' on/off. When entering annotate mode, the previous mode (read or transcribe) is stored so the user returns to it when exiting.
  • +
+ +

3. Component Architecture

+

3a. New components

+ + + + + + +
ComponentPurpose
TranscriptionReadView.svelteRight panel content in read mode. Renders transcription blocks as flowing prose (<article> with <p> per block). Handles scroll-sync click handlers.
ModeSwitcher.svelteSegmented control (Lesen | Bearbeiten). Props: mode (bindable), hasBlocks (disables Lesen when false). Emits mode changes.
+ +

3b. Modified components

+ + + + + + + +
ComponentChange
DocumentTopBar.svelteReplace the Transkribieren button with ModeSwitcher. Keep the Annotieren button separate. Add mode bindable prop.
[id]/+page.svelteAdd mode state. Conditionally render TranscriptionReadView vs TranscriptionEditView in the right panel based on mode.
PdfAnnotationLayer.svelteAccept dimmed prop. When true: annotation rects get opacity 0.3, no numbered badges, but remain clickable for scroll-sync.
+ +

4. Read View Rendering

+

4a. Text rendering

+
    +
  • Fetch transcription blocks from GET /api/documents/{id}/transcription-blocks (same endpoint as transcribe mode).
  • +
  • Render each block as a <p data-block-id="{block.annotation_id}"> inside an <article> element.
  • +
  • Typography: font-family: Tinos, Georgia, serif; font-size: 16px; line-height: 1.85.
  • +
  • Padding: 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]">.
  • +
  • Text is not contenteditable. No cursor, no selection highlights, no editing.
  • +
+ +

4b. Scroll sync

+
    +
  • Each paragraph has a click handler that dispatches a highlight-annotation event with the annotation_id.
  • +
  • The PDF viewer listens for this event, scrolls to the annotation, and applies a CSS animation (flash-fade, 1.5s ease-out).
  • +
  • Reverse direction: clicking a dimmed annotation on the PDF dispatches highlight-paragraph with the annotation_id. The text panel scrolls the matching paragraph into view and applies a background highlight that fades.
  • +
  • Use scrollIntoView({ behavior: 'smooth', block: 'center' }) for both directions.
  • +
  • The highlight CSS animation: background rgba(0,199,177,.10) → transparent over 1.5s.
  • +
+ +

5. PDF Annotations in Read Mode

+
    +
  • Turquoise annotation rectangles are rendered but dimmed: border opacity 0.3, background opacity 0.04.
  • +
  • No numbered badges (the .ann-num elements are hidden via display: none).
  • +
  • Annotations remain clickable — they trigger scroll-sync to the matching paragraph.
  • +
  • When an annotation is flash-highlighted (via scroll-sync), it briefly returns to full opacity before fading back to dimmed.
  • +
  • Yellow comment annotations are not shown in read mode (they belong to annotate mode only).
  • +
+ +

6. Status Bar

+
    +
  • Positioned at the bottom of the text panel (not the full viewport).
  • +
  • Content: "{n} Abschnitte · Zuletzt bearbeitet: {userName}, {HH:mm}"
  • +
  • The “Zuletzt bearbeitet” timestamp is the most recent updated_at across all transcription blocks for this document.
  • +
  • The user name comes from the updated_by field of that most recently updated block.
  • +
  • i18n keys: transcription_status_sections, transcription_status_last_edited.
  • +
+ +

7. Mobile Layout (viewport < 768px)

+
    +
  • The side-by-side split becomes vertical: PDF strip at top, text below.
  • +
  • PDF strip collapsed height: 70px. Shows a miniature scan preview.
  • +
  • Tap strip to expand (~50vh, max 300px). Tap again to collapse. Smooth CSS transition (300ms ease).
  • +
  • Expand/collapse hint text in bottom-right corner of the strip.
  • +
  • Mode switcher abbreviation: “Lesen | Bearb.” (i18n key: mode_edit_short).
  • +
  • Scroll-sync on paragraph tap: if PDF is collapsed, auto-expand first, then scroll to annotation.
  • +
  • Text typography: 15px (slightly larger than desktop) with line-height: 1.9.
  • +
+ +

8. Empty State

+
    +
  • When transcription_blocks count is 0, “Lesen” tab is disabled (opacity: 0.35, cursor: not-allowed, aria-disabled="true").
  • +
  • Mode defaults to 'transcribe'.
  • +
  • Right panel shows empty state: pencil icon in 48px sand circle, title (“Noch keine Transkription”), description (“Zeichne Bereiche auf dem Scan…”).
  • +
  • As soon as the first block is saved, “Lesen” becomes clickable. Mode does not auto-switch.
  • +
+ +

9. i18n Keys

+ + + + + + + + + + + + + +
Keydeen
mode_readLesenRead
mode_editBearbeitenEdit
mode_edit_shortBearb.Edit
transcription_status_sections{n} Abschnitte{n} sections
transcription_status_last_editedZuletzt bearbeitet: {name}, {time}Last edited: {name}, {time}
transcription_empty_titleNoch keine TranskriptionNo transcription yet
transcription_empty_descZeichne 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_expandScan vergrößernExpand scan
scan_collapseScan verkleinernCollapse scan
+ +

10. Accessibility

+
    +
  • Mode switcher: role="tablist" with role="tab" children, aria-selected on active tab.
  • +
  • Disabled “Lesen” tab: aria-disabled="true", tabindex="-1".
  • +
  • Read view text: semantic HTML — <article> wrapping <p> elements. No contenteditable.
  • +
  • Paragraphs are clickable: role="button", tabindex="0", aria-label="Abschnitt N — klicken um Scan-Position anzuzeigen".
  • +
  • PDF strip toggle on mobile: role="button", aria-expanded="{expanded}", aria-label="Scan {expanded ? 'verkleinern' : 'vergrößern'}".
  • +
  • Scroll-sync animations respect prefers-reduced-motion: skip the 1.5s fade, apply instant highlight that disappears after 200ms.
  • +
+
+ +
+ +