DASHBOARD NEW BLOCK MOBILE-FIRST

# Ergänzungs-Liste — Dashboard-Block

Familienarchiv · 2026-04-20 · Leonie Voss, UX Lead

## The call

A dedicated list-block on the dashboard, placed between the Resume strip and the MissionControlStrip, surfaces the documents that were just uploaded and still need metadata. It replaces the orphaned `DashboardNeedsMetadata.svelte` component with a spec'd, senior-friendly row design.

Why here, not a 4th column in MissionControlStrip: (1) strip is already at 3-column visual capacity; (2) batch-upload reality (10–15 docs arriving at once) makes a count tile useless — seniors need to see which docs are waiting; (3) placing it directly under the DropZone sightline makes upload→enrich the most visually-coupled pair on the page.

Scope of this spec: the block itself (layout, row anatomy, empty/loading/error states, a11y, responsive behavior at 320/768/1440). The post-upload success banner is included as a coupled interaction. The strip redesign, pagination, and the other three pipeline stages (segment / transcribe / review) are not in scope — they earn their own specs.

01 — Block anatomy

## Three pieces: eyebrow, list, footer

The block is a single `<section>` landmark with an eyebrow heading, a divided list of up to 5 rows, and an optional footer link when more than 5 docs are pending. At `incompleteDocs.length === 0` the block renders nothing — no "all clear" state, no empty card. A clean dashboard means no work to do.

Benötigen Metadaten 12
PDF
Brief Oma Hilde 1962
vor 2 Min.
›
PDF
Geburtsurkunde Opa
vor 2 Min.
›
JPG
Foto Hochzeit Tante Elsa
vor 2 Min.
›
PDF
Postkarte Bodensee
vor 2 Min.
›
PDF
Meldebescheinigung 1971
vor 3 Min.
›
320px · 12 docs pending · 5 shown, rest via footer link to /enrich
Benötigen Metadaten 12
PDF
Brief Oma Hilde 1962 — An meinen lieben Ludwig
vor 2 Minuten hochgeladen · 3 Seiten
›
PDF
Geburtsurkunde Opa Friedrich
vor 2 Minuten hochgeladen · 1 Seite
›
JPG
Foto Hochzeit Tante Elsa — Standesamt Ulm
vor 2 Minuten hochgeladen
›
PDF
Postkarte Bodensee 1958
vor 2 Minuten hochgeladen · 2 Seiten
›
TIF
Meldebescheinigung Stadt Tübingen 1971
vor 3 Minuten hochgeladen · 1 Seite
›
1440px · main content column (768–960px target) · 12 docs pending · row 2 shows keyboard focus state
Row cap = 5. When pending docs exceed 5, the block shows the 5 most-recently-uploaded and a footer link to `/enrich`. This caps block height so the dashboard stays navigable, and reinforces `/enrich` as the canonical "long queue" page.
02 — Row anatomy

## Icon · title · upload time · chevron

The entire row is one `<a href="/enrich/{id}">`. No nested interactive elements — one tap, one destination. Touch target: 72px minimum row height on mobile, 64px on desktop. Both exceed the 48px WCAG 2.2 AA floor for the senior audience.

### Left: file-type badge (20×20px)

Visual differentiation between PDF / JPG / PNG / TIF. Not decorative — gives seniors a scannable category cue without reading. Uses redundant color + text ("PDF", "JPG") so it passes WCAG 1.4.1.

### Center: title + upload time

Title in `font-serif text-base` (16px mobile) / `text-lg` (18px desktop). Truncated with `text-ellipsis` on narrow viewports. Relative time ("vor 2 Min.") in `font-sans text-xs text-ink-2`.

### Right: chevron (decorative)

Navigation affordance. `aria-hidden="true"`. Becomes slightly more prominent on hover (`opacity-30 → 70`).

### States: hover, focus-visible, active

Hover: `bg-brand-mint/10` wash. Focus-visible: `ring-2 ring-brand-navy ring-offset-2` inside the row (visible against row bg). Active: `bg-brand-mint/20`.

