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