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.
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.
/enrich. This caps block height so the dashboard stays navigable, and reinforces /enrich as the canonical "long queue" page.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.
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.
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.
Navigation affordance. aria-hidden="true". Becomes slightly more prominent on hover (opacity-30 → 70).
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.
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.length === 0)Render null. The block disappears entirely. Seniors don't need a "nothing to do" card — absence is clear.
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).
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.
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.
/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.| 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 |
1fr minus the DropZone sidebar — not the full viewport. At 1440px total width, the block lands around 900–960px wide.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 →" |
<section aria-labelledby="enrich-heading"> with an <h2 id="enrich-heading">. SR users reach the block via landmark nav.<ol>, not <div>. Upload time is the implicit order.aria-live="polite" so SR announces "12 Dokumente benötigen Metadaten" when the number changes after upload.focus-visible:ring-2 ring-brand-navy ring-inset. Outer ring would clip; inner ring is always visible against hover/active bg.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.prefers-reduced-motion (transition-duration 0.01ms).role="status" aria-live="polite"; manual dismiss button labeled aria-label="Benachrichtigung schließen".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.
dark:text-red-300 instead of text-red-600). Run axe-playwright in both themes per project convention./enrich (by upload date, by file type, by uploader).| 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