ui: Korrespondenz view — unclear empty state, misplaced search header, focus misdirection, small text #179

Closed
opened 2026-04-06 00:22:41 +02:00 by marcel · 9 comments
Owner

User Feedback Summary

First-time users were disoriented in the Korrespondenz view. Three distinct problems were identified in a usability session.


Problem 1 — Search header placed too high / low visual hierarchy

The search bar sits in a position that makes it feel disconnected from the empty content area. Users didn't immediately understand that the search bar is the primary entry point into the view.

Fix: The search bar (or a clear call-to-action label) should be visually centred in the empty state — not anchored to the top. Consider a hero-style empty state with the search bar as the focal point, similar to a start page.


Problem 2 — Empty state search triggers wrong input field

When the user types in the empty-state search prompt, focus is incorrectly moved to a search input inside a row element instead of staying on the empty-state search bar itself. The search should execute from — and stay anchored to — the empty-state input.

Fix: The empty-state search bar must retain focus throughout the interaction. Do not steal focus or delegate keystrokes to another input. The search should run directly from the empty-state field.


Problem 3 — Text in the empty/initial view is too small

Body text and hints in the empty/initial Korrespondenz view are rendered at a size that falls below the 16px minimum required for our dual-audience design (seniors 60+).

Fix: All visible text in the empty state must be text-base (16px) minimum. Hint/helper text may use text-sm (14px) only if it is supplementary, never for primary messaging.


Acceptance Criteria

  • Empty state has a visually centred search bar as the primary focal point
  • Typing into the empty-state search bar keeps focus on that input (no focus delegation to row inputs)
  • Search executes from the empty-state field without navigating focus elsewhere
  • All primary text in the empty state is ≥ 16px (text-base)
  • Helper/hint text is ≥ 14px (text-sm)
  • Layout works at 320px viewport width (mobile-first)
  • Touch targets on search bar and any CTA ≥ 44×44px
## User Feedback Summary First-time users were disoriented in the Korrespondenz view. Three distinct problems were identified in a usability session. --- ## Problem 1 — Search header placed too high / low visual hierarchy The search bar sits in a position that makes it feel disconnected from the empty content area. Users didn't immediately understand that the search bar is the primary entry point into the view. **Fix:** The search bar (or a clear call-to-action label) should be visually centred in the empty state — not anchored to the top. Consider a hero-style empty state with the search bar as the focal point, similar to a start page. --- ## Problem 2 — Empty state search triggers wrong input field When the user types in the empty-state search prompt, focus is incorrectly moved to a search input inside a row element instead of staying on the empty-state search bar itself. The search should execute from — and stay anchored to — the empty-state input. **Fix:** The empty-state search bar must retain focus throughout the interaction. Do not steal focus or delegate keystrokes to another input. The search should run directly from the empty-state field. --- ## Problem 3 — Text in the empty/initial view is too small Body text and hints in the empty/initial Korrespondenz view are rendered at a size that falls below the 16px minimum required for our dual-audience design (seniors 60+). **Fix:** All visible text in the empty state must be `text-base` (16px) minimum. Hint/helper text may use `text-sm` (14px) only if it is supplementary, never for primary messaging. --- ## Acceptance Criteria - [ ] Empty state has a visually centred search bar as the primary focal point - [ ] Typing into the empty-state search bar keeps focus on that input (no focus delegation to row inputs) - [ ] Search executes from the empty-state field without navigating focus elsewhere - [ ] All primary text in the empty state is ≥ 16px (`text-base`) - [ ] Helper/hint text is ≥ 14px (`text-sm`) - [ ] Layout works at 320px viewport width (mobile-first) - [ ] Touch targets on search bar and any CTA ≥ 44×44px
marcel added the ui label 2026-04-06 00:23:05 +02:00
Author
Owner

🎨 Leonie Voss — UX Design Lead

Design discussion outcomes — interaction model and layout decisions agreed with the product owner.


