diff --git a/frontend/src/lib/activity/ChronikEmptyState.svelte b/frontend/src/lib/activity/ChronikEmptyState.svelte
deleted file mode 100644
index ecf2b865..00000000
--- a/frontend/src/lib/activity/ChronikEmptyState.svelte
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
- {#if variant === 'first-run'}
-
- {:else if variant === 'filter-empty'}
-
- {:else}
-
- {/if}
-
-
- {title}
-
- {#if body}
-
- {body}
-
- {/if}
-
diff --git a/frontend/src/lib/activity/ChronikEmptyState.svelte.spec.ts b/frontend/src/lib/activity/ChronikEmptyState.svelte.spec.ts
deleted file mode 100644
index 4e3446a4..00000000
--- a/frontend/src/lib/activity/ChronikEmptyState.svelte.spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { describe, it, expect, afterEach } from 'vitest';
-import { cleanup, render } from 'vitest-browser-svelte';
-import { page } from 'vitest/browser';
-
-import ChronikEmptyState from './ChronikEmptyState.svelte';
-
-afterEach(cleanup);
-
-describe('ChronikEmptyState', () => {
- it('renders first-run variant title', async () => {
- render(ChronikEmptyState, { variant: 'first-run' });
- await expect.element(page.getByText('Noch nichts geschehen')).toBeInTheDocument();
- });
-
- it('renders filter-empty variant title', async () => {
- render(ChronikEmptyState, { variant: 'filter-empty' });
- await expect.element(page.getByText('Nichts in dieser Ansicht')).toBeInTheDocument();
- });
-
- it('renders inbox-zero variant title', async () => {
- render(ChronikEmptyState, { variant: 'inbox-zero' });
- await expect.element(page.getByText('Keine neuen Erwähnungen')).toBeInTheDocument();
- });
-
- it('applies the expected data-variant attribute', async () => {
- render(ChronikEmptyState, { variant: 'first-run' });
- const wrapper = document.querySelector('[data-testid="chronik-empty-state"]');
- expect(wrapper?.getAttribute('data-variant')).toBe('first-run');
- });
-});
diff --git a/frontend/src/lib/activity/ChronikEmptyState.svelte.test.ts b/frontend/src/lib/activity/ChronikEmptyState.svelte.test.ts
deleted file mode 100644
index 920a76db..00000000
--- a/frontend/src/lib/activity/ChronikEmptyState.svelte.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { describe, it, expect, afterEach } from 'vitest';
-import { cleanup, render } from 'vitest-browser-svelte';
-import { page } from 'vitest/browser';
-import ChronikEmptyState from './ChronikEmptyState.svelte';
-
-afterEach(cleanup);
-
-describe('ChronikEmptyState', () => {
- it('renders the first-run title and body and the clock icon', async () => {
- render(ChronikEmptyState, { props: { variant: 'first-run' as const } });
-
- await expect.element(page.getByText('Noch nichts geschehen')).toBeVisible();
- await expect.element(page.getByText(/sobald jemand aus der familie/i)).toBeVisible();
-
- const wrapper = document.querySelector('[data-testid="chronik-empty-state"]');
- expect(wrapper?.getAttribute('data-variant')).toBe('first-run');
- });
-
- it('renders the filter-empty title and body', async () => {
- render(ChronikEmptyState, { props: { variant: 'filter-empty' as const } });
-
- await expect.element(page.getByText('Nichts in dieser Ansicht')).toBeVisible();
- await expect.element(page.getByText('In diesem Filter gibt es keine Einträge.')).toBeVisible();
-
- const wrapper = document.querySelector('[data-testid="chronik-empty-state"]');
- expect(wrapper?.getAttribute('data-variant')).toBe('filter-empty');
- });
-
- it('renders the inbox-zero title and no body paragraph', async () => {
- render(ChronikEmptyState, { props: { variant: 'inbox-zero' as const } });
-
- await expect.element(page.getByText('Keine neuen Erwähnungen')).toBeVisible();
-
- // Only one (the title) since body is empty
- const wrapper = document.querySelector('[data-testid="chronik-empty-state"]');
- const paragraphs = wrapper?.querySelectorAll('p');
- expect(paragraphs?.length).toBe(1);
- expect(wrapper?.getAttribute('data-variant')).toBe('inbox-zero');
- });
-
- it('uses the accent color icon for inbox-zero (vs ink-3 for others)', async () => {
- render(ChronikEmptyState, { props: { variant: 'inbox-zero' as const } });
-
- const wrapper = document.querySelector('[data-testid="chronik-empty-state"]');
- const svg = wrapper?.querySelector('svg');
- expect(svg?.getAttribute('class')).toContain('text-accent');
- });
-
- it('uses the ink-3 color icon for first-run', async () => {
- render(ChronikEmptyState, { props: { variant: 'first-run' as const } });
-
- const wrapper = document.querySelector('[data-testid="chronik-empty-state"]');
- const svg = wrapper?.querySelector('svg');
- expect(svg?.getAttribute('class')).toContain('text-ink-3');
- });
-});
diff --git a/frontend/src/routes/aktivitaeten/+page.svelte b/frontend/src/routes/aktivitaeten/+page.svelte
index 06a0ff6b..a7665a43 100644
--- a/frontend/src/routes/aktivitaeten/+page.svelte
+++ b/frontend/src/routes/aktivitaeten/+page.svelte
@@ -10,7 +10,7 @@ import {
import ChronikFuerDichBox from '$lib/activity/ChronikFuerDichBox.svelte';
import ChronikFilterPills from '$lib/activity/ChronikFilterPills.svelte';
import ChronikTimeline from '$lib/activity/ChronikTimeline.svelte';
-import ChronikEmptyState from '$lib/activity/ChronikEmptyState.svelte';
+import EmptyState from '$lib/shared/primitives/EmptyState.svelte';
import ChronikErrorCard from '$lib/activity/ChronikErrorCard.svelte';
import type { components } from '$lib/generated/api';
import type { FilterValue } from './+page.server';
@@ -88,6 +88,22 @@ const emptyVariant = $derived<'first-run' | 'filter-empty' | 'inbox-zero'>(
data.activityFeed.length === 0 ? 'first-run' : 'filter-empty'
);
+const emptyHeading = $derived(
+ emptyVariant === 'first-run'
+ ? m.chronik_empty_first_run_title()
+ : emptyVariant === 'filter-empty'
+ ? m.chronik_empty_filter_title()
+ : m.chronik_inbox_zero_title()
+);
+
+const emptySubline = $derived(
+ emptyVariant === 'first-run'
+ ? m.chronik_empty_first_run_body()
+ : emptyVariant === 'filter-empty'
+ ? m.chronik_empty_filter_body()
+ : ''
+);
+
function retry() {
location.reload();
}
@@ -118,7 +134,7 @@ function retry() {