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.
BLOG_WRITERs erstellen und pflegen Geschichten über mehrere Sitzungen, verknüpfen historische Personen und Dokumente und entscheiden selbst über Veröffentlichung.
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.
| 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 |
| 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 |
| 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 |
| 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" |
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 |
| 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 |