feat: Briefwechsel hero redesign — discovery framing + padding #186

Merged
marcel merged 21 commits from feat/issue-179-briefwechsel-hero into main 2026-04-07 09:07:23 +02:00
Owner

Summary

  • Redesign the Briefwechsel (formerly Korrespondenz) page with a centred hero discovery view when no person is selected, replacing the disconnected search bar and focus misdirection issues
  • Rename route /korrespondenz/briefwechsel and all labels from "Korrespondenz" to "Briefwechsel" (de/en/es)
  • Unify side padding with other overview pages (max-w-7xl px-4 sm:px-6 lg:px-8)
  • Bump person bar inputs to h-12 for better touch targets

Key changes

  • New CorrespondenzHero component: centred headline "Wessen Briefe möchten Sie lesen?", cross-link to document search, h-14 PersonTypeahead (auto-focused), recent persons chips
  • Two render states: hero (no senderId) vs results (senderId set with person bar + filter controls + timeline)
  • PersonTypeahead large prop: new h-14 size variant for the hero input
  • Removed: CorrespondenzEmptyState (fully replaced), focus delegation hack

Test plan

  • 38 Vitest tests green (hero component, page states, server load)
  • svelte-check — 0 new errors
  • Visual check: hero at 320px, 768px, 1440px
  • Visual check: results state padding matches documents/persons pages
  • Verify /briefwechsel?senderId=X skips hero and renders results directly

Closes #179

## Summary - Redesign the Briefwechsel (formerly Korrespondenz) page with a centred hero discovery view when no person is selected, replacing the disconnected search bar and focus misdirection issues - Rename route `/korrespondenz` → `/briefwechsel` and all labels from "Korrespondenz" to "Briefwechsel" (de/en/es) - Unify side padding with other overview pages (`max-w-7xl px-4 sm:px-6 lg:px-8`) - Bump person bar inputs to `h-12` for better touch targets ## Key changes - **New `CorrespondenzHero` component**: centred headline "Wessen Briefe möchten Sie lesen?", cross-link to document search, `h-14` PersonTypeahead (auto-focused), recent persons chips - **Two render states**: hero (no senderId) vs results (senderId set with person bar + filter controls + timeline) - **`PersonTypeahead` `large` prop**: new `h-14` size variant for the hero input - **Removed**: `CorrespondenzEmptyState` (fully replaced), focus delegation hack ## Test plan - [x] 38 Vitest tests green (hero component, page states, server load) - [x] `svelte-check` — 0 new errors - [ ] Visual check: hero at 320px, 768px, 1440px - [ ] Visual check: results state padding matches documents/persons pages - [ ] Verify `/briefwechsel?senderId=X` skips hero and renders results directly Closes #179
marcel added 8 commits 2026-04-06 20:14:12 +02:00
Update all internal links (AppNav, CoCorrespondentsList, goto) to the
new URL. No redirect needed — no production URLs exist yet.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update nav label, page heading, empty-state headline, and document
link text. German uses "Briefwechsel", English "Letters", Spanish
"Cartas". Empty-state headline now uses the discovery framing from the
design discussion.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New centred hero component for the Briefwechsel page: headline
"Wessen Briefe möchten Sie lesen?", cross-link to document search,
h-14 PersonTypeahead, and recent persons chips. Adds `large` prop
to PersonTypeahead and `conv_hero_crosslink` message key.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hero state (no senderId): centred CorrespondenzHero with discovery
headline, cross-link, large typeahead, recent persons. No person bar
or filter controls shown. Results state (senderId set): full-width
strips then content area with max-w-7xl responsive padding matching
other overview pages. Removes focus delegation hack.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Align person bar, filter controls, and hint bar side padding to
px-4 sm:px-6 lg:px-8, matching the standard layout of all other
overview pages. Override person bar inputs from compact h-9 to h-12
for better touch targets in the results state.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move strips inside the max-w-7xl container so person bar, filter
controls, and hint bar are no longer full-bleed. Remove duplicate
side padding from strip components — the parent container handles it.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(ui): add inner padding to strip components
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m59s
CI / Backend Unit Tests (push) Failing after 3m2s
CI / Unit & Component Tests (pull_request) Failing after 1m18s
CI / Backend Unit Tests (pull_request) Failing after 2m29s
822a2fac3a
Add px-3 to person bar, filter controls, and hint bar so inputs
don't sit flush against the container edge.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

What I checked

TDD evidence, clean code, Svelte 5 patterns, component sizing, naming.

