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 @@
+
+
+
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/);
+ });
+});