Commit Graph

1096 Commits

Author SHA1 Message Date
Marcel
efcc347c00 refactor: move shared components to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:40:14 +02:00
Marcel
d6db7a07bd refactor: move shared utilities to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:35:15 +02:00
Marcel
7cb922e90f refactor: move user domain components to lib/user/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:28:17 +02:00
Marcel
7dd05af867 refactor: move tag domain components to lib/tag/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:27:25 +02:00
Marcel
d5d36e661a refactor: move person domain components and utils to lib/person/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:26:21 +02:00
Marcel
920742ba1c refactor: move ocr domain components to lib/ocr/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:23:55 +02:00
Marcel
051d2f246e refactor: move notification domain to lib/notification/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:22:02 +02:00
Marcel
8ff5d6f842 refactor: move geschichte domain to lib/geschichte/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:20:07 +02:00
Marcel
1e656d2db4 refactor: move document transcription, annotation, viewer sub-packages
- transcription/: TranscriptionBlock, Column, EditView, PanelHeader, ReadView,
  Section + transcriptionMarkers, blockConflictMerge, saveBlockWithConflictRetry
  + useBlockAutoSave, useBlockDragDrop hooks
- annotation/: AnnotationLayer, AnnotationShape, AnnotationEditOverlay
- viewer/: PdfViewer, PdfControls + useFileLoader, usePdfRenderer hooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:01:39 +02:00
Marcel
e7f8aa5894 refactor: move document domain core to lib/document/
Moves ~25 components, utils (search, filename, groupDocuments,
documentStatusLabel, validateFile), bulkSelection store, and
TranscriptionSection sub-component. Fixes broken relative imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:56:36 +02:00
Marcel
422e86fbf1 refactor: move conversation domain to lib/conversation/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:48:50 +02:00
Marcel
c7fda6a027 chore: remove accidentally nested generated/generated/ artifact
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:47:45 +02:00
Marcel
a843d27663 refactor: move activity domain components to lib/activity/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:47:09 +02:00
Marcel
d4f666e981 test(person-mention): move i18n test to its own describe block
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m38s
CI / Backend Unit Tests (pull_request) Failing after 3m16s
CI / OCR Service Tests (pull_request) Successful in 41s
CI / Unit & Component Tests (push) Failing after 3m38s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m11s
Move `transcription_block_placeholder contains @ mention trigger` out of
`describe('PersonMentionEditor — placeholder behavior')` into a new
`describe('PersonMentionEditor — i18n message content')` block so each
describe group has a single, clear responsibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:30:11 +02:00
Marcel
42cf078bb6 feat(person-mention): update transcription placeholder with @mention discoverability hint
Replaces the generic "Type text here..." placeholder in TranscriptionBlock
with copy that teaches the @Name trigger inline (Leonie Voss design review,
issue #370). No new DOM, no new i18n keys — just the three existing
`transcription_block_placeholder` strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:29:33 +02:00
Marcel
9b6d8fbef1 fix(geschichten): bump filter pills to 44px touch target
Senior-author persona requires 44px minimum touch targets on every
interactive control. The /geschichten filter row had three pills
(All / chip / + Person wählen) at h-9 (36px), missing the rule that
the toolbar already follows. Bumped all three to h-11.

Test added in page.svelte.spec.ts asserts the className contains
h-11 on every pill variant.

Addresses Leonie's iteration-3 concern #6 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 09:03:55 +02:00
Marcel
4f3020ffab feat(geschichten): make Geschichte panel rows fully clickable
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m20s
CI / OCR Service Tests (push) Successful in 49s
CI / Backend Unit Tests (push) Failing after 3m16s
CI / Unit & Component Tests (pull_request) Failing after 3m52s
CI / Backend Unit Tests (pull_request) Failing after 3m11s
CI / OCR Service Tests (pull_request) Successful in 48s
The story rows on the person detail page now match the
PersonDocumentList pattern: the entire row is a single anchor with a
hover background, and the title gets group-hover:underline. Author,
date, and body excerpt are all part of the same clickable area, so
the touch target matches the visual rhythm of the document panels
above.
2026-05-03 08:45:04 +02:00
Marcel
96d023a7cb feat(geschichten): chip-row UI for multi-person AND filter
The /geschichten list page now renders one removable chip per active
person filter and lets users add more via the existing typeahead. The
URL uses repeated ?personId= params (matching the documents tag
filter), which the regenerated API client passes straight through to
the backend's new array-bound endpoint. New translation keys cover the
chip remove aria-label, the AND hint shown while picking, and the
multi-person empty state.
2026-05-03 08:37:28 +02:00
Marcel
c23fad7dc8 test(geschichten): cover GeschichteEditor title guard, status mode, pre-fill, payload
10 browser-based component tests:
- title-empty disables both DRAFT save buttons
- inline title-required error appears after blur
- DRAFT mode renders "Entwurf speichern" + "Veröffentlichen"
- PUBLISHED mode renders "Speichern" + "Zurück zu Entwurf"
- initialPersons / initialDocuments props render as chips on first paint
- title input is populated from a geschichte prop
- "Entwurf speichern" passes trimmed title + status=DRAFT to onSubmit
- "Veröffentlichen" passes status=PUBLISHED
- personIds / documentIds from initial props flow through onSubmit

Closes Felix's review B1 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:51:40 +02:00
Marcel
11c0d49907 test(geschichten): cover GeschichtenCard render, threshold, write-action gate
Browser-based component spec asserting:
- empty geschichten → no <section> rendered
- >= 1 story → heading + story link visible
- canWrite=false → no "+ Geschichte schreiben" link
- canWrite=true → link with /geschichten/new?personId pre-fill
- 0–2 stories → no footer link
- 3+ stories → "Alle Geschichten zu {name}" footer link to /geschichten?personId
- excerpt is plain text (no <strong>, no <script>)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:49:26 +02:00
Marcel
da249369ee test(geschichten): cover DocumentMultiSelect search, chip add/remove
Browser-based component spec mirroring PersonTypeahead.svelte.spec.ts:
renders empty input, surfaces pre-selected chips with formatted date,
emits hidden documentIds inputs for each chip, debounces the search
against /api/documents/search, adds a chip on click, hides already-
selected docs from new dropdown results, and removes a chip on × click.

Closes Felix's review B2 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:47:54 +02:00
Marcel
74b13abf53 fix(geschichten): widen story body and lift section-header contrast
Story-detail body now uses an explicit Tailwind block-element selector
ruleset instead of the `prose` plugin, so the body fills the full max-w-3xl
parent width — previously `prose` clamped to ~65ch inside an already narrow
page.

GeschichtenCard heading and the "+ Geschichte schreiben" link now use
text-ink-2 (#4b5563 = 7.6:1 on white, AAA-passable) instead of text-ink-3
or text-ink/60. Same fix on the "+ Geschichte anhängen" link in the
Document drawer column and on the Personen / Dokumente section headers
on the story detail page.

Closes Leonie's review B1, B2 and S4 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:46:31 +02:00
Marcel
ad535e314b refactor(extract-text): rename stripHtml → extractText and document non-sanitiser status
Adds a module docstring at the top of extractText.ts spelling out that this
is text extraction, not XSS sanitisation, and that callers must rely on
safeHtml() (DOMPurify) for security. Adds a Vitest test block with classic
XSS-shaped payloads (<script>, <svg/onload>, <iframe srcdoc>, javascript:
href) asserting that no markup is re-emitted, even though the module is
explicitly not a sanitiser.

Updates the two callers (/geschichten index, GeschichtenCard) to import
from the new path. The collapse-whitespace pass also makes the regex
fallback's output saner for excerpt rendering.

Closes Nora's review B1 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:44:40 +02:00
Marcel
35ec7e799f feat(admin): add BLOG_WRITE to group permission checkbox UI
Both /admin/groups/new and /admin/groups/[id] now expose BLOG_WRITE in the
standard-permissions card so admins can grant Geschichten authoring through
the UI instead of running raw SQL. Adds Paraglide labels in de/en/es.

Closes Markus's review B1 on PR #382.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:41:09 +02:00
Marcel
b698f9f223 test(persons): add seventh GET mock for the geschichten API call
Some checks failed
CI / Backend Unit Tests (push) Failing after 3m24s
CI / Unit & Component Tests (push) Failing after 4m56s
CI / OCR Service Tests (push) Successful in 50s
CI / Unit & Component Tests (pull_request) Failing after 3m51s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m18s
The /persons/[id] +page.server.ts now fetches geschichten in parallel with
the other endpoints. Each test in this spec mocks the typed-client's GET
call sequentially, so each chain needs one extra resolved value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:12:50 +02:00
Marcel
ed270f68e1 feat(geschichten): wire discovery integrations on Person and Document pages
Person detail (/persons/[id]):
- Server load fetches GET /api/geschichten?status=PUBLISHED&personId={id}
  in parallel with the existing person/document queries.
- Renders <GeschichtenCard> below the received-documents list when the
  person has at least one published story.

Document detail (/documents/[id]):
- Server load adds the same parallel call with documentId={id}.
- DocumentTopBar gains geschichten + canBlogWrite props that flow through
  to DocumentMetadataDrawer.
- DocumentMetadataDrawer's grid expands to lg:grid-cols-4 when the
  Geschichten column should appear (stories exist OR user can author),
  and shows "+ Geschichte anhängen" / "Alle anzeigen" links following the
  >= 3-story threshold from issue comment #5758.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:01:19 +02:00
Marcel
fe1014a08a feat(geschichten): add /geschichten routes (index, detail, new, edit)
- /geschichten — published-stories index with filter pills + "+ Neue Geschichte"
  for BLOG_WRITERs; supports ?personId and ?documentId pre-filtering
- /geschichten/[id] — reader detail with sanitised {@html} body, person and
  document chip sections, BLOG_WRITER edit/delete with confirm dialog
- /geschichten/new — editor with optional ?personId and ?documentId pre-fill
  (silent ignore on unknown IDs to avoid leaking entity existence)
- /geschichten/[id]/edit — editor populated from existing story; BLOG_WRITE
  guard redirects readers to the detail page

All routes load via createApiClient(fetch) with !response.ok error handling
following the project pattern; PATCH/DELETE go through raw fetch which the
Vite dev proxy / Caddy production proxy authenticates via cookie.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:54:31 +02:00
Marcel
9e6efacbcb feat(geschichten): add stripHtml util and GeschichtenCard component
stripHtml() strips tags via DOMParser (browser) with a regex fallback for
SSR. plainExcerpt() truncates at a word boundary with an ellipsis. Both
covered by Vitest specs.

GeschichtenCard renders the top 3 published stories about a person on
/persons/[id], with an editorial excerpt, publication date, author, and a
"+ Geschichte schreiben" link visible only to BLOG_WRITERs. Footer link to
/geschichten?personId=... appears once geschichten.length >= 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:50:58 +02:00
Marcel
ab3e633a0c feat(geschichten): add GeschichteEditor with Tiptap toolbar
Tiptap StarterKit configured for B/I/¶/H2/H3/UL/OL/history; code, codeBlock,
blockquote, strike, horizontalRule and hardBreak disabled to keep output
matching the backend HTML allow-list. Two-column responsive layout with the
editor body on the left and Personen / Dokumente / Status sections in the
sidebar. Sticky save bar adapts to DRAFT vs PUBLISHED state. Title-required
guard with inline error and beforeNavigate dirty-state guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:49:10 +02:00
Marcel
b381b2078a feat(geschichten): add DocumentMultiSelect chip + typeahead component
Mirrors PersonMultiSelect for documents: chip-style multi-select backed by
GET /api/documents/search?q=. Used in the Geschichte editor sidebar to link
referenced documents to a story.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:46:02 +02:00
Marcel
9e7861fa03 feat(geschichten): frontend foundation — canBlogWrite, sanitize util, nav, i18n
- Derives canBlogWrite in +layout.server.ts the same way as canAnnotate.
- Adds Geschichten link to AppNav (desktop + mobile, between Stammbaum and Admin).
- Adds error_geschichte_not_found mapping to errors.ts and translation keys
  for the Geschichten index, detail, editor, and confirmation copy in
  de/en/es.
- Adds isomorphic-dompurify-backed safeHtml() helper with allow-list
  matching the backend OWASP policy (p/br/strong/em/h2/h3/ul/ol/li),
  plus Vitest spec.
- Updates legacy spec test data so the new required canBlogWrite layout
  prop type-checks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:43:29 +02:00
Marcel
afd6d0b20d chore(api): regenerate types with Geschichte endpoints
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:36:13 +02:00
Marcel
db66d0cc61 fix(document-page): add .catch() to task deep-link tick promise
Some checks failed
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 3m7s
CI / Unit & Component Tests (push) Failing after 3m22s
Addresses @felix — tick().then() had no error handler; console.error
is now logged on failure, matching the existing deep-link scroll pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:38:05 +02:00
Marcel
7dc5dc6f71 feat(document-page): auto-open transcription panel when ?task=transcribe is present
On mount, reads the task query param before the comment deep-link handler.
When task=transcribe, opens the transcription panel, scrolls the close button
into view, moves focus to it, then strips the param from the URL via replaceState.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:38:05 +02:00
Marcel
d974d39d17 feat(TranscriptionColumn): deep-link to transcription panel via ?task=transcribe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:38:05 +02:00
Marcel
5e4e487d5f feat(SegmentationColumn): deep-link to transcription panel via ?task=transcribe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:38:05 +02:00
Marcel
b3fe9b1171 refactor(PersonMentionEditor): use data-editor-inner attribute for stable querySelector
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m26s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 3m2s
CI / Unit & Component Tests (pull_request) Failing after 3m19s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m11s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:29:49 +02:00
Marcel
3c7c7a9aa4 refactor(TranscriptionReadView): rename handleMentionLeave, closeTimer to \$state, add 150ms comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:27:43 +02:00
Marcel
9908f7afdc test(TranscriptionReadView): cover hover card timer and keyboard focus behavior
Five new tests verify:
- Card stays open when mouse moves mention → card (cancels 150ms timer)
- Card closes immediately on card mouseleave (no timer)
- Re-entering a mention cancels a pending close
- Card stays open when keyboard focus moves mention → card (WCAG 2.1.1)
- Card closes when keyboard focus leaves the card entirely

The keyboard tests drove adding onfocusin/onfocusout to PersonHoverCard's
root div, reusing the existing onmouseenter/onmouseleave callbacks so that
screen-reader and keyboard users get the same stay-open affordance as
mouse users. relatedTarget check prevents spurious closes on intra-card
focus movement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:23:44 +02:00
Marcel
96d9ff5db1 fix(PersonHoverCard): move chip colon into DOM for consistent screen reader announcement
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m24s
CI / OCR Service Tests (push) Successful in 29s
CI / Backend Unit Tests (push) Failing after 2m59s
CI / Unit & Component Tests (pull_request) Failing after 3m25s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m19s
Replaces CSS ::after { content: ':' } with literal colon inside the
chip-type span. CSS-generated content is announced inconsistently
across NVDA+Chrome and VoiceOver+Safari; a real text node is always
reliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:32:21 +02:00
Marcel
0113367d05 refactor(TranscriptionReadView): remove dead else branch in handleMentionLeave
Only mouseleave is wired in attachMentionHandlers so the else branch
could never fire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:29:26 +02:00
Marcel
b087de84c4 test(PersonMentionEditor): add placeholder show/hide behavior coverage
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m15s
CI / OCR Service Tests (push) Successful in 28s
CI / Backend Unit Tests (push) Failing after 2m59s
CI / Unit & Component Tests (pull_request) Failing after 3m16s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 3m3s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:56:12 +02:00
Marcel
3e07f6798c refactor(PersonHoverCard): extract showMaidenName derived, verify chip-type contrast, fix stale position test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:55:45 +02:00
Marcel
7ccd541d40 fix(hover-card): use orientation-aware relationship labels; allow spaces in mention
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m35s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m6s
CI / Unit & Component Tests (pull_request) Failing after 4m38s
CI / OCR Service Tests (pull_request) Successful in 42s
CI / Backend Unit Tests (pull_request) Failing after 3m5s
PersonHoverCard was showing the hovered person as their own parent when stored
as the object side of a PARENT_OF row — now uses chipLabel/otherName from
relationshipLabels (same helpers the person detail page uses) to resolve the
correct name and label from the caller's perspective.

PersonMentionEditor: add allowSpaces:true so typing a last name after a space
no longer exits mention mode mid-query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:22:37 +02:00
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
49443ad16a docs(PersonMentionEditor): document client-side fetch exception inline
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m20s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 3m4s
CI / Unit & Component Tests (push) Failing after 3m44s
CI / OCR Service Tests (push) Successful in 40s
CI / Backend Unit Tests (push) Failing after 3m14s
Per Markus #5616, the leaf-component fetch in the Tiptap suggestion plugin
violates the project-wide rule from frontend/CLAUDE.md ("Data flows from
+page.server.ts via props — never client-side API fetch"). Add an inline
block-comment explaining why this exception is justified (suggestion runs
client-side per keystroke; same auth surface; no server-side reshape
benefit) and points future readers at the open ADR follow-up plus Nora's
PersonSummaryDTO response-shape audit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:21:33 +02:00
Marcel
e6844c403c feat(MentionDropdown): restore "Neue Person anlegen" empty-state link
The Tiptap rewrite dropped the inline "create new person" affordance the
textarea-era component used to render. Without it the workflow regresses:
transcriber must close the dropdown, navigate to /persons/new, come back,
re-type the query. The m.person_mention_create_new() key is still in all
three locale files — add the link back as a 44px-tall row with a top
border separating it from the empty-state message.

target=_blank keeps document/editor state intact; rel=noopener prevents
reverse-tabnabbing. mousedown preventDefault keeps the editor focused
(the dropdown row pattern used for option rows).

Test: empty-state renders a link to /persons/new with the localised label.

Leonie #5621 (Major) + Elicit OQ-373-04.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:20:42 +02:00
Marcel
f1932fd5f6 fix(person-mention): WCAG 1.4.11 contrast for mention pill and dropdown ring
Two non-text-contrast failures, both flagged by Leonie #5621:

1. PersonMentionEditor mention pill: decoration-brand-mint (#A6DAD8) on
   white is ≈1.7:1 — fails the 3:1 minimum for meaningful UI indicators.
   Switch to decoration-ink/50, which matches the read-mode .person-mention
   rule (≈6.4:1) and keeps a unified underline language across modes.

2. MentionDropdown highlighted-row ring: ring-brand-mint on bg-brand-mint/20
   is ≈2.5:1 — same failure class. Switch to ring-brand-navy (≈14.5:1
   against the highlight background) so keyboard-driven selection has a
   clearly visible indicator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:19:34 +02:00
Marcel
ba88febc77 fix(PersonMentionEditor): guard setEditable effect against re-entry loop
The disabled-state effect calls editor.setEditable, which triggers a
ProseMirror transaction → onUpdate → bind:value/mentionedPersons writes →
host re-render → child prop pass-through → effect re-fires. Without an
idempotence check, this exceeds Svelte's effect_update_depth and crashes
every consuming spec (TranscriptionBlock 22/22). Compare editor.isEditable
against the desired value first; only call setEditable when it actually
needs to change.

Follow-up to 6ef888a1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:18:40 +02:00
Marcel
fa7b97acdc test(PersonMentionEditor): assert no HTML injection via mention displayName
Adds a CWE-79 regression test: a sidecar entry whose displayName contains
an <img onerror=alert(1)> payload must round-trip through deserialize and
the Tiptap renderHTML without producing a real <img> element in the editor
DOM. Locks down the "renderHTML's third tuple entry is a text node, never
parsed as HTML" invariant so a future "use innerHTML for performance"
refactor cannot silently regress.

Nora #5618 detection-gap concern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:14:19 +02:00