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
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.

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

BreakpointRow heightTitle sizeMeta visibilityPadding
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
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.

RegionTailwindPixel valuesNotes
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

09 — Test plan

What QA (Sara) will verify

TestWhat it verifies
Vitest: component rendersWith 0 docs, renders nothing. With 1–5, no footer. With 6+, footer link shows "Alle {n} anzeigen".
Vitest: count badgeBadge reflects incompleteDocs.length, not capped list length.
Playwright: axe, light modeDashboard passes a11y with block populated. Focus ring visible on keyboard nav through rows.
Playwright: axe, dark modeSame as above with [data-theme="dark"]. File-type badge contrast ratios verified.
Playwright: 320/768/1440 screenshotsBlock renders at all three breakpoints without overflow or truncation beyond title.
Playwright: upload → banner → click CTAAfter 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 flowTab 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