Resolved

  • Interaction model — sequential hero: Empty state shows a single centred Person A typeahead (h-14 / 56px). After Person A is selected, it becomes a locked chip and Person B typeahead appears in its place. Once both are selected, the hero collapses and the top filter bar becomes active with results below.

  • Top bar prominence: After results load, the filter bar stays at the top but inputs are bumped to h-12 (48px) — more prominent than the current ~42px height.

  • Progressive disclosure: Date range and sort controls are only shown in the top bar state. They are irrelevant before two people are selected and should not appear in the hero empty state.

  • Text sizes: Primary empty state text text-base minimum (16px). Supplementary/hint text text-sm (14px) is acceptable for non-primary messaging only.

  • Touch targets: Hero search input h-14 (56px) — comfortably exceeds the 44px minimum for seniors and mobile users.


Implementation notes

  • The current ConversationFilterBar always renders as a full panel. It will need to be split into two display states: hero (single typeahead, centred) and compact top bar (full controls, h-12 inputs).
  • The Svelte page state already tracks senderId and receiverId separately — these can drive which hero step is active (!senderId → show Person A input, senderId && !receiverId → show Person B input, both set → show results + top bar).
  • Focus management is critical: each hero typeahead should receive focus automatically when it appears, so the user never has to click.

The current filter-panel-always-visible pattern was the root cause of all three reported problems. The sequential hero removes that ambiguity entirely.

## 🎨 Leonie Voss — UX Design Lead Design discussion outcomes — interaction model and layout decisions agreed with the product owner. --- ### Resolved - **Interaction model — sequential hero:** Empty state shows a single centred Person A typeahead (`h-14` / 56px). After Person A is selected, it becomes a locked chip and Person B typeahead appears in its place. Once both are selected, the hero collapses and the top filter bar becomes active with results below. - **Top bar prominence:** After results load, the filter bar stays at the top but inputs are bumped to `h-12` (48px) — more prominent than the current ~42px height. - **Progressive disclosure:** Date range and sort controls are only shown in the top bar state. They are irrelevant before two people are selected and should not appear in the hero empty state. - **Text sizes:** Primary empty state text `text-base` minimum (16px). Supplementary/hint text `text-sm` (14px) is acceptable for non-primary messaging only. - **Touch targets:** Hero search input `h-14` (56px) — comfortably exceeds the 44px minimum for seniors and mobile users. --- ### Implementation notes - The current `ConversationFilterBar` always renders as a full panel. It will need to be split into two display states: hero (single typeahead, centred) and compact top bar (full controls, `h-12` inputs). - The Svelte page state already tracks `senderId` and `receiverId` separately — these can drive which hero step is active (`!senderId` → show Person A input, `senderId && !receiverId` → show Person B input, both set → show results + top bar). - Focus management is critical: each hero typeahead should receive focus automatically when it appears, so the user never has to click. --- The current filter-panel-always-visible pattern was the root cause of all three reported problems. The sequential hero removes that ambiguity entirely.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Component decomposition

The current ConversationFilterBar.svelte does one job today (full filter panel) but will need to serve two visual states after this change. I'd split it before writing a line of logic:

  • ConversationHero.svelte — the centred hero, owns the sequential step display
  • ConversationHeroStep.svelte — a single step (label + PersonTypeahead + locked chip), reused for both Person A and B
  • ConversationFilterBar.svelte — the existing top bar, bumped to h-12 inputs, unchanged in structure

