Im horizontalen Fächer kostet jedes optionale Geschwister eine eigene Spalte — Breite für gerade das, was am entbehrlichsten ist. Zwei oder mehr aufeinanderfolgende optionale ENDknoten werden deshalb als Kaskade gestapelt: erste Stufe am Stiel von oben wie ein gewöhnliches Kind, jede weitere eine Stufe tiefer und weiter rechts, an einem gestrichelten Winkel, der an der linken Kante der vorigen Stufe herabfällt und waagerecht in ihren Kreis einbiegt. Verworfen: die naheliegendere SENKRECHTE Spalte unter einem Anschlusspunkt. Schmaler und in einem Bruchteil der Zeit gebaut — sähe aber fast genau aus wie eine any-of-Gruppe (gestapelte Spalte an gestrichelter Leiste), unterschieden nur durch Tinte statt Grau. Genau diese Verwechslung zu vermeiden ist der Zweck von Kreis und Farbgebung. Ebenfalls verworfen: eine echte Diagonale — Rahmenkanten sind achsenparallel, sie bräuchte die SVG-Ebene und wäre eine zweite Zeichenebene neben allen anderen Linien, nachzuführen bei jedem Rendern, Moduswechsel und Zoom. Rechte Winkel geben denselben Kaskaden-Eindruck im vorhandenen Mechanismus. - Nur Endknoten: Der Platzgewinn entsteht gerade daraus, dass kein Teilbaum mitgestapelt wird — ein optionaler Knoten MIT Kindern spart nichts und behält seine Spalte. Technisch dasselbe: die Stufengeometrie setzt Zellenhöhe == Knotenhöhe voraus. Geprüft wird genau das (ein Element-Kind, und das ist der Knoten) — schließt auch den Geister-Knoten aus. - Gruppiert wird in app.js, nicht im Renderer. Die DOM-Ebene gibt es semantisch nicht und müsste in den drei übrigen Anordnungen (vertikal, kompakt, all-of unter any-of) wieder neutralisiert werden, jede mit hand-getunter Geometrie. `display:contents` löst das nicht: richtet die Boxen, aber die >-Selektoren jener Regeln greifen weiter auf dem DOM. applyOptStairs() baut die Gruppe nur im Fächer und löst sie beim Moduswechsel auf — dieselbe Kategorie wie alignStems() und drawCheapPath(); SPEC §9 bleibt für den RENDERER wörtlich wahr, Lese- und Fokusreihenfolge unberührt. - Der Export folgt der Kaskade. Erste Fassung reihte alle Stufen flach als Kinder ein und ließ den Export selbst routen — die Linie zur dritten Stufe lief dann HINTER der zweiten hindurch und las sich wie eine Eltern-Kind- Beziehung. Keine Schönheitsfrage, sondern eine falsche Aussage über die Struktur. Jetzt hängt nur die erste Stufe an der Sammelleiste. Nicht durch Tests gedeckt: applyOptStairs() arbeitet auf dem DOM, und für app.js gibt es keine Testumgebung (die Suite prüft die headless-Module). Verifiziert im Browser: Kaskade in horizontal (drei Stufen, Winkel treffen die Kreise), Moduswechsel horizontal↔kompakt↔vertikal mehrfach hin und zurück — Gruppe entsteht und löst sich rückstandsfrei auf, Knotenzahl (9) und Dokumentordnung in allen drei Modi unverändert, keine übrig gebliebenen --i-Variablen. SVG-Export gerendert geprüft: drei Kreise, Winkelkette statt kreuzender Linien. Vitest 60/60 (Renderer unverändert). Im mitgelieferten Werkbaum-Plan stehen die beiden Editor-Zugaben jetzt nebeneinander und zeigen die Treppe. Reihenfolgeänderung ist für „Was ist neu?" folgenlos — die Knoten-Identität ist der Label-Pfad (D28), test-abgedeckt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Werkbaum
English · Deutsch
▶ Try it live: https://werkbaum.javagil.de (stable) · https://mhoennig.github.io/werkbaum/ (latest build 🚧)
A textual, Markdown-like notation for work breakdown structures with and/or decomposition — plus a live editor that renders it as a diagram.
[~] Werkbaum (XL) https://wiki.example.de/relaunch
- [~] Document store
| [x] Text file with copy+paste in the frontend (S)
- [x] Parser
- [x] Text input field in the frontend
| [ ] Backend
- [~] Display/rendering (XL)
- [/] H (S) @anna
- [ ] CMS integration (M)
| [ ] WordPress
| [?] Headless CMS
- = mandatory sub-package (all of, side by side in the diagram) ·
| = alternative (any of, stacked) · […] = status ·
(M) = T-shirt effort · @name = responsibility · %% = comment.
Usage
Open the hosted editor — edit text on the left, the diagram is built live on the right. Toggles: transposed (narrow) layout, show discarded elements.
Jumping between diagram and text
Diagram and editor are linked in both directions:
- Alt+click a node (keyboard: Alt+Enter, touch: long press) selects its line in the text editor — collapsed editor panel opens first.
- Moving the cursor in the text highlights the matching node in the diagram.
A plain click keeps its old meaning: a node carrying a URL still opens it.
Loading a diagram from a URL
The editor can pull its notation text from an external text file via the
sourceUrl query parameter — handy for sharing a plan or keeping the source in
Git or a wiki:
https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-0.werkbaum
Ready-made examples in docs/examples/ — open them one
after another, each becomes its own document, so afterwards you can switch
between all of them in the editor title bar:
| Example | Shows |
|---|---|
| ▶ 0 · Online shop relaunch | a software project with all eight states |
| ▶ 1 · New kitchen | a non-software plan, lots of alternatives — good for the cheapest-path toggle |
| ▶ 2 · Community conference | a wide plan with many people; compare horizontal vs. compact |
| ▶ 3 · Three workstreams | several roots = several trees side by side |
| ▶ Werkbaum itself | what exists today and where it could go |
(The links point at the latest build 🚧; the stable instance supports
sourceUrl from its next production deploy on.)
The loaded text becomes its own document whose name is the URL, so your own documents stay untouched; the same link updates that one document instead of piling up copies. The URL is the source of truth — it is re-fetched on every load, so local edits to it do not survive a reload.
Notation files use the extension .werkbaum (UTF-8; see docs/SPEC.md §12
and D24) — a convention, not a contract: sourceUrl reads any text file over
http(s) regardless of extension or content type.
Caveat — CORS: the browser only fetches a foreign host if it sends
Access-Control-Allow-Origin. raw.githubusercontent.com and GitLab raw links
do; an arbitrary web server often does not. If loading fails the previous
content stays and a warning explains why. Only http/https are allowed.
Running it locally
The editor source now lives as ES modules under frontend/src/, bundled by
Vite (see docs/DECISIONS.md D19). Because browsers block
ES-module imports over file://, opening frontend/index.html directly no
longer works — use one of:
cd frontend
npm install # once
npm run dev # dev server at http://localhost:8137
npm test # Vitest unit tests
npm run build # -> frontend/dist/index.html (single self-contained file)
The built dist/index.html inlines all JS, CSS and the favicon, so that file
does open standalone via file:// and is what gets deployed.
Build hint & your own production install
Non-production builds carry a small hint next to the title (symbol + tooltip) so it's clear this is not the stable instance:
- Dev server (
npm run dev) → 🔧 "Preview – local development build" - Default build (
npm run build, incl. the GitHub Pages deploy) → 🚧 "Latest build – may still be buggy"
For your own production install the hint is switched off:
cd frontend
npm ci # or: npm install
npm run build:prod # -> frontend/dist/index.html WITHOUT the hint
build:prod runs Vite in mode prod; frontend/.env.prod sets
VITE_BUILD_BADGE=none, so the badge code is tree-shaken away entirely (it isn't
even present in the output source). Drop the resulting dist/index.html onto your
web space/server (standalone, file://-capable). Details: app.js
(mountBuildBadge), docs/DECISIONS.md D16.
Two things otherwise handled only by the Pages workflow, to fix up yourself when
self-hosting: the footer MIT-License link is relative to ../LICENSE (place
that file one level above index.html, or adjust the link), and the version
number stays the source placeholder 1.0 (the workflow otherwise replaces it
from VERSION + commit count).
Easier: scripts/deploy-prod.sh. The script runs exactly that prod build
and both fix-ups (drop LICENSE alongside + straighten the link, footer
version + commit link just like the Pages workflow) and mirrors the result to a
server via rsync/SSH:
scripts/deploy-prod.sh mih00@mih00.hostsharing.net:~/doms/werkbaum.javagil.de/htdocs-ssl
The target is either this argument or — with no argument — the DEPLOY_TARGET
variable from the git-ignored file .env (template:
.env.example, copy it once and fill in the path). An argument
takes precedence.
Without -y it first shows a --dry-run preview and asks for confirmation.
rsync --delete ensures nothing old is left at the target — so the target
directory is treated as exclusive to Werkbaum (an in-flight Let's Encrypt
challenge under .well-known/ is spared via --filter=protect, and web-friendly
755/644 permissions are enforced). (Hostsharing: a directly-served domain is
served from …/htdocs-ssl/; as a subdomain under another domain the web
directory would be …/subs-ssl/<name>/.)
Project documents
frontend/— editor ·backend/— Kotlin/Spring (scaffold to follow, see backend/README.md)docs/SPEC.md— normative language definitiondocs/DECISIONS.md— design decisions with rationaledocs/ROADMAP.md— Mermaid plugin, Taiga integration, Tenzudocs/TASKS.md— open tasks (checkboxes)docs/brand/BRAND.md— logo, wordmark, usage rulesdocs/design/— design derivation of the brandCLAUDE.md— project context for Claude Code
Note: The detailed project documentation under
docs/is maintained in German, the project's source language (seeCLAUDE.md). This README is available in English and German.
Deployment
The editor is published as a static page on GitHub Pages via GitHub
Actions (workflow: .github/workflows/pages.yml). Triggered on every push to
main and manually (workflow_dispatch).
The workflow sets up Node, runs npm ci, npm test (Vitest) and npm run build
(Vite), then publishes the bundled frontend/dist/index.html as index.html at
the root URL, plus LICENSE for the MIT link in the footer. The favicon is
already inlined into the build, so nothing else needs copying; only the runtime
../LICENSE link is straightened on the copy. A failing test blocks the deploy.
backend/ and the remaining docs/ are not published.
While assembling the site, the workflow also stamps the version into the footer:
major.minor comes from the VERSION file (bumped by an explicit "bump
commit"), and the micro part is the number of commits since that last bump —
so it grows with every commit and resets to 0 right after a bump
(Werkbaum 1.0.0, 1.0.1, … then bump VERSION to 1.1 → 1.1.0). Nothing is
written back to the repo. In the footer the name Werkbaum links to the
repository, while the version number links to that exact commit
(…/commit/<sha>). Opened locally, the editor shows the source placeholder
(Werkbaum 1.0).
One-time setup: In the repo settings under Pages, select Source = "GitHub Actions". The repo must be public for this (GitHub Pages via Actions is only available for private repos on a paid plan).
License
MIT — see LICENSE. © 2026 Michael Hönnig. The bundled IBM Plex fonts
are under the SIL Open Font License 1.1 (see LICENSE and
frontend/src/fonts/OFL.txt).
