feat(documents): badge undated rows instead of a bare em-dash

DocumentRow rendered a bare em-dash for null-dated letters — a glyph a
screen reader announces as nothing. Both breakpoints now render the single
DocumentDate component unconditionally (no {#if}/—/{:else}), so the cue
cannot drift; its unknown state is a neutral metadata chip ("Datum
unbekannt", text-ink-3, ≥4.5:1 both themes) with a non-color calendar glyph,
never red/amber. Present dates render at honest precision via
formatDocumentDate ("Juni 1916", not a fabricated day).

Refs #668
This commit is contained in:
Marcel
2026-05-27 18:48:45 +02:00
parent f1fc3dc1ce
commit bca3f34cec
3 changed files with 56 additions and 19 deletions

View File

@@ -28,13 +28,21 @@ const showRawLine = $derived(
</script> </script>
<span class="inline-flex flex-col"> <span class="inline-flex flex-col">
<span class="inline-flex items-center gap-1"> {#if isUnknown}
{#if isUnknown} <!--
<!-- Non-color cue (WCAG 1.4.1): a calendar-with-question glyph. The visible Neutral metadata chip (#668): an undated letter is an absence, NOT an error,
"Datum unbekannt" text is the redundant textual cue, so the icon is so the chip is neutral (text-ink-3 on bg-surface, ≥4.5:1 in both themes) —
decorative and hidden from assistive tech (per Leonie's a11y note). --> never red/amber. Matches the archive-metadata chip pattern in the row. The
non-color cue (WCAG 1.4.1) is the calendar-with-question glyph; the visible
"Datum unbekannt" text is the redundant textual cue and IS the accessibility,
so it is announced inline (never aria-hidden) while the icon is decorative.
-->
<span
data-testid="undated-badge"
class="inline-flex items-center gap-1 rounded border border-line px-1.5 py-0.5 font-sans text-[10px] tracking-widest text-ink-3 uppercase"
>
<svg <svg
class="h-3.5 w-3.5 shrink-0 text-ink-3" class="h-3 w-3 shrink-0 text-ink-3"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -48,9 +56,13 @@ const showRawLine = $derived(
<path d="M9 16a1.5 1.5 0 0 1 3 0c0 1-1.5 1.2-1.5 2.2" /> <path d="M9 16a1.5 1.5 0 0 1 3 0c0 1-1.5 1.2-1.5 2.2" />
<path d="M10.5 21h.01" /> <path d="M10.5 21h.01" />
</svg> </svg>
{/if} {label}
<span>{label}</span> </span>
</span> {:else}
<span class="inline-flex items-center gap-1">
<span>{label}</span>
</span>
{/if}
{#if showRawLine} {#if showRawLine}
<!-- Visible secondary line (WCAG 1.4.13 — not tooltip-only). raw is untrusted <!-- Visible secondary line (WCAG 1.4.13 — not tooltip-only). raw is untrusted
verbatim spreadsheet text; rendered via default Svelte interpolation, which verbatim spreadsheet text; rendered via default Svelte interpolation, which

View File

@@ -168,16 +168,12 @@ function safeTagColor(color: string | null | undefined): string {
document DETAIL page, never in list/search rows — list rows surface only the document DETAIL page, never in list/search rows — list rows surface only the
honest label to keep scan-rows compact. showRaw={false} enforces this; the honest label to keep scan-rows compact. showRaw={false} enforces this; the
DocumentListItem payload also intentionally omits metaDateRaw. --> DocumentListItem payload also intentionally omits metaDateRaw. -->
{#if doc.documentDate} <DocumentDate
<DocumentDate iso={doc.documentDate}
iso={doc.documentDate} precision={doc.metaDatePrecision}
precision={doc.metaDatePrecision} end={doc.metaDateEnd}
end={doc.metaDateEnd} showRaw={false}
showRaw={false} />
/>
{:else}
{/if}
</div> </div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<ProgressRing percentage={item.completionPercentage} /> <ProgressRing percentage={item.completionPercentage} />

View File

@@ -73,6 +73,35 @@ describe('DocumentRow title', () => {
}); });
}); });
// ─── Date rendering (#668) ──────────────────────────────────────────────────
describe('DocumentRow date rendering', () => {
it('renders a "Datum unbekannt" badge for an undated document', async () => {
const item = makeItem({ documentDate: undefined, metaDatePrecision: 'UNKNOWN' });
render(DocumentRow, { item });
// The badge text appears (once per breakpoint block).
await expect.element(page.getByText('Datum unbekannt').first()).toBeInTheDocument();
});
it('does not render a bare em-dash for an undated document', async () => {
const item = makeItem({ documentDate: undefined, metaDatePrecision: 'UNKNOWN' });
render(DocumentRow, { item });
await expect.element(page.getByText('—', { exact: true }).first()).not.toBeInTheDocument();
});
it('renders the full date for a day-precision document', async () => {
const item = makeItem({ documentDate: '1943-12-24', metaDatePrecision: 'DAY' });
render(DocumentRow, { item });
await expect.element(page.getByText(/24\. Dezember 1943/).first()).toBeInTheDocument();
});
it('renders month precision honestly without fabricating a day', async () => {
const item = makeItem({ documentDate: '1916-06-01', metaDatePrecision: 'MONTH' });
render(DocumentRow, { item });
await expect.element(page.getByText(/Juni 1916/).first()).toBeInTheDocument();
});
});
// ─── Snippet ────────────────────────────────────────────────────────────────── // ─── Snippet ──────────────────────────────────────────────────────────────────
describe('DocumentRow snippet', () => { describe('DocumentRow snippet', () => {