feat(timeline): add WorldBand for HISTORICAL context bands
Full-width muted band; RANGE renders a span pill (1914–1918) with a Zeitraum aria-label (REQ-009); a RANGE with no end degrades to the start year, no pill, no crash (REQ-010). World glyph is a redundant non-color cue with sr-only label (REQ-018); text uses text-ink-2 to hold AA in both themes (REQ-019). Refs #779 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
43
frontend/src/lib/timeline/WorldBand.svelte
Normal file
43
frontend/src/lib/timeline/WorldBand.svelte
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
|
import { getAccentConfig } from './eventCardConfig';
|
||||||
|
import { timelineDateLabel } from './dateLabel';
|
||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-width muted band for a HISTORICAL event, laid across the axis as context
|
||||||
|
* (REQ-009). A RANGE carries a visible span pill ("1914–1918") with a Zeitraum
|
||||||
|
* aria-label; a RANGE with no end degrades to the start year, no pill (REQ-010).
|
||||||
|
* The glyph is a redundant non-color cue with an sr-only label (REQ-018); text
|
||||||
|
* uses text-ink-2 to stay AA in both themes (REQ-019).
|
||||||
|
*/
|
||||||
|
let { entry }: { entry: TimelineEntryDTO } = $props();
|
||||||
|
|
||||||
|
const config = $derived(getAccentConfig(entry));
|
||||||
|
const dateLabel = $derived(timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd));
|
||||||
|
const fromYear = $derived(entry.eventDate ? entry.eventDate.slice(0, 4) : null);
|
||||||
|
const toYear = $derived(entry.eventDateEnd ? entry.eventDateEnd.slice(0, 4) : null);
|
||||||
|
const showSpan = $derived(entry.precision === 'RANGE' && fromYear != null && toYear != null);
|
||||||
|
const dateText = $derived(showSpan ? null : entry.precision === 'RANGE' ? fromYear : dateLabel);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="my-3 border-y border-line bg-canvas px-4 py-2 text-center">
|
||||||
|
<span class="font-serif text-sm text-ink-2 italic">
|
||||||
|
<span aria-hidden="true" style="color: var(--c-tag-slate)">{config.glyph}</span>
|
||||||
|
<span class="sr-only">{config.label}</span>
|
||||||
|
{entry.title}
|
||||||
|
</span>
|
||||||
|
{#if showSpan && fromYear && toYear}
|
||||||
|
<span
|
||||||
|
data-testid="world-range"
|
||||||
|
class="ml-2 inline-block rounded-full border border-line px-2 py-0.5 font-sans text-xs text-ink-2"
|
||||||
|
aria-label={m.timeline_range_aria({ from: fromYear, to: toYear })}
|
||||||
|
>
|
||||||
|
{fromYear}–{toYear}
|
||||||
|
</span>
|
||||||
|
{:else if dateText}
|
||||||
|
<span class="ml-2 font-sans text-xs text-ink-3">{dateText}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
48
frontend/src/lib/timeline/WorldBand.svelte.spec.ts
Normal file
48
frontend/src/lib/timeline/WorldBand.svelte.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
|
import WorldBand from './WorldBand.svelte';
|
||||||
|
import { makeEntry } from './test-factories';
|
||||||
|
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
function historical(overrides = {}) {
|
||||||
|
return makeEntry({
|
||||||
|
kind: 'EVENT',
|
||||||
|
derived: false,
|
||||||
|
type: 'HISTORICAL',
|
||||||
|
title: 'Erster Weltkrieg',
|
||||||
|
senderName: '',
|
||||||
|
receiverName: '',
|
||||||
|
precision: 'RANGE',
|
||||||
|
eventDate: '1914-01-01',
|
||||||
|
eventDateEnd: '1918-12-31',
|
||||||
|
documentId: undefined,
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WorldBand', () => {
|
||||||
|
it('renders the historical title with the world glyph + "Weltgeschehen" cue (REQ-018)', () => {
|
||||||
|
render(WorldBand, { entry: historical() });
|
||||||
|
expect(document.body.textContent).toContain('Erster Weltkrieg');
|
||||||
|
const hidden = document.querySelector('[aria-hidden="true"]');
|
||||||
|
expect(hidden?.textContent).toBe('◍');
|
||||||
|
const srOnly = document.querySelector('.sr-only');
|
||||||
|
expect(srOnly?.textContent).toBe('Weltgeschehen');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a RANGE span pill 1914–1918 with a Zeitraum aria-label (REQ-009)', () => {
|
||||||
|
render(WorldBand, { entry: historical() });
|
||||||
|
const pill = document.querySelector('[data-testid="world-range"]');
|
||||||
|
expect(pill).not.toBeNull();
|
||||||
|
expect(pill?.textContent).toContain('1914–1918');
|
||||||
|
expect(pill?.getAttribute('aria-label')).toBe('Zeitraum: 1914 bis 1918');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('degrades a RANGE with no end to the start year, no span pill, no crash (REQ-010)', () => {
|
||||||
|
render(WorldBand, { entry: historical({ eventDateEnd: undefined }) });
|
||||||
|
expect(document.querySelector('[data-testid="world-range"]')).toBeNull();
|
||||||
|
expect(document.body.textContent).toContain('Erster Weltkrieg');
|
||||||
|
expect(document.body.textContent).toContain('1914');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user