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:
co-authored by
Claude Fable 5
parent
7e2c7cd3e0
commit
9bbe7d78cc
@@ -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
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -699,6 +699,15 @@ 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.
|
||||
- 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.
|
||||
|
||||
+6
-1
@@ -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,
|
||||
D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die
|
||||
Ü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
|
||||
kein `render()` — der Aufrufer muss dann selbst zeichnen. Sonst bleibt das
|
||||
Bild stehen (aufgefallen beim Aufklappen eines nur per Überlagerung
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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">
|
||||
|
||||
+88
-15
@@ -1,6 +1,6 @@
|
||||
import './style.css';
|
||||
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 { formatWarning } from './warnings.js';
|
||||
import { padUrls } from './remote.js';
|
||||
@@ -969,23 +969,44 @@ function writeFoldToText(line, collapsed){
|
||||
klein[line-1] = setFoldMark(klein[line-1], collapsed ? '>' : null);
|
||||
let neu = klein.join('\n');
|
||||
|
||||
if(!foldStateMatches(neu, 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;
|
||||
}
|
||||
if(!foldStateMatches(neu, want)) return writeAllFoldMarks(roots, want);
|
||||
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
|
||||
der Zustand — die Überlagerungen werden dann geleert, damit sie ihn nicht
|
||||
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();
|
||||
});
|
||||
|
||||
/* 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 = {
|
||||
de: {
|
||||
@@ -1470,6 +1516,9 @@ 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",
|
||||
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).",
|
||||
@@ -1550,6 +1599,9 @@ 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",
|
||||
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).",
|
||||
@@ -1630,6 +1682,9 @@ 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",
|
||||
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).",
|
||||
@@ -1710,6 +1765,9 @@ 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",
|
||||
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).",
|
||||
@@ -1790,6 +1848,9 @@ 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",
|
||||
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).",
|
||||
@@ -1870,6 +1931,9 @@ const I18N = {
|
||||
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
||||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||||
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
||||
foldMenu:"Развернуть и свернуть поддеревья",
|
||||
foldExpandAll:"развернуть все",
|
||||
foldCollapseSmall:"свернуть меньше размера M",
|
||||
implicitSizeTooltip:"Размер не указан — для оценки затрат принят как M",
|
||||
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
|
||||
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
|
||||
@@ -1950,6 +2014,9 @@ const I18N = {
|
||||
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
||||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||||
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
||||
foldMenu:"उपवृक्ष खोलें और समेटें",
|
||||
foldExpandAll:"सभी खोलें",
|
||||
foldCollapseSmall:"आकार M से छोटे समेटें",
|
||||
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए M माना गया",
|
||||
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
|
||||
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
|
||||
@@ -2019,6 +2086,9 @@ const I18N = {
|
||||
fullscreenTooltip:"全屏——面板占据整个窗口宽度",
|
||||
discardedTooltip:"显示/隐藏已放弃的节点及其子树",
|
||||
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
||||
foldMenu:"展开和折叠子树",
|
||||
foldExpandAll:"全部展开",
|
||||
foldCollapseSmall:"折叠小于 M 的节点",
|
||||
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
|
||||
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
||||
jumpHint:"Alt+点击:跳转到文本中的该行",
|
||||
@@ -2099,6 +2169,9 @@ const I18N = {
|
||||
fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用",
|
||||
discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示",
|
||||
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
||||
foldMenu:"サブツリーの展開と折りたたみ",
|
||||
foldExpandAll:"すべて展開",
|
||||
foldCollapseSmall:"サイズ M 未満を折りたたむ",
|
||||
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
|
||||
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
||||
jumpHint:"Alt+クリック:テキストの該当行へ移動",
|
||||
|
||||
@@ -21,6 +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; }
|
||||
|
||||
/* Sichtbare Kinder: verworfene ausblenden, außer showDiscarded ist gesetzt. */
|
||||
export function visibleChildren(n, showDiscarded){
|
||||
if(showDiscarded) return n.children;
|
||||
|
||||
@@ -397,6 +397,20 @@
|
||||
.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 } from '../src/model.js';
|
||||
import { initialCollapsed, computeCheapSet, belowM } from '../src/model.js';
|
||||
import { renderTreeHtml } from '../src/render.js';
|
||||
|
||||
const t = key => key;
|
||||
@@ -252,3 +252,33 @@ describe('Günstigster Pfad an eingeklappten Knoten', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user