fix(geschichte): raise journey reading text to the accessibility floor
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m49s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 4m7s
CI / fail2ban Regex (pull_request) Successful in 49s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m49s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 4m7s
CI / fail2ban Regex (pull_request) Successful in 49s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s
Intro, letter titles, curator annotations, and interludes rendered at 12-14px — copied from the scaled mockup sizes in LR-2. Narrative text is now 16-18px, meta lines and links 14px; the LR-2 impl-ref table is updated to match. Closes #800 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -636,19 +636,19 @@
|
||||
<tr><td>Metabar</td><td>flex items-center gap-3 pb-4 border-b border-subtle mb-4</td><td>gleich wie Story</td></tr>
|
||||
<tr><td>Bearbeiten/Löschen</td><td>nur BLOG_WRITE; auf Mobile im ··· BottomSheet</td><td>gleich wie Story</td></tr>
|
||||
<tr class="grp"><td colspan="3">Intro-Absatz</td></tr>
|
||||
<tr><td>Intro (body)</td><td>font-serif text-sm text-ink-2 italic leading-relaxed mb-6 pb-4 border-b border-dashed border-subtle</td><td>nur rendern wenn body nicht leer; kein HTML-Rendering — plaintext</td></tr>
|
||||
<tr><td>Intro (body)</td><td>font-serif text-lg text-ink-2 italic leading-relaxed mb-6 pb-4 border-b border-dashed border-subtle</td><td>nur rendern wenn body nicht leer; kein HTML-Rendering — plaintext</td></tr>
|
||||
<tr class="grp"><td colspan="3">Dokument-Item</td></tr>
|
||||
<tr><td>Item-Zeile</td><td>mb-3</td><td>kein flex nötig — Karte ist full-width</td></tr>
|
||||
<tr><td>Dokumentkarte</td><td>bg-surface border border-line rounded-sm p-3</td><td></td></tr>
|
||||
<tr><td>Brieftitel</td><td>font-serif text-sm text-ink leading-snug mb-0.5</td><td>document.title</td></tr>
|
||||
<tr><td>Briefmeta</td><td>text-xs text-ink-3 mb-2</td><td>formatDate(document.documentDate) · "von X an Y"</td></tr>
|
||||
<tr><td>Brief öffnen Link</td><td>inline-flex items-center gap-1 text-xs font-semibold text-ink hover:text-primary</td><td>href="/documents/{item.document.id}"</td></tr>
|
||||
<tr><td>Brieftitel</td><td>font-serif text-base text-ink leading-snug mb-0.5</td><td>document.title</td></tr>
|
||||
<tr><td>Briefmeta</td><td>text-sm text-ink-3 mb-2</td><td>formatDate(document.documentDate) · "von X an Y"</td></tr>
|
||||
<tr><td>Brief öffnen Link</td><td>inline-flex items-center gap-1 text-sm font-semibold text-ink hover:text-primary</td><td>href="/documents/{item.document.id}"</td></tr>
|
||||
<tr class="grp"><td colspan="3">Kuratoren-Annotation</td></tr>
|
||||
<tr><td>Annotation</td><td>mt-3 pl-3 border-l-2 border-brand-mint bg-muted rounded-r-sm py-1.5 pr-2</td><td>nur rendern wenn item.note vorhanden</td></tr>
|
||||
<tr><td>Annotations-Text</td><td>text-xs italic text-ink-2 leading-relaxed</td><td></td></tr>
|
||||
<tr><td>Annotations-Text</td><td>text-base italic text-ink-2 leading-relaxed</td><td></td></tr>
|
||||
<tr class="grp"><td colspan="3">Interlude-Item</td></tr>
|
||||
<tr><td>Interlude-Block</td><td>pl-3 border-l-2 border-orange-400 bg-orange-50 rounded-r-sm py-2 pr-3 my-4</td><td>item.document === null</td></tr>
|
||||
<tr><td>Interlude-Text</td><td>text-xs italic text-ink leading-relaxed</td><td>item.note; plaintext</td></tr>
|
||||
<tr><td>Interlude-Text</td><td>text-base italic text-ink leading-relaxed</td><td>item.note; plaintext</td></tr>
|
||||
<tr class="grp"><td colspan="3">Mobile</td></tr>
|
||||
<tr><td>··· Menü</td><td>ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen</td><td>BLOG_WRITE; gleich wie Story</td></tr>
|
||||
<tr><td>Touch Target (Brief öffnen)</td><td>min-h-[44px] durch padding auf der Karte</td><td>WCAG 2.2 AA</td></tr>
|
||||
|
||||
@@ -14,5 +14,5 @@ let { note }: Props = $props();
|
||||
class="my-4 rounded-r-sm border-l-2 border-orange-400 bg-orange-50 py-2 pr-3 pl-3"
|
||||
>
|
||||
<!-- plaintext — do NOT use {@html} here -->
|
||||
<p class="text-xs leading-relaxed text-ink italic">{note}</p>
|
||||
<p class="text-base leading-relaxed text-ink italic">{note}</p>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,14 @@ describe('JourneyInterlude', () => {
|
||||
expect(el?.getAttribute('aria-label')).toBe(m.journey_interlude_aria_label());
|
||||
});
|
||||
|
||||
it('note text uses readable body size (text-base, #800)', async () => {
|
||||
render(JourneyInterlude, { props: { note: 'Notiz' } });
|
||||
|
||||
const text = document.querySelector('[role="note"] p');
|
||||
expect(text!.className).toContain('text-base');
|
||||
expect(text!.className).not.toContain('text-xs');
|
||||
});
|
||||
|
||||
it('XSS: note is rendered as plaintext — injected payload does not execute', async () => {
|
||||
// Interlude uses Svelte text interpolation ({note}), NOT {@html}.
|
||||
render(JourneyInterlude, {
|
||||
|
||||
@@ -27,14 +27,14 @@ const hasNote = $derived(item.note != null && item.note.trim().length > 0);
|
||||
<div class="mb-3">
|
||||
<div class="rounded-sm border border-line bg-surface p-3">
|
||||
<!-- plaintext — do NOT use {@html} here -->
|
||||
<p class="mb-0.5 font-serif text-sm leading-snug text-ink">{doc.title}</p>
|
||||
<p class="mb-0.5 font-serif text-base leading-snug text-ink">{doc.title}</p>
|
||||
{#if metaLine}
|
||||
<p class="mb-2 text-xs text-ink-3">{metaLine}</p>
|
||||
<p class="mb-2 text-sm text-ink-3">{metaLine}</p>
|
||||
{/if}
|
||||
<a
|
||||
href="/documents/{doc.id}"
|
||||
aria-label={openAriaLabel}
|
||||
class="inline-flex items-center gap-1 text-xs font-semibold text-ink hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
class="inline-flex items-center gap-1 text-sm font-semibold text-ink hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
>
|
||||
<svg class="h-3 w-3 shrink-0" viewBox="0 0 10 12" fill="none">
|
||||
<rect x="1" y="1" width="8" height="10" rx="1" stroke="currentColor" stroke-width="1" />
|
||||
@@ -59,7 +59,7 @@ const hasNote = $derived(item.note != null && item.note.trim().length > 0);
|
||||
{#if hasNote}
|
||||
<!-- plaintext — do NOT use {@html} here -->
|
||||
<div class="mt-3 rounded-r-sm border-l-2 border-brand-mint bg-muted py-1.5 pr-2 pl-3">
|
||||
<p class="text-xs leading-relaxed text-ink-2 italic">{item.note}</p>
|
||||
<p class="text-base leading-relaxed text-ink-2 italic">{item.note}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -134,6 +134,21 @@ describe('JourneyItemCard', () => {
|
||||
expect(note!.className).toContain('bg-muted');
|
||||
});
|
||||
|
||||
it('reading text sizes meet the accessibility floor (#800)', async () => {
|
||||
render(JourneyItemCard, { props: { item: baseItem({ note: 'Ein wichtiger Brief' }) } });
|
||||
|
||||
const title = page.getByText('Brief an Helene');
|
||||
expect((await title.element()).className).toContain('text-base');
|
||||
|
||||
const link = await page.getByRole('link', { name: /öffnen/i }).element();
|
||||
expect(link.className).toContain('text-sm');
|
||||
expect(link.className).not.toContain('text-xs');
|
||||
|
||||
const noteText = document.querySelector('[class*="border-l-2"] p');
|
||||
expect(noteText!.className).toContain('text-base');
|
||||
expect(noteText!.className).not.toContain('text-xs');
|
||||
});
|
||||
|
||||
it('omits annotation block when note is blank or whitespace', async () => {
|
||||
render(JourneyItemCard, { props: { item: baseItem({ note: ' ' }) } });
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const validItems = $derived(
|
||||
{#if introText}
|
||||
<!-- plaintext — do NOT use {@html} here -->
|
||||
<p
|
||||
class="border-subtle mb-6 border-b border-dashed pb-4 font-serif text-sm leading-relaxed text-ink-2 italic"
|
||||
class="border-subtle mb-6 border-b border-dashed pb-4 font-serif text-lg leading-relaxed text-ink-2 italic"
|
||||
>
|
||||
{introText}
|
||||
</p>
|
||||
|
||||
@@ -62,6 +62,17 @@ describe('JourneyReader', () => {
|
||||
await expect.element(page.getByText('Eine Reise durch die Geschichte.')).toBeVisible();
|
||||
});
|
||||
|
||||
it('intro paragraph uses readable body size (text-lg, #800)', async () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: { geschichte: baseGeschichte({ body: 'Eine Reise durch die Geschichte.' }) }
|
||||
});
|
||||
|
||||
const intro = document.querySelector('p');
|
||||
expect(intro!.className).toContain('text-lg');
|
||||
expect(intro!.className).not.toContain('text-sm');
|
||||
});
|
||||
|
||||
it('omits intro paragraph when body is null', async () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
|
||||
Reference in New Issue
Block a user