From e4da28d795180e10f0c38e8767c8ae4d2fb35743 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Jun 2026 11:00:54 +0200 Subject: [PATCH] feat(timeline): frame /zeitstrahl in a canvas with a meta line (REQ-001/002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The timeline now sits inside a bordered, rounded bg-canvas sheet. Below the heading a sub-line composes the year range, the letter and event counts (from timelineMeta), and the static "Gruppierung: Datum" — joined by " · " so the range drops out when there are no year bands and the whole line is absent for an empty timeline. Semantic tokens only; AA-legible text-xs. Refs #833 Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/zeitstrahl/+page.svelte | 30 ++++++++- .../src/routes/zeitstrahl/page.svelte.spec.ts | 66 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 frontend/src/routes/zeitstrahl/page.svelte.spec.ts diff --git a/frontend/src/routes/zeitstrahl/+page.svelte b/frontend/src/routes/zeitstrahl/+page.svelte index b00a8160..c84147a1 100644 --- a/frontend/src/routes/zeitstrahl/+page.svelte +++ b/frontend/src/routes/zeitstrahl/+page.svelte @@ -1,9 +1,28 @@ @@ -11,6 +30,13 @@ let { data }: { data: PageData } = $props();
-

{m.timeline_heading()}

- + +
+

{m.timeline_heading()}

+ {#if hasContent} +

{metaLine}

+ {/if} + +
diff --git a/frontend/src/routes/zeitstrahl/page.svelte.spec.ts b/frontend/src/routes/zeitstrahl/page.svelte.spec.ts new file mode 100644 index 00000000..50782a41 --- /dev/null +++ b/frontend/src/routes/zeitstrahl/page.svelte.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import * as m from '$lib/paraglide/messages.js'; +import Page from './+page.svelte'; +import { makeEntry, makeYear, makeTimelineDTO } from '$lib/timeline/test-factories'; + +afterEach(() => cleanup()); + +const event = (title: string) => + makeEntry({ + kind: 'EVENT', + derived: true, + derivedType: 'BIRTH', + title, + senderName: '', + receiverName: '', + documentId: undefined + }); + +describe('/zeitstrahl page', () => { + it('wraps the timeline in a bordered, rounded canvas frame (REQ-001)', () => { + render(Page, { + data: { timeline: makeTimelineDTO({ years: [makeYear(1909, [makeEntry()])] }) } + }); + const canvas = document.querySelector('[data-testid="timeline-canvas"]'); + expect(canvas).not.toBeNull(); + expect(canvas?.classList.contains('bg-canvas')).toBe(true); + expect(canvas?.classList.contains('border')).toBe(true); + // the timeline renders inside the frame + expect(canvas?.querySelector('ol')).not.toBeNull(); + }); + + it('renders the meta sub-line with range, counts, and grouping (REQ-002)', () => { + const dto = makeTimelineDTO({ + years: [ + makeYear(1909, [ + makeEntry({ documentId: 'a' }), + makeEntry({ documentId: 'b' }), + event('Geburt') + ]), + makeYear(1924, [makeEntry({ documentId: 'c' }), event('Tod')]) + ] + }); + render(Page, { data: { timeline: dto } }); + const sub = document.querySelector('[data-testid="timeline-meta"]'); + expect(sub?.textContent).toContain('1909–1924'); + expect(sub?.textContent).toContain(m.timeline_letters_count({ count: 3 })); + expect(sub?.textContent).toContain(m.timeline_events_count({ count: 2 })); + expect(sub?.textContent).toContain(m.timeline_grouping_date()); + }); + + it('omits the range segment when there are no year bands (REQ-002)', () => { + render(Page, { + data: { timeline: makeTimelineDTO({ undated: [makeEntry({ documentId: 'u1' })] }) } + }); + const sub = document.querySelector('[data-testid="timeline-meta"]'); + expect(sub).not.toBeNull(); + expect(sub?.textContent).not.toContain('–'); + expect(sub?.textContent).toContain(m.timeline_letters_count({ count: 1 })); + }); + + it('omits the entire sub-line for an empty timeline (REQ-002)', () => { + render(Page, { data: { timeline: makeTimelineDTO() } }); + expect(document.querySelector('[data-testid="timeline-meta"]')).toBeNull(); + }); +});