Neue Route Frontend Backend

Dokumente-Seite — /documents

Dedicated search and browse page for all documents. Separates the document list from the dashboard hub. Uses per-year group cards with flat divide-y rows, a horizontal split row (content left · metadata right), a circular progress ring, and contributor avatars.

Spec · Leonie Voss · 2026-04-19 · Issue TBD

Design decisions

The hub (/) becomes pure dashboard — no more dual-mode switching. The "Documents" nav tab points to /documents, a focused search/browse page.

Row layout: two-column split — title and snippet occupy the full left column for maximum scan width; date, sender, receiver, archive location, progress ring and contributor avatars live in a fixed 240px right panel. This keeps metadata consistently positioned across all rows.

List structure: one white card container per year group (matching the current border border-line bg-surface shadow-sm pattern), rows separated by divide-y dividers — no gaps, no individual row cards. The year label is an inset header row within each card.

Progress ring shows work completion as a percentage (0–100%). It is driven by a new completionPercentage field on the search result DTO, computed server-side from annotation block counts. Contributor avatars require a new contributors array (initials + color) on the search DTO.


Section 1
Full page mockup — filter panel open, search active
Scaled at ~56%. Desktop 1200px concept width.
Hochlader
MR
31 documents Sort
Date ↓
Filters 1
Date range
From
To
Sender
Search person…
Receiver
Search person…
Tags BriefFotoPostkarteUrkunde
1924
Demo: Ierlicher Brief — Belgern
… Hiermit übersende ich Ihnen den gewünschten Brief meines Vaters, welcher einige interessante Hinweise zur Familiengeschichte enthält …
Brief
Familie
Date 31. Mai 1924
From Louise Aon Boden
To Marcel Raddatz
Archive Box 3 · Folder A
100%
MR
LS
1923
W-0614 – 8. September 1923 – Tölz
… Clara schreibt über die Ankunft in Tölz und erwähnt den letzten Brief von Fauld Rupley, der noch keine Antwort erhalten hat …
Brief
Date 8. Sept. 1923
From Clara Lam
To Fauld Rupley
Archive Box 1 · Folder C
75%
AK
W-0196 – 2. September 1923 – B. Lichterfelde
… Prediger's Haushaltung enthält einen Brief; Zusammen mit der Vollmacht aus dem Vorjahr ergibt sich folgendes Bild …
Brief
Date 2. Sept. 1923
From Müller de Gruym
To Herbert Cram
40%
MR
LS
AK
W-0397 – 2. September 1923 – B. Lichterfelde
… zum einleitend Kommentar hieraus, den Herrn, zum Brief az sechzig und weitere Passagen …
Brief
Date 2. Sept. 1923
From Müller de Gruym
0%
No contributors
Fig 1 — /documents · 1200px · search: "brief" · filter panel open · sort: Date ↓

Section 2
Page structure & zones

① Global search bar

Full-width row below the topbar. Contains the search input (flex-1), result count (right of input), and "+ New Document" button. Background white, bottom border border-line. Sticky — stays visible on scroll.

Same search bar pattern as the current homepage. Debounce 500 ms on text input; immediate on clear.

② Sort / count bar

Slim bar below search. Shows result count (left), sort dropdown (right), and Filters toggle button (far right). Background white, bottom border border-line. Sticky — stacks below search bar on scroll.

Filters button shows a mint badge with active filter count. When filters are open the button fills navy.

③ Collapsible filter panel

Drops open below the sort bar. Contains four groups: Date range (two inputs), Sender (PersonTypeahead), Receiver (PersonTypeahead), Tags (clickable pills). White background, bottom border border-line.

Closed by default on page load unless URL already has active filter params. Animate open/close with transition-all duration-200.


Section 2b
Mobile breakpoints
Three responsive tiers: <sm (mobile), sm–lg (tablet), lg+ (desktop).

< sm — < 640px (mobile)

Document row: single-column block. Left/right split collapses. Metadata (date, from, to, archive) moves below the tags row as a 2×2 compact grid. Progress ring and contributor stack appear in a bottom row directly below the grid.

Filter panel: single-column stack (flex-col). Sort bar wraps if needed.

sm – lg — 640–1023px

Document row: two-column split restored. Metadata column narrower: sm:w-48 (192px) instead of w-60 to fit tablet viewports.

Sticky bars span full width via negative margins. Filter panel: flex-row flex-wrap, groups can wrap.

lg+ — ≥ 1024px (desktop)

Full two-column split. Metadata column: lg:w-60 (240px). Filter panel: four groups in a single row. Max content width max-w-7xl (1280px) — from app layout container, no extra padding on list body.

Dokumente
MR
31 documents Sort
Date ↓
Filters
1924
Demo: Ierlicher Brief — Belgern
… Hiermit übersende ich Ihnen den gewünschten Brief
Brief
Date 31. Mai 1924
From L. von Boden
Archive Box 3 · A
To M. Raddatz
100%
MR
LS
W-0614 – Sept. 1923 – Tölz
… Clara schreibt über den letzten Brief von Fauld Rupley …
Brief
Date 8. Sept. 1923
From Clara Lam
To F. Rupley
75%
AK
Fig 2 — /documents · 375px mobile · search "brief" · filter closed

Mobile row — CSS-only approach

No JS needed. The <a> link is always block. On sm+ the inner element switches to flex items-stretch, showing the right metadata column (hidden sm:flex) and hiding the mobile compact grid (sm:hidden).

This means the DOM contains both layouts simultaneously — the metadata grid inside the left column (mobile only) and the right metadata panel (sm+ only). Both share the same data, just rendered differently.

Minimum touch target: the entire row is the <a>, guaranteed ≥44px on mobile given title + snippet + tags + metadata grid.


Section 3
Year group card
One card per year group. Rows inside use divide-y — no gaps between rows.

Card container

Matches current DocumentList outer container exactly: border border-line bg-surface shadow-sm. No border-radius (keeps it flush). Margin between consecutive year cards: mb-4.

Rendered only when sort = DATE. For other sort modes (SENDER, RECEIVER, TITLE) the year header is replaced by the relevant group label using the same card pattern.

Year header row

First child of each card. Background bg-sand, text text-xs font-bold uppercase tracking-widest text-ink-3. Height py-1.5 px-5. Bottom border border-b border-line.

Not a standalone divider — it is part of the card so the top border of the card frames the year label on three sides.


Section 4
Document row — two-column split

Left column — content

Flex-1, min-width 0. Padding p-4 pr-5. Right border border-r border-line-2.

Titlefont-serif text-base font-bold text-ink with search highlight underlines. mb-1.5.

Snippetfont-serif text-sm italic text-ink-2 line-clamp-2 mb-2 with highlight underlines. Only rendered when a match snippet is present.

Tags — existing tag pill pattern bg-muted text-ink text-[10px] font-bold uppercase tracking-widest rounded px-2 py-0.5. Gap gap-1.5 flex-wrap.

Right column — metadata panel

Fixed width w-60 (240px). Padding p-3.5. Flex column, justify-between.

Meta lines (top group) — font-sans text-[11px] text-ink-2 mb-1. Label: font-bold uppercase tracking-wide text-[10px] text-ink-3 mr-1.5. Lines: Date · From · To · Archive (Box · Folder). Archive only rendered when archiveBox is set.

Bottom row — flexbox, space-between. Left: progress ring. Right: ContributorStack.

Accessibility: The ring conveys progress by both percentage text and arc fill — not colour alone. Contributors show initials as text inside the avatar. Both pass the redundant-cue requirement from the Leonie Voss persona. Minimum touch target for the row link: the full row is the <a> element, always ≥44px tall given the content. Row hover: hover:bg-muted/50 transition-colors duration-200.

Section 5
Progress ring

Anatomy

SVG donut ring, 36×36px. Track circle: stroke="#E4E2D7" (stroke-brand-sand) width 3px. Fill arc: stroke="#A6DAD8" (stroke-accent) width 3px, stroke-linecap="round". Rotated −90° so arc starts at 12 o'clock.

Centre label: percentage text font-sans text-[8px] font-bold. Colour: mint (text-accent-dark) when >0%, gray-400 when 0%.

Circumference of r=13: 2π×13 ≈ 81.7px. Stroke-dasharray: {pct * 81.7} 81.7.

Data source — new API field

New field completionPercentage: number (0–100, integer) on the document search result DTO. Computed server-side:

round((reviewedBlocks / max(totalBlocks, 1)) * 100)

If a document has no annotation blocks yet (no transcription started), returns 0. Backend change: new subquery in the document search repository to COUNT annotation blocks (all vs. reviewed) per document, joined into the search projection.


Section 6
Contributor avatar stack

Anatomy

Reuse existing ContributorStack.svelte component (added in commit 031f6ea). Avatars 22×22px, -ml-1.5 overlap, white 2px border.

Show max 3 avatars. If more: +N text element in gray-400. When no contributors: render text-[9px] text-ink-3 uppercase tracking-wide label "No contributors".

Data source — new API field

New field contributors: ActivityActorDTO[] on the document search result DTO. ActivityActorDTO already exists (used in dashboard queue items): { initials: string, color: string, name?: string }.

Backend: join from document → annotation_blocks → created_by → users. Distinct by user. Order by most-recent contribution. Limit 4. New query in document search repository.


Section 7
Backend changes required
New fields on document search DTO — Two new fields must be added to the object returned by GET /api/documents/search. These require a new projection or join in the repository layer. No schema migration needed — purely computed from existing annotation_block data.
FieldTypeSourceNotes
completionPercentageint (0–100)COUNT(reviewed annotation blocks) / COUNT(all blocks)0 when no blocks exist
contributorsActivityActorDTO[]Distinct users with annotation_block contributions, ordered by recencyMax 4; reuse existing DTO
archiveBoxString?Already on Document entity — just not in search responseExpose existing field
archiveFolderString?Already on Document entity — just not in search responseExpose existing field

Section 8 — Implementation Reference
Exact Tailwind classes & pixel values
ElementTailwind classesPixels / valueNotes
Page chrome
Search bar wrapperbg-white border-b border-line -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-3.5 flex items-center gap-3 sticky top-[65px] z-20padding 14px responsiveTopbar = 1px accent + 64px nav = 65px. Negative margins break out of container padding so bar spans full container width.
Search inputflex-1 h-9 border border-ink rounded-sm px-3 font-sans text-sm text-ink bg-whiteheight 36pxActive: navy border
Sort bar wrapperbg-white border-b border-line -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-3 sticky top-[113px] z-20padding 10px responsiveStacks below search bar (65 + 48 = 113px)
Filters toggle (closed)h-7 px-3 border border-line rounded-sm font-sans text-[10px] font-bold uppercase tracking-wide text-ink flex items-center gap-1.5height 28px
Filters toggle (open)h-7 px-3 bg-ink text-white rounded-sm font-sans text-[10px] font-bold uppercase tracking-wide flex items-center gap-1.5height 28pxNavy fill when active
Filter panel wrapperbg-white border-b border-line -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-4 flex flex-col sm:flex-row sm:flex-wrap gap-4padding 16px responsiveUse Svelte slide transition; stacks vertically on mobile
List bodypy-5vertical padding onlyNo extra horizontal padding — app container handles it
Year group card
Card containerborder border-line bg-surface shadow-sm mb-4 overflow-hiddenMatches current DocumentList outer div exactly
Year headerbg-sand border-b border-line px-5 py-1.5 font-sans text-[10px] font-bold uppercase tracking-widest text-ink-3padding 6px 20px
Row listdivide-y divide-line-2Matches current <ul> pattern
Document row
Row wrapper <li>group transition-colors duration-200 hover:bg-muted/50Same hover pattern as current
Row inner (link)block sm:flex sm:items-stretchFull-row <a href="/documents/{id}">; flex only on sm+
Left columnp-4 sm:flex-1 sm:min-w-0 sm:pr-5 sm:border-r sm:border-line-2padding 16pxRight border only on sm+
Right column (sm+)hidden sm:flex sm:w-48 lg:w-60 flex-shrink-0 p-3.5 flex-col justify-between gap-2sm: 192px · lg: 240pxHidden on mobile; narrower on tablet
Mobile metadata gridsm:hidden border-t border-line-2 mt-3 pt-3 grid grid-cols-2 gap-x-4 gap-y-0.52×2 compact grid shown only on mobile, inside left col
Mobile meta bottom rowsm:hidden flex items-center justify-between mt-3Ring + contributors on mobile, shown only <sm
Document titlefont-serif text-base font-bold text-ink mb-1.5 leading-snug group-hover:underline16px / 700
Snippet textfont-serif text-sm italic text-ink-2 line-clamp-2 mb-214pxOnly when snippet present
Meta labelfont-sans text-[10px] font-bold uppercase tracking-wide text-ink-3 mr-1.510px / 700DATE · FROM · TO · ARCHIVE
Meta valuefont-sans text-[11px] text-ink-211px
Progress ring
SVG containerrelative w-9 h-9 flex-shrink-036×36px
Track circlestroke="var(--c-sand)" stroke-width="3"r=13, circumference 81.7px
Fill arcstroke="var(--c-accent)" stroke-width="3" stroke-linecap="round"dasharray = pct/100 × 81.7rotate(−90deg)
Percentage labelabsolute inset-0 flex items-center justify-center font-sans text-[8px] font-bold8px / 800Mint when >0, gray-400 when 0
New files
NEW frontend/src/routes/documents/+page.svelteDocument list page (extract from homepage)
NEW frontend/src/routes/documents/+page.server.tsLoads search results, same API call as current homepage
CHANGED frontend/src/routes/AppNav.svelteDocuments tab href: //documents
CHANGED frontend/src/routes/+page.svelteRemove dual-mode logic; always render dashboard
CHANGED frontend/src/routes/+page.server.tsRemove search branch; always fetch dashboard data
CHANGED frontend/src/routes/DocumentList.svelteRefactor to new two-column layout + year cards
NEW query backend/.../DocumentSearchRepositoryAdd completionPercentage + contributors to search projection