Commit Graph

529 Commits

Author SHA1 Message Date
Marcel
f06d034b36 fix(bulk-upload): i18n hardcoded strings in BulkDropZone and FileSwitcherStrip
- Add bulk_drop_desc, bulk_select_files, bulk_drop_zone_label, bulk_remove_file
  keys to de/en/es message files
- BulkDropZone: use m.bulk_drop_zone_label(), m.bulk_drop_desc(),
  m.bulk_select_files() — removes all hardcoded German
- FileSwitcherStrip: use m.bulk_remove_file() on × button; move aria-live
  from <ul> to a dedicated visually-hidden region above the strip (screen
  readers now announce changes without coupling the live region to the list)
- Spec: import FileEntry from component instead of re-declaring; use
  data-remove-id selector instead of hardcoded German aria-label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
ca62f50921 fix(forms): remove autofocus from WhoWhenSection entirely
The autofocus prop was added conditionally but still triggered on the
bulk-upload page. Removing it completely — callers that need focus
management can handle it independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
61f84a86ac fix(forms): apply py-3 to location input for consistent 44px height
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
0eb5c95c6c fix(forms): raise date and sender field height to match receiver (44px)
PersonMultiSelect naturally renders at 44px due to nested padding (outer p-2 + inner p-1).
Apply py-3 px-2 to the date input and PersonTypeahead default mode so all three fields
align visually.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
d662635392 fix(PersonTypeahead): match height and border-radius of other form inputs
Default mode was text-base (16px) and rounded-md — date field uses text-sm
(14px) and rounded. Aligning these makes Sender/Date/Receiver rows consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
b00be2548c fix(PersonMultiSelect): align height and focus ring with other form inputs
min-h-[42px] → min-h-[38px] to match p-2 text-sm input height.
Add shadow-sm (was missing vs date/sender inputs).
focus-within:ring-1 ring-ink → focus-within:ring-2 ring-focus-ring to match
the focus style used consistently across all other form inputs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
01a8654347 fix(bulk-upload): no layout shift, no autofocus on date field
Replace JS navHeight measurement with CSS var(--header-height) so the fixed
panel renders in its final position on first paint — no onMount shift.

