Kuratierungs-Oberfläche für JourneyEditor auf /geschichten/[id]/edit (wenn type === 'JOURNEY'). Geordnete Briefliste mit Drag-to-Reorder, Dokumenten-Picker, Interlude-Notizen und Inline-Annotation-Editing. Ersetzt den TipTap-Editor für den Journey-Typ.
BLOG_WRITERs kuratieren eine geordnete Briefsequenz — Briefe hinzufügen, Zwischentexte einfügen, Reihenfolge per Drag anpassen, Notizen inline bearbeiten.
Der JourneyEditor ist eine parallele Implementierung zum bestehenden GeschichteEditor und wird auf derselben Edit-Route eingeblendet wenn type === 'JOURNEY'. Das Split-Layout (70/30) bleibt erhalten: links die Briefliste, rechts die Sidebar mit Personen und Status.
Die linke Fläche zeigt: oben einen optionalen Einleitungs-Textarea (body), darunter die geordnete Itemliste, ganz unten eine Aktionsleiste mit „+ Brief hinzufügen" und „+ Zwischentext hinzufügen". Jedes Item hat einen Drag-Handle, eine Positionsnummer, den Inhalt und einen Entfernen-Button.
Dokument-Items zeigen Titel und Kurz-Metadaten. Eine „Notiz hinzufügen/bearbeiten"-Aktion expandiert ein Textarea direkt unterhalb des Items — kein Modal, kein separates Formular. Interlude-Items (reiner Zwischentext) zeigen direkt ein editierbares Textarea mit orangenem Hintergrund zur klaren visuellen Unterscheidung.
Speicheraktionen: Speichern (bei veröffentlichter Journey) oder „Entwurf speichern" + „Veröffentlichen" (bei DRAFT). Die Savebarlogik ist identisch zum GeschichteEditor. Alle Mutationen lösen sofort einen API-Call aus und aktualisieren den lokalen Zustand optimistisch — kein separates Save für einzelne Items.
Ausgangszustand einer neuen oder leeren Lesereise. Titel-Input oben. Darunter ein optionaler Einleitungs-Textarea. Leere Itemliste mit Leerstate-Text. Aktionsleiste mit zwei Buttons. Sidebar: Personen-Verknüpfung und Status-Anzeige. Keine Items → „Veröffentlichen" noch nicht aktiv (Disabled-Hint erscheint).
Varianten: Neuer Entwurf ohne Titel (hier gezeigt) · Mit Titel, leere Liste
| Element | Wert | Hinweise |
|---|---|---|
| Seitenstruktur | ||
| Bedingte Verzweigung | {#if geschichte.type === 'JOURNEY'}<JourneyEditor />{:else}<GeschichteEditor />{/if} | in edit/+page.svelte; Props: geschichte: Geschichte |
| Split-Layout | flex flex-1 overflow-hidden (gleich wie GeschichteEditor) | 70/30; Sidebar identisch |
| Topbar-Badge | „REISE" Pill neben dem Titel-Label | orange; kein interaktives Element; zeigt Typ |
| Titel-Input | ||
| Titel-Input | font-serif text-2xl border-b border-line pb-2 w-full bg-transparent outline-none | bind:value={title}; gleiche Validierung wie GeschichteEditor (required) |
| Einleitungs-Textarea | ||
| Intro-Textarea | font-serif text-sm italic text-ink-3 leading-relaxed w-full resize-none bg-transparent outline-none border-none py-1 | bind:value={body}; plaintext; auto-resize per rows-attr oder JS |
| Label | text-[10px] font-bold uppercase tracking-widest text-ink-3 mb-1 | „EINLEITUNG (OPTIONAL)" |
| Leerstate | ||
| Leerstate-Container | py-8 text-center border border-dashed border-line rounded-sm bg-surface | verschwindet sobald erstes Item vorhanden |
| Leerstate-Text | font-serif text-xs text-ink-3 italic | |
| Veröffentlichen-Button | ||
| Disabled-Zustand | disabled={items.length === 0 || !title.trim()} | opacity-40 + cursor-not-allowed; keine Tooltip nötig — Sidebar-Hint erklärt es |
Gefüllte Itemliste mit gemischten Typen: Dokument-Item ohne Notiz, Interlude-Item (reiner Zwischentext), Dokument-Item mit bestehender Notiz. Jedes Item zeigt Drag-Handle links, Positionsnummer, Inhalt und Entfernen-Button. Aktionsleiste bleibt unter der Liste sichtbar.
Varianten: Veröffentlichte Journey (hier gezeigt) · Entwurf · Mobile
| Element | Wert | Hinweise |
|---|---|---|
| Item-Zeile allgemein | ||
| Item-Container | flex items-stretch bg-white border border-line rounded-sm mb-2 overflow-hidden | interlude: bg-orange-50 border-orange-200 |
| Drag-Handle | w-4 bg-surface border-r border-line flex items-center justify-center cursor-grab shrink-0 | aria-label="Reihenfolge ändern"; cursor-grabbing während Drag |
| Positions-Nr. | w-5 text-[10px] font-bold text-ink-3 flex items-start justify-center pt-2 shrink-0 | aus Array-Index, nicht item.position |
| Entfernen-Button | w-6 flex items-start justify-center pt-2 shrink-0 | × aria-label="Eintrag entfernen"; hover: text-red-500; Confirm nur wenn note vorhanden |
| Dokument-Item | ||
| Brieftitel | text-[11px] font-semibold text-ink leading-snug mb-0.5 | document.title |
| Briefmeta | text-xs text-ink-3 | formatDate(doc.documentDate) · "von X" oder "von X an Y" |
| Notiz-Textarea (sichtbar) | w-full min-h-[40px] font-serif text-xs italic bg-surface border border-line rounded-sm p-1.5 resize-none focus:border-primary focus:bg-white mt-2 | auto-expand; bind:value={item.note} |
| „Notiz hinzufügen" Link | text-xs font-semibold text-blue-600 inline-flex items-center gap-1 mt-1 | togglet Notiz-Textarea |
| „Notiz entfernen" Link | text-xs text-ink-3 inline-flex items-center gap-1 mt-1 | zeigt sich wenn note.trim() nicht leer; setzt note = '' und blendet Textarea aus |
| Interlude-Item | ||
| Interlude-Container | bg-orange-50 border-orange-200 (überschreibt Item-Container) | kein Positions-Kreis; Positions-Spalte zeigt Icon statt Zahl |
| Label „Zwischentext" | text-[9px] font-bold uppercase tracking-widest text-orange-700 mb-1 | immer sichtbar; nicht editierbar |
| Zwischentext-Textarea | w-full min-h-[44px] font-serif text-xs italic bg-white/60 border border-orange-200 rounded-sm p-1.5 resize-none focus:border-orange-400 | bind:value={item.note}; auto-expand; min 44px für Touch-Target |
| Aktionsleiste | ||
| Add Bar | flex gap-2 pt-2 pb-1 | immer unten sichtbar, auch wenn Liste gefüllt |
| „Brief hinzufügen" Button | border border-dashed border-line rounded-sm px-3 py-1.5 text-xs font-semibold text-ink-2 hover:border-primary hover:text-primary flex items-center gap-1 | öffnet existierende DocumentPicker-Komponente als Dropdown/Modal |
| „Zwischentext hinzufügen" Button | gleich wie Brief-Button | fügt neues Interlude-Item am Ende ein; Fokus auf das neue Textarea |
| Drag-to-Reorder | ||
| Bibliothek | @dnd-kit/core oder svelte-dnd-action (bereits im Projekt prüfen) | kein neues Package ohne Absprache |
| Reorder-API-Call | PUT /api/geschichten/{id}/items/reorder — body: [{id, position}] für alle Items | nach jedem Drop ausgelöst; optimistisch: lokalen State sofort aktualisieren |
| Accessibility | Drag-Handle: role="button" tabIndex=0; Keyboard: Space startet Drag, Arrow hoch/runter verschiebt, Space/Enter bestätigt, Esc abbricht | WCAG 2.1 SC 2.1.1 |
Wenn der Nutzer auf „Notiz hinzufügen" klickt, expandiert das Item um ein Textarea direkt unterhalb der Briefmeta — kein Modal. Der Fokus springt automatisch in das Textarea. Das Textarea hat einen blauen Fokusring als Orientierungshilfe. Ein API-PATCH wird beim Verlassen des Textareas (blur) ausgelöst, nicht bei jedem Tastendruck.
Inset-Ansicht — kein vollständiger Seiten-Mockup nötig
| Element | Wert | Hinweise |
|---|---|---|
| Toggleverhalten | ||
| Lokaler State | let noteOpen = item.note !== null and item.note !== '' | öffnet sich automatisch wenn Notiz bereits vorhanden |
| „Notiz hinzufügen" Klick | noteOpen = true; tick().then(() => noteTextarea.focus()) | Fokus nach Svelte-Tick um DOM-Update abzuwarten |
| Textarea blur-Handler | on:blur={() => saveNote(item.id, note)} | PATCH /api/geschichten/{id}/items/{itemId} mit {note} |
| Leere Notiz on blur | wenn note.trim() === '' → noteOpen = false; note = null | verhindert leere Notizen im Backend |
| Fokus-Styling | ||
| Fokus-Ring | focus:border-primary focus:ring-2 focus:ring-primary/20 focus:bg-white | sichtbarer Ring für Keyboard-Navigation; ring-offset für Abstand |
| Spar-Hint | text-[9px] text-ink-3 mt-1 | „Wird gespeichert, wenn du das Feld verlässt."; verschwindet wenn noteOpen = false |
| Barrierefreiheit | ||
| aria-label Textarea | aria-label="Kuratoren-Notiz für {document.title}" | spezifisch; Screen-Reader nennt Brief-Kontext |
| aria-expanded Toggle | aria-expanded={noteOpen} auf „Notiz hinzufügen"-Button | kommuniziert Expand-State |
Auf Mobile (320px) entfällt die Sidebar-Split. Die Personen- und Status-Sektion werden als ausklappbare Sektionen unter der Itemliste gezeigt. Drag-to-Reorder ist auf Mobile durch Long-Press aktiviert. Die Aktionsleiste scrollt mit dem Inhalt.
Primäre Zielgruppe für den Editor: Desktop/Tablet. Mobile ist sekundär — alle Funktionen erreichbar, aber Drag ist schwerer bedienbar.
| Element | Wert | Hinweise |
|---|---|---|
| Layout-Anpassungen | ||
| Split entfällt | @media (max-width: 768px): flex-col; Sidebar-Sektionen als Collapsibles am Ende | gleich wie GeschichteEditor auf Mobile |
| Collapsibles | details/summary oder eigene boolean-Toggle; Personen + Status separat | geschlossen beim ersten Laden; Fokus öffnet |
| Touch & Drag | ||
| Drag auf Mobile | Long-Press (500ms) auf dem Drag-Handle aktiviert Drag | dnd-kit unterstützt Touch nativ; kein separates Config nötig |
| Touch Target Items | min-h-[44px] für jede Item-Zeile | WCAG 2.2 AA; durch Padding gesichert |
| Add-Buttons | flex-1; volle verfügbare Breite geteilt | min-h-[44px] als Touch-Target |
| Savebar | ||
| Savebar Mobile | flex gap-2; „Zurück zu Entwurf" komprimiert zu „Entwurf" | Volltext passt nicht auf 320px |
| Datei | Typ | Beschreibung |
|---|---|---|
src/lib/geschichte/JourneyEditor.svelte | Svelte-Komponente | Hauptkomponente; Props: geschichte: Geschichte |
src/lib/geschichte/JourneyItemRow.svelte | Svelte-Komponente | Eine Zeile (Dokument oder Interlude); Props: item: JourneyItem, position: number, Events: remove, noteChange |
GeschichteEditor.svelte erhält ein neues Prop type: GeschichteType.type === 'JOURNEY': rendere JourneyEditor statt TipTap-Editor. Die Sidebar (Personen, Status, Savebar) bleibt identisch.+page.svelte) verankert — JourneyEditor gibt nur Änderungen nach oben (Svelte-Events oder bindable Props), die Seite hält den Save-State.| Aktion | Endpoint | Body |
|---|---|---|
| Brief hinzufügen | POST /api/geschichten/{id}/items | {documentId: UUID} |
| Zwischentext hinzufügen | POST /api/geschichten/{id}/items | {note: string} |
| Notiz speichern/bearbeiten | PATCH /api/geschichten/{id}/items/{itemId} | {note: string | null} |
| Item entfernen | DELETE /api/geschichten/{id}/items/{itemId} | — |
| Reihenfolge speichern | PUT /api/geschichten/{id}/items/reorder | [{id: UUID, position: number}] |
aria-live="polite"-Fehlerhinweis anzeigen.DocumentPicker-Komponente (prüfe $lib/document/ auf vorhandene Typeahead-Komponenten).POST /items mit documentId, neues Item wird an das Ende der Liste angehängt und eingeblendet.@dnd-kit/core oder svelte-dnd-action bereits im package.json ist. Kein neues Package einführen ohne Absprache.[{id, position}] berechnen (position = index * 10 lässt Lücken für künftige Inserts) und PUT /items/reorder senden.<ol>-Element — kommuniziert die Ordnung an Screenreader.role="button", tabindex="0", aria-label="Reihenfolge von '{title}' ändern".aria-label="'{title}' entfernen"; kein reines ×-Zeichen ohne Label.aria-label="Kuratoren-Notiz für '{title}'".focus-visible:ring-2 focus-visible:ring-primary auf allen Buttons und Textareas.Geschichte.body dient für JOURNEY als Einleitungstext (Plaintext, kein HTML). Kein Rich-Text-Rendering auf der Leseseite nötig.