+page.svelte already holds senderId and receiverId as $state. The three render cases (!senderId, senderId && !receiverId, both) map directly to {#if} branches selecting either the hero or the bar.

Auto-focus on Person B

When Person A is selected and Person B's input appears, it needs to receive focus automatically — otherwise users on desktop have to click, and keyboard users are lost. This requires bind:this on the Person B typeahead + a tick() + element.focus() after the state transition. Flag this as required in the AC, it's easy to forget.

Bookmarked / deep-linked URLs

+page.svelte reads senderId and receiverId from URL params via the data prop. If both are already set on initial load (e.g. a bookmarked conversation), the hero should be skipped entirely and results rendered immediately. The current logic already handles this — just confirm it still holds after the refactor.

Test surface

  • Unit: Vitest for each of the three hero states (no person, A only, both)
  • Unit: confirm ontoggleSort, onswapPersons, onapplyFilters still fire correctly from the top bar state
  • Verify keepFocus: true in goto() is preserved — removing it will break typeahead keyboard navigation
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Component decomposition The current `ConversationFilterBar.svelte` does one job today (full filter panel) but will need to serve two visual states after this change. I'd split it before writing a line of logic: - `ConversationHero.svelte` — the centred hero, owns the sequential step display - `ConversationHeroStep.svelte` — a single step (label + PersonTypeahead + locked chip), reused for both Person A and B - `ConversationFilterBar.svelte` — the existing top bar, bumped to `h-12` inputs, unchanged in structure `+page.svelte` already holds `senderId` and `receiverId` as `$state`. The three render cases (`!senderId`, `senderId && !receiverId`, `both`) map directly to `{#if}` branches selecting either the hero or the bar. ### Auto-focus on Person B When Person A is selected and Person B's input appears, it needs to receive focus automatically — otherwise users on desktop have to click, and keyboard users are lost. This requires `bind:this` on the Person B typeahead + a `tick()` + `element.focus()` after the state transition. Flag this as required in the AC, it's easy to forget. ### Bookmarked / deep-linked URLs `+page.svelte` reads `senderId` and `receiverId` from URL params via the `data` prop. If both are already set on initial load (e.g. a bookmarked conversation), the hero should be skipped entirely and results rendered immediately. The current logic already handles this — just confirm it still holds after the refactor. ### Test surface - Unit: Vitest for each of the three hero states (no person, A only, both) - Unit: confirm `ontoggleSort`, `onswapPersons`, `onapplyFilters` still fire correctly from the top bar state - Verify `keepFocus: true` in `goto()` is preserved — removing it will break typeahead keyboard navigation
Author
Owner

🏗️ Markus Keller — Application Architect

No backend changes required

This is a pure frontend refactor. No new API endpoints, no schema changes, no Spring Boot work. The existing senderId/receiverId query params drive everything — the three UI states are entirely derived from those two values.

SSR behaviour on initial load

SvelteKit renders +page.svelte server-side on first request. If a bookmarked URL arrives with ?senderId=X&receiverId=Y, the server load function populates both, and the page should hydrate directly into the results view — hero never rendered. This is the correct behaviour and requires no special handling, but it should be validated during implementation since it affects whether the hero component is ever mounted server-side.

State machine is clean

The three states (!senderId, senderId && !receiverId, both) map to a linear state machine with no cycles. No $effect chains needed — $derived booleans from senderId and receiverId drive the {#if} branches cleanly. Keep it that way.

One coupling risk

ConversationFilterBar currently holds the swap-persons button, which is only meaningful when both persons are selected. After the split, ensure the swap button does not accidentally appear in the hero state. The existing {senderId && receiverId ? 'flex' : 'hidden md:flex'} conditional handles this today — make sure it survives the refactor.

No ADR needed

This is a UX improvement within existing module boundaries, not an architectural decision. No ADR required.

## 🏗️ Markus Keller — Application Architect ### No backend changes required This is a pure frontend refactor. No new API endpoints, no schema changes, no Spring Boot work. The existing `senderId`/`receiverId` query params drive everything — the three UI states are entirely derived from those two values. ### SSR behaviour on initial load SvelteKit renders `+page.svelte` server-side on first request. If a bookmarked URL arrives with `?senderId=X&receiverId=Y`, the server load function populates both, and the page should hydrate directly into the results view — hero never rendered. This is the correct behaviour and requires no special handling, but it should be validated during implementation since it affects whether the hero component is ever mounted server-side. ### State machine is clean The three states (`!senderId`, `senderId && !receiverId`, `both`) map to a linear state machine with no cycles. No `$effect` chains needed — `$derived` booleans from `senderId` and `receiverId` drive the `{#if}` branches cleanly. Keep it that way. ### One coupling risk `ConversationFilterBar` currently holds the swap-persons button, which is only meaningful when both persons are selected. After the split, ensure the swap button does not accidentally appear in the hero state. The existing `{senderId && receiverId ? 'flex' : 'hidden md:flex'}` conditional handles this today — make sure it survives the refactor. ### No ADR needed This is a UX improvement within existing module boundaries, not an architectural decision. No ADR required.
Author
Owner

🧪 Sara Holt — QA Engineer

Missing acceptance criteria for the sequential flow

The current AC covers the end states but not the transitions. I'd add:

  • After Person A is selected, Person B input appears and receives focus automatically
  • Person A selection is displayed as a locked chip (name visible, editable) while Person B input is active
  • Navigating to /conversations?senderId=X&receiverId=Y skips the hero and renders results directly
  • Clearing Person A (if editable) returns the view to step 1

Test plan

Vitest (component layer):

  • ConversationHero renders Person A input when senderId is empty
  • ConversationHero renders locked Person A chip + Person B input when senderId is set but receiverId is empty
  • ConversationHero is not rendered when both are set

Playwright (E2E):

  • Journey: load /conversations → select Person A → verify step 2 appears with focus → select Person B → verify top bar visible and results load
  • Journey: load /conversations?senderId=X&receiverId=Y → verify hero is never shown
  • Accessibility: checkA11y on all three states (hero step 1, hero step 2, results view)
  • Visual regression: 320px, 768px, 1440px for each state

Edge cases not covered in the issue

  • What happens if Person A is selected but has no correspondents? Does Person B's typeahead show an empty list with a helpful message, or does it look broken?
  • The restrictToCorrespondentsOf prop on the Person B typeahead already handles filtering — but if it returns zero results, the user is stuck in step 2 with no way to proceed unless they can clear Person A.
## 🧪 Sara Holt — QA Engineer ### Missing acceptance criteria for the sequential flow The current AC covers the end states but not the transitions. I'd add: - [ ] After Person A is selected, Person B input appears and receives focus automatically - [ ] Person A selection is displayed as a locked chip (name visible, editable) while Person B input is active - [ ] Navigating to `/conversations?senderId=X&receiverId=Y` skips the hero and renders results directly - [ ] Clearing Person A (if editable) returns the view to step 1 ### Test plan **Vitest (component layer):** - `ConversationHero` renders Person A input when `senderId` is empty - `ConversationHero` renders locked Person A chip + Person B input when `senderId` is set but `receiverId` is empty - `ConversationHero` is not rendered when both are set **Playwright (E2E):** - Journey: load `/conversations` → select Person A → verify step 2 appears with focus → select Person B → verify top bar visible and results load - Journey: load `/conversations?senderId=X&receiverId=Y` → verify hero is never shown - Accessibility: `checkA11y` on all three states (hero step 1, hero step 2, results view) - Visual regression: 320px, 768px, 1440px for each state ### Edge cases not covered in the issue - What happens if Person A is selected but has no correspondents? Does Person B's typeahead show an empty list with a helpful message, or does it look broken? - The `restrictToCorrespondentsOf` prop on the Person B typeahead already handles filtering — but if it returns zero results, the user is stuck in step 2 with no way to proceed unless they can clear Person A.
Author
Owner

🔒 Nora "NullX" Steiner — Security Engineer

No new attack surface introduced

This is a UI-only refactor. No new endpoints, no new data flows, no auth boundary changes. Nothing to flag on the change itself.

Pre-existing smell worth noting while we're here

The Person B typeahead uses restrictToCorrespondentsOf={senderId} to filter suggestions to only people who have corresponded with Person A. This is a UX convenience — but if the filtering is enforced only on the frontend, an attacker can call the persons search API directly without the filter parameter and enumerate all persons in the archive regardless of correspondence history.

This isn't introduced by this issue, but since the hero redesign makes the filter more prominent (Person B explicitly restricted to Person A's correspondents), it's worth confirming the backend enforces the same constraint — or at minimum acknowledges that persons search is intentionally unrestricted.

Recommendation: Check whether GET /api/persons?correspondentOf={id} is the enforced query, or if the typeahead just filters client-side. If it's client-side only, that's an information disclosure finding against the existing implementation. Low severity for a family archive with known users, but worth tracking.

## 🔒 Nora "NullX" Steiner — Security Engineer ### No new attack surface introduced This is a UI-only refactor. No new endpoints, no new data flows, no auth boundary changes. Nothing to flag on the change itself. ### Pre-existing smell worth noting while we're here The Person B typeahead uses `restrictToCorrespondentsOf={senderId}` to filter suggestions to only people who have corresponded with Person A. This is a UX convenience — but if the filtering is enforced **only on the frontend**, an attacker can call the persons search API directly without the filter parameter and enumerate all persons in the archive regardless of correspondence history. This isn't introduced by this issue, but since the hero redesign makes the filter more prominent (Person B explicitly restricted to Person A's correspondents), it's worth confirming the backend enforces the same constraint — or at minimum acknowledges that persons search is intentionally unrestricted. **Recommendation:** Check whether `GET /api/persons?correspondentOf={id}` is the enforced query, or if the typeahead just filters client-side. If it's client-side only, that's an information disclosure finding against the existing implementation. Low severity for a family archive with known users, but worth tracking.
Author
Owner

🚀 Tobias Wendt — DevOps & Platform Engineer

No infrastructure impact

Pure SvelteKit frontend change. No new environment variables, no new services, no Docker Compose changes, no CI pipeline changes needed.

One thing to watch: SSR bundle size

The hero adds a new visual state with its own component tree. At this scale it's negligible, but if the hero includes any new animation library imports, check the bundle size delta with npm run build before merging — SvelteKit's output includes per-route chunk sizes in the build log.

CI note

The existing Gitea Actions workflow runs npm run check (svelte-check) and npm run build on every push. The new components will be type-checked automatically. No workflow changes needed.

That's all from my side — clean change from an ops perspective.

## 🚀 Tobias Wendt — DevOps & Platform Engineer ### No infrastructure impact Pure SvelteKit frontend change. No new environment variables, no new services, no Docker Compose changes, no CI pipeline changes needed. ### One thing to watch: SSR bundle size The hero adds a new visual state with its own component tree. At this scale it's negligible, but if the hero includes any new animation library imports, check the bundle size delta with `npm run build` before merging — SvelteKit's output includes per-route chunk sizes in the build log. ### CI note The existing Gitea Actions workflow runs `npm run check` (svelte-check) and `npm run build` on every push. The new components will be type-checked automatically. No workflow changes needed. That's all from my side — clean change from an ops perspective.
Author
Owner

🎨 Leonie Voss — UX Design Lead

The sequential hero design is agreed (see earlier discussion comment). A few implementation details that need to be defined before the developer starts:

Person A chip design (unspecified)

Once Person A is selected and Person B's input appears, Person A must be shown as a chip or label — not just disappear. The chip needs:

  • The person's full name, readable at text-base (16px)
  • A clear/edit affordance (×) so the user can go back to step 1
  • Minimum touch target 44×44px on the clear button

Without this, users who pick the wrong person have no recovery path.

Transition between steps

No animation library needed — a simple CSS opacity + translate transition on the entering element is enough. Keep it under 200ms so it doesn't feel sluggish for senior users. Do not use prefers-reduced-motion-breaking animations.

"No correspondents" empty state in step 2

Sara raised this too — if Person A has no correspondents, Person B's typeahead is empty and the user is stuck. The hero step 2 should show a fallback message inline: e.g. "Keine Korrespondenz für [Name] gefunden" with a link back to step 1. This is a usability blocker if unaddressed.

Top bar input height

The agreed h-12 (48px) applies to the PersonTypeahead inputs in ConversationFilterBar. The date inputs and sort button should match this height for visual consistency — currently the sort button is h-[42px] hardcoded. Align everything to h-12.

## 🎨 Leonie Voss — UX Design Lead The sequential hero design is agreed (see earlier discussion comment). A few implementation details that need to be defined before the developer starts: ### Person A chip design (unspecified) Once Person A is selected and Person B's input appears, Person A must be shown as a chip or label — not just disappear. The chip needs: - The person's full name, readable at `text-base` (16px) - A clear/edit affordance (×) so the user can go back to step 1 - Minimum touch target 44×44px on the clear button Without this, users who pick the wrong person have no recovery path. ### Transition between steps No animation library needed — a simple CSS opacity + translate transition on the entering element is enough. Keep it under 200ms so it doesn't feel sluggish for senior users. Do not use `prefers-reduced-motion`-breaking animations. ### "No correspondents" empty state in step 2 Sara raised this too — if Person A has no correspondents, Person B's typeahead is empty and the user is stuck. The hero step 2 should show a fallback message inline: e.g. *"Keine Korrespondenz für [Name] gefunden"* with a link back to step 1. This is a usability blocker if unaddressed. ### Top bar input height The agreed `h-12` (48px) applies to the `PersonTypeahead` inputs in `ConversationFilterBar`. The date inputs and sort button should match this height for visual consistency — currently the sort button is `h-[42px]` hardcoded. Align everything to `h-12`.
Author
Owner

🎨 Leonie Voss — UX Design Lead

Design direction update — sequential hero dropped, discovery framing adopted. Supersedes the earlier interaction model comment.


Resolved

  • Sequential hero dropped — The UI works with one person selected. No need for a two-step sequential input. The hero shows a single centred PersonTypeahead (h-14 / 56px). Once a person is selected, results load immediately.

  • Person B is optional — After selection, the view collapses to the top bar. Both PersonTypeaheads are shown symmetrically: Person A pre-filled and editable, Person B empty with placeholder "Optional: zweite Person". Person B narrows the results to a specific exchange but is never required.

  • Hero headline: "Wessen Briefe möchten Sie lesen?" — warm, personal, action-oriented. Signals exploration, not search.

  • Cross-link above the typeahead: "Suchen Sie ein bestimmtes Dokument? → Zur Dokumentensuche"text-sm, placed above the typeahead in the hero. Helps users who land here looking for a specific document find their way to the document search page.

  • Nav label + page title: "Briefwechsel" — replaces "Korrespondenz" throughout. Warmer, more accessible to non-archivists and senior users.


Superseded from previous discussion

  • Sequential hero (Person A chip → Person B typeahead) — dropped entirely
  • Three UI states (!senderId, senderId && !receiverId, both) — now two states: hero (no person selected) and results (person selected, top bar visible)

The discovery framing is the right mental model for this page. "Briefwechsel" positions it clearly alongside the document search — one page for exploration, one for targeted lookup. The cross-link makes the distinction explicit for users who arrive at the wrong entry point.

## 🎨 Leonie Voss — UX Design Lead Design direction update — sequential hero dropped, discovery framing adopted. Supersedes the earlier interaction model comment. --- ### Resolved - **Sequential hero dropped** — The UI works with one person selected. No need for a two-step sequential input. The hero shows a single centred PersonTypeahead (`h-14` / 56px). Once a person is selected, results load immediately. - **Person B is optional** — After selection, the view collapses to the top bar. Both PersonTypeaheads are shown symmetrically: Person A pre-filled and editable, Person B empty with placeholder *"Optional: zweite Person"*. Person B narrows the results to a specific exchange but is never required. - **Hero headline:** *"Wessen Briefe möchten Sie lesen?"* — warm, personal, action-oriented. Signals exploration, not search. - **Cross-link above the typeahead:** *"Suchen Sie ein bestimmtes Dokument? → Zur Dokumentensuche"* — `text-sm`, placed above the typeahead in the hero. Helps users who land here looking for a specific document find their way to the document search page. - **Nav label + page title: "Briefwechsel"** — replaces "Korrespondenz" throughout. Warmer, more accessible to non-archivists and senior users. --- ### Superseded from previous discussion - ~~Sequential hero (Person A chip → Person B typeahead)~~ — dropped entirely - ~~Three UI states (`!senderId`, `senderId && !receiverId`, `both`)~~ — now two states: hero (no person selected) and results (person selected, top bar visible) --- The discovery framing is the right mental model for this page. "Briefwechsel" positions it clearly alongside the document search — one page for exploration, one for targeted lookup. The cross-link makes the distinction explicit for users who arrive at the wrong entry point.
Author
Owner

Implementation complete — feat/issue-179-briefwechsel-hero

All acceptance criteria addressed. Here's what was implemented:

Commits

  1. a9228d1 — Rename route /korrespondenz/briefwechsel (all internal links updated)
  2. efac704 — Rename labels "Korrespondenz" → "Briefwechsel" in de/en/es messages
  3. e9acd44 — New CorrespondenzHero component with discovery headline, cross-link, h-14 PersonTypeahead, recent persons chips. Adds large prop to PersonTypeahead.
  4. f39d9e6 — Two render states: hero (no senderId) vs results (senderId set). Unified padding with max-w-7xl. Removed focus delegation hack.
  5. 7b2324e — Bump person bar inputs to h-12, unify strip padding
  6. d5e3de5 — Constrain results state to max-w-7xl like other overview pages
  7. fbf5e9f — Remove old CorrespondenzEmptyState (fully replaced)
  8. 822a2fa — Add inner padding to strip components

Acceptance criteria

  • Empty state has a visually centred search bar as the primary focal point (CorrespondenzHero with h-14 typeahead)
  • Typing into the hero typeahead keeps focus (no delegation to another input)
  • Search executes from the hero field directly
  • All primary text ≥ 16px (text-base / text-2xl)
  • Helper text ≥ 14px (text-sm)
  • Layout works at 320px (responsive px-4 sm:px-6 lg:px-8)
  • Touch targets on hero input ≥ 44×44px (h-14 = 56px)
  • Nav label + page title renamed to "Briefwechsel"
  • Cross-link to document search in hero state
  • Side padding unified with other overview pages

Test coverage

38 tests across 3 test files — all green:

  • CorrespondenzHero.svelte.spec.ts (5 tests): headline, cross-link, typeahead, recent persons, chip click
  • page.svelte.spec.ts (27 tests): hero/results state switching, recent persons, hint bar, filter controls, swap, year dividers, new doc link
  • page.server.spec.ts (6 tests): server load function
## Implementation complete — `feat/issue-179-briefwechsel-hero` All acceptance criteria addressed. Here's what was implemented: ### Commits 1. **`a9228d1`** — Rename route `/korrespondenz` → `/briefwechsel` (all internal links updated) 2. **`efac704`** — Rename labels "Korrespondenz" → "Briefwechsel" in de/en/es messages 3. **`e9acd44`** — New `CorrespondenzHero` component with discovery headline, cross-link, `h-14` PersonTypeahead, recent persons chips. Adds `large` prop to `PersonTypeahead`. 4. **`f39d9e6`** — Two render states: hero (no senderId) vs results (senderId set). Unified padding with `max-w-7xl`. Removed focus delegation hack. 5. **`7b2324e`** — Bump person bar inputs to `h-12`, unify strip padding 6. **`d5e3de5`** — Constrain results state to `max-w-7xl` like other overview pages 7. **`fbf5e9f`** — Remove old `CorrespondenzEmptyState` (fully replaced) 8. **`822a2fa`** — Add inner padding to strip components ### Acceptance criteria - ✅ Empty state has a visually centred search bar as the primary focal point (`CorrespondenzHero` with `h-14` typeahead) - ✅ Typing into the hero typeahead keeps focus (no delegation to another input) - ✅ Search executes from the hero field directly - ✅ All primary text ≥ 16px (`text-base` / `text-2xl`) - ✅ Helper text ≥ 14px (`text-sm`) - ✅ Layout works at 320px (responsive `px-4 sm:px-6 lg:px-8`) - ✅ Touch targets on hero input ≥ 44×44px (`h-14` = 56px) - ✅ Nav label + page title renamed to "Briefwechsel" - ✅ Cross-link to document search in hero state - ✅ Side padding unified with other overview pages ### Test coverage 38 tests across 3 test files — all green: - `CorrespondenzHero.svelte.spec.ts` (5 tests): headline, cross-link, typeahead, recent persons, chip click - `page.svelte.spec.ts` (27 tests): hero/results state switching, recent persons, hint bar, filter controls, swap, year dividers, new doc link - `page.server.spec.ts` (6 tests): server load function
Sign in to join this conversation.
No Label ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#179