All checks were successful
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 4m2s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
nightly / deploy-staging (push) Successful in 2m44s
lesereisen-reader-spec.html — Issue #752 LR-0 type selector on /geschichten/new LR-1 REISE badge on the list LR-2 Journey reader (ordered cards, interlude asides, no position numbers) lesereisen-editor-spec.html — Issue #753 LE-1 empty JourneyEditor layout LE-2 editor with mixed items (documents + interludes, drag handles) LE-3 inline note-editing state LE-4 mobile layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
809 lines
58 KiB
HTML
809 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Lesereisen — Journey-Editor · Familienarchiv</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
||
<style>
|
||
:root{--color-page:#FAFAF7;--color-surface:#F5F4EE;--color-subtle:#EDECEA;--color-border:#D8D7D0;--color-text-muted:#6B6A63;--color-text:#1C1C18;--navy:#012851;--mint:#A1DCD8;--sand:#F0EFE9;--turquoise:#00C7B1;--blue-tint:#E6F1FB;--blue:#2D7DD2;--blue-dark:#185FA5;--green-tint:#E8F5EA;--green:#3D8C4A;--green-dark:#2E6E39;--orange-tint:#FEF0E6;--orange:#E8862A;--orange-dark:#B46820;--font-display:'Fraunces',Georgia,serif;--font-sans:'DM Sans',system-ui,sans-serif;--font-mono:'DM Mono',monospace;--radius-sm:4px;--radius-md:6px;--radius-lg:10px;--radius-xl:16px;--shadow-card:0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);--shadow-raised:0 4px 12px rgba(28,28,24,.08),0 2px 4px rgba(28,28,24,.04);--shadow-overlay:0 8px 32px rgba(28,28,24,.12),0 2px 8px rgba(28,28,24,.06);}
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||
body{font-family:var(--font-sans);background:#E8E7E2;color:var(--color-text);font-size:14px;line-height:1.6;}
|
||
.doc{max-width:1200px;margin:0 auto;padding:48px 40px 120px;}
|
||
.doc-header{display:flex;justify-content:space-between;align-items:flex-end;padding-bottom:28px;border-bottom:1px solid var(--color-border);margin-bottom:48px;background:var(--color-page);margin:-48px -40px 48px;padding:48px 40px 28px;border-radius:var(--radius-xl) var(--radius-xl) 0 0;}
|
||
.doc-header h1{font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
|
||
.doc-header p{font-size:13px;color:var(--color-text-muted);max-width:680px;}
|
||
.doc-meta{font-family:var(--font-mono);font-size:11px;color:var(--color-text-muted);text-align:right;line-height:1.9;}
|
||
.pill{display:inline-block;padding:2px 8px;border-radius:var(--radius-sm);font-size:10px;font-weight:500;letter-spacing:.05em;}
|
||
.pill-o{background:var(--orange-tint);color:var(--orange-dark);}
|
||
.section{margin-bottom:64px;}
|
||
.section-title{font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--color-text-muted);padding-bottom:10px;border-bottom:1px solid var(--color-border);margin-bottom:24px;}
|
||
.prose{font-size:13px;color:var(--color-text-muted);line-height:1.65;max-width:720px;margin-bottom:20px;}
|
||
.jh{padding:20px 24px;border-radius:var(--radius-xl);margin-bottom:40px;display:flex;align-items:center;gap:16px;}
|
||
.jh .jn{font-family:var(--font-display);font-size:48px;font-weight:300;line-height:1;opacity:.5;}
|
||
.jh h2{font-family:var(--font-display);font-size:22px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
|
||
.jh p{font-size:13px;line-height:1.5;}.jh .fl{font-family:var(--font-mono);font-size:11px;margin-top:6px;opacity:.7;}
|
||
.jh-o{background:var(--orange-tint);border:1px solid #F0C99A;}
|
||
.jh-o .jn{color:var(--orange);}
|
||
.jh-o p,.jh-o .fl{color:var(--orange-dark);}
|
||
.scr{margin-bottom:56px;}
|
||
.scr-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;}
|
||
.scr-head h3{font-family:var(--font-display);font-size:20px;font-weight:500;letter-spacing:-.02em;}
|
||
.scr-id{font-family:var(--font-mono);font-size:11px;color:var(--color-text-muted);padding:2px 8px;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-page);}
|
||
.scr-desc{font-size:12px;color:var(--color-text-muted);line-height:1.6;max-width:720px;margin-bottom:6px;}
|
||
.scr-var{font-size:11px;color:var(--color-text-muted);margin-bottom:20px;}.scr-var strong{color:var(--color-text);}
|
||
.previews{display:flex;gap:32px;flex-wrap:wrap;justify-content:center;align-items:flex-start;margin-bottom:20px;}
|
||
.prev-col{display:flex;flex-direction:column;align-items:center;gap:10px;}
|
||
.bp-lbl{font-family:var(--font-mono);font-size:10px;color:var(--color-text-muted);}
|
||
.desk{width:100%;max-width:1040px;background:var(--color-page);border-radius:var(--radius-xl);overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.06);display:flex;flex-direction:column;}
|
||
.phone{width:320px;flex-shrink:0;background:var(--color-page);border-radius:36px;overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.07);display:flex;flex-direction:column;border:6px solid #1C1C18;}
|
||
.pst{padding:10px 20px 0;display:flex;justify-content:space-between;align-items:center;font-size:12px;background:var(--color-page);}.pst b{font-weight:600;}.pst span{font-size:10px;}
|
||
.pb{flex:1;overflow-y:auto;display:flex;flex-direction:column;}
|
||
.fa-nav{height:32px;background:var(--navy);display:flex;align-items:center;padding:0 12px;gap:8px;flex-shrink:0;}
|
||
.fa-logo{font-size:7px;font-weight:900;color:#fff;letter-spacing:.8px;border-bottom:2px solid var(--mint);padding-bottom:1px;}
|
||
.fa-link{font-size:5.5px;color:rgba(255,255,255,.4);font-weight:700;text-transform:uppercase;letter-spacing:.05em;}
|
||
.fa-link.active{color:var(--mint);}
|
||
.fa-nav-r{margin-left:auto;display:flex;gap:5px;align-items:center;}
|
||
.fa-av{width:16px;height:16px;background:rgba(255,255,255,.1);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:rgba(255,255,255,.5);}
|
||
.m-nav{height:26px;background:var(--navy);display:flex;align-items:center;padding:0 10px;gap:6px;flex-shrink:0;}
|
||
.m-logo{font-size:6px;font-weight:900;color:#fff;letter-spacing:.7px;border-bottom:1.5px solid var(--mint);padding-bottom:1px;}
|
||
.m-nav-r{margin-left:auto;display:flex;gap:4px;align-items:center;}
|
||
.m-av{width:14px;height:14px;background:rgba(255,255,255,.1);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:4.5px;font-weight:800;color:rgba(255,255,255,.5);}
|
||
.m-ham{display:flex;flex-direction:column;gap:2px;width:12px;}
|
||
.m-ham span{height:1.5px;background:rgba(255,255,255,.6);border-radius:1px;}
|
||
|
||
/* ── impl-ref table ── */
|
||
.agent{background:var(--color-text);color:#E8E8E2;padding:24px;border-radius:var(--radius-lg);margin-top:20px;}
|
||
.agent h4{font-size:9px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:#5A5A55;margin-bottom:12px;}
|
||
.at{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:10px;}
|
||
.at thead tr{border-bottom:1px solid #2A2A26;}
|
||
.at th{text-align:left;padding:6px 10px;font-size:8px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:#5A5A55;font-family:var(--font-sans);}
|
||
.at td{padding:5px 10px;border-bottom:1px solid #1E1E1A;vertical-align:top;line-height:1.5;}
|
||
.at tr:last-child td{border-bottom:none;}
|
||
.at td:first-child{color:#7A7A72;}
|
||
.at td:nth-child(2){color:#E8E8E2;font-weight:500;}
|
||
.at td:nth-child(3){color:#5A5A55;}
|
||
.at .grp td{padding-top:14px;font-family:var(--font-sans);font-size:8px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:#3A3A36;}
|
||
|
||
/* ── LLM guide ── */
|
||
.llm{background:var(--color-page);border:2px solid var(--navy);border-radius:var(--radius-xl);padding:32px 40px;margin-top:64px;}
|
||
.llm h2{font-family:var(--font-display);font-size:22px;font-weight:500;letter-spacing:-.02em;margin-bottom:8px;color:var(--navy);}
|
||
.llm h3{font-size:14px;font-weight:600;margin:20px 0 8px;}
|
||
.llm h4{font-size:12px;font-weight:600;margin:14px 0 6px;color:var(--color-text-muted);}
|
||
.llm p,.llm li{font-size:13px;color:var(--color-text-muted);line-height:1.65;}
|
||
.llm ul,.llm ol{padding-left:20px;margin-bottom:12px;}
|
||
.llm li{margin-bottom:4px;}
|
||
.llm code{font-family:var(--font-mono);font-size:11px;background:var(--color-surface);padding:1px 5px;border-radius:3px;}
|
||
.llm table{width:100%;border-collapse:collapse;margin:12px 0;font-size:12px;}
|
||
.llm th,.llm td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--color-border);}
|
||
.llm th{font-weight:500;color:var(--color-text);font-size:11px;text-transform:uppercase;letter-spacing:.05em;}
|
||
.llm td{color:var(--color-text-muted);}
|
||
|
||
/* ── Editor chrome (shared with writer spec) ── */
|
||
.ed-topbar{background:#fff;border-bottom:1px solid #e4e2d7;display:flex;align-items:center;padding:0 14px;gap:8px;height:38px;flex-shrink:0;}
|
||
.ed-back{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;color:var(--color-text-muted);flex-shrink:0;}
|
||
.ed-title-label{font-family:var(--font-sans);font-size:10px;font-weight:500;color:var(--color-text);flex:1;}
|
||
.ed-status-pill{display:inline-flex;align-items:center;padding:2px 7px;border-radius:20px;font-size:8px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;flex-shrink:0;}
|
||
.ed-status-draft{background:#F0EFE9;color:#6B6A63;border:1px solid #D8D7D0;}
|
||
.ed-status-pub{background:var(--green-tint);color:var(--green-dark);border:1px solid #A0D8A8;}
|
||
.ed-delete-link{font-size:8px;font-weight:600;color:#DC4C3E;margin-left:8px;}
|
||
.ed-split{display:flex;flex:1;overflow:hidden;}
|
||
.ed-sidebar{width:210px;flex-shrink:0;border-left:1px solid #e4e2d7;background:#fff;display:flex;flex-direction:column;overflow-y:auto;}
|
||
.ed-sb-section{padding:12px 12px 10px;}
|
||
.ed-sb-title{font-size:8px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;}
|
||
.ed-sb-divider{height:1px;background:#e4e2d7;}
|
||
.ed-search-row{display:flex;align-items:center;gap:6px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:4px 8px;margin-bottom:6px;}
|
||
.ed-search-input{font-size:9px;color:var(--color-text-muted);}
|
||
.ed-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 7px;background:var(--sand);border:1px solid var(--color-border);border-radius:12px;font-size:8px;font-weight:500;color:var(--color-text);margin:0 4px 4px 0;}
|
||
.ed-chip-x{color:var(--color-text-muted);font-size:9px;cursor:pointer;margin-left:2px;}
|
||
.ed-hint{font-size:8px;color:var(--color-text-muted);line-height:1.5;margin-top:4px;}
|
||
.ed-savebar{background:#fff;border-top:1px solid #e4e2d7;padding:9px 14px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;gap:10px;}
|
||
.ed-savebar-hint{font-size:8px;color:var(--color-text-muted);}
|
||
.ed-savebar-actions{display:flex;align-items:center;gap:7px;}
|
||
.ed-btn-ghost{font-size:9px;font-weight:600;padding:5px 12px;border-radius:var(--radius-sm);border:1px solid var(--color-border);color:var(--color-text);background:#fff;cursor:pointer;white-space:nowrap;}
|
||
.ed-btn-ghost.retract{color:#B46820;border-color:#E8D5B0;}
|
||
.ed-btn-primary{font-size:9px;font-weight:600;padding:5px 12px;border-radius:var(--radius-sm);background:var(--navy);color:#fff;border:none;cursor:pointer;white-space:nowrap;}
|
||
|
||
/* ── Journey Editor main area ── */
|
||
.je-main{flex:1;display:flex;flex-direction:column;padding:14px 16px;overflow-y:auto;gap:8px;background:var(--color-page);}
|
||
.je-title-input{font-family:var(--font-display);font-size:15px;font-weight:400;color:var(--color-text);border:none;border-bottom:1px solid var(--color-border);padding:4px 0 6px;width:100%;outline:none;background:transparent;letter-spacing:-.01em;}
|
||
.je-title-input.placeholder{color:var(--color-text-muted);font-style:italic;}
|
||
.je-sep{height:1px;background:var(--color-border);margin:2px 0;}
|
||
.je-intro-area{font-family:Georgia,serif;font-size:9px;line-height:1.7;color:var(--color-text-muted);font-style:italic;border:none;padding:5px 0;width:100%;outline:none;background:transparent;min-height:36px;resize:none;}
|
||
.je-intro-label{font-size:7.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:2px;}
|
||
.je-list-label{font-size:7.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:5px;margin-top:4px;}
|
||
|
||
/* ── Item rows ── */
|
||
.je-item{display:flex;align-items:stretch;gap:0;background:#fff;border:1px solid #E4E2D7;border-radius:4px;margin-bottom:5px;overflow:hidden;}
|
||
.je-drag{width:16px;background:#F5F4EE;border-right:1px solid #E4E2D7;display:flex;align-items:center;justify-content:center;cursor:grab;flex-shrink:0;}
|
||
.je-drag-dots{display:flex;flex-direction:column;gap:2px;}
|
||
.je-drag-dot{width:3px;height:3px;border-radius:50%;background:#C4C3BC;}
|
||
.je-num{width:20px;display:flex;align-items:flex-start;justify-content:center;padding-top:8px;font-size:8px;font-weight:700;color:#9B9A93;flex-shrink:0;}
|
||
.je-body{flex:1;padding:7px 8px 7px 4px;}
|
||
.je-doc-title{font-size:9px;font-weight:600;color:var(--navy);line-height:1.3;margin-bottom:2px;}
|
||
.je-doc-meta{font-size:7.5px;color:var(--color-text-muted);margin-bottom:5px;}
|
||
.je-note-area{width:100%;min-height:32px;font-family:Georgia,serif;font-size:8px;line-height:1.55;color:var(--color-text);font-style:italic;border:1px solid var(--color-border);border-radius:3px;background:var(--color-surface);padding:4px 6px;resize:none;outline:none;}
|
||
.je-note-add{font-size:7.5px;font-weight:600;color:var(--blue);cursor:pointer;display:inline-flex;align-items:center;gap:2px;}
|
||
.je-remove{width:24px;display:flex;align-items:flex-start;justify-content:center;padding-top:7px;flex-shrink:0;}
|
||
.je-remove-x{font-size:11px;color:#C4C3BC;cursor:pointer;line-height:1;font-weight:300;}
|
||
.je-interlude-bg{background:var(--orange-tint);border-color:#F0C99A;}
|
||
.je-interlude-icon{font-size:8px;color:var(--orange);margin-bottom:2px;}
|
||
.je-interlude-area{width:100%;min-height:36px;font-family:Georgia,serif;font-size:8px;line-height:1.6;color:var(--color-text);font-style:italic;border:1px solid #F0C99A;border-radius:3px;background:rgba(255,255,255,.6);padding:4px 6px;resize:none;outline:none;}
|
||
.je-empty{padding:16px;text-align:center;border:1px dashed var(--color-border);border-radius:4px;background:var(--color-surface);}
|
||
.je-empty-text{font-family:Georgia,serif;font-size:8px;color:var(--color-text-muted);font-style:italic;}
|
||
|
||
/* ── Add bar ── */
|
||
.je-add-bar{display:flex;gap:7px;padding:6px 0 4px;}
|
||
.je-add-btn{font-size:8px;font-weight:600;padding:5px 10px;border-radius:3px;border:1px dashed var(--color-border);color:var(--color-text-muted);background:transparent;cursor:pointer;display:flex;align-items:center;gap:3px;}
|
||
.je-add-btn:hover{border-color:var(--navy);color:var(--navy);}
|
||
|
||
/* ── Inline note editing state (highlight) ── */
|
||
.je-note-editing{border-color:var(--navy);background:#fff;}
|
||
|
||
/* ── Mobile journey editor ── */
|
||
.mob-topbar{background:#fff;border-bottom:1px solid #e4e2d7;display:flex;align-items:center;padding:0 10px;gap:6px;height:34px;flex-shrink:0;}
|
||
.mob-back{font-size:8px;color:var(--color-text-muted);}
|
||
.mob-label{font-family:var(--font-sans);font-size:9px;font-weight:500;color:var(--color-text);flex:1;}
|
||
.mob-body{flex:1;overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:7px;background:var(--color-page);}
|
||
.mob-title-input{font-family:var(--font-display);font-size:13px;color:var(--color-text-muted);font-style:italic;border:none;border-bottom:1px solid var(--color-border);padding:3px 0 5px;width:100%;background:transparent;outline:none;}
|
||
.mob-collapsible{background:#fff;border:1px solid #e4e2d7;border-radius:3px;overflow:hidden;}
|
||
.mob-coll-hdr{display:flex;align-items:center;justify-content:space-between;padding:7px 9px;font-size:8.5px;font-weight:600;color:var(--color-text);}
|
||
.mob-coll-chevron{font-size:9px;color:var(--color-text-muted);}
|
||
.mob-savebar{background:#fff;border-top:1px solid #e4e2d7;padding:8px 10px;display:flex;gap:6px;flex-shrink:0;}
|
||
.mob-btn{font-size:8.5px;font-weight:600;padding:7px 0;border-radius:3px;text-align:center;flex:1;}
|
||
.mob-btn-ghost{border:1px solid var(--color-border);color:var(--color-text);background:#fff;}
|
||
.mob-btn-primary{background:var(--navy);color:#fff;border:none;}
|
||
.mob-je-item{display:flex;align-items:stretch;gap:0;background:#fff;border:1px solid #E4E2D7;border-radius:3px;margin-bottom:4px;overflow:hidden;}
|
||
.mob-je-drag{width:14px;background:#F5F4EE;border-right:1px solid #E4E2D7;display:flex;align-items:center;justify-content:center;}
|
||
.mob-je-body{flex:1;padding:6px 7px;}
|
||
.mob-je-title{font-size:8.5px;font-weight:600;color:var(--navy);line-height:1.3;margin-bottom:1px;}
|
||
.mob-je-meta{font-size:7px;color:var(--color-text-muted);}
|
||
.mob-je-note{margin-top:4px;padding:3px 5px;background:var(--color-surface);border-left:2px solid var(--mint);font-size:7.5px;font-style:italic;color:var(--color-text-muted);}
|
||
.mob-je-interlude{background:var(--orange-tint);border-color:#F0C99A;}
|
||
.mob-je-interlude-text{font-size:7.5px;font-style:italic;color:var(--color-text);}
|
||
|
||
@media(max-width:900px){.doc{padding:24px 16px 80px;}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="doc">
|
||
|
||
<!-- ═══ DOC HEADER ═══ -->
|
||
<div class="doc-header">
|
||
<div>
|
||
<h1>Lesereisen — Journey-Editor</h1>
|
||
<p>Kuratierungs-Oberfläche für <code>JourneyEditor</code> auf <code>/geschichten/[id]/edit</code> (wenn <code>type === 'JOURNEY'</code>). Geordnete Briefliste mit Drag-to-Reorder, Dokumenten-Picker, Interlude-Notizen und Inline-Annotation-Editing. Ersetzt den TipTap-Editor für den Journey-Typ.</p>
|
||
</div>
|
||
<div class="doc-meta">
|
||
Familienarchiv<br/>
|
||
<span class="pill pill-o">Final Spec</span><br/>
|
||
2026-06-07 · @leonievoss<br/>
|
||
<span style="font-size:10px;margin-top:4px;display:inline-block;">Issue #753</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ JOURNEY HEADER ═══ -->
|
||
<div class="jh jh-o">
|
||
<div class="jn">E</div>
|
||
<div>
|
||
<h2>Journey-Editor</h2>
|
||
<p>BLOG_WRITERs kuratieren eine geordnete Briefsequenz — Briefe hinzufügen, Zwischentexte einfügen, Reihenfolge per Drag anpassen, Notizen inline bearbeiten.</p>
|
||
<div class="fl">/geschichten/[id]/edit (type === 'JOURNEY')</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ KONZEPT ═══ -->
|
||
<div class="section">
|
||
<div class="section-title">Konzept</div>
|
||
<p class="prose">Der <code>JourneyEditor</code> ist eine parallele Implementierung zum bestehenden <code>GeschichteEditor</code> und wird auf derselben Edit-Route eingeblendet wenn <code>type === 'JOURNEY'</code>. Das Split-Layout (70/30) bleibt erhalten: links die Briefliste, rechts die Sidebar mit Personen und Status.</p>
|
||
<p class="prose">Die linke Fläche zeigt: oben einen optionalen Einleitungs-Textarea (<code>body</code>), 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.</p>
|
||
<p class="prose">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.</p>
|
||
<p class="prose">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.</p>
|
||
</div>
|
||
|
||
<!-- ═══ SCREEN LE-1: EMPTY EDITOR ═══ -->
|
||
<div class="section">
|
||
<div class="section-title">Screens — Leerer Editor</div>
|
||
|
||
<div class="scr">
|
||
<div class="scr-head">
|
||
<h3>LE-1 — Journey-Editor leer</h3>
|
||
<span class="scr-id">Issue #753 · LE-1</span>
|
||
</div>
|
||
<p class="scr-desc">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).</p>
|
||
<p class="scr-var"><strong>Varianten:</strong> Neuer Entwurf ohne Titel (hier gezeigt) · Mit Titel, leere Liste</p>
|
||
|
||
<div class="previews">
|
||
<div class="prev-col" style="width:100%;max-width:1040px;">
|
||
<span class="bp-lbl">Desktop — 1040px · Neuer Entwurf</span>
|
||
<div class="desk" style="min-height:500px;">
|
||
<div class="fa-nav">
|
||
<span class="fa-logo">ARCHIV</span>
|
||
<span style="width:1px;height:14px;background:rgba(255,255,255,.1);margin:0 2px;"></span>
|
||
<span class="fa-link">Dokumente</span>
|
||
<span class="fa-link">Personen</span>
|
||
<span class="fa-link active">Geschichten</span>
|
||
<span class="fa-link">Chronik</span>
|
||
<div class="fa-nav-r">
|
||
<div class="fa-av" style="background:#012851;color:var(--mint);font-size:5px;font-weight:800;">MR</div>
|
||
</div>
|
||
</div>
|
||
<div class="ed-topbar">
|
||
<div class="ed-back">←</div>
|
||
<div class="ed-title-label" style="display:flex;align-items:center;gap:6px;">
|
||
Neue Lesereise
|
||
<span style="font-size:7px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--orange-dark);background:var(--orange-tint);border:1px solid #F0C99A;padding:1px 5px;border-radius:3px;">REISE</span>
|
||
</div>
|
||
<div class="ed-status-pill ed-status-draft">ENTWURF</div>
|
||
</div>
|
||
<div class="ed-split">
|
||
<!-- Left: Journey editor area -->
|
||
<div class="je-main">
|
||
<input class="je-title-input placeholder" type="text" value="" placeholder="Titel der Lesereise" readonly/>
|
||
<div class="je-sep"></div>
|
||
<div>
|
||
<div class="je-intro-label">Einleitung (optional)</div>
|
||
<textarea class="je-intro-area" placeholder="Optionaler Einleitungstext für diese Lesereise…" readonly></textarea>
|
||
</div>
|
||
<div class="je-sep"></div>
|
||
<div class="je-list-label">Briefe & Zwischentexte</div>
|
||
<div class="je-empty">
|
||
<div class="je-empty-text">Noch keine Einträge. Füge den ersten Brief oder einen Zwischentext hinzu.</div>
|
||
</div>
|
||
<div class="je-add-bar">
|
||
<button class="je-add-btn">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Brief hinzufügen
|
||
</button>
|
||
<button class="je-add-btn">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Zwischentext hinzufügen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- Right: Sidebar -->
|
||
<div class="ed-sidebar">
|
||
<div class="ed-sb-section">
|
||
<div class="ed-sb-title">Personen</div>
|
||
<div class="ed-search-row">
|
||
<span style="font-size:9px;color:var(--color-text-muted);">🔍</span>
|
||
<div class="ed-search-input">Person suchen…</div>
|
||
</div>
|
||
<div class="ed-hint">Welche historischen Personen kommen in dieser Lesereise vor?</div>
|
||
</div>
|
||
<div class="ed-sb-divider"></div>
|
||
<div class="ed-sb-section">
|
||
<div class="ed-sb-title">Status</div>
|
||
<div class="ed-status-pill ed-status-draft" style="font-size:9px;">ENTWURF</div>
|
||
<div class="ed-hint" style="margin-top:6px;">Noch nicht öffentlich sichtbar. Füge mindestens einen Brief hinzu, um zu veröffentlichen.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ed-savebar">
|
||
<span class="ed-savebar-hint">Alle Änderungen werden als Entwurf gespeichert.</span>
|
||
<div class="ed-savebar-actions">
|
||
<button class="ed-btn-ghost">Entwurf speichern</button>
|
||
<button class="ed-btn-primary" style="opacity:.4;cursor:not-allowed;">Veröffentlichen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="agent">
|
||
<h4>impl-ref — LE-1 Leerer Editor</h4>
|
||
<table class="at">
|
||
<thead><tr><th>Element</th><th>Wert</th><th>Hinweise</th></tr></thead>
|
||
<tbody>
|
||
<tr class="grp"><td colspan="3">Seitenstruktur</td></tr>
|
||
<tr><td>Bedingte Verzweigung</td><td>{#if geschichte.type === 'JOURNEY'}<JourneyEditor />{:else}<GeschichteEditor />{/if}</td><td>in edit/+page.svelte; Props: geschichte: Geschichte</td></tr>
|
||
<tr><td>Split-Layout</td><td>flex flex-1 overflow-hidden (gleich wie GeschichteEditor)</td><td>70/30; Sidebar identisch</td></tr>
|
||
<tr><td>Topbar-Badge</td><td>„REISE" Pill neben dem Titel-Label</td><td>orange; kein interaktives Element; zeigt Typ</td></tr>
|
||
<tr class="grp"><td colspan="3">Titel-Input</td></tr>
|
||
<tr><td>Titel-Input</td><td>font-serif text-2xl border-b border-line pb-2 w-full bg-transparent outline-none</td><td>bind:value={title}; gleiche Validierung wie GeschichteEditor (required)</td></tr>
|
||
<tr class="grp"><td colspan="3">Einleitungs-Textarea</td></tr>
|
||
<tr><td>Intro-Textarea</td><td>font-serif text-sm italic text-ink-3 leading-relaxed w-full resize-none bg-transparent outline-none border-none py-1</td><td>bind:value={body}; plaintext; auto-resize per rows-attr oder JS</td></tr>
|
||
<tr><td>Label</td><td>text-[10px] font-bold uppercase tracking-widest text-ink-3 mb-1</td><td>„EINLEITUNG (OPTIONAL)"</td></tr>
|
||
<tr class="grp"><td colspan="3">Leerstate</td></tr>
|
||
<tr><td>Leerstate-Container</td><td>py-8 text-center border border-dashed border-line rounded-sm bg-surface</td><td>verschwindet sobald erstes Item vorhanden</td></tr>
|
||
<tr><td>Leerstate-Text</td><td>font-serif text-xs text-ink-3 italic</td><td></td></tr>
|
||
<tr class="grp"><td colspan="3">Veröffentlichen-Button</td></tr>
|
||
<tr><td>Disabled-Zustand</td><td>disabled={items.length === 0 || !title.trim()}</td><td>opacity-40 + cursor-not-allowed; keine Tooltip nötig — Sidebar-Hint erklärt es</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SCREEN LE-2: EDITOR WITH ITEMS ═══ -->
|
||
<div class="section">
|
||
<div class="section-title">Screens — Editor mit Einträgen</div>
|
||
|
||
<div class="scr">
|
||
<div class="scr-head">
|
||
<h3>LE-2 — Journey-Editor mit Einträgen</h3>
|
||
<span class="scr-id">Issue #753 · LE-2</span>
|
||
</div>
|
||
<p class="scr-desc">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.</p>
|
||
<p class="scr-var"><strong>Varianten:</strong> Veröffentlichte Journey (hier gezeigt) · Entwurf · Mobile</p>
|
||
|
||
<div class="previews">
|
||
<div class="prev-col" style="width:100%;max-width:1040px;">
|
||
<span class="bp-lbl">Desktop — 1040px · VERÖFFENTLICHT</span>
|
||
<div class="desk" style="min-height:580px;">
|
||
<div class="fa-nav">
|
||
<span class="fa-logo">ARCHIV</span>
|
||
<span style="width:1px;height:14px;background:rgba(255,255,255,.1);margin:0 2px;"></span>
|
||
<span class="fa-link">Dokumente</span>
|
||
<span class="fa-link">Personen</span>
|
||
<span class="fa-link active">Geschichten</span>
|
||
<div class="fa-nav-r">
|
||
<div class="fa-av" style="background:#012851;color:var(--mint);font-size:5px;font-weight:800;">KR</div>
|
||
</div>
|
||
</div>
|
||
<div class="ed-topbar">
|
||
<div class="ed-back">←</div>
|
||
<div class="ed-title-label" style="display:flex;align-items:center;gap:6px;">
|
||
Lesereise bearbeiten
|
||
<span style="font-size:7px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--orange-dark);background:var(--orange-tint);border:1px solid #F0C99A;padding:1px 5px;border-radius:3px;">REISE</span>
|
||
</div>
|
||
<div class="ed-status-pill ed-status-pub">VERÖFFENTLICHT</div>
|
||
<span class="ed-delete-link">Löschen</span>
|
||
</div>
|
||
<div class="ed-split">
|
||
<!-- Left: Journey editor area with items -->
|
||
<div class="je-main">
|
||
<input class="je-title-input" type="text" value="Briefe aus Breslau 1938–1942" readonly/>
|
||
<div class="je-sep"></div>
|
||
<div>
|
||
<div class="je-intro-label">Einleitung (optional)</div>
|
||
<textarea class="je-intro-area" readonly style="color:var(--color-text);">Der Briefwechsel zwischen Franz Raddatz und seiner Schwester Emma umspannt vier Jahre — von den letzten Friedenssommern bis zum Ende des Krieges.</textarea>
|
||
</div>
|
||
<div class="je-sep"></div>
|
||
<div class="je-list-label">Briefe & Zwischentexte</div>
|
||
|
||
<!-- Item 1: Document, no note -->
|
||
<div class="je-item">
|
||
<div class="je-drag">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="je-num">1</div>
|
||
<div class="je-body">
|
||
<div class="je-doc-title">Brief vom 12. Juli 1938</div>
|
||
<div class="je-doc-meta">12. Juli 1938 · von Franz Raddatz an Emma Müller</div>
|
||
<div class="je-note-add">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Notiz hinzufügen
|
||
</div>
|
||
</div>
|
||
<div class="je-remove"><div class="je-remove-x">×</div></div>
|
||
</div>
|
||
|
||
<!-- Item 2: Interlude -->
|
||
<div class="je-item je-interlude-bg">
|
||
<div class="je-drag" style="background:rgba(232,134,42,.08);border-right-color:#F0C99A;">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot" style="background:#D4A574;"></div><div class="je-drag-dot" style="background:#D4A574;"></div>
|
||
<div class="je-drag-dot" style="background:#D4A574;"></div><div class="je-drag-dot" style="background:#D4A574;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="je-num" style="color:var(--orange-dark);">
|
||
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" style="margin-top:7px;"><path d="M2 4h8M2 7h5" stroke="var(--orange)" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
</div>
|
||
<div class="je-body" style="padding-top:6px;">
|
||
<div style="font-size:7.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--orange-dark);margin-bottom:4px;">Zwischentext</div>
|
||
<textarea class="je-interlude-area" readonly>Im Sommer 1938 schrieb Franz voller Zuversicht — er hatte kaum eine Ahnung, wie bald sich die Welt um ihn herum verändern würde.</textarea>
|
||
</div>
|
||
<div class="je-remove"><div class="je-remove-x" style="color:#D4A574;">×</div></div>
|
||
</div>
|
||
|
||
<!-- Item 3: Document with note -->
|
||
<div class="je-item">
|
||
<div class="je-drag">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="je-num">2</div>
|
||
<div class="je-body">
|
||
<div class="je-doc-title">Postkarte aus Breslau, August 1938</div>
|
||
<div class="je-doc-meta" style="margin-bottom:5px;">22. Aug. 1938 · von Franz Raddatz an Emma Müller</div>
|
||
<textarea class="je-note-area" readonly>Diese Karte ist ungewöhnlich kurz für Franz — vier Zeilen, fast hastig. Ein Zeichen der aufkommenden Unruhe in den Nachrichten?</textarea>
|
||
<div class="je-note-add" style="margin-top:3px;color:var(--color-text-muted);">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||
Notiz entfernen
|
||
</div>
|
||
</div>
|
||
<div class="je-remove"><div class="je-remove-x">×</div></div>
|
||
</div>
|
||
|
||
<!-- Item 4: Document, no note -->
|
||
<div class="je-item">
|
||
<div class="je-drag">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="je-num">3</div>
|
||
<div class="je-body">
|
||
<div class="je-doc-title">Brief vom 3. September 1939</div>
|
||
<div class="je-doc-meta">3. Sept. 1939 · von Emma Müller an Franz Raddatz</div>
|
||
<div class="je-note-add">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Notiz hinzufügen
|
||
</div>
|
||
</div>
|
||
<div class="je-remove"><div class="je-remove-x">×</div></div>
|
||
</div>
|
||
|
||
<!-- Add bar -->
|
||
<div class="je-add-bar">
|
||
<button class="je-add-btn">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Brief hinzufügen
|
||
</button>
|
||
<button class="je-add-btn">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Zwischentext hinzufügen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Sidebar -->
|
||
<div class="ed-sidebar">
|
||
<div class="ed-sb-section">
|
||
<div class="ed-sb-title">Personen</div>
|
||
<div class="ed-search-row">
|
||
<span style="font-size:9px;color:var(--color-text-muted);">🔍</span>
|
||
<div class="ed-search-input">Person suchen…</div>
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;margin-top:4px;">
|
||
<span class="ed-chip">
|
||
<span style="width:10px;height:10px;border-radius:50%;background:#012851;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:var(--mint);">FR</span>
|
||
Franz Raddatz
|
||
<span class="ed-chip-x">×</span>
|
||
</span>
|
||
<span class="ed-chip">
|
||
<span style="width:10px;height:10px;border-radius:50%;background:#534AB7;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:#fff;">EM</span>
|
||
Emma Müller
|
||
<span class="ed-chip-x">×</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="ed-sb-divider"></div>
|
||
<div class="ed-sb-section">
|
||
<div class="ed-sb-title">Status</div>
|
||
<div class="ed-status-pill ed-status-pub" style="font-size:9px;">VERÖFFENTLICHT</div>
|
||
<div class="ed-hint" style="margin-top:6px;">Änderungen gehen sofort live.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ed-savebar">
|
||
<span class="ed-savebar-hint">Änderungen sofort live — Leser sehen die aktuelle Version.</span>
|
||
<div class="ed-savebar-actions">
|
||
<button class="ed-btn-ghost retract">Zurück zu Entwurf</button>
|
||
<button class="ed-btn-primary">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="agent">
|
||
<h4>impl-ref — LE-2 Items-Liste</h4>
|
||
<table class="at">
|
||
<thead><tr><th>Element</th><th>Wert</th><th>Hinweise</th></tr></thead>
|
||
<tbody>
|
||
<tr class="grp"><td colspan="3">Item-Zeile allgemein</td></tr>
|
||
<tr><td>Item-Container</td><td>flex items-stretch bg-white border border-line rounded-sm mb-2 overflow-hidden</td><td>interlude: bg-orange-50 border-orange-200</td></tr>
|
||
<tr><td>Drag-Handle</td><td>w-4 bg-surface border-r border-line flex items-center justify-center cursor-grab shrink-0</td><td>aria-label="Reihenfolge ändern"; cursor-grabbing während Drag</td></tr>
|
||
<tr><td>Positions-Nr.</td><td>w-5 text-[10px] font-bold text-ink-3 flex items-start justify-center pt-2 shrink-0</td><td>aus Array-Index, nicht item.position</td></tr>
|
||
<tr><td>Entfernen-Button</td><td>w-6 flex items-start justify-center pt-2 shrink-0</td><td>× aria-label="Eintrag entfernen"; hover: text-red-500; Confirm nur wenn note vorhanden</td></tr>
|
||
<tr class="grp"><td colspan="3">Dokument-Item</td></tr>
|
||
<tr><td>Brieftitel</td><td>text-[11px] font-semibold text-ink leading-snug mb-0.5</td><td>document.title</td></tr>
|
||
<tr><td>Briefmeta</td><td>text-xs text-ink-3</td><td>formatDate(doc.documentDate) · "von X" oder "von X an Y"</td></tr>
|
||
<tr><td>Notiz-Textarea (sichtbar)</td><td>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</td><td>auto-expand; bind:value={item.note}</td></tr>
|
||
<tr><td>„Notiz hinzufügen" Link</td><td>text-xs font-semibold text-blue-600 inline-flex items-center gap-1 mt-1</td><td>togglet Notiz-Textarea</td></tr>
|
||
<tr><td>„Notiz entfernen" Link</td><td>text-xs text-ink-3 inline-flex items-center gap-1 mt-1</td><td>zeigt sich wenn note.trim() nicht leer; setzt note = '' und blendet Textarea aus</td></tr>
|
||
<tr class="grp"><td colspan="3">Interlude-Item</td></tr>
|
||
<tr><td>Interlude-Container</td><td>bg-orange-50 border-orange-200 (überschreibt Item-Container)</td><td>kein Positions-Kreis; Positions-Spalte zeigt Icon statt Zahl</td></tr>
|
||
<tr><td>Label „Zwischentext"</td><td>text-[9px] font-bold uppercase tracking-widest text-orange-700 mb-1</td><td>immer sichtbar; nicht editierbar</td></tr>
|
||
<tr><td>Zwischentext-Textarea</td><td>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</td><td>bind:value={item.note}; auto-expand; min 44px für Touch-Target</td></tr>
|
||
<tr class="grp"><td colspan="3">Aktionsleiste</td></tr>
|
||
<tr><td>Add Bar</td><td>flex gap-2 pt-2 pb-1</td><td>immer unten sichtbar, auch wenn Liste gefüllt</td></tr>
|
||
<tr><td>„Brief hinzufügen" Button</td><td>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</td><td>öffnet existierende DocumentPicker-Komponente als Dropdown/Modal</td></tr>
|
||
<tr><td>„Zwischentext hinzufügen" Button</td><td>gleich wie Brief-Button</td><td>fügt neues Interlude-Item am Ende ein; Fokus auf das neue Textarea</td></tr>
|
||
<tr class="grp"><td colspan="3">Drag-to-Reorder</td></tr>
|
||
<tr><td>Bibliothek</td><td>@dnd-kit/core oder svelte-dnd-action (bereits im Projekt prüfen)</td><td>kein neues Package ohne Absprache</td></tr>
|
||
<tr><td>Reorder-API-Call</td><td>PUT /api/geschichten/{id}/items/reorder — body: [{id, position}] für alle Items</td><td>nach jedem Drop ausgelöst; optimistisch: lokalen State sofort aktualisieren</td></tr>
|
||
<tr><td>Accessibility</td><td>Drag-Handle: role="button" tabIndex=0; Keyboard: Space startet Drag, Arrow hoch/runter verschiebt, Space/Enter bestätigt, Esc abbricht</td><td>WCAG 2.1 SC 2.1.1</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SCREEN LE-3: INLINE NOTE EDITING ═══ -->
|
||
<div class="section">
|
||
<div class="section-title">Screens — Inline-Notiz-Editing</div>
|
||
|
||
<div class="scr">
|
||
<div class="scr-head">
|
||
<h3>LE-3 — Notiz-Textarea wird geöffnet</h3>
|
||
<span class="scr-id">Issue #753 · LE-3</span>
|
||
</div>
|
||
<p class="scr-desc">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.</p>
|
||
<p class="scr-var"><strong>Inset-Ansicht — kein vollständiger Seiten-Mockup nötig</strong></p>
|
||
|
||
<div class="previews">
|
||
<div class="prev-col" style="width:100%;max-width:560px;">
|
||
<span class="bp-lbl">Inset — Notiz-Textarea geöffnet (Fokus)</span>
|
||
<div style="background:#E8E7E2;padding:16px;border-radius:var(--radius-xl);">
|
||
<!-- Item before (no note) -->
|
||
<div class="je-item" style="margin-bottom:5px;">
|
||
<div class="je-drag">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="je-num">1</div>
|
||
<div class="je-body">
|
||
<div class="je-doc-title">Brief vom 12. Juli 1938</div>
|
||
<div class="je-doc-meta">12. Juli 1938 · Franz → Emma</div>
|
||
<div class="je-note-add">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M5 1v8M1 5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
Notiz hinzufügen
|
||
</div>
|
||
</div>
|
||
<div class="je-remove"><div class="je-remove-x">×</div></div>
|
||
</div>
|
||
|
||
<!-- Item with opened note textarea (focused) -->
|
||
<div class="je-item">
|
||
<div class="je-drag">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="je-num">2</div>
|
||
<div class="je-body">
|
||
<div class="je-doc-title">Postkarte aus Breslau, August 1938</div>
|
||
<div class="je-doc-meta" style="margin-bottom:5px;">22. Aug. 1938 · Franz → Emma</div>
|
||
<!-- Focused textarea -->
|
||
<textarea class="je-note-area je-note-editing" style="outline:none;box-shadow:0 0 0 2px rgba(1,40,81,.2);" readonly placeholder="Kuratoren-Notiz für diesen Brief…">|</textarea>
|
||
<div style="font-size:7px;color:var(--color-text-muted);margin-top:3px;">Wird gespeichert, wenn du das Feld verlässt.</div>
|
||
<div class="je-note-add" style="color:var(--color-text-muted);margin-top:2px;">
|
||
<svg width="7" height="7" viewBox="0 0 10 10" fill="none"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||
Notiz entfernen
|
||
</div>
|
||
</div>
|
||
<div class="je-remove"><div class="je-remove-x">×</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="agent">
|
||
<h4>impl-ref — LE-3 Inline-Notiz</h4>
|
||
<table class="at">
|
||
<thead><tr><th>Element</th><th>Wert</th><th>Hinweise</th></tr></thead>
|
||
<tbody>
|
||
<tr class="grp"><td colspan="3">Toggleverhalten</td></tr>
|
||
<tr><td>Lokaler State</td><td>let noteOpen = item.note !== null and item.note !== ''</td><td>öffnet sich automatisch wenn Notiz bereits vorhanden</td></tr>
|
||
<tr><td>„Notiz hinzufügen" Klick</td><td>noteOpen = true; tick().then(() => noteTextarea.focus())</td><td>Fokus nach Svelte-Tick um DOM-Update abzuwarten</td></tr>
|
||
<tr><td>Textarea blur-Handler</td><td>on:blur={() => saveNote(item.id, note)}</td><td>PATCH /api/geschichten/{id}/items/{itemId} mit {note}</td></tr>
|
||
<tr><td>Leere Notiz on blur</td><td>wenn note.trim() === '' → noteOpen = false; note = null</td><td>verhindert leere Notizen im Backend</td></tr>
|
||
<tr class="grp"><td colspan="3">Fokus-Styling</td></tr>
|
||
<tr><td>Fokus-Ring</td><td>focus:border-primary focus:ring-2 focus:ring-primary/20 focus:bg-white</td><td>sichtbarer Ring für Keyboard-Navigation; ring-offset für Abstand</td></tr>
|
||
<tr><td>Spar-Hint</td><td>text-[9px] text-ink-3 mt-1</td><td>„Wird gespeichert, wenn du das Feld verlässt."; verschwindet wenn noteOpen = false</td></tr>
|
||
<tr class="grp"><td colspan="3">Barrierefreiheit</td></tr>
|
||
<tr><td>aria-label Textarea</td><td>aria-label="Kuratoren-Notiz für {document.title}"</td><td>spezifisch; Screen-Reader nennt Brief-Kontext</td></tr>
|
||
<tr><td>aria-expanded Toggle</td><td>aria-expanded={noteOpen} auf „Notiz hinzufügen"-Button</td><td>kommuniziert Expand-State</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SCREEN LE-4: MOBILE ═══ -->
|
||
<div class="section">
|
||
<div class="section-title">Screens — Mobile Editor</div>
|
||
|
||
<div class="scr">
|
||
<div class="scr-head">
|
||
<h3>LE-4 — Mobile Journey-Editor</h3>
|
||
<span class="scr-id">Issue #753 · LE-4</span>
|
||
</div>
|
||
<p class="scr-desc">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.</p>
|
||
<p class="scr-var"><strong>Primäre Zielgruppe für den Editor: Desktop/Tablet. Mobile ist sekundär — alle Funktionen erreichbar, aber Drag ist schwerer bedienbar.</strong></p>
|
||
|
||
<div class="previews">
|
||
<div class="prev-col">
|
||
<span class="bp-lbl">Mobile — 320px · mit Einträgen</span>
|
||
<div class="phone" style="min-height:580px;">
|
||
<div class="pst"><b>9:41</b><span>●●●</span></div>
|
||
<div class="pb">
|
||
<div class="m-nav">
|
||
<span class="m-logo">ARCHIV</span>
|
||
<div class="m-nav-r">
|
||
<div class="m-av">KR</div>
|
||
<div class="m-ham"><span></span><span></span><span></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="mob-topbar">
|
||
<span class="mob-back">←</span>
|
||
<span class="mob-label">Lesereise bearbeiten</span>
|
||
<div class="ed-status-pill ed-status-pub" style="font-size:7px;padding:1px 5px;">VERÖFF.</div>
|
||
</div>
|
||
<div class="mob-body">
|
||
<input class="mob-title-input" type="text" value="Briefe aus Breslau 1938–1942" readonly/>
|
||
|
||
<!-- Item 1: Document -->
|
||
<div class="mob-je-item">
|
||
<div class="mob-je-drag">
|
||
<div class="je-drag-dots">
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
<div class="je-drag-dot"></div><div class="je-drag-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="mob-je-body">
|
||
<div class="mob-je-title">Brief vom 12. Juli 1938</div>
|
||
<div class="mob-je-meta">12. Juli 1938 · Franz → Emma</div>
|
||
</div>
|
||
<div style="padding:6px 6px 0 0;font-size:10px;color:#C4C3BC;">×</div>
|
||
</div>
|
||
|
||
<!-- Item 2: Interlude -->
|
||
<div class="mob-je-item mob-je-interlude">
|
||
<div class="mob-je-drag" style="background:rgba(232,134,42,.08);border-right-color:#F0C99A;"></div>
|
||
<div class="mob-je-body">
|
||
<div style="font-size:6.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--orange-dark);margin-bottom:3px;">Zwischentext</div>
|
||
<div class="mob-je-interlude-text">Im Sommer 1938 schrieb Franz voller Zuversicht…</div>
|
||
</div>
|
||
<div style="padding:6px 6px 0 0;font-size:10px;color:#D4A574;">×</div>
|
||
</div>
|
||
|
||
<!-- Item 3: Document with note -->
|
||
<div class="mob-je-item">
|
||
<div class="mob-je-drag"></div>
|
||
<div class="mob-je-body">
|
||
<div class="mob-je-title">Postkarte Aug. 1938</div>
|
||
<div class="mob-je-meta">22. Aug. 1938 · Franz → Emma</div>
|
||
<div class="mob-je-note">Diese Karte ist ungewöhnlich kurz für Franz…</div>
|
||
</div>
|
||
<div style="padding:6px 6px 0 0;font-size:10px;color:#C4C3BC;">×</div>
|
||
</div>
|
||
|
||
<!-- Add bar -->
|
||
<div style="display:flex;gap:5px;padding:4px 0;">
|
||
<button class="je-add-btn" style="flex:1;font-size:7.5px;padding:6px 8px;justify-content:center;">+ Brief</button>
|
||
<button class="je-add-btn" style="flex:1;font-size:7.5px;padding:6px 8px;justify-content:center;">+ Zwischentext</button>
|
||
</div>
|
||
|
||
<!-- Collapsible: Personen -->
|
||
<div class="mob-collapsible">
|
||
<div class="mob-coll-hdr">
|
||
Personen
|
||
<span class="mob-coll-chevron">›</span>
|
||
</div>
|
||
</div>
|
||
<!-- Collapsible: Status -->
|
||
<div class="mob-collapsible">
|
||
<div class="mob-coll-hdr">
|
||
Status & Speichern
|
||
<span class="mob-coll-chevron">›</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mob-savebar">
|
||
<button class="mob-btn mob-btn-ghost" style="font-size:8px;flex:0 0 auto;padding:7px 10px;">Entwurf</button>
|
||
<button class="mob-btn mob-btn-primary">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="agent">
|
||
<h4>impl-ref — LE-4 Mobile</h4>
|
||
<table class="at">
|
||
<thead><tr><th>Element</th><th>Wert</th><th>Hinweise</th></tr></thead>
|
||
<tbody>
|
||
<tr class="grp"><td colspan="3">Layout-Anpassungen</td></tr>
|
||
<tr><td>Split entfällt</td><td>@media (max-width: 768px): flex-col; Sidebar-Sektionen als Collapsibles am Ende</td><td>gleich wie GeschichteEditor auf Mobile</td></tr>
|
||
<tr><td>Collapsibles</td><td>details/summary oder eigene boolean-Toggle; Personen + Status separat</td><td>geschlossen beim ersten Laden; Fokus öffnet</td></tr>
|
||
<tr class="grp"><td colspan="3">Touch & Drag</td></tr>
|
||
<tr><td>Drag auf Mobile</td><td>Long-Press (500ms) auf dem Drag-Handle aktiviert Drag</td><td>dnd-kit unterstützt Touch nativ; kein separates Config nötig</td></tr>
|
||
<tr><td>Touch Target Items</td><td>min-h-[44px] für jede Item-Zeile</td><td>WCAG 2.2 AA; durch Padding gesichert</td></tr>
|
||
<tr><td>Add-Buttons</td><td>flex-1; volle verfügbare Breite geteilt</td><td>min-h-[44px] als Touch-Target</td></tr>
|
||
<tr class="grp"><td colspan="3">Savebar</td></tr>
|
||
<tr><td>Savebar Mobile</td><td>flex gap-2; „Zurück zu Entwurf" komprimiert zu „Entwurf"</td><td>Volltext passt nicht auf 320px</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ LLM IMPLEMENTATION GUIDE ═══ -->
|
||
<div class="llm">
|
||
<h2>Implementation Guide — Journey-Editor</h2>
|
||
|
||
<h3>Neue Komponente</h3>
|
||
<table>
|
||
<thead><tr><th>Datei</th><th>Typ</th><th>Beschreibung</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>src/lib/geschichte/JourneyEditor.svelte</code></td><td>Svelte-Komponente</td><td>Hauptkomponente; Props: <code>geschichte: Geschichte</code></td></tr>
|
||
<tr><td><code>src/lib/geschichte/JourneyItemRow.svelte</code></td><td>Svelte-Komponente</td><td>Eine Zeile (Dokument oder Interlude); Props: <code>item: JourneyItem, position: number</code>, Events: <code>remove, noteChange</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>Edit-Page-Integration</h3>
|
||
<ul>
|
||
<li><code>GeschichteEditor.svelte</code> erhält ein neues Prop <code>type: GeschichteType</code>.</li>
|
||
<li>Wenn <code>type === 'JOURNEY'</code>: rendere <code>JourneyEditor</code> statt TipTap-Editor. Die Sidebar (Personen, Status, Savebar) bleibt identisch.</li>
|
||
<li>Die Savebar-Logik ist in der Edit-Page (<code>+page.svelte</code>) verankert — <code>JourneyEditor</code> gibt nur Änderungen nach oben (Svelte-Events oder bindable Props), die Seite hält den Save-State.</li>
|
||
</ul>
|
||
|
||
<h3>API-Calls</h3>
|
||
<table>
|
||
<thead><tr><th>Aktion</th><th>Endpoint</th><th>Body</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Brief hinzufügen</td><td><code>POST /api/geschichten/{id}/items</code></td><td><code>{documentId: UUID}</code></td></tr>
|
||
<tr><td>Zwischentext hinzufügen</td><td><code>POST /api/geschichten/{id}/items</code></td><td><code>{note: string}</code></td></tr>
|
||
<tr><td>Notiz speichern/bearbeiten</td><td><code>PATCH /api/geschichten/{id}/items/{itemId}</code></td><td><code>{note: string | null}</code></td></tr>
|
||
<tr><td>Item entfernen</td><td><code>DELETE /api/geschichten/{id}/items/{itemId}</code></td><td>—</td></tr>
|
||
<tr><td>Reihenfolge speichern</td><td><code>PUT /api/geschichten/{id}/items/reorder</code></td><td><code>[{id: UUID, position: number}]</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>Optimistische Updates</h3>
|
||
<ul>
|
||
<li>Alle Mutationen (add, remove, reorder, noteChange) aktualisieren den lokalen State <em>sofort</em>, der API-Call läuft parallel.</li>
|
||
<li>Bei Fehler: lokalen State zurückrollen und einen <code>aria-live="polite"</code>-Fehlerhinweis anzeigen.</li>
|
||
<li>Notiz-Saving ist ein Sonderfall: es gibt kein optimistisches Update da der Wert bereits live im Textarea ist — nur blur → PATCH.</li>
|
||
</ul>
|
||
|
||
<h3>DocumentPicker-Integration</h3>
|
||
<ul>
|
||
<li>Der „Brief hinzufügen"-Button öffnet die bestehende <code>DocumentPicker</code>-Komponente (prüfe <code>$lib/document/</code> auf vorhandene Typeahead-Komponenten).</li>
|
||
<li>Nach Auswahl eines Dokuments: <code>POST /items</code> mit <code>documentId</code>, neues Item wird an das Ende der Liste angehängt und eingeblendet.</li>
|
||
<li>Bereits in der Journey enthaltene Dokumente: in der Picker-Ergebnisliste mit einem „Bereits enthalten"-Hinweis markieren und deaktivieren.</li>
|
||
</ul>
|
||
|
||
<h3>Drag-to-Reorder</h3>
|
||
<ul>
|
||
<li>Bibliothek: prüfe zunächst ob <code>@dnd-kit/core</code> oder <code>svelte-dnd-action</code> bereits im <code>package.json</code> ist. Kein neues Package einführen ohne Absprache.</li>
|
||
<li>Nach dem Drop: neue Reihenfolge als Array <code>[{id, position}]</code> berechnen (position = index * 10 lässt Lücken für künftige Inserts) und <code>PUT /items/reorder</code> senden.</li>
|
||
<li>Keyboard-Drag: Space/Enter startet, Arrow Up/Down verschiebt, Space/Enter bestätigt, Escape abbricht. Screenreader-Announcement: „Eintrag X von Position Y nach Z verschoben".</li>
|
||
</ul>
|
||
|
||
<h3>Barrierefreiheit</h3>
|
||
<ul>
|
||
<li>Items-Liste: <code><ol></code>-Element — kommuniziert die Ordnung an Screenreader.</li>
|
||
<li>Drag-Handle: <code>role="button"</code>, <code>tabindex="0"</code>, <code>aria-label="Reihenfolge von '{title}' ändern"</code>.</li>
|
||
<li>Entfernen-Button: <code>aria-label="'{title}' entfernen"</code>; kein reines ×-Zeichen ohne Label.</li>
|
||
<li>Notiz-Textarea: <code>aria-label="Kuratoren-Notiz für '{title}'"</code>.</li>
|
||
<li>Touch-Targets: alle interaktiven Elemente min 44×44px (WCAG 2.2 AA).</li>
|
||
<li>Fokusring: <code>focus-visible:ring-2 focus-visible:ring-primary</code> auf allen Buttons und Textareas.</li>
|
||
</ul>
|
||
|
||
<h3>Abgrenzung zu GeschichteEditor</h3>
|
||
<ul>
|
||
<li>TipTap wird für JOURNEY <em>nicht</em> geladen — kein unnötiger Bundle-Load.</li>
|
||
<li>Die Sidebar (Personen, Status) ist für beide Typen identisch — kein Duplikat, die Sidebar-Komponente wird geteilt.</li>
|
||
<li>Savebar-Logik (DRAFT/PUBLISHED/Retract) ist identisch — JourneyEditor ändert sie nicht.</li>
|
||
<li><code>Geschichte.body</code> dient für JOURNEY als Einleitungstext (Plaintext, kein HTML). Kein Rich-Text-Rendering auf der Leseseite nötig.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|