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 = `
${blocksHtml}
`; + // In Dreier-Reihen aufteilen, Snake-Direction abwechselnd + const ROW_SIZE = 3; + const rows = []; + for (let i = 0; i < visible.length; i += ROW_SIZE) { + rows.push({ + steps: visible.slice(i, i + ROW_SIZE), + direction: (rows.length % 2 === 0) ? 'ltr' : 'rtl', + }); + } + + let trackHtml = ''; + rows.forEach((row, rowIdx) => { + const isLastRow = rowIdx === rows.length - 1; + let rowHtml = `
`; + row.steps.forEach((s, i) => { + const isLastBlockOverall = isLastRow && i === row.steps.length - 1; + rowHtml += this._renderBlock(s, isLastBlockOverall); + // Inner-Pfeil zwischen Blöcken einer Reihe (nicht hinter dem letzten) + if (i < row.steps.length - 1) { + rowHtml += `
`; + } + }); + rowHtml += '
'; + trackHtml += rowHtml; + + // U-Turn-Pfeil zwischen dieser und der nächsten Reihe + if (!isLastRow) { + const lastInRow = row.steps[row.steps.length - 1]; + const side = row.direction === 'ltr' ? 'right' : 'left'; + trackHtml += this._renderUturn(side, lastInRow.key); + } + }); + + stage.innerHTML = `
${trackHtml}
`; this._bindBlockEvents(stage); }, - _renderBlock(stepDef, index, total) { + _renderBlock(stepDef, isLastOverall) { const st = this._stateByKey[stepDef.key]; const status = (st && st.status) || 'pending'; const cv = st ? st.count_value : null; const cs = st ? st.count_secondary : null; - const isLast = (index === total - 1); - const arrow = isLast ? '' : `
`; - const loopMark = isLast && this._isResearch + const loopMark = isLastOverall && this._isResearch ? `
` : ''; const icon = this._icons[stepDef.icon] || this._icons.search; @@ -219,7 +243,26 @@ const Pipeline = { ${loopMark} - ${arrow} + `; + }, + + /** U-Turn-Pfeil zwischen zwei Reihen (Snake-Übergang). */ + _renderUturn(side, fromKey) { + // SVG-Bogen + Pfeilkopf, side="right" startet rechts oben, "left" startet links oben. + // viewBox: 100 wide, 44 high + const path = side === 'right' + ? 'M 92 0 L 92 18 A 14 14 0 0 1 78 32 L 8 32' + : 'M 8 0 L 8 18 A 14 14 0 0 0 22 32 L 92 32'; + const head = side === 'right' + ? '' + : ''; + return ` + `; }, @@ -251,12 +294,12 @@ const Pipeline = { const cEl = block.querySelector('.pipeline-block-count'); if (cEl) cEl.innerHTML = this._formatCount(stepKey, cv, cs, status); - // Aktiven Pfeil zum nächsten Block markieren - const arrows = stage.querySelectorAll('.pipeline-arrow'); - arrows.forEach(a => a.classList.remove('is-flowing')); + // Aktiven Pfeil/U-Turn zum nächsten Block markieren (alles mit data-from) + stage.querySelectorAll('.pipeline-arrow, .pipeline-uturn') + .forEach(a => a.classList.remove('is-flowing')); if (status === 'done') { - const arrow = stage.querySelector(`.pipeline-arrow[data-from="${stepKey}"]`); - if (arrow) arrow.classList.add('is-flowing'); + const next = stage.querySelector(`[data-from="${stepKey}"]`); + if (next) next.classList.add('is-flowing'); } },