Commit Graph

1080 Commits

Author SHA1 Message Date
Marcel
bbde9e8497 refactor(person-mention): rename shadowed m param in TranscriptionBlock bind setter
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m20s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 2m55s
CI / Unit & Component Tests (pull_request) Failing after 3m7s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 2m58s
Same fix as 79349644 — the bind:mentionedPersons setter parameter `m`
shadowed the imported Paraglide m helper used two lines later in
placeholder={m.transcription_block_placeholder()}. Functionally fine
because the inner scope ends before the outer reference, but a clarity
trap. Renamed to next.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:02:13 +02:00
Marcel
793496440c refactor(person-mention): rename shadowed Paraglide m variable in dedup check
Felix #1: inside selectPerson the .some((m) => ...) parameter shadowed the
imported Paraglide m helper. Functionally fine, but a footgun. Rename to
existing for clarity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:50:35 +02:00
Marcel
e3175f493c test(transcription): backfill mentionedPersons on missed read-view fixture
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m18s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 3m5s
CI / Unit & Component Tests (pull_request) Failing after 3m10s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 3m7s
The b2 fixture in the second describe block had been missed when the
TranscriptionBlockData type added the mentionedPersons field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:39:45 +02:00
Marcel
64a61f705c feat(transcription): handle 409 rename-mid-edit conflict on block save (B12b)
When PersonService renames a person while a transcriber is editing a
block that mentions them, the block-save endpoint returns 409 (carrying
the new ErrorCode.PERSON_RENAME_CONFLICT from PR-A). saveBlock now:

1. Refetches the latest server snapshot of the block.
2. Calls mergeBlockOnConflict to combine: server's mentionedPersons
   (post-rename displayNames win) + transcriber's unsaved text + any
   local-only mentions added since the last save.
3. Updates the local block state with the merged result.
4. Re-throws so the autosave indicator surfaces the conflict and the
   pending payload is preserved for retry (B12).

The merge logic is a pure function so it can be unit-tested in
isolation and reused for any future conflict-resolution scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:35:27 +02:00
Marcel
e50aab2578 test(autosave): preserve text + mentionedPersons across save failure (B12)
Locks in the behaviour added with the saveFn signature widening: a
rejected save keeps the in-flight payload around so handleRetry resends
it without the caller having to re-pass anything.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:33:35 +02:00
Marcel
02d3e2ab61 feat(transcription): swap plain textarea for PersonMentionEditor and thread mentionedPersons through autosave
- TranscriptionBlockData now carries mentionedPersons (matches backend
  schema added in PR-A).
- useBlockAutoSave.saveFn signature widens to (blockId, text, mentions);
  pendingMentions is tracked alongside pendingTexts and is preserved on
  failure so a retry resends the in-flight payload (B12).
- TranscriptionBlock.svelte renders <PersonMentionEditor>, exposing the
  textarea node back through a captureTextarea callback so the existing
  quote-selection feature still works.
- saveBlock in routes/documents/[id]/+page.svelte forwards mentions on
  PUT.
- flushOnUnload sends mentions in the keepalive payload too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:32:09 +02:00
Marcel
c4ee2c666b feat(transcription): add PersonMentionEditor with typeahead + keyboard nav
Mirrors MentionEditor for users but searches /api/persons?q=, allows
multi-word queries (delegated to detectPersonMention), displays life
dates next to each result, and uses min-h-[44px] rows for WCAG 2.2 AA
touch targets. Selection writes both the @DisplayName text and a
{personId, displayName} sidecar entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:22:30 +02:00
Marcel
bf8fb00dd2 i18n(person-mention): add 5 locale keys for editor + read-mode
Adds the 3 keys mandated by the plan (open_link, hover_hint, load_error)
plus the editor's popup_empty + btn_label so PersonMentionEditor mirrors
the existing user-mention editor's i18n pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:05:51 +02:00
Marcel
b3ce15f0dd feat(mention): add detectPersonMention with multi-word query support
Comment mentions stop at a space; person mentions must accept spaces
because historical display names are commonly multi-word.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:03:20 +02:00
Marcel
c7013f4902 refactor(mention): extract shared escapeHtml helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:02:03 +02:00
Marcel
8b498665df chore(frontend): regenerate api.ts for PersonMention sidecar + PERSON_RENAME_CONFLICT
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m12s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Failing after 3m10s
CI / Unit & Component Tests (pull_request) Failing after 3m8s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m4s
openapi-typescript regenerated against the dev backend now exposes:

