# Geschichten — Writer Journey

Erstellung, Bearbeitung und Veröffentlichung von Geschichten durch BLOG_WRITERs. Routen /geschichten/new und /geschichten/[id]/edit mit geteiltem Layout: Texteditor links, Metadaten-Sidebar rechts.

Final Spec
2026-05-02
@leonievoss
Issue #381
W

## Writer Journey

BLOG_WRITERs erstellen und pflegen Geschichten über mehrere Sitzungen, verknüpfen historische Personen und Dokumente und entscheiden selbst über Veröffentlichung.

/geschichten/new · /geschichten/[id]/edit
Konzept

Geschichten werden in dedizierten Vollseiten-Routen verfasst – kein Inline-Edit, kein Modal. Dadurch hat der Writer maximale Konzentration auf den Text und alle Metadaten in einer einzigen Ansicht.

Das Desktop-Layout teilt die Seite im Verhältnis 70/30: links der Fließtext-Editor (Titel + Werkzeugleiste + Body), rechts eine schmale Sidebar für Personen-Verlinkung, Dokumenten-Anhänge und Status. Beide Spalten scrollen unabhängig voneinander.

Die Speicherleiste am unteren Rand ist sticky und verändert ihre Aktionen je nach aktuellem Status der Geschichte: Im Zustand ENTWURF bietet sie "Entwurf speichern" und "Veröffentlichen". Im Zustand VERÖFFENTLICHT bietet sie "Speichern" (live) und "Zurück zu Entwurf" (Retract). Ein "Löschen"-Link erscheint in der Topbar nur dann, wenn die Geschichte bereits existiert (kein Löschen beim Anlegen einer neuen Geschichte).

PersonMultiSelect und ein Dokument-Suche-Typeahead (beide bereits im Projekt als etablierte Patterns vorhanden) übernehmen die Verknüpfung von historischen Personen und Dokumenten. Für den Rich-Text-Body genügt ein minimaler contenteditable-Ansatz mit execCommand für Fett, Kursiv und Absätze – keine externe Bibliothek nötig für den MVP.

Screens

### W-1 — /geschichten/new · Neuer Entwurf

US-BLOG-001
Leeres Editor-Layout für eine neue Geschichte. Kein "Löschen"-Link (Geschichte existiert noch nicht). Save-Bar im ENTWURF-Modus mit zwei Aktionen.
Desktop · 1040 px · min-height 560 px · Split 70/30
Desktop — 1040 px
←
Neue Geschichte
ENTWURF
B
I
¶
Alle Änderungen werden als Entwurf gespeichert.
VALIDIERUNGS-STATE — Titel leer beim Speichern
● Bitte gib einen Titel ein.

#### impl-ref · W-1

Element | Wert / Klassen | Anmerkung
--- | --- | ---
Layout
Seiten-Layout | `flex flex-col h-screen` | Feste Höhe, kein Scrollen der Hülle
Editor-Split | `flex flex-1 overflow-hidden` | Beide Panels scrollen eigenständig
Linkes Panel | `flex-1 flex flex-col p-5 overflow-y-auto` | Nimmt restliche Breite
Rechte Sidebar | `w-[280px] shrink-0 border-l border-brand-sand bg-white p-4 overflow-y-auto` | Feste Breite 280 px
Editor-Elemente
Titel-Input | `w-full font-serif text-[22px] text-ink placeholder-ink-3 border-0 border-b border-transparent focus:border-brand-mint focus:outline-none pb-2 mb-4` | Kein Rahmen, nur Unterstrich bei Fokus
Toolbar-Button | `w-7 h-7 flex items-center justify-center rounded border border-line text-xs font-bold text-ink hover:bg-muted` | 28 × 28 px, je B / I / ¶
Body-Textarea | `flex-1 w-full font-serif text-[13px] text-ink resize-none focus:outline-none min-h-[280px]` | contenteditable oder textarea MVP
Speicherleiste
Save-Bar | `sticky bottom-0 border-t border-brand-sand bg-white px-5 py-3 flex items-center justify-between` | Immer sichtbar, sticky
"Entwurf speichern" | `rounded border border-line px-4 py-2 text-sm font-medium text-ink hover:bg-muted` | Ghost-Button
"Veröffentlichen" | `rounded bg-primary text-primary-fg px-4 py-2 text-sm font-medium hover:bg-primary/90` | Navy-Primär-Button
Komponenten
PersonMultiSelect | Wiederverwendung `$lib/components/PersonMultiSelect.svelte` | Identisches Pattern wie Dokument-Bearbeitung
Dokument-Typeahead | Neue Komponente `$lib/components/DocumentTypeahead.svelte` | GET /api/documents?search=; Chips mit Titel + Datum
Validierung Titel | Rotes `border-b border-red-500` + Fehlertext unter dem Input | Nur beim Submit-Versuch auslösen

### W-2 — Entwurf mit Personen und Dokumenten verknüpft

