frontend: Treppe für aufeinanderfolgende optionale Endknoten

Im horizontalen Fächer kostet jedes optionale Geschwister eine eigene Spalte —
Breite für gerade das, was am entbehrlichsten ist. Zwei oder mehr
aufeinanderfolgende optionale ENDknoten werden deshalb als Kaskade gestapelt:
erste Stufe am Stiel von oben wie ein gewöhnliches Kind, jede weitere eine
Stufe tiefer und weiter rechts, an einem gestrichelten Winkel, der an der
linken Kante der vorigen Stufe herabfällt und waagerecht in ihren Kreis
einbiegt.

Verworfen: die naheliegendere SENKRECHTE Spalte unter einem Anschlusspunkt.
Schmaler und in einem Bruchteil der Zeit gebaut — sähe aber fast genau aus wie
eine any-of-Gruppe (gestapelte Spalte an gestrichelter Leiste), unterschieden
nur durch Tinte statt Grau. Genau diese Verwechslung zu vermeiden ist der Zweck
von Kreis und Farbgebung. Ebenfalls verworfen: eine echte Diagonale —
Rahmenkanten sind achsenparallel, sie bräuchte die SVG-Ebene und wäre eine
zweite Zeichenebene neben allen anderen Linien, nachzuführen bei jedem Rendern,
Moduswechsel und Zoom. Rechte Winkel geben denselben Kaskaden-Eindruck im
vorhandenen Mechanismus.

- Nur Endknoten: Der Platzgewinn entsteht gerade daraus, dass kein Teilbaum
  mitgestapelt wird — ein optionaler Knoten MIT Kindern spart nichts und behält
  seine Spalte. Technisch dasselbe: die Stufengeometrie setzt Zellenhöhe ==
  Knotenhöhe voraus. Geprüft wird genau das (ein Element-Kind, und das ist der
  Knoten) — schließt auch den Geister-Knoten aus.
- Gruppiert wird in app.js, nicht im Renderer. Die DOM-Ebene gibt es semantisch
  nicht und müsste in den drei übrigen Anordnungen (vertikal, kompakt, all-of
  unter any-of) wieder neutralisiert werden, jede mit hand-getunter Geometrie.
  `display:contents` löst das nicht: richtet die Boxen, aber die >-Selektoren
  jener Regeln greifen weiter auf dem DOM. applyOptStairs() baut die Gruppe nur
  im Fächer und löst sie beim Moduswechsel auf — dieselbe Kategorie wie
  alignStems() und drawCheapPath(); SPEC §9 bleibt für den RENDERER wörtlich
  wahr, Lese- und Fokusreihenfolge unberührt.
- Der Export folgt der Kaskade. Erste Fassung reihte alle Stufen flach als
  Kinder ein und ließ den Export selbst routen — die Linie zur dritten Stufe
  lief dann HINTER der zweiten hindurch und las sich wie eine Eltern-Kind-
  Beziehung. Keine Schönheitsfrage, sondern eine falsche Aussage über die
  Struktur. Jetzt hängt nur die erste Stufe an der Sammelleiste.

Nicht durch Tests gedeckt: applyOptStairs() arbeitet auf dem DOM, und für
app.js gibt es keine Testumgebung (die Suite prüft die headless-Module).
Verifiziert im Browser: Kaskade in horizontal (drei Stufen, Winkel treffen die
Kreise), Moduswechsel horizontal↔kompakt↔vertikal mehrfach hin und zurück —
Gruppe entsteht und löst sich rückstandsfrei auf, Knotenzahl (9) und
Dokumentordnung in allen drei Modi unverändert, keine übrig gebliebenen
--i-Variablen. SVG-Export gerendert geprüft: drei Kreise, Winkelkette statt
kreuzender Linien. Vitest 60/60 (Renderer unverändert).

Im mitgelieferten Werkbaum-Plan stehen die beiden Editor-Zugaben jetzt
nebeneinander und zeigen die Treppe. Reihenfolgeänderung ist für „Was ist neu?"
folgenlos — die Knoten-Identität ist der Label-Pfad (D28), test-abgedeckt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-27 21:25:22 +02:00
co-authored by Claude Opus 4.8
parent 091e8849d8
commit 56c18cb8cb
6 changed files with 203 additions and 5 deletions
+12
View File
@@ -197,6 +197,18 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Abzweig zeichnen dessen Pseudoelemente. Gestrichelt wird nur die Kante zum
Knoten, nie die Sammelleiste — im Fächer `border-left`/`-right`, gestapelt
`border-top`.
- Treppe optionaler Endknoten (D29, Nachtrag 3): `applyOptStairs()` gruppiert
**in app.js**, nicht im Renderer — die DOM-Ebene `li.opt-group > ul.opt-stair`
gibt es semantisch nicht und müsste in den drei übrigen Anordnungen wieder
neutralisiert werden. Deshalb: nur im Fächer bauen, beim Moduswechsel
auflösen (die Funktion räumt immer zuerst auf, `applyLayout` arbeitet auf dem
bereits gruppierten Baum). Reihenfolge: `applyOptStairs()` **vor**
`alignStems()`/`drawCheapPath()`, es verschiebt Knoten. Bedingung ist „`<li>`
hat genau ein Element-Kind, und das ist der Knoten" — also kein Teilbaum und
kein Geister-Knoten, weil die Stufengeometrie Zellenhöhe == Knotenhöhe
voraussetzt. Der SVG-Export hängt nur die **erste** Stufe an die Leiste und
zieht die übrigen als Winkelkette nach; flach eingereiht lief die Linie zur
dritten Stufe hinter der zweiten hindurch.
- `--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