Detail header gains the author avatar with a two-line author block; journeys say 'zusammengestellt am' instead of 'veröffentlicht am'. Bearbeiten/Löschen move into the metabar for stories too (were at the article bottom). StoryReader renders real document reference cards (icon, title, date · von X an Y) instead of a placeholder link, person chips get avatar initials, and journey items lose the doubled spacing. Shared formatDocumentMetaLine() in geschichte/utils feeds both readers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
4.1 KiB
Svelte
124 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { safeHtml } from '$lib/shared/utils/sanitize';
|
|
import { getInitials, personAvatarColor } from '$lib/person/personFormat';
|
|
import { formatDocumentMetaLine } from './utils';
|
|
import type { components } from '$lib/generated/api';
|
|
|
|
type GeschichteView = components['schemas']['GeschichteView'];
|
|
|
|
interface Props {
|
|
geschichte: GeschichteView;
|
|
}
|
|
|
|
let { geschichte: g }: Props = $props();
|
|
|
|
const sanitized = $derived(safeHtml(g.body));
|
|
|
|
const documentItems = $derived(g.items.filter((i) => i.document));
|
|
|
|
function personName(p: { firstName?: string; lastName?: string }): string {
|
|
return [p.firstName, p.lastName].filter(Boolean).join(' ').trim();
|
|
}
|
|
</script>
|
|
|
|
<!--
|
|
Body styles are explicit (no `prose`) so the text uses the full max-w-3xl
|
|
parent width — Tailwind Typography's default `max-w-prose` clamps to ~65ch
|
|
and produces a much narrower column inside an already narrow page, which
|
|
Leonie flagged as unreadable for the senior-author persona.
|
|
|
|
Sanitised via safeHtml() (DOMPurify) on render — matches backend OWASP allow-list.
|
|
-->
|
|
<div
|
|
class="font-serif text-lg leading-relaxed text-ink [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-2xl [&_h2]:font-bold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-xl [&_h3]:font-bold [&_li]:mb-1 [&_ol]:mb-4 [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:mb-4 [&_ul]:mb-4 [&_ul]:list-disc [&_ul]:pl-6"
|
|
>
|
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
{@html sanitized}
|
|
</div>
|
|
|
|
<!-- Personen -->
|
|
{#if g.persons && g.persons.length > 0}
|
|
<section class="mt-10 border-t border-line pt-6">
|
|
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase">
|
|
{m.geschichten_persons_section()}
|
|
</h2>
|
|
<ul class="flex flex-wrap gap-2">
|
|
{#each g.persons as p (p.id)}
|
|
<li>
|
|
<a
|
|
href="/persons/{p.id}"
|
|
style="display: inline-flex; min-height: 44px"
|
|
class="inline-flex min-h-[44px] items-center gap-2 rounded-full border border-line bg-surface px-3 py-1.5 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full font-sans text-[8px] font-bold text-white"
|
|
style="background-color: {personAvatarColor(p.id)}"
|
|
>
|
|
{getInitials(personName(p))}
|
|
</span>
|
|
{personName(p)}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
{/if}
|
|
|
|
<!-- Dokumente (JourneyItems) -->
|
|
{#if documentItems.length > 0}
|
|
<section class="mt-8 border-t border-line pt-6">
|
|
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase">
|
|
{m.geschichten_documents_section()}
|
|
</h2>
|
|
<ul class="flex flex-col gap-2">
|
|
{#each documentItems as item (item.id)}
|
|
<li>
|
|
<a
|
|
href="/documents/{item.document!.id}"
|
|
class="flex items-start gap-3 rounded-sm border border-line bg-surface p-3 transition-shadow hover:shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
class="flex h-9 w-9 shrink-0 items-center justify-center rounded bg-muted"
|
|
>
|
|
<svg class="h-4 w-4 text-ink-3" viewBox="0 0 10 12" fill="none">
|
|
<rect
|
|
x="1"
|
|
y="1"
|
|
width="8"
|
|
height="10"
|
|
rx="1"
|
|
stroke="currentColor"
|
|
stroke-width="1"
|
|
/>
|
|
<path
|
|
d="M3 4h4M3 6.5h4M3 9h2"
|
|
stroke="currentColor"
|
|
stroke-width=".8"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
<span class="min-w-0">
|
|
<span class="block font-sans text-sm leading-snug font-semibold text-ink">
|
|
{item.document!.title}
|
|
</span>
|
|
{#if formatDocumentMetaLine(item.document!)}
|
|
<span class="block font-sans text-xs text-ink-3">
|
|
{formatDocumentMetaLine(item.document!)}
|
|
</span>
|
|
{/if}
|
|
</span>
|
|
</a>
|
|
{#if item.note}
|
|
<!-- plaintext — do NOT use {@html} here -->
|
|
<p class="mt-1 font-sans text-sm text-ink-3">{item.note}</p>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
{/if}
|