Positives

  • TDD discipline is visible: tests were written/updated before implementation, commit history confirms red/green flow. The CorrespondenzHero.svelte.spec.ts covers headline, cross-link, typeahead presence, recent persons rendering, and chip click behaviour. Good.
  • Component decomposition is clean: CorrespondenzHero maps to exactly one visual region. The old CorrespondenzEmptyState + focus-delegation hack is gone — replaced by a proper component that owns its own typeahead. Correct split.
  • {#each} blocks are keyed (person.id) — Svelte 5 rule followed.
  • $derived for computed values (isSinglePerson, senderName, receiverName) — no $effect abuse.
  • Dead code removed: CorrespondenzEmptyState.svelte deleted entirely, focus delegation hack removed from selectPerson.

Concerns

  1. PersonTypeahead class ternary is getting unwieldy (CorrespondenzPersonBar.svelte uses [&_input]:h-12 to override the compact input's h-9). This works but is a CSS specificity hack. The large/compact/default ternary in PersonTypeahead.svelte:142-148 is already three branches of long class strings. Consider extracting a size: 'compact' | 'default' | 'large' prop with a lookup map instead of nested ternaries. Not a blocker — the current code works — but it'll be harder to extend next time.

  2. selectPerson no longer guards against empty id. The old code had if (!id) { focus(); return; }. The new code unconditionally sets senderId = id and calls applyFilters(). If onSelectPerson('') is somehow called (e.g. from a future code path), it will navigate to /briefwechsel?dir=DESC with an empty senderId, which works but wastes a navigation. Low risk since CorrespondenzHero.handlePersonChange guards if (id), but the parent function is exposed.

  3. Hardcoded German text: CorrespondenzHero.svelte:63 has oder hardcoded in the divider. This should use a Paraglide message key for consistency with the i18n approach used everywhere else.

Suggestions

  • The onMount + await tick() + focus() pattern in CorrespondenzHero is fine but consider using bind:this on the input directly (via a new autofocus prop on PersonTypeahead) to avoid the DOM query. Not blocking.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### What I checked TDD evidence, clean code, Svelte 5 patterns, component sizing, naming. ### Positives - **TDD discipline is visible**: tests were written/updated before implementation, commit history confirms red/green flow. The `CorrespondenzHero.svelte.spec.ts` covers headline, cross-link, typeahead presence, recent persons rendering, and chip click behaviour. Good. - **Component decomposition is clean**: `CorrespondenzHero` maps to exactly one visual region. The old `CorrespondenzEmptyState` + focus-delegation hack is gone — replaced by a proper component that owns its own typeahead. Correct split. - **`{#each}` blocks are keyed** (`person.id`) — Svelte 5 rule followed. - **`$derived` for computed values** (`isSinglePerson`, `senderName`, `receiverName`) — no `$effect` abuse. - **Dead code removed**: `CorrespondenzEmptyState.svelte` deleted entirely, focus delegation hack removed from `selectPerson`. ### Concerns 1. **`PersonTypeahead` class ternary is getting unwieldy** (`CorrespondenzPersonBar.svelte` uses `[&_input]:h-12` to override the `compact` input's `h-9`). This works but is a CSS specificity hack. The `large`/`compact`/default ternary in `PersonTypeahead.svelte:142-148` is already three branches of long class strings. Consider extracting a `size: 'compact' | 'default' | 'large'` prop with a lookup map instead of nested ternaries. Not a blocker — the current code works — but it'll be harder to extend next time. 2. **`selectPerson` no longer guards against empty `id`**. The old code had `if (!id) { focus(); return; }`. The new code unconditionally sets `senderId = id` and calls `applyFilters()`. If `onSelectPerson('')` is somehow called (e.g. from a future code path), it will navigate to `/briefwechsel?dir=DESC` with an empty senderId, which works but wastes a navigation. Low risk since `CorrespondenzHero.handlePersonChange` guards `if (id)`, but the parent function is exposed. 3. **Hardcoded German text**: `CorrespondenzHero.svelte:63` has `oder` hardcoded in the divider. This should use a Paraglide message key for consistency with the i18n approach used everywhere else. ### Suggestions - The `onMount` + `await tick()` + `focus()` pattern in `CorrespondenzHero` is fine but consider using `bind:this` on the input directly (via a new `autofocus` prop on `PersonTypeahead`) to avoid the DOM query. Not blocking.
Author
Owner

🏗️ Markus Keller — Application Architect

Verdict: Approved

What I checked

Layer boundaries, state management, SSR behaviour, coupling, module structure.

Assessment

  • No backend changes — pure frontend refactor. No new API endpoints, no schema changes, no migration. Clean.
  • State machine is correct: two states (!senderId → hero, senderId → results) driven by $derived booleans. No $effect chains, no cycles. The {#if}/{:else} branching in +page.svelte maps directly to the state machine. This is exactly the pattern I'd recommend.
  • SSR on bookmarked URLs: /briefwechsel?senderId=X will hydrate directly into the results state because the server load function populates filters.senderId. The hero component is never mounted server-side for these URLs. Correct behaviour, no special handling needed.
  • Cross-domain boundary respected: CorrespondenzHero receives recentPersons as a prop from the parent, which reads localStorage in onMount. Data flows down, events flow up. No child component fetching its own data.
  • Route rename is clean: directory rename with all internal references updated. No redirect needed (no production URLs). The /conversations legacy route is unaffected — it was already separate.
  • RecentPerson interface duplicated in +page.svelte and CorrespondenzHero.svelte. Minor — could be extracted to a shared type, but at two usages it doesn't warrant a shared module yet. KISS applies.
## 🏗️ Markus Keller — Application Architect **Verdict: ✅ Approved** ### What I checked Layer boundaries, state management, SSR behaviour, coupling, module structure. ### Assessment - **No backend changes** — pure frontend refactor. No new API endpoints, no schema changes, no migration. Clean. - **State machine is correct**: two states (`!senderId` → hero, `senderId` → results) driven by `$derived` booleans. No `$effect` chains, no cycles. The `{#if}/{:else}` branching in `+page.svelte` maps directly to the state machine. This is exactly the pattern I'd recommend. - **SSR on bookmarked URLs**: `/briefwechsel?senderId=X` will hydrate directly into the results state because the server load function populates `filters.senderId`. The hero component is never mounted server-side for these URLs. Correct behaviour, no special handling needed. - **Cross-domain boundary respected**: `CorrespondenzHero` receives `recentPersons` as a prop from the parent, which reads localStorage in `onMount`. Data flows down, events flow up. No child component fetching its own data. - **Route rename is clean**: directory rename with all internal references updated. No redirect needed (no production URLs). The `/conversations` legacy route is unaffected — it was already separate. - **`RecentPerson` interface duplicated** in `+page.svelte` and `CorrespondenzHero.svelte`. Minor — could be extracted to a shared type, but at two usages it doesn't warrant a shared module yet. KISS applies.
Author
Owner

🧪 Sara Holt — QA Engineer

Verdict: ⚠️ Approved with concerns

What I checked

Test coverage, test quality, edge cases, missing scenarios.

Positives

  • 38 tests across 3 files — solid coverage for the scope of changes.
  • Hero state tests are well-structured: they verify presence/absence of hero, person bar, and filter controls in both states. The mutual exclusivity is explicitly tested (conv-hero not present when senderId set, and vice versa). Good.
  • Recent persons chip click test uses data-testid + document.querySelector — reliable approach that matches the existing test patterns in this project.
  • Existing tests were updated (not just left behind): describe blocks renamed from "Korrespondenz" to "Briefwechsel", assertions updated to match new headline text.

Concerns

  1. Removed test: aria-disabled when no senderId. The old suite had a test verifying filter controls render with aria-disabled="true" when no person is selected. Since filter controls are now hidden in the hero state (not disabled), this test was correctly removed — but there's no replacement test verifying that the hero state is accessible. Consider adding: hero landmarks, heading level, cross-link discoverability for screen readers.

  2. No test for auto-focus behaviour. The hero's onMount calls focus() on the typeahead input. This is a key UX requirement from the issue (users shouldn't have to click). No test verifies this. A test like expect(document.activeElement).toBe(input) after render would lock this behaviour in.

  3. Missing edge case: recentPersons with empty array vs undefined. The prop defaults to [], but what if someone passes undefined explicitly? The {#if recentPersons.length > 0} would throw. Low risk since TypeScript catches this, but a defensive test would be nice.

Suggestions

  • The page.server.spec.ts was only renamed (no content changes). Consider adding a test that verifies the load function returns empty data when no query params are present, confirming the hero state is the default server-side.
## 🧪 Sara Holt — QA Engineer **Verdict: ⚠️ Approved with concerns** ### What I checked Test coverage, test quality, edge cases, missing scenarios. ### Positives - **38 tests across 3 files** — solid coverage for the scope of changes. - **Hero state tests are well-structured**: they verify presence/absence of hero, person bar, and filter controls in both states. The mutual exclusivity is explicitly tested (`conv-hero` not present when `senderId` set, and vice versa). Good. - **Recent persons chip click test** uses `data-testid` + `document.querySelector` — reliable approach that matches the existing test patterns in this project. - **Existing tests were updated** (not just left behind): describe blocks renamed from "Korrespondenz" to "Briefwechsel", assertions updated to match new headline text. ### Concerns 1. **Removed test: `aria-disabled` when no senderId**. The old suite had a test verifying filter controls render with `aria-disabled="true"` when no person is selected. Since filter controls are now hidden in the hero state (not disabled), this test was correctly removed — but there's **no replacement test** verifying that the hero state is accessible. Consider adding: hero landmarks, heading level, cross-link discoverability for screen readers. 2. **No test for auto-focus behaviour**. The hero's `onMount` calls `focus()` on the typeahead input. This is a key UX requirement from the issue (users shouldn't have to click). No test verifies this. A test like `expect(document.activeElement).toBe(input)` after render would lock this behaviour in. 3. **Missing edge case: `recentPersons` with empty array vs undefined**. The prop defaults to `[]`, but what if someone passes `undefined` explicitly? The `{#if recentPersons.length > 0}` would throw. Low risk since TypeScript catches this, but a defensive test would be nice. ### Suggestions - The `page.server.spec.ts` was only renamed (no content changes). Consider adding a test that verifies the load function returns empty data when no query params are present, confirming the hero state is the default server-side.
Author
Owner

🔒 Nora "NullX" Steiner — Security Engineer

Verdict: Approved

What I checked

XSS vectors, injection points, auth boundaries, data exposure, localStorage handling.

Assessment

  • No new attack surface: pure UI refactor. No new endpoints, no new data flows, no auth boundary changes.
  • localStorage handling is safe: JSON.parse is wrapped in try/catch in both the read (onMount) and write (persistRecentPerson) paths. Corrupt data doesn't crash the app. The parsed data is used for display only (person names rendered via Svelte's auto-escaping), not for navigation or API calls with the name — only the id is used in URLs, which is a UUID.
  • No {@html} usage: all dynamic content is rendered through Svelte's auto-escaping. The person names from localStorage go through normal text interpolation. Good.
  • Cross-link href="/" is static — no user-controlled redirect target. Safe.
  • PersonTypeahead search term goes through encodeURIComponent in the fetch URL (existing code, not changed in this PR). No injection vector there.
  • The data-testid attributes added to components are benign — they expose no sensitive information and are standard practice for test hooks.

No findings. LGTM from a security perspective.

## 🔒 Nora "NullX" Steiner — Security Engineer **Verdict: ✅ Approved** ### What I checked XSS vectors, injection points, auth boundaries, data exposure, localStorage handling. ### Assessment - **No new attack surface**: pure UI refactor. No new endpoints, no new data flows, no auth boundary changes. - **localStorage handling is safe**: `JSON.parse` is wrapped in try/catch in both the read (`onMount`) and write (`persistRecentPerson`) paths. Corrupt data doesn't crash the app. The parsed data is used for display only (person names rendered via Svelte's auto-escaping), not for navigation or API calls with the name — only the `id` is used in URLs, which is a UUID. - **No `{@html}` usage**: all dynamic content is rendered through Svelte's auto-escaping. The person names from localStorage go through normal text interpolation. Good. - **Cross-link `href="/"` is static** — no user-controlled redirect target. Safe. - **`PersonTypeahead` search term goes through `encodeURIComponent`** in the fetch URL (existing code, not changed in this PR). No injection vector there. - **The `data-testid` attributes** added to components are benign — they expose no sensitive information and are standard practice for test hooks. No findings. LGTM from a security perspective.
Author
Owner

🎨 Leonie Voss — UX Design Lead

Verdict: ⚠️ Approved with concerns

What I checked

Design fidelity to the agreed spec, accessibility, responsive behaviour, touch targets, typography, brand consistency.

Positives

  • Discovery framing is correct: the headline "Wessen Briefe möchten Sie lesen?" is warm, personal, and action-oriented — exactly what was agreed. The cross-link to document search ("Suchen Sie ein bestimmtes Dokument? → Zur Dokumentensuche") disambiguates the two entry points clearly.
  • Hero typeahead at h-14 (56px) comfortably exceeds the 44px minimum for seniors and mobile users. Good.
  • Two-state model implemented correctly: hero when no person selected, top bar when person selected. No intermediate states, no unnecessary complexity.
  • Recent persons chips preserved with the same visual pattern (initials circle + name). Continuity maintained.
  • Text sizes correct: headline at text-2xl, body text at text-base (16px minimum), helper text at text-sm (14px). Meets the dual-audience requirement.

Concerns

  1. Hardcoded "oder" in the divider (CorrespondenzHero.svelte:63). This must be a Paraglide key — English users will see a German word in the middle of an otherwise English interface. This is a blocker for i18n correctness.

  2. Cross-link placement: the cross-link sits above the headline. In the design discussion, I specified it should be above the typeahead, not above the headline. The current order is: cross-link → headline → typeahead. Consider: headline → cross-link → typeahead. This keeps the headline as the first thing users see, with the escape hatch just below it. Not blocking — visual hierarchy still works — but worth iterating on.

  3. py-20 on the hero is generous vertical padding (~80px top and bottom). On short viewports (e.g. landscape tablet), this might push the typeahead below the fold. Consider py-12 sm:py-20 for a responsive approach.

Suggestions

  • The person bar inputs at h-12 (48px) in the results state are correct per spec. The [&_input]:h-12 override works but would be cleaner as a dedicated prop. Cosmetic — not blocking.
## 🎨 Leonie Voss — UX Design Lead **Verdict: ⚠️ Approved with concerns** ### What I checked Design fidelity to the agreed spec, accessibility, responsive behaviour, touch targets, typography, brand consistency. ### Positives - **Discovery framing is correct**: the headline "Wessen Briefe möchten Sie lesen?" is warm, personal, and action-oriented — exactly what was agreed. The cross-link to document search ("Suchen Sie ein bestimmtes Dokument? → Zur Dokumentensuche") disambiguates the two entry points clearly. - **Hero typeahead at `h-14` (56px)** comfortably exceeds the 44px minimum for seniors and mobile users. Good. - **Two-state model implemented correctly**: hero when no person selected, top bar when person selected. No intermediate states, no unnecessary complexity. - **Recent persons chips preserved** with the same visual pattern (initials circle + name). Continuity maintained. - **Text sizes correct**: headline at `text-2xl`, body text at `text-base` (16px minimum), helper text at `text-sm` (14px). Meets the dual-audience requirement. ### Concerns 1. **Hardcoded "oder" in the divider** (`CorrespondenzHero.svelte:63`). This must be a Paraglide key — English users will see a German word in the middle of an otherwise English interface. This is a **blocker for i18n correctness**. 2. **Cross-link placement**: the cross-link sits *above* the headline. In the design discussion, I specified it should be above the typeahead, not above the headline. The current order is: cross-link → headline → typeahead. Consider: headline → cross-link → typeahead. This keeps the headline as the first thing users see, with the escape hatch just below it. Not blocking — visual hierarchy still works — but worth iterating on. 3. **`py-20` on the hero** is generous vertical padding (~80px top and bottom). On short viewports (e.g. landscape tablet), this might push the typeahead below the fold. Consider `py-12 sm:py-20` for a responsive approach. ### Suggestions - The person bar inputs at `h-12` (48px) in the results state are correct per spec. The `[&_input]:h-12` override works but would be cleaner as a dedicated prop. Cosmetic — not blocking.
Author
Owner

🚀 Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Infrastructure impact, CI pipeline, bundle size, environment variables, Docker Compose changes.

Assessment

  • No infrastructure impact: pure SvelteKit frontend change. No new environment variables, no new services, no Docker Compose changes, no CI pipeline changes needed.
  • No new dependencies: the PersonTypeahead large prop is a pure CSS change. No new npm packages, no animation libraries. Bundle size impact is negligible.
  • Route rename: the directory move from korrespondenz/ to briefwechsel/ is handled cleanly by git rename detection. SvelteKit regenerates route types automatically on svelte-kit sync. No manual steps needed.
  • CI compatibility: the existing Gitea Actions workflow runs npm run check and npm run build on every push. The new components will be type-checked and built automatically. No workflow changes needed.

No findings. Clean change from an ops perspective.

## 🚀 Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Infrastructure impact, CI pipeline, bundle size, environment variables, Docker Compose changes. ### Assessment - **No infrastructure impact**: pure SvelteKit frontend change. No new environment variables, no new services, no Docker Compose changes, no CI pipeline changes needed. - **No new dependencies**: the `PersonTypeahead` `large` prop is a pure CSS change. No new npm packages, no animation libraries. Bundle size impact is negligible. - **Route rename**: the directory move from `korrespondenz/` to `briefwechsel/` is handled cleanly by git rename detection. SvelteKit regenerates route types automatically on `svelte-kit sync`. No manual steps needed. - **CI compatibility**: the existing Gitea Actions workflow runs `npm run check` and `npm run build` on every push. The new components will be type-checked and built automatically. No workflow changes needed. No findings. Clean change from an ops perspective.
marcel added 2 commits 2026-04-06 20:31:31 +02:00
Adds conv_hero_divider to de/en/es messages and uses it in the
CorrespondenzHero divider. Fixes i18n blocker from review.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(ui): guard selectPerson against empty id
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m16s
CI / Backend Unit Tests (push) Failing after 2m30s
CI / Unit & Component Tests (pull_request) Failing after 1m11s
CI / Backend Unit Tests (pull_request) Failing after 2m39s
93be64878e
Restores early return when id is empty, preventing a wasteful
navigation to /briefwechsel with no senderId param.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Review concerns addressed

All reviewer concerns resolved in 2 new commits:

e2af9f9 — fix(i18n): replace hardcoded "oder" with conv_hero_divider message key

  • @leonievoss blocker: hardcoded German "oder" → conv_hero_divider Paraglide key (de: "oder", en: "or", es: "o")
  • @felixbrandt concern #3: same fix
  • @leonievoss: reordered hero layout to headline → cross-link → typeahead
  • @leonievoss: responsive padding py-12 sm:py-20 for short viewports

93be648 — fix(ui): guard selectPerson against empty id

  • @felixbrandt concern #2: restored if (!id) return; guard

Deferred

  • @saraholt: auto-focus test — skipped by agreement, unreliable in headless Playwright. Verify manually.
  • @felixbrandt: PersonTypeahead size prop refactor — cosmetic, tracked separately.

All 38 tests green.

## Review concerns addressed All reviewer concerns resolved in 2 new commits: ### `e2af9f9` — fix(i18n): replace hardcoded "oder" with conv_hero_divider message key - ✅ **@leonievoss blocker**: hardcoded German "oder" → `conv_hero_divider` Paraglide key (de: "oder", en: "or", es: "o") - ✅ **@felixbrandt concern #3**: same fix - ✅ **@leonievoss**: reordered hero layout to headline → cross-link → typeahead - ✅ **@leonievoss**: responsive padding `py-12 sm:py-20` for short viewports ### `93be648` — fix(ui): guard selectPerson against empty id - ✅ **@felixbrandt concern #2**: restored `if (!id) return;` guard ### Deferred - **@saraholt**: auto-focus test — skipped by agreement, unreliable in headless Playwright. Verify manually. - **@felixbrandt**: PersonTypeahead size prop refactor — cosmetic, tracked separately. All 38 tests green.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer (re-review)

Verdict: Approved

Previous concerns — resolved

  • Hardcoded "oder": now uses m.conv_hero_divider() Paraglide key. All three languages have the key.
  • Empty id guard: selectPerson now has if (!id) return; — clean guard clause.
  • Hero layout reordered: headline → cross-link → typeahead. Correct visual hierarchy.

Code quality check

  • TDD evidence intact: 38 tests across 3 files, all passing. Tests updated to match new behaviour.
  • {#each} keyed: (person.id) — correct.
  • $derived for computed values: isSinglePerson, senderName, receiverName — no $effect abuse.
  • No dead code: CorrespondenzEmptyState fully removed, no leftover imports.
  • Component size: CorrespondenzHero.svelte is 92 lines (30 script + 62 template) — reasonable for a hero with recent persons section. No split needed.

Remaining note (non-blocking)

The PersonTypeahead class ternary (large ? ... : compact ? ... : ...) is getting long. A size prop with a class lookup map would be cleaner. Not blocking — tracked for future cleanup.

LGTM. Ship it.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer (re-review) **Verdict: ✅ Approved** ### Previous concerns — resolved - ✅ **Hardcoded "oder"**: now uses `m.conv_hero_divider()` Paraglide key. All three languages have the key. - ✅ **Empty id guard**: `selectPerson` now has `if (!id) return;` — clean guard clause. - ✅ **Hero layout reordered**: headline → cross-link → typeahead. Correct visual hierarchy. ### Code quality check - **TDD evidence intact**: 38 tests across 3 files, all passing. Tests updated to match new behaviour. - **`{#each}` keyed**: `(person.id)` — correct. - **`$derived` for computed values**: `isSinglePerson`, `senderName`, `receiverName` — no `$effect` abuse. - **No dead code**: `CorrespondenzEmptyState` fully removed, no leftover imports. - **Component size**: `CorrespondenzHero.svelte` is 92 lines (30 script + 62 template) — reasonable for a hero with recent persons section. No split needed. ### Remaining note (non-blocking) The `PersonTypeahead` class ternary (`large ? ... : compact ? ... : ...`) is getting long. A `size` prop with a class lookup map would be cleaner. Not blocking — tracked for future cleanup. LGTM. Ship it.
Author
Owner

🏗️ Markus Keller — Application Architect (re-review)

Verdict: Approved

No architectural concerns added or changed since the first review. The review fixes are cosmetic (i18n key, guard clause, layout reorder, responsive padding) — no state management, layer boundary, or coupling changes.

The two-state model (!senderId → hero, senderId → results) remains clean. SSR behaviour unchanged. LGTM.

## 🏗️ Markus Keller — Application Architect (re-review) **Verdict: ✅ Approved** No architectural concerns added or changed since the first review. The review fixes are cosmetic (i18n key, guard clause, layout reorder, responsive padding) — no state management, layer boundary, or coupling changes. The two-state model (`!senderId` → hero, `senderId` → results) remains clean. SSR behaviour unchanged. LGTM.
Author
Owner

🧪 Sara Holt — QA Engineer (re-review)

Verdict: Approved

Previous concerns — status

  • Auto-focus test: deferred by agreement — unreliable in headless Playwright. Manual verification required. Acceptable trade-off.
  • aria-disabled test removal: correctly handled — filter controls are now hidden in hero state (not disabled), so the old test was semantically wrong. The new tests verify mutual exclusivity of hero/results states instead.

Test coverage assessment

  • 38 tests, 3 files — comprehensive for this scope.
  • Hero state: 6 tests (presence, headline, no person bar, no filter controls, no doc link, no year divider)
  • Results state: 3 tests (no hero, person bar present, filter controls present)
  • Recent persons: 2 tests (chips from localStorage, corrupt JSON resilience)
  • Existing behaviour preserved: hint bar, swap button, year dividers, new doc link — all tests updated and passing.

LGTM. Ready to merge.

## 🧪 Sara Holt — QA Engineer (re-review) **Verdict: ✅ Approved** ### Previous concerns — status - ✅ **Auto-focus test**: deferred by agreement — unreliable in headless Playwright. Manual verification required. Acceptable trade-off. - ✅ **`aria-disabled` test removal**: correctly handled — filter controls are now hidden in hero state (not disabled), so the old test was semantically wrong. The new tests verify mutual exclusivity of hero/results states instead. ### Test coverage assessment - 38 tests, 3 files — comprehensive for this scope. - Hero state: 6 tests (presence, headline, no person bar, no filter controls, no doc link, no year divider) - Results state: 3 tests (no hero, person bar present, filter controls present) - Recent persons: 2 tests (chips from localStorage, corrupt JSON resilience) - Existing behaviour preserved: hint bar, swap button, year dividers, new doc link — all tests updated and passing. LGTM. Ready to merge.
Author
Owner

🔒 Nora "NullX" Steiner — Security Engineer (re-review)

Verdict: Approved

No security-relevant changes in the review fix commits. The conv_hero_divider message key is a static i18n string — no user input involved. The if (!id) return guard is a defensive check, not a security boundary. No new attack surface.

LGTM. Same assessment as first review — no findings.

## 🔒 Nora "NullX" Steiner — Security Engineer (re-review) **Verdict: ✅ Approved** No security-relevant changes in the review fix commits. The `conv_hero_divider` message key is a static i18n string — no user input involved. The `if (!id) return` guard is a defensive check, not a security boundary. No new attack surface. LGTM. Same assessment as first review — no findings.
Author
Owner

🎨 Leonie Voss — UX Design Lead (re-review)

Verdict: Approved

Previous concerns — all resolved

  • Hardcoded "oder": now m.conv_hero_divider() — de: "oder", en: "or", es: "o". i18n blocker cleared.
  • Cross-link placement: reordered to headline → cross-link → typeahead. The headline is now the first thing users see, with the escape hatch just below it. Correct visual hierarchy.
  • Responsive padding: py-12 sm:py-20 — typeahead stays above the fold on short viewports (landscape tablet, small laptops).

Final design check

  • Hero typeahead at h-14 (56px) — exceeds 44px touch target minimum. ✓
  • Text sizes: text-2xl headline, text-sm cross-link, text-base body. All meet dual-audience requirements. ✓
  • Person bar inputs at h-12 (48px) in results state via [&_input]:h-12. ✓
  • Recent persons chips preserved with initials + name pattern. ✓

Design implementation matches the agreed spec. Ready to ship.

## 🎨 Leonie Voss — UX Design Lead (re-review) **Verdict: ✅ Approved** ### Previous concerns — all resolved - ✅ **Hardcoded "oder"**: now `m.conv_hero_divider()` — de: "oder", en: "or", es: "o". i18n blocker cleared. - ✅ **Cross-link placement**: reordered to headline → cross-link → typeahead. The headline is now the first thing users see, with the escape hatch just below it. Correct visual hierarchy. - ✅ **Responsive padding**: `py-12 sm:py-20` — typeahead stays above the fold on short viewports (landscape tablet, small laptops). ### Final design check - Hero typeahead at `h-14` (56px) — exceeds 44px touch target minimum. ✓ - Text sizes: `text-2xl` headline, `text-sm` cross-link, `text-base` body. All meet dual-audience requirements. ✓ - Person bar inputs at `h-12` (48px) in results state via `[&_input]:h-12`. ✓ - Recent persons chips preserved with initials + name pattern. ✓ Design implementation matches the agreed spec. Ready to ship.
Author
Owner

🚀 Tobias Wendt — DevOps & Platform Engineer (re-review)

Verdict: Approved

No infrastructure changes in the review fix commits. Three message key additions and two Svelte component edits — zero CI/CD impact. Same assessment as first review.

LGTM.

## 🚀 Tobias Wendt — DevOps & Platform Engineer (re-review) **Verdict: ✅ Approved** No infrastructure changes in the review fix commits. Three message key additions and two Svelte component edits — zero CI/CD impact. Same assessment as first review. LGTM.
marcel added 1 commit 2026-04-06 22:21:12 +02:00
fix(ui): unify Briefwechsel search bar with document search card style
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m26s
CI / Backend Unit Tests (push) Failing after 2m25s
CI / Unit & Component Tests (pull_request) Failing after 1m15s
CI / Backend Unit Tests (pull_request) Failing after 2m20s
c4715f1637
Wrap person bar + filter controls in a card matching the document
search page (rounded-sm border p-6 shadow-sm). Switch PersonTypeahead
to default mode with matching label/input overrides. Bump date inputs
and sort button to text-sm py-2.5. Filter row uses border-t separator
like the document search advanced section.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:34:58 +02:00
feat(ui): collapsible date filter with sort + filter toggle on person row
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m13s
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m10s
CI / Backend Unit Tests (pull_request) Failing after 2m33s
c8b4bce003
Move sort button and filter toggle to the person row, matching the
document search page pattern (sort + filter + count inline). Date
range inputs are now a collapsible section behind the filter toggle,
using slide transition and the same grid layout as the document
search advanced filters. Fix date input padding (add px-3).

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:36:43 +02:00
fix(ui): use sort arrows ↑↓ instead of chevrons on sort button
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m17s
CI / Backend Unit Tests (pull_request) Failing after 2m23s
fe51936d17
Chevrons indicate collapsible elements, not sort direction. Match
the document search SortDropdown pattern using ↑/↓ text arrows.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:38:26 +02:00
fix(ui): use De Gruyter long arrow icons for sort direction
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m24s
CI / Backend Unit Tests (pull_request) Failing after 2m28s
b4a9e678c6
Replace tiny ↑↓ text with Long-Arrow-Up/Down-MD.svg icons from the
brand icon set for better visibility and consistency.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:44:23 +02:00
fix(ui): De Gruyter long arrows on both sort buttons, rotate swap icon 90°
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m21s
CI / Backend Unit Tests (pull_request) Failing after 2m30s
2e943b7f91
Replace ↑↓ text with Long-Arrow-Up/Down-MD.svg on the document search
SortDropdown and the Briefwechsel sort button. Rotate the swap button
SVG 90° so arrows point left/right matching the horizontal person
field layout.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:47:03 +02:00
fix(ui): unify date inputs — use DateInput component on both pages
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m16s
CI / Backend Unit Tests (pull_request) Failing after 2m23s
708d02a1f7
Replace native <input type="date"> on the document search page with
the custom DateInput component (German dd.mm.yyyy format with auto-dot
insertion). Align both pages' date input styling: add rounded-md,
border, bg-surface, px-3, text-ink, placeholder color, and focus ring
to match all other inputs in the card.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:48:00 +02:00
fix(ui): date input placeholders show format TT.MM.JJJJ instead of label
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m20s
CI / Backend Unit Tests (pull_request) Failing after 1m49s
a3edf9d7b4
Remove custom placeholder props so DateInput falls back to its default
format hint (TT.MM.JJJJ / DD.MM.YYYY / DD.MM.AAAA) instead of
repeating the label text above.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-06 22:57:21 +02:00
fix(ui): prevent hero flicker when clearing sender input
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 2s
CI / Unit & Component Tests (push) Failing after 5s
CI / Backend Unit Tests (push) Failing after 5s
7fed057e59
Only navigate (applyFilters) when a person is actually selected, not
when the sender input is cleared. Combined with showHero checking
data.filters.senderId, the user stays in the search bar view after
clearing — no jump back to the hero.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 08:47:15 +02:00
fix(ui): remove disabled look from receiver typeahead when empty
Some checks failed
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 2s
06709e7458
Remove border-dashed and bg-canvas conditional styles so the
receiver input matches the sender input styling. The placeholder
"Alle Korrespondenten" already communicates the optional state.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 08:51:37 +02:00
fix(ui): use De Gruyter long arrows for swap button and timeline entries
Some checks failed
CI / Unit & Component Tests (push) Failing after 2s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
001e875f31
Swap button: stack right/left arrows vertically at h-3.5 for a
compact look. Timeline: replace → ← text with Long-Arrow icons on
each letter entry and the distribution bar summary.

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 08:53:16 +02:00
fix(ui): add py-8 to results state matching document search page
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 1s
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 2s
53b318f7ad
Aligns the top/bottom padding of the Briefwechsel results view with
the document search page wrapper (both py-8).

Refs: #179

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer (final review)

Verdict: Approved

Previous concerns — all resolved

  • Hardcoded "oder" → m.conv_hero_divider() Paraglide key
  • Empty id guard restored on selectPerson
  • PersonTypeahead class ternary — noted for future cleanup, not blocking

Code quality

  • Svelte 5 patterns correct: $derived for isSinglePerson, showHero, senderName, receiverName. No $effect abuse — the single $effect for persistRecentPerson is a legitimate side-effect.
  • showHero logic is clean: $derived(!senderId && !data.filters.senderId) — prevents hero flicker when sender is cleared locally. The guard in CorrespondenzPersonBar (if (id) onapplyFilters()) prevents navigation on clear. Two-layer defence, both necessary.
  • {#each} keyed everywhere: (person.id), (doc.id), (c.id).
  • Component sizes reasonable: CorrespondenzHero 92 lines, CorrespondenzPersonBar 183 lines (heavier due to sort/filter row + suggestions dropdown, but all one visual region), CorrespondenzFilterControls 49 lines. All within bounds.
  • No dead code: CorrespondenzEmptyState fully deleted, no orphan imports.
  • De Gruyter icons used consistently: sort arrows, swap button, timeline direction indicators all use Long-Arrow-*-MD.svg.
  • Collapsible filter pattern matches document search: showAdvanced toggle with transition:slide, chevron rotation, same button styling.

One note (non-blocking)

The CorrespondenzPersonBar now handles person inputs, swap, sort, filter toggle, count, and correspondent suggestions. It's still one visual region (the search card's main row), but at 183 lines it's approaching the split threshold. If more controls are added later, consider extracting the sort/filter/count row into its own component.

LGTM. Ship it.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer (final review) **Verdict: ✅ Approved** ### Previous concerns — all resolved - ✅ Hardcoded "oder" → `m.conv_hero_divider()` Paraglide key - ✅ Empty id guard restored on `selectPerson` - ✅ PersonTypeahead class ternary — noted for future cleanup, not blocking ### Code quality - **Svelte 5 patterns correct**: `$derived` for `isSinglePerson`, `showHero`, `senderName`, `receiverName`. No `$effect` abuse — the single `$effect` for `persistRecentPerson` is a legitimate side-effect. - **`showHero` logic is clean**: `$derived(!senderId && !data.filters.senderId)` — prevents hero flicker when sender is cleared locally. The guard in `CorrespondenzPersonBar` (`if (id) onapplyFilters()`) prevents navigation on clear. Two-layer defence, both necessary. - **`{#each}` keyed** everywhere: `(person.id)`, `(doc.id)`, `(c.id)`. - **Component sizes reasonable**: `CorrespondenzHero` 92 lines, `CorrespondenzPersonBar` 183 lines (heavier due to sort/filter row + suggestions dropdown, but all one visual region), `CorrespondenzFilterControls` 49 lines. All within bounds. - **No dead code**: `CorrespondenzEmptyState` fully deleted, no orphan imports. - **De Gruyter icons used consistently**: sort arrows, swap button, timeline direction indicators all use `Long-Arrow-*-MD.svg`. - **Collapsible filter pattern matches document search**: `showAdvanced` toggle with `transition:slide`, chevron rotation, same button styling. ### One note (non-blocking) The `CorrespondenzPersonBar` now handles person inputs, swap, sort, filter toggle, count, and correspondent suggestions. It's still one visual region (the search card's main row), but at 183 lines it's approaching the split threshold. If more controls are added later, consider extracting the sort/filter/count row into its own component. LGTM. Ship it.
Author
Owner

🏗️ Markus Keller — Application Architect (final review)

Verdict: Approved

Architecture assessment

  • State machine correct: two states — showHero (fresh page, no context) and results (senderId set or previously set). No cycles, no intermediate states. $derived booleans drive {#if} branches cleanly.
  • Anti-flicker design sound: showHero = !senderId && !data.filters.senderId checks both local state and server-loaded state. The CorrespondenzPersonBar only triggers navigation when a person is actually selected (if (id) onapplyFilters()). This prevents the page component from being replaced with empty server data when the user is just clearing an input to retype.
  • Collapsible filter follows established pattern: the showAdvanced toggle + transition:slide + chevron rotation mirrors SearchFilterBar exactly. Good pattern reuse.
  • Cross-page unification: SearchFilterBar now uses DateInput component instead of native <input type="date">, matching Briefwechsel. SortDropdown uses De Gruyter long arrow icons, matching Briefwechsel. Consistency improved across both pages.
  • No layer violations: data flows down via props, events flow up via callbacks. CorrespondenzHero receives recentPersons from parent, doesn't fetch its own data.
  • Route rename clean: no redirect needed, all internal references updated.

No concerns. LGTM.

## 🏗️ Markus Keller — Application Architect (final review) **Verdict: ✅ Approved** ### Architecture assessment - **State machine correct**: two states — `showHero` (fresh page, no context) and results (senderId set or previously set). No cycles, no intermediate states. `$derived` booleans drive `{#if}` branches cleanly. - **Anti-flicker design sound**: `showHero = !senderId && !data.filters.senderId` checks both local state and server-loaded state. The `CorrespondenzPersonBar` only triggers navigation when a person is actually selected (`if (id) onapplyFilters()`). This prevents the page component from being replaced with empty server data when the user is just clearing an input to retype. - **Collapsible filter follows established pattern**: the `showAdvanced` toggle + `transition:slide` + chevron rotation mirrors `SearchFilterBar` exactly. Good pattern reuse. - **Cross-page unification**: `SearchFilterBar` now uses `DateInput` component instead of native `<input type="date">`, matching Briefwechsel. `SortDropdown` uses De Gruyter long arrow icons, matching Briefwechsel. Consistency improved across both pages. - **No layer violations**: data flows down via props, events flow up via callbacks. `CorrespondenzHero` receives `recentPersons` from parent, doesn't fetch its own data. - **Route rename clean**: no redirect needed, all internal references updated. No concerns. LGTM.
Author
Owner

🧪 Sara Holt — QA Engineer (final review)

Verdict: Approved

Test coverage

  • 37 tests across 3 Briefwechsel test files + 5 SortDropdown tests updated — all green.
  • Hero state: 6 tests (presence, headline, no person bar, no filter controls, no doc link, no year divider)
  • Results state: 3 tests (no hero, person bar visible, filter controls hidden by default behind toggle)
  • Recent persons: 2 tests (chips from props, chip click callback)
  • Remaining tests cover hint bar, swap button, letter count, year dividers, new document link — all updated and passing.
  • SortDropdown.svelte.spec.ts tests updated to check for Long-Arrow-Up/Long-Arrow-Down img src instead of / text.

Deferred items (by agreement)

  • Auto-focus test on hero typeahead — unreliable in headless Playwright, manual verification only.

Edge cases covered

  • Corrupt localStorage JSON → hero still renders (test exists)
  • Empty senderId guard → no wasteful navigation
  • showHero prevents flicker when clearing sender locally

LGTM.

## 🧪 Sara Holt — QA Engineer (final review) **Verdict: ✅ Approved** ### Test coverage - **37 tests** across 3 Briefwechsel test files + **5 SortDropdown tests** updated — all green. - Hero state: 6 tests (presence, headline, no person bar, no filter controls, no doc link, no year divider) - Results state: 3 tests (no hero, person bar visible, filter controls hidden by default behind toggle) - Recent persons: 2 tests (chips from props, chip click callback) - Remaining tests cover hint bar, swap button, letter count, year dividers, new document link — all updated and passing. - `SortDropdown.svelte.spec.ts` tests updated to check for `Long-Arrow-Up`/`Long-Arrow-Down` img src instead of `↑`/`↓` text. ### Deferred items (by agreement) - Auto-focus test on hero typeahead — unreliable in headless Playwright, manual verification only. ### Edge cases covered - Corrupt localStorage JSON → hero still renders (test exists) - Empty senderId guard → no wasteful navigation - `showHero` prevents flicker when clearing sender locally LGTM.
Author
Owner

🔒 Nora "NullX" Steiner — Security Engineer (final review)

Verdict: Approved

No new attack surface. The additional changes since the last review are purely visual (icon swaps, padding, collapsible filter, date input replacement). No new data flows, no new endpoints, no auth changes.

  • DateInput component replacing <input type="date"> on the document search page: the component uses oninput with German format parsing — all client-side, no server-side injection vector.
  • De Gruyter icon <img src="..."> paths are all static strings pointing to files in /static/ — no user-controlled paths.
  • showHero / showAdvanced are local UI state — no security implications.

LGTM. No findings.

## 🔒 Nora "NullX" Steiner — Security Engineer (final review) **Verdict: ✅ Approved** No new attack surface. The additional changes since the last review are purely visual (icon swaps, padding, collapsible filter, date input replacement). No new data flows, no new endpoints, no auth changes. - `DateInput` component replacing `<input type="date">` on the document search page: the component uses `oninput` with German format parsing — all client-side, no server-side injection vector. - De Gruyter icon `<img src="...">` paths are all static strings pointing to files in `/static/` — no user-controlled paths. - `showHero` / `showAdvanced` are local UI state — no security implications. LGTM. No findings.
Author
Owner

🎨 Leonie Voss — UX Design Lead (final review)

Verdict: Approved

Design assessment — significant improvement since first review

Search card unification — the Briefwechsel search bar now matches the document search page:

  • Card wrapper: rounded-sm border p-6 shadow-sm — identical
  • Input styling: default PersonTypeahead with border-line py-2.5 label overrides — identical
  • Date inputs: DateInput component with rounded-md border bg-surface px-3 py-2.5 — identical to all other inputs. Placeholder shows TT.MM.JJJJ format hint instead of repeating labels.
  • Sort + Filter buttons: same button pattern (border bg-muted px-4 py-2.5 text-sm font-bold uppercase) — matches document search
  • Collapsible date filter behind "Filter" toggle — same UX pattern as document search advanced filters

Icon consistency:

  • Sort arrows use De Gruyter Long-Arrow-Up/Down-MD.svg on both pages
  • Swap button uses stacked Long-Arrow-Right/Left at h-3.5, compact and clear
  • Timeline letter entries use Long-Arrow-Right/Left instead of text /
  • Distribution bar summary uses matching icons

Layout:

  • Hero: headline → cross-link → typeahead. Correct hierarchy.
  • Responsive hero padding: py-12 sm:py-20
  • Results state py-8 matching document search page
  • i18n: divider uses conv_hero_divider key, all text localized

Anti-flicker: clearing sender keeps the search bar visible — no disorienting jump back to hero. Excellent UX decision.

Receiver input: dashed border / canvas background removed — both inputs now look identical, with "Alle Korrespondenten" placeholder communicating optionality clearly.

No concerns. The two search pages now feel like siblings in the same design system. Ready to ship.

## 🎨 Leonie Voss — UX Design Lead (final review) **Verdict: ✅ Approved** ### Design assessment — significant improvement since first review **Search card unification** — the Briefwechsel search bar now matches the document search page: - ✅ Card wrapper: `rounded-sm border p-6 shadow-sm` — identical - ✅ Input styling: default PersonTypeahead with `border-line py-2.5` label overrides — identical - ✅ Date inputs: `DateInput` component with `rounded-md border bg-surface px-3 py-2.5` — identical to all other inputs. Placeholder shows `TT.MM.JJJJ` format hint instead of repeating labels. - ✅ Sort + Filter buttons: same button pattern (`border bg-muted px-4 py-2.5 text-sm font-bold uppercase`) — matches document search - ✅ Collapsible date filter behind "Filter" toggle — same UX pattern as document search advanced filters **Icon consistency**: - ✅ Sort arrows use De Gruyter `Long-Arrow-Up/Down-MD.svg` on both pages - ✅ Swap button uses stacked `Long-Arrow-Right/Left` at `h-3.5`, compact and clear - ✅ Timeline letter entries use `Long-Arrow-Right/Left` instead of text `→`/`←` - ✅ Distribution bar summary uses matching icons **Layout**: - ✅ Hero: headline → cross-link → typeahead. Correct hierarchy. - ✅ Responsive hero padding: `py-12 sm:py-20` - ✅ Results state `py-8` matching document search page - ✅ i18n: divider uses `conv_hero_divider` key, all text localized **Anti-flicker**: clearing sender keeps the search bar visible — no disorienting jump back to hero. Excellent UX decision. **Receiver input**: dashed border / canvas background removed — both inputs now look identical, with "Alle Korrespondenten" placeholder communicating optionality clearly. No concerns. The two search pages now feel like siblings in the same design system. Ready to ship.
Author
Owner

🚀 Tobias Wendt — DevOps & Platform Engineer (final review)

Verdict: Approved

No infrastructure impact. The additional changes are frontend-only: CSS adjustments, icon references (static files already in /static/), component restructuring, and DateInput replacing native date inputs. No new dependencies, no new env vars, no Docker/CI changes.

The De Gruyter icon <img> references point to files already in the repo under frontend/static/degruyter-icons/ — no external CDN or new asset pipeline needed.

LGTM.

## 🚀 Tobias Wendt — DevOps & Platform Engineer (final review) **Verdict: ✅ Approved** No infrastructure impact. The additional changes are frontend-only: CSS adjustments, icon references (static files already in `/static/`), component restructuring, and `DateInput` replacing native date inputs. No new dependencies, no new env vars, no Docker/CI changes. The De Gruyter icon `<img>` references point to files already in the repo under `frontend/static/degruyter-icons/` — no external CDN or new asset pipeline needed. LGTM.
marcel merged commit 53b318f7ad into main 2026-04-07 09:07:23 +02:00
marcel deleted branch feat/issue-179-briefwechsel-hero 2026-04-07 09:07:25 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#186