frontend: optionale Abzweige gestrichelt + Stiel trifft die Knotenmitte
Zwei Nachbesserungen aus dem ersten Blick auf `+` (D29). **Gestrichelter Abzweig.** Der hohle Kreis allein war zu leise. Der Einwand gegen einen dritten Linienstil bleibt richtig, greift aber nicht so weit wie gedacht: Gestrichelt wird NUR der Abzweig zum Knoten (nicht die Sammelleiste — die gehört der ganzen Gruppe) und zwar in TINTE, während die any-of-Linien gestrichelt in Grau sind. Entscheidend ist, dass beide sich nie am selben Verteiler treffen können: | darf nach SPEC §3 nicht mit -/+ gemischt werden. Der Kreis bleibt zusätzlich — er sagt, WELCHER Knoten gemeint ist. Umgesetzt an den vorhandenen Pseudoelementen, ohne neue Zeichenebene: im Fächer der senkrechte Stiel (border-left von ::after, beim letzten Kind border-right von ::before), gestapelt der waagerechte Ast (border-top von ::before); die jeweils andere Kante ist die Leiste und bleibt durchgezogen. Dafür braucht auch das <li> die Klasse `opt` — den Abzweig zeichnet es, nicht der Knoten. Der SVG-Export zieht mit (dash || isOpt). **Stiel trifft die Knotenmitte auch waagerecht (`--stem-x`).** Dabei fiel ein älterer Fehler auf, gemeldet als „die Linie zu Wahl trifft den Knoten nicht": Im horizontalen Fächer lief der Stiel zu einem Knoten mit any-of-Kindern neben dem Knoten vorbei (gemessen 13,4 px). Dieselbe Verwechslung, die D10 senkrecht schon behoben hat, nur in der anderen Achse — der Stiel saß bei 50 % der ZELLE, und das ist nur die Knotenmitte, solange der Knoten darin zentriert steht. `li.has-or` ist aber flex-start (der Knoten steht links, damit die any-of-Sammelleiste unter ihm aufsetzt), und die Zelle ist so breit wie der Teilbaum. Rein in CSS nicht lösbar: gebraucht wird die Knotenbreite, und kein Selektor macht sie einer Elternregel zugänglich (Anchor Positioning ist Chrome-only). `alignStems()` misst deshalb nach jedem render() und in applyLayout() die Knotenmitte der betroffenen Zellen und setzt `--stem-x`; die Pseudoelemente rechnen über left:var(--stem-x, 50%) bzw. right:calc(100% - var(--stem-x, 50%)). Der Rückfallwert 50 % hält alle übrigen Zellen ohne Messung richtig, die transponierten Modi setzen left/right ohnehin fest. Messwerte werden wie in drawCheapPath() durch `zoom` zurückgerechnet. Verifiziert: Vitest 60/60 (2 neue Tests: `opt` auch am <li> neben dem Gate der eigenen Kinder; <li> ohne Attribut, wenn weder Kinder noch optional). Im Browser gemessen: Abweichung Stiel↔Knotenmitte bei „Wahl" 13,4 px → 0,0 px, alle übrigen Zellen unverändert 0,0 px ohne gesetzte Variable. Angesehen in horizontal und kompakt: gestrichelter Ast in Tinte zum Kreis, Sammelleiste durchgezogen, deutlich unterscheidbar vom gestrichelt-grauen any-of-Ast daneben. SVG-Export gerendert geprüft: 2 von 9 Tinte-Linien gestrichelt, beide Kreise am Auftreffpunkt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
aa3e087ee6
commit
091e8849d8
@@ -812,3 +812,41 @@ Stärke.
|
|||||||
**Verhaltensänderung:** Ein `+` am Zeilenanfang ist jetzt ein Zeichen und
|
**Verhaltensänderung:** Ein `+` am Zeilenanfang ist jetzt ein Zeichen und
|
||||||
gehört nicht mehr zum Label (`+ 5 % Puffer` ergibt das Label „5 % Puffer").
|
gehört nicht mehr zum Label (`+ 5 % Puffer` ergibt das Label „5 % Puffer").
|
||||||
Test-abgedeckt, damit es niemanden unbemerkt trifft.
|
Test-abgedeckt, damit es niemanden unbemerkt trifft.
|
||||||
|
|
||||||
|
**Nachtrag 1 — der Abzweig wird doch gestrichelt.** Der hohle Kreis allein war
|
||||||
|
zu leise; „ein gestrichelter Ast wäre deutlicher" (Nutzer). Der oben notierte
|
||||||
|
Einwand gegen einen dritten Linienstil bleibt richtig, greift hier aber nicht
|
||||||
|
so weit wie gedacht: Gestrichelt wird **nur der Abzweig zum Knoten**, nicht die
|
||||||
|
Sammelleiste, und zwar in **Tinte** — die any-of-Linien sind gestrichelt in
|
||||||
|
**Grau**. Entscheidend ist, dass beide sich in einer Gruppe nie begegnen können:
|
||||||
|
`|` darf nach §3 nicht mit `-`/`+` gemischt werden, ein gestrichelt-grauer und
|
||||||
|
ein gestrichelt-blauer Ast hängen also nie am selben Verteiler. Der Kreis bleibt
|
||||||
|
zusätzlich — er sagt, **welcher** Knoten gemeint ist, und trägt die Kennzeichnung
|
||||||
|
auch dort, wo der Strich im Gedränge untergeht.
|
||||||
|
|
||||||
|
Umgesetzt an den vorhandenen Pseudoelementen, ohne neue Zeichenebene: Im Fächer
|
||||||
|
ist der Abzweig der senkrechte Stiel (`border-left` von `::after`, beim letzten
|
||||||
|
Kind `border-right` von `::before`), in den gestapelten Anordnungen der
|
||||||
|
waagerechte Ast (`border-top` von `::before`) — die jeweils andere Kante ist die
|
||||||
|
Leiste und bleibt durchgezogen. Dafür braucht auch das **`<li>`** die Klasse
|
||||||
|
`opt` (den Abzweig zeichnet es, nicht der Knoten).
|
||||||
|
|
||||||
|
**Nachtrag 2 — Stiel trifft die Knotenmitte auch waagerecht (`--stem-x`).**
|
||||||
|
Beim Prüfen fiel ein älterer Fehler auf: Im horizontalen Fächer lief der Stiel
|
||||||
|
zu einem Knoten mit any-of-Kindern **neben dem Knoten vorbei** (gemessen 13,4 px).
|
||||||
|
Ursache ist dieselbe Verwechslung, die D10 schon senkrecht behoben hat, nur in
|
||||||
|
der anderen Achse: Der Stiel saß bei 50 % der **Zelle**, und das ist nur dann
|
||||||
|
die Knotenmitte, wenn der Knoten in der Zelle zentriert steht. `li.has-or` ist
|
||||||
|
aber `align-items:flex-start` — der Knoten steht links, damit die
|
||||||
|
any-of-Sammelleiste unter ihm aufsetzt —, und die Zelle ist so breit wie der
|
||||||
|
Teilbaum.
|
||||||
|
|
||||||
|
Rein in CSS ist das nicht lösbar: Die nötige Größe ist die **Knotenbreite**, und
|
||||||
|
kein Selektor kann sie einer Elternregel zugänglich machen (Anchor Positioning
|
||||||
|
ist Chrome-only). Deshalb misst `alignStems()` nach jedem Rendern die Knotenmitte
|
||||||
|
der betroffenen Zellen und setzt sie als `--stem-x`; die Pseudoelemente rechnen
|
||||||
|
über `left:var(--stem-x, 50%)` / `right:calc(100% - var(--stem-x, 50%))`. Der
|
||||||
|
Rückfallwert 50 % hält alle übrigen Zellen ohne Messung richtig, und die
|
||||||
|
transponierten Modi setzen `left`/`right` ohnehin fest und bleiben unberührt.
|
||||||
|
Gemessen wird — wie in `drawCheapPath()` — durch `zoom` zurückgerechnet, sodass
|
||||||
|
der Wert beim Zoomen gültig bleibt.
|
||||||
|
|||||||
+23
-11
@@ -131,17 +131,23 @@ nebeneinander (schmales Diagramm rechts).
|
|||||||
ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die
|
ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die
|
||||||
**Anordnung**, nicht die Linienfarbe.
|
**Anordnung**, nicht die Linienfarbe.
|
||||||
|
|
||||||
**Optionale Knoten (`+`, §3):** Sie hängen an der normalen all-of-Linie —
|
**Optionale Knoten (`+`, §3):** Sie hängen an der normalen all-of-Zerlegung,
|
||||||
Anordnung und Linienstil bleiben unverändert. Gekennzeichnet wird der Knoten
|
die Anordnung bleibt unverändert. Zwei Kennzeichen, beide auch im Grafikexport:
|
||||||
selbst durch einen **kleinen hohlen Kreis** (weiß gefüllt, Rand in Tinte) genau
|
|
||||||
dort, wo der Abzweig ihn trifft: in der horizontalen Fächer-Anordnung **oben
|
- Der **Abzweig zum Knoten** ist **gestrichelt in Tinte** (`#41556E`). Nur der
|
||||||
mittig**, in den gestapelten Anordnungen (vertikal, kompakt, unterhalb einer
|
Abzweig — die **Sammelleiste bleibt durchgezogen**, sie gehört der ganzen
|
||||||
any-of-Gruppe) **links auf halber Höhe**. Übernommen aus den Feature-Diagrammen
|
Geschwistergruppe. Die any-of-Linien sind ebenfalls gestrichelt, aber in
|
||||||
(FODA: gefüllter Punkt = erforderlich, hohler Punkt = optional). Bewusst **kein
|
**Grau** (`#6B7A8C`); die Farbe hält beide auseinander, und weil `|` nicht mit
|
||||||
weiterer Linienstil**: gestrichelt gehört der any-of-Zerlegung, und im kompakten
|
`-`/`+` gemischt werden darf (§3), treffen sie in einer Gruppe nie aufeinander.
|
||||||
Modus trägt allein der Linienstil die Gate-Codierung (D15) — ein dritter Stil
|
- Am Auftreffpunkt sitzt ein **kleiner hohler Kreis** (weiß gefüllt, Rand in
|
||||||
wäre dort nicht mehr sicher unterscheidbar. Der Kreis erscheint auch im
|
Tinte): in der horizontalen Fächer-Anordnung **oben mittig**, in den
|
||||||
Grafikexport. Siehe D29.
|
gestapelten Anordnungen (vertikal, kompakt, unterhalb einer any-of-Gruppe)
|
||||||
|
**links auf halber Höhe**. Übernommen aus den Feature-Diagrammen (FODA:
|
||||||
|
gefüllter Punkt = erforderlich, hohler Punkt = optional). Er markiert
|
||||||
|
eindeutig, **welcher** Knoten optional ist — auch dort, wo der Strich allein
|
||||||
|
mit den grauen any-of-Linien verwechselt werden könnte.
|
||||||
|
|
||||||
|
Siehe D29.
|
||||||
|
|
||||||
### Horizontal (Normalmodus)
|
### Horizontal (Normalmodus)
|
||||||
- **all of:** Kinder nebeneinander, klassischer Organigramm-Fächer.
|
- **all of:** Kinder nebeneinander, klassischer Organigramm-Fächer.
|
||||||
@@ -169,6 +175,12 @@ Grafikexport. Siehe D29.
|
|||||||
deterministisch auf Knotenmitte liegen (Offset 23 px = 5 px Listenabstand
|
deterministisch auf Knotenmitte liegen (Offset 23 px = 5 px Listenabstand
|
||||||
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
|
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
|
||||||
des Teilbaums.
|
des Teilbaums.
|
||||||
|
- Dasselbe gilt **waagerecht**: Im horizontalen Fächer trifft der Stiel die
|
||||||
|
**Knotenmitte**, nicht die Mitte der Zelle. Beides fällt nur zusammen, solange
|
||||||
|
der Knoten in seiner Zelle zentriert steht — ein Knoten mit any-of-Kindern
|
||||||
|
steht dort aber **linksbündig** (damit die Sammelleiste unter ihm aufsetzt),
|
||||||
|
während seine Zelle so breit ist wie der Teilbaum. Der Stiel wird deshalb an
|
||||||
|
der gemessenen Knotenmitte ausgerichtet.
|
||||||
- In den transponierten Modi (vertikal, kompakt) stehen untereinander
|
- In den transponierten Modi (vertikal, kompakt) stehen untereinander
|
||||||
gestapelte Geschwister mit **zusätzlichem Abstand nach unten** (damit das
|
gestapelte Geschwister mit **zusätzlichem Abstand nach unten** (damit das
|
||||||
Größen-Badge oben rechts nicht mit den Tags unten rechts des darüber
|
Größen-Badge oben rechts nicht mit den Tags unten rechts des darüber
|
||||||
|
|||||||
+11
-1
@@ -192,7 +192,17 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
**oben/50 %** im horizontalen Fächer, Rück-Ausnahme wieder links für
|
**oben/50 %** im horizontalen Fächer, Rück-Ausnahme wieder links für
|
||||||
`ul.or>li.has-and>ul.and>li` (D18). Im SVG-Export muss er **nach** den Knoten
|
`ul.or>li.has-and>ul.and>li` (D18). Im SVG-Export muss er **nach** den Knoten
|
||||||
gezeichnet werden (`optMarks`, Schritt 3a) — er liegt halb außerhalb der Box
|
gezeichnet werden (`optMarks`, Schritt 3a) — er liegt halb außerhalb der Box
|
||||||
und würde sonst vom Knoten-Rechteck überdeckt.
|
und würde sonst vom Knoten-Rechteck überdeckt. Der **Abzweig** ist zusätzlich
|
||||||
|
gestrichelt (Tinte); dafür trägt auch das **`<li>`** die Klasse `opt`, denn den
|
||||||
|
Abzweig zeichnen dessen Pseudoelemente. Gestrichelt wird nur die Kante zum
|
||||||
|
Knoten, nie die Sammelleiste — im Fächer `border-left`/`-right`, gestapelt
|
||||||
|
`border-top`.
|
||||||
|
- `--stem-x` (D29, Nachtrag 2): Im horizontalen Fächer sitzt der Stiel bei 50 %
|
||||||
|
des `<li>`. Das ist nur dann die Knotenmitte, wenn der Knoten in der Zelle
|
||||||
|
zentriert steht — `li.has-or` ist aber linksbündig und die Zelle so breit wie
|
||||||
|
der any-of-Teilbaum. `alignStems()` misst deshalb nach jedem `render()` (und
|
||||||
|
in `applyLayout`) die Knotenmitte und setzt `--stem-x`; Fallback im CSS ist
|
||||||
|
`50%`. Messwerte durch `zoom` zurückrechnen, sonst stimmt es nur bei 100 %.
|
||||||
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
||||||
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
||||||
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
||||||
|
|||||||
+28
-5
@@ -84,6 +84,7 @@ function render(){
|
|||||||
|
|
||||||
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
||||||
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${formatWarning(w, t)}</div>`).join('');
|
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${formatWarning(w, t)}</div>`).join('');
|
||||||
|
alignStems();
|
||||||
drawCheapPath();
|
drawCheapPath();
|
||||||
/* 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. */
|
||||||
@@ -119,6 +120,25 @@ function overlaySvg(cls, w, h){
|
|||||||
return svgEl('svg', {class:'cheap-overlay ' + cls, width:w, height:h,
|
return svgEl('svg', {class:'cheap-overlay ' + cls, width:w, height:h,
|
||||||
viewBox:`0 0 ${w.toFixed(1)} ${h.toFixed(1)}`});
|
viewBox:`0 0 ${w.toFixed(1)} ${h.toFixed(1)}`});
|
||||||
}
|
}
|
||||||
|
/* Stielposition der all-of-Abzweige im horizontalen Fächer (siehe style.css).
|
||||||
|
Nur `li.has-or` braucht die Messung: dort steht der Knoten linksbündig, das
|
||||||
|
<li> ist aber so breit wie sein any-of-Teilbaum — der Stiel bei 50 % liefe am
|
||||||
|
Knoten vorbei. Die transponierten Modi setzen left/right ohnehin fest und
|
||||||
|
bleiben deshalb unberührt. Messwerte sind durch `zoom` skaliert und werden
|
||||||
|
wie in drawCheapPath() zurückgerechnet. */
|
||||||
|
function alignStems(){
|
||||||
|
out.querySelectorAll('ul.and>li').forEach(li => li.style.removeProperty('--stem-x'));
|
||||||
|
if(out.classList.contains('vertical') || out.classList.contains('kompakt')) return;
|
||||||
|
const z = zoom || 1;
|
||||||
|
out.querySelectorAll('ul.and>li.has-or').forEach(li => {
|
||||||
|
const node = li.querySelector(':scope > .node');
|
||||||
|
if(!node) return;
|
||||||
|
const lr = li.getBoundingClientRect(), nr = node.getBoundingClientRect();
|
||||||
|
if(!lr.width) return; /* Panel eingeklappt */
|
||||||
|
li.style.setProperty('--stem-x', ((nr.left - lr.left + nr.width/2)/z).toFixed(1) + 'px');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function drawCheapPath(){
|
function drawCheapPath(){
|
||||||
out.querySelectorAll('svg.cheap-overlay').forEach(e => e.remove());
|
out.querySelectorAll('svg.cheap-overlay').forEach(e => e.remove());
|
||||||
if(!cheapPathOn) return;
|
if(!cheapPathOn) return;
|
||||||
@@ -190,7 +210,7 @@ function diagramToSvg(){
|
|||||||
.map(cli => cli.querySelector(':scope > .node, :scope > a.node')).filter(Boolean);
|
.map(cli => cli.querySelector(':scope > .node, :scope > a.node')).filter(Boolean);
|
||||||
const kids = kidEls.map(R);
|
const kids = kidEls.map(R);
|
||||||
if(!kids.length) return;
|
if(!kids.length) return;
|
||||||
const markOpt = (i, x, y) => { if(kidEls[i].classList.contains('opt')) optMarks.push({x, y}); };
|
const isOpt = i => kidEls[i].classList.contains('opt');
|
||||||
const p = R(parentEl);
|
const p = R(parentEl);
|
||||||
const avgdx = kids.reduce((s,k)=>s+(k.cx-p.cx),0)/kids.length;
|
const avgdx = kids.reduce((s,k)=>s+(k.cx-p.cx),0)/kids.length;
|
||||||
const avgdy = kids.reduce((s,k)=>s+(k.cy-p.cy),0)/kids.length;
|
const avgdy = kids.reduce((s,k)=>s+(k.cy-p.cy),0)/kids.length;
|
||||||
@@ -202,8 +222,9 @@ function diagramToSvg(){
|
|||||||
parts.push(seg(px, p.cy, busX, p.cy, stroke, dash));
|
parts.push(seg(px, p.cy, busX, p.cy, stroke, dash));
|
||||||
parts.push(seg(busX, Math.min(...ys), busX, Math.max(...ys), stroke, dash));
|
parts.push(seg(busX, Math.min(...ys), busX, Math.max(...ys), stroke, dash));
|
||||||
kids.forEach((k, i) => {
|
kids.forEach((k, i) => {
|
||||||
parts.push(seg(busX, k.cy, toRight?k.x:k.r, k.cy, stroke, dash));
|
const x = toRight ? k.x : k.r, o = isOpt(i);
|
||||||
markOpt(i, toRight?k.x:k.r, k.cy);
|
parts.push(seg(busX, k.cy, x, k.cy, stroke, dash || o));
|
||||||
|
if(o) optMarks.push({x, y: k.cy});
|
||||||
});
|
});
|
||||||
} else { /* oben→unten */
|
} else { /* oben→unten */
|
||||||
const toDown = avgdy >= 0;
|
const toDown = avgdy >= 0;
|
||||||
@@ -213,8 +234,9 @@ function diagramToSvg(){
|
|||||||
parts.push(seg(p.cx, py, p.cx, busY, stroke, dash));
|
parts.push(seg(p.cx, py, p.cx, busY, stroke, dash));
|
||||||
parts.push(seg(Math.min(...xs), busY, Math.max(...xs), busY, stroke, dash));
|
parts.push(seg(Math.min(...xs), busY, Math.max(...xs), busY, stroke, dash));
|
||||||
kids.forEach((k, i) => {
|
kids.forEach((k, i) => {
|
||||||
parts.push(seg(k.cx, busY, k.cx, toDown?k.y:k.b, stroke, dash));
|
const y = toDown ? k.y : k.b, o = isOpt(i);
|
||||||
markOpt(i, k.cx, toDown?k.y:k.b);
|
parts.push(seg(k.cx, busY, k.cx, y, stroke, dash || o));
|
||||||
|
if(o) optMarks.push({x: k.cx, y});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -530,6 +552,7 @@ function applyLayout(mode){
|
|||||||
out.classList.toggle('kompakt', mode === 'kompakt');
|
out.classList.toggle('kompakt', mode === 'kompakt');
|
||||||
app.classList.toggle('side', mode !== 'horizontal');
|
app.classList.toggle('side', mode !== 'horizontal');
|
||||||
if(!isMobile()) applySplit(); /* Desktop: Preset neu setzen. Mobil: freie --drow-Aufteilung behalten */
|
if(!isMobile()) applySplit(); /* Desktop: Preset neu setzen. Mobil: freie --drow-Aufteilung behalten */
|
||||||
|
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 */
|
||||||
}
|
}
|
||||||
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
||||||
|
|||||||
+15
-2
@@ -23,6 +23,17 @@ function extraCls(n, opts){
|
|||||||
return [cheap, fresh, n.optional ? 'opt' : ''].filter(Boolean).join(' ');
|
return [cheap, fresh, n.optional ? 'opt' : ''].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Klassen des <li>: Gate der eigenen Kinder (steuert die Anordnung) plus
|
||||||
|
`opt`, wenn der Knoten selbst optional ist (steuert den Strich des
|
||||||
|
Abzweigs). Leere Liste ⇒ gar kein Attribut. */
|
||||||
|
function liClass(visibleKids, opts, optional){
|
||||||
|
const cls = [
|
||||||
|
visibleKids.length ? (gateOf(visibleKids) === 'or' ? 'has-or' : 'has-and') : '',
|
||||||
|
optional ? 'opt' : ''
|
||||||
|
].filter(Boolean);
|
||||||
|
return cls.length ? ` class="${cls.join(' ')}"` : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function esc(s){
|
export function esc(s){
|
||||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
}
|
}
|
||||||
@@ -98,7 +109,9 @@ function renderChildren(node, warnings, opts){
|
|||||||
const gate = gateOf(kids);
|
const gate = gateOf(kids);
|
||||||
const items = kids.map(k => {
|
const items = kids.map(k => {
|
||||||
const vk = visibleChildren(k, opts.showDiscarded);
|
const vk = visibleChildren(k, opts.showDiscarded);
|
||||||
const liCls = vk.length ? (gateOf(vk) === 'or' ? ' class="has-or"' : ' class="has-and"') : '';
|
/* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird
|
||||||
|
für optionale Knoten gestrichelt (D29). */
|
||||||
|
const liCls = liClass(vk, opts, k.optional);
|
||||||
return `<li${liCls}>` +
|
return `<li${liCls}>` +
|
||||||
nodeHtml(k, extraCls(k, opts), opts) +
|
nodeHtml(k, extraCls(k, opts), opts) +
|
||||||
renderChildren(k, warnings, opts) +
|
renderChildren(k, warnings, opts) +
|
||||||
@@ -114,7 +127,7 @@ export function renderTreeHtml(roots, opts){
|
|||||||
const warnings = [];
|
const warnings = [];
|
||||||
const html = roots.map(root => {
|
const html = roots.map(root => {
|
||||||
const vk = visibleChildren(root, opts.showDiscarded);
|
const vk = visibleChildren(root, opts.showDiscarded);
|
||||||
const liCls = vk.length ? (gateOf(vk) === 'or' ? ' class="has-or"' : ' class="has-and"') : '';
|
const liCls = liClass(vk, opts, root.optional);
|
||||||
return `<li${liCls}>` +
|
return `<li${liCls}>` +
|
||||||
nodeHtml(root, ('root-node ' + extraCls(root, opts)).trim(), opts) +
|
nodeHtml(root, ('root-node ' + extraCls(root, opts)).trim(), opts) +
|
||||||
renderChildren(root, warnings, opts) +
|
renderChildren(root, warnings, opts) +
|
||||||
|
|||||||
+27
-4
@@ -570,12 +570,19 @@
|
|||||||
content:'';position:absolute;top:0;left:50%;height:14px;border-left:2px solid var(--line);
|
content:'';position:absolute;top:0;left:50%;height:14px;border-left:2px solid var(--line);
|
||||||
}
|
}
|
||||||
ul.and>li{padding:16px 12px 0}
|
ul.and>li{padding:16px 12px 0}
|
||||||
|
/* Der Stiel liegt bei `--stem-x` — normalerweise 50 % des <li>, was die
|
||||||
|
Knotenmitte ist, solange der Knoten im <li> zentriert steht. Bei
|
||||||
|
`li.has-or` ist er das NICHT (linksbündig, damit die any-of-Leiste unter
|
||||||
|
ihm aufsetzt) und der Teilbaum macht das <li> breiter als den Knoten: der
|
||||||
|
Stiel liefe daneben vorbei. `alignStems()` in app.js misst dort die
|
||||||
|
Knotenmitte und setzt `--stem-x` — D10 in der Waagerechten. */
|
||||||
ul.and>li::before,
|
ul.and>li::before,
|
||||||
ul.and>li::after{
|
ul.and>li::after{
|
||||||
content:'';position:absolute;top:0;right:50%;
|
content:'';position:absolute;top:0;
|
||||||
border-top:2px solid var(--line);width:50%;height:16px;
|
left:0;right:calc(100% - var(--stem-x, 50%));
|
||||||
|
border-top:2px solid var(--line);height:16px;
|
||||||
}
|
}
|
||||||
ul.and>li::after{right:auto;left:50%;border-left:2px solid var(--line)}
|
ul.and>li::after{left:var(--stem-x, 50%);right:0;border-left:2px solid var(--line)}
|
||||||
ul.and>li:only-child{padding-top:14px}
|
ul.and>li:only-child{padding-top:14px}
|
||||||
ul.and>li:only-child::before,ul.and>li:only-child::after{border-top:none}
|
ul.and>li:only-child::before,ul.and>li:only-child::after{border-top:none}
|
||||||
ul.and>li:only-child::after{border-left:2px solid var(--line);height:14px}
|
ul.and>li:only-child::after{border-left:2px solid var(--line);height:14px}
|
||||||
@@ -631,7 +638,23 @@
|
|||||||
font-weight:400;
|
font-weight:400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- OPTIONAL (`+`, SPEC §3/D29): hohler Kreis am Abzweig ---
|
/* --- OPTIONAL (`+`, SPEC §3/D29): gestrichelter Abzweig + hohler Kreis ---
|
||||||
|
Gestrichelt wird NUR der Abzweig zum Knoten, nicht die Sammelleiste — die
|
||||||
|
gehört der ganzen Geschwistergruppe. Im Fächer ist der Abzweig der
|
||||||
|
senkrechte Stiel (border-left von ::after, beim letzten Kind border-right
|
||||||
|
von ::before), in den gestapelten Anordnungen der waagerechte Ast
|
||||||
|
(border-top von ::before); die Leiste ist jeweils die andere Kante.
|
||||||
|
Gestrichelt in TINTE — any-of ist gestrichelt in Grau (D15), die Farbe
|
||||||
|
hält beide auseinander, der Kreis unten bestätigt es. */
|
||||||
|
ul.and>li.opt::after{border-left-style:dashed}
|
||||||
|
ul.and>li.opt:last-child::before{border-right-style:dashed}
|
||||||
|
.tree.vertical ul.and>li.opt::before,
|
||||||
|
.tree.kompakt ul.and>li.opt::before,
|
||||||
|
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li.opt::before{
|
||||||
|
border-top-style:dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hohler Kreis am Abzweig ---
|
||||||
Kein dritter Linienstil: gestrichelt gehört any-of, und im kompakten Modus
|
Kein dritter Linienstil: gestrichelt gehört any-of, und im kompakten Modus
|
||||||
codiert allein der Stil das Gate (D15). Stattdessen die FODA-Konvention —
|
codiert allein der Stil das Gate (D15). Stattdessen die FODA-Konvention —
|
||||||
gefüllter Punkt = erforderlich (hier: gar keiner), hohler Punkt = optional.
|
gefüllter Punkt = erforderlich (hier: gar keiner), hohler Punkt = optional.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ describe('Renderer — Kennzeichnung und Gemischt-Warnung', () => {
|
|||||||
expect(html).toContain('>Zugabe<');
|
expect(html).toContain('>Zugabe<');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Der Abzweig wird von den <li>-Pseudoelementen gezeichnet — für den
|
||||||
|
gestrichelten Ast braucht das <li> die Klasse ebenfalls (D29). */
|
||||||
|
it('gibt auch dem <li> die Klasse `opt`, neben dem Gate der eigenen Kinder', () => {
|
||||||
|
const {html} = render(`[ ] Wurzel\n + [ ] Zugabe\n | [ ] A\n | [ ] B`);
|
||||||
|
expect(html).toContain('<li class="has-or opt">');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt das <li> ohne Attribut, wenn weder Kinder noch optional', () => {
|
||||||
|
const {html} = render(`[ ] Wurzel\n - [ ] Pflicht`);
|
||||||
|
expect(html).toContain('<li>');
|
||||||
|
});
|
||||||
|
|
||||||
it('warnt NICHT, wenn `-` und `+` nebeneinander stehen', () => {
|
it('warnt NICHT, wenn `-` und `+` nebeneinander stehen', () => {
|
||||||
const {warnings} = render(`[ ] Wurzel\n - [ ] Pflicht\n + [ ] Zugabe`);
|
const {warnings} = render(`[ ] Wurzel\n - [ ] Pflicht\n + [ ] Zugabe`);
|
||||||
expect(warnings).toEqual([]);
|
expect(warnings).toEqual([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user