diff --git a/docs/specs/geschichten-reader-journey-spec.html b/docs/specs/geschichten-reader-journey-spec.html index ac537ed7..f5970dbd 100644 --- a/docs/specs/geschichten-reader-journey-spec.html +++ b/docs/specs/geschichten-reader-journey-spec.html @@ -421,7 +421,7 @@ Seitenstruktur Page titlefont-family:var(--font-display);font-size:24px;color:var(--navy)Fraunces, nicht fett - Editorial list cardbg-white shadow-sm border border-brand-sand rounded-smwraps alle Zeilen + Editorial list cardbg-white shadow-sm border border-brand-sand rounded-sm (implementiert: bg-surface border-line — semantische Tokens für Dark Mode)wraps alle Zeilen Listenzeile List rowflex gap-0 border-b border-brand-sand last:border-0 hover:bg-surfacemin-h-[44px] auf Mobile Meta columnw-40 shrink-0 flex flex-col gap-1 p-3 border-r border-line-2feste Breite — breit genug für text-sm Namen ohne Umbruch @@ -658,7 +658,7 @@ Doc reference cardflex gap-3 items-start bg-white border border-brand-sand rounded-sm p-3 hover:shadow-smlinks zu /documents/[id] Doc iconw-9 h-9 bg-surface rounded flex items-center justify-center shrink-0Dateisymbol SVG Mobile - … Menü (Mobile)ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + LöschenBLOG_WRITE-Aktionen auf Mobile + … Menü (Mobile)ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen (implementiert: Bearbeiten/Löschen bleiben inline in der Metazeile auf allen Breiten — kein BottomSheet)BLOG_WRITE-Aktionen auf Mobile Person chips (Mobile)flex-wrap, volle Breitekein horizontales Scrollen Doc cards (Mobile)flex-col gap-2stapeln vertikal @@ -713,7 +713,7 @@

Barrierefreiheit

diff --git a/docs/specs/lesereisen-reader-spec.html b/docs/specs/lesereisen-reader-spec.html index 7c0756d0..060ec634 100644 --- a/docs/specs/lesereisen-reader-spec.html +++ b/docs/specs/lesereisen-reader-spec.html @@ -634,7 +634,7 @@ Journey-Badgeinline-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über dem Titel; nicht für STORY Titelfont-serif text-3xl text-ink leading-tight mb-4gleich wie Story Metabarflex items-center gap-3 pb-4 border-b border-subtle mb-4gleich wie Story - Bearbeiten/Löschennur BLOG_WRITE; auf Mobile im ··· BottomSheetgleich wie Story + Bearbeiten/Löschennur BLOG_WRITE; auf Mobile im ··· BottomSheet (implementiert: inline in der Metazeile auf allen Breiten)gleich wie Story Intro-Absatz Intro (body)font-serif text-lg text-ink-2 italic leading-relaxed mb-6 pb-4 border-b border-dashed border-subtlenur rendern wenn body nicht leer; kein HTML-Rendering — plaintext Dokument-Item @@ -650,7 +650,7 @@ Interlude-Blockpl-3 border-l-2 border-journey-border bg-journey-tint rounded-r-sm py-2 pr-3 my-4item.document === null Interlude-Texttext-base italic text-ink leading-relaxeditem.note; plaintext Mobile - ··· Menüml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + LöschenBLOG_WRITE; gleich wie Story + ··· Menüml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen (implementiert: kein BottomSheet — Aktionen inline)BLOG_WRITE; gleich wie Story Touch Target (Brief öffnen)min-h-[44px] durch padding auf der KarteWCAG 2.2 AA @@ -711,7 +711,7 @@

Berechtigungen

Barrierefreiheit

