feat(ui): two render states — hero vs results — with unified padding

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>
This commit is contained in:
Marcel
2026-04-06 19:43:54 +02:00
parent e9acd44acb
commit f39d9e6f30
4 changed files with 126 additions and 87 deletions

View File

@@ -53,17 +53,29 @@ const withDocs = {
documents: [makeDoc()]
};
// ─── Empty state (no senderId) ────────────────────────────────────────────────
// ─── Hero state (no senderId) ────────────────────────────────────────────────
describe('Korrespondenz page empty state', () => {
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<HTMLElement>('[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');