DTO extension needed. Current `IncompleteDocumentDTO` only carries `id` and `title`. To render "vor 2 Min." and the file-type badge, add `uploadedAt: Instant` and `mimeType: String`. Small, cheap backend change. Without these fields the block still works (skip meta line, use a generic doc icon) but the senior-facing UX is meaningfully worse.
03 — States

## Empty · loading · error · after-upload

### Empty (`length === 0`)

Render `null`. The block disappears entirely. Seniors don't need a "nothing to do" card — absence is clear.

(block not rendered)

### Loading

On initial dashboard SSR this data comes in with the page load — no spinner needed. If you later add client-side refresh, use a skeleton (3 rows of gray blocks at 72px height, `animate-pulse`, respects `prefers-reduced-motion`).

### Error

If the `/api/documents/incomplete` call fails, render the block in a muted error state: eyebrow reads "Liste konnte nicht geladen werden", with a retry link. Do not suppress — the user needs to know their queue may be out of date.

### After-upload (immediately after DropZone fires)

Transient success banner above the block. Auto-dismiss after 8s, with a manual close X. Content: "12 Dokumente hochgeladen" + "Jetzt ergänzen →" CTA → scrolls/focuses the list block. Use `aria-live="polite"` so screen readers announce the count.

✓
12 Dokumente hochgeladen. Jetzt ergänzen, damit sie durchsuchbar werden.
Ergänzen → ×
After-upload success banner · renders above the list block · auto-dismiss 8s · manual dismiss always available
No auto-redirect. After a batch of 12 uploads, dumping the user into `/enrich/{firstId}` is disorienting. The banner gives them the moment without taking control away. The list block becomes the persistent landing spot for every return visit.
04 — Responsive behavior

## Mobile-first, three breakpoints

Breakpoint | Row height | Title size | Meta visibility | Padding
--- | --- | --- | --- | ---
320px | 72px | text-base (16px) | Relative time only | px-3 py-3
768px | 72px | text-lg (18px) | Time + page count | px-5 py-4
1440px | 64px | text-lg (18px) | Time + page count | px-6 py-4

Block width follows the main content column. On the dashboard grid this is `1fr` minus the DropZone sidebar — not the full viewport. At 1440px total width, the block lands around 900–960px wide.
05 — Implementation reference

## Exact Tailwind classes & pixel values

Rewire and extend `frontend/src/lib/components/DashboardNeedsMetadata.svelte`. The component already exists and is correctly typed — the changes are: new row anatomy, file-type icon, relative time, 5-item cap with footer, a11y landmark, and focus/hover states.

Region | Tailwind | Pixel values | Notes
--- | --- | --- | ---
Section wrapper | `<section aria-labelledby="enrich-heading" class="mb-6">` | margin-bottom 24px | Landmark for SR nav
Eyebrow row | `flex items-center justify-between mb-3 px-1` | mb 12px | Title + count badge
Eyebrow heading | `text-xs font-bold uppercase tracking-widest text-gray-500` | 12px / 700 | Not gray-400 — must pass AA on sand bg
Count badge | `bg-brand-navy text-white text-xs font-bold px-2 py-0.5 rounded-full` | 12px / 700 | Use `aria-live="polite"`
List container | `bg-white border border-line rounded-sm shadow-sm overflow-hidden` | border 1px, radius 2px | Matches card pattern
List element | `<ol class="divide-y divide-line-2">` | divider 1px | Ordered — upload time is the order
Row link | `group flex items-center gap-3 px-3 py-3 md:px-5 md:py-4 min-h-[72px] lg:min-h-[64px] hover:bg-brand-mint/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-inset transition-colors` | min-height 72px mobile / 64px desktop | Whole row is the <a>
File-type badge | `shrink-0 w-5 h-5 md:w-6 md:h-6 rounded-sm flex items-center justify-center text-[10px] font-bold` | 20×20px mobile / 24×24 desktop | Color per mime: red/PDF, green/JPG|PNG, purple/TIF
Title | `font-serif text-base md:text-lg text-ink group-hover:underline truncate` | 16px mobile / 18px desktop | Single-line truncate
Meta line | `font-sans text-xs text-ink-2 mt-0.5` | 12px | "vor N Min./Std." + optional "· N Seiten"
Chevron | `shrink-0 w-5 h-5 opacity-30 group-hover:opacity-70 transition-opacity` | 20×20px | Use `aria-hidden="true"`
Footer (when >5) | `border-t border-line bg-brand-sand/20 px-5 py-3 flex justify-end` | py 12px | Only render when `length > 5`
Footer link | `font-sans text-xs font-bold uppercase tracking-widest text-brand-navy hover:underline` | 12px / 700 | "Alle {n} anzeigen →"

