From d9b7b7aad47698dc519ea1bb3abfc61b1ed47801 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 20 Apr 2026 20:02:51 +0200 Subject: [PATCH] docs(specs): add enrichment list-block dashboard spec Design spec for a dashboard widget that surfaces documents needing metadata after batch upload. Placed between Resume strip and MissionControlStrip rather than as a 4th strip column (strip at visual capacity; batch reality makes count tiles useless for seniors). Covers responsive behavior at 320/768/1440, row anatomy with 72/64px touch targets, state matrix (empty/loading/error/ after-upload), full a11y contract, dark-mode verification notes, and an impl-ref table with exact Tailwind classes. Refs #296 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/specs/enrichment-list-block-spec.html | 577 +++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 docs/specs/enrichment-list-block-spec.html diff --git a/docs/specs/enrichment-list-block-spec.html b/docs/specs/enrichment-list-block-spec.html new file mode 100644 index 00000000..40dc858e --- /dev/null +++ b/docs/specs/enrichment-list-block-spec.html @@ -0,0 +1,577 @@ + + + + + +Ergänzungs-Liste — Dashboard-Block · Spec (Issue t.b.d.) + + + +
+ + +
+
+ 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
320px72pxtext-base (16px)Relative time onlypx-3 py-3
768px72pxtext-lg (18px)Time + page countpx-5 py-4
1440px64pxtext-lg (18px)Time + page countpx-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 24pxLandmark for SR nav
Eyebrow rowflex items-center justify-between mb-3 px-1mb 12pxTitle + count badge
Eyebrow headingtext-xs font-bold uppercase tracking-widest text-gray-50012px / 700Not gray-400 — must pass AA on sand bg
Count badgebg-brand-navy text-white text-xs font-bold px-2 py-0.5 rounded-full12px / 700Use aria-live="polite"
List containerbg-white border border-line rounded-sm shadow-sm overflow-hiddenborder 1px, radius 2pxMatches card pattern
List element<ol class="divide-y divide-line-2">divider 1pxOrdered — upload time is the order
Row linkgroup 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-colorsmin-height 72px mobile / 64px desktopWhole row is the <a>
File-type badgeshrink-0 w-5 h-5 md:w-6 md:h-6 rounded-sm flex items-center justify-center text-[10px] font-bold20×20px mobile / 24×24 desktopColor per mime: red/PDF, green/JPG|PNG, purple/TIF
Titlefont-serif text-base md:text-lg text-ink group-hover:underline truncate16px mobile / 18px desktopSingle-line truncate
Meta linefont-sans text-xs text-ink-2 mt-0.512px"vor N Min./Std." + optional "· N Seiten"
Chevronshrink-0 w-5 h-5 opacity-30 group-hover:opacity-70 transition-opacity20×20pxUse aria-hidden="true"
Footer (when >5)border-t border-line bg-brand-sand/20 px-5 py-3 flex justify-endpy 12pxOnly render when length > 5
Footer linkfont-sans text-xs font-bold uppercase tracking-widest text-brand-navy hover:underline12px / 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 +

+ +
+ +