statusLabel() was a one-line alias for formatDocumentStatus() with no
additional behaviour. Remove it and update DocumentStatusChip.svelte to
call formatDocumentStatus() directly. Remove the corresponding alias
test suite from the spec file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds src/lib/tag/__fixtures__/cross-domain.fixture.ts — a permanent fixture
that demonstrates the boundaries rule firing on a tag → person import. The
fixture is excluded from npm run lint via --ignore-pattern; run
npm run lint:boundary-demo to see it produce an error (exit 1).
Documents the full allow-list, the escape hatches ($lib/shared/ move, explicit
rule entry, eslint-disable-next-line), and the verify command in COLLABORATING.md.
Refs #410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds eslint-plugin-boundaries with one element type per Tier-1 domain and an
explicit allow-list encoding the architectural dependency graph:
- document may import from: shared, person, tag, ocr, activity, conversation
- geschichte may import from: shared, person, document
- ocr may import from: shared, document
- activity may import from: shared, notification
- all others (person, tag, user, notification, conversation): shared only
- routes may import from any domain
Default is 'disallow', so any unlisted cross-domain import is an error.
Two eslint-disable-next-line comments remain in shared/discussion where
person-domain helpers (getInitials, formatLifeDateRange) are needed to render
participant metadata; moving them to shared would lose the person-type context.
Closes#410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MissionControlStrip is a document-processing pipeline visualiser — it
imports document-domain components (SegmentationColumn, TranscriptionColumn,
ReadyColumn) and belongs in the document domain. It was placed in
shared/dashboard, creating a shared → document coupling that the upcoming
boundaries rule would block.
Refs #410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FieldLabelBadge is a generic UI primitive (additive/replace badge used in form
field labels). It lived in the document domain but was already imported by
PersonTypeahead (person domain), creating a person → document coupling.
Moving it to shared/primitives eliminates that cross-domain dependency.
Refs #410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These functions describe DocumentStatus display logic (dot colours, readable
labels) and belong in the document domain. They were incorrectly placed in
personFormat.ts. Moving them to documentStatusLabel.ts removes the
person → document dependency and prepares the codebase for the
boundaries/dependencies ESLint rule.
Refs #410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>