06 — Accessibility (WCAG 2.2 AA)

## What axe-playwright must confirm

#### A11y contract

- Landmark: `<section aria-labelledby="enrich-heading">` with an `<h2 id="enrich-heading">`. SR users reach the block via landmark nav.
- Ordered list: `<ol>`, not `<div>`. Upload time is the implicit order.
- Count live region: count badge gets `aria-live="polite"` so SR announces "12 Dokumente benötigen Metadaten" when the number changes after upload.
- Touch target: 72px row height on mobile ≫ WCAG 2.2 floor of 48px. The whole row is tappable, not just the title.
- Focus visibility: `focus-visible:ring-2 ring-brand-navy ring-inset`. Outer ring would clip; inner ring is always visible against hover/active bg.
- File-type cue: color plus text ("PDF", "JPG") — passes 1.4.1 (not color alone).
- Contrast: title `text-ink` on white = 14.5:1 (AAA). Meta `text-ink-2` on white — must verify ≥4.5:1 in both light and dark mode.
- Reduced motion: skeleton pulse and hover transitions respect `prefers-reduced-motion` (transition-duration 0.01ms).
- Banner: `role="status" aria-live="polite"`; manual dismiss button labeled `aria-label="Benachrichtigung schließen"`.

07 — Dark mode

## Works via semantic tokens, verify contrast separately

All colors come from existing tokens (`bg-white`, `text-ink`, `text-ink-2`, `border-line`, `bg-surface`). No hard-coded hex values. Dark mode inherits the remapped tokens.

Verify in dark mode: the file-type badges use mime-specific colors (red/green/purple) against white—in dark mode those same colors sit on a near-black background. Test each badge at its dark-mode contrast ratio; some may need lightness adjustment (e.g. `dark:text-red-300` instead of `text-red-600`). Run axe-playwright in both themes per project convention.
08 — Explicitly out of scope

## What this spec does NOT cover

Future work (separate specs):

- List-block treatment for the other pipeline stages (segment, transcribe, review). If this pattern works, they likely graduate out of MissionControlStrip too.
- Bulk-enrich ("same sender for all", "apply these 3 tags to all") — a natural follow-up for batch-upload reality.
- Sort/filter on `/enrich` (by upload date, by file type, by uploader).
- Strip redesign once a 4th+ pipeline stage forces the issue.
- Thumbnails in rows (deferred — depends on backend preview generation).

09 — Test plan

## What QA (Sara) will verify

Test | What it verifies
--- | ---
Vitest: component renders | With 0 docs, renders nothing. With 1–5, no footer. With 6+, footer link shows "Alle {n} anzeigen".
Vitest: count badge | Badge reflects `incompleteDocs.length`, not capped list length.
Playwright: axe, light mode | Dashboard passes a11y with block populated. Focus ring visible on keyboard nav through rows.
Playwright: axe, dark mode | Same as above with `[data-theme="dark"]`. File-type badge contrast ratios verified.
Playwright: 320/768/1440 screenshots | Block renders at all three breakpoints without overflow or truncation beyond title.
Playwright: upload → banner → click CTA | After DropZone fires with 3+ files: banner appears, list block populates, CTA scrolls/focuses list. Banner auto-dismisses at 8s. Manual X dismisses immediately.
Playwright: keyboard flow | Tab from DropZone reaches banner (if present), then list rows in order, then footer link. Enter on row navigates to `/enrich/{id}`.

Leonie Voss · UX Lead · Familienarchiv · 2026-04-20