feat(timeline): frame /zeitstrahl in a canvas with a meta line (REQ-001/002)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as m from '$lib/paraglide/messages.js';
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
import TimelineView from '$lib/timeline/TimelineView.svelte';
|
import TimelineView from '$lib/timeline/TimelineView.svelte';
|
||||||
|
import { timelineMeta } from '$lib/timeline/timelineMeta';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const meta = $derived(timelineMeta(data.timeline));
|
||||||
|
const hasContent = $derived(data.timeline.years.length > 0 || data.timeline.undated.length > 0);
|
||||||
|
|
||||||
|
// Compose the sub-line from segments joined by " · " so the range drops out
|
||||||
|
// cleanly when there are no year bands; the whole line is absent when the
|
||||||
|
// timeline is empty (REQ-002). Counts come from the route alone, never from
|
||||||
|
// TimelineView.
|
||||||
|
const metaLine = $derived.by(() => {
|
||||||
|
const segments: string[] = [];
|
||||||
|
if (meta.firstYear !== null && meta.lastYear !== null) {
|
||||||
|
segments.push(`${meta.firstYear}–${meta.lastYear}`);
|
||||||
|
}
|
||||||
|
segments.push(m.timeline_letters_count({ count: meta.letterCount }));
|
||||||
|
segments.push(m.timeline_events_count({ count: meta.eventCount }));
|
||||||
|
segments.push(m.timeline_grouping_date());
|
||||||
|
return segments.join(' · ');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -11,6 +30,13 @@ let { data }: { data: PageData } = $props();
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="mx-auto max-w-5xl px-4 py-8">
|
<div class="mx-auto max-w-5xl px-4 py-8">
|
||||||
<h1 class="mb-8 font-serif text-2xl font-bold text-brand-navy">{m.timeline_heading()}</h1>
|
<!-- The .tl-canvas sheet: a framed surface the timeline reads as a finished
|
||||||
<TimelineView timeline={data.timeline} />
|
life-thread rather than bare page chrome (REQ-001). -->
|
||||||
|
<div data-testid="timeline-canvas" class="rounded-[10px] border border-line bg-canvas p-6">
|
||||||
|
<h1 class="font-serif text-2xl font-bold text-brand-navy">{m.timeline_heading()}</h1>
|
||||||
|
{#if hasContent}
|
||||||
|
<p data-testid="timeline-meta" class="mt-1 mb-6 font-sans text-xs text-ink-3">{metaLine}</p>
|
||||||
|
{/if}
|
||||||
|
<TimelineView timeline={data.timeline} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
66
frontend/src/routes/zeitstrahl/page.svelte.spec.ts
Normal file
66
frontend/src/routes/zeitstrahl/page.svelte.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user