The key was orphaned when TranscriptionEditView's empty state was replaced
by TranscribeCoachEmptyState. Removed from de/en/es to avoid accumulating
unreferenced strings. (Felix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleAuth in hooks.server.ts is in the sequence() chain and redirects
unauthenticated users at runtime regardless of prerender. Adding a comment
so the next reader doesn't mistake this for a security hole. (Markus/Nora)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap page content in <main> so AT users can jump to main content (Nora)
- Closing card "Fehlt eine Regel?" was <h2> after two existing <h2> siblings
but styled like a card title, not a section label; downgrade to <h3> to
fix the heading hierarchy (Sara/Leonie)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace style="grid-template-columns: 34px 1fr; align-items: start;"
with Tailwind grid-cols-[34px_1fr] items-start (Felix: inline styles)
- Add aria-label="Schritt N von 3" on each <li> so screen readers announce
step position when the numeric badge is aria-hidden (Nora/Sara)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raw hex bypassed the token system and wouldn't remap in dark mode.
Now uses --color-parchment which has a proper dark-mode counterpart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace one-shot $derived(.matches) snapshot with $state + addEventListener
so the static/animated branch reacts when the user toggles OS reduced-motion
at runtime (Felix: non-reactive media query)
- Replace bg-[#FAF8F1] raw hex with bg-parchment design token so the SVG
background remaps correctly in dark mode (Felix/Markus)
Also update TranscriptionPanelHeader.svelte.test.ts to expect role="region"
after the HelpPopover ARIA fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- role="tooltip" → role="region" + aria-label={label}: tooltip semantics
are wrong for a click-triggered panel (Nora/Sara)
- expand button to 44×44px with inner visual <span>: WCAG 2.5.8 touch
target for 60+ transcriber audience (Sara/Leonie)
- replace Math.random() with module-level counter: SSR/hydration mismatch
when server and client generate different IDs (Felix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds --c-parchment (#faf8f1 light / #041828 dark) to :root and both
dark-mode blocks, exposed as --color-parchment via @theme inline.
Prerequisite for replacing bg-[#FAF8F1] raw-hex in RichtlinienRuleCard
and TranscribeDragDemo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New coach card replaces the icon+sentence empty state in the Transcribe
panel (edit mode). Three-step guide with 5-s SMIL drawing animation in
step 1 only. Animation freezes at the final frame when
prefers-reduced-motion is active. Footer links to Wikipedia Kurrent and
the Richtlinien page open in new tabs with visible '(öffnet in neuem Tab)'
annotations. 34 new i18n keys in de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ConversationThumbnail still imported the `$lib/thumbnails` helper that
a02f6cdc deleted, so every SSR render of /briefwechsel crashed with
"Cannot find module '$lib/thumbnails'". Finish that refactor by reading
`doc.thumbnailUrl` straight off the Document DTO (same shape
DocumentThumbnail already uses), and update the spec fixtures to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
<a aria-disabled="true"> is the documented pattern but screen readers
still announce "Previous, link, disabled" on pagination bounds — noise
users don't need because the disabled state is purely visual. Switching
to <span aria-hidden="true"> removes the bound control from the AT tree
entirely (Leonie's recommendation). Visual parity preserved via a
disabledBase Tailwind class (same layout + cursor-not-allowed + opacity-40).
Tests updated: "disabled prev/next" assertions now check for aria-hidden
and no href — the active-state href/aria-current assertions are
unchanged. (#316)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
triggerSearch (local state, filter change) and buildPageHref (server data,
page nav) were each iterating over the same ~10 filter params. Any new
filter would have had to land in two places. buildSearchParams is now the
single source of truth for which params the /documents URL understands;
both callers just pass their snapshot and an optional targetPage. (#316)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frontend side of the /documents pagination work. The page.server.ts load
reads ?page= from the URL, forwards page+size=50 to the backend, and
exposes the new totalElements/pageNumber/pageSize/totalPages fields on
`data`. +page.svelte renders a <Pagination> component below the result
list; buildPageHref preserves every filter param and only updates page.
The existing triggerSearch debounce flow intentionally drops `page`
when any filter changes, so filter edits reset to page 0 automatically.
<Pagination> uses plain <a href> links (not goto) so SvelteKit's default
scroll restoration scrolls new pages to the top — the expected senior-UX
behaviour. Decorative chevrons wrapped in aria-hidden spans, 44px touch
targets, focus-visible ring, stacks vertically under 640px. The control
hides itself when totalPages ≤ 1.
Test coverage: 9 cases on Pagination (label, aria-current, prev/next
enable/disable, makeHref invocation, decorative chevron, touch target),
plus a filter-reset assertion on +page.svelte (page 5 → edit q →
goto URL must drop page=). Adds i18n keys in de/en/es. Manual edit to
api.ts pending a post-merge npm run generate:api against a rebuilt
dev backend. (#315)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
h-16 w-16 looked undersized in the 180×252 strip container (~25% of
the height). h-24 w-24 gives ~38% visual weight, matching the ratio
DocumentThumbnail uses for its lg (120×168) fallback (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Uses the same heroicon as DocumentThumbnail so the "no thumbnail yet"
signal reads identically across the app: one shape, one meaning. The
parchment SVG still lives on in the fully-empty state (no resume doc
at all), where it represents a different thing — we removed it only
from the "document exists, thumbnail not generated yet" branch (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the generic parchment SVG placeholder with an <img> pointing at
the backend's thumbnail endpoint when the document has one. The 180×252
container matches DocumentThumbnail's 5:7 A4 convention so the
dashboard tile sits visually next to the list/person-sublist tiles
instead of looking squatter than they do. dark:mix-blend-multiply keeps
paper scans from glaring on a dark page background (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The helper had a single consumer (DocumentThumbnail) and its only job
was to compose what the backend's Document.getThumbnailUrl() now
produces. Deleting it locks the single-source-of-truth invariant —
there is no longer a way to build a thumbnail URL on the client (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The backend now exposes thumbnailUrl as a serialised computed property
on Document, so the component drops its dependency on the frontend
URL-builder. PersonDocumentList's inline Doc prop type follows the
same shift (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reflects the new @JsonProperty getter on Document. Kept as a minimal
manual edit rather than a full regen because the running dev backend
belongs to the main workspace and swapping JARs there would be a
side effect on a parallel worktree's state. `npm run generate:api`
will converge on the same shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #311 dropped the right/left arrow icons that signalled whether a
letter was sent or received. Readers who don't decode the colored
left border (new users, color-blind users, users at a glance) had
no visual cue for direction. Restore a 20×20 arrow inline with the
title — right-arrow for outgoing, left-arrow for incoming — kept
decorative (aria-hidden) since the aria-label already announces
"Gesendet:" / "Empfangen:".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 168px-tall thumbnail tile was dominating rows where the text
column only rendered at text-xs / text-sm — visually the right
column sat half-empty. Three changes:
- Title: text-sm → text-lg
- Summary: text-sm → text-base
- Meta + tag chips: text-xs → text-sm
And remove the "vor N Jahren" chip entirely. The documentDate
in the meta row already carries the temporal context and the
chip was adding visual noise without new information.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Makes `max` an optional prop with default 3 — the common row-layout
case doesn't need to name the cap explicitly. ThumbnailRow's callsite
drops to `<TagChipList tags={doc.tags ?? []} />`, consistent with how
other shared components in $lib/components expose sensible defaults.
Refs #305
Fixes @leonievoss round-2 follow-up from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds row_direction_sent / row_direction_received keys across the
three locale files (de: Gesendet/Empfangen, en: Sent/Received, es:
Enviada/Recibida) and routes ThumbnailRow's directionLabel through
Paraglide. An English or Spanish screen-reader user now hears
"Sent:" / "Enviada:" in their language, matching the DistributionBar
i18n pass.
Refs #305
Fixes @leonievoss round-2 follow-up from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewires briefwechsel-rows.visual.spec.ts against the shared fixture
(seedBilateralPair + cleanupBilateralPair), adds afterAll cleanup,
and folds the conv-person-bar visibility gate into openBilateral()
so both the structural test and the snapshot block fail loudly on
a hero-state regression — matching the a11y spec's safety net.
Refs #305
Fixes @saraholt follow-ups 1 + 2 + 3 from PR round-2 review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lifts the three-API-call seeding (create sender, create receiver,
create document) out of briefwechsel-a11y.spec.ts and into a
dedicated fixtures module. The spec now calls seedBilateralPair()
in beforeAll and cleanupBilateralPair() in afterAll so the test
DB doesn't accrue seeded rows across reruns.
Two caveats captured in the helper docstring: the backend has no
person-delete endpoint (only the document is purged), and the
timestamped last names make leftover persons collision-free.
Refs #305
Fixes @saraholt follow-up 1 + 2 from PR round-2 review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends the seeding pattern from the a11y spec: beforeAll creates two
persons + one document so the page renders the row layout. The
structural test now asserts the ConversationThumbnail tile AND the
DistributionBar are present — a regression that drops to the hero
or breaks the row wiring fails here instead of silently passing a
hero-state check.
Snapshot block stays gated on VISUAL=1 (baselines captured during
review against a seeded backend) so the structural coverage ships
immediately and the pixel-diff coverage ships once baselines land.
Refs #305
Fixes @saraholt blocker 2 from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous version navigated to /briefwechsel with no params, which
renders the hero state — axe-core scanned the hero, not the new
ThumbnailRow / ConversationThumbnail / DistributionBar. This commit
seeds two persons + one document via the API in beforeAll, then
drives the URL with ?senderId=X&receiverId=Y so each of the
36 test runs (3 viewports × 2 themes × 2 assertions) actually scans
the intended DOM. Also asserts that conv-person-bar is visible first,
so a regression that drops the page back to hero fails explicitly
rather than silently passing an empty sweep.
Refs #305
Fixes @saraholt blocker 1 from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lifts the three-chip-plus-"+N" tag row out of ThumbnailRow into a
standalone TagChipList component so the chip cap + overflow policy
lives in one place and can be reused on other surfaces (document
detail header is a candidate). ThumbnailRow drops from 110 to ~90
lines and no longer owns tag-slicing logic — it just asks for the
list with max=3.
Behavior is byte-identical: same data-testid, same max cap, same
"+N" overflow indicator. All ThumbnailRow row-level tag tests
continue to pass against the new composition.
Refs #305
Fixes @felixbrandt suggestion 1 from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defaults `now` in $props() destructure so each row instance freezes
its reference time at mount, instead of calling new Date() inside
the $derived every reactivity tick. No behavioural change — the
date math is stable across re-renders for a given row — but drops
the nullish-coalesce dance and is cleaner under Storybook-style
testing where a deterministic `now` is injected.
Refs #305
Fixes @felixbrandt suggestion 3 from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
relativeYearsDe already returns "" for future dates (covered in its
own spec), but the integration wiring inside ThumbnailRow was
untested. Adds a regression that a doc with documentDate in the
future produces no "vor N Jahren" or "vor weniger als 1 Jahr" chip.
Refs #305
Fixes @saraholt concern 5 from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidates the hansPerson / annaPerson fixture into a makePerson()
factory matching the makeDoc convention, adds an assertion that
the bilateral list renders one ConversationThumbnail tile per
document (catches a broken {#each} keying wired around the
DistributionBar), and decouples the DistributionBar aria-label
assertion from the German locale now that i18n lands via Paraglide.
Refs #305
Fixes @saraholt concerns 3 + 4 from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumps the multi-page badge from text-xs (12px) / px-1.5 py-0.5 to
text-sm (14px) / px-2 py-1. Meets senior-legibility on a 320px phone
without crowding the 120-wide tile — the badge stays tucked in the
top-right corner.
Refs #305
Fixes @leonievoss senior-accessibility concern from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the hardcoded German strings ("Briefverteilung in diesem Zeitraum",
"{n} von {name}") and routes every visible + assistive-tech string
through dist_bar_aria and dist_bar_segment message keys. An English
or Spanish user now sees "from" / "de" instead of "von" both on
screen and in the aria-label their screen reader announces.
Refs #305
Fixes @leonievoss i18n concern from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this prefix, a color-blind user or screen-reader user has no
indication of correspondence direction — the colored left border is
information but not announced, and the arrow glyphs were removed in
the earlier layout pass. Prepending "Gesendet:" or "Empfangen:" to
the aria-label gives assistive-tech users the direction first so the
row identity is unambiguous even without color perception.
Refs #305
Fixes @leonievoss WCAG 1.4.1 concern from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a dedicated axe-core sweep for /briefwechsel so contrast or
semantic regressions on the new row layout fail independently of
the catch-all accessibility suite. Scoped to the main landmark so
shared chrome violations (if any) aren't double-reported.
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a Playwright spec gated on VISUAL=1 with one snapshot per
(mobile/tablet/desktop × light/dark) = 6 baselines. Snapshots stay
skipped in CI until the baseline set is captured and committed —
running `playwright test --update-snapshots briefwechsel-rows`
against a seeded backend generates them.
Structural check runs unconditionally so the file is wired into CI
today rather than waiting for the baseline capture step.
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two new assertions for the extracted DistributionBar — it must
appear in bilateral mode and stay hidden in single-person mode — and
repairs the shared makeDoc fixture: the embedded Person now carries
personType + displayName so the fixture matches the regenerated
Document schema without TypeScript complaints.
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops the inline row markup, arrow icons, status-dot helper, and the
otherPartyName helper that only fed it. Each visible row is now a
ThumbnailRow, which owns its own aria-label, border color, meta and
tag rendering. The year-divider and "new document" footer are
untouched — they were always intended to stay as timeline chrome.
Also widens the documents prop shape to include the summary, tags
and thumbnail metadata that ThumbnailRow consumes; the backend
already returns these fields via the Document schema so no server
change was required.
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Combines ConversationThumbnail with a quote-styled summary, truncated
meta line, and up to three tag chips (the rest collapsed into "+N").
The colored left border tells a reader at a glance whether this
letter left or entered the perspective person's mailbox — replacing
the previous status dot + script-type icons that were too busy for
the list view. Relative-year label ("vor 76 Jahren") is derived from
documentDate so the list carries temporal context without a full
date column.
Rendering rules:
- title falls back to originalFilename when empty
- summary uses a text expression, never {@html}, so inline markup
in the summary field is escaped (XSS regression test locks this)
- focus-visible outline + focus-within hover keep keyboard-only
users in sync with mouse hover feedback
- aria-label always pairs title with the formatted date so screen
readers hear both identifiers
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reads thumbnailAspect from the backend and swaps between a 120×168
portrait tile and a 168×120 landscape tile so postcards and photos
don't get cropped into a portrait frame. Shows a page-count badge
top-right for multi-page PDFs, and a pulsing skeleton while the
async thumbnail job hasn't run yet. URL assembly goes through the
existing thumbnailUrl helper so cache-busting stays consistent
with DocumentThumbnail.
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The correspondence timeline labels each row with its distance from today
("vor 86 Jahren"). Uses calendar-field math so the anniversary day
flips exactly — an ms-based 365.25d average misses by a day on leap
years. Invalid / future dates return "" so the caller can hide the
label rather than print "vor 0 Jahren".
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops the inline bilateral-distribution markup and the short-name /
percentage helpers that only existed to feed it. ConversationTimeline
now hands senderName, receiverName, and the two counts to the shared
component and lets it own the rendering.
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>