- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Improves visibility of the clickability affordance on uncalibrated
displays and for senior users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>