From a9228d156f83bc610ffa8b85d14494487008d053 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 19:22:22 +0200 Subject: [PATCH 01/21] =?UTF-8?q?refactor(ui):=20rename=20route=20/korresp?= =?UTF-8?q?ondenz=20=E2=86=92=20/briefwechsel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/routes/AppNav.svelte | 8 ++++---- .../{korrespondenz => briefwechsel}/+page.server.ts | 0 .../routes/{korrespondenz => briefwechsel}/+page.svelte | 2 +- .../ConversationFilterBar.svelte | 0 .../ConversationTimeline.svelte | 0 .../CorrespondentSuggestionsDropdown.svelte | 0 .../CorrespondenzEmptyState.svelte | 0 .../CorrespondenzFilterControls.svelte | 0 .../CorrespondenzPersonBar.svelte | 0 .../SinglePersonHintBar.svelte | 0 .../{korrespondenz => briefwechsel}/page.server.spec.ts | 0 .../{korrespondenz => briefwechsel}/page.svelte.spec.ts | 0 .../src/routes/persons/[id]/CoCorrespondentsList.svelte | 2 +- 13 files changed, 6 insertions(+), 6 deletions(-) rename frontend/src/routes/{korrespondenz => briefwechsel}/+page.server.ts (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/+page.svelte (98%) rename frontend/src/routes/{korrespondenz => briefwechsel}/ConversationFilterBar.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/ConversationTimeline.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/CorrespondentSuggestionsDropdown.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/CorrespondenzEmptyState.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/CorrespondenzFilterControls.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/CorrespondenzPersonBar.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/SinglePersonHintBar.svelte (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/page.server.spec.ts (100%) rename frontend/src/routes/{korrespondenz => briefwechsel}/page.svelte.spec.ts (100%) diff --git a/frontend/src/routes/AppNav.svelte b/frontend/src/routes/AppNav.svelte index 4fe77704..026b7f72 100644 --- a/frontend/src/routes/AppNav.svelte +++ b/frontend/src/routes/AppNav.svelte @@ -60,9 +60,9 @@ function handleOverlayKeydown(event: KeyboardEvent) { @@ -161,9 +161,9 @@ function handleOverlayKeydown(event: KeyboardEvent) { diff --git a/frontend/src/routes/korrespondenz/+page.server.ts b/frontend/src/routes/briefwechsel/+page.server.ts similarity index 100% rename from frontend/src/routes/korrespondenz/+page.server.ts rename to frontend/src/routes/briefwechsel/+page.server.ts diff --git a/frontend/src/routes/korrespondenz/+page.svelte b/frontend/src/routes/briefwechsel/+page.svelte similarity index 98% rename from frontend/src/routes/korrespondenz/+page.svelte rename to frontend/src/routes/briefwechsel/+page.svelte index c6cddf6d..3ac6308f 100644 --- a/frontend/src/routes/korrespondenz/+page.svelte +++ b/frontend/src/routes/briefwechsel/+page.svelte @@ -56,7 +56,7 @@ function applyFilters() { if (fromDate) params.set('from', fromDate); if (toDate) params.set('to', toDate); params.set('dir', sortDir); - goto(`/korrespondenz?${params.toString()}`, { keepFocus: true }); + goto(`/briefwechsel?${params.toString()}`, { keepFocus: true }); } function toggleSort() { diff --git a/frontend/src/routes/korrespondenz/ConversationFilterBar.svelte b/frontend/src/routes/briefwechsel/ConversationFilterBar.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/ConversationFilterBar.svelte rename to frontend/src/routes/briefwechsel/ConversationFilterBar.svelte diff --git a/frontend/src/routes/korrespondenz/ConversationTimeline.svelte b/frontend/src/routes/briefwechsel/ConversationTimeline.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/ConversationTimeline.svelte rename to frontend/src/routes/briefwechsel/ConversationTimeline.svelte diff --git a/frontend/src/routes/korrespondenz/CorrespondentSuggestionsDropdown.svelte b/frontend/src/routes/briefwechsel/CorrespondentSuggestionsDropdown.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/CorrespondentSuggestionsDropdown.svelte rename to frontend/src/routes/briefwechsel/CorrespondentSuggestionsDropdown.svelte diff --git a/frontend/src/routes/korrespondenz/CorrespondenzEmptyState.svelte b/frontend/src/routes/briefwechsel/CorrespondenzEmptyState.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/CorrespondenzEmptyState.svelte rename to frontend/src/routes/briefwechsel/CorrespondenzEmptyState.svelte diff --git a/frontend/src/routes/korrespondenz/CorrespondenzFilterControls.svelte b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/CorrespondenzFilterControls.svelte rename to frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte diff --git a/frontend/src/routes/korrespondenz/CorrespondenzPersonBar.svelte b/frontend/src/routes/briefwechsel/CorrespondenzPersonBar.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/CorrespondenzPersonBar.svelte rename to frontend/src/routes/briefwechsel/CorrespondenzPersonBar.svelte diff --git a/frontend/src/routes/korrespondenz/SinglePersonHintBar.svelte b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte similarity index 100% rename from frontend/src/routes/korrespondenz/SinglePersonHintBar.svelte rename to frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte diff --git a/frontend/src/routes/korrespondenz/page.server.spec.ts b/frontend/src/routes/briefwechsel/page.server.spec.ts similarity index 100% rename from frontend/src/routes/korrespondenz/page.server.spec.ts rename to frontend/src/routes/briefwechsel/page.server.spec.ts diff --git a/frontend/src/routes/korrespondenz/page.svelte.spec.ts b/frontend/src/routes/briefwechsel/page.svelte.spec.ts similarity index 100% rename from frontend/src/routes/korrespondenz/page.svelte.spec.ts rename to frontend/src/routes/briefwechsel/page.svelte.spec.ts diff --git a/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte index b6f0b01d..1db48079 100644 --- a/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte +++ b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte @@ -30,7 +30,7 @@ function initials(name: string): string {
{#each coCorrespondents as c (c.id)} -- 2.49.1 From efac704d59e1345b81a1cee5b25df20dc296554f Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 19:23:47 +0200 Subject: [PATCH 02/21] =?UTF-8?q?feat(i18n):=20rename=20Korrespondenz=20?= =?UTF-8?q?=E2=86=92=20Briefwechsel=20in=20all=20languages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/messages/de.json | 8 ++++---- frontend/messages/en.json | 8 ++++---- frontend/messages/es.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index a78baf18..fdc37e57 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -16,7 +16,7 @@ "error_internal_error": "Ein unerwarteter Fehler ist aufgetreten.", "nav_documents": "Dokumente", "nav_persons": "Personen", - "nav_conversations": "Korrespondenz", + "nav_conversations": "Briefwechsel", "nav_admin": "Admin", "nav_logout": "Abmelden", "btn_save": "Speichern", @@ -130,7 +130,7 @@ "person_co_correspondents_heading": "Häufige Korrespondenten", "person_correspondents_hint": "klicken für Konversation", "person_show_more": "+ {count} weitere anzeigen", - "conv_heading": "Korrespondenz", + "conv_heading": "Briefwechsel", "conv_subtitle": "Briefwechsel einer Person durchsuchen — mit oder ohne Korrespondent.", "conv_label_person_a": "Person A (Absender)", "conv_label_person_b": "Korrespondent", @@ -139,13 +139,13 @@ "conv_sort_label": "Sortierung:", "conv_sort_newest": "Neueste zuerst", "conv_sort_oldest": "Älteste zuerst", - "conv_empty_heading": "Korrespondenz durchsuchen", + "conv_empty_heading": "Wessen Briefe möchten Sie lesen?", "conv_empty_text": "Wähle eine Person aus dem Archiv um deren Briefe zu sehen — mit oder ohne Korrespondent.", "conv_no_results_heading": "Keine Dokumente gefunden.", "conv_no_results_text": "Versuchen Sie, den Zeitraum anzupassen.", "conv_swap_btn": "Personen tauschen", "conv_summary": "{count} Dokumente · {yearFrom}–{yearTo}", - "conv_new_doc_link": "Neues Dokument in dieser Korrespondenz", + "conv_new_doc_link": "Neues Dokument in diesem Briefwechsel", "conv_label_correspondent_optional": "Korrespondent", "conv_hint_single_person": "Alle Briefe von {name} — wähle einen Korrespondenten oben um einzugrenzen", "conv_hint_single_person_filtered": "Alle Briefe von {name} · {from}–{to} · {sortLabel}", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 5e447abb..e323e3ca 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -16,7 +16,7 @@ "error_internal_error": "An unexpected error occurred.", "nav_documents": "Documents", "nav_persons": "Persons", - "nav_conversations": "Correspondence", + "nav_conversations": "Letters", "nav_admin": "Admin", "nav_logout": "Sign out", "btn_save": "Save", @@ -130,7 +130,7 @@ "person_co_correspondents_heading": "Frequent correspondents", "person_correspondents_hint": "click to view conversation", "person_show_more": "+ {count} more", - "conv_heading": "Correspondence", + "conv_heading": "Letters", "conv_subtitle": "Browse a person's letters — with or without a correspondent.", "conv_label_person_a": "Person A (Sender)", "conv_label_person_b": "Correspondent", @@ -139,13 +139,13 @@ "conv_sort_label": "Sort:", "conv_sort_newest": "Newest first", "conv_sort_oldest": "Oldest first", - "conv_empty_heading": "Browse correspondence", + "conv_empty_heading": "Whose letters would you like to read?", "conv_empty_text": "Choose a person from the archive to see their letters — with or without a correspondent.", "conv_no_results_heading": "No documents found.", "conv_no_results_text": "Try adjusting the time period.", "conv_swap_btn": "Swap persons", "conv_summary": "{count} documents · {yearFrom}–{yearTo}", - "conv_new_doc_link": "New document in this correspondence", + "conv_new_doc_link": "New document in this exchange", "conv_label_correspondent_optional": "Correspondent", "conv_hint_single_person": "All letters from {name} — choose a correspondent above to narrow down", "conv_hint_single_person_filtered": "All letters from {name} · {from}–{to} · {sortLabel}", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 95641956..0bdeabb5 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -16,7 +16,7 @@ "error_internal_error": "Se ha producido un error inesperado.", "nav_documents": "Documentos", "nav_persons": "Personas", - "nav_conversations": "Correspondencia", + "nav_conversations": "Cartas", "nav_admin": "Admin", "nav_logout": "Cerrar sesión", "btn_save": "Guardar", @@ -130,7 +130,7 @@ "person_co_correspondents_heading": "Corresponsales frecuentes", "person_correspondents_hint": "clic para ver conversación", "person_show_more": "+ {count} más", - "conv_heading": "Correspondencia", + "conv_heading": "Cartas", "conv_subtitle": "Explore las cartas de una persona — con o sin corresponsal.", "conv_label_person_a": "Persona A (Remitente)", "conv_label_person_b": "Corresponsal", @@ -139,13 +139,13 @@ "conv_sort_label": "Ordenar:", "conv_sort_newest": "Más reciente primero", "conv_sort_oldest": "Más antiguo primero", - "conv_empty_heading": "Explorar correspondencia", + "conv_empty_heading": "¿De quién desea leer las cartas?", "conv_empty_text": "Elige una persona del archivo para ver sus cartas — con o sin corresponsal.", "conv_no_results_heading": "No se encontraron documentos.", "conv_no_results_text": "Intente ajustar el período de tiempo.", "conv_swap_btn": "Intercambiar personas", "conv_summary": "{count} documentos · {yearFrom}–{yearTo}", - "conv_new_doc_link": "Nuevo documento en esta correspondencia", + "conv_new_doc_link": "Nuevo documento en este intercambio", "conv_label_correspondent_optional": "Corresponsal", "conv_hint_single_person": "Todas las cartas de {name} — elige un corresponsal arriba para filtrar", "conv_hint_single_person_filtered": "Todas las cartas de {name} · {from}–{to} · {sortLabel}", -- 2.49.1 From e9acd44acb3cf95e0f097d090fa51238a29d18bf Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 19:37:58 +0200 Subject: [PATCH 03/21] feat(ui): add CorrespondenzHero with discovery headline and large typeahead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/messages/de.json | 1 + frontend/messages/en.json | 1 + frontend/messages/es.json | 1 + .../src/lib/components/PersonTypeahead.svelte | 10 ++- .../briefwechsel/CorrespondenzHero.svelte | 90 +++++++++++++++++++ .../CorrespondenzHero.svelte.spec.ts | 50 +++++++++++ 6 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 frontend/src/routes/briefwechsel/CorrespondenzHero.svelte create mode 100644 frontend/src/routes/briefwechsel/CorrespondenzHero.svelte.spec.ts diff --git a/frontend/messages/de.json b/frontend/messages/de.json index fdc37e57..4c933f16 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -141,6 +141,7 @@ "conv_sort_oldest": "Älteste zuerst", "conv_empty_heading": "Wessen Briefe möchten Sie lesen?", "conv_empty_text": "Wähle eine Person aus dem Archiv um deren Briefe zu sehen — mit oder ohne Korrespondent.", + "conv_hero_crosslink": "Suchen Sie ein bestimmtes Dokument? → Zur Dokumentensuche", "conv_no_results_heading": "Keine Dokumente gefunden.", "conv_no_results_text": "Versuchen Sie, den Zeitraum anzupassen.", "conv_swap_btn": "Personen tauschen", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e323e3ca..70335627 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -141,6 +141,7 @@ "conv_sort_oldest": "Oldest first", "conv_empty_heading": "Whose letters would you like to read?", "conv_empty_text": "Choose a person from the archive to see their letters — with or without a correspondent.", + "conv_hero_crosslink": "Looking for a specific document? → Go to document search", "conv_no_results_heading": "No documents found.", "conv_no_results_text": "Try adjusting the time period.", "conv_swap_btn": "Swap persons", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 0bdeabb5..c33c6ee2 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -141,6 +141,7 @@ "conv_sort_oldest": "Más antiguo primero", "conv_empty_heading": "¿De quién desea leer las cartas?", "conv_empty_text": "Elige una persona del archivo para ver sus cartas — con o sin corresponsal.", + "conv_hero_crosslink": "¿Busca un documento en particular? → Ir a la búsqueda", "conv_no_results_heading": "No se encontraron documentos.", "conv_no_results_text": "Intente ajustar el período de tiempo.", "conv_swap_btn": "Intercambiar personas", diff --git a/frontend/src/lib/components/PersonTypeahead.svelte b/frontend/src/lib/components/PersonTypeahead.svelte index 8e9cf930..316d54d7 100644 --- a/frontend/src/lib/components/PersonTypeahead.svelte +++ b/frontend/src/lib/components/PersonTypeahead.svelte @@ -13,6 +13,7 @@ interface Props { suggestedName?: string; placeholder?: string; compact?: boolean; + large?: boolean; restrictToCorrespondentsOf?: string; onchange?: (value: string) => void; onfocused?: () => void; @@ -26,6 +27,7 @@ let { suggestedName = '', placeholder, compact = false, + large = false, restrictToCorrespondentsOf, onchange, onfocused @@ -140,9 +142,11 @@ function selectPerson(person: Person) { oninput={handleInput} onfocus={handleFocus} placeholder={placeholder ?? m.comp_typeahead_placeholder()} - class={compact - ? 'mt-1 block h-9 w-full rounded border border-line bg-surface px-2 text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring' - : 'mt-1 block w-full rounded-md border border-line bg-surface p-2 text-ink shadow-sm placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'} + class={large + ? 'mt-2 block h-14 w-full rounded-md border border-line bg-surface px-4 text-base text-ink shadow-sm placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring' + : compact + ? 'mt-1 block h-9 w-full rounded border border-line bg-surface px-2 text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring' + : 'mt-1 block w-full rounded-md border border-line bg-surface p-2 text-ink shadow-sm placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'} /> {#if showDropdown && (results.length > 0 || loading)} diff --git a/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte b/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte new file mode 100644 index 00000000..924d37a2 --- /dev/null +++ b/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte @@ -0,0 +1,90 @@ + + +
+ + + {m.conv_hero_crosslink()} + + + +

+ {m.conv_empty_heading()} +

+ + +
+ +
+ + + {#if recentPersons.length > 0} +
+
+ oder +
+
+ +
+ + {m.conv_empty_recent_label()} + +
+ {#each recentPersons as person (person.id)} + + {/each} +
+
+ {/if} +
diff --git a/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte.spec.ts b/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte.spec.ts new file mode 100644 index 00000000..b3108334 --- /dev/null +++ b/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte.spec.ts @@ -0,0 +1,50 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import CorrespondenzHero from './CorrespondenzHero.svelte'; + +vi.mock('$app/navigation', () => ({ goto: vi.fn() })); + +afterEach(cleanup); + +const noop = () => {}; + +describe('CorrespondenzHero — headline and cross-link', () => { + it('renders the discovery headline', async () => { + render(CorrespondenzHero, { onSelectPerson: noop }); + await expect.element(page.getByText(/Wessen Briefe möchten Sie lesen/i)).toBeInTheDocument(); + }); + + it('renders a cross-link to the document search page', async () => { + render(CorrespondenzHero, { onSelectPerson: noop }); + const link = page.getByRole('link', { name: /Zur Dokumentensuche/i }); + await expect.element(link).toBeInTheDocument(); + await expect.element(link).toHaveAttribute('href', '/'); + }); + + it('renders a person typeahead input', async () => { + render(CorrespondenzHero, { onSelectPerson: noop }); + await expect.element(page.getByTestId('conv-hero').getByRole('textbox')).toBeInTheDocument(); + }); +}); + +describe('CorrespondenzHero — recent persons', () => { + it('shows recent person chips when provided', async () => { + render(CorrespondenzHero, { + onSelectPerson: noop, + recentPersons: [{ id: 'r1', name: 'Clara Braun' }] + }); + await expect.element(page.getByText('Clara Braun')).toBeInTheDocument(); + }); + + it('calls onSelectPerson when a recent person chip is clicked', async () => { + const spy = vi.fn(); + render(CorrespondenzHero, { + onSelectPerson: spy, + recentPersons: [{ id: 'r1', name: 'Clara Braun' }] + }); + await expect.element(page.getByText('Clara Braun')).toBeInTheDocument(); + document.querySelector('[data-testid="recent-person-r1"]')!.click(); + expect(spy).toHaveBeenCalledWith('r1'); + }); +}); -- 2.49.1 From f39d9e6f30f16b866896b50ecdc6f5ffbec16545 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 19:43:54 +0200 Subject: [PATCH 04/21] =?UTF-8?q?feat(ui):=20two=20render=20states=20?= =?UTF-8?q?=E2=80=94=20hero=20vs=20results=20=E2=80=94=20with=20unified=20?= =?UTF-8?q?padding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/routes/briefwechsel/+page.svelte | 137 ++++++++++-------- .../CorrespondenzFilterControls.svelte | 1 + .../CorrespondenzPersonBar.svelte | 5 +- .../routes/briefwechsel/page.svelte.spec.ts | 70 ++++++--- 4 files changed, 126 insertions(+), 87 deletions(-) diff --git a/frontend/src/routes/briefwechsel/+page.svelte b/frontend/src/routes/briefwechsel/+page.svelte index 3ac6308f..a280e90b 100644 --- a/frontend/src/routes/briefwechsel/+page.svelte +++ b/frontend/src/routes/briefwechsel/+page.svelte @@ -1,30 +1,26 @@ - -
- - - - - - - - {#if isSinglePerson} - +
+ +
+{:else} + +
+ - {/if} -
- -
- {#if !senderId} - - {:else if data.documents.length === 0} -
-

{m.conv_no_results_heading()}

-

{m.conv_no_results_text()}

-
- {:else} - - {/if} -
+ + {#if isSinglePerson} + + {/if} +
+ +
+ {#if data.documents.length === 0} +
+

{m.conv_no_results_heading()}

+

{m.conv_no_results_text()}

+
+ {:else} + + {/if} +
+{/if} diff --git a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte index d26bd3bb..78313cda 100644 --- a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte +++ b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte @@ -27,6 +27,7 @@ let isActive = $derived(!!(fromDate || toDate || sortDir !== 'DESC'));
-
+
{ - it('shows the search heading when no person is selected', async () => { +describe('Briefwechsel page – hero state', () => { + it('shows the hero when no person is selected', async () => { render(Page, { data: baseData }); - await expect.element(page.getByText(/Korrespondenz durchsuchen/i)).toBeInTheDocument(); + await expect.element(page.getByTestId('conv-hero')).toBeInTheDocument(); }); - it('shows the empty-search button', async () => { + it('shows the discovery headline', async () => { render(Page, { data: baseData }); - await expect.element(page.getByTestId('conv-empty-search')).toBeInTheDocument(); + await expect.element(page.getByText(/Wessen Briefe möchten Sie lesen/i)).toBeInTheDocument(); + }); + + it('does not show the person bar in hero state', async () => { + render(Page, { data: baseData }); + await expect.element(page.getByTestId('conv-hero')).toBeInTheDocument(); + await expect.element(page.getByTestId('conv-person-bar')).not.toBeInTheDocument(); + }); + + it('does not show filter controls in hero state', async () => { + render(Page, { data: baseData }); + await expect.element(page.getByTestId('conv-hero')).toBeInTheDocument(); + await expect.element(page.getByTestId('conv-filter-controls')).not.toBeInTheDocument(); }); it('does not show the new document link when no person is selected', async () => { @@ -77,9 +89,29 @@ describe('Korrespondenz page – empty state', () => { }); }); +// ─── Results state (senderId set) ──────────────────────────────────────────── + +describe('Briefwechsel page – results state', () => { + it('does not show the hero when senderId is set', async () => { + render(Page, { data: withSender }); + await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument(); + await expect.element(page.getByTestId('conv-hero')).not.toBeInTheDocument(); + }); + + it('shows the person bar when senderId is set', async () => { + render(Page, { data: withSender }); + await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument(); + }); + + it('shows filter controls when senderId is set', async () => { + render(Page, { data: withSender }); + await expect.element(page.getByTestId('conv-filter-controls')).toBeInTheDocument(); + }); +}); + // ─── Recent persons chips ───────────────────────────────────────────────────── -describe('Korrespondenz page – recent persons', () => { +describe('Briefwechsel page – recent persons', () => { it('shows recent person chips from localStorage', async () => { localStorage.setItem( 'korrespondenz_recent_persons', @@ -93,15 +125,14 @@ describe('Korrespondenz page – recent persons', () => { it('does not crash when localStorage contains corrupt JSON', async () => { localStorage.setItem('korrespondenz_recent_persons', '}{not valid json'); render(Page, { data: baseData }); - // Empty state heading is still shown — no chip list crash - await expect.element(page.getByText(/Korrespondenz durchsuchen/i)).toBeInTheDocument(); + await expect.element(page.getByText(/Wessen Briefe möchten Sie lesen/i)).toBeInTheDocument(); localStorage.removeItem('korrespondenz_recent_persons'); }); }); // ─── Single-person hint bar ─────────────────────────────────────────────────── -describe('Korrespondenz page – single-person hint bar', () => { +describe('Briefwechsel page – single-person hint bar', () => { it('shows hint bar when only senderId is set', async () => { render(Page, { data: withSender }); await expect.element(page.getByText(/Alle Briefe von Hans Müller/i)).toBeInTheDocument(); @@ -120,13 +151,7 @@ describe('Korrespondenz page – single-person hint bar', () => { // ─── Filter controls disabled state ────────────────────────────────────────── -describe('Korrespondenz page – filter strip Row 2 disabled state', () => { - it('renders filter controls with aria-disabled when no senderId', async () => { - render(Page, { data: baseData }); - const strip = document.querySelector('[aria-disabled="true"]'); - expect(strip).not.toBeNull(); - }); - +describe('Briefwechsel page – filter strip Row 2 disabled state', () => { it('filter controls are not aria-disabled when senderId is set', async () => { render(Page, { data: withSender }); const strip = document.querySelector('[aria-disabled="false"]'); @@ -136,7 +161,7 @@ describe('Korrespondenz page – filter strip Row 2 disabled state', () => { // ─── Strip letter count ─────────────────────────────────────────────────────── -describe('Korrespondenz page – strip letter count', () => { +describe('Briefwechsel page – strip letter count', () => { it('shows 0 Briefe when senderId is set but no documents', async () => { render(Page, { data: withSender }); await expect.element(page.getByTestId('conv-strip-count')).toHaveTextContent('0 Briefe'); @@ -150,7 +175,7 @@ describe('Korrespondenz page – strip letter count', () => { // ─── No results ─────────────────────────────────────────────────────────────── -describe('Korrespondenz page – no results', () => { +describe('Briefwechsel page – no results', () => { it('shows "no documents found" when a person is selected but there are no documents', async () => { render(Page, { data: withSender }); await expect.element(page.getByText(/Keine Dokumente gefunden/i)).toBeInTheDocument(); @@ -159,12 +184,11 @@ describe('Korrespondenz page – no results', () => { // ─── Swap button ────────────────────────────────────────────────────────────── -describe('Korrespondenz page – swap button', () => { +describe('Briefwechsel page – swap button', () => { it('swap button is invisible when only one person is set', async () => { render(Page, { data: withSender }); const btn = document.querySelector('[data-testid="conv-swap-btn"]'); expect(btn).not.toBeNull(); - // opacity-0 is applied via class when swapVisible is false expect(btn!.className).toMatch(/opacity-0/); }); @@ -187,7 +211,7 @@ describe('Korrespondenz page – swap button', () => { // ─── Year dividers ──────────────────────────────────────────────────────────── -describe('Korrespondenz page – year dividers', () => { +describe('Briefwechsel page – year dividers', () => { it('renders a year divider for the first document', async () => { render(Page, { data: withDocs }); await expect.element(page.getByTestId('year-divider').first()).toHaveTextContent('1923'); @@ -222,7 +246,7 @@ describe('Korrespondenz page – year dividers', () => { // ─── New document link ──────────────────────────────────────────────────────── -describe('Korrespondenz page – new document link', () => { +describe('Briefwechsel page – new document link', () => { it('shows the link with correct href for a write user (bilateral)', async () => { render(Page, { data: { ...withDocs, canWrite: true } }); const link = page.getByTestId('conv-new-doc-link'); -- 2.49.1 From 7b2324ecfb42136f6dc7f4456cccc923313bcfd1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 19:46:36 +0200 Subject: [PATCH 05/21] fix(ui): unify strip padding and bump person bar inputs to h-12 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 --- .../src/routes/briefwechsel/CorrespondenzFilterControls.svelte | 2 +- frontend/src/routes/briefwechsel/CorrespondenzPersonBar.svelte | 2 +- frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte index 78313cda..5d686ffa 100644 --- a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte +++ b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte @@ -28,7 +28,7 @@ let isActive = $derived(!!(fromDate || toDate || sortDir !== 'DESC'));
diff --git a/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte index 034e0284..b2a45162 100644 --- a/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte +++ b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte @@ -19,7 +19,7 @@ let toYear = $derived(toDate ? toDate.substring(0, 4) : '');
Date: Mon, 6 Apr 2026 19:49:30 +0200 Subject: [PATCH 06/21] fix(ui): constrain results state to max-w-7xl like other overview pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/routes/briefwechsel/+page.svelte | 40 +++++++++---------- .../CorrespondenzFilterControls.svelte | 2 +- .../CorrespondenzPersonBar.svelte | 2 +- .../briefwechsel/SinglePersonHintBar.svelte | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/frontend/src/routes/briefwechsel/+page.svelte b/frontend/src/routes/briefwechsel/+page.svelte index a280e90b..53f095f7 100644 --- a/frontend/src/routes/briefwechsel/+page.svelte +++ b/frontend/src/routes/briefwechsel/+page.svelte @@ -99,7 +99,7 @@ function selectPerson(id: string) {
{:else} -
+
{/if} -
-
- {#if data.documents.length === 0} -
-

{m.conv_no_results_heading()}

-

{m.conv_no_results_text()}

-
- {:else} - - {/if} +
+ {#if data.documents.length === 0} +
+

{m.conv_no_results_heading()}

+

{m.conv_no_results_text()}

+
+ {:else} + + {/if} +
{/if} diff --git a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte index 5d686ffa..0740c06c 100644 --- a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte +++ b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte @@ -28,7 +28,7 @@ let isActive = $derived(!!(fromDate || toDate || sortDir !== 'DESC'));
diff --git a/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte index b2a45162..31734da0 100644 --- a/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte +++ b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte @@ -19,7 +19,7 @@ let toYear = $derived(toDate ? toDate.substring(0, 4) : '');
Date: Mon, 6 Apr 2026 19:50:07 +0200 Subject: [PATCH 07/21] refactor(ui): remove CorrespondenzEmptyState, replaced by CorrespondenzHero Refs: #179 Co-Authored-By: Claude Sonnet 4.6 --- .../CorrespondenzEmptyState.svelte | 120 ------------------ 1 file changed, 120 deletions(-) delete mode 100644 frontend/src/routes/briefwechsel/CorrespondenzEmptyState.svelte diff --git a/frontend/src/routes/briefwechsel/CorrespondenzEmptyState.svelte b/frontend/src/routes/briefwechsel/CorrespondenzEmptyState.svelte deleted file mode 100644 index b6a3ad64..00000000 --- a/frontend/src/routes/briefwechsel/CorrespondenzEmptyState.svelte +++ /dev/null @@ -1,120 +0,0 @@ - - -
- -
- -
- - -

{m.conv_empty_heading()}

- - -

- {m.conv_empty_text()} -

- - - - - - {#if recentPersons.length > 0} - -
-
- oder -
-
- -
- - {m.conv_empty_recent_label()} - -
- {#each recentPersons as person (person.id)} - - - {/each} -
-
- {/if} -
-- 2.49.1 From 822a2fac3a106ec706748f26f161eeb4d643b6bc Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 19:53:03 +0200 Subject: [PATCH 08/21] fix(ui): add inner padding to strip components 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 --- .../src/routes/briefwechsel/CorrespondenzFilterControls.svelte | 2 +- frontend/src/routes/briefwechsel/CorrespondenzPersonBar.svelte | 2 +- frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte index 0740c06c..e47c1c25 100644 --- a/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte +++ b/frontend/src/routes/briefwechsel/CorrespondenzFilterControls.svelte @@ -28,7 +28,7 @@ let isActive = $derived(!!(fromDate || toDate || sortDir !== 'DESC'));
diff --git a/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte index 31734da0..00fe9a59 100644 --- a/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte +++ b/frontend/src/routes/briefwechsel/SinglePersonHintBar.svelte @@ -19,7 +19,7 @@ let toYear = $derived(toDate ? toDate.substring(0, 4) : '');
Date: Mon, 6 Apr 2026 20:29:31 +0200 Subject: [PATCH 09/21] fix(i18n): replace hardcoded "oder" with conv_hero_divider message key 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 --- frontend/messages/de.json | 1 + frontend/messages/en.json | 1 + frontend/messages/es.json | 1 + .../routes/briefwechsel/CorrespondenzHero.svelte | 16 +++++++++------- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 4c933f16..ea422b13 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -160,6 +160,7 @@ "conv_suggestions_all_label": "Alle Korrespondenten von {name}", "conv_letters_count": "{count} Briefe", "conv_empty_search_placeholder": "Person suchen…", + "conv_hero_divider": "oder", "conv_empty_recent_label": "Zuletzt geöffnet", "conv_asym_sent": "{count} von {name} →", "conv_asym_received": "{count} von {name} ←", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 70335627..c58d2061 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -160,6 +160,7 @@ "conv_suggestions_all_label": "All correspondents of {name}", "conv_letters_count": "{count} letters", "conv_empty_search_placeholder": "Search person…", + "conv_hero_divider": "or", "conv_empty_recent_label": "Recently opened", "conv_asym_sent": "{count} from {name} →", "conv_asym_received": "{count} from {name} ←", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index c33c6ee2..10c0b414 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -160,6 +160,7 @@ "conv_suggestions_all_label": "Todos los corresponsales de {name}", "conv_letters_count": "{count} cartas", "conv_empty_search_placeholder": "Buscar persona…", + "conv_hero_divider": "o", "conv_empty_recent_label": "Recientemente abiertos", "conv_asym_sent": "{count} de {name} →", "conv_asym_received": "{count} de {name} ←", diff --git a/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte b/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte index 924d37a2..e059370d 100644 --- a/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte +++ b/frontend/src/routes/briefwechsel/CorrespondenzHero.svelte @@ -32,18 +32,18 @@ function handlePersonChange(id: string) {
- - - {m.conv_hero_crosslink()} - -

{m.conv_empty_heading()}

+ + + {m.conv_hero_crosslink()} + +
0}
- oder + {m.conv_hero_divider()}
-- 2.49.1 From 93be64878eacb85f2badb1faed26d84cb01db4c2 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 20:30:30 +0200 Subject: [PATCH 10/21] fix(ui): guard selectPerson against empty id 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 --- frontend/src/routes/briefwechsel/+page.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/routes/briefwechsel/+page.svelte b/frontend/src/routes/briefwechsel/+page.svelte index 53f095f7..d4383e8f 100644 --- a/frontend/src/routes/briefwechsel/+page.svelte +++ b/frontend/src/routes/briefwechsel/+page.svelte @@ -86,6 +86,7 @@ function swapPersons() { } function selectPerson(id: string) { + if (!id) return; senderId = id; receiverId = ''; applyFilters(); -- 2.49.1 From c4715f1637bc0172459f21000acf4664661d69ce Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 22:20:32 +0200 Subject: [PATCH 11/21] fix(ui): unify Briefwechsel search bar with document search card style 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 --- frontend/src/routes/briefwechsel/+page.svelte | 58 ++++++++++--------- .../CorrespondenzFilterControls.svelte | 24 ++++---- .../CorrespondenzPersonBar.svelte | 15 ++--- .../briefwechsel/SinglePersonHintBar.svelte | 2 +- 4 files changed, 49 insertions(+), 50 deletions(-) diff --git a/frontend/src/routes/briefwechsel/+page.svelte b/frontend/src/routes/briefwechsel/+page.svelte index d4383e8f..050d6567 100644 --- a/frontend/src/routes/briefwechsel/+page.svelte +++ b/frontend/src/routes/briefwechsel/+page.svelte @@ -99,37 +99,39 @@ function selectPerson(id: string) {
{:else} - +
- - - - - {#if isSinglePerson} - + - {/if} -
+ + + {#if isSinglePerson} + + {/if} +
+ +
{#if data.documents.length === 0}
-