feat: Falt-Voreinstellungen im Diagramm-Kopf (D44)

Ein Menü mit zwei Einträgen: alle aufklappen, unter Größe M zuklappen.

Das Kriterium ist die Größe, nicht die Tiefe. Die Ebene ist eine Eigenschaft
der Schreibweise — ein fein zerlegter Zweig verschwände bei derselben Zahl
früher als ein grob notierter. Die T-Shirt-Größe sagt etwas über die Arbeit,
und ab M muss ohnehin zerlegt sein (SPEC §5): „unter M" ist genau die Menge,
die keine Zerlegung mehr braucht.

Ohne Größenangabe wird nicht zugeklappt. Der günstigste Pfad wertet fehlende
Größen als M (D18), aber das ist eine Kostenannahme, keine Aussage des Autors.

Beide Einträge beschreiben einen vollständigen Zustand — sonst hinge die
Wirkung vom Vorzustand ab. Rückgängig nimmt den ganzen Vorgang in einem
Schritt zurück.

Umgesetzt über denselben Weg wie das einzelne Umklappen; der Voll-Rewrite ist
dafür aus writeFoldToText() in writeAllFoldMarks() gewandert. Die
Rückfall-Kette gilt unverändert: Im Werkbaum-Plan ist der Zustand wegen einer
Fokusmarke nicht in Marken ausdrückbar, dort trägt die Sitzungs-Überlagerung.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 10:31:04 +02:00
co-authored by Claude Fable 5
parent 7e2c7cd3e0
commit 9bbe7d78cc
8 changed files with 232 additions and 17 deletions
+60
View File
@@ -2717,3 +2717,63 @@ Markdown; die Konvention nutzt `.txt` nur als kleinsten gemeinsamen Nenner),
und der Footer-Link rückt zwischen Versionsnummer und Copyright — zur 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
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**.
**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.
**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
ist es eine bewusst konservative **Kostenannahme** („mindestens M“), hier wäre
es eine Aussage über den Willen des Autors. Wer keine Größe angegeben hat, hat
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).
**Umgesetzt über denselben Weg wie das einzelne Umklappen** — die
Sitzungs-Überlagerungen setzen, dann in den Text schreiben (D38-Nachtrag 2).
Dafür wanderte der Voll-Rewrite aus `writeFoldToText()` in ein eigenes
`writeAllFoldMarks(roots, want)`: Für eine Voreinstellung gibt es keinen
minimalen Patch, sie fasst den ganzen Baum an. Damit gilt auch hier die
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.
+9
View File
@@ -699,6 +699,15 @@ 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
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.
- 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.
+6 -1
View File
@@ -340,7 +340,12 @@ 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. **`replaceTextUndoable()` meldet bei unveränderten Text 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
`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
`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
+17
View File
@@ -159,6 +159,23 @@
<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
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>
<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">
+88 -15
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 } from './model.js'; import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, belowM } 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';
@@ -969,23 +969,44 @@ function writeFoldToText(line, collapsed){
klein[line-1] = setFoldMark(klein[line-1], collapsed ? '>' : null); klein[line-1] = setFoldMark(klein[line-1], collapsed ? '>' : null);
let neu = klein.join('\n'); let neu = klein.join('\n');
if(!foldStateMatches(neu, want)){ if(!foldStateMatches(neu, want)) return writeAllFoldMarks(roots, want);
/* 2) Vollständig: alle Marken neu setzen. Nötig, wenn ein `<` den Zustand
nicht mehr trifft dann wird es hier aufgelöst. */
const keys = nodeKeys(roots);
const ganz = zeilen.slice();
keys.forEach((key, n) => {
ganz[n.line-1] = setFoldMark(ganz[n.line-1], want.has(key) ? '>' : null);
});
neu = ganz.join('\n');
/* 3) Nicht ausdrückbar etwa weil ein `!!!` im Zweig seinen Knoten immer
wieder hervorholt (SPEC §9). Dann NICHT schreiben: ein Text, der
etwas anderes sagt als das Bild, wäre schlimmer als keine Marke. */
if(!foldStateMatches(neu, want)) return false;
}
return withEditorWritable(() => replaceTextUndoable(neu)); return withEditorWritable(() => replaceTextUndoable(neu));
} }
/* Alle Marken neu setzen der Weg, wenn ein `<` den Zustand nicht mehr trifft
(dann wird es hier aufgelöst) und zugleich der einzige sinnvolle für die
Voreinstellungen, die ohnehin den ganzen Baum anfassen. Liefert false, wenn
der Zustand in Marken NICHT ausdrückbar ist etwa weil ein `!!!` im Zweig
seinen Knoten immer wieder hervorholt (SPEC §9). Dann wird nicht
geschrieben: ein Text, der etwas anderes sagt als das Bild, wäre schlimmer
als keine Marke. */
function writeAllFoldMarks(roots, want){
const keys = nodeKeys(roots);
const zeilen = src.value.split('\n');
keys.forEach((key, n) => {
zeilen[n.line-1] = setFoldMark(zeilen[n.line-1], want.has(key) ? '>' : null);
});
const neu = zeilen.join('\n');
if(!foldStateMatches(neu, want)) return false;
return withEditorWritable(() => replaceTextUndoable(neu));
}
/* Voreinstellungen aus dem Diagramm-Kopf (SPEC §9, D44): den ganzen Baum auf
einmal auf- bzw. bis unter Größe M zuklappen. Gesetzt wird über dieselben
Sitzungs-Überlagerungen wie beim einzelnen Umklappen und danach in den Text
geschrieben es ist derselbe Vorgang, nur für viele Knoten. Deshalb auch
EIN Undo-Schritt: `replaceTextUndoable` schreibt genau einmal. */
function applyFoldPreset(mode){
const roots = parse(src.value).roots;
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);
});
if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear();
else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */
}
/* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text /* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text
der Zustand die Überlagerungen werden dann geleert, damit sie ihn nicht der Zustand die Überlagerungen werden dann geleert, damit sie ihn nicht
maskieren können. Nach dem Neubau bekommt derselbe Knoten den Fokus zurück, maskieren können. Nach dem Neubau bekommt derselbe Knoten den Fokus zurück,
@@ -1445,6 +1466,31 @@ 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 = {
de: { de: {
@@ -1470,6 +1516,9 @@ 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",
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).",
@@ -1550,6 +1599,9 @@ 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",
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).",
@@ -1630,6 +1682,9 @@ 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",
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).",
@@ -1710,6 +1765,9 @@ 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:"Déplier et replier les sous-arbres",
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).",
@@ -1790,6 +1848,9 @@ 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",
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).",
@@ -1870,6 +1931,9 @@ const I18N = {
riskTooltip:"Высокий риск – оценка ещё не ясна.", riskTooltip:"Высокий риск – оценка ещё не ясна.",
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом", discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются", cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
foldMenu:"Развернуть и свернуть поддеревья",
foldExpandAll:"развернуть все",
foldCollapseSmall:"свернуть меньше размера M",
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M", implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна", fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
@@ -1950,6 +2014,9 @@ const I18N = {
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।", riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ", discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं", cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
foldMenu:"उपवृक्ष खोलें और समेटें",
foldExpandAll:"सभी खोलें",
foldCollapseSmall:"आकार M से छोटे समेटें",
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया", implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं", fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
@@ -2019,6 +2086,9 @@ const I18N = {
fullscreenTooltip:"全屏——面板占据整个窗口宽度", fullscreenTooltip:"全屏——面板占据整个窗口宽度",
discardedTooltip:"显示/隐藏已放弃的节点及其子树", discardedTooltip:"显示/隐藏已放弃的节点及其子树",
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化", cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
foldMenu:"展开和折叠子树",
foldExpandAll:"全部展开",
foldCollapseSmall:"折叠小于 M 的节点",
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计", implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
ghostTooltip:"从 M 号起,元素应进一步细分。", ghostTooltip:"从 M 号起,元素应进一步细分。",
jumpHint:"Alt+点击:跳转到文本中的该行", jumpHint:"Alt+点击:跳转到文本中的该行",
@@ -2099,6 +2169,9 @@ const I18N = {
fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用", fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用",
discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示", discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示",
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示", cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
foldMenu:"サブツリーの展開と折りたたみ",
foldExpandAll:"すべて展開",
foldCollapseSmall:"サイズ M 未満を折りたたむ",
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う", implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。", ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
jumpHint:"Alt+クリック:テキストの該当行へ移動", jumpHint:"Alt+クリック:テキストの該当行へ移動",
+7
View File
@@ -21,6 +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):
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; }
/* 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){
if(showDiscarded) return n.children; if(showDiscarded) return n.children;
+14
View File
@@ -397,6 +397,20 @@
.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
+31 -1
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 } from '../src/model.js'; import { initialCollapsed, computeCheapSet, belowM } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js'; import { renderTreeHtml } from '../src/render.js';
const t = key => key; const t = key => key;
@@ -252,3 +252,33 @@ describe('Günstigster Pfad an eingeklappten Knoten', () => {
expect(stations(html)).toEqual(['N']); expect(stations(html)).toEqual(['N']);
}); });
}); });
/* 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)]);
it('trifft XS und S, nicht M und darüber', () => {
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],
['e', false], ['f', false]]);
});
it('trifft Knoten OHNE Größenangabe nicht', () => {
/* Der günstigste Pfad rechnet fehlende Größen als M (D18) das ist eine
Kostenannahme, keine Aussage des Autors, und faltet hier nichts zu. */
expect(groessen('[ ] ohne')).toEqual([['ohne', false]]);
});
it('ist von Status und Optionalität unabhängig', () => {
expect(groessen('[x] a (S)\n[-] b (S)\n[ ] c (L)').map(([, b]) => b))
.toEqual([true, true, false]);
});
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);
});
});