US-BLOG-002
Gleiche Layout-Struktur wie W-1, aber mit ausgefülltem Titel, Fließtext und verknüpften Personen und Dokumenten. Status bleibt ENTWURF.
Desktop only · Mobile-Ansicht siehe W-4
Desktop — 1040 px
←
Der Sommer in Breslau
ENTWURF
B
I
¶

Es war der Sommer 1927, als Franz Raddatz zum ersten Mal nach Breslau reiste. Die Briefe, die er seiner Frau Emma in diesen Wochen schickte, zeugen von einer tiefen Heimweh und gleichzeitig einer neuen Faszination für die lebhafte Stadt an der Oder.

Die Kinder warteten zu Hause in Lauban. Emma hielt die Familie zusammen, wie sie es immer getan hatte — mit Ruhe und einer unerschütterlichen Zuversicht, die aus jedem Satz ihrer Antwortbriefe sprach.

Alle Änderungen werden als Entwurf gespeichert.

### W-3 — Veröffentlichte Geschichte bearbeiten /geschichten/[id]/edit

US-BLOG-003
Gleiche Layout-Struktur, aber Status VERÖFFENTLICHT. Die Speicherleiste zeigt andere Aktionen. Der "Löschen"-Link erscheint in der Topbar (Geschichte existiert bereits). Änderungen sind sofort live.
Desktop · Publish-Flow ist umgekehrt: Retract statt Publish
Desktop — 1040 px
←
Der Sommer in Breslau
VERÖFFENTLICHT
Löschen
B
I
¶

Es war der Sommer 1927, als Franz Raddatz zum ersten Mal nach Breslau reiste. Die Briefe, die er seiner Frau Emma in diesen Wochen schickte, zeugen von einer tiefen Heimweh und gleichzeitig einer neuen Faszination für die lebhafte Stadt an der Oder.

Die Kinder warteten zu Hause in Lauban. Emma hielt die Familie zusammen, wie sie es immer getan hatte — mit Ruhe und einer unerschütterlichen Zuversicht, die aus jedem Satz ihrer Antwortbriefe sprach.

Änderungen sind sofort live.

#### impl-ref · W-3 — Veröffentlicht-Zustand

Element | Wert / Klassen | Anmerkung
--- | --- | ---
VERÖFFENTLICHT-Badge | `rounded-full bg-green-100 text-green-800 text-[10px] font-bold px-2 py-0.5 uppercase tracking-wide` | Grüne Pill, kein Rahmen
"Speichern"-Button | `rounded bg-primary text-primary-fg px-4 py-2 text-sm font-medium` | Immer aktiv, speichert sofort live
"Zurück zu Entwurf" | `rounded border border-line px-4 py-2 text-sm font-medium text-amber-700 hover:bg-amber-50` | Setzt status=DRAFT; Bestätigung optional
"Löschen"-Link | `text-sm text-red-600 font-medium hover:underline` | Nur sichtbar wenn Geschichte existiert; öffnet Bestätigungs-Dialog
Save-Bar-Hinweis | "Änderungen sind sofort live." (xs, muted) | Ersetzt den ENTWURF-Hinweis

### W-4 — Mobile Ansicht /geschichten/new

US-BLOG-001 · 320 px
Gestapeltes Layout ohne horizontale Teilung. Sidebar-Inhalte werden in einem ausklappbaren Bereich unterhalb des Body-Editors angezeigt. Speicherleiste mit vertikal gestapelten Buttons.
Phone · 320 px · Sidebar kollabiert; kein Split
Phone — 320 px
9:41●●●●
←
Neue Geschichte
ENTWURF
B
I
¶
Schreibe hier deine Geschichte…
Personen & Dokumente ›
Personen
🔍
Person suchen…
Dokumente
🔍
Dokument suchen…

#### impl-ref · W-4 — Mobile

Element | Wert / Klassen | Anmerkung
--- | --- | ---
Editor-Layout (Mobile) | `flex flex-col h-screen` | Kein horizontaler Split
Sidebar-Collapsible | `<details>` oder Svelte `bind:open` | Standardmäßig geschlossen; Chevron dreht sich
Save-Bar Mobile | `flex flex-col gap-2 p-3 bg-white border-t` | Buttons vertikal gestapelt, volle Breite
"Entwurf speichern" Mobile | `w-full rounded border border-line py-2 text-sm font-medium text-ink` | Ghost, volle Breite
"Veröffentlichen" Mobile | `w-full rounded bg-primary text-primary-fg py-2 text-sm font-medium` | Primary, volle Breite, oben

### W-5 — Lösch-Bestätigung (Dialog)

US-BLOG-006
Modal-Dialog über dem Editor (W-3-Zustand: veröffentlichte Geschichte). Dunkles Scrim überlagert den Editor. Dialog liegt zentriert darüber. Kein custom Dialog — Wiederverwendung des bestehenden confirm-Service.
Desktop · Ausgelöst durch Klick auf "Löschen" in der Topbar
Desktop — 1040 px (mit Modal)
←
Der Sommer in Breslau
VERÖFFENTLICHT
Löschen

Es war der Sommer 1927, als Franz Raddatz zum ersten Mal nach Breslau reiste…

Änderungen sind sofort live.
Geschichte löschen?
Diese Aktion kann nicht rückgängig gemacht werden. Die Geschichte wird dauerhaft gelöscht und aus allen verlinkten Personen- und Dokumentseiten entfernt.

#### impl-ref · W-5 — Lösch-Dialog

Element | Wert / Anmerkung
--- | ---
Dialog-Implementierung | Wiederverwendung `getConfirmService()` aus `$lib/services/confirm.svelte.js` — kein custom Dialog nötig
Scrim | `fixed inset-0 bg-black/40 z-40 flex items-center justify-center`
Dialog-Box | `bg-white rounded-lg shadow-overlay w-[400px] p-6 flex flex-col gap-4 z-50`
Titel | `font-serif text-[18px] font-medium text-ink`
"Abbrechen" | Ghost-Button — schließt Dialog, kein State-Wechsel
"Löschen" | `rounded bg-red-600 text-white px-4 py-2 text-sm font-medium hover:bg-red-700` — DELETE /api/geschichten/{id}, dann redirect /geschichten
Nach Löschen | Redirect auf `/geschichten` (Index), Toast "Geschichte gelöscht"

Implementierungs-Guide für LLMs

Alle fünf Screens teilen dieselbe Svelte-Komponente `GeschichteEditor.svelte`. Der Unterschied zwischen /geschichten/new und /geschichten/[id]/edit liegt ausschließlich in den Load-Daten und im initialen Status-Zustand.

Route | Komponente | Load-Funktion
--- | --- | ---
`/geschichten/new` | `GeschichteEditor.svelte` | Kein Load nötig — leerer Zustand, status=DRAFT
`/geschichten/[id]/edit` | `GeschichteEditor.svelte` | GET /api/geschichten/{id} → Geschichte by id; wirft 404 wenn nicht gefunden

#### Technische Entscheidungen

Thema | Entscheidung | Begründung
--- | --- | ---
Rich-Text-Editor
MVP-Implementierung | Minimales `contenteditable` div oder `<textarea>` mit `document.execCommand` für B/I/¶ | Issue #381 erfordert nur Bold, Italic, Absatzumbrüche — keine Bibliothek, kein Bundle-Overhead
Persistenz-Format | HTML-String im Backend (VARCHAR / TEXT) | Einfachstes Format; bei Bedarf später auf Markdown oder ProseMirror JSON migrierbar
Personen & Dokumente
PersonMultiSelect | Direktes Wiederverwenden von `$lib/components/PersonMultiSelect.svelte` | Identisches Pattern wie im Dokument-Bearbeitungsformular — kein neues Rad erfinden
Dokument-Typeahead | Neue Komponente `$lib/components/DocumentTypeahead.svelte` | GET /api/documents?search= — gleicher Aufbau wie PersonTypeahead; Chips zeigen Titel + Datum
Permissions
Route Guard | Server-seitiger Check in +page.server.ts: wenn User kein BLOG_WRITE → redirect /geschichten | Niemals Editor-Controls in Lese-Ansichten zeigen; Client-seitige Prüfung reicht nicht
Autorschaft | Jeder BLOG_WRITER kann jede Geschichte bearbeiten; `author`-Feld ist nur Anzeige | Familienarchiv ist kein Blog mit privaten Drafts; Kollaboration ist erwünscht
Status-Logik
Publish-Action | PATCH /api/geschichten/{id} mit `{ "status": "PUBLISHED" }` | Kein separater Endpunkt nötig — Status ist ein Feld des Modells
Retract-Action | PATCH /api/geschichten/{id} mit `{ "status": "DRAFT" }` | Umkehrbar; keine separate Bestätigung (anders als Löschen)
"Löschen"-Sichtbarkeit | Nur sichtbar wenn `data.geschichte !== null` (d.h. Edit-Route, nicht New-Route) | Kein Löschen für nicht-existierende Geschichten
Löschen
Bestätigungs-Dialog | Wiederverwendung `getConfirmService()` aus `$lib/services/confirm.svelte.js` | Kein custom Dialog; bereits im Projekt vorhanden
Nach DELETE | DELETE /api/geschichten/{id} → 204 → redirect /geschichten + Toast | Standard-Muster wie bei Personen und Dokumenten
Mobile Responsive
Breakpoint | Unter 640 px (sm): Split aufheben, Sidebar als Collapsible | Transcribers (60+) auf Laptop/Tablet; Reader (jünger) auf Phones — Responsive für Writer ist Minor
Collapsible-Trigger | "Personen & Dokumente" mit Chevron; standardmäßig geschlossen | Body-Editor hat Priorität; Metadaten sind sekundär auf kleinen Bildschirmen