diff --git a/frontend/src/lib/geschichte/README.md b/frontend/src/lib/geschichte/README.md index f1e6e959..b569a567 100644 --- a/frontend/src/lib/geschichte/README.md +++ b/frontend/src/lib/geschichte/README.md @@ -23,16 +23,17 @@ Utilities: `utils.ts`. | `JourneyItemRow.svelte` | `JourneyEditor.svelte` | Item row: drag handle, move-up/down, note textarea (PATCH on blur), inline remove confirm | | `JourneyAddBar.svelte` | `JourneyEditor.svelte` | Two add buttons: document picker (`DocumentPickerDropdown`) and interlude draft form | | `GeschichtenCard.svelte` | Person-detail sidebar | Sidebar card showing the 3 most recent stories linked to a person — NOT the list page | -| `GeschichteListRow.svelte` | `/geschichten` (list) | Row component for the list; shows JOURNEY type badge (`text-xs` orange pill) | +| `GeschichteListRow.svelte` | `/geschichten` (list) | Editorial list row: meta column (avatar, author, date, REISE badge), title + excerpt content column | | `StoryReader.svelte` | `/geschichten/[id]` (STORY branch) | Renders sanitised rich-text body, persons section, documents section, and author actions | | `JourneyReader.svelte` | `/geschichten/[id]` (JOURNEY branch) | Renders intro paragraph, ordered items list, empty-state; delegates to ItemCard/Interlude | -| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Whole-card `` for a document item; dated/undated aria-label, ✎ annotation glyph | -| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Typographic aside between letters; ❦ glyph, `aria-label="Kuratorennotiz"` | +| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Card per document item: title, meta line (date · von X an Y), "Brief öffnen →" link, mint-border note | +| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Left-accent interlude box between letters (mode-aware tokens); `aria-label="Kuratorennotiz"` | ## utils.ts -`formatAuthorName(author)` — joins `firstName + lastName`, falls back to `email` (for list/summary shapes). -`formatAuthorDisplayName(author)` — returns `displayName` (for detail `AuthorView` shape). +`formatAuthorName(author)` — joins `firstName + lastName`, falls back to the localized `person_unknown` key (for list/summary shapes; email is not exposed). +`formatAuthorDisplayName(author)` — returns `displayName`, localizing the server's `[Unbekannt]` fallback (for detail `AuthorView` shape). +`formatDocumentMetaLine(doc)` — `"12.07.1938 · von Franz an Emma"`; shared by `JourneyItemCard`, `JourneyItemRow`, and the story doc-reference cards. `formatPublishedAt(publishedAt, style)` — wraps `formatDate` with null check; `style` is `'short'` (list) or `'long'` (detail). ## Public list is PUBLISHED-only diff --git a/frontend/src/lib/shared/utils/extractText.ts b/frontend/src/lib/shared/utils/extractText.ts index 331d9dea..a5ccb976 100644 --- a/frontend/src/lib/shared/utils/extractText.ts +++ b/frontend/src/lib/shared/utils/extractText.ts @@ -1,13 +1,18 @@ /** - * **Not a sanitizer.** This module extracts visible text from a (presumed - * already-sanitised) HTML string for excerpt rendering. It is safe ONLY - * because the Geschichte body is sanitised against the OWASP allow-list - * on the server before persistence, and via DOMPurify on render. + * **Not a sanitizer.** This module extracts visible text from an HTML (or + * plain-text) string for excerpt rendering. The safety invariant is: the + * OUTPUT must only ever be rendered via Svelte text interpolation — never + * `{@html}`. The DOMParser document is inert (scripts don't execute), but + * the returned string is whatever text the input carried. + * + * Note on inputs: STORY bodies are additionally sanitised against the OWASP + * allow-list on the server; JOURNEY intros are stored VERBATIM (unsanitised + * by design — see GeschichteService.bodyForType) and arrive here untrusted. * * Do not use these helpers to defend against XSS — `safeHtml()` in * `./sanitize.ts` is the only sanitiser. Calling `extractText()` on - * untrusted input that has not been sanitised does not protect against - * `javascript:` URLs, event-handler attributes, or `` payloads. + * untrusted input does not protect against `javascript:` URLs, + * event-handler attributes, or `` payloads. */ /**