# Stammbaum — Relationship Badge · Document Detail

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

Familienarchiv · 2026-04-27 · Leonie Voss, UX Lead
Issue #358 Part C — Badge only Depends on PersonSummaryDTO.familyMember

## 1 · Design tokens

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

Light theme — bg-surface = #ffffff

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-hidden` 1.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)

Dark theme — bg-surface = #011526

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-hidden` non-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"`.

## 2 · Visual mockup — light & dark

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.

Light theme
Brief an die Eltern, 15. März 1923
15. März 1923
Details ▾
Bearbeiten
Details
Datum
15. März 1923
Ort
München
Status
Transkribiert
Personen
Von
KR
Karl Raddatz
An
HR
Heinrich Raddatz
Verwandtschaft
Sohn
Vater
Schlagwörter
Familie Krieg

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 theme
Brief an die Eltern, 15. März 1923
15. März 1923
Details ▾
Bearbeiten
Details
Datum
15. März 1923
Ort
München
Status
Transkribiert
Personen
Von
KR
Karl Raddatz
An
HR
Heinrich Raddatz
Verwandtschaft
Sohn
Vater
Schlagwörter
Familie Krieg

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.

## 3 · Edge cases — when the badge is silent

All three cases result in no "Verwandtschaft" row. The drawer's Personen column is unaffected.

Not a family member
Von
KR
Karl Raddatz
An
NE
N.N. Engel
— no Verwandtschaft row —
Receiver does not have `familyMember = true`. Inference endpoint is never called.
Multiple receivers
Von
KR
Karl Raddatz
An
HR
Heinrich Raddatz
ER
Elfriede Raddatz
— no Verwandtschaft row —
`receivers.length > 1`: badge is silently omitted. Multi-receiver documents are rare in this archive; revisit if data shows otherwise.
No kinship path found
Von
KR
Karl Raddatz
An
HR
Heinrich Raddatz
— no Verwandtschaft row —
Both are family members, but the backend returns `404` (no path in graph). `inferredRelationship` is set to `null`. No error shown.

## 4 · Data flow

`inferredRelationship` is loaded server-side and passed as a separate prop alongside the document — it is not added to the document object.

+page.server.ts
Loads document as today. Then, if `sender.familyMember && receivers.length === 1 && receivers[0].familyMember`, calls `GET /api/persons/{senderId}/relationship-to/{receiverId}`. 404 → null. Returns `{ document, inferredRelationship }`.
Modified
↓
+page.svelte
Receives `data.inferredRelationship`. Passes to `<DocumentTopBar>` as a new optional prop.
Modified
↓
DocumentTopBar.svelte
New optional prop `inferredRelationship?: { labelFromA: string; labelFromB: string } | null`. Passes through to `<DocumentMetadataDrawer>`.
Modified
↓
DocumentMetadataDrawer.svelte
New optional prop. Renders `{#if inferredRelationship} <RelationshipBadge .../> {/if}` at the bottom of the Personen column.
Modified
↓
RelationshipBadge.svelte — `src/lib/components/RelationshipBadge.svelte`
Props: `labelFromA: string`, `labelFromB: string`. Purely presentational — no logic, no API calls.
New
Conditional in +page.server.ts
`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.

## 5 · RelationshipBadge.svelte — exact markup

src/lib/components/RelationshipBadge.svelte

    <script lang="ts">
      import { m } from '$lib/paraglide/messages.js';

      type Props = { labelFromA: string; labelFromB: string };
      let { labelFromA, labelFromB }: Props = $props();
    </script>

    <div>
      <p class="mb-1 font-sans text-xs font-medium text-ink-3">
        {m.doc_details_field_relationship()}
      </p>
      <div class="flex items-center gap-1.5 px-2 font-serif text-sm text-ink">
        <span class="font-semibold">{labelFromA}</span>
        <svg
          class="h-3 w-3 shrink-0 text-accent"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2.5"
          aria-hidden="true"
        >
          <path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5-5 5M6 12h12" />
        </svg>
        <span class="font-semibold">{labelFromB}</span>
      </div>
    </div>

Placement in DocumentMetadataDrawer.svelte — Personen column, end of block

    <!-- existing receiver section -->
    {#if receivers.length > 0}
      <div>
        <p class="mb-1 font-sans text-xs font-medium text-ink-3">
          {m.doc_details_field_receivers()}
        </p>
        <!-- ... receiver cards ... -->
      </div>
    {/if}

    <!-- new: relationship badge —— after receivers -->
    {#if inferredRelationship}
      <RelationshipBadge
        labelFromA={inferredRelationship.labelFromA}
        labelFromB={inferredRelationship.labelFromB}
      />
    {/if}

## 6 · i18n

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

## 7 · Implementation notes

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.