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.
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.
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.
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.
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.
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=….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.
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.
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.
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.
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.
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.
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.
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.
<a> element, always ≥44px tall given the content. Row hover: hover:bg-muted/50 transition-colors duration-200.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.
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.
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".
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.
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 |
| 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 |