- components.schemas.PersonMention with personId + displayName
- TranscriptionBlock and CreateTranscriptionBlockDTO/UpdateTranscriptionBlockDTO
  carry the optional mentionedPersons array
- (No new path entries: hover-card and typeahead reuse existing endpoints
  GET /api/persons, GET /api/persons/{id}, GET /api/persons/{id}/relationships.)

Sealed inside PR-A so the frontend PR-B can import the new types from main
without rebasing across an unrelated regen. Per Tobias' chain-tightening
note in the consolidation summary.

Refs #362

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:10:54 +02:00
Marcel
4bc4267e5a feat(person): ErrorCode.PERSON_RENAME_CONFLICT for optimistic-lock conflicts
Adds the structured error code returned when a rename rolls back because a
referenced transcription block was edited concurrently (OptimisticLockException
on transcription_blocks.version). Mirrors the contract in
frontend src/lib/errors.ts and adds the localised message keys
error_person_rename_conflict in de/en/es so the UI surfaces a retry hint
instead of a generic 500.

The actual translation of OptimisticLockException → DomainException
(PERSON_RENAME_CONFLICT) lands in the next commit alongside the integration
test that proves the rollback semantics.

Refs #362

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:33:06 +02:00
Marcel
cb93f55396 refactor(stammbaum): StammbaumSidePanel composes AddRelationshipForm — removes inline form duplication
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m6s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 2m54s
CI / Unit & Component Tests (push) Failing after 3m2s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Has been cancelled
Replaces the 86-line duplicated inline add-relationship form with
<AddRelationshipForm onSubmit={handleAddRelationship}>. The {#key node.id}
wrapper resets the form's open state when the selected tree node changes.
Year inputs now have <label> elements (WCAG 1.3.1) via the shared component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
3cfaae06da feat(stammbaum): AddRelationshipForm accepts onSubmit callback prop for fetch-based submission
When onSubmit is provided the form has no server action and calls the
callback with typed RelFormData instead. Uses a shared {#snippet} for
the form body so the two submission paths share one template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
10b1bab57b fix(stammbaum): state-aware aria-label on family-member toggle — WCAG accessible name
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
000333d540 fix(stammbaum): WCAG text-[10px] → text-xs in PersonRelationshipsCard chip labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
5817a79151 test(stammbaum): year-range validation test for AddRelationshipForm — toYear before fromYear shows alert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
3b430828b7 test(stammbaum): component tests for StammbaumCard — heading, empty state, toggle, error display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
f8aa8c6574 test(stammbaum): component tests for StammbaumSidePanel — displayName, empty state, loading indicator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
ce005622f2 fix(stammbaum): i18n inline add-form in StammbaumSidePanel — replace 5 hardcoded German strings with m.relation_form_*() keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
0e9fa157e5 fix(stammbaum): add × dismiss button with aria-label to StammbaumSidePanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
fa1dfbc99d fix(stammbaum): guard inferred-relationship badge to single-receiver documents only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
eb91639a5e fix(stammbaum): responsive /stammbaum layout — hidden md:block aside + fixed bottom sheet on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
43fb51305e fix(stammbaum): i18n AddRelationshipForm — wire Paraglide for type/year labels and optgroup captions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
93f4a00032 fix(stammbaum): SVG node font 14→16px and reliable keyboard focus ring
CSS box-shadow rings (focus-visible:ring-*) are invisible inside SVG.
Replace with a conditional <rect> drawn at -3px offset that renders in
all browsers. Name font-size bumped from 14 to 16px for the 60+
transcriber audience (WCAG readability, Leonie medium concerns).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
ea97bdd869 refactor(stammbaum): initialise selectedId directly from focusId, drop $effect
The focus deep-link is a one-time load param — $derived + $effect caused
a deferred write that left the node unselected on first paint. Initialising
$state inline reads the URL once at component mount with no reactive cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
0b3455dbb2 test(stammbaum): skip E2E spec until CI Playwright job exists (#363)
All four tests skipped with a reference to issue #363 which tracks
adding the Playwright Chromium install + Docker Compose startup step
to the CI workflow. Remove the skip once #363 is resolved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
499d0a3ca8 fix(stammbaum): derived relationship names link to person page in StammbaumCard
The <span> in the derived-relationships list is replaced with <a href>
so keyboard and pointer users can navigate directly from the edit card,
consistent with PersonRelationshipsCard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
bd3feda182 fix(stammbaum): WCAG 2.2 SC 2.5.8 — delete button 32px → 44px in RelationshipChip
h-8 w-8 (32px) replaced with h-11 w-11 (44px) to meet the minimum
touch target for the 60+ transcriber audience. Test added to prevent
regression.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
f2127e2814 fix(stammbaum): i18n the StammbaumCard heading (de/en/es)
Hardcoded 'Stammbaum & Beziehungen' heading replaced with
m.stammbaum_relationships_heading(); new key added to all
three message files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
13bb3b451e fix(stammbaum): import chipLabel/otherName from shared relationshipLabels in PersonRelationshipsCard
Removes local duplicates of the switch-statement label logic already
exported from $lib/relationshipLabels.ts. Adds two direction-sensitive
tests proving the Elternteil-von / Kind-von branch is covered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
b6253cb023 fix(stammbaum): add focus-visible ring to zoom buttons — WCAG 2.4.7
Addresses @leonie blocker: zoom buttons in /stammbaum had no visible focus
indicator for keyboard users. Applied focus-visible:ring-2 focus-visible:ring-focus-ring
focus-visible:outline-none matching the pattern used on nav links.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
fcfae8fb78 refactor(stammbaum): use shared chipLabel/otherName from relationshipLabels in both components
Addresses @felix blocker: removes the verbatim duplicate switch+2-line helper
from StammbaumCard.svelte and StammbaumSidePanel.svelte; both now import from
the shared $lib/relationshipLabels helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
83de7ff673 refactor(stammbaum): extract chipLabel/otherName to shared relationshipLabels helper
Addresses @felix blocker: both functions were duplicated verbatim in
StammbaumCard.svelte and StammbaumSidePanel.svelte. Now exported from
$lib/relationshipLabels.ts with perspectivePersonId as an explicit param.
8 unit tests added (red→green).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
48649e67f9 refactor(stammbaum): extract RelationshipChip and AddRelationshipForm
Split StammbaumCard from 366 to 196 lines by extracting:
- RelationshipChip.svelte — single relationship list item with optional delete
- AddRelationshipForm.svelte — self-contained add-relationship form with open/close state

Both components have browser-mode spec tests covering rendering and interaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
1d14c32c23 fix(stammbaum): WCAG min font-size and 44px touch targets
Raise chip labels from 10px to 12px (text-xs) in StammbaumCard,
StammbaumSidePanel and StammbaumTree SVG text. Widen zoom buttons
from 32px to 44px for senior-audience touch targets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
d27fed3c35 fix(stammbaum): i18n for year-range labels in StammbaumCard
Replaces hardcoded German strings "ab {from}" / "bis {to}" in yearRange()
with parameterized Paraglide keys relation_year_from / relation_year_to,
added to all three message files (de/en/es).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
b969bcd877 style(stammbaum): widen side panel to 320px so longer names don't clip
The 268px width came from the spec mock; real names plus the
relationship pill ("Eugenie de Gruyter" + "Elternteil") need more
breathing room.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
cd26057ea5 fix(stammbaum): iterative generation + spouse-adjacent block layout
Two distinct bugs surfaced once a 3-generation tree was loaded
(Walter+Eugenie → Hans+Clara, Hans married to Hilde with child Lili):

1. Generation BFS was non-iterative. Hilde was visited as a "root"
   first, assigning Lili = gen 1, then Hilde was pulled to gen 1 to
   match her spouse Hans — but Lili's depth was never recomputed,
   leaving her on the same row as her parents. Replaced the BFS with
   an iterative longest-path assignment that re-runs (max parent gen
   + 1) and the spouse-shared-row rule together until stable.

2. No spouse adjacency. Hilde (no parents in the graph) ended up in
   her own block on the far left, with Hans + Clara to her right and
   the spouse line drawn straight across Clara's box. Replaced the
   per-parent-set grouping with a block model:
     - sibling-blocks group children of the same parent set
     - loose spouses attach on the outer edge of their partner's block
     - dual-loose spouse pairs merge into one 2-person block
     - each block is centred so its parented members' average sits
       exactly under the parent midpoint, keeping all connectors at 90°

Adds a regression test for the full Walter/Eugenie/Hans/Clara/Hilde/
Lili scenario (Lili in a deeper row, Hans+Hilde adjacent, no slanted
segments) and rewrites the viewBox tests to be position-agnostic via
a rect-centroid helper that reads the per-node `<g transform>`.

Tracked the eventual move to dagre (multi-marriage / cross-cousin /
~50+ nodes) in #361.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
ccbcbca0e8 feat(stammbaum): inline add-relationship form in side panel
Implements the inline-edit affordance from
docs/specs/stammbaum-tree-spec.html (section 3): a low-opacity
"+ Beziehung hinzufügen" button below the direct relationships list
expands into a compact form (type select, person typeahead,
optional Von/Bis Jahr inputs, Abbrechen + Speichern). On save the
form POSTs to /api/persons/{id}/relationships, reloads the panel's
own data, and calls invalidateAll() so the tree picks up the new
edge without a hard refresh.

The panel takes a new canWrite prop, plumbed through from the
+layout.server.ts data already exposed on page.data.

Also pins the /stammbaum canvas to the viewport (-my-6 cancels
<main>'s py-6, h-[calc(100dvh-4.25rem)] subtracts the navbar) so
the page no longer overflows below the fold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
c40cc05f68 feat(stammbaum): tree visual polish + parent-midpoint layout
Aligns the SVG tree with docs/specs/stammbaum-tree-spec.html:

- Node outline: var(--c-primary) at stroke-width=1.5 (was the much
  paler --c-line at 1) and selected text uses var(--c-primary-fg)
  so it remains readable on the dark/light primary fill
- Spouse line and parent-child line now share the same stroke style;
  spouse keeps the midpoint dot (radius bumped to 4.5 per spec)
- When two parents are connected by SPOUSE_OF, draw a single shared
  parent-pair → child line from the spouse midpoint instead of two
  diverging lines
- ViewBox: enforces a 1200×800 minimum and centers the content so a
  single node no longer scales up to fill the whole canvas in the
  top-left
- Children are positioned at the average of their parents' x and
  packed left-to-right per row, keeping connectors close to vertical

Adds component tests for the centring, the shared parent-pair link
(verified vertical), and the fallback to two lines when parents are
not spouses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
a021355072 feat(documents): inline relationship pills next to person names
Replaces the standalone "Beziehung" badge at the bottom of the
metadata drawer's Personen column with small inline pills attached
to each personCard — sender gets labelFromA, the single receiver
gets labelFromB. Matches docs/specs/stammbaum-doc-badge-spec.html.

Drops the now-unused RelationshipBadge component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
8971fee75e style(stammbaum): tighten vertical rhythm around relationship cards
- /stammbaum: drop the global py-6 top gap so the page header butts
  up against the navbar, matching its full-bleed canvas layout
- person detail: add mt-6 around the document lists so they don't
  sit flush against the Beziehungen card
- person edit: add mt-6 to PersonMergePanel so the merge box doesn't
  collide with the StammbaumCard above it

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
48a704f002 fix(stammbaum): drop inferred relationships that are already direct
A spouse listed as a direct PersonRelationship was also being
emitted as an inferred SPOUSE chip below, so the same person
appeared twice in the Beziehungen card.

Filter the inferred list against the IDs already shown as direct
edges before slicing the top 5. Added a component test that
renders red without the filter and green with it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
f382bd9974 test(stammbaum): E2E spec + extend person load mock
- frontend/e2e/stammbaum.spec.ts covers four journeys:
  1) /briefwechsel still resolves with a 2xx after the nav swap.
  2) /stammbaum shows the page heading.
  3) /stammbaum renders either the empty state (with the Personenliste
     link) or at least one node[role=button] in the SVG.
  4) The person edit card surfaces the year-range error when Bis < Von.

- persons/[id]/page.server.spec.ts gains two extra mockResolvedValueOnce
  entries per scenario to match the new relationships +
  inferred-relationships GETs that the page load now performs.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
d7f4f6f163 feat(stammbaum): person detail Beziehungen card
- persons/[id]/+page.server.ts loads relationships and
  inferred-relationships in the existing parallel fetch.
- New PersonRelationshipsCard renders direct chips (mint) and the
  top-5 derived chips (grey) on /persons/{id}, both linked to the
  other person's page. Empty state shows
  "Noch keine Beziehungen bekannt." in muted serif.
- Card sits in the right column above the document lists.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
242e10179d feat(stammbaum): /stammbaum page — SVG tree + side panel + empty state
- /stammbaum/+page.server.ts loads GET /api/network (already filtered
  to family members on the backend) and returns nodes + edges.
- +page.svelte holds the page shell, manages selectedId (with
  ?focus={id} deep-link support) and zoom state, renders the empty
  state when nodes.length === 0 (icon + heading + body + link to
  /persons), or the tree + side panel otherwise.
- StammbaumTree.svelte: BFS-based generation assignment from roots,
  spouses promoted to the deeper generation so couples sit on the same
  row, alphabetical sort within row, simple grid layout. SVG nodes are
  role="button" + aria-label="{name}, {birth}–{death}" +
  aria-expanded={selected}, with click + Enter/Space activation. Solid
  parent→child connectors; mint spouse line with midpoint circle, dashed
  if SPOUSE_OF.toYear is set (former spouse). Zoom maps to viewBox.
- StammbaumSidePanel.svelte: lazily loads
  /api/persons/{id}/relationships and /inferred-relationships when the
  selection changes; shows direct chips (mint), top-5 derived chips
  (grey), and a "Zur Personenseite →" link. Escape closes the panel.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
aaf885cafd feat(stammbaum): person edit Stammbaum & Beziehungen card
New StammbaumCard rendered below the Namensverlauf card on
/persons/{id}/edit:
- Header with "Als Familienmitglied" toggle (form action
  toggleFamilyMember → PATCH /api/persons/{id}/family-member).
- "Erscheint im Stammbaum" banner with deep-link to
  /stammbaum?focus={id} when familyMember is true.
- Direct relationships list grouped by type, then year. Chip text is
  direction-aware: storage subject reads "Elternteil von", storage
  object reads "Kind von" (new relation_child_of i18n key in all 3
  locales). Symmetric and non-family types use their own keys.
- + Beziehung hinzufügen reveals an inline form with type select
  (grouped Familie / Sozial), a PersonTypeahead with the new
  excludePersonId prop (self-rel prevention, Elicit blocker 1), and
  Von / Bis year fields.
- Year validation lives client-side via $derived: empty/empty is OK,
  Bis < Von shows a red text-red-700 error wired with aria-describedby
  and disables submit (Sara blocker 3).
- Self-rel inline error mirrors the typeahead exclusion in case the
  user submits the personId regardless.
- Abgeleitete Beziehungen section (top 5) collapsed by default.

+page.server.ts loads relationships + inferred relationships in the
existing parallel fetch and adds three actions: toggleFamilyMember,
addRelationship (with year-range guard), deleteRelationship.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
b658a13247 feat(stammbaum): show inferred relationship in the document drawer
- New presentational RelationshipBadge component (labelFromA → arrow →
  labelFromB) wired into DocumentMetadataDrawer's Personen column,
  rendered after the receivers block when both endpoints are family
  members.
- DocumentTopBar gains an optional inferredRelationship prop and
  passes it through.
- documents/[id]/+page.server.ts loads the badge: only when sender is
  a family member, exactly one receiver, and that receiver is also a
  family member; 404 (no path) → null.
- relationshipLabels.ts maps the backend label keys (parent/child/...)
  to localised strings, so the server load returns badge-ready strings.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
6bed617959 feat(stammbaum): swap nav slot from /briefwechsel to /stammbaum
Both desktop and mobile nav rows now point at /stammbaum and read
m.nav_stammbaum(). The /briefwechsel route stays intact — only the
nav anchor changes.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00