fix(geschichte-detail): span directory width with centered reading column
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m9s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 4m14s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s

The detail page stayed at max-w-3xl and looked narrower than every
other page. The outer container now matches the directory width
(max-w-7xl) so the sheet spans the page like Dokumente/Personen, while
an inner max-w-3xl column keeps the prose line length readable.

Refs #799
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-10 22:49:56 +02:00
parent 2d38122833
commit 264d7268c4
4 changed files with 78 additions and 62 deletions

View File

@@ -640,7 +640,7 @@
<thead><tr><th>Element</th><th>Wert</th><th>Hinweise</th></tr></thead> <thead><tr><th>Element</th><th>Wert</th><th>Hinweise</th></tr></thead>
<tbody> <tbody>
<tr class="grp"><td colspan="3">Artikel-Container</td></tr> <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 container</td><td>max-w-7xl mx-auto px-4 py-8; innere Lesespalte: max-w-3xl mx-auto</td><td>Seite so breit wie Dokumente/Personen; Textspalte bleibt lesbar zentriert</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>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>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><td>Back button</td><td>&lt;BackButton /&gt; aus $lib/components/BackButton.svelte</td><td>history.back(); nicht &lt;a href&gt;</td></tr>

View File

@@ -629,7 +629,7 @@
<tbody> <tbody>
<tr class="grp"><td colspan="3">Seitenstruktur</td></tr> <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>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-Container</td><td>max-w-7xl mx-auto px-4 py-8; innere Lesespalte: max-w-3xl mx-auto</td><td>gleich wie StoryReader (R-2)</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>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-journey-tint text-journey border border-journey-border mb-2</td><td>über dem Titel; nicht für STORY</td></tr> <tr><td>Journey-Badge</td><td>inline-flex px-2 py-px rounded-sm text-[10px] font-bold uppercase tracking-widest bg-journey-tint text-journey border border-journey-border 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>Titel</td><td>font-serif text-3xl text-ink leading-tight mb-4</td><td>gleich wie Story</td></tr>

View File

@@ -48,7 +48,7 @@ async function handleDelete() {
} }
</script> </script>
<div class="mx-auto max-w-3xl px-4 py-8"> <div class="mx-auto max-w-7xl px-4 py-8">
<div class="mb-6"> <div class="mb-6">
<BackButton /> <BackButton />
</div> </div>
@@ -57,74 +57,76 @@ async function handleDelete() {
aria-labelledby="geschichte-title" aria-labelledby="geschichte-title"
class="rounded-sm border border-line bg-sheet 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"> <div class="mx-auto max-w-3xl">
{#if isJourney} <header class="mb-6">
<span {#if isJourney}
class="mb-2 inline-flex rounded-sm border border-journey-border bg-journey-tint px-2 py-px text-[10px] font-bold tracking-widest text-journey uppercase"
>
{m.journey_badge_detail()}
</span>
{/if}
<h1 id="geschichte-title" class="mb-4 font-serif text-3xl leading-tight text-ink">
{g.title}
</h1>
<div class="mb-4 flex items-center gap-3 border-b border-line-2 pb-4">
{#if authorName}
<span <span
aria-hidden="true" class="mb-2 inline-flex rounded-sm border border-journey-border bg-journey-tint px-2 py-px text-[10px] font-bold tracking-widest text-journey uppercase"
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full font-sans text-xs font-bold text-white"
style="background-color: {personAvatarColor(g.author?.id ?? authorName)}"
> >
{getInitials(authorName)} {m.journey_badge_detail()}
</span> </span>
{/if} {/if}
<div> <h1 id="geschichte-title" class="mb-4 font-serif text-3xl leading-tight text-ink">
{g.title}
</h1>
<div class="mb-4 flex items-center gap-3 border-b border-line-2 pb-4">
{#if authorName} {#if authorName}
<p class="font-sans text-sm leading-tight font-semibold text-ink">{authorName}</p> <span
aria-hidden="true"
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full font-sans text-xs font-bold text-white"
style="background-color: {personAvatarColor(g.author?.id ?? authorName)}"
>
{getInitials(authorName)}
</span>
{/if} {/if}
{#if publishedAt} <div>
<p class="font-sans text-xs text-ink-3"> {#if authorName}
{#if isJourney} <p class="font-sans text-sm leading-tight font-semibold text-ink">{authorName}</p>
{m.journey_compiled_on({ date: publishedAt })} {/if}
{:else} {#if publishedAt}
{m.geschichten_published_on({ date: publishedAt })} <p class="font-sans text-xs text-ink-3">
{/if} {#if isJourney}
</p> {m.journey_compiled_on({ date: publishedAt })}
{:else}
{m.geschichten_published_on({ date: publishedAt })}
{/if}
</p>
{/if}
</div>
{#if data.canBlogWrite}
<div class="ml-auto flex items-center gap-3">
<a
href="/geschichten/{g.id}/edit"
class="inline-flex h-9 items-center rounded border border-line bg-surface px-3 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.btn_edit()}
</a>
<button
type="button"
onclick={handleDelete}
class="inline-flex h-9 items-center rounded font-sans text-sm font-medium text-danger hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.btn_delete()}
</button>
</div>
{/if} {/if}
</div> </div>
{#if data.canBlogWrite} </header>
<div class="ml-auto flex items-center gap-3">
<a
href="/geschichten/{g.id}/edit"
class="inline-flex h-9 items-center rounded border border-line bg-surface px-3 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.btn_edit()}
</a>
<button
type="button"
onclick={handleDelete}
class="inline-flex h-9 items-center rounded font-sans text-sm font-medium text-danger hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.btn_delete()}
</button>
</div>
{/if}
</div>
</header>
{#if deleteError} {#if deleteError}
<p <p
role="alert" role="alert"
class="mb-4 rounded border border-danger/30 bg-danger/10 px-4 py-3 font-sans text-sm text-danger" class="mb-4 rounded border border-danger/30 bg-danger/10 px-4 py-3 font-sans text-sm text-danger"
> >
{deleteError} {deleteError}
</p> </p>
{/if} {/if}
{#if isJourney} {#if isJourney}
<JourneyReader geschichte={g} /> <JourneyReader geschichte={g} />
{:else} {:else}
<StoryReader geschichte={g} /> <StoryReader geschichte={g} />
{/if} {/if}
</div>
</article> </article>
</div> </div>

View File

@@ -70,6 +70,20 @@ describe('geschichten/[id] page', () => {
.toBeVisible(); .toBeVisible();
}); });
it('spans the directory width with a centered reading column inside the sheet (#799)', async () => {
render(GeschichtePage, {
context: new Map([[CONFIRM_KEY, createConfirmService()]]),
props: { data: baseData() }
});
const outer = document.querySelector('[class*="mx-auto"]');
expect(outer!.className).toContain('max-w-7xl');
const column = document.querySelector('article [class*="max-w-3xl"]');
expect(column).not.toBeNull();
expect(column!.className).toContain('mx-auto');
});
it('renders the article on a reading-sheet surface card (#797)', async () => { it('renders the article on a reading-sheet surface card (#797)', async () => {
render(GeschichtePage, { render(GeschichtePage, {
context: new Map([[CONFIRM_KEY, createConfirmService()]]), context: new Map([[CONFIRM_KEY, createConfirmService()]]),