From 495210052fa2b0d1778161f83734e0d1d5f4497c Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 16:48:33 +0200 Subject: [PATCH 01/15] style: add mint-soft, line-soft, link-quiet, ink-4 tokens (#483) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/layout.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/src/routes/layout.css b/frontend/src/routes/layout.css index 85c41e67..9137a68f 100644 --- a/frontend/src/routes/layout.css +++ b/frontend/src/routes/layout.css @@ -80,6 +80,12 @@ /* Static brand tokens (not themed) */ --color-brand-navy: var(--palette-navy); --color-brand-mint: var(--palette-mint); + + /* Reader dashboard tokens */ + --color-mint-soft: var(--c-mint-soft); /* Mint-Pill background */ + --color-line-soft: var(--c-line-soft); /* Stat-column dividers, row borders */ + --color-link-quiet: var(--c-link-quiet); /* "Alle …" secondary links */ + --color-ink-4: var(--c-ink-4); /* Card-head labels (light = ink-3, dark = #7080a8) */ } /* ─── 4. Light mode (default) ─────────────────────────────────────────────── */ @@ -160,6 +166,12 @@ with axe (tracked in #480) before tweaking the palette. */ --timeline-bar-idle: rgba(161, 220, 216, 0.35); --timeline-bar-outside: var(--c-line); + + /* Reader dashboard tokens — light mode */ + --c-mint-soft: #d4f0ee; /* Mint-Pill BG — decorative fill (1.1:1 on white, WCAG carve-out) */ + --c-line-soft: #f0ede6; /* Stat-column dividers, card row separator */ + --c-link-quiet: #4a6e8a; /* Quiet link — 4.65:1 on #f0efe9 canvas WCAG AA ✓ */ + --c-ink-4: #6b7280; /* Card-head labels (= ink-3 in light) */ } /* ─── 5. Dark mode ─────────────────────────────────────────────────────────── */ @@ -236,6 +248,12 @@ clears WCAG 1.4.11 non-text contrast for large UI elements. */ --timeline-bar-idle: #3a6e8c; --timeline-bar-outside: #1a2735; + + /* Reader dashboard tokens — dark mode */ + --c-mint-soft: rgba(161, 218, 216, 0.14); + --c-line-soft: rgba(255, 255, 255, 0.06); + --c-link-quiet: var(--palette-mint); + --c-ink-4: #7080a8; /* 5.0:1 on #011526 WCAG AA ✓ */ } } @@ -308,6 +326,12 @@ clears WCAG 1.4.11 non-text contrast for large UI elements. */ --timeline-bar-idle: #3a6e8c; --timeline-bar-outside: #1a2735; + + /* Reader dashboard tokens — dark mode */ + --c-mint-soft: rgba(161, 218, 216, 0.14); + --c-line-soft: rgba(255, 255, 255, 0.06); + --c-link-quiet: var(--palette-mint); + --c-ink-4: #7080a8; /* 5.0:1 on #011526 WCAG AA ✓ */ } /* ─── 6. Icon inversion — De Gruyter icons are black SVGs loaded as ──── */ -- 2.49.1 From 2f48dfabd1802f5d2e025c24651e5607e08d4db7 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 16:50:36 +0200 Subject: [PATCH 02/15] i18n: add reader header-bar keys, remove dashboard_badge_updated (#483) Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 10 +++++++++- frontend/messages/en.json | 10 +++++++++- frontend/messages/es.json | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 6e07a47f..e8af8a25 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -459,9 +459,17 @@ "dashboard_reader_recent_docs_heading": "Zuletzt aktualisiert", "dashboard_reader_recent_stories_heading": "Neue Geschichten", "dashboard_badge_new": "Neu", - "dashboard_badge_updated": "Aktualisiert", "dashboard_reader_all_stories": "Alle Geschichten →", "dashboard_reader_doc_count_suffix": "Dok.", + "dashboard_all_documents": "Alle Dokumente", + "dashboard_greeting_time_morning": "Morgen", + "dashboard_greeting_time_afternoon": "Mittag", + "dashboard_greeting_time_evening": "Abend", + "dashboard_welcome": "Herzlich willkommen, {name}.", + "dashboard_reader_stats_documents_short": "Dok.", + "dashboard_reader_stats_persons_short": "Pers.", + "dashboard_reader_stats_stories_short": "Gesch.", + "dashboard_reader_draft_meta": "Entwurf · zuletzt bearbeitet {relative}", "dashboard_resume_label": "Zuletzt geöffnet:", "dashboard_resume_fallback": "Unbekanntes Dokument", "doc_status_placeholder": "Platzhalter", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 1f120c98..e448d26c 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -459,9 +459,17 @@ "dashboard_reader_recent_docs_heading": "Recently Updated", "dashboard_reader_recent_stories_heading": "New Stories", "dashboard_badge_new": "New", - "dashboard_badge_updated": "Updated", "dashboard_reader_all_stories": "All Stories →", "dashboard_reader_doc_count_suffix": "docs.", + "dashboard_all_documents": "All Documents", + "dashboard_greeting_time_morning": "Morning", + "dashboard_greeting_time_afternoon": "Afternoon", + "dashboard_greeting_time_evening": "Evening", + "dashboard_welcome": "Welcome, {name}.", + "dashboard_reader_stats_documents_short": "Docs.", + "dashboard_reader_stats_persons_short": "Pers.", + "dashboard_reader_stats_stories_short": "Stor.", + "dashboard_reader_draft_meta": "Draft · last edited {relative}", "dashboard_resume_label": "Last opened:", "dashboard_resume_fallback": "Unknown document", "doc_status_placeholder": "Placeholder", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index e24244bb..f8f576b8 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -459,9 +459,17 @@ "dashboard_reader_recent_docs_heading": "Actualizados recientemente", "dashboard_reader_recent_stories_heading": "Nuevas historias", "dashboard_badge_new": "Nuevo", - "dashboard_badge_updated": "Actualizado", "dashboard_reader_all_stories": "Todas las historias →", "dashboard_reader_doc_count_suffix": "docs.", + "dashboard_all_documents": "Todos los documentos", + "dashboard_greeting_time_morning": "Mañana", + "dashboard_greeting_time_afternoon": "Tarde", + "dashboard_greeting_time_evening": "Noche", + "dashboard_welcome": "Bienvenido, {name}.", + "dashboard_reader_stats_documents_short": "Docs.", + "dashboard_reader_stats_persons_short": "Pers.", + "dashboard_reader_stats_stories_short": "Hist.", + "dashboard_reader_draft_meta": "Borrador · editado hace {relative}", "dashboard_resume_label": "Último abierto:", "dashboard_resume_fallback": "Documento desconocido", "doc_status_placeholder": "Marcador", -- 2.49.1 From b5f9fcfdfd90c14f6a6d6e397ed567d37ec4d58b Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 16:54:02 +0200 Subject: [PATCH 03/15] feat(dashboard): add ReaderHeaderBar with greeting + stat columns (TDD, #483) Co-Authored-By: Claude Sonnet 4.6 --- .../shared/dashboard/ReaderHeaderBar.svelte | 87 +++++++++++++++ .../dashboard/ReaderHeaderBar.svelte.spec.ts | 100 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte create mode 100644 frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts diff --git a/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte new file mode 100644 index 00000000..e3574721 --- /dev/null +++ b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte @@ -0,0 +1,87 @@ + + +
+ +
+ + {timeLabel} + + + {m.dashboard_welcome({ name })} + +
+ + + + + + +
diff --git a/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts new file mode 100644 index 00000000..c9d81c9b --- /dev/null +++ b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +import ReaderHeaderBar from './ReaderHeaderBar.svelte'; + +afterEach(() => { + cleanup(); +}); + +describe('ReaderHeaderBar', () => { + it('renders a link to /documents with document count', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /42/ }); + await expect.element(link).toHaveAttribute('href', '/documents'); + }); + + it('renders a link to /persons with person count', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /7/ }); + await expect.element(link).toHaveAttribute('href', '/persons'); + }); + + it('renders a link to /geschichten with story count', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /3/ }); + await expect.element(link).toHaveAttribute('href', '/geschichten'); + }); + + it('documents stat link has min-h-[44px] for touch target', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /42/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('persons stat link has min-h-[44px] for touch target', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /7/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('stories stat link has min-h-[44px] for touch target', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /3/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('shows "—" when counts are null', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: null, persons: null, stories: null }); + const wrapper = page.getByRole('banner'); + const text = ((await wrapper.element()) as HTMLElement).textContent; + expect(text?.match(/—/g)?.length).toBeGreaterThanOrEqual(3); + }); + + it('time label uses text-brand-navy class for morning hour', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 8 }); + const timeLabel = page.getByText(/Morgen/i); + await expect.element(timeLabel).toBeInTheDocument(); + const cls = ((await timeLabel.element()) as HTMLElement).className; + expect(cls).toMatch(/text-brand-navy/); + }); + + it('shows afternoon label for hour 14', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 14 }); + const timeLabel = page.getByText(/Mittag/i); + await expect.element(timeLabel).toBeInTheDocument(); + }); + + it('shows evening label for hour 20', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 20 }); + const timeLabel = page.getByText(/Abend/i); + await expect.element(timeLabel).toBeInTheDocument(); + }); + + it('welcome line contains the user name', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 8 }); + const welcome = page.getByText(/Anna/); + await expect.element(welcome).toBeInTheDocument(); + }); + + it('wrapper has dark mode classes', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1 }); + const wrapper = page.getByRole('banner'); + const cls = ((await wrapper.element()) as HTMLElement).className; + expect(cls).toMatch(/dark:bg-surface/); + expect(cls).toMatch(/dark:border-white\/8/); + }); + + it('renders a vertical divider with bg-line class', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1 }); + const wrapper = page.getByRole('banner'); + const el = (await wrapper.element()) as HTMLElement; + const divider = el.querySelector('[aria-hidden="true"]'); + expect(divider).not.toBeNull(); + expect(divider!.className).toMatch(/bg-line/); + }); +}); -- 2.49.1 From ae6355d206333928bbecc474c2aa2165eea67f63 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 16:56:52 +0200 Subject: [PATCH 04/15] =?UTF-8?q?refactor(dashboard):=20ReaderPersonChips?= =?UTF-8?q?=20=E2=86=92=20grid=20layout=20with=20mint-pill=20doc=20count?= =?UTF-8?q?=20(TDD,=20#483)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../shared/dashboard/ReaderPersonChips.svelte | 25 ++++---- .../ReaderPersonChips.svelte.spec.ts | 59 ++++++++++++++++--- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte b/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte index f78a9b6a..e91a4310 100644 --- a/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte +++ b/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte @@ -27,37 +27,34 @@ interface Props { const { persons }: Props = $props(); -
-

- {m.dashboard_reader_person_chips_heading()} -

+
{#if persons.length === 0}

{m.dashboard_reader_no_persons()}

{/if} - +
diff --git a/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte.spec.ts index 4aebb735..0ae40474 100644 --- a/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte.spec.ts +++ b/frontend/src/lib/shared/dashboard/ReaderPersonChips.svelte.spec.ts @@ -32,7 +32,7 @@ const person2: PersonSummaryDTO = { }; describe('ReaderPersonChips', () => { - it('renders a chip for each person with correct href', async () => { + it('renders a card for each person with correct href', async () => { render(ReaderPersonChips, { persons: [person1, person2] }); const link1 = page.getByRole('link', { name: /Anna Müller/ }); await expect @@ -44,12 +44,44 @@ describe('ReaderPersonChips', () => { .toHaveAttribute('href', '/persons/aaaaaaaa-0000-0000-0000-000000000002'); }); - it('shows document count in each chip', async () => { + it('person card has min-h-[44px] touch target', async () => { render(ReaderPersonChips, { persons: [person1] }); - const chip = page.getByRole('link', { name: /Anna Müller/ }); - await expect.element(chip).toBeInTheDocument(); - const text = ((await chip.element()) as HTMLElement).textContent; - expect(text).toContain('23'); + const link = page.getByRole('link', { name: /Anna Müller/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('doc count renders as mint pill with bg-mint-soft', async () => { + render(ReaderPersonChips, { persons: [person1] }); + const link = page.getByRole('link', { name: /Anna Müller/ }); + const el = (await link.element()) as HTMLElement; + const pill = el.querySelector('[class*="bg-mint-soft"]'); + expect(pill).not.toBeNull(); + expect(pill!.textContent).toContain('23'); + }); + + it('doc count pill has rounded-full class', async () => { + render(ReaderPersonChips, { persons: [person1] }); + const link = page.getByRole('link', { name: /Anna Müller/ }); + const el = (await link.element()) as HTMLElement; + const pill = el.querySelector('[class*="rounded-full"]'); + expect(pill).not.toBeNull(); + }); + + it('person grid uses grid layout', async () => { + render(ReaderPersonChips, { persons: [person1, person2] }); + const section = page.getByRole('region'); + const el = (await section.element()) as HTMLElement; + const grid = el.querySelector('[class*="grid"]'); + expect(grid).not.toBeNull(); + }); + + it('wrapper is a section with aria-label', async () => { + render(ReaderPersonChips, { persons: [person1] }); + const section = page.getByRole('region'); + await expect.element(section).toBeInTheDocument(); + const label = ((await section.element()) as HTMLElement).getAttribute('aria-label'); + expect(label).toBeTruthy(); }); it('renders an "Alle Personen" link to /persons', async () => { @@ -58,6 +90,13 @@ describe('ReaderPersonChips', () => { await expect.element(allLink).toHaveAttribute('href', '/persons'); }); + it('"Alle Personen" link has text-link-quiet class', async () => { + render(ReaderPersonChips, { persons: [person1] }); + const allLink = page.getByRole('link', { name: /Alle Personen/i }); + const cls = ((await allLink.element()) as HTMLElement).className; + expect(cls).toMatch(/text-link-quiet/); + }); + it('exposes a focus-visible ring on the "Alle Personen" link', async () => { render(ReaderPersonChips, { persons: [person1] }); const allLink = page.getByRole('link', { name: /Alle Personen/i }); @@ -73,7 +112,13 @@ describe('ReaderPersonChips', () => { expect(cls).toMatch(/min-h-\[44px\]/); }); - it('renders empty state without chips when persons array is empty', async () => { + it('does not render h2 heading', async () => { + render(ReaderPersonChips, { persons: [person1] }); + const heading = page.getByRole('heading', { level: 2 }); + await expect.element(heading).not.toBeInTheDocument(); + }); + + it('renders empty state without person cards when persons array is empty', async () => { render(ReaderPersonChips, { persons: [] }); const chips = page.getByRole('link', { name: /Müller|Schmidt/ }); await expect.element(chips).not.toBeInTheDocument(); -- 2.49.1 From e1c78e3fbee8c02fb5d9fd756c629f618d0ccd68 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 17:01:46 +0200 Subject: [PATCH 05/15] refactor(dashboard): ReaderRecentDocs compact card-head, mint-pill badge (TDD, #483) Co-Authored-By: Claude Sonnet 4.6 --- .../shared/dashboard/ReaderRecentDocs.svelte | 88 ++++++++++++------- .../dashboard/ReaderRecentDocs.svelte.spec.ts | 87 ++++++++++++++---- 2 files changed, 124 insertions(+), 51 deletions(-) diff --git a/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte b/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte index d569b57c..8d1b3e64 100644 --- a/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte +++ b/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte @@ -16,49 +16,71 @@ function isNew(doc: Document): boolean { } - - + {:else} + — + {/if} + + + + + {relativeTimeDe(new Date(doc.updatedAt))} -
+ {/each} diff --git a/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte.spec.ts index 8960a5fa..f12f09f1 100644 --- a/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte.spec.ts +++ b/frontend/src/lib/shared/dashboard/ReaderRecentDocs.svelte.spec.ts @@ -37,30 +37,73 @@ describe('ReaderRecentDocs', () => { await expect.element(link).toHaveAttribute('href', '/documents/doc1'); }); - it('shows "Neu" badge when createdAt equals updatedAt', async () => { + it('card has overflow-hidden and flex-col classes (no p-6, no shadow-sm)', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const heading = page.getByRole('heading', { level: 3 }); + const card = (await heading.element())?.closest('div'); + const rootCard = card?.parentElement; + const cls = rootCard?.className ?? ''; + expect(cls).toMatch(/overflow-hidden/); + expect(cls).toMatch(/flex-col/); + expect(cls).not.toMatch(/\bp-6\b/); + expect(cls).not.toMatch(/shadow-sm/); + }); + + it('card-head contains an h3 (not h2)', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const h3 = page.getByRole('heading', { level: 3 }); + await expect.element(h3).toBeInTheDocument(); + const h2 = page.getByRole('heading', { level: 2 }); + await expect.element(h2).not.toBeInTheDocument(); + }); + + it('"Alle Dokumente" link in card-head points to /documents', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const link = page.getByRole('link', { name: /Alle Dokumente/i }); + await expect.element(link).toHaveAttribute('href', '/documents'); + }); + + it('"Alle Dokumente" link has min-h-[44px]', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const link = page.getByRole('link', { name: /Alle Dokumente/i }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('doc-row link has min-h-[44px] touch target', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const link = page.getByRole('link', { name: /Brief an Hans/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('thumb element has correct classes', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const link = page.getByRole('link', { name: /Brief an Hans/ }); + const el = (await link.element()) as HTMLElement; + const thumb = el.querySelector('[class*="w-5"][class*="h-6"]'); + expect(thumb).not.toBeNull(); + expect(thumb!.className).toMatch(/bg-canvas/); + expect(thumb!.className).toMatch(/border-line/); + expect(thumb!.className).toMatch(/rounded-/); + }); + + it('shows "Neu" mint-pill badge when createdAt equals updatedAt', async () => { render(ReaderRecentDocs, { documents: [baseDoc] }); const badge = page.getByText(/^Neu$/i); await expect.element(badge).toBeInTheDocument(); - }); - - it('shows "Aktualisiert" badge when updatedAt differs from createdAt', async () => { - render(ReaderRecentDocs, { documents: [updatedDoc] }); - const badge = page.getByText(/^Aktualisiert$/i); - await expect.element(badge).toBeInTheDocument(); - }); - - it('renders the "Aktualisiert" badge with high-contrast text-ink-1', async () => { - render(ReaderRecentDocs, { documents: [updatedDoc] }); - const badge = page.getByText(/^Aktualisiert$/i); const cls = ((await badge.element()) as HTMLElement).className; - expect(cls).toMatch(/text-ink-1/); - expect(cls).not.toMatch(/text-ink-3(?!\/)/); + expect(cls).toMatch(/bg-mint-soft/); + expect(cls).toMatch(/rounded-full/); + expect(cls).toMatch(/text-brand-navy/); }); - it('does not show "Neu" badge when updatedAt differs from createdAt', async () => { + it('shows no badge when updatedAt differs from createdAt', async () => { render(ReaderRecentDocs, { documents: [updatedDoc] }); const badge = page.getByText(/^Neu$/i); await expect.element(badge).not.toBeInTheDocument(); + const updatedBadge = page.getByText(/^Aktualisiert$/i); + await expect.element(updatedBadge).not.toBeInTheDocument(); }); it('shows "Neu" badge when createdAt and updatedAt represent the same instant in different ISO formats', async () => { @@ -75,7 +118,7 @@ describe('ReaderRecentDocs', () => { await expect.element(badge).toBeInTheDocument(); }); - it('renders sender link when sender is present', async () => { + it('renders sender name text when sender is present', async () => { const docWithSender: Document = { ...baseDoc, sender: { @@ -88,7 +131,15 @@ describe('ReaderRecentDocs', () => { } }; render(ReaderRecentDocs, { documents: [docWithSender] }); - const senderLink = page.getByRole('link', { name: /Anna Müller/ }); - await expect.element(senderLink).toHaveAttribute('href', '/persons/p1'); + const link = page.getByRole('link', { name: /Brief an Hans/ }); + const el = (await link.element()) as HTMLElement; + expect(el.textContent).toContain('Anna Müller'); + }); + + it('shows em-dash when sender is absent', async () => { + render(ReaderRecentDocs, { documents: [baseDoc] }); + const link = page.getByRole('link', { name: /Brief an Hans/ }); + const el = (await link.element()) as HTMLElement; + expect(el.textContent).toContain('—'); }); }); -- 2.49.1 From e598f5a5064a97478365b8f7ec98f50ee8760456 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 17:07:16 +0200 Subject: [PATCH 06/15] refactor(dashboard): ReaderRecentStories card-head link, touch targets (TDD, #483) Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/ReaderRecentStories.svelte | 37 +++++++++------- .../ReaderRecentStories.svelte.spec.ts | 42 ++++++++++++++++++- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte b/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte index ab5b0a30..e903a9da 100644 --- a/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte +++ b/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte @@ -24,33 +24,38 @@ function excerpt(body: string | undefined): string { {#if stories.length > 0} -
-

- {m.dashboard_reader_recent_stories_heading()} -

-