Files
familienarchiv/frontend/src/routes/geschichten/[id]/+page.svelte
Marcel 66e9309d8a fix(geschichte-ui): a11y and visual round-3 batch
- JourneyItemCard: 'Brief öffnen' back to a >=44px touch target with the
  height regression spec restored
- GeschichteListRow: REISE badges text-[10px] -> text-xs; drop the
  hardcoded aria-label and the mobile badge's aria-hidden so phone screen
  readers learn a row is a Lesereise; mobile avatar initials -> color dot
- detail page: badge text-xs, metabar Edit/Delete h-9 -> h-11, avatar
  color keyed by name to match the list
- JourneyReader: dead border-subtle class -> border-line-2
- DocumentPickerDropdown: aria-controls only while the listbox exists
- JourneyAddBar: aria-expanded/aria-controls on both toggles + focus
  hand-off into the revealed picker input / interlude textarea
- GeschichteSidebar: section h2s hidden below sm (summary already shows
  the label there)
- JourneyCreate: bg-brand-navy -> semantic bg-primary/text-primary-fg;
  title maxlength=255
- JourneyItemRow interlude: neutral frame border + left accent only,
  token utilities instead of arbitrary var() syntax and inline style

Review round 3: Leonie (1-8 + round-1 leftovers), Elicit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:30:59 +02:00

133 lines
4.1 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { m } from '$lib/paraglide/messages.js';
import { formatDate } from '$lib/shared/utils/date';
import { formatAuthorDisplayName } from '$lib/geschichte/utils';
import { getInitials, personAvatarColor } from '$lib/person/personFormat';
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
import { csrfFetch } from '$lib/shared/cookies';
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
import BackButton from '$lib/shared/primitives/BackButton.svelte';
import StoryReader from '$lib/geschichte/StoryReader.svelte';
import JourneyReader from '$lib/geschichte/JourneyReader.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const g = $derived(data.geschichte);
const isJourney = $derived(g.type === 'JOURNEY');
const publishedAt = $derived.by(() => {
if (!g.publishedAt) return null;
return formatDate(g.publishedAt.slice(0, 10), 'long');
});
const authorName = $derived(formatAuthorDisplayName(g.author));
const confirm = getConfirmService();
let deleteError = $state<string | null>(null);
async function handleDelete() {
deleteError = null;
const ok = await confirm.confirm({
title: m.geschichte_delete_confirm_title(),
body: m.geschichte_delete_confirm_body(),
confirmLabel: m.btn_delete(),
cancelLabel: m.btn_cancel(),
destructive: true
});
if (!ok) return;
const res = await csrfFetch(`/api/geschichten/${g.id}`, { method: 'DELETE' });
if (res.ok) {
goto('/geschichten');
} else {
const err = await parseBackendError(res);
deleteError = getErrorMessage(err?.code);
}
}
</script>
<div class="mx-auto max-w-7xl px-4 py-8">
<div class="mb-6">
<BackButton />
</div>
<article
aria-labelledby="geschichte-title"
class="rounded-sm border border-line bg-sheet px-5 py-6 shadow-sm sm:px-10 sm:py-10"
>
<div class="mx-auto max-w-3xl">
<header class="mb-6">
{#if isJourney}
<span
class="mb-2 inline-flex rounded-sm border border-journey-border bg-journey-tint px-2 py-px text-xs 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
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(authorName)}"
>
{getInitials(authorName)}
</span>
{/if}
<div>
{#if authorName}
<p class="font-sans text-sm leading-tight font-semibold text-ink">{authorName}</p>
{/if}
{#if publishedAt}
<p class="font-sans text-xs text-ink-3">
{#if isJourney}
{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-11 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-11 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}
<p
role="alert"
class="mb-4 rounded border border-danger/30 bg-danger/10 px-4 py-3 font-sans text-sm text-danger"
>
{deleteError}
</p>
{/if}
{#if isJourney}
<JourneyReader geschichte={g} />
{:else}
<StoryReader geschichte={g} />
{/if}
</div>
</article>
</div>