Analysepipeline: Snake-Layout (3x3) statt linearer Reihe
Pipeline laeuft jetzt zickzack: Reihe 1 von links nach rechts, U-Turn nach unten, Reihe 2 von rechts nach links, U-Turn nach unten, Reihe 3 wieder von links nach rechts. Karte waechst auf benoetigte Hoehe statt horizontalem Scrollen. - pipeline.js: Bloecke werden in Dreier-Gruppen aufgeteilt, Direction ltr/rtl wechselt pro Reihe. Zwischen Reihen rendert ein SVG-U-Turn-Pfeil (Bogen mit Pfeilkopf) die Verbindung. Daten-Fluss-Animation (is-flowing) funktioniert sowohl auf Inner-Pfeilen als auch auf U-Turns. - CSS: .pipeline-row mit flex-direction abhaengig von data-direction. rtl-Reihen kippen Pfeilkopf und Animation in entgegengesetzte Richtung. U-Turn-Pfad als SVG mit stroke-dasharray-Animation bei aktivem Fluss. - Mobile (<900px): Snake aufgeloest, alle Reihen werden vertikal gestapelt, U-Turns ausgeblendet — bestehende Vertikal-Stilistik bleibt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -5659,15 +5659,26 @@ body.tutorial-active .tutorial-cursor {
|
|||||||
}
|
}
|
||||||
.pipeline-stage {
|
.pipeline-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: auto;
|
overflow: visible;
|
||||||
overflow-y: visible;
|
|
||||||
}
|
}
|
||||||
.pipeline-track {
|
.pipeline-track {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
padding: var(--sp-md) 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.pipeline-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--sp-md);
|
gap: var(--sp-md);
|
||||||
min-width: max-content;
|
flex-wrap: nowrap;
|
||||||
padding: var(--sp-md) 0;
|
}
|
||||||
|
.pipeline-row[data-direction="ltr"] { justify-content: flex-start; }
|
||||||
|
.pipeline-row[data-direction="rtl"] {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
.pipeline-empty {
|
.pipeline-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -5797,6 +5808,61 @@ body.tutorial-active .tutorial-cursor {
|
|||||||
to { background-position: 12px 0; }
|
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 {
|
.pipeline-loop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -10px;
|
bottom: -10px;
|
||||||
@@ -5928,7 +5994,14 @@ body.tutorial-active .tutorial-cursor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@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 { 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-icon { width: 28px; height: 28px; margin-bottom: 0; flex-shrink: 0; }
|
||||||
.pipeline-block-title { margin-bottom: 2px; }
|
.pipeline-block-title { margin-bottom: 2px; }
|
||||||
@@ -5941,8 +6014,10 @@ body.tutorial-active .tutorial-cursor {
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
}
|
}
|
||||||
.pipeline-arrow::after {
|
.pipeline-arrow::after,
|
||||||
|
.pipeline-row[data-direction="rtl"] .pipeline-arrow::after {
|
||||||
right: 50%;
|
right: 50%;
|
||||||
|
left: auto;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: -4px;
|
bottom: -4px;
|
||||||
border-top: 6px solid var(--border);
|
border-top: 6px solid var(--border);
|
||||||
@@ -5951,12 +6026,18 @@ body.tutorial-active .tutorial-cursor {
|
|||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
transform: translateX(50%);
|
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: linear-gradient(180deg, var(--accent), var(--accent) 50%, transparent 50%, transparent);
|
||||||
background-size: 100% 12px;
|
background-size: 100% 12px;
|
||||||
animation: pipelineFlowVertical 0.8s linear infinite;
|
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 {
|
@keyframes pipelineFlowVertical {
|
||||||
from { background-position: 0 0; }
|
from { background-position: 0 0; }
|
||||||
to { background-position: 0 12px; }
|
to { background-position: 0 12px; }
|
||||||
|
|||||||
@@ -165,20 +165,15 @@ const Pipeline = {
|
|||||||
}, 600);
|
}, 600);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Vollbild-Pipeline (Tab "Analysepipeline") rendern. */
|
/** Vollbild-Pipeline (Tab "Analysepipeline") als 3x3-Snake rendern. */
|
||||||
_render() {
|
_render() {
|
||||||
const stage = document.getElementById('pipeline-stage');
|
const stage = document.getElementById('pipeline-stage');
|
||||||
const meta = document.getElementById('pipeline-header-meta');
|
const meta = document.getElementById('pipeline-header-meta');
|
||||||
const sidenote = document.getElementById('pipeline-sidenote');
|
const sidenote = document.getElementById('pipeline-sidenote');
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
|
|
||||||
// Header: letzter Refresh
|
if (meta) meta.textContent = this._formatHeader();
|
||||||
if (meta) {
|
if (sidenote) sidenote.hidden = !this._isResearch;
|
||||||
meta.textContent = this._formatHeader();
|
|
||||||
}
|
|
||||||
if (sidenote) {
|
|
||||||
sidenote.hidden = !this._isResearch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Brandneue Lage ohne Refresh
|
// Brandneue Lage ohne Refresh
|
||||||
if (!this._lastRefreshHeader) {
|
if (!this._lastRefreshHeader) {
|
||||||
@@ -186,26 +181,55 @@ const Pipeline = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steps + Pfeile
|
// Sichtbare Blöcke (skipped komplett ausgeblendet — Anforderung 4b)
|
||||||
const visible = (this._definition || []).filter(s => {
|
const visible = (this._definition || []).filter(s => {
|
||||||
const st = this._stateByKey[s.key];
|
const st = this._stateByKey[s.key];
|
||||||
// Übersprungene komplett ausblenden (laut Anforderung 4b)
|
|
||||||
return !st || st.status !== 'skipped';
|
return !st || st.status !== 'skipped';
|
||||||
});
|
});
|
||||||
|
|
||||||
const blocksHtml = visible.map((s, i) => this._renderBlock(s, i, visible.length)).join('');
|
// In Dreier-Reihen aufteilen, Snake-Direction abwechselnd
|
||||||
stage.innerHTML = `<div class="pipeline-track">${blocksHtml}</div>`;
|
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 = `<div class="pipeline-row" data-direction="${row.direction}">`;
|
||||||
|
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 += `<div class="pipeline-arrow" data-from="${s.key}" data-arrow-type="inner"></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rowHtml += '</div>';
|
||||||
|
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 = `<div class="pipeline-track">${trackHtml}</div>`;
|
||||||
this._bindBlockEvents(stage);
|
this._bindBlockEvents(stage);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderBlock(stepDef, index, total) {
|
_renderBlock(stepDef, isLastOverall) {
|
||||||
const st = this._stateByKey[stepDef.key];
|
const st = this._stateByKey[stepDef.key];
|
||||||
const status = (st && st.status) || 'pending';
|
const status = (st && st.status) || 'pending';
|
||||||
const cv = st ? st.count_value : null;
|
const cv = st ? st.count_value : null;
|
||||||
const cs = st ? st.count_secondary : null;
|
const cs = st ? st.count_secondary : null;
|
||||||
const isLast = (index === total - 1);
|
const loopMark = isLastOverall && this._isResearch
|
||||||
const arrow = isLast ? '' : `<div class="pipeline-arrow" data-from="${stepDef.key}"></div>`;
|
|
||||||
const loopMark = isLast && this._isResearch
|
|
||||||
? `<div class="pipeline-loop" title="Mehrfach-Durchlauf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></svg></div>`
|
? `<div class="pipeline-loop" title="Mehrfach-Durchlauf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></svg></div>`
|
||||||
: '';
|
: '';
|
||||||
const icon = this._icons[stepDef.icon] || this._icons.search;
|
const icon = this._icons[stepDef.icon] || this._icons.search;
|
||||||
@@ -219,7 +243,26 @@ const Pipeline = {
|
|||||||
</div>
|
</div>
|
||||||
${loopMark}
|
${loopMark}
|
||||||
</div>
|
</div>
|
||||||
${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'
|
||||||
|
? '<polyline points="14,26 8,32 14,38"/>'
|
||||||
|
: '<polyline points="86,26 92,32 86,38"/>';
|
||||||
|
return `
|
||||||
|
<div class="pipeline-uturn" data-side="${side}" data-from="${fromKey}" data-arrow-type="uturn" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 100 44" preserveAspectRatio="none">
|
||||||
|
<path d="${path}" class="pipeline-uturn-path"/>
|
||||||
|
<g class="pipeline-uturn-head">${head}</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -251,12 +294,12 @@ const Pipeline = {
|
|||||||
const cEl = block.querySelector('.pipeline-block-count');
|
const cEl = block.querySelector('.pipeline-block-count');
|
||||||
if (cEl) cEl.innerHTML = this._formatCount(stepKey, cv, cs, status);
|
if (cEl) cEl.innerHTML = this._formatCount(stepKey, cv, cs, status);
|
||||||
|
|
||||||
// Aktiven Pfeil zum nächsten Block markieren
|
// Aktiven Pfeil/U-Turn zum nächsten Block markieren (alles mit data-from)
|
||||||
const arrows = stage.querySelectorAll('.pipeline-arrow');
|
stage.querySelectorAll('.pipeline-arrow, .pipeline-uturn')
|
||||||
arrows.forEach(a => a.classList.remove('is-flowing'));
|
.forEach(a => a.classList.remove('is-flowing'));
|
||||||
if (status === 'done') {
|
if (status === 'done') {
|
||||||
const arrow = stage.querySelector(`.pipeline-arrow[data-from="${stepKey}"]`);
|
const next = stage.querySelector(`[data-from="${stepKey}"]`);
|
||||||
if (arrow) arrow.classList.add('is-flowing');
|
if (next) next.classList.add('is-flowing');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren