Files
werkbaum/frontend/index.html
T
mhoennigandClaude Opus 4.8 aa3e087ee6 notation: + für optionale Knoten — Zugaben statt Pflicht oder Alternative
Die Notation kannte nur „erforderlich" (-) und „wählbar" (|). Ein einzelnes
zusätzliches Feature — weder nötig noch Alternative zu etwas anderem — musste
als normales -Kind notiert werden und log damit. Feature-Modelle (FODA)
unterscheiden seit den 90ern mandatory/optional/alternative; `+` ergänzt die
fehlende zweite Beziehung. Mnemonik: `-` Teilpaket, `+` Zugabe, `|` Alternative.

Anlass ist nicht die Optik, sondern der günstigste Pfad (D18): markCheapest()
lief bei all-of über ALLE Kinder, jede Zugabe steckte also im errechneten
Minimum — systematisch zu groß, und zwar umso mehr, je ehrlicher ein Plan auch
die Kür notiert. Sichtbar wird es beim Alternativenvergleich: eine Alternative
mit teurer Zugabe verlor gegen eine schlichtere, obwohl die Zugabe gar nicht
dazugehört.

- Parser setzt `optional:true` und lässt `type:'and'` stehen — `+` gehört zum
  Knoten, nicht zur Gruppe. Dadurch bleiben gateOf() und die mixedGate-Warnung
  unverändert richtig: sie meldet weiter genau dann, wenn | mit -/+ gemischt
  wird. `-` neben `+` ist erlaubt und still — „diese drei sind nötig, das hier
  wäre schön" ist der Normalfall, nicht der Fehlerfall.
- Aus dem Pfad fallen optionale Knoten über pathChildren() heraus, die eine
  Stelle, die cheapestCost() und markCheapest() gemeinsam nutzen — deshalb
  wirkt es samt Teilbaum.
- Darstellung: hohler Kreis mittig auf der Knotenkante, wo der Abzweig
  auftrifft (FODA-Konvention). Bewusst KEIN dritter Linienstil: im kompakten
  Modus codiert allein der Stil das Gate (D15), gepunktet müsste sich dort
  gegen gestrichelt-grau behaupten. Der Kreis ist orthogonal dazu.
  CSS-Grundfall ist gestapelt (links/50 %), Ausnahme der horizontale Fächer
  (oben/50 %), Rück-Ausnahme der gestapelte all-of-Teilbaum unter any-of (D18)
  — andersherum wären es vier Ausnahmen statt zwei.
- SVG-Export zeichnet den Kreis NACH den Knoten (optMarks, Schritt 3a): er
  liegt halb außerhalb der Box und würde sonst vom Knoten-Rechteck überdeckt.
- Legende, Knoten-Tooltip und aria-label in allen neun Sprachen; hint_root
  formuliert die neue Mischregel.

Bekannte Schwäche, bewusst in Kauf genommen: Bei aktivem Pfad-Umschalter wird
der optionale Knoten ausgeblasst (opacity:.32) — und mit ihm sein Kreis, der
die Erklärung dafür wäre. `opacity` am Elternteil schlägt auf Pseudoelemente
durch, das lässt sich nicht zurücknehmen. Das Zurücktreten ist hier die
Hauptaussage (wie bei nicht gewählten Alternativen), Tooltip/aria/Legende
liefern die Begründung nach.

Verhaltensänderung: `+` am Zeilenanfang ist jetzt ein Zeichen und nicht mehr
Teil des Labels (`+ 5 % Puffer` ergibt „5 % Puffer"). Test-abgedeckt.

SPEC §1/§3/§9/§10 zuerst, dann Code (CLAUDE). Das kanonische Beispiel in §10
enthält jetzt eine `+`-Zeile und ist mit der Test-Fixture wieder deckungsgleich.
Der mitgelieferte Werkbaum-Plan markiert Drucklayout, „Was ist neu?" und die
Personenfarben als Zugaben — „Was ist neu?" war der Auslöser der Frage.

Verifiziert: 12 neue Tests (Parser setzt optional/type; Status/Größe/Tags/URL
am +-Knoten; führendes + wird verbraucht; Pfad lässt Zugabe samt Teilbaum aus;
Kosten des Elternknotens ohne Zugabe; Alternativenvergleich ohne Zugaben;
optionale Knoten bleiben sichtbar; opt-Klasse; keine Warnung bei -/+; Warnung
bei |/+; aria-label). Vitest 58/58, Snapshot zeigt `node opt` OHNE `cheap`.
Im Browser in allen drei Modi angesehen: Kreis sitzt in horizontal oben mittig,
in vertikal und kompakt links auf halber Höhe, jeweils genau auf dem Ende des
Abzweigs; SVG-Export enthält beide Kreise an denselben Punkten (gerendert
geprüft, nicht nur im Quelltext).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:58:52 +02:00

177 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Werkbaum PSP &amp; Lean Pathfinding Editor (Work Breakdown Structure)</title>
<meta name="description" content="Werkbaum ist ein textbasierter Editor für PSP/Lean Pathfinding und Projektstrukturpläne (WBS): Schreibe die Struktur als Text, das And/Or-Diagramm entsteht live. Auch für Feature-Trees &amp; Requirements.">
<link rel="icon" type="image/svg+xml" href="../docs/brand/favicon.svg">
</head>
<body>
<header>
<div class="header-row">
<h1 style="display:flex;align-items:center;gap:11px">
<svg width="34" height="34" viewBox="0 0 64 64" aria-hidden="true">
<rect x="22" y="5" width="20" height="12" rx="3.5" fill="#243447"/>
<path d="M32 17 V22 M14 22 H50 M14 22 V27 M50 22 V27" fill="none" stroke="#243447" stroke-width="3"/>
<rect x="4" y="27" width="20" height="12" rx="3.5" fill="#fff" stroke="#243447" stroke-width="3"/>
<rect x="40" y="27" width="20" height="12" rx="3.5" fill="#fff" stroke="#243447" stroke-width="3"/>
<path d="M50 39 V46" fill="none" stroke="#0F766E" stroke-width="3" stroke-dasharray="4 4"/>
<rect x="40" y="46" width="20" height="12" rx="3.5" fill="#fff" stroke="#0F766E" stroke-width="3"/>
</svg>
<span data-i18n-title="brandTooltip" title="„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).">Werk<span style="color:var(--or)">baum</span></span>
</h1>
<div class="header-tools">
<button type="button" id="fsToggle" class="fsbtn" aria-pressed="false" data-i18n-title="fullscreenTooltip" data-i18n-aria="fullscreenTooltip" title="Vollbild Panels nutzen die ganze Fensterbreite" aria-label="Vollbild Panels nutzen die ganze Fensterbreite">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
</button>
<div class="langsel" role="radiogroup" aria-label="Sprache / Language">
<button type="button" data-lang="de" title="Deutsch">DE</button>
<button type="button" data-lang="en" title="English">EN</button>
<button type="button" data-lang="es" title="Español">ES</button>
<button type="button" data-lang="fr" title="Français">FR</button>
<button type="button" class="lang-extra" data-lang="pl" title="Polski">PL</button>
<button type="button" class="lang-extra" data-lang="ru" title="Русский">RU</button>
<button type="button" class="lang-extra" data-lang="hi" title="हिन्दी">HI</button>
<button type="button" class="lang-extra" data-lang="zh" title="中文"></button>
<button type="button" class="lang-extra" data-lang="ja" title="日本語"></button>
<button type="button" id="langMore" class="langmore" aria-expanded="false" data-i18n-title="langMore" data-i18n-aria="langMore" title="weitere Sprachen" aria-label="weitere Sprachen"></button>
</div>
</div>
</div>
<p><span class="sub-full" data-i18n="subtitle">Werkbaum Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree/Requirements-Baum Diagramm-Editor.</span><span class="sub-short" data-i18n="subtitleShort">WBS, Feature- &amp; Requirements-Bäume als Text</span></p>
</header>
<div class="app" id="app">
<div class="panel editor">
<div class="panel-head">
<span class="doctrigger" id="docTrigger" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false" data-i18n-title="docSwitchTooltip" data-i18n-aria="docSwitchTooltip" title="Dokument wählen oder verwalten" aria-label="Dokument wählen oder verwalten">
<span class="docname" id="docName"></span>
<svg class="doc-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</span>
<div class="docmenu" id="docMenu" role="menu" data-i18n-aria="docMenuAria" aria-label="Dokumente" hidden>
<div class="docmenu-list" id="docList"></div>
<div class="docmenu-actions">
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button>
<button type="button" class="docact" id="docRename" data-i18n="docRename">Umbenennen</button>
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
</div>
</div>
<button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button>
<button type="button" class="copybtn legendbtn" id="legendBtn" data-i18n-title="legendTooltip" data-i18n-aria="legendTooltip" title="Legende ein-/ausblenden" aria-label="Legende ein-/ausblenden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</button>
<div class="winbtns" aria-label="Editor">
<button type="button" class="winbtn" data-state="b" data-i18n-title="minimize" title="minimieren"></button>
<button type="button" class="winbtn" data-state="normal" data-i18n-title="normal" title="normal"></button>
<button type="button" class="winbtn" data-state="a" data-i18n-title="maximize" title="maximieren"></button>
</div>
</div>
<div class="editor-body">
<textarea id="src" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
<div class="hint-gutter" id="hintGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="hintGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Editor und Legende größenverändern" hidden></div>
<div class="agenda" id="agenda">
<button type="button" class="agenda-summary" id="agendaToggle" aria-expanded="false" aria-controls="hint" data-i18n="agenda">Agenda</button>
<div class="hint" id="hint">
Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.<br>
<code>-</code>&nbsp; Teilpaket, alle erforderlich <em>(all of)</em><br>
<code>+</code>&nbsp; Zugabe, nicht erforderlich <em>(optional)</em><br>
<code class="or-code">|</code>&nbsp; Alternative, eine wählen <em>(any of)</em><br>
Zeile ohne Zeichen = Wurzelknoten. <code class="or-code">|</code> nicht mit <code>-</code>/<code>+</code> mischen.<br>
Status als Kästchen nach dem Zeichen, z.&nbsp;B. <code>- [~] Frontend</code>:
<div class="chips">
<span class="chip st-idee">[?] Idee</span>
<span class="chip st-geplant">[&nbsp;] geplant</span>
<span class="chip st-arbeit">[~] in Arbeit</span>
<span class="chip st-durchstich">[/] Durchstich</span>
<span class="chip st-fertig">[x] fertig</span>
<span class="chip st-prod">[^] in Produktion</span>
<span class="chip st-verworfen">[-] verworfen</span>
</div>
Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:<br>
<code>- [ ] Backend (L) https://…</code><br>
Ab <code>(M)</code> gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.<br>
Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende.
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
</div>
</div>
</div>
</div>
<div class="gutter" id="gutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="gutterAria" aria-label="Bereiche größenverändern" title="Ziehen zum Verschieben, Doppelklick setzt zurück"></div>
<div class="panel right">
<div class="panel-head">
<span data-i18n="diagramTitle">Diagramm</span>
<button type="button" class="copybtn togglebtn" id="showc" aria-pressed="false" data-i18n-title="discardedTooltip" data-i18n-aria="discardedTooltip" title="Verworfene Knoten samt Teilbaum ein-/ausblenden" aria-label="Verworfene Knoten samt Teilbaum ein-/ausblenden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="2" stroke-dasharray="3 2.5"/><path d="M7 12h10"/></svg>
</button>
<button type="button" class="copybtn togglebtn" id="cheapBtn" aria-pressed="true" data-i18n-title="cheapTooltip" data-i18n-aria="cheapTooltip" title="Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück" aria-label="Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="6" r="2.5"/><circle cx="19" cy="18" r="2.5"/><path d="M7.5 6H14a3 3 0 0 1 0 6H10a3 3 0 0 0 0 6h6.5"/></svg>
</button>
<button type="button" class="copybtn freshbtn" id="freshBtn" title="Neu in Produktion" aria-label="Neu in Produktion" hidden>
<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>
<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">
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="4" rx="1"/><rect x="2.5" y="17" width="6" height="4" rx="1"/><rect x="15.5" y="17" width="6" height="4" rx="1"/><path d="M12 7V13M5.5 13H18.5M5.5 13V17M18.5 13V17"/></svg></span>
</label>
<label data-i18n-title="modeKompakt" title="Kompakt alles nach unten, platzsparend">
<input type="radio" name="layout" value="kompakt" data-i18n-aria="modeKompakt" aria-label="Kompakt alles nach unten">
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="6" height="4" rx="1"/><path d="M6 7V20"/><path d="M6 11H10M6 17H10"/><rect x="10" y="9" width="9" height="4" rx="1"/><rect x="10" y="15" width="9" height="4" rx="1"/></svg></span>
</label>
<label data-i18n-title="modeVertikal" title="Vertikal Baum nach rechts, Diagramm neben dem Editor">
<input type="radio" name="layout" value="vertikal" data-i18n-aria="modeVertikal" aria-label="Vertikal Baum nach rechts">
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span>
</label>
</div>
<button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTooltip" data-i18n-aria="copyDiagramTooltip" title="Diagramm als PNG-Bild in die Zwischenablage kopieren" aria-label="Diagramm als PNG-Bild in die Zwischenablage kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button>
<div class="dlgroup" id="dlGroup">
<button type="button" class="copybtn dltrigger" id="dlTrigger" aria-haspopup="true" aria-expanded="false" data-i18n-title="downloadMenu" data-i18n-aria="downloadMenu" title="Diagramm herunterladen (SVG/PNG)" aria-label="Diagramm herunterladen (SVG/PNG)">
<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 3v12M8 11l4 4 4-4M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>
</button>
<div class="dlmenu">
<button type="button" class="copybtn dlbtn" id="dlDiagram" data-i18n-title="downloadDiagramTooltip" data-i18n-aria="downloadDiagramTooltip" title="Diagramm als SVG-Datei herunterladen" aria-label="Diagramm als SVG-Datei herunterladen">
<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 3v12M8 11l4 4 4-4M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg><span class="dl-label">SVG</span>
</button>
<button type="button" class="copybtn dlbtn" id="dlDiagramPng" data-i18n-title="downloadPngTooltip" data-i18n-aria="downloadPngTooltip" title="Diagramm als PNG-Datei herunterladen" aria-label="Diagramm als PNG-Datei herunterladen">
<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 3v12M8 11l4 4 4-4M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg><span class="dl-label">PNG</span>
</button>
</div>
</div>
<div class="winbtns" aria-label="Diagramm">
<button type="button" class="winbtn" data-state="a" data-i18n-title="minimize" title="minimieren"></button>
<button type="button" class="winbtn" data-state="normal" data-i18n-title="normal" title="normal"></button>
<button type="button" class="winbtn" data-state="b" data-i18n-title="maximize" title="maximieren"></button>
</div>
</div>
<div class="diagram"><ul class="tree" id="out"></ul></div>
<div class="warnings" id="warn" role="status" aria-live="polite"></div>
<div class="zoomctl" data-i18n-aria="zoomAria" aria-label="Zoom (Strg/Cmd + Mausrad)">
<button type="button" id="zoomToggle" class="zoomToggle" data-i18n-title="zoomTooltip" title="Zoom anzeigen" aria-label="Zoom anzeigen">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="6"/><path d="m15 15 4 4M10 7v6M7 10h6"/></svg>
</button>
<div class="zoomcontrols">
<button type="button" id="zoomOut" data-i18n-title="zoomOut" title="verkleinern"></button>
<button type="button" id="zoomReset" data-i18n-title="zoomReset" title="zurücksetzen">100&nbsp;%</button>
<button type="button" id="zoomIn" data-i18n-title="zoomIn" title="vergrößern">+</button>
</div>
</div>
</div>
</div>
<footer class="site-footer"><a href="https://github.com/mhoennig/werkbaum" target="_blank" rel="noopener">Werkbaum</a>&nbsp;<a class="ver" href="https://github.com/mhoennig/werkbaum/commit/main" target="_blank" rel="noopener">1.0</a> © 2026 <a href="https://michael.hoennig.de" target="_blank" rel="noopener">Michael Hönnig</a> · <span class="vibe" title="Fable 5 and Opus 4.8">Built with Claude Code</span> <a href="../LICENSE" target="_blank" rel="noopener">License</a> · <a href="https://michael.hoennig.de/imprint.html" target="_blank" rel="noopener" data-i18n="imprint">Impressum</a> · <a id="privacyLink" href="https://michael.hoennig.de/datenschutzerklaerung.html" target="_blank" rel="noopener" data-i18n="privacy">Datenschutz</a></footer>
<script type="module" src="./src/app.js"></script>
</body>
</html>