Commit Graph

14 Commits

Author SHA1 Message Date
Marcel
37edac4da6 fix(hover-card): maiden name false positive, placeholder on non-empty editor, card persistence
- PersonHoverCard: alias is compared against both `lastName` and `displayName`
  before showing as maiden name — prevents false positive when alias is stored
  as the full current name (e.g. "Maria Schmidt" ≠ "Schmidt" but name unchanged)
- PersonMentionEditor: data-placeholder was set statically so the CSS ::before
  rule showed the placeholder on any blur even with content; now a $effect
  toggles the attribute based on editor.isEmpty
- TranscriptionReadView: hovering onto the card itself cancels the 150ms close
  timer so the card stays open while reading it; leaving the card closes it
  immediately — onmouseenter/onmouseleave wired through PersonHoverCard props
- hoverCardPosition: removed scrollX/scrollY offset since the card is now
  position:fixed (scroll is already baked into getBoundingClientRect coords)
- MentionDropdown: raised z-index from z-20 to z-50 to render above the hover card
- vite.config.ts: pre-bundle Tiptap packages to avoid HMR waterfall on first load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 18:26:44 +02:00
Marcel
3365f5845e fix(person-mention): hover card mounts on focusin for keyboard users (WCAG 2.1.1)
Leonie FINDING-01 (Critical) + Elicit E3: only mouseenter triggered the
hover card, so a keyboard user tabbing through transcribed text reached the
anchor but never saw the rich-context preview. For the senior audience
constraint that's a hard regression.

Wire focusin/focusout alongside mouseenter/mouseleave on the delegated
listener. Same handleMentionEnter/Leave run — getBoundingClientRect works
identically on focused elements. focusin/focusout bubble naturally so no
capture phase needed.

Two new tests assert focusin mounts the card and focusout unmounts it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:57:48 +02:00
Marcel
3faac13533 fix(person-mention): respect modified-click and middle-click for new-tab nav
Felix #7: handleMentionClick unconditionally preventDefault'd and goto'd,
breaking ctrl-click / cmd-click / shift-click / alt-click / middle-click —
"open in new tab" is a real workflow for researchers comparing two persons.

Add isPlainPrimaryClick() guard. Modified clicks fall through to the
browser's default anchor handling (the <a href="/persons/{id}"> opens in
the new tab as expected). Plain left-clicks still SPA-navigate via goto().

Three new tests assert ctrl-click, meta-click, and middle-click are not
preventDefault'd.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:56:23 +02:00
Marcel
5890bb3abd refactor(person-mention): split fetchHoverData into pure load + cache wrapper
Felix #1: fetchHoverData was doing four things — cache lookup, fetch, JSON
parsing, 404 normalisation. Split into:

  loadHoverData(personId)       — pure fetch + 404→null + non-OK→throw
  getOrFetchHoverData(personId) — five-line cache wrapper around the above

Also document the cache-lifetime trade-off (Markus #4, Elicit OQ-372-02):
the cache is per-mount, so closing and reopening the transcription panel
rebuilds it. That's intentional given the read-only nature of the view —
revisit if stale-card user reports surface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:54:35 +02:00
Marcel
060db69108 refactor(person-mention): extract computeHoverCardPosition into testable util
Three reviewer concerns land here:
- Felix #2: magic numbers 0.7 and 300 belong in named constants
- Sara #6: the position function had 4 branches and 2 thresholds with zero tests
- Leonie FINDING-05: at 320px viewport the flip-left could push the card
  past the right edge — needed a viewport clamp

Move the function to src/lib/utils/hoverCardPosition.ts as a pure
(rect, viewport) → {top, left} mapping, with named exports CARD_WIDTH_PX,
CARD_HEIGHT_PX, CARD_GAP_PX, BOTTOM_BAND_RATIO, RIGHT_FLIP_THRESHOLD_PX.
Add a viewport clamp so left + CARD_WIDTH never exceeds the right edge.

Ten unit tests cover default placement, flip-up (both triggers), flip-left,
flip-right-edge clamp, and scroll offset. TranscriptionReadView passes the
current window viewport in on each call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:53:29 +02:00
Marcel
1842e23c81 refactor(person-mention): centralise PERSON_MENTION_SELECTOR constant
Markus flagged that 'a.person-mention' is a magic string repeated four times
in TranscriptionReadView, plus the CSS rule, plus tests. Extract into a single
exported constant so the renderer template, the delegated event handlers,
and the consumer-side selectors all import the same value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:51:25 +02:00
Marcel
488d4384a1 refactor(person-mention): brand renderer return types as SafeHtml
Markus, Felix, and Nora independently flagged the {@html …} boundary as a
distributed-knowledge security risk: today renderBody and renderTranscriptionBody
return string, so the next refactor that does {@html block.text} (instead of
{@html renderBlockHtml(block)}) is one typo away from a stored-XSS regression.

Introduce a SafeHtml brand type (string with a phantom __brand) returned by
both renderers and by renderBlockHtml in TranscriptionReadView. Compile-time
enforcement of the escape invariant — costs zero runtime, makes the contract
auditable in one file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:48:26 +02:00
Marcel
6a6967d841 refactor(person-mention): hoist LoadState + HoverData into shared types module
Markus flagged the LoadState export from PersonHoverCard.svelte as a
view-vs-orchestrator boundary smell — both files own the same shape, and a
third caller (admin previews, briefwechsel cards) would create a circular
import. Move the types into src/lib/types/personHoverCard.ts so the contract
is module-stable.

Also harden .prettierignore + eslint.config.js so a stray .svelte-kit.old/
backup directory (rotated by SvelteKit during dev) doesn't break the lint
hook — matches the existing .svelte-kit-backup/ convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:46:42 +02:00
Marcel
1fd38830fe feat(person-mention): TranscriptionReadView wires hover card and click nav
Composes splitByMarkers + renderTranscriptionBody so [unleserlich]
markers render as <em data-marker> siblings of the mention anchor —
neither nested inside the other (B19b).

Hover card lifecycle on each .person-mention anchor:
  mouseenter → set aria-describedby, place card via getBoundingClientRect
               (default below-right; flip up if <200px from bottom or
                mention is in bottom 30% of viewport; flip left if
                <300px from right), fire fetch, mount card with
                skeleton state
  resolved   → swap card to loaded state with person + family
                relationships (PARENT_OF / SPOUSE_OF / SIBLING_OF only)
  404        → degrade: mark anchor with data-person-deleted="true",
                unmount card, suppress future hovers/clicks
  network    → swap card to error state — link still navigates
  mouseleave → drop aria-describedby, unmount card

Per-page SvelteMap<personId, Promise> cache (B15.5) so a sweep across
N mentions of the same person fires the backend once. Click handler
calls goto() so SvelteKit handles routing without a full reload.

Event listeners are attached once per article via a Svelte action
because the anchor HTML is injected via {@html ...} and would not
receive declarative bindings. The eslint-disable comment mirrors
the rationale on CommentMessage.svelte:88-89.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:21:35 +02:00
Marcel
10fdaf7d00 refactor(ui): use CSS variable for turquoise in flash animations
Replaces hardcoded rgba(0,199,177,...) with color-mix using
var(--color-turquoise) for dark mode compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:58:04 +02:00
Marcel
b01a9ef406 refactor(ui): use bg-turquoise/10 token for paragraph hover
Replaces hardcoded rgba value with the project's turquoise color
token for dark mode compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:54:57 +02:00
Marcel
e31b73303e fix(ui): bump paragraph hover opacity from 6% to 10%
Improves visibility of the clickability affordance on uncalibrated
displays and for senior users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:53:17 +02:00
Marcel
10cecb01f5 feat(a11y): respect prefers-reduced-motion for scroll-sync
Uses scrollIntoView behavior 'instant' instead of 'smooth', skips
CSS animations (static highlight instead), and extends timeout to
2s for reduced-motion users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:27:01 +02:00
Marcel
306eef2e95 feat(ui): add TranscriptionReadView for flowing prose display
Renders transcription blocks as readable text with [unleserlich]/[...]
markers styled as italic muted text. Supports click-to-sync and
flash highlight for scroll-sync feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:14:53 +02:00