feat: Querverbindungen der Abhängigkeiten — gekrümmt, blassgrau, Pfeil aufs Gebrauchte (SPEC §9, D41)

Eigene Zeichenebene wie der Pfad-Spline (Overlay-SVGs, /zoom): Kanten aus
den neuen data-id/data-deps-Attributen des Renderers, dünn und blassgrau
HINTER den Knoten — das Unterscheidungsmerkmal zu den orthogonalen
Baumlinien ist die Krümmung, keine neue Farbe (die Palette ist vergeben,
Rot bewusst nicht). Pfeilspitze zeigt auf das Gebrauchte, Endpunkte auf
den Knotenkanten. Die Kanten des ausgewählten Knotens (Tastaturfokus,
sonst Cursor-Zeile) liegen hervorgehoben in Tinte auf einer vorderen
Ebene. Kanten zu verborgenen Zielen entfallen; erste ID-Vergabe gewinnt;
Selbst-Abhängigkeiten zeichnen nichts. Basis-Kanten auch im SVG-Export
(Schritt 1a) und im Druck, die Hervorhebung nicht. Browser-geprüft in
allen drei Modi; Renderer-Attribute testgedeckt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-22 19:05:33 +02:00
co-authored by Claude Fable 5
parent a42710b38f
commit afe536e33c
8 changed files with 198 additions and 10 deletions
+38
View File
@@ -1947,3 +1947,41 @@ zusätzlich einzeln anzuprangern wäre nur Lärm (SKIP-Ziel im Parser).
Einzige Ausnahme ist `%%`: Der Kommentar fällt im ganzen Dokument als Einzige Ausnahme ist `%%`: Der Kommentar fällt im ganzen Dokument als
Erstes weg (einheitliche Regel, §1) — so lassen sich auch Beschreibungen Erstes weg (einheitliche Regel, §1) — so lassen sich auch Beschreibungen
kommentieren. kommentieren.
## D41 — Querverbindungen: Krümmung statt Farbe, Pfeil auf das Gebrauchte
Die Abhängigkeits-Kanten (SPEC §9) sind die erste Linienart, die nicht der
Zerlegung folgt — §11 verlangte dafür eine eigene Zeichenebene. Gebaut wie der
Pfad-Spline (D18): Overlay-SVGs in `#out`, die den CSS-`zoom` erben, neu
gezeichnet nach Render und Moduswechsel. Die Bau-Entscheidungen:
**Das Unterscheidungsmerkmal ist die Krümmung, nicht eine neue Farbe.** Die
Palette ist vergeben: Tinte und Grau gehören den Baumlinien, Petrol der
Interaktion und dem günstigsten Pfad, `--warn` dem Geister-Knoten — und Rot
ist bewusst nicht vergeben (D34 zu LEAN-PATHFINDING). Also tragen die Kanten
dasselbe Blassgrau wie die any-of-Führung, aber **geschwungen** (quadratische
Kurve mit seitlichem Bauch): Alle Zerlegungslinien sind achsenparallel, jede
gekrümmte Linie ist damit auf einen Blick „keine Zerlegung“ — dasselbe Mittel,
mit dem sich der Pfad-Spline absetzt. Dünn (1,5 px) und blass (0,35), denn der
Baum trägt die Hauptaussage; die **Pfeilspitze zeigt auf das Gebrauchte**
(„braucht“-Richtung), Endpunkte auf den Knotenkanten statt -mitten, damit die
Spitze sichtbar bleibt.
**„Ausgewählt“ hat zwei Lesarten, beide gelten:** der Knoten mit
Tastaturfokus im Diagramm, sonst der Knoten der Cursor-Zeile (D25). Dessen
ein- und ausgehende Kanten wandern auf eine vordere Ebene in Tinte (2 px,
0,9) — vorn dürfen sie den Baum überlagern, denn die Hervorhebung ist
flüchtig und gerade angefragt. Kein Klick-Auswahlzustand: Der einfache Klick
ist der Link (§6), und Knoten fokussieren sich beim Klicken ohnehin.
**Kanten enden an sichtbaren Knoten oder gar nicht.** Ziel eingeklappt (D38)
oder als verworfen ausgeblendet → keine Kante; eine Kante zum eingeklappten
Vorfahren wäre eine falsche Aussage über das Ziel. Bei doppelter ID zielt die
Kante auf die erste Vergabe — dieselbe Auflösung wie beim effektiven Status
(D39). Die Basis-Kanten gehören in Export und Druck (Aussage über den Plan);
die Hervorhebung nicht (Interaktion). Selbst-Abhängigkeiten zeichnen keine
Kante — ein Kreis von einem Knoten zu sich selbst sagte nichts.
**Nicht durch Tests gedeckt:** `drawDepLinks()` arbeitet wie `alignStems()`
auf dem DOM; headless getestet sind die `data-id`/`data-deps`-Attribute des
Renderers, geprüft wurde im Browser (alle drei Modi, Fokus- und
Cursor-Hervorhebung, Export).
+20 -6
View File
@@ -620,6 +620,23 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
Siehe D38. Siehe D38.
### Querverbindungen der Abhängigkeiten (`:#…`, §1)
Abhängigkeiten werden als **optisch sekundäre** Kanten gezeichnet: dünn,
blassgrau (`#6B7A8C`), **geschwungen** — die Krümmung unterscheidet sie von
den orthogonalen Baumlinien (dasselbe Mittel wie beim Pfad-Spline) — auf einer
eigenen Zeichenebene **hinter** den Knoten, mit einer kleinen **Pfeilspitze
auf das Gebrauchte** („braucht“-Richtung). Der Baum trägt weiterhin die
Hauptaussage.
- Die Kanten des **ausgewählten** Knotens — Tastaturfokus im Diagramm, sonst
der Knoten der Cursor-Zeile — liegen hervorgehoben in Tinte auf einer
vorderen Ebene, ein- wie ausgehende.
- Kanten zu gerade nicht sichtbaren Knoten (eingeklappt §9, verworfen
ausgeblendet §4) entfallen; bei doppelter ID zielt die Kante auf die erste
Vergabe (D36/D39).
- Die Basis-Kanten erscheinen auch im **Grafikexport** und im **Druck**; die
Hervorhebung ist Interaktion und erscheint nicht. Siehe D41.
### Grafikexport des Diagramms ### Grafikexport des Diagramms
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen: + Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
@@ -735,12 +752,9 @@ Diskrepanz-Marke unten links). Siehe D39.
### Querverbindungen im Diagramm ### Querverbindungen im Diagramm
Abhängigkeiten werden als **optisch sekundäre** Querverbindungen gezeichnet **Umgesetzt** — siehe §9 (Querverbindungen der Abhängigkeiten): eigene
(dünn oder gestrichelt) — sie dürfen den Baum nicht überlagern, der die SVG-Zeichenebene, gekrümmte blassgraue Kanten mit Pfeilspitze hinter den
Hauptaussage trägt. Bei ausgewähltem Knoten werden dessen ein- und ausgehende Knoten, Hervorhebung am ausgewählten Knoten. Begründung: D41.
Abhängigkeiten hervorgehoben. Das ist die erste Linienart, die **nicht** der
Zerlegung folgt; sie braucht deshalb eine eigene Zeichenebene (SVG, wie der
Pfad-Spline in §9), nicht die Rahmenkanten der Knoten.
### Günstigster Pfad mit Abhängigkeiten ### Günstigster Pfad mit Abhängigkeiten
+4 -1
View File
@@ -94,8 +94,11 @@ entscheiden, **bevor** Code entsteht.
Abhängigkeiten nur einmal zählen). Erweitert D18; die gierige Wahl je Abhängigkeiten nur einmal zählen). Erweitert D18; die gierige Wahl je
Alternativgruppe ist damit nicht mehr optimal — Verfahren wählen und Alternativgruppe ist damit nicht mehr optimal — Verfahren wählen und
**benennen**. **benennen**.
- [ ] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär); - [x] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär);
bei ausgewähltem Knoten ein-/ausgehende hervorheben. bei ausgewähltem Knoten ein-/ausgehende hervorheben.
→ Umgesetzt (D41): gekrümmte blassgraue Kanten mit Pfeilspitze auf das
Gebrauchte, hinter den Knoten (`drawDepLinks`); Hervorhebung in Tinte
für Fokus-Knoten bzw. Cursor-Zeile; Basis-Kanten auch im Export/Druck.
- [x] Faltmarken `>` / `<` parsen; interaktives Auf-/Zuklappen im Diagramm. - [x] Faltmarken `>` / `<` parsen; interaktives Auf-/Zuklappen im Diagramm.
→ Schreibweise und Export-Verhalten entschieden (D34-Nachtrag): Marke → Schreibweise und Export-Verhalten entschieden (D34-Nachtrag): Marke
hinter dem Zerlegungszeichen (`- > [x] …`), Export/Druck folgen der hinter dem Zerlegungszeichen (`- > [x] …`), Export/Druck folgen der
+9
View File
@@ -292,6 +292,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Knoten-Referenzen; der Parser prüft nur Existenz (`unknownDep`), Zyklen Knoten-Referenzen; der Parser prüft nur Existenz (`unknownDep`), Zyklen
werden bewusst nicht einmal erkannt (zulässig, „gemeinsam fertig"). Keine werden bewusst nicht einmal erkannt (zulässig, „gemeinsam fertig"). Keine
Diagramm-Darstellung — nur Tooltip (`→ #a, #b`) und `a11yDeps`. Diagramm-Darstellung — nur Tooltip (`→ #a, #b`) und `a11yDeps`.
- Querverbindungen (SPEC §9, D41): `drawDepLinks()` zeichnet die
Abhängigkeits-Kanten als Overlay-SVGs (wie `drawCheapPath`, `/zoom`
umrechnen) aus den Renderer-Attributen `data-id`/`data-deps` — gekrümmt
(Merkmal gegenüber den orthogonalen Baumlinien), blassgrau hinter den
Knoten, Pfeil aufs Gebrauchte; Hervorhebung (Fokus bzw. Cursor-Zeile,
`activeDepNode()`) in Tinte auf der vorderen Ebene. Aufgerufen aus
`highlightCurrentNode()` (läuft in jedem `render()`), `applyLayout()` und
den focusin/focusout-Handlern — NICHT doppelt in `render()` einhängen.
Export zeichnet nur die Basis-Kanten (Schritt 1a).
- Effektiver Status (SPEC §4/§9, D39): `effectiveStatus(roots)` in model.js - Effektiver Status (SPEC §4/§9, D39): `effectiveStatus(roots)` in model.js
liefert NUR die Diskrepanzen (Map Knoten → effektiver Status-Key; Minimum liefert NUR die Diskrepanzen (Map Knoten → effektiver Status-Key; Minimum
des Fortschritts-Rangs über die Abhängigkeits-Hülle, Fixpunkt — Zyklen des Fortschritts-Rangs über die Abhängigkeits-Hülle, Fixpunkt — Zyklen
+104 -1
View File
@@ -127,6 +127,8 @@ function render(){
applyOptStairs(); /* muss vor dem Messen laufen — es verschiebt Knoten */ applyOptStairs(); /* muss vor dem Messen laufen — es verschiebt Knoten */
alignStems(); alignStems();
drawCheapPath(); drawCheapPath();
/* Querverbindungen (D41) zeichnet highlightCurrentNode() unten mit
es kennt die zweite Hälfte der Auswahl (Cursor-Zeile). */
/* Der Baum ist neu gebaut die Markierung der Cursor-Zeile neu setzen (D25). /* Der Baum ist neu gebaut die Markierung der Cursor-Zeile neu setzen (D25).
Ohne Scrollen: beim Tippen soll das Diagramm stehen bleiben. */ Ohne Scrollen: beim Tippen soll das Diagramm stehen bleiben. */
highlightCurrentNode(false); highlightCurrentNode(false);
@@ -277,6 +279,95 @@ function drawCheapPath(){
out.appendChild(front); out.appendChild(front);
} }
/* ---------- Querverbindungen der Abhängigkeiten (SPEC §9, D41) ----------
Die erste Linienart, die nicht der Zerlegung folgt deshalb eine eigene
Zeichenebene wie beim Pfad-Spline. Basis-Kanten dünn, blassgrau und
GEKRÜMMT (die Krümmung unterscheidet sie von den orthogonalen Baumlinien),
hinter den Knoten, mit Pfeilspitze auf das Gebrauchte. Die Kanten des
fokussierten Knotens bzw. der Cursor-Zeile liegen hervorgehoben in Tinte
auf einer vorderen Ebene. */
function depEdges(){
const byId = new Map();
out.querySelectorAll('.node[data-id]').forEach(el => {
if(!byId.has(el.dataset.id)) byId.set(el.dataset.id, el); /* erste Vergabe gewinnt (D36) */
});
const edges = [];
out.querySelectorAll('.node[data-deps]').forEach(el => {
for(const d of el.dataset.deps.split(' ')){
const target = byId.get(d);
/* verborgene (eingeklappte/ausgeblendete) oder unbekannte Ziele: keine Kante */
if(target && target !== el) edges.push([el, target]);
}
});
return edges;
}
/* Gekrümmte Kante zwischen zwei Knotenkästen: Endpunkte auf den Kanten
(Strahl von Mitte zu Mitte, am Rechteck geklippt), Kontrollpunkt senkrecht
zur Verbindung versetzt. */
function depCurve(a, b){
const dx = b.cx - a.cx, dy = b.cy - a.cy;
const len = Math.hypot(dx, dy) || 1;
const clip = (r, tx, ty) => {
const ex = tx - r.cx, ey = ty - r.cy;
const sx = ex ? (r.w/2) / Math.abs(ex) : Infinity;
const sy = ey ? (r.h/2) / Math.abs(ey) : Infinity;
const s = Math.min(sx, sy, 1);
return {x: r.cx + ex*s, y: r.cy + ey*s};
};
const p1 = clip(a, b.cx, b.cy), p2 = clip(b, a.cx, a.cy);
const bow = Math.min(40, len/4);
const ctrl = {x:(p1.x + p2.x)/2 - dy/len*bow, y:(p1.y + p2.y)/2 + dx/len*bow};
return {d:`M${p1.x.toFixed(1)},${p1.y.toFixed(1)} Q${ctrl.x.toFixed(1)},${ctrl.y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`,
end:p2, ctrl};
}
function depArrow(end, from){
const dx = end.x - from.x, dy = end.y - from.y;
const l = Math.hypot(dx, dy) || 1;
const ux = dx/l, uy = dy/l, s = 5;
const a = {x: end.x - ux*s*1.8 - uy*s, y: end.y - uy*s*1.8 + ux*s};
const b = {x: end.x - ux*s*1.8 + uy*s, y: end.y - uy*s*1.8 - ux*s};
return `M${end.x.toFixed(1)},${end.y.toFixed(1)} L${a.x.toFixed(1)},${a.y.toFixed(1)} L${b.x.toFixed(1)},${b.y.toFixed(1)} Z`;
}
/* Ausgewählt" heißt: Tastaturfokus im Diagramm, sonst der Knoten der
Cursor-Zeile (D25) beide Lesarten von ich schaue auf diesen Knoten". */
function activeDepNode(){
const f = document.activeElement;
if(f && out.contains(f) && f.closest) {
const n = f.closest('.node[data-line]');
if(n) return n;
}
return currentNodeEl;
}
function drawDepLinks(){
out.querySelectorAll('svg.dep-overlay').forEach(e => e.remove());
const edges = depEdges();
if(!edges.length) return;
const outRect = out.getBoundingClientRect();
if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */
const z = zoom || 1;
const rect = el => { const r = el.getBoundingClientRect();
return {x:(r.left - outRect.left)/z, y:(r.top - outRect.top)/z,
w:r.width/z, h:r.height/z,
cx:(r.left - outRect.left + r.width/2)/z,
cy:(r.top - outRect.top + r.height/2)/z}; };
const w = outRect.width/z, h = outRect.height/z;
const back = overlaySvg('dep-overlay dep-back', w, h);
const front = overlaySvg('dep-overlay dep-front', w, h);
const hi = activeDepNode();
for(const [from, to] of edges){
const hl = hi && (from === hi || to === hi);
const c = depCurve(rect(from), rect(to));
const layer = hl ? front : back;
layer.appendChild(svgEl('path', {class:'dep-edge' + (hl ? ' hl' : ''), d:c.d}));
layer.appendChild(svgEl('path', {class:'dep-arrow' + (hl ? ' hl' : ''), d:depArrow(c.end, c.ctrl)}));
}
out.insertBefore(back, out.firstChild);
if(front.childNodes.length) out.appendChild(front);
}
/* Hervorhebung folgt dem Fokus; die Basis-Kanten selbst ändern sich nicht. */
out.addEventListener('focusin', drawDepLinks);
out.addEventListener('focusout', () => setTimeout(drawDepLinks, 0));
/* ---------- Diagramm als Grafik (SVG → PNG) ---------- */ /* ---------- Diagramm als Grafik (SVG → PNG) ---------- */
/* Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges /* Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges
SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als
@@ -383,6 +474,14 @@ function diagramToSvg(){
}); });
}); });
/* 1a) Querverbindungen der Abhängigkeiten (D41) Basis-Kanten hinter den
Knoten; die Fokus-Hervorhebung ist Interaktion und wird nicht exportiert. */
depEdges().forEach(([from, to]) => {
const c = depCurve(R(from), R(to));
parts.push(`<path d="${c.d}" fill="none" stroke="#6B7A8C" stroke-width="1.5" opacity="0.35" stroke-linecap="round"/>`);
parts.push(`<path d="${depArrow(c.end, c.ctrl)}" fill="#6B7A8C" opacity="0.35"/>`);
});
/* 1b) Günstigster-Pfad: kräftige Linie hinter den Knoten */ /* 1b) Günstigster-Pfad: kräftige Linie hinter den Knoten */
if(cheapPts.length >= 2) parts.push(cheapLine('0.9')); if(cheapPts.length >= 2) parts.push(cheapLine('0.9'));
@@ -814,8 +913,11 @@ function highlightCurrentNode(scroll){
currentNodeEl = caretLine == null currentNodeEl = caretLine == null
? null ? null
: out.querySelector('.node[data-line="' + caretLine + '"]'); : out.querySelector('.node[data-line="' + caretLine + '"]');
if(!currentNodeEl) return; if(!currentNodeEl){ drawDepLinks(); return; }
currentNodeEl.classList.add('current'); currentNodeEl.classList.add('current');
/* Die Cursor-Zeile ist die zweite Lesart von ausgewählt" ihre
Abhängigkeits-Kanten hervorheben (D41). */
drawDepLinks();
/* Nur beim Zeilenwechsel scrollen, sonst ruckelte das Diagramm beim Tippen. */ /* Nur beim Zeilenwechsel scrollen, sonst ruckelte das Diagramm beim Tippen. */
if(scroll) currentNodeEl.scrollIntoView({block:'nearest', inline:'nearest', behavior:'smooth'}); if(scroll) currentNodeEl.scrollIntoView({block:'nearest', inline:'nearest', behavior:'smooth'});
} }
@@ -873,6 +975,7 @@ function applyLayout(mode){
applyOptStairs(); /* Treppe gilt nur im Fächer — beim Moduswechsel bauen/auflösen */ applyOptStairs(); /* Treppe gilt nur im Fächer — beim Moduswechsel bauen/auflösen */
alignStems(); /* Stiel gilt nur im Fächer — beim Moduswechsel neu setzen/löschen */ alignStems(); /* Stiel gilt nur im Fächer — beim Moduswechsel neu setzen/löschen */
drawCheapPath(); /* Blatt-Positionen ändern sich mit dem Modus */ drawCheapPath(); /* Blatt-Positionen ändern sich mit dem Modus */
drawDepLinks(); /* Knoten-Positionen ebenso (D41) */
} }
document.querySelectorAll('input[name="layout"]').forEach(radio => { document.querySelectorAll('input[name="layout"]').forEach(radio => {
radio.addEventListener('change', () => { if(radio.checked) applyLayout(radio.value); }); radio.addEventListener('change', () => { if(radio.checked) applyLayout(radio.value); });
+6 -2
View File
@@ -91,6 +91,10 @@ function nodeHtml(n, extra, opts, fold){
für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im
Tooltip macht die sonst unsichtbare Alt-Klick-Geste auffindbar. */ Tooltip macht die sonst unsichtbare Alt-Klick-Geste auffindbar. */
const lineAttr = n.line ? ` data-line="${n.line}"` : ''; const lineAttr = n.line ? ` data-line="${n.line}"` : '';
/* ID und Abhängigkeiten als data-Attribute (D41): Grundlage für die
Querverbindungs-Ebene und den Export beide arbeiten auf dem DOM. */
const idAttr = n.id ? ` data-id="${attr(n.id)}"` : '';
const depsAttr = n.deps && n.deps.length ? ` data-deps="${attr(n.deps.join(' '))}"` : '';
/* Beschreibung zuerst im Tooltip (mehrzeilig, D40), dann die Kurz-Fakten. */ /* Beschreibung zuerst im Tooltip (mehrzeilig, D40), dann die Kurz-Fakten. */
const tip = [n.desc || '', const tip = [n.desc || '',
n.id ? '#' + n.id : '', n.id ? '#' + n.id : '',
@@ -139,8 +143,8 @@ function nodeHtml(n, extra, opts, fold){
ownChip; ownChip;
const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`; const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`;
const html = n.url const html = n.url
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${aria}${expanded}${title}>${inner}</a>` ? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${idAttr}${depsAttr}${aria}${expanded}${title}>${inner}</a>`
: `<div class="${cls}" tabindex="0"${lineAttr}${aria}${expanded}${title}>${inner}</div>`; : `<div class="${cls}" tabindex="0"${lineAttr}${idAttr}${depsAttr}${aria}${expanded}${title}>${inner}</div>`;
const ghostTip = attr(t('ghostTooltip')); const ghostTip = attr(t('ghostTooltip'));
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`; const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
return html + (need ? ghost : ''); return html + (need ? ghost : '');
+8
View File
@@ -702,6 +702,14 @@
Blättern zeigen, welche Knoten wirklich auf dem Pfad enden durchquerte Blättern zeigen, welche Knoten wirklich auf dem Pfad enden durchquerte
Fremdknoten tragen keinen Punkt. Reihenfolge im DOM steuert vorn/hinten. */ Fremdknoten tragen keinen Punkt. Reihenfolge im DOM steuert vorn/hinten. */
.cheap-overlay{position:absolute;left:0;top:0;overflow:visible;pointer-events:none} .cheap-overlay{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
/* Querverbindungen der Abhängigkeiten (SPEC §9, D41): dünn, blassgrau,
gekrümmt die Krümmung unterscheidet sie von den orthogonalen Baumlinien.
Hervorgehobene Kanten (fokussierter Knoten/Cursor-Zeile) in Tinte. */
.dep-overlay{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
.dep-edge{fill:none;stroke:var(--muted);stroke-width:1.5;opacity:.35;stroke-linecap:round}
.dep-arrow{fill:var(--muted);opacity:.35}
.dep-edge.hl{stroke:var(--line);stroke-width:2;opacity:.9}
.dep-arrow.hl{fill:var(--line);opacity:.9}
.cheap-path{fill:none;stroke:var(--or);stroke-width:2.5;stroke-linecap:round; .cheap-path{fill:none;stroke:var(--or);stroke-width:2.5;stroke-linecap:round;
stroke-dasharray:8 6;opacity:.9} stroke-dasharray:8 6;opacity:.9}
.cheap-path.faint{opacity:.2} .cheap-path.faint{opacity:.2}
+9
View File
@@ -79,4 +79,13 @@ describe('Darstellung — Tooltip und aria, kein eigenes Zeichen', () => {
expect(html).not.toContain('a11yDeps'); expect(html).not.toContain('a11yDeps');
expect(html).not.toContain('→'); expect(html).not.toContain('→');
}); });
/* Grundlage der Querverbindungs-Ebene (D41): sie arbeitet auf dem DOM und
braucht ID und Abhängigkeiten als data-Attribute. */
it('schreibt data-id und data-deps an die Knoten', () => {
const {html} = render(`[ ] A #a\n[ ] B #b\n[ ] C :#a,#b`);
expect(html).toContain('data-id="a"');
expect(html).toContain('data-deps="a b"');
expect(html).not.toContain('data-deps=""');
});
}); });