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.
FAMILIENARCHIV
Documents Persons Letters Admin
Hochlader
MR
brief
31 Dokumente
+ New Document
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`.

Routing: New route `frontend/src/routes/documents/+page.svelte` and `+page.server.ts`. AppNav "Documents" tab `href` changes from `/` to `/documents`. Homepage `+page.svelte` loses dual-mode — always renders dashboard. No redirect from `/?q=…`.

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.

FAMILIENARCHIV
Dokumente
MR
brief
31 Dok.
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`.

Title — `font-serif text-base font-bold text-ink` with search highlight underlines. `mb-1.5`.

Snippet — `font-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.

Field | Type | Source | Notes
--- | --- | --- | ---
`completionPercentage` | `int` (0–100) | COUNT(reviewed annotation blocks) / COUNT(all blocks) | 0 when no blocks exist
`contributors` | `ActivityActorDTO[]` | Distinct users with annotation_block contributions, ordered by recency | Max 4; reuse existing DTO
`archiveBox` | `String?` | Already on Document entity — just not in search response | Expose existing field
`archiveFolder` | `String?` | Already on Document entity — just not in search response | Expose existing field

Section 8 — Implementation Reference
Exact Tailwind classes & pixel values

Element | Tailwind classes | Pixels / value | Notes
--- | --- | --- | ---
Page chrome
Search bar wrapper | `bg-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-20` | padding 14px responsive | Topbar = 1px accent + 64px nav = 65px. Negative margins break out of container padding so bar spans full container width.
Search input | `flex-1 h-9 border border-ink rounded-sm px-3 font-sans text-sm text-ink bg-white` | height 36px | Active: navy border
Sort bar wrapper | `bg-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-20` | padding 10px responsive | Stacks 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.5` | height 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.5` | height 28px | Navy fill when active
Filter panel wrapper | `bg-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-4` | padding 16px responsive | Use Svelte `slide` transition; stacks vertically on mobile
List body | `py-5` | vertical padding only | No extra horizontal padding — app container handles it
Year group card
Card container | `border border-line bg-surface shadow-sm mb-4 overflow-hidden` | — | Matches current DocumentList outer div exactly
Year header | `bg-sand border-b border-line px-5 py-1.5 font-sans text-[10px] font-bold uppercase tracking-widest text-ink-3` | padding 6px 20px | —
Row list | `divide-y divide-line-2` | — | Matches current `<ul>` pattern
Document row
Row wrapper `<li>` | `group transition-colors duration-200 hover:bg-muted/50` | — | Same hover pattern as current
Row inner (link) | `block sm:flex sm:items-stretch` | — | Full-row `<a href="/documents/{id}">`; flex only on sm+
Left column | `p-4 sm:flex-1 sm:min-w-0 sm:pr-5 sm:border-r sm:border-line-2` | padding 16px | Right 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-2` | sm: 192px · lg: 240px | Hidden on mobile; narrower on tablet
Mobile metadata grid | `sm:hidden border-t border-line-2 mt-3 pt-3 grid grid-cols-2 gap-x-4 gap-y-0.5` | — | 2×2 compact grid shown only on mobile, inside left col
Mobile meta bottom row | `sm:hidden flex items-center justify-between mt-3` | — | Ring + contributors on mobile, shown only <sm
Document title | `font-serif text-base font-bold text-ink mb-1.5 leading-snug group-hover:underline` | 16px / 700 | —
Snippet text | `font-serif text-sm italic text-ink-2 line-clamp-2 mb-2` | 14px | Only when snippet present
Meta label | `font-sans text-[10px] font-bold uppercase tracking-wide text-ink-3 mr-1.5` | 10px / 700 | DATE · FROM · TO · ARCHIVE
Meta value | `font-sans text-[11px] text-ink-2` | 11px | —
Progress ring
SVG container | `relative w-9 h-9 flex-shrink-0` | 36×36px | —
Track circle | `stroke="var(--c-sand)"` stroke-width="3" | r=13, circumference 81.7px | —
Fill arc | `stroke="var(--c-accent)"` stroke-width="3" stroke-linecap="round" | dasharray = pct/100 × 81.7 | rotate(−90deg)
Percentage label | `absolute inset-0 flex items-center justify-center font-sans text-[8px] font-bold` | 8px / 800 | Mint when >0, gray-400 when 0
New files
NEW `frontend/src/routes/documents/+page.svelte` | — | — | Document list page (extract from homepage)
NEW `frontend/src/routes/documents/+page.server.ts` | — | — | Loads search results, same API call as current homepage
CHANGED `frontend/src/routes/AppNav.svelte` | — | — | Documents tab href: `/` → `/documents`
CHANGED `frontend/src/routes/+page.svelte` | — | — | Remove dual-mode logic; always render dashboard
CHANGED `frontend/src/routes/+page.server.ts` | — | — | Remove search branch; always fetch dashboard data
CHANGED `frontend/src/routes/DocumentList.svelte` | — | — | Refactor to new two-column layout + year cards
NEW query `backend/.../DocumentSearchRepository` | — | — | Add completionPercentage + contributors to search projection