Both PersonMultiSelect and DocumentMultiSelect remove buttons were ~12px tap
targets (below the 44px WCAG minimum) — pad them to min-h/min-w 44px with a
focus-visible ring (SVG stays 12px). Add an optional emptyLabel slot inside the
chip container and a hiddenInputName prop on PersonMultiSelect (mirroring
DocumentMultiSelect) so EventForm can wire personIds without touching
WhoWhenSection. Document the intentional bare typeahead fetch in
documentTypeahead.ts (same-origin in prod, Vite-proxied in dev).
Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls the date + precision + RANGE-end-date region into a generic primitive in
$lib/shared/primitives/ so both document/ (WhoWhenSection) and timeline/
(EventForm, #781) can consume it without a cross-domain import. Preserves the
aria-live="polite" outer wrapper, onMount one-time seeding, $bindable
precision/endDateIso, the PRECISIONS array, and forwards data-testid attributes
so the existing WhoWhenSection spec selectors survive. WhoWhenSection spec stays
green (7/7).
Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a RANGE-reveal regression test to WhoWhenSection's spec. The existing
spec covered only date pre-fill / hideDate / location, leaving the end-date
region without a red signal. This must stay green across the #781 extraction
of DatePrecisionField into $lib/shared/primitives/.
Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NVDA+Chrome <2022 and VoiceOver iOS <16 need explicit aria-modal="true";
showModal() implicit modal semantics are not enough for older AT. One-line
patch benefits all dialog uses.
Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
REQ-024 was updated (issue #779) to require localized sr-only/aria
labels instead of German-only. Pin the de/en/es values so they cannot
silently drift back to the German source strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timeline_layer_* and timeline_derived_* shipped German values in the
English and Spanish catalogs, so EN/ES screen-reader users heard German
for the world/family layer and birth/death/marriage cues. Translate them;
de.json stays canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The undated bucket is assembled from all entries, so it can contain
events as well as letters. Rendering every undated entry with LetterCard
produced a dead /documents/undefined link and "Unknown -> Unknown" for
events. Dispatch on kind/type like YearBand does (WorldBand/EventPill/
LetterCard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the per-entry {#each} key logic into a shared entryKey.ts so the
undated bucket in TimelineView can reuse it. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
break-words on sender/receiver/title so a 25+char correspondent name cannot
force horizontal overflow on a 320px phone (REQ-005).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renders year bands in DTO order with interior empty-year runs folded into one
GapSpan (REQ-015), a single <ol> in chronological DOM order (REQ-006), the undated
bucket via {#if} (REQ-016), and a calm empty state (REQ-017). personId is a
declared seam (issue #10), undefined here, never passed to leaf cards (REQ-025).
Centered desktop spine / left phone spine via scoped CSS. Owns no <main>.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One <section> per year with a sticky <h2> at top:4rem (REQ-006). Events render in
DTO order as pills/bands; letters render as individual cards while <= 12 (REQ-011)
or collapse to one density strip above that (REQ-012); DTO order is never re-sorted
(REQ-003). Letters carry an alternating data-side for the centered desktop axis
(REQ-004); single left column on phone (REQ-005). Derived-safe {#each} key.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Letter count + 12-month density sparkline + a >=44px keyboard-focusable expand
toggle that reveals that year's LetterCards (REQ-012). Sparkline values from the
shared monthHistogram.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A thin dashed span rendering '{from}–{to} · keine Einträge', collapsing to a
single year when the run has length 1 (REQ-015).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-width muted band; RANGE renders a span pill (1914–1918) with a Zeitraum
aria-label (REQ-009); a RANGE with no end degrades to the start year, no pill,
no crash (REQ-010). World glyph is a redundant non-color cue with sr-only label
(REQ-018); text uses text-ink-2 to hold AA in both themes (REQ-019).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centered axis pill: derived life-events (* Geburt / † Tod / ⚭ Heirat) and curated
PERSONAL events (★, mint border) via getAccentConfig. Glyph wrapped aria-hidden +
sr-only label (REQ-018). Edit affordance only for a curated event with eventId,
never derived/null (REQ-008). REQ-007.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single archive letter: sender → receiver (Unbekannt fallback for empty names,
REQ-014), title, precision date chip via timelineDateLabel (omitted when null,
REQ-013), linking to exactly /documents/{documentId} with no target (REQ-023).
44px touch target enforced inline + focus-visible ring (REQ-020). OCR/import
text via {...} escaping + whitespace-pre-line, no {@html} (REQ-021).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getAccentConfig(entry) maps each EVENT to its glyph (* / † / ⚭ / ★ / ◍), German
redundant-cue label, and accent kind (REQ-007/008/018). test-factories build
TimelineEntryDTO/TimelineDTO mirroring the real wire shape for component specs.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A minimal presentational bar series (one bar per value, heights scaled to the
max, faint floor for empty buckets). Lives in shared so both the timeline
density strip and the document chart can use it. REQ-012 (supports).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isDense(count) thresholds dense year bands at >12 letters (REQ-012);
monthHistogram(letters, year) buckets a band's letters into exactly 12 month
buckets via the shared fillDensityGaps, counting each letter on its eventDate
anchor month and ignoring undated entries (REQ-027). Imports shared only.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate the 10 pure helpers (monthBoundaryFrom/To, buildMonthSequence,
fillDensityGaps, clipBucketsToRange, aggregateToYears, selectionBoundaryFrom/To,
tickIndicesFor, formatTickLabel) and their unit tests out of document/timeline.ts
into a shared module so lib/timeline/ can consume them without importing
lib/document/. The /api/documents/density glue (buildDensityUrl, fetchDensity,
DensityState, DensityFilters) stays in document/timeline.ts. Re-point the three
density components and the density-filter spec at the shared module.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Note above the DatePrecision type that it mirrors the Java DatePrecision enum,
must be updated manually in lockstep with that enum, and must not be migrated
to the OpenAPI-generated type — it drives the shared client-side formatter
shared by documents and the timeline date-label facade.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timelineDateLabel delegates to the shared formatDocumentDate so a timeline
chip renders identically to the same date on a document, in the active
locale (REQ-001/REQ-002). UNKNOWN precision and null/undefined/'' eventDate
short-circuit to null with no formatter call (REQ-003/REQ-004); raw is always
null since timeline events carry no verbatim spreadsheet cell. The facade
owns no precision logic of its own (REQ-005).
Register the new `timeline` frontend domain in the eslint boundaries config
(allowed to import only `shared`) and add src/lib/timeline/** to the vitest
coverage include (REQ-006). The spec partially mocks the paraglide runtime
via importOriginal so getLocale is stubbed while the formatter still resolves
real season/range message exports.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Red phase for the timeline date-label helper. Asserts delegation to the
shared formatDocumentDate (localized DAY de/en, SEASON de, same-year RANGE)
and the null cases for UNKNOWN/empty eventDate. The runtime mock path keeps
the `.js` suffix so it matches the import under test.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regenerated frontend/src/lib/generated/api.ts from the OpenAPI spec — adds the
/api/timeline/events paths and TimelineEventRequest/TimelineEventView schemas.
CI has no OpenAPI drift guard, so the regen is committed here. (Operation-id
churn create->create_1 etc. is cosmetic; the typed client keys off paths, not
operation ids; the timeline PersonView merges with geschichte's identical one.)
Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
errors.ts ErrorCode union + getErrorMessage() cases for the four new codes,
with de/en/es i18n keys. Conflict messages are calm/recoverable
('...wurde zwischenzeitlich geändert. Bitte neu laden.'). Per #775.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A partial date (e.g. "14.03.") left the hidden ISO input empty, so
saving the edit form silently cleared a stored date. PersonLifeDateField
now delegates to the shared DateInput primitive (inline format error,
calendar validation) and sets a custom validity while the error is
present, so the browser blocks native submission for both person forms.
A full clear stays submittable - that is the intentional clear path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dropdown and editor typed /api/persons list items as the full Person
entity. The actual wire shape is PersonSummaryDTO, which until the
previous commit had no date fields - so the life-date subtitle rendered
blank in production while fixtures (built from the entity type) kept the
tests green. Retype items as the summary projection and guard the two
personId consumers against the schema-optional id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
birthDatePrecision/deathDatePrecision are @Schema REQUIRED, so the
generated Person type makes them non-optional — fixtures that were
type-clean before the regen get UNKNOWN defaults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New PersonLifeDateField (German date input + hidden ISO + DAY/MONTH/YEAR
precision select, min-h-44px, sm: side-by-side) used for birth and death
in both forms. Legacy APPROX precision seeds the select as YEAR so an
untouched save never claims DAY. Server actions send date+precision
pairs or omit both; obsolete year i18n keys removed, 9 form keys added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cards compose aria-hidden * / † glyphs in markup so screen readers only
announce the dates; PersonSummaryDTO list card stays year-shaped by
design (ADR-039). MentionDropdown subtitle wraps instead of truncating
so DAY-precision ranges fit at 320px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New formatLifeDate single-date helper carries no glyph so cards can wrap
* / † in aria-hidden spans. Missing precision falls back to YEAR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Person gains birthDate/deathDate + required precision enums;
PersonSummaryDTO, PersonNodeDTO, and RelationshipDTO keep derived
integer years. familyForest/buildLayout tests still pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend enum, frontend ErrorCode mirror, getErrorMessage cases, and
error message i18n keys (de/en/es) incl. the mixed-precision workaround
hint in error_birth_after_death.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manual update since Docker compose backend runs old build; regenerate with
npm run generate:api once new backend is deployed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strengthen one renderTranscriptionBody case into the AC-6 contract: a
@DisplayName with an empty mentionedPersons array (the deleted-person case
V71 produces) must render as plain readable text with no <a>, person-mention
class, data-person-id, or href. Guards against a future renderer refactor
silently reintroducing the dead-link-on-deleted-person degradation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #718 keyboard-tab-order test hardcoded the visual order
['Eugenie','Walter','Clara','Hans'] on the assumption that buildLayout
sorts each generation alphabetically. #724 replaced that with the
tidy-tree layout, which orders a couple's run by structural ownership
(earliest birth year, then a deterministic id tie-break) — so Walter
(id …a1) now owns the run and Eugenie renders to his right.
Both PRs were green independently; the stale assertion only surfaced
once #718 and #724 landed together on main. Correct the expected reading
order to ['Walter','Eugenie','Clara','Hans'] and refresh the now-wrong
'alphabetical' comment. The companion self-validating test (DOM order ==
sorted by y,x) already guarded the real property, so only the hardcoded
assertion needed updating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-up (Sara, fast-follow): the t no-active-region guard and the
draw-cue arm/disarm rule lived inline in the page with no direct coverage.
Extracted to pure resolveTrainingMark() (no-op when no region; recognition
enrolled flip) and canArmDraw()/shouldDisarmDraw(), each with unit tests
(10 cases total). The page now arms the draw cue only via canArmDraw and
disarms via shouldDisarmDraw, and routes t through resolveTrainingMark.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review follow-up (Leonie, Felix, Markus): bump cheatsheet key caps to text-sm
for the 60+ audience, add a focus-visible ring to the close button, simplify
the draw-hint guard to {#if drawArmed} (the $effect already clears it outside
edit mode), and document why the transcribeShortcuts action ignores its node
and binds to window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review follow-up (Sara): the prior single-owner evidence was two separate
unit facts against an inert DOM stub. This renders a real AnnotationShape,
attaches the live transcribeShortcuts action, focuses the region, and presses
Delete once — asserting deleteCurrentRegion fires exactly once. A genuine
integration guard against re-introducing a double-bind.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review follow-up (Sara): j/k wrap-around and fresh-entry had no direct
coverage — the logic lived inline in the page where the action spec only
mocks the callbacks. Extracted to a pure stepRegion() with 9 unit tests
(empty list, forward/back, both wraps, fresh-entry null + unknown id,
length-1). Also replaces the inline nested ternary Felix flagged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review follow-up (Requirements Engineer, Leonie) — closes the unmet
acceptance row. The coach card's "press ?" tip rendered unconditionally, so
a touch-only tablet transcriber (no hardware keyboard) was told to press a
key they don't have. The hint is now gated behind a fine-pointer media
query ([@media(pointer:coarse)]:hidden); the cheatsheet itself only opens
via the "?" key, so it already never surfaces without a keyboard. Also bumps
the key cap from 11px to text-xs for the 60+ audience.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review follow-up (Leonie, Requirements Engineer): the Delete key cap was a
hardcoded German "Entf" shown to EN/ES users — now driven by key_cap_delete
(Entf/Del/Supr). The annotation read-only aria-label was a hardcoded German
"Block anzeigen" in all locales — now annotation_view_label. Renamed the Esc
row label from "Bereich schließen" to "Panel schließen" so it no longer
collides with "Bereich" (= region) used elsewhere in the cheatsheet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>