Add autofocus prop to WhoWhenSection (default true, preserves document-edit
behaviour) and pass autofocus={false} from BulkDocumentEditLayout so the date
field does not steal focus before the user has even dropped any files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
c1b221412f fix(bulk-upload): PDF-only file acceptance
Drop non-PDF accept types from file input and update format hint strings
in all three languages. JPEG/PNG/TIFF were never officially supported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
76c14ea604 fix(bulk-upload): form layout polish and drop zone sizing
- Drop zone box doubled: max-w-xl, larger icon (80px), bigger padding and text
- Title field wrapped in its own card (matches WhoWhenSection/DescriptionSection)
- Removed double-wrapping outer card around WhoWhenSection + DescriptionSection
- Added space-y-4 between form sections for consistent breathing room
- ScopeCard per-file label: text-accent → text-primary for legible contrast in light theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
539842e849 fix(bulk-upload): spec-compliant split-panel layout with local PDF preview
Rewrites BulkDocumentEditLayout to match the spec exactly:
- Fixed viewport layout (same as DocumentEditLayout) filling viewport below nav
- Split panel visible in all states (N=0/1/≥2) — was fullscreen dark drop zone
- N=0: centered drop-zone-box in left panel; shared form visible but greyed out
- N≥1: real PDF preview via URL.createObjectURL (no server upload required)
- N≥2: FileSwitcherStrip at bottom of left panel; count pill + discard in topbar
- FileEntry gains previewUrl; blob URLs created on add, revoked on remove/destroy
- save() checks response.ok and marks failed files with status: 'error'
- BulkDropZone redesigned: spec-accurate box with circular mint icon, serif title
- FileSwitcherStrip: number badges, arrows, keyboard nav via data-chip-id selector
- ScopeCard, UploadSaveBar: hardcoded German replaced with Paraglide i18n keys
- +page.svelte simplified to bare component render (layout is self-contained)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
ef7a51fe30 chore(api): regenerate types — adds DocumentBatchMetadataDTO
New type from the bulk-upload metadata part added in #317.
Generated from backend running with --spring.profiles.active=dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
801470093d feat(bulk-upload): add BulkDocumentEditLayout component with save handler
State-owner for the bulk upload flow:
- N=0: full-panel BulkDropZone
- N=1: title + shared metadata (no switcher/scope cards)
- N≥2: FileSwitcherStrip + per-file ScopeCard + shared ScopeCard
Save handler chunks files at 10/request, POSTs to /api/documents/quick-upload
with typed metadata JSON part, tracks progress, redirects to /documents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
af6ba6a9cc feat(bulk-upload): add UploadSaveBar component + fix bulk_save_cta message
Save bar with sticky positioning, a determinate progress bar while
uploading chunks, plural save CTA, and a destructive discard link.
Replaces broken ICU plural in bulk_save_cta with two-key approach
(bulk_save_cta_one / bulk_save_cta) since Paraglide 2.5 does not support
ICU plural syntax.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
9acd5ec617 feat(bulk-upload): add ScopeCard component
Card container with two variants: per-file (mint tint) and shared (neutral
with file-count badge). Used to visually separate per-file vs shared
metadata sections in the bulk upload layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
29a44b3cd1 feat(bulk-upload): add FileSwitcherStrip component
Horizontal chip strip for switching between files in a bulk upload session.
Supports keyboard navigation (arrow keys cycle within the strip), error state
chips, and onSelect/onRemove callbacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
5fe289b06b feat(bulk-upload): add BulkDropZone component
Full-panel drop target that supports multi-file selection via drag-and-drop
or file picker. Fires onFilesAdded callback with the full File array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
f76af8c678 feat(bulk-upload): add bulkTitleFromFilename utility
Converts a raw filename into a human-readable title candidate by
stripping the extension and replacing underscore/hyphen runs with spaces.
Reuses the existing stripExtension() helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
69c739c6e3 feat(i18n): add BATCH_TOO_LARGE error code + 16 bulk-upload Paraglide keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
48d034dcb8 fix(transcribe-coach): propagate hover from 44px button group to inner span
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
hover: on the <span> only fired on the 20×20px visual circle, not the
full 44×44px touch target. Add `group` + `focus-visible:ring-*` to the
outer button; switch to `group-hover:` on the inner span so the visual
response covers the entire interactive area.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:02 +02:00
Marcel
5b7c37391c test(HelpPopover): use userEvent.keyboard for Enter/Space tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:02 +02:00
Marcel
077f5c85df fix(TranscribeCoachEmptyState): Tailwind grid instead of inline styles; step aria-labels
- 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>
2026-04-25 12:24:02 +02:00
Marcel
018e272a3b fix(RichtlinienRuleCard): bg-[#FAF8F1] → bg-parchment design token
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>
2026-04-25 12:24:02 +02:00
Marcel
0c4a0ead7b fix(TranscribeDragDemo): reactive prefersReducedMotion + bg-parchment token
- 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>
2026-04-25 12:24:02 +02:00
Marcel
82b12d4383 fix(HelpPopover): role=region, 44px touch target, counter-based ID
- 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>
2026-04-25 12:24:02 +02:00
Marcel
b234db0472 feat(richtlinien): add /hilfe/transkription page with RichtlinienRuleCard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:28:25 +02:00
Marcel
7c3a8e7651 feat(transcribe): add HelpPopover primitive and wire (?) chip into panel header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:19:48 +02:00
Marcel
dff203d526 feat(transcribe): wire coach into TranscriptionEditView, hide training footer when empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:06:50 +02:00
Marcel
86584a53a8 feat(transcribe): add TranscribeCoachEmptyState and TranscribeDragDemo components
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>
2026-04-24 21:00:01 +02:00
Marcel
6e021fb23a fix(briefwechsel): repair 500 by consuming backend thumbnailUrl directly
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m46s
CI / OCR Service Tests (push) Successful in 29s
CI / Backend Unit Tests (push) Failing after 2m56s
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>
2026-04-24 13:27:19 +02:00
Marcel
18b88672ec fix(pagination): bound controls render as aria-hidden spans — address @leonievoss
<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>
2026-04-24 13:20:24 +02:00
Marcel
78ac5d663d feat(documents): paginate search with a Pagination control
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>
2026-04-24 13:20:24 +02:00
Marcel
7007491d8c style(dashboard): address @leonievoss — scale fallback icon to match larger container
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m26s
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 3m7s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 3m17s
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>
2026-04-24 07:26:23 +02:00
Marcel
72cd6f5bbc feat(dashboard): fall back to document-text heroicon when no thumbnail yet
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>
2026-04-24 07:26:23 +02:00
Marcel
1d44bbb1bd feat(dashboard): render real document thumbnail in resume strip
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>
2026-04-24 07:26:23 +02:00
Marcel
a02f6cdcd7 refactor(thumbnails): drop frontend URL-builder now that backend owns the convention
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>
2026-04-24 07:26:23 +02:00
Marcel
817749889a refactor(document-thumbnail): read doc.thumbnailUrl instead of composing locally
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>
2026-04-24 07:26:23 +02:00
Marcel
a8b9133b80 chore(api): regenerate Document type with thumbnailUrl field
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>
2026-04-24 07:26:23 +02:00
Marcel
0b33f323ee feat(briefwechsel): restore direction arrow next to row title
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m45s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m56s
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>
2026-04-23 21:57:57 +02:00
Marcel
334b624063 feat(briefwechsel): bump row typography and drop relative-year chip
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m50s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Has been cancelled
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>
2026-04-23 21:52:23 +02:00
Marcel
503ce49ef7 refactor(briefwechsel): TagChipList defaults max to 3
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m51s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 2m56s
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>
2026-04-23 21:38:56 +02:00
Marcel
f5a30c71b7 i18n(briefwechsel): ThumbnailRow direction label via Paraglide
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>
2026-04-23 21:38:56 +02:00
Marcel
a7ab5e6e69 refactor(briefwechsel): extract TagChipList from ThumbnailRow
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>
2026-04-23 21:38:56 +02:00
Marcel
9ecf7f4dfc refactor(briefwechsel): ThumbnailRow captures now at prop binding
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>
2026-04-23 21:38:56 +02:00
Marcel
01bfc59849 test(briefwechsel): lock future-date relative-year hiding at the row layer
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>
2026-04-23 21:38:56 +02:00
Marcel
7090f9a0e0 feat(briefwechsel): ConversationThumbnail page badge legible at small sizes
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>
2026-04-23 21:38:56 +02:00
Marcel
d4617a96d1 i18n(briefwechsel): DistributionBar reads text + aria-label via Paraglide
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>
2026-04-23 21:38:56 +02:00
Marcel
b9dda9a938 feat(briefwechsel): ThumbnailRow aria-label leads with Gesendet/Empfangen
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>
2026-04-23 21:38:56 +02:00
Marcel
cc118ffb16 feat(briefwechsel): add ThumbnailRow for the new correspondence row layout
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>
2026-04-23 21:38:56 +02:00
Marcel
407bfbd5f1 feat(briefwechsel): add ConversationThumbnail with aspect + page badge
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>
2026-04-23 21:38:56 +02:00
Marcel
a52d481a8e feat(relativeTime): add relativeYearsDe helper for historical letter dates
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>
2026-04-23 21:38:56 +02:00