change: Falt-Voreinstellung wird ein Umschalter, Schwelle ab M abwärts (D44)

Statt eines Aufklapp-Menüs mit zwei Einträgen jetzt ein Umschalter neben
„verworfene einblenden" und „günstigster Pfad". Die beiden Einträge waren nie
unabhängige Befehle, sondern die zwei Stellungen einer Sache — jeder
beschreibt einen vollständigen Faltzustand, zusammen decken sie ihn ab.

Die Schwelle schließt M jetzt ein (atMostM statt belowM): Ein M-Paket ist
selbst noch die Einheit, die man als Ganzes plant, seine Zerlegung ist schon
das Innere. Offen bleiben L, XL, XXL — und Knoten ohne Größenangabe.

Der Zustand des Knopfes wird bei jedem Rendern am Baum abgelesen, nicht
gemerkt: Klappt jemand danach einen Knoten von Hand um, springt er von selbst
heraus, statt etwas zu behaupten, das nicht mehr stimmt. Deshalb auch nicht
persistiert — der Faltzustand steht im Text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 10:44:03 +02:00
co-authored by Claude Fable 5
parent 9bbe7d78cc
commit 417006960f
8 changed files with 124 additions and 144 deletions
+48 -32
View File
@@ -2718,22 +2718,27 @@ und der Footer-Link rückt zwischen Versionsnummer und Copyright — zur
Werkzeug-Ecke des Footers statt ans Ende hinter die Rechtstexte. Der Werkzeug-Ecke des Footers statt ans Ende hinter die Rechtstexte. Der
Dateiname bleibt als Linktext (übersetzungsfrei), die Wurzel-Lage bleibt. Dateiname bleibt als Linktext (übersetzungsfrei), die Wurzel-Lage bleibt.
## D44 — Falt-Voreinstellungen: ein Menü, „unter M“ statt einer Tiefenzahl ## D44 — Falt-Umschalter: „ab M abwärts“ statt einer Tiefenzahl
Einzeln zu falten (D38) reicht für einen Plan mit 167 Knoten nicht — man will Einzeln zu falten (D38) reicht für einen Plan mit 167 Knoten nicht — man will
den Baum am Stück auf eine Arbeitshöhe bringen. Der Diagramm-Kopf bekommt den Baum am Stück auf eine Arbeitshöhe bringen. Der Diagramm-Kopf bekommt
deshalb ein Menü mit zwei Einträgen: **alle aufklappen** und **unter Größe M dafür einen **Umschalter** neben „verworfene einblenden“ und „günstigster
zuklappen**. Pfad“: gedrückt ist alles **ab Größe M abwärts** zugeklappt, nicht gedrückt
ist der ganze Baum offen.
**Das Kriterium ist die Größe, nicht die Tiefe.** Die naheliegende Alternative **Das Kriterium ist die Größe, nicht die Tiefe.** Die naheliegende Alternative
wäre „bis Ebene n aufklappen“ gewesen. Die Ebene ist aber eine Eigenschaft der wäre „bis Ebene n aufklappen“ gewesen. Die Ebene ist aber eine Eigenschaft der
Schreibweise, nicht der Sache: Wie tief etwas steht, hängt davon ab, wie fein Schreibweise, nicht der Sache: Wie tief etwas steht, hängt davon ab, wie fein
der Autor zerlegt hat, und ein sorgfältig aufgeschlüsselter Zweig verschwände der Autor zerlegt hat, und ein sorgfältig aufgeschlüsselter Zweig verschwände
bei derselben Zahl früher als ein grob notierter. Die T-Shirt-Größe sagt bei derselben Zahl früher als ein grob notierter. Die T-Shirt-Größe sagt
dagegen etwas über die **Arbeit**, und die Notation hat dafür bereits eine dagegen etwas über die **Arbeit** und weil `M` die kleinste Größe ist, die
Schwelle: Ab `M` **muss** ein Element zerlegt sein (§5/D8). „Unter M“ ist also überhaupt eine Zerlegung verlangt (§5/D8), ist „M und kleiner“ genau die
genau die Menge, die keine Zerlegung mehr braucht — was darunter hängt, ist Menge, deren Inneres Detail ist. Übrig bleibt der Plan auf der Höhe von `L`
Detail. Die Voreinstellung zeigt damit den Plan auf der Höhe, auf der noch aufwärts, also dort, wo die großen Brocken stehen.
Entscheidungen anstehen.
*(Zuerst gebaut war die Schwelle **unter** M — offen blieb dann auch jedes
`M`. Der Nutzer hat sie auf `M` einschließlich korrigiert: Ein M-Paket ist
selbst noch die Einheit, die man als Ganzes plant; seine Zerlegung ist schon
das Innere.)*
**Ohne Größenangabe wird nicht zugeklappt.** Der günstigste Pfad wertet eine **Ohne Größenangabe wird nicht zugeklappt.** Der günstigste Pfad wertet eine
fehlende Größe als `M` (D18) — das wäre hier der falsche Präzedenzfall: Dort fehlende Größe als `M` (D18) — das wäre hier der falsche Präzedenzfall: Dort
@@ -2743,15 +2748,34 @@ nichts gesagt; einen Zweig deswegen zu verbergen, behandelte eine Vermutung
wie eine Angabe. Ein Knoten ohne Größe bleibt also offen — und fällt dadurch wie eine Angabe. Ein Knoten ohne Größe bleibt also offen — und fällt dadurch
angenehmerweise auf. angenehmerweise auf.
**Beide Einträge beschreiben einen vollständigen Zustand.** „Unter M **Ein Umschalter, kein Menü.** Zuerst gebaut war ein Aufklapp-Menü mit zwei
zuklappen“ klappt nicht nur zu, es klappt alles **andere auf**. Sonst wäre die Einträgen („alle aufklappen“ / „zuklappen“); der Nutzer wollte einen
Wirkung vom Vorzustand abhängig und zweimal Drücken ergäbe zweierlei — für Umschalter. Das ist die bessere Form, und der Grund ist nicht nur Platz: Die
eine Voreinstellung die falsche Eigenschaft. Der Preis: Ein von Hand gesetztes beiden Einträge waren nie unabhängige Befehle, sondern die **zwei Stellungen
`>` an einem großen Knoten wird dabei aufgelöst. Das ist vertretbar, weil der einer Sache** — jeder beschreibt einen vollständigen Faltzustand, und
Nutzer gerade ausdrücklich eine Gesamtansicht angefordert hat, und weil zusammen decken sie ihn ganz ab. Genau das ist ein Umschalter. Ein Menü
**Rückgängig** den ganzen Vorgang in einem Schritt zurücknimmt (nachgemessen: verlangte zwei Klicks für etwas, das einen braucht, und der Diagramm-Kopf hat
`replaceTextUndoable` schreibt genau einmal, Strg+Z stellt Text und Diagramm das Idiom längst zweimal (D18, D4).
wieder her).
**Der Zustand wird abgelesen, nicht gemerkt.** Ein gemerktes Flag würde lügen,
sobald jemand danach einen einzelnen Knoten umklappt. Stattdessen prüft
`render()` bei jedem Durchlauf, ob **jeder** faltbare Knoten so steht, wie die
Voreinstellung ihn stellen würde; nur dann ist der Knopf gedrückt. Er springt
damit von selbst heraus, wenn man von Hand etwas aufklappt, und von selbst
wieder hinein, wenn man es zurücknimmt (beides nachgemessen). Deshalb wird er
auch **nicht** in `werkbaum-ui` persistiert: Der Faltzustand steht im Text
(D38-Nachtrag 2), der Knopf liest ihn nur ab. Sonderfall: Gäbe es gar keinen
Knoten bis `M`, wären beide Stellungen derselbe Zustand — dann bleibt der
Knopf heraußen, statt gedrückt dazusitzen, ohne dass etwas zugeklappt ist.
**Beide Stellungen beschreiben einen vollständigen Zustand.** Zuklappen klappt
also alles **andere auf**. Sonst wäre die Wirkung vom Vorzustand abhängig und
zweimal Drücken ergäbe zweierlei — für einen Umschalter die falsche
Eigenschaft. Der Preis: Ein von Hand gesetztes `>` an einem großen Knoten wird
dabei aufgelöst. Vertretbar, weil der Nutzer gerade ausdrücklich eine
Gesamtansicht angefordert hat, und weil **Rückgängig** den ganzen Vorgang in
einem Schritt zurücknimmt (nachgemessen: `replaceTextUndoable` schreibt genau
einmal, Strg+Z stellt Text und Diagramm wieder her).
**Umgesetzt über denselben Weg wie das einzelne Umklappen** — die **Umgesetzt über denselben Weg wie das einzelne Umklappen** — die
Sitzungs-Überlagerungen setzen, dann in den Text schreiben (D38-Nachtrag 2). Sitzungs-Überlagerungen setzen, dann in den Text schreiben (D38-Nachtrag 2).
@@ -2762,18 +2786,10 @@ Rückfall-Kette unverändert — ist der Zustand in Marken **nicht ausdrückbar*
wird nicht geschrieben und die Überlagerung trägt ihn für die Sitzung. wird nicht geschrieben und die Überlagerung trägt ihn für die Sitzung.
Genau der Fall tritt im mitgelieferten Werkbaum-Plan auf und ist beim Bauen Genau der Fall tritt im mitgelieferten Werkbaum-Plan auf und ist beim Bauen
nachgemessen worden: Zwei Knoten sind kleiner als `M` und haben Kinder nachgemessen worden: In Zeile 103 steht eine Fokusmarke `!!!` unterhalb eines
(Zeile 94 und 100), aber in Zeile 103 steht eine Fokusmarke `!!!` unterhalb Knotens, der zugeklappt werden soll — die holt ihren Knoten immer wieder
von 94 — die holt ihren Knoten immer wieder hervor, „94 eingeklappt“ ist als hervor, dieser Markensatz ist also nicht schreibbar. Das Bild stimmt trotzdem
Markensatz also nicht schreibbar. Das Bild stimmt trotzdem (die Überlagerung (die Überlagerung gewinnt in `render()`), der Text bleibt unangetastet. In
gewinnt in `render()`), der Text bleibt unangetastet. In einem Plan ohne `!!!` einem Plan ohne `!!!` werden die Marken geschrieben: `- [ ] > Mittel (M)`,
werden die Marken geschrieben: `- [ ] > Klein (S)` und `- [ ] > Teil 3 (S)`, `- [ ] > Klein (S)` und `- [ ] > Teil 3 (M)`, während `(L)`, `(XL)` und der
während `(L)`, `(XL)` und der Knoten ohne Größe offen bleiben. Knoten ohne Größe offen bleiben.
**Ein Menü, nicht zwei Knöpfe.** Der Diagramm-Kopf trägt schon acht
Bedienelemente und bricht auf dem Telefon knapp nicht um (D17). Zwei weitere
Icon-Knöpfe hätten das gekostet — und die beiden Einträge brauchen **Text**:
„unter Größe M zuklappen“ ist als Piktogramm nicht zu sagen. Dasselbe Idiom
wie das Download-Menü (D17), nur auf **allen** Bildschirmgrößen aufklappend
statt nur auf schmalen. Nachgemessen bei 375 px: Das Menü steht mit 159 px
vollständig im Bild, die Kopfzeile bleibt einzeilig.
+13 -9
View File
@@ -699,15 +699,19 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird
lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes
sagt als das Bild. sagt als das Bild.
- **Für den ganzen Baum** gibt es im Diagramm-Kopf ein Menü mit zwei - **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Umschalter**:
Voreinstellungen: **alle aufklappen** und **unter Größe M zuklappen**. Beide gedrückt ist alles **ab Größe M abwärts** zugeklappt, nicht gedrückt ist der
beschreiben einen **vollständigen** Faltzustand — was die Voreinstellung ganze Baum **offen**. Betroffen ist jeder Knoten mit Kindern, dessen
nicht zuklappt, ist danach offen; „unter Größe M“ meint jeden Knoten mit **angegebene** Größe (§5) `M` oder kleiner ist — offen bleiben `L`, `XL`,
Kindern, dessen **angegebene** Größe (§5) kleiner als `M` ist. Ein Knoten `XXL`. Ein Knoten **ohne** Größenangabe wird nicht zugeklappt: Der günstigste
**ohne** Größenangabe wird nicht zugeklappt: Der günstigste Pfad rechnet Pfad rechnet fehlende Größen zwar als `M` (unten), das ist aber eine
fehlende Größen zwar als `M` (§9), das ist aber eine Kostenannahme und keine Kostenannahme und keine Aussage des Autors.
Aussage des Autors. Geschrieben wird wie beim einzelnen Umklappen — ein - Beide Stellungen beschreiben einen **vollständigen** Faltzustand — was der
einziger Rückgängig-Schritt für den ganzen Vorgang. Siehe D44. Umschalter nicht zuklappt, ist danach offen. Sein Zustand wird **nicht
gemerkt, sondern am Baum abgelesen**: Wer danach einen Knoten von Hand
umklappt, sieht ihn von selbst herausspringen. Geschrieben wird wie beim
einzelnen Umklappen — ein einziger Rückgängig-Schritt für den ganzen
Vorgang. Siehe D44.
- Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden - Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
unverändert über den ganzen Baum. unverändert über den ganzen Baum.
+8 -5
View File
@@ -340,12 +340,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Mobil die Tastatur hoch (`keyboardOnJump(true)`). Bei `src.readOnly` (Pad, Mobil die Tastatur hoch (`keyboardOnJump(true)`). Bei `src.readOnly` (Pad,
D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die
Überlagerungen werden nach erfolgreichem Schreiben geleert, sonst maskieren Überlagerungen werden nach erfolgreichem Schreiben geleert, sonst maskieren
sie den Text. **Voreinstellungen** im Diagramm-Kopf (`applyFoldPreset`, D44) sie den Text. Der **Umschalter** im Diagramm-Kopf (`applyFoldPreset`, D44)
gehen denselben Weg: Überlagerungen für ALLE faltbaren Knoten setzen geht denselben Weg: Überlagerungen für ALLE faltbaren Knoten setzen
(`belowM(n)` bzw. `false`), dann `writeAllFoldMarks()` — der aus (`atMostM(n)` bzw. `false`), dann `writeAllFoldMarks()` — der aus
`writeFoldToText()` herausgelöste Voll-Rewrite; einen minimalen Patch gibt es `writeFoldToText()` herausgelöste Voll-Rewrite; einen minimalen Patch gibt es
hier nicht. Ein Knoten **ohne** Größe zählt nicht als „unter M" (anders als hier nicht. Ein Knoten **ohne** Größe zählt nicht als „bis M" (anders als
bei den Pfadkosten, D18). **`replaceTextUndoable()` meldet bei unveränderten Text bei den Pfadkosten, D18). Sein `aria-pressed` wird **in `render()` am Baum
abgelesen**, nicht gemerkt und nicht persistiert — sonst behauptete er nach
einem einzelnen Umklappen etwas Falsches.
**`replaceTextUndoable()` meldet bei unveränderten Text
`false`** (D38-Nachtrag 3): Ohne Textänderung feuert kein `input`, also läuft `false`** (D38-Nachtrag 3): Ohne Textänderung feuert kein `input`, also läuft
kein `render()` — der Aufrufer muss dann selbst zeichnen. Sonst bleibt das kein `render()` — der Aufrufer muss dann selbst zeichnen. Sonst bleibt das
Bild stehen (aufgefallen beim Aufklappen eines nur per Überlagerung Bild stehen (aufgefallen beim Aufklappen eines nur per Überlagerung
+3 -15
View File
@@ -159,23 +159,11 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg>
<span class="freshcount" id="freshCount" aria-hidden="true">0</span> <span class="freshcount" id="freshCount" aria-hidden="true">0</span>
</button> </button>
<!-- Faltung für den ganzen Baum (SPEC §9, D44). Ein Auslöser, zwei <!-- Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die
Optionen — deshalb ein Aufklapp-Menü und keine zwei Knöpfe. --> beiden Nachbarn. Gedrückt = alles ab Größe M abwärts ist zugeklappt. -->
<div class="foldgroup" id="foldGroup"> <button type="button" class="copybtn togglebtn" id="foldSmallBtn" aria-pressed="false" data-i18n-title="foldSmallTooltip" data-i18n-aria="foldSmallTooltip" title="Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf" aria-label="Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf">
<button type="button" class="copybtn foldtrigger" id="foldTrigger" aria-haspopup="true" aria-expanded="false" data-i18n-title="foldMenu" data-i18n-aria="foldMenu" title="Teilbäume auf- und zuklappen" aria-label="Teilbäume auf- und zuklappen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 4h13M8 9h13M8 15h13M8 20h13M2 6l2.5 2.5L2 11"/></svg>
</button>
<div class="foldmenu">
<button type="button" class="copybtn foldbtn" id="foldExpandAll">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4"/></svg>
<span class="fold-label" data-i18n="foldExpandAll">alle aufklappen</span>
</button>
<button type="button" class="copybtn foldbtn" id="foldCollapseSmall">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21V3M8 8l4 4 4-4M8 16l4-4 4 4"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21V3M8 8l4 4 4-4M8 16l4-4 4 4"/></svg>
<span class="fold-label" data-i18n="foldCollapseSmall">unter Größe M zuklappen</span>
</button> </button>
</div>
</div>
<div class="seg" role="radiogroup" aria-label="Darstellung"> <div class="seg" role="radiogroup" aria-label="Darstellung">
<label data-i18n-title="modeHorizontal" title="Horizontal Organigramm, Diagramm über dem Editor"> <label data-i18n-title="modeHorizontal" title="Horizontal Organigramm, Diagramm über dem Editor">
<input type="radio" name="layout" value="horizontal" checked data-i18n-aria="modeHorizontal" aria-label="Horizontal Organigramm"> <input type="radio" name="layout" value="horizontal" checked data-i18n-aria="modeHorizontal" aria-label="Horizontal Organigramm">
+35 -53
View File
@@ -1,6 +1,6 @@
import './style.css'; import './style.css';
import { parse, setFoldMark } from './parser.js'; import { parse, setFoldMark } from './parser.js';
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, belowM } from './model.js'; import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js';
import { esc, renderTreeHtml } from './render.js'; import { esc, renderTreeHtml } from './render.js';
import { formatWarning } from './warnings.js'; import { formatWarning } from './warnings.js';
import { padUrls } from './remote.js'; import { padUrls } from './remote.js';
@@ -124,13 +124,25 @@ function render(){
const initFold = initialCollapsed(roots, true); const initFold = initialCollapsed(roots, true);
const keys = nodeKeys(roots); const keys = nodeKeys(roots);
const collapsedSet = new Set(); const collapsedSet = new Set();
let foldSmallExact = true, foldSmallAny = false;
foldByLine = new Map(); foldByLine = new Map();
keys.forEach((key, n) => { keys.forEach((key, n) => {
const ov = foldOverrides.get(key); const ov = foldOverrides.get(key);
const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0; const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0;
if(collapsed) collapsedSet.add(n); if(collapsed) collapsedSet.add(n);
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0}); foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
/* Der Umschalter (D44) merkt sich nichts, er liest ab: gedrückt genau
dann, wenn JEDER faltbare Knoten so steht, wie die Voreinstellung ihn
stellen würde. */
if(n.children.length > 0){
if(atMostM(n)) foldSmallAny = true;
if(collapsed !== atMostM(n)) foldSmallExact = false;
}
}); });
/* `foldSmallAny`: Gäbe es gar keinen Knoten bis M, wären zugeklappt" und
alles offen" derselbe Zustand der Knopf säße dann gedrückt da, ohne
dass etwas zugeklappt ist. */
foldSmallBtn.setAttribute('aria-pressed', foldSmallExact && foldSmallAny ? 'true' : 'false');
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet, const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet,
freshSet, collapsedSet, freshSet, collapsedSet,
effStatus: effectiveStatus(roots)}); effStatus: effectiveStatus(roots)});
@@ -1001,7 +1013,7 @@ function applyFoldPreset(mode){
if(!roots.length) return; if(!roots.length) return;
nodeKeys(roots).forEach((key, n) => { nodeKeys(roots).forEach((key, n) => {
if(!n.children.length) return; /* nur faltbare Knoten */ if(!n.children.length) return; /* nur faltbare Knoten */
foldOverrides.set(key, mode === 'small' ? belowM(n) : false); foldOverrides.set(key, mode === 'small' ? atMostM(n) : false);
}); });
if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear(); if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear();
else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */ else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */
@@ -1399,6 +1411,18 @@ function discardedShown(){ return showc.getAttribute('aria-pressed') === 'true';
function setDiscarded(on){ showc.setAttribute('aria-pressed', on ? 'true' : 'false'); } function setDiscarded(on){ showc.setAttribute('aria-pressed', on ? 'true' : 'false'); }
showc.addEventListener('click', () => { setDiscarded(!discardedShown()); render(); saveUI(); }); showc.addEventListener('click', () => { setDiscarded(!discardedShown()); render(); saveUI(); });
/* Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die beiden
Nachbarn. Gedrückt heißt alles unter Größe M ist zugeklappt". Der Zustand
wird nicht gemerkt, sondern bei jedem Rendern am Baum ABGELESEN
(`syncFoldSmallBtn`) klappt jemand danach einen Knoten von Hand um, geht
der Knopf von selbst heraus, statt etwas zu behaupten, das nicht mehr
stimmt. Deshalb auch nicht in `werkbaum-ui` persistiert: Der Faltzustand
steht im Text (D38-Nachtrag 2), der Knopf liest ihn nur ab. */
const foldSmallBtn = document.getElementById('foldSmallBtn');
foldSmallBtn.addEventListener('click', () => {
applyFoldPreset(foldSmallBtn.getAttribute('aria-pressed') === 'true' ? 'all' : 'small');
});
/* Günstigster-Pfad-Hervorhebung an/aus */ /* Günstigster-Pfad-Hervorhebung an/aus */
const cheapBtn = document.getElementById('cheapBtn'); const cheapBtn = document.getElementById('cheapBtn');
cheapBtn.addEventListener('click', () => { cheapBtn.addEventListener('click', () => {
@@ -1466,30 +1490,6 @@ document.addEventListener('click', e => {
if(dlGroup.classList.contains('open') && !dlGroup.contains(e.target)) closeDlMenu(); if(dlGroup.classList.contains('open') && !dlGroup.contains(e.target)) closeDlMenu();
}); });
/* Faltung für den ganzen Baum (SPEC §9, D44) dasselbe Menü-Idiom, hier auf
allen Bildschirmgrößen, weil die Einträge Text tragen. */
const foldGroup = document.getElementById('foldGroup');
const foldTrigger = document.getElementById('foldTrigger');
function closeFoldMenu(){
foldGroup.classList.remove('open');
foldTrigger.setAttribute('aria-expanded', 'false');
}
foldTrigger.addEventListener('click', () => {
const open = foldGroup.classList.toggle('open');
foldTrigger.setAttribute('aria-expanded', String(open));
});
document.addEventListener('click', e => {
if(foldGroup.classList.contains('open') && !foldGroup.contains(e.target)) closeFoldMenu();
});
document.addEventListener('keydown', e => {
if(e.key === 'Escape' && foldGroup.classList.contains('open')){ closeFoldMenu(); foldTrigger.focus(); }
});
document.getElementById('foldExpandAll').addEventListener('click', () => {
closeFoldMenu(); applyFoldPreset('all');
});
document.getElementById('foldCollapseSmall').addEventListener('click', () => {
closeFoldMenu(); applyFoldPreset('small');
});
/* ---------- Internationalisierung (DE/EN/ES/FR) ---------- */ /* ---------- Internationalisierung (DE/EN/ES/FR) ---------- */
const I18N = { const I18N = {
@@ -1516,9 +1516,7 @@ const I18N = {
riskTooltip:"High Risk Aufwand noch unklar.", riskTooltip:"High Risk Aufwand noch unklar.",
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden", discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
cheapTooltip:"Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück", cheapTooltip:"Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück",
foldMenu:"Teilbäume auf- und zuklappen", foldSmallTooltip:"Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf",
foldExpandAll:"alle aufklappen",
foldCollapseSmall:"unter Größe M zuklappen",
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung als M angenommen", implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung als M angenommen",
fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite", fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite",
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).", brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
@@ -1599,9 +1597,7 @@ const I18N = {
riskTooltip:"High risk effort still unclear.", riskTooltip:"High risk effort still unclear.",
discardedTooltip:"Show/hide discarded nodes and their subtree", discardedTooltip:"Show/hide discarded nodes and their subtree",
cheapTooltip:"Highlight the cheapest path unneeded alternatives recede", cheapTooltip:"Highlight the cheapest path unneeded alternatives recede",
foldMenu:"Expand and collapse subtrees", foldSmallTooltip:"Collapse nodes of size M and smaller press again to expand them all",
foldExpandAll:"expand all",
foldCollapseSmall:"collapse below size M",
implicitSizeTooltip:"No size given assumed as M for the cost estimate", implicitSizeTooltip:"No size given assumed as M for the cost estimate",
fullscreenTooltip:"Full screen panels use the full window width", fullscreenTooltip:"Full screen panels use the full window width",
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).", brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
@@ -1682,9 +1678,7 @@ const I18N = {
riskTooltip:"Alto riesgo esfuerzo aún incierto.", riskTooltip:"Alto riesgo esfuerzo aún incierto.",
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol", discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan", cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
foldMenu:"Desplegar y plegar subárboles", foldSmallTooltip:"Plegar los nodos de talla M o menor: pulsa de nuevo para desplegarlos todos",
foldExpandAll:"desplegar todo",
foldCollapseSmall:"plegar por debajo de la talla M",
implicitSizeTooltip:"Sin tamaño indicado: se asume M para el cálculo de costes", implicitSizeTooltip:"Sin tamaño indicado: se asume M para el cálculo de costes",
fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana", fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana",
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).", brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
@@ -1765,9 +1759,7 @@ const I18N = {
riskTooltip:"Risque élevé effort encore incertain.", riskTooltip:"Risque élevé effort encore incertain.",
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre", discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
cheapTooltip:"Mettre en évidence le chemin le moins coûteux les alternatives inutiles s'estompent", cheapTooltip:"Mettre en évidence le chemin le moins coûteux les alternatives inutiles s'estompent",
foldMenu:"plier et replier les sous-arbres", foldSmallTooltip:"Replier les nœuds de taille M et moins appuyez à nouveau pour tout déplier",
foldExpandAll:"tout déplier",
foldCollapseSmall:"replier en dessous de la taille M",
implicitSizeTooltip:"Aucune taille indiquée considérée comme M pour l'estimation des coûts", implicitSizeTooltip:"Aucune taille indiquée considérée comme M pour l'estimation des coûts",
fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre", fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre",
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).", brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
@@ -1848,9 +1840,7 @@ const I18N = {
riskTooltip:"Wysokie ryzyko nakład jeszcze niejasny.", riskTooltip:"Wysokie ryzyko nakład jeszcze niejasny.",
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem", discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
cheapTooltip:"Wyróżnij najtańszą ścieżkę niepotrzebne alternatywy są przygaszone", cheapTooltip:"Wyróżnij najtańszą ścieżkę niepotrzebne alternatywy są przygaszone",
foldMenu:"Rozwijanie i zwijanie poddrzew", foldSmallTooltip:"Zwiń węzły o rozmiarze M i mniejsze naciśnij ponownie, aby rozwinąć wszystkie",
foldExpandAll:"rozwiń wszystko",
foldCollapseSmall:"zwiń poniżej rozmiaru M",
implicitSizeTooltip:"Nie podano rozmiaru przyjęto M do szacowania kosztów", implicitSizeTooltip:"Nie podano rozmiaru przyjęto M do szacowania kosztów",
fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna", fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna",
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).", brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
@@ -1931,9 +1921,7 @@ const I18N = {
riskTooltip:"Высокий риск – оценка ещё не ясна.", riskTooltip:"Высокий риск – оценка ещё не ясна.",
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом", discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются", cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
foldMenu:"Развернуть и свернуть поддеревья", foldSmallTooltip:"Свернуть узлы размера M и меньше — нажмите ещё раз, чтобы развернуть все",
foldExpandAll:"развернуть все",
foldCollapseSmall:"свернуть меньше размера M",
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M", implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна", fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
@@ -2014,9 +2002,7 @@ const I18N = {
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।", riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ", discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं", cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
foldMenu:"उपवृक्ष खोलें और समेटें", foldSmallTooltip:"आकार M और उससे छोटे नोड समेटें – सभी को खोलने के लिए फिर दबाएँ",
foldExpandAll:"सभी खोलें",
foldCollapseSmall:"आकार M से छोटे समेटें",
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया", implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं", fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
@@ -2086,9 +2072,7 @@ const I18N = {
fullscreenTooltip:"全屏——面板占据整个窗口宽度", fullscreenTooltip:"全屏——面板占据整个窗口宽度",
discardedTooltip:"显示/隐藏已放弃的节点及其子树", discardedTooltip:"显示/隐藏已放弃的节点及其子树",
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化", cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
foldMenu:"展开和折叠子树", foldSmallTooltip:"折叠尺寸 M 及更小的节点——再次点击可全部展开",
foldExpandAll:"全部展开",
foldCollapseSmall:"折叠小于 M 的节点",
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计", implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
ghostTooltip:"从 M 号起,元素应进一步细分。", ghostTooltip:"从 M 号起,元素应进一步细分。",
jumpHint:"Alt+点击:跳转到文本中的该行", jumpHint:"Alt+点击:跳转到文本中的该行",
@@ -2169,9 +2153,7 @@ const I18N = {
fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用", fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用",
discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示", discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示",
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示", cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
foldMenu:"サブツリーの展開と折りたたみ", foldSmallTooltip:"サイズ M 以下のノードを折りたたむ – もう一度押すとすべて展開",
foldExpandAll:"すべて展開",
foldCollapseSmall:"サイズ M 未満を折りたたむ",
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う", implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。", ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
jumpHint:"Alt+クリック:テキストの該当行へ移動", jumpHint:"Alt+クリック:テキストの該当行へ移動",
+7 -6
View File
@@ -21,12 +21,13 @@ export function needsBreakdown(n){
return !!n.size && SIZE_RANK[n.size] >= SIZE_RANK.M && !n.children.length; return !!n.size && SIZE_RANK[n.size] >= SIZE_RANK.M && !n.children.length;
} }
/* Faltung-Voreinstellung alles unter Größe M zuklappen" (SPEC §9, D44): /* Faltung-Voreinstellung Größe M und kleiner zuklappen" (SPEC §9, D44):
Knoten mit ANGEGEBENER Größe kleiner als M. Fehlt die Angabe, wird nichts Knoten mit ANGEGEBENER Größe bis einschließlich M offen bleiben also nur
angenommen der günstigste Pfad rechnet sie zwar als M (D18), das ist aber L, XL, XXL. Fehlt die Angabe, wird nichts angenommen: Der günstigste Pfad
eine Kostenannahme des Werkzeugs und keine Aussage des Autors; danach den rechnet sie zwar als M (D18), das ist aber eine Kostenannahme des Werkzeugs
Baum zuzuklappen hieße, eine Vermutung wie eine Angabe zu behandeln. */ und keine Aussage des Autors; danach den Baum zuzuklappen hieße, eine
export function belowM(n){ return !!n.size && SIZE_RANK[n.size] < SIZE_RANK.M; } Vermutung wie eine Angabe zu behandeln. */
export function atMostM(n){ return !!n.size && SIZE_RANK[n.size] <= SIZE_RANK.M; }
/* Sichtbare Kinder: verworfene ausblenden, außer showDiscarded ist gesetzt. */ /* Sichtbare Kinder: verworfene ausblenden, außer showDiscarded ist gesetzt. */
export function visibleChildren(n, showDiscarded){ export function visibleChildren(n, showDiscarded){
-14
View File
@@ -397,20 +397,6 @@
.dlgroup{display:inline-flex;align-items:center;gap:3px} .dlgroup{display:inline-flex;align-items:center;gap:3px}
.dlmenu{display:inline-flex;gap:3px} .dlmenu{display:inline-flex;gap:3px}
.dltrigger{display:none} .dltrigger{display:none}
/* Faltung für den ganzen Baum (SPEC §9, D44): Anders als die Download-Gruppe
klappt diese IMMER auf die beiden Einträge tragen Text, der inline die
Kopfzeile sprengte. Idiom und Maße wie dort. */
.foldgroup{display:inline-flex;align-items:center;position:relative}
.foldtrigger[aria-expanded="true"]{border-color:var(--or);color:var(--or)}
.foldmenu{display:none}
.foldgroup.open .foldmenu{
display:flex;flex-direction:column;gap:5px;
position:absolute;top:calc(100% + 6px);right:0;z-index:40;
background:var(--card);border:1px solid rgba(36,52,71,.15);
border-radius:8px;padding:5px;box-shadow:0 6px 18px rgba(36,52,71,.28);
}
.foldbtn{gap:6px;width:100%;justify-content:flex-start;padding-inline:8px;white-space:nowrap}
.fold-label{font-size:.68rem;font-weight:500;letter-spacing:.01em}
.legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */ .legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */
.panetoggle{display:none} /* ebenso: dort gibt es nur einen Bereich */ .panetoggle{display:none} /* ebenso: dort gibt es nur einen Bereich */
/* Zurückschreiben der Faltung (D38-Nachtrag 2): Für die Dauer des SYNCHRONEN /* Zurückschreiben der Faltung (D38-Nachtrag 2): Für die Dauer des SYNCHRONEN
+8 -8
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { parse, setFoldMark } from '../src/parser.js'; import { parse, setFoldMark } from '../src/parser.js';
import { initialCollapsed, computeCheapSet, belowM } from '../src/model.js'; import { initialCollapsed, computeCheapSet, atMostM } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js'; import { renderTreeHtml } from '../src/render.js';
const t = key => key; const t = key => key;
@@ -256,12 +256,12 @@ describe('Günstigster Pfad an eingeklappten Knoten', () => {
/* Voreinstellung unter Größe M zuklappen" im Diagramm-Kopf (SPEC §9, D44). /* Voreinstellung unter Größe M zuklappen" im Diagramm-Kopf (SPEC §9, D44).
Das Falten selbst liegt in app.js (kein headless-Testziel); geprüft wird Das Falten selbst liegt in app.js (kein headless-Testziel); geprüft wird
das Prädikat, das entscheidet, WELCHE Knoten es trifft. */ das Prädikat, das entscheidet, WELCHE Knoten es trifft. */
describe('belowM — Auswahl für „unter Größe M zuklappen"', () => { describe('atMostM — Auswahl für „Größe M und kleiner zuklappen"', () => {
const groessen = txt => roots(txt).map(n => [n.label, belowM(n)]); const groessen = txt => roots(txt).map(n => [n.label, atMostM(n)]);
it('trifft XS und S, nicht M und darüber', () => { it('trifft XS, S und M — offen bleiben nur L, XL, XXL', () => {
expect(groessen('[ ] a (XS)\n[ ] b (S)\n[ ] c (M)\n[ ] d (L)\n[ ] e (XL)\n[ ] f (XXL)')) expect(groessen('[ ] a (XS)\n[ ] b (S)\n[ ] c (M)\n[ ] d (L)\n[ ] e (XL)\n[ ] f (XXL)'))
.toEqual([['a', true], ['b', true], ['c', false], ['d', false], .toEqual([['a', true], ['b', true], ['c', true], ['d', false],
['e', false], ['f', false]]); ['e', false], ['f', false]]);
}); });
@@ -277,8 +277,8 @@ describe('belowM — Auswahl für „unter Größe M zuklappen"', () => {
}); });
it('gilt auch für Knoten tief im Baum', () => { it('gilt auch für Knoten tief im Baum', () => {
const [w] = roots('[ ] W (XL)\n - [ ] A (S)\n - [ ] A1 (XS)\n - [ ] B (L)'); const [w] = roots('[ ] W (XL)\n - [ ] A (M)\n - [ ] A1 (XS)\n - [ ] B (L)');
expect([belowM(w), ...w.children.map(belowM)]).toEqual([false, true, false]); expect([atMostM(w), ...w.children.map(atMostM)]).toEqual([false, true, false]);
expect(belowM(w.children[0].children[0])).toBe(true); expect(atMostM(w.children[0].children[0])).toBe(true);
}); });
}); });