diff --git a/src/static/css/style.css b/src/static/css/style.css index e56bfaf..4e1538b 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -5659,15 +5659,26 @@ body.tutorial-active .tutorial-cursor { } .pipeline-stage { position: relative; - overflow-x: auto; - overflow-y: visible; + overflow: visible; } .pipeline-track { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0; + padding: var(--sp-md) 0; + width: 100%; +} +.pipeline-row { display: flex; align-items: stretch; gap: var(--sp-md); - min-width: max-content; - padding: var(--sp-md) 0; + flex-wrap: nowrap; +} +.pipeline-row[data-direction="ltr"] { justify-content: flex-start; } +.pipeline-row[data-direction="rtl"] { + flex-direction: row-reverse; + justify-content: flex-start; } .pipeline-empty { text-align: center; @@ -5797,6 +5808,61 @@ body.tutorial-active .tutorial-cursor { to { background-position: 12px 0; } } +/* Pfeil in rtl-Reihe: Pfeilkopf nach links, Animation rückwärts */ +.pipeline-row[data-direction="rtl"] .pipeline-arrow::after { + border-left: none; + border-right: 6px solid var(--border); + right: auto; + left: -4px; +} +.pipeline-row[data-direction="rtl"] .pipeline-arrow.is-flowing::after { + border-right-color: var(--accent); + border-left-color: transparent; +} +.pipeline-row[data-direction="rtl"] .pipeline-arrow.is-flowing { + animation: pipelineFlowReverse 0.8s linear infinite; +} +@keyframes pipelineFlowReverse { + from { background-position: 12px 0; } + to { background-position: 0 0; } +} + +/* U-Turn-Pfeil zwischen Snake-Reihen */ +.pipeline-uturn { + height: 36px; + width: 100%; + margin: var(--sp-xs) 0; + pointer-events: none; + overflow: visible; +} +.pipeline-uturn svg { + width: 100%; + height: 100%; + overflow: visible; +} +.pipeline-uturn-path { + fill: none; + stroke: var(--border); + stroke-width: 2; + stroke-linecap: round; +} +.pipeline-uturn-head { + fill: none; + stroke: var(--border); + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.pipeline-uturn.is-flowing .pipeline-uturn-path { + stroke: var(--accent); + stroke-dasharray: 8 6; + animation: pipelineUturnDash 0.9s linear infinite; +} +.pipeline-uturn.is-flowing .pipeline-uturn-head { stroke: var(--accent); } +@keyframes pipelineUturnDash { + to { stroke-dashoffset: -28; } +} + .pipeline-loop { position: absolute; bottom: -10px; @@ -5928,7 +5994,14 @@ body.tutorial-active .tutorial-cursor { } @media (max-width: 900px) { - .pipeline-track { flex-direction: column; min-width: auto; align-items: stretch; } + /* Snake aufloesen — alle Reihen werden vertikal gestapelt */ + .pipeline-row, + .pipeline-row[data-direction="rtl"] { + flex-direction: column; + align-items: stretch; + } + .pipeline-uturn { display: none; } + .pipeline-block { flex: 0 0 auto; width: 100%; min-height: auto; flex-direction: row; padding: var(--sp-md); text-align: left; gap: var(--sp-md); } .pipeline-block-icon { width: 28px; height: 28px; margin-bottom: 0; flex-shrink: 0; } .pipeline-block-title { margin-bottom: 2px; } @@ -5941,8 +6014,10 @@ body.tutorial-active .tutorial-cursor { align-self: center; background: var(--border); } - .pipeline-arrow::after { + .pipeline-arrow::after, + .pipeline-row[data-direction="rtl"] .pipeline-arrow::after { right: 50%; + left: auto; top: auto; bottom: -4px; border-top: 6px solid var(--border); @@ -5951,12 +6026,18 @@ body.tutorial-active .tutorial-cursor { border-right: 4px solid transparent; transform: translateX(50%); } - .pipeline-arrow.is-flowing { + .pipeline-arrow.is-flowing, + .pipeline-row[data-direction="rtl"] .pipeline-arrow.is-flowing { background: linear-gradient(180deg, var(--accent), var(--accent) 50%, transparent 50%, transparent); background-size: 100% 12px; animation: pipelineFlowVertical 0.8s linear infinite; } - .pipeline-arrow.is-flowing::after { border-top-color: var(--accent); } + .pipeline-arrow.is-flowing::after, + .pipeline-row[data-direction="rtl"] .pipeline-arrow.is-flowing::after { + border-top-color: var(--accent); + border-right-color: transparent; + border-left-color: transparent; + } @keyframes pipelineFlowVertical { from { background-position: 0 0; } to { background-position: 0 12px; } diff --git a/src/static/js/pipeline.js b/src/static/js/pipeline.js index 613ed2d..799adaf 100644 --- a/src/static/js/pipeline.js +++ b/src/static/js/pipeline.js @@ -165,20 +165,15 @@ const Pipeline = { }, 600); }, - /** Vollbild-Pipeline (Tab "Analysepipeline") rendern. */ + /** Vollbild-Pipeline (Tab "Analysepipeline") als 3x3-Snake rendern. */ _render() { const stage = document.getElementById('pipeline-stage'); const meta = document.getElementById('pipeline-header-meta'); const sidenote = document.getElementById('pipeline-sidenote'); if (!stage) return; - // Header: letzter Refresh - if (meta) { - meta.textContent = this._formatHeader(); - } - if (sidenote) { - sidenote.hidden = !this._isResearch; - } + if (meta) meta.textContent = this._formatHeader(); + if (sidenote) sidenote.hidden = !this._isResearch; // Brandneue Lage ohne Refresh if (!this._lastRefreshHeader) { @@ -186,26 +181,55 @@ const Pipeline = { return; } - // Steps + Pfeile + // Sichtbare Blöcke (skipped komplett ausgeblendet — Anforderung 4b) const visible = (this._definition || []).filter(s => { const st = this._stateByKey[s.key]; - // Übersprungene komplett ausblenden (laut Anforderung 4b) return !st || st.status !== 'skipped'; }); - const blocksHtml = visible.map((s, i) => this._renderBlock(s, i, visible.length)).join(''); - stage.innerHTML = `