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:
co-authored by
Claude Fable 5
parent
9bbe7d78cc
commit
417006960f
+48
-32
@@ -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
|
||||
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
|
||||
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
|
||||
zuklappen**.
|
||||
dafür einen **Umschalter** neben „verworfene einblenden“ und „günstigster
|
||||
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
|
||||
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
|
||||
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
|
||||
dagegen etwas über die **Arbeit**, und die Notation hat dafür bereits eine
|
||||
Schwelle: Ab `M` **muss** ein Element zerlegt sein (§5/D8). „Unter M“ ist also
|
||||
genau die Menge, die keine Zerlegung mehr braucht — was darunter hängt, ist
|
||||
Detail. Die Voreinstellung zeigt damit den Plan auf der Höhe, auf der noch
|
||||
Entscheidungen anstehen.
|
||||
dagegen etwas über die **Arbeit** — und weil `M` die kleinste Größe ist, die
|
||||
überhaupt eine Zerlegung verlangt (§5/D8), ist „M und kleiner“ genau die
|
||||
Menge, deren Inneres Detail ist. Übrig bleibt der Plan auf der Höhe von `L`
|
||||
aufwärts, also dort, wo die großen Brocken stehen.
|
||||
|
||||
*(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
|
||||
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
|
||||
angenehmerweise auf.
|
||||
|
||||
**Beide Einträge beschreiben einen vollständigen Zustand.** „Unter M
|
||||
zuklappen“ klappt nicht nur zu, es klappt alles **andere auf**. Sonst wäre die
|
||||
Wirkung vom Vorzustand abhängig und zweimal Drücken ergäbe zweierlei — für
|
||||
eine Voreinstellung die falsche Eigenschaft. Der Preis: Ein von Hand gesetztes
|
||||
`>` an einem großen Knoten wird dabei aufgelöst. Das ist 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).
|
||||
**Ein Umschalter, kein Menü.** Zuerst gebaut war ein Aufklapp-Menü mit zwei
|
||||
Einträgen („alle aufklappen“ / „zuklappen“); der Nutzer wollte einen
|
||||
Umschalter. Das ist die bessere Form, und der Grund ist nicht nur Platz: Die
|
||||
beiden Einträge waren nie unabhängige Befehle, sondern die **zwei Stellungen
|
||||
einer Sache** — jeder beschreibt einen vollständigen Faltzustand, und
|
||||
zusammen decken sie ihn ganz ab. Genau das ist ein Umschalter. Ein Menü
|
||||
verlangte zwei Klicks für etwas, das einen braucht, und der Diagramm-Kopf hat
|
||||
das Idiom längst zweimal (D18, D4).
|
||||
|
||||
**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
|
||||
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.
|
||||
|
||||
Genau der Fall tritt im mitgelieferten Werkbaum-Plan auf und ist beim Bauen
|
||||
nachgemessen worden: Zwei Knoten sind kleiner als `M` und haben Kinder
|
||||
(Zeile 94 und 100), aber in Zeile 103 steht eine Fokusmarke `!!!` unterhalb
|
||||
von 94 — die holt ihren Knoten immer wieder hervor, „94 eingeklappt“ ist als
|
||||
Markensatz also nicht schreibbar. Das Bild stimmt trotzdem (die Überlagerung
|
||||
gewinnt in `render()`), der Text bleibt unangetastet. In einem Plan ohne `!!!`
|
||||
werden die Marken geschrieben: `- [ ] > Klein (S)` und `- [ ] > Teil 3 (S)`,
|
||||
während `(L)`, `(XL)` und der 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.
|
||||
nachgemessen worden: In Zeile 103 steht eine Fokusmarke `!!!` unterhalb eines
|
||||
Knotens, der zugeklappt werden soll — die holt ihren Knoten immer wieder
|
||||
hervor, dieser Markensatz ist also nicht schreibbar. Das Bild stimmt trotzdem
|
||||
(die Überlagerung gewinnt in `render()`), der Text bleibt unangetastet. In
|
||||
einem Plan ohne `!!!` werden die Marken geschrieben: `- [ ] > Mittel (M)`,
|
||||
`- [ ] > Klein (S)` und `- [ ] > Teil 3 (M)`, während `(L)`, `(XL)` und der
|
||||
Knoten ohne Größe offen bleiben.
|
||||
|
||||
+13
-9
@@ -699,15 +699,19 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
||||
weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird
|
||||
lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes
|
||||
sagt als das Bild.
|
||||
- **Für den ganzen Baum** gibt es im Diagramm-Kopf ein Menü mit zwei
|
||||
Voreinstellungen: **alle aufklappen** und **unter Größe M zuklappen**. Beide
|
||||
beschreiben einen **vollständigen** Faltzustand — was die Voreinstellung
|
||||
nicht zuklappt, ist danach offen; „unter Größe M“ meint jeden Knoten mit
|
||||
Kindern, dessen **angegebene** Größe (§5) kleiner als `M` ist. Ein Knoten
|
||||
**ohne** Größenangabe wird nicht zugeklappt: Der günstigste Pfad rechnet
|
||||
fehlende Größen zwar als `M` (§9), das ist aber eine Kostenannahme und keine
|
||||
Aussage des Autors. Geschrieben wird wie beim einzelnen Umklappen — ein
|
||||
einziger Rückgängig-Schritt für den ganzen Vorgang. Siehe D44.
|
||||
- **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Umschalter**:
|
||||
gedrückt ist alles **ab Größe M abwärts** zugeklappt, nicht gedrückt ist der
|
||||
ganze Baum **offen**. Betroffen ist jeder Knoten mit Kindern, dessen
|
||||
**angegebene** Größe (§5) `M` oder kleiner ist — offen bleiben `L`, `XL`,
|
||||
`XXL`. Ein Knoten **ohne** Größenangabe wird nicht zugeklappt: Der günstigste
|
||||
Pfad rechnet fehlende Größen zwar als `M` (unten), das ist aber eine
|
||||
Kostenannahme und keine Aussage des Autors.
|
||||
- Beide Stellungen beschreiben einen **vollständigen** Faltzustand — was der
|
||||
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
|
||||
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
|
||||
unverändert über den ganzen Baum.
|
||||
|
||||
+8
-5
@@ -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,
|
||||
D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die
|
||||
Überlagerungen werden nach erfolgreichem Schreiben geleert, sonst maskieren
|
||||
sie den Text. **Voreinstellungen** im Diagramm-Kopf (`applyFoldPreset`, D44)
|
||||
gehen denselben Weg: Überlagerungen für ALLE faltbaren Knoten setzen
|
||||
(`belowM(n)` bzw. `false`), dann `writeAllFoldMarks()` — der aus
|
||||
sie den Text. Der **Umschalter** im Diagramm-Kopf (`applyFoldPreset`, D44)
|
||||
geht denselben Weg: Überlagerungen für ALLE faltbaren Knoten setzen
|
||||
(`atMostM(n)` bzw. `false`), dann `writeAllFoldMarks()` — der aus
|
||||
`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
|
||||
bei den Pfadkosten, D18). **`replaceTextUndoable()` meldet bei unveränderten Text
|
||||
hier nicht. Ein Knoten **ohne** Größe zählt nicht als „bis M" (anders als
|
||||
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
|
||||
kein `render()` — der Aufrufer muss dann selbst zeichnen. Sonst bleibt das
|
||||
Bild stehen (aufgefallen beim Aufklappen eines nur per Überlagerung
|
||||
|
||||
+5
-17
@@ -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>
|
||||
<span class="freshcount" id="freshCount" aria-hidden="true">0</span>
|
||||
</button>
|
||||
<!-- Faltung für den ganzen Baum (SPEC §9, D44). Ein Auslöser, zwei
|
||||
Optionen — deshalb ein Aufklapp-Menü und keine zwei Knöpfe. -->
|
||||
<div class="foldgroup" id="foldGroup">
|
||||
<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>
|
||||
<span class="fold-label" data-i18n="foldCollapseSmall">unter Größe M zuklappen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die
|
||||
beiden Nachbarn. Gedrückt = alles ab Größe M abwärts ist zugeklappt. -->
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
<div class="seg" role="radiogroup" aria-label="Darstellung">
|
||||
<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">
|
||||
|
||||
+35
-53
@@ -1,6 +1,6 @@
|
||||
import './style.css';
|
||||
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 { formatWarning } from './warnings.js';
|
||||
import { padUrls } from './remote.js';
|
||||
@@ -124,13 +124,25 @@ function render(){
|
||||
const initFold = initialCollapsed(roots, true);
|
||||
const keys = nodeKeys(roots);
|
||||
const collapsedSet = new Set();
|
||||
let foldSmallExact = true, foldSmallAny = false;
|
||||
foldByLine = new Map();
|
||||
keys.forEach((key, n) => {
|
||||
const ov = foldOverrides.get(key);
|
||||
const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0;
|
||||
if(collapsed) collapsedSet.add(n);
|
||||
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,
|
||||
freshSet, collapsedSet,
|
||||
effStatus: effectiveStatus(roots)});
|
||||
@@ -1001,7 +1013,7 @@ function applyFoldPreset(mode){
|
||||
if(!roots.length) return;
|
||||
nodeKeys(roots).forEach((key, n) => {
|
||||
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();
|
||||
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'); }
|
||||
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 */
|
||||
const cheapBtn = document.getElementById('cheapBtn');
|
||||
cheapBtn.addEventListener('click', () => {
|
||||
@@ -1466,30 +1490,6 @@ document.addEventListener('click', e => {
|
||||
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) ---------- */
|
||||
const I18N = {
|
||||
@@ -1516,9 +1516,7 @@ const I18N = {
|
||||
riskTooltip:"High Risk – Aufwand noch unklar.",
|
||||
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
||||
cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück",
|
||||
foldMenu:"Teilbäume auf- und zuklappen",
|
||||
foldExpandAll:"alle aufklappen",
|
||||
foldCollapseSmall:"unter Größe M zuklappen",
|
||||
foldSmallTooltip:"Knoten der Größe M und kleiner zuklappen – erneut drücken klappt alle wieder auf",
|
||||
implicitSizeTooltip:"Keine Größe angegeben – für die Kostenschätzung als M angenommen",
|
||||
fullscreenTooltip:"Vollbild – Panels nutzen die ganze Fensterbreite",
|
||||
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.",
|
||||
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
||||
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
||||
foldMenu:"Expand and collapse subtrees",
|
||||
foldExpandAll:"expand all",
|
||||
foldCollapseSmall:"collapse below size M",
|
||||
foldSmallTooltip:"Collapse nodes of size M and smaller – press again to expand them all",
|
||||
implicitSizeTooltip:"No size given – assumed as M for the cost estimate",
|
||||
fullscreenTooltip:"Full screen – panels use the full window width",
|
||||
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.",
|
||||
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",
|
||||
foldMenu:"Desplegar y plegar subárboles",
|
||||
foldExpandAll:"desplegar todo",
|
||||
foldCollapseSmall:"plegar por debajo de la talla M",
|
||||
foldSmallTooltip:"Plegar los nodos de talla M o menor: pulsa de nuevo para desplegarlos todos",
|
||||
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",
|
||||
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.",
|
||||
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",
|
||||
foldMenu:"Déplier et replier les sous-arbres",
|
||||
foldExpandAll:"tout déplier",
|
||||
foldCollapseSmall:"replier en dessous de la taille M",
|
||||
foldSmallTooltip:"Replier les nœuds de taille M et moins – appuyez à nouveau pour tout déplier",
|
||||
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",
|
||||
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — l’arbre de l’organigramme des tâches (WBS).",
|
||||
@@ -1848,9 +1840,7 @@ const I18N = {
|
||||
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
||||
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
||||
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
||||
foldMenu:"Rozwijanie i zwijanie poddrzew",
|
||||
foldExpandAll:"rozwiń wszystko",
|
||||
foldCollapseSmall:"zwiń poniżej rozmiaru M",
|
||||
foldSmallTooltip:"Zwiń węzły o rozmiarze M i mniejsze – naciśnij ponownie, aby rozwinąć wszystkie",
|
||||
implicitSizeTooltip:"Nie podano rozmiaru – przyjęto M do szacowania kosztów",
|
||||
fullscreenTooltip:"Pełny ekran – panele wykorzystują całą szerokość okna",
|
||||
brandTooltip:"„Werkbaum” znaczy mniej więcej ‚drzewo pracy’ — drzewo struktury podziału pracy (WBS).",
|
||||
@@ -1931,9 +1921,7 @@ const I18N = {
|
||||
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
||||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||||
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
||||
foldMenu:"Развернуть и свернуть поддеревья",
|
||||
foldExpandAll:"развернуть все",
|
||||
foldCollapseSmall:"свернуть меньше размера M",
|
||||
foldSmallTooltip:"Свернуть узлы размера M и меньше — нажмите ещё раз, чтобы развернуть все",
|
||||
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
|
||||
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
|
||||
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
|
||||
@@ -2014,9 +2002,7 @@ const I18N = {
|
||||
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
||||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||||
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
||||
foldMenu:"उपवृक्ष खोलें और समेटें",
|
||||
foldExpandAll:"सभी खोलें",
|
||||
foldCollapseSmall:"आकार M से छोटे समेटें",
|
||||
foldSmallTooltip:"आकार M और उससे छोटे नोड समेटें – सभी को खोलने के लिए फिर दबाएँ",
|
||||
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
|
||||
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
|
||||
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
|
||||
@@ -2086,9 +2072,7 @@ const I18N = {
|
||||
fullscreenTooltip:"全屏——面板占据整个窗口宽度",
|
||||
discardedTooltip:"显示/隐藏已放弃的节点及其子树",
|
||||
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
||||
foldMenu:"展开和折叠子树",
|
||||
foldExpandAll:"全部展开",
|
||||
foldCollapseSmall:"折叠小于 M 的节点",
|
||||
foldSmallTooltip:"折叠尺寸 M 及更小的节点——再次点击可全部展开",
|
||||
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
|
||||
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
||||
jumpHint:"Alt+点击:跳转到文本中的该行",
|
||||
@@ -2169,9 +2153,7 @@ const I18N = {
|
||||
fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用",
|
||||
discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示",
|
||||
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
||||
foldMenu:"サブツリーの展開と折りたたみ",
|
||||
foldExpandAll:"すべて展開",
|
||||
foldCollapseSmall:"サイズ M 未満を折りたたむ",
|
||||
foldSmallTooltip:"サイズ M 以下のノードを折りたたむ – もう一度押すとすべて展開",
|
||||
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
|
||||
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
||||
jumpHint:"Alt+クリック:テキストの該当行へ移動",
|
||||
|
||||
@@ -21,12 +21,13 @@ export function needsBreakdown(n){
|
||||
return !!n.size && SIZE_RANK[n.size] >= SIZE_RANK.M && !n.children.length;
|
||||
}
|
||||
|
||||
/* Faltung-Voreinstellung „alles unter Größe M zuklappen" (SPEC §9, D44):
|
||||
Knoten mit ANGEGEBENER Größe kleiner als M. Fehlt die Angabe, wird nichts
|
||||
angenommen — der günstigste Pfad rechnet sie zwar als M (D18), das ist aber
|
||||
eine Kostenannahme des Werkzeugs und keine Aussage des Autors; danach den
|
||||
Baum zuzuklappen hieße, eine Vermutung wie eine Angabe zu behandeln. */
|
||||
export function belowM(n){ return !!n.size && SIZE_RANK[n.size] < SIZE_RANK.M; }
|
||||
/* Faltung-Voreinstellung „Größe M und kleiner zuklappen" (SPEC §9, D44):
|
||||
Knoten mit ANGEGEBENER Größe bis einschließlich M — offen bleiben also nur
|
||||
L, XL, XXL. Fehlt die Angabe, wird nichts angenommen: Der günstigste Pfad
|
||||
rechnet sie zwar als M (D18), das ist aber eine Kostenannahme des Werkzeugs
|
||||
und keine Aussage des Autors; danach den Baum zuzuklappen hieße, eine
|
||||
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. */
|
||||
export function visibleChildren(n, showDiscarded){
|
||||
|
||||
@@ -397,20 +397,6 @@
|
||||
.dlgroup{display:inline-flex;align-items:center;gap:3px}
|
||||
.dlmenu{display:inline-flex;gap:3px}
|
||||
.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 */
|
||||
.panetoggle{display:none} /* ebenso: dort gibt es nur einen Bereich */
|
||||
/* Zurückschreiben der Faltung (D38-Nachtrag 2): Für die Dauer des SYNCHRONEN
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
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';
|
||||
|
||||
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).
|
||||
Das Falten selbst liegt in app.js (kein headless-Testziel); geprüft wird
|
||||
das Prädikat, das entscheidet, WELCHE Knoten es trifft. */
|
||||
describe('belowM — Auswahl für „unter Größe M zuklappen"', () => {
|
||||
const groessen = txt => roots(txt).map(n => [n.label, belowM(n)]);
|
||||
describe('atMostM — Auswahl für „Größe M und kleiner zuklappen"', () => {
|
||||
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)'))
|
||||
.toEqual([['a', true], ['b', true], ['c', false], ['d', false],
|
||||
.toEqual([['a', true], ['b', true], ['c', true], ['d', 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', () => {
|
||||
const [w] = roots('[ ] W (XL)\n - [ ] A (S)\n - [ ] A1 (XS)\n - [ ] B (L)');
|
||||
expect([belowM(w), ...w.children.map(belowM)]).toEqual([false, true, false]);
|
||||
expect(belowM(w.children[0].children[0])).toBe(true);
|
||||
const [w] = roots('[ ] W (XL)\n - [ ] A (M)\n - [ ] A1 (XS)\n - [ ] B (L)');
|
||||
expect([atMostM(w), ...w.children.map(atMostM)]).toEqual([false, true, false]);
|
||||
expect(atMostM(w.children[0].children[0])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user