Spec for Part C of issue #358. A "Verwandtschaft" metadata row appears inside the
metadata drawer's Personen column when a family kinship can be inferred between the document's sender
and its single receiver. Implemented as a new presentational component
RelationshipBadge.svelte.
All colour values used by the badge and its context. The component uses only semantic Tailwind tokens — no hardcoded hex. Light and dark themes are handled automatically by layout.css.
| text-ink-3 | #6b7280 — "Verwandtschaft" label4.8:1 AA ✓ |
| text-ink | #012851 — label values ("Sohn", "Vater")14.5:1 AAA ✓ |
| text-accent | #a1dcd8 — decorative arrow aria-hidden1.5:1 — non-text only |
| font-serif | Tinos — used for label values (matches person card names in same column) |
| font-sans | Montserrat — used for "Verwandtschaft" label (matches "Von" / "An" labels above) |
| text-ink-3 | #8b97a5 — "Verwandtschaft" label7.1:1 AAA ✓ |
| text-ink | #f0efe9 — label values ("Sohn", "Vater")14.5:1 AAA ✓ |
| text-accent | #00c7b1 — decorative arrow aria-hiddennon-text only |
| bg-surface | #011526 — drawer background in dark mode |
⚠ text-accent (#a1dcd8 light / #00c7b1 dark) must only be used for the decorative arrow SVG.
It fails WCAG at small text sizes. The arrow carries no information — it is purely visual and is marked aria-hidden="true".
The badge renders as the last item in the Personen column of the expandable metadata drawer. Shown at ~65% scale. Drawer is open. Both themes side by side.
Drawer open. "Verwandtschaft" row sits below the single receiver card, using identical label and value typography to "Von" / "An". Arrow is mint accent, aria-hidden.
Dark mode. Same semantic tokens; surface flips to #011526, ink to #f0efe9, accent arrow to #00c7b1 (turquoise). Both label and value pass WCAG AAA on the dark surface.
All three cases result in no "Verwandtschaft" row. The drawer's Personen column is unaffected.
familyMember = true. Inference endpoint is never called.receivers.length > 1: badge is silently omitted. Multi-receiver documents are rare in this archive; revisit if data shows otherwise.404 (no path in graph). inferredRelationship is set to null. No error shown.inferredRelationship is loaded server-side and passed as a separate prop alongside the document — it is not added to the document object.
sender.familyMember && receivers.length === 1 && receivers[0].familyMember, calls GET /api/persons/{senderId}/relationship-to/{receiverId}. 404 → null. Returns { document, inferredRelationship }.
data.inferredRelationship. Passes to <DocumentTopBar> as a new optional prop.
inferredRelationship?: { labelFromA: string; labelFromB: string } | null. Passes through to <DocumentMetadataDrawer>.
{#if inferredRelationship} <RelationshipBadge .../> {/if} at the bottom of the Personen column.
src/lib/components/RelationshipBadge.sveltelabelFromA: string, labelFromB: string. Purely presentational — no logic, no API calls.
sender.familyMember requires PersonSummaryDTO.familyMember: boolean on the backend (part of issue #358 backend work). Until that field ships, the condition evaluates to false and the badge never renders — silent, correct fallback. No feature flag needed.
One new key. The relationship label strings (labelFromA / labelFromB) come pre-translated from the backend — no additional frontend keys needed for them.
| Key | de (default) | en | es |
|---|---|---|---|
| doc_details_field_relationship | Verwandtschaft | Relationship | Parentesco |
| Arrow is decorative | The SVG arrow carries no semantic information — the directional meaning is already in the label order (sender's label first, receiver's label second). Always aria-hidden="true". Screen readers announce: "Verwandtschaft: Sohn. Vater." which is unambiguous. |
| text-accent on arrow | Use text-accent (maps to #a1dcd8 light / #00c7b1 dark) only on the arrow SVG stroke. Never on any text element. Both values fail WCAG at body text sizes — they are only safe for non-text decorative elements. |
| No logic in component | RelationshipBadge.svelte has no conditional logic and no API calls. All conditions (familyMember flags, receiver count, 404 handling) live in +page.server.ts. If inferredRelationship is non-null, the badge renders — full stop. |
| Backend prerequisite | PersonSummaryDTO must expose familyMember: boolean. Until it ships, sender.familyMember is undefined → condition is false → no API call made → badge silently absent. No code changes needed on both sides of the ship date. |
| Multiple receivers | Check receivers.length === 1 in +page.server.ts before calling the inference endpoint. Do not call it for 0 or 2+ receivers. Badge is absent on multi-receiver documents regardless of family membership. |
| Prop threading | inferredRelationship is typed as { labelFromA: string; labelFromB: string } | null on DocumentTopBar and DocumentMetadataDrawer. The path field from InferredRelationshipDTO is loaded in the server but not passed to the component — reserved for a future tooltip. |
| Future tooltip | The inference path (data.inferredRelationship.path) is available server-side and can be surfaced as a tooltip on the badge in a follow-up. No design work needed now — just thread the prop when the time comes. |