diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index 8d803ee7..c6eccdf1 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -1035,6 +1035,7 @@
"nav_geschichten": "Geschichten",
"nav_zeitstrahl": "Zeitstrahl",
"timeline_heading": "Zeitstrahl",
+ "timeline_add_event": "Ereignis hinzufügen",
"timeline_empty_state": "Noch keine Ereignisse.",
"timeline_undated_section": "Ohne Datum",
"timeline_unknown_person": "Unbekannt",
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 8cedb0ea..0bfeb8e1 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -1035,6 +1035,7 @@
"nav_geschichten": "Stories",
"nav_zeitstrahl": "Timeline",
"timeline_heading": "Timeline",
+ "timeline_add_event": "Add event",
"timeline_empty_state": "No events yet.",
"timeline_undated_section": "Without Date",
"timeline_unknown_person": "Unknown",
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index f03a1cbd..37ee161e 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -1035,6 +1035,7 @@
"nav_geschichten": "Historias",
"nav_zeitstrahl": "Línea de tiempo",
"timeline_heading": "Línea de tiempo",
+ "timeline_add_event": "Añadir evento",
"timeline_empty_state": "Aún no hay eventos.",
"timeline_undated_section": "Sin Fecha",
"timeline_unknown_person": "Desconocido",
diff --git a/frontend/src/routes/zeitstrahl/+page.svelte b/frontend/src/routes/zeitstrahl/+page.svelte
index 7126a5ba..759b3340 100644
--- a/frontend/src/routes/zeitstrahl/+page.svelte
+++ b/frontend/src/routes/zeitstrahl/+page.svelte
@@ -74,7 +74,20 @@ const metaLine = $derived.by(() => {
border is intentionally omitted (the page is already bg-canvas), per the
review of REQ-001 — the sheet reads through its padding, not a frame line. -->
-
{m.timeline_heading()}
+
+
{#if hasContent}
{metaLine}
{
{:else}
-
+
{/if}
diff --git a/frontend/src/routes/zeitstrahl/page.svelte.spec.ts b/frontend/src/routes/zeitstrahl/page.svelte.spec.ts
index 3266c171..3f8a8d7b 100644
--- a/frontend/src/routes/zeitstrahl/page.svelte.spec.ts
+++ b/frontend/src/routes/zeitstrahl/page.svelte.spec.ts
@@ -213,3 +213,55 @@ describe('/zeitstrahl layer filter (#780)', () => {
await expect.element(meta).toHaveTextContent(m.timeline_events_count({ count: 2 }));
});
});
+
+describe('/zeitstrahl curator affordances (#842)', () => {
+ const curated = (eventId: string) =>
+ makeEntry({
+ kind: 'EVENT',
+ type: 'PERSONAL',
+ derived: false,
+ eventId,
+ title: 'Auswanderung',
+ senderName: '',
+ receiverName: '',
+ documentId: undefined
+ });
+
+ const withWrite = (timeline: ReturnType) => ({
+ ...pageData(timeline),
+ canWrite: true
+ });
+
+ it('renders the add-event CTA in a wrapping header when the viewer can write (REQ-001)', () => {
+ render(Page, { data: withWrite(makeTimelineDTO({ years: [makeYear(1909, [makeEntry()])] })) });
+ const add = document.querySelector(
+ '[data-testid="timeline-add-event"]'
+ ) as HTMLAnchorElement | null;
+ expect(add).not.toBeNull();
+ expect(add?.getAttribute('href')).toBe('/zeitstrahl/events/new');
+ // The header wraps so the CTA drops below the heading at narrow widths (≤360px)
+ // rather than overflowing — REQ-001.
+ expect(add?.closest('header')?.classList.contains('flex-wrap')).toBe(true);
+ });
+
+ it('renders no add-event CTA when the viewer cannot write (REQ-002)', () => {
+ render(Page, {
+ data: pageData(makeTimelineDTO({ years: [makeYear(1909, [makeEntry()])] }))
+ });
+ expect(document.querySelector('[data-testid="timeline-add-event"]')).toBeNull();
+ });
+
+ it('threads canWrite to the timeline so a curator sees an event edit link (REQ-001/009)', () => {
+ render(Page, {
+ data: withWrite(makeTimelineDTO({ years: [makeYear(1924, [curated('p9')])] }))
+ });
+ expect(document.querySelector('a[href="/zeitstrahl/events/p9/edit"]')).not.toBeNull();
+ });
+
+ it('shows no event edit link to a reader (REQ-007)', () => {
+ render(Page, {
+ data: pageData(makeTimelineDTO({ years: [makeYear(1924, [curated('p9')])] }))
+ });
+ expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
+ });
+});