feat(ui): add sheet surface token between canvas and white cards
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m56s
CI / OCR Service Tests (pull_request) Successful in 26s
CI / Backend Unit Tests (pull_request) Successful in 4m8s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s

The reading sheet used bg-surface (white), so the document cards inside
the article had the same background as the sheet itself. The spec's
three-level hierarchy is canvas → article panel (#FAFAF7) → white cards;
introduce --color-sheet (mode-aware) and use it on the article. Also
move JourneyItemCard from bg-white to bg-surface so dark mode remaps it,
and tint the curator annotation with bg-muted so it stands off the card.

Refs #797
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-10 22:32:10 +02:00
parent b926bdefde
commit 63acb5417f
7 changed files with 31 additions and 8 deletions

View File

@@ -641,7 +641,7 @@
<tbody>
<tr class="grp"><td colspan="3">Artikel-Container</td></tr>
<tr><td>Article container</td><td>max-w-3xl mx-auto px-4 py-8</td><td>zentriert, volle Breite auf Mobile</td></tr>
<tr><td>Article sheet</td><td>rounded-sm border border-line bg-surface shadow-sm px-5 py-6 sm:px-10 sm:py-10</td><td>Lesebogen-Panel auf dem Canvas (siehe Mockup); BackButton bleibt außerhalb</td></tr>
<tr><td>Article sheet</td><td>rounded-sm border border-line bg-sheet shadow-sm px-5 py-6 sm:px-10 sm:py-10</td><td>Lesebogen-Panel zwischen Canvas und weißen Karten (Token --color-sheet); BackButton bleibt außerhalb</td></tr>
<tr><td>Story title</td><td>font-family:var(--font-display);font-size:clamp(22px,4vw,32px);color:var(--navy)</td><td>Fraunces, nicht fett</td></tr>
<tr><td>Back button</td><td>&lt;BackButton /&gt; aus $lib/components/BackButton.svelte</td><td>history.back(); nicht &lt;a href&gt;</td></tr>
<tr class="grp"><td colspan="3">Metazeile</td></tr>

View File

@@ -630,7 +630,7 @@
<tr class="grp"><td colspan="3">Seitenstruktur</td></tr>
<tr><td>Bedingte Logik</td><td>{#if geschichte.type === 'JOURNEY'} JourneyReader {:else} StoryReader {/if}</td><td>in +page.svelte von /geschichten/[id]</td></tr>
<tr><td>Artikel-Container</td><td>max-w-3xl mx-auto px-4 py-8</td><td>gleich wie StoryReader</td></tr>
<tr><td>Artikel-Sheet</td><td>rounded-sm border border-line bg-surface shadow-sm px-5 py-6 sm:px-10 sm:py-10</td><td>Lesebogen-Panel auf dem Canvas, gleich wie Story (R-2); BackButton bleibt außerhalb</td></tr>
<tr><td>Artikel-Sheet</td><td>rounded-sm border border-line bg-sheet shadow-sm px-5 py-6 sm:px-10 sm:py-10</td><td>Lesebogen-Panel zwischen Canvas und weißen Karten (Token --color-sheet), gleich wie Story (R-2); BackButton bleibt außerhalb</td></tr>
<tr><td>Journey-Badge</td><td>inline-flex px-2 py-px rounded-sm text-[10px] font-bold uppercase tracking-widest bg-orange-50 text-orange-700 border border-orange-200 mb-2</td><td>über dem Titel; nicht für STORY</td></tr>
<tr><td>Titel</td><td>font-serif text-3xl text-ink leading-tight mb-4</td><td>gleich wie Story</td></tr>
<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>
@@ -639,12 +639,12 @@
<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 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-white border border-line rounded-sm p-3</td><td></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 class="grp"><td colspan="3">Kuratoren-Annotation</td></tr>
<tr><td>Annotation</td><td>mt-3 pl-3 border-l-2 border-mint bg-surface rounded-r-sm py-1.5 pr-2</td><td>nur rendern wenn item.note vorhanden</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 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>

View File

@@ -25,7 +25,7 @@ const hasNote = $derived(item.note != null && item.note.trim().length > 0);
</script>
<div class="mb-3">
<div class="rounded-sm border border-line bg-white p-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>
{#if metaLine}
@@ -58,7 +58,7 @@ const hasNote = $derived(item.note != null && item.note.trim().length > 0);
</a>
{#if hasNote}
<!-- plaintext — do NOT use {@html} here -->
<div class="mt-3 rounded-r-sm border-l-2 border-brand-mint bg-surface py-1.5 pr-2 pl-3">
<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>
</div>
{/if}

View File

@@ -118,6 +118,22 @@ describe('JourneyItemCard', () => {
expect(note!.className).toContain('border-brand-mint');
});
it('card uses the surface token, not bg-white, so dark mode remaps it', async () => {
render(JourneyItemCard, { props: { item: baseItem() } });
const card = document.querySelector('[class*="border-line"]');
expect(card).not.toBeNull();
expect(card!.className).toContain('bg-surface');
expect(card!.className).not.toContain('bg-white');
});
it('annotation block is tinted with bg-muted to stand off the white card', async () => {
render(JourneyItemCard, { props: { item: baseItem({ note: 'Ein wichtiger Brief' }) } });
const note = document.querySelector('[class*="border-l-2"]');
expect(note!.className).toContain('bg-muted');
});
it('omits annotation block when note is blank or whitespace', async () => {
render(JourneyItemCard, { props: { item: baseItem({ note: ' ' }) } });

View File

@@ -55,7 +55,7 @@ async function handleDelete() {
<article
aria-labelledby="geschichte-title"
class="rounded-sm border border-line bg-surface px-5 py-6 shadow-sm sm:px-10 sm:py-10"
class="rounded-sm border border-line bg-sheet px-5 py-6 shadow-sm sm:px-10 sm:py-10"
>
<header class="mb-6">
{#if isJourney}

View File

@@ -78,7 +78,8 @@ describe('geschichten/[id] page', () => {
const article = document.querySelector('article');
expect(article).not.toBeNull();
for (const cls of ['bg-surface', 'border-line', 'rounded-sm', 'shadow-sm']) {
// bg-sheet sits between the sand canvas and the white cards inside the article
for (const cls of ['bg-sheet', 'border-line', 'rounded-sm', 'shadow-sm']) {
expect(article!.className).toContain(cls);
}
});

View File

@@ -34,6 +34,9 @@
--color-overlay: var(--c-overlay);
--color-muted: var(--c-muted);
/* Reading sheet — article panel between canvas and the white cards it contains */
--color-sheet: var(--c-sheet);
/* Borders */
--color-line: var(--c-line);
--color-line-2: var(--c-line-2);
@@ -106,6 +109,7 @@
--c-surface: #ffffff;
--c-overlay: #ffffff;
--c-muted: #f5f4ef;
--c-sheet: #fafaf7; /* between canvas and surface — spec .g-article value */
--c-line: #e4e2d7;
--c-line-2: #eeede8;
@@ -213,6 +217,7 @@
--c-surface: #011526;
--c-overlay: #011e38;
--c-muted: #011a30;
--c-sheet: #011222; /* between canvas and surface */
--c-line: #0d3358;
--c-line-2: #092843;
@@ -306,6 +311,7 @@
--c-surface: #011526;
--c-overlay: #011e38;
--c-muted: #011a30;
--c-sheet: #011222; /* between canvas and surface */
--c-line: #0d3358;
--c-line-2: #092843;