6bbc91e84913284fa685b26a744d62a85817b74d
88
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6ecf3e4c69 |
feat: Knoten-IDs (#auth) parsen — doppelte ID warnt (SPEC §1, D36)
Extraktionsschritt 6 (nach den Tags): das erste alleinstehend angesetzte `#name`-Token wird die Knoten-ID (Zeichenmenge wie `@name`); weitere `#`-Token bleiben im Label (reservierte Ticket-Referenzen), `:#a,#b` und `C#` werden nicht gefressen. Doppelte ID → Warnung duplicateId an der späteren Zeile mit Nennung der ersten; die spätere gilt trotzdem. Sichtbar im Tooltip (erste Position) und als a11yId im aria-label — noch keine eigene Diagramm-Darstellung. Legendenzeile + Warntext in allen 9 Sprachen; die drei `#`-Erwähnungen im mitgelieferten Plan sind eingeklammert, damit sie Erwähnungen bleiben. 12 neue Tests (tests/ids.test.js). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d276c94840 |
feat: XOR-Gruppen (=) — genau eine Alternative (SPEC §3, D35)
Parser erkennt `=` als disjunktives Gate (nur mit folgendem Leerraum — `=SUMME(…)` bleibt Label); Mischungen melden mixedGate wie gehabt. Die XOR-Regel warnt je WEITERER realisierter Alternative (xorConflict mit Zeilennummer; realisiert = [~]/[/]/[x]/[^], siehe D35). Darstellung erbt die komplette any-of-Geometrie (ul class="or xor"), ergänzt um eine „1"-Plakette am Austritt der Sammelleiste — auch im Grafikexport. Legende (+ hint_root) und Warntext in allen 9 Sprachen; SPEC §11-Eintrag in §1/§3/§9 überführt; 13 neue Tests (tests/xor.test.js). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
837922aaa7 |
frontend: Zeilennummern im Texteditor + Alt+Klick auch im Text
Zwei Lücken, die beim Arbeiten am Text auffallen: **Zeilennummern (D33).** Die Warnungen nennen Zeilennummern („Zeile 12: …"), das Textfeld zeigte keine — man musste abzählen. Der Streifen links vom Textfeld hebt zusätzlich die Cursor-Zeile und Zeilen mit Warnung hervor. Die Zahlen stehen auf **gemessenen** Höhen (Spiegel-`div` aus D25, ein Marker je Zeile), nicht auf „Zeilenhöhe × n": Der Text bricht weich um, eine lange Zeile belegt mehrere Bildzeilen und behält doch eine Nummer. Bei 375 px Breite brechen 134 der 143 Zeilen um — gerechnet liefe der Streifen sofort davon. Dabei fiel ein älterer Fehler im Spiegel auf: `width = src.clientWidth` bei `content-box` machte ihn um genau die 32 px Innenabstand zu breit, er brach später um als das Textfeld. Mit `box-sizing:border-box` stimmen die Umbrüche nachweislich überein (170 Bildzeilen beiderseits) — das behebt nebenbei die Zielgenauigkeit des Sprungs zu langen Zeilen (D25). **Alt+Klick im Textfeld (Nachtrag zu D25).** Die Rückrichtung lief bisher allein über die Cursor-Zeile — und die scrollt bewusst nur `nearest` und nur beim Zeilenwechsel, sonst ruckelte das Diagramm beim Tippen. Als „zeig mir das" taugt sie damit nicht. Alt+Klick im Text (Tastatur Alt+Enter) zentriert den Knoten jetzt und gibt ihm den Fokus: derselbe Modifier in beide Richtungen, und mit Fokus wird daraus ein vollständiger Hin- und Rückweg. Die Legenden-Zeile nennt beide Richtungen (kein zehnter i18n-Schlüssel), 9 Sprachen nachgezogen. SPEC §9 ergänzt, D33 angelegt, Plan-Dokument und frontend/CLAUDE.md nachgeführt. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
953277ab83 |
frontend: Fokusmarke bekommt einen eigenen Strahlenkranz in Petrol
Der mit der Cursor-Zeile geteilte Ring war zu leise — „das erkennt man zu wenig" (Nutzer). Damit fällt auch die in D32 als Preis notierte Zweideutigkeit weg: Sie war das Zugeständnis für die Sparsamkeit, und wenn die Sparsamkeit nicht funktioniert, ist das Zugeständnis nicht mehr zu rechtfertigen. Gebaut wie der gelbe Kranz aus D28 (Schein nach außen, Füllung bleibt dem Status, kein Blinken, `z-index:2` gegen das Abschneiden durch später gemalte Geschwister) — aber in PETROL, nicht in Gelb. Die Frage stand ausdrücklich im Raum; die Antwort ist nein, weil die beiden Verschiedenes bedeuten und gleichzeitig auftreten: Gelb ist eine stehende Tatsache über den Plan („seit deinem letzten Besuch live gegangen"), Petrol ein Zuruf („schau jetzt hierhin"). Im Pad-Betrieb ist die Kombination der Normalfall — man zeigt auf das, was sich geändert hat. Wären beide gelb, könnte ein Knoten nicht beides sagen. Der Schein ist helles Teal (#14B8A6), der Ring dunkles Petrol. Die erste Fassung nahm für beides `--or` und wirkte trotz identischer Geometrie leiser als das Gelb. Der Grund ist nicht Geschmack: Ein dunkler Schein auf weißem Grund liest sich als Schatten, ein heller als Licht. Gegen die Stationspunkte des günstigsten Pfads (D18) geprüft statt angenommen — zwei Marken gezielt auf Pfad-Endknoten (mit Punkt), eine daneben (ohne): Der Kranz sitzt eng am Knoten und hat einen harten Ring, der Stationspunkt ist ein großer blasser Fleck ohne Rand. Verwechseln kann man das nicht. Kombinationen ausbuchstabiert, weil sie real vorkommen: `focusmark.current` (Tinte innen, Teal außen) und `fresh.focusmark` (Petrol-Ring innen, Gelb außen — zwei Scheine nach außen gehen nicht). Letztere steht nach der ersten: gleiche Spezifität, und bei allen drei sollen Zuruf und Neuheit gewinnen. SPEC §9 und D32 nachgezogen. |
||
|
|
6917b420d8 |
docs: eingebettetes Pad — Update-Poller als Ursache ausgeschlossen (D31)
Gemeldet: Das eingebettete Etherpad lässt sich nach einer Weile nicht mehr bearbeiten; Verdacht auf den Update-Poller. Der ist es nicht, und das ist belegt statt vermutet: - `checkForUpdates()` holt `location.href`, also den Werkbaum-Origin — es kann die Pad-Drosselung gar nicht auslösen. - Es lädt die Seite nicht neu; `checkAndShowUpdateNotification()` und `showUpdateDebug()` hängen nur `position:fixed`-Elemente an `<body>`. Kein Neuaufbau eines Containers um den Rahmen — der WÜRDE ihn neu laden, weil ein Umhängen im DOM jeden `<iframe>` neu lädt. - Gemessen über ~6 Minuten: window-Marker überlebt (kein Seiten-Reload), `load`-Zähler am Rahmen bleibt 0 (kein Rahmen-Reload), Sichtbarkeit durchgehend `visible`. Reproduziert wurde der Fehler nicht. Nach sechs Minuten lief die Verbindung noch — geprüft ohne Tippen, indem das Pad von außen geändert wurde: der Rahmen übernahm die Änderung sofort. Etherpads eigene Meldungen sind von außen nicht lesbar (fremdstämmiger Rahmen, eigener Konsolen-Kontext). Als Verdacht festgehalten, ausdrücklich unbewiesen: das `SameSite=Lax`-Cookie. Die erste Verbindung gelingt, aber beim Wiederaufbau nach einem Abbruch fehlt die Autoren-Identität — sichtbar, aber nicht mehr beschreibbar. Passt zu „nach einer Weile" und dazu, dass es im eigenen Tab nicht auftritt. Der entscheidende Test gehört dem, der es sieht: Pad im eigenen Tab öffnen. Geht es dort, ist es der Dritt-Kontext (nur serverseitig zu beheben). Klemmt es dort auch, liegt es am Pad. Behelf ist schon eingebaut: Einmal durch den Ansichts-Wähler schalten lädt den Rahmen neu (über `about:blank`). Bewusst NICHT in den Neu-laden-Knopf gelegt — der zieht nach dem Tippen Spiegel und Diagramm nach, und ein Rahmen-Reload dabei kostete jedes Mal die Schreibmarke im Pad. |
||
|
|
816a77d9e7 |
notation: !!! als Fokusmarke — ein geteilter Zeigefinger im Text
Beim gemeinsamen Arbeiten an einem Pad (D31) fehlte das Naheliegendste: „schau mal hier". Etherpad zeigt die Cursor der anderen im Pad, aber Werkbaum kommt nicht an sie heran — ein fremdstämmiger Rahmen gibt keinen DOM-Zugriff, der Klartext-Export überträgt nur Text. Eine Marke IM Text ist damit nicht der Notbehelf, sondern der einzige Weg. Und sie hat etwas, das ein Cursor nicht hat: alle sehen dieselbe Stelle. Erkannt wird `!!!` nur ALLEINSTEHEND (`(^|\s)!!!(?=\s|$)`), sonst verlöre jedes „Achtung!!!" seine Ausrufezeichen und niemand fände den Grund. Bewusst ohne Lookbehind — Safari kennt es erst ab 16.4, und die Notation soll nicht an einer Browserversion hängen. Position in der Extraktion (SPEC §1): nach den Tags; der Kommentar fällt vorher weg, eine Marke hinter `%%` wirkt also nicht. Darstellung ist DIESELBE wie die der Cursor-Zeile (D25), nicht eine vierte: neben Fokusrahmen, Strahlenkranz (D28) und Cursor-Ring wäre ein weiterer Ring ein Zeichen zu viel — es gibt nur einen Begriff „hier schauen". Preis: Im Bild ist nicht zu unterscheiden, ob die eigene Cursor-Zeile oder eine fremde Marke leuchtet; für Screenreader benennt `a11yFocusMark` es ausdrücklich. Ins Bild geholt wird nur bei ÄNDERUNG der Marke, Schlüssel ist der Label-Text statt der Zeilennummer (Umsortieren im Pad soll nicht neu scrollen). Ohne diese Bedingung zöge jeder Neubau den Blick zurück und man käme nicht weg. Beides geprüft: gleiche Marke -> Scrollstand bleibt 0, Marke gewandert -> springt auf 242 und markiert den neuen Knoten. Beim Hinsehen gefunden: Die Pfad-Inversion verschluckte den Zeigefinger. Der erste markierte Knoten im Test war eine nicht gewählte any-of-Alternative („Headless CMS") — `opacity:.32; saturate(.4)`, der Ring praktisch unsichtbar. Der Zeiger zeigte auf nichts. Dieselbe Ausnahme wie für D28 behebt es, und hier ist sie zwingender: „Sollten wir diese verworfene Alternative noch einmal ansehen?" ist einer der wahrscheinlichsten Gründe, überhaupt zu zeigen. Anders als beim optionalen Knoten (D29), wo das Zurücktreten die Aussage IST. Nicht im Druck und nicht im Grafikexport (wie die Cursor-Zeile): Die Marke sagt „schau jetzt hierhin", nicht „so ist der Plan". Nicht ins kanonische Beispiel (SPEC §10) aufgenommen — das ist zugleich Test-Fixture, ein dauerhafter Zeigefinger darin wäre eine Aussage, die niemand gemacht hat. Verworfen war `%%!` (Kommentar-Variante, bliebe harmlos liegen und könnte die Begründung tragen) — der Nutzer wählte `!!!`, weil es im Moment des Zeigens schnell zu tippen ist, und genau dann will man nicht über Syntax nachdenken. Der Preis steht in D32: die Marke bleibt sichtbar stehen, bis jemand sie löscht; Werkbaum kann sie nicht entfernen (kein Schreibzugriff auf das Pad). SPEC §1/§9 zuerst, dann D32, dann Code. 15 neue Tests (86 -> 101), Legende und i18n in allen 9 Sprachen. |
||
|
|
0a08706877 |
frontend: Pad einbetten — drei Ansichten mit Splitter, Spiegel bleibt
Das Einbetten ist möglich, ohne D25 zu opfern. Nachgemessen zuerst, ob der
Server es überhaupt zulässt: pad.hostsharing.net sendet kein
`X-Frame-Options` und keine CSP mit `frame-ancestors`, und im Versuch baute das
Pad in einem fremdstämmigen Rahmen seine inneren Editorframes auf
(`contentWindow.length === 2` = ace_outer/ace_inner), samt Werkzeugleiste,
heiler Einrückung und lebender Socket-Verbindung.
Einbetten heißt aber nicht Ersetzen: Ein cross-origin-iframe gibt keinen
DOM-Zugriff, ein ersetztes Textfeld kostete also beide Richtungen von D25
(Alt+Klick -> Zeile, Cursor-Zeile -> Knoten). Deshalb drei Ansichten, reihum
über einen Wähler in der Editor-Titelzeile:
- Pad und Text, geteilt durch einen eigenen Splitter (Mechanik und Idiom vom
Legenden-Splitter aus D26: von der Spiegelseite gezogen, Doppelklick setzt
zurück, `--pcol`/`--prow` getrennt gehalten und persistiert). Der Spiegel darf
schmal werden und trägt weiter die Sprünge — die arbeiten auf unserem
eigenen `<textarea>`.
- nur Pad — Spiegel ausgeblendet; `revealEditor()` schaltet bei einem Sprung
auf „beide", sonst zeigte der Sprung ins Nichts.
- nur Text — wie bisher, kein Rahmen.
Ein Wähler statt dreier Knöpfe, weil die Titelzeile schon Dokument, Pad, Neu
laden, Kopieren, Legende und Fensterknöpfe trägt (mobil dreifach eng, D17) —
derselbe Reihum-Griff wie beim Modus-Wähler dort. Der Zustand steckt im Symbol
(geteilt / links gefüllt / rechts gefüllt), der Tooltip nennt ihn im Klartext.
Der Rahmen bekommt nur sichtbar ein `src`, sonst `about:blank`. Das ist kein
Geiz um Bytes: Ein geladenes Pad verbindet sich per Socket und macht dich in
dessen Anwesenden-Liste sichtbar. „Nur Text" ist damit die Ansicht, die nichts
von dir verrät.
Zwei Dinge, die beim Prüfen auffielen und behoben sind:
- Eine überzählige `}` ließ das Modul gar nicht laden. Die Messung sah trotzdem
plausibel aus, weil die abgefragten Werte die statischen HTML-Vorgaben waren —
seither prüft jede Messung zuerst, ob überhaupt Knoten gerendert sind, und
`node --check` läuft vor dem Blick in den Browser.
- In „nur Pad" war der Spiegel weiterhin sichtbar (sogar breiter als das Pad):
die Regel `.src-area.pv-pad>textarea{display:none}` fehlte.
Bleibender Preis: `Set-Cookie: token=…; SameSite=Lax`. Das Cookie ist Etherpads
Autoren-Identität und wird im fremdstämmigen Rahmen nicht mitgesendet — man ist
bei jedem Laden ein neuer Autor. Nur serverseitig zu beheben
(`cookie.sameSite: "None"`), nicht in Werkbaum. Steht in D31.
Auf kleinem Bildschirm nachgemessen statt geschätzt: bei 375x812 bleiben Pad
317 px und Spiegel 180 px, per Splitter verschiebbar — brauchbar, also keine
Sonderregel für Mobil.
SPEC §9 und D31 nachgezogen; 5 neue i18n-Schlüssel in allen 9 Sprachen.
|
||
|
|
793561c873 |
frontend: Pad auf Knopfdruck holen statt zu takten — Etherpad drosselt
Der Hintergrund-Takt aus dem letzten Commit funktionierte in der Praxis nicht:
im Netzwerk-Mitschnitt stapelten sich Anfragen und wurden abgebrochen
("cancelled"). Die Ursache ist nicht Langsamkeit, sondern ein Rate Limit —
Etherpads `importExportRateLimiting` ist serienmäßig an und lässt 10 Abrufe je
90 s und IP zu; der Takt wollte 36. Jenseits davon antwortet die Gegenseite
nicht mit 429, sondern hält die Verbindung ohne Antwort offen. Nachgemessen:
12:48:12 Abbruch nach 25 s (0 Bytes)
Abbruch nach 25 s (0 Bytes)
12:50:23 HTTP 200 nach 0,436 s <- nach ~40 s Pause
Zwei Minuten totgestellt, dann sofort in 0,4 s da. Gegen eine Drosselung kann
ein Takt nicht gewinnen, er erzeugt sie. Also ein Knopf. Damit entfallen
Stabilitätstakt, Sichtbarkeits-Wächter, Anti-Stapel-Riegel und Wiederanlauf.
Der Knopf greift zudem gut mit "Was ist neu?" (D28) zusammen: drücken, und was
seither in Produktion ging, leuchtet auf.
Verhindern UND melden, beides:
- Mindestabstand 10 s zwischen zwei Abrufen. Das ergibt höchstens 9 je 90 s und
bleibt damit beweisbar unter Etherpads Budget — die Drosselung wird gar nicht
erst ausgelöst. Nach einem Abbruch sind wir schon drüber: dann das ganze
Fenster (90 s) abwarten statt weiter dagegen zu rennen.
- Der Knopf bleibt in der Sperre **klickbar** (nur `aria-disabled`, nicht
`disabled`): Ein Klick meldet den Grund und nennt die Grenze samt Restzeit.
Die Meldung landet in `#warn`, das eine Live-Region ist, wird also angesagt.
Ein `disabled` erklärt nichts.
- Eigener Warnungstyp `sourceTimeout` für den Abbruch: `sourceLoad` zeigt auf
CORS und schickte hier auf die falsche Fährte.
- `setPadBusy()` dreht das Symbol während des Abrufs — das können bis zu 20 s
sein, und ohne Zeichen wirkt der Knopf kaputt.
Zwei Fehler des Vorgänger-Commits fielen dabei auf:
- `padSource` und der Takt standen HINTER dem `await` des ersten Abrufs. Ein
einziger Fehlschlag — bei dieser Gegenseite der Normalfall — ließ das Dokument
tot liegen, ohne Knopf und ohne Wiederversuch, bis zum Neuladen der Seite.
Genau das war die vom Nutzer gemeldete Fehlermeldung.
- Kein Erfolg räumte die Warnung des ersten Versuchs weg; sie hätte gestanden,
während der Text längst da war.
Dazu: `deleteDoc()` beendet die Pad-Quelle, sonst legte ein späterer Abruf das
gelöschte Dokument wieder an und aktivierte es auch gleich.
Nebenbefunde, beide nachgemessen und in D31 festgehalten:
- Das *Tippen* im Etherpad-Editor war der letzte offene Punkt. Der Nutzer hat
` - [ ] Layout` eingerückt eingetippt; der Export gibt es zeichengenau
zurück (vier echte Leerzeichen, kein Tab, kein Listen-Markup).
- Einbetten verweigert der Server nicht: kein `X-Frame-Options`, keine CSP mit
`frame-ancestors`, und im Versuch baute das Pad in einem fremdstämmigen Rahmen
seine Editorframes auf. Preis ist `SameSite=Lax` am Autoren-Cookie (jedes
Laden ein neuer Autor; nur serverseitig zu beheben) und der Verlust beider
Richtungen von D25, falls das Textpanel ersetzt wird.
86 Tests (85 -> 86); Schreibschutz zeigt den Text jetzt in Tinte statt grau —
hier wird gelesen, der Plantext ist der Hauptinhalt.
|
||
|
|
3310cab7be |
frontend: ?etherpad= — Echtzeit-Zusammenarbeit über ein geliehenes Pad
Werkbaum hat kein Backend, und das eigentlich Schwere an gemeinsamem Bearbeiten ist das Zusammenführen gleichzeitiger Änderungen — im Plan als `[!] Merging simultaneous edits` markiert. Etherpad hat das gelöst. Also geliehen statt nachgebaut: das Pad ist die Schreibfläche, Werkbaum die Ansicht. Nachgemessen an pad.hostsharing.net, bevor irgendwas gebaut wurde: - der Klartext-Export sendet `Access-Control-Allow-Origin: *`, - das kanonische Beispiel (SPEC §10) kommt byte-identisch zurück, - der HTML-Export zeigt kein Listen-Markup — Etherpad deutet `-` nicht zur Aufzählung um und behält die führenden Leerzeichen. Das war das Risiko, das die Idee hätte erledigen können. Nicht geprüft ist das Tippen im Etherpad-Editor selbst (Tab-Einrückung, mögliches Auto-Bullet); dafür braucht es einen echten Browser. Eigener Parameter statt `?sourceUrl=`: Die URL, die ein Mensch in der Hand hat, ist die Pad-URL — `/export/txt` hängt Werkbaum selbst an. Vor allem aber lizenziert der eigene Parameter das andere Verhalten, sodass D23 unangetastet bleibt: `sourceUrl` heißt weiter „statische Datei, einmal pro Laden geholt", bestehende Links bekommen kein Polling. Name und id sind die vollständige Pad-URL (Pad-Namen sind nur pro Instanz eindeutig). Textfeld schreibgeschützt, Knopf öffnet das Pad: ohne das verschwände getippter Text beim nächsten Abruf. Schrift bleibt Tinte statt grau — hier wird gelesen, der Plantext ist der Hauptinhalt. Drei Riegel im Takt, jeder aus einem echten Fehler: - `padBusy` — im Netzwerk-Mitschnitt stapelten sich die Abrufe, weil die Gegenseite langsamer war als der Takt; eine spät eintreffende alte Antwort hätte neueren Text überschrieben, - Abbruch nach 10 s — sonst bliebe der Riegel bei hängender Gegenseite für immer zu, - `visibilityState` + `visibilitychange` — nicht im Hintergrund abrufen, aber bei Rückkehr sofort. Der Stabilitätstakt übernimmt erst beim zweiten gleichen Abruf, sonst sieht man die anderen mitten im Tippen. Die Normalisierung der Pad-Adresse liegt headless in `remote.js`, damit sie testbar ist (23 neue Tests, 60 -> 83). Im Vorschau-Browser meldet `visibilityState` „hidden" und HMR lädt bei jeder Quelländerung neu — ein Reload sieht wie eine geglückte Übernahme aus; nachgewiesen wurde die Übernahme deshalb mit einem Marker auf `window`, der einen Reload nicht überlebt. SPEC §9 zuerst, dann D31, dann Code. Der Plan bekommt den Knoten nach D30 mit `[x]`, nicht `[^]`. |
||
|
|
56c18cb8cb |
frontend: Treppe für aufeinanderfolgende optionale Endknoten
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> |
||
|
|
091e8849d8 |
frontend: optionale Abzweige gestrichelt + Stiel trifft die Knotenmitte
Zwei Nachbesserungen aus dem ersten Blick auf `+` (D29). **Gestrichelter Abzweig.** Der hohle Kreis allein war zu leise. Der Einwand gegen einen dritten Linienstil bleibt richtig, greift aber nicht so weit wie gedacht: Gestrichelt wird NUR der Abzweig zum Knoten (nicht die Sammelleiste — die gehört der ganzen Gruppe) und zwar in TINTE, während die any-of-Linien gestrichelt in Grau sind. Entscheidend ist, dass beide sich nie am selben Verteiler treffen können: | darf nach SPEC §3 nicht mit -/+ gemischt werden. Der Kreis bleibt zusätzlich — er sagt, WELCHER Knoten gemeint ist. Umgesetzt an den vorhandenen Pseudoelementen, ohne neue Zeichenebene: im Fächer der senkrechte Stiel (border-left von ::after, beim letzten Kind border-right von ::before), gestapelt der waagerechte Ast (border-top von ::before); die jeweils andere Kante ist die Leiste und bleibt durchgezogen. Dafür braucht auch das <li> die Klasse `opt` — den Abzweig zeichnet es, nicht der Knoten. Der SVG-Export zieht mit (dash || isOpt). **Stiel trifft die Knotenmitte auch waagerecht (`--stem-x`).** Dabei fiel ein älterer Fehler auf, gemeldet als „die Linie zu Wahl trifft den Knoten nicht": Im horizontalen Fächer lief der Stiel zu einem Knoten mit any-of-Kindern neben dem Knoten vorbei (gemessen 13,4 px). Dieselbe Verwechslung, die D10 senkrecht schon behoben hat, nur in der anderen Achse — der Stiel saß bei 50 % der ZELLE, und das ist nur die Knotenmitte, solange der Knoten darin zentriert steht. `li.has-or` ist aber flex-start (der Knoten steht links, damit die any-of-Sammelleiste unter ihm aufsetzt), und die Zelle ist so breit wie der Teilbaum. Rein in CSS nicht lösbar: gebraucht wird die Knotenbreite, und kein Selektor macht sie einer Elternregel zugänglich (Anchor Positioning ist Chrome-only). `alignStems()` misst deshalb nach jedem render() und in applyLayout() die Knotenmitte der betroffenen Zellen und setzt `--stem-x`; die Pseudoelemente rechnen über left:var(--stem-x, 50%) bzw. right:calc(100% - var(--stem-x, 50%)). Der Rückfallwert 50 % hält alle übrigen Zellen ohne Messung richtig, die transponierten Modi setzen left/right ohnehin fest. Messwerte werden wie in drawCheapPath() durch `zoom` zurückgerechnet. Verifiziert: Vitest 60/60 (2 neue Tests: `opt` auch am <li> neben dem Gate der eigenen Kinder; <li> ohne Attribut, wenn weder Kinder noch optional). Im Browser gemessen: Abweichung Stiel↔Knotenmitte bei „Wahl" 13,4 px → 0,0 px, alle übrigen Zellen unverändert 0,0 px ohne gesetzte Variable. Angesehen in horizontal und kompakt: gestrichelter Ast in Tinte zum Kreis, Sammelleiste durchgezogen, deutlich unterscheidbar vom gestrichelt-grauen any-of-Ast daneben. SVG-Export gerendert geprüft: 2 von 9 Tinte-Linien gestrichelt, beide Kreise am Auftreffpunkt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
7a52277b84 |
frontend: „Was ist neu?" — neu in Produktion mit gelbem Strahlenkranz
Dokumente von außen (mitgeliefert D27, ?sourceUrl= D23) ändern sich, ohne dass der Betrachter es merkt. Sie zeigen jetzt, was sich seit seinem letzten Besuch getan hat. - „Neu" heißt bewusst NICHT „Zeile hinzugefügt", sondern **neu in Produktion**: ein Knoten trägt jetzt [^] und tat es in der zuletzt gesehenen Fassung nicht. Ein Zeilendiff meldete vor allem Rauschen; die Nachricht, die zählt, ist was live gegangen ist. Nebeneffekt: es leuchtet eine Handvoll Knoten, nicht dreißig. - Basis ist die zuletzt GESEHENE Fassung je Dokument (`werkbaum-seen`), nicht die letzte Auslieferung — wer Fassungen überspringt, sieht alles seither. Fortgeschrieben wird erst beim Bestätigen, sonst wäre die Meldung nach einem Neuladen weg, bevor sie jemand bemerkt. Beim Erstkontakt leuchtet nichts. - Knoten-Identität ist der Label-Pfad, nicht die Zeilennummer: Umeinrücken und Umsortieren erzeugen keine Falschmeldungen; gleichnamige Geschwister per Index; Umbenennen gilt als neuer Knoten (der Text ist der Vertrag, D14). - Darstellung: gelber Strahlenkranz nach außen, kein Blinken (WCAG 2.2.2/2.3.1; Blinken zöge dauerhaft den Blick statt einmal zu melden). Außen, weil die Füllung dem Status gehört (SPEC §4). Zusammen mit der Cursor-Zeile: Tinte innen, Gelb außen. Nicht im Druck und nicht im Grafikexport — die Markierung hängt am persönlichen Besuchsstand. - Knopf im Diagramm-Kopf nur, wenn es etwas gibt; nennt die Anzahl, Klick bestätigt. Kein Dauer-Umschalter. - Bei selbst bearbeitetem mitgeliefertem Dokument wird nichts hervorgehoben — dort fehlt die saubere Vergleichsbasis (D27). Fehler beim Bauen, der zuerst durchrutschte: Die Menge wurde beim Laden aus einem eigenen Parse-Durchlauf berechnet. Der Zähler stimmte, aber kein Knoten leuchtete — `Set.has()` prüft Objektidentität, und die gerenderten Knoten kamen aus einem anderen Parse. `render()` bildet die Menge jetzt bei jedem Durchlauf aus den gerade geparsten Wurzeln; vorgehalten wird nur der geparste Basisbaum. Verifiziert: 9 neue Modelltests (Statuswechsel, neuer [^]-Knoten, nicht-[^] ignoriert, unverändert ignoriert, Erstkontakt leer, Umsortieren/Tabs ohne Falschmeldung, gleiches Label unter verschiedenen Eltern, gleichnamige Geschwister, verworfene). Im Browser: Erstkontakt setzt nur die Basis (Knopf versteckt); zurückgedrehte Basis lässt genau die erwarteten Knoten leuchten (Zähler 3, dann 1); Bestätigen räumt auf und überlebt das Neuladen; ohne Bestätigen bleibt die Meldung über ein Neuladen stehen. Vitest 46/46. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
68abdc6625 |
docs+frontend: Gemeinsam arbeiten in Roadmap und Werkbaum-Plan
- docs/ROADMAP.md: neuer Abschnitt „Gemeinsam an einem Diagramm arbeiten" in drei Stufen — Lesen teilen (?sourceUrl=, vorhanden), asynchron über Git (heute schon möglich, weil der Plan Text ist: git diff/blame sind lesbar; Ausbau = Backend committet jede Änderung), live tippen (Transport ist der kleinere Teil, das Zusammenführen gleichzeitiger Änderungen der eigentliche; Text-CRDT passt wegen D14 ohne eigene Merge-Logik). Notiert auch, warum Git direkt aus dem Browser kein Live-Sync ist (Sync auf Zuruf, CORS-Proxy nötig, Konfliktmarker mitten in der Notation) und was vor einer Entscheidung offen ist (erste Laufzeit-Abhängigkeit, Commit-Granularität, Rechte). - docs/examples/example-werkbaum.werkbaum: passender Zweig „Working together on one plan" (94 Knoten, parst ohne Warnung). Dabei aufgefallen: Das mitgelieferte Dokument wurde nur EINMAL angelegt — die Ergänzung hätte niemanden erreicht, der die Seite schon geöffnet hatte. `werkbaum-seeded` hält deshalb statt '1' den Fingerabdruck (FNV-1a) der ausgelieferten Fassung; bei neuer Fassung wird der Text nachgezogen, aber nur wenn der Nutzer ihn nicht bearbeitet hat. Gelöscht bleibt gelöscht. `resetToDefaults()` setzt den Merker mit. Verifiziert im Browser, vier Fälle: alte unveränderte Fassung wird nachgezogen (98 Zeilen, neuer Zweig da); selbst geänderter Text bleibt unangetastet; gelöschtes Dokument kehrt nicht zurück; frischer Zustand zeigt die neue Fassung (89 sichtbare Knoten, keine Warnung). Vitest 37/37. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e160015fc7 |
frontend: Mitgeliefertes Dokument heißt „Werkbaum" (Tippfehler korrigiert)
„Werkbank" war ein Vertipper, gemeint war der Projektname. - WERKBAUM_NAME auf „Werkbaum" gesetzt. - `loadDocs()` zieht den Namen bei bereits angelegten Dokumenten nach — aber nur, solange er unverändert der ausgelieferte „Werkbank" ist. Eine eigene Umbenennung des Nutzers bleibt stehen; ein fremdes Dokument, das zufällig „Werkbank" heißt, wird nicht angefasst (die Regel greift über die reservierte id, nicht über den Namen). Verifiziert im Browser: Bestandszustand mit drei Dokumenten (Example, id=werkbaum/„Werkbank", eigenes d9/„Werkbank") -> nach dem Laden ist nur id=werkbaum umbenannt, d9 bleibt unberührt; frischer Zustand zeigt „Example" und „Werkbaum" im Wähler, Wechsel rendert 75 Knoten ohne Warnung. Vitest 37/37. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6fb45ff37a |
frontend: „Werkbank" — Werkbaums eigener Plan als mitgeliefertes Dokument
Der interessanteste Beispielbaum war nur über einen ?sourceUrl=-Link erreichbar. Er steht jetzt direkt im Dokument-Wähler. - Eine Quelle, keine Kopie: `?raw`-Import aus docs/examples/example-werkbaum.werkbaum — derselben Datei, die auch der ?sourceUrl=-Link lädt. Vite bettet sie in die eine Ausgabedatei ein (D19), nichts wird nachgeladen (D20). Die Beispieldatei ist damit Build-Eingabe. - `seedShippedDocs()` legt das Dokument genau EINMAL an — auch für Bestandsnutzer mit vorhandener Dokumentenliste — und merkt sich das in `werkbaum-seeded`. Ohne Merker gäbe es nur schlechte Alternativen: entweder bekämen bestehende Nutzer es nie, oder ein bewusst gelöschtes Dokument kehrte bei jedem Laden zurück. Das aktive Dokument bleibt unverändert. - Fester, nicht lokalisierter Name wie „Example" (D22). Der Reset setzt jetzt beide mitgelieferten Dokumente zurück; eigene bleiben unangetastet. Verifiziert im Browser: frischer Zustand -> Example + Werkbank, aktiv bleibt Example; Wechsel auf Werkbank rendert 75 Knoten ohne Warnung; Bestandsnutzer mit eigener Liste bekommt Werkbank ergänzt, ohne aus seinem Dokument gerissen zu werden; nach Löschen kehrt es nicht zurück. Build bleibt eine einzelne Datei (361 kB), Vitest 37/37. Offen (in D27 notiert): der Text wird nach dem Anlegen nicht mehr aktualisiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6888762ac8 |
frontend: Legende scrollbar + Splitter zwischen Editor und Legende
Die Legende wurde abgeschnitten statt zu scrollen — obwohl `.hint` seit jeher `flex:1 1 auto; min-height:0; overflow:auto` trägt. Ursache: Chrome legt den Inhalt eines <details> in das Pseudo-Element `::details-content`. Damit war `.hint` kein Flex-Kind von `.agenda` mehr, die Begrenzung griff nicht und der Kasten wuchs auf seine Inhaltshöhe (gemessen 585 px in einem 282 px hohen Container) und wurde geclippt. Eine Regel für `::details-content` wirkte im Test, ist aber Chrome-eigen — Firefox und Safari kennen das Pseudo-Element nicht. - Die Legende ist deshalb kein <details> mehr, sondern `div.agenda` + `button.agenda-summary`; der Zustand hängt an der Klasse `open` (`aria-expanded` am Button, `#legendBtn` und Splitter-Sichtbarkeit über `setAgendaOpen()` synchron). `.hint` ist wieder echtes Flex-Kind und scrollt in allen Browsern. - Neuer Splitter `#hintGutter` im selben Idiom wie der große (Pointer-Capture, Doppelklick setzt zurück): horizontal nebeneinander -> Breite `--hcol`, gestapelt (side/mobil) -> Höhe `--hrow`. Beide getrennt gehalten und in `werkbaum-ui` persistiert (samt Auf-/Zu-Zustand), damit ein Moduswechsel die jeweils andere Aufteilung nicht zerstört. Grenzen 90 px bis 85 %. - Die 85-%-Obergrenze steht zusätzlich als max-width/max-height im CSS: die gespeicherte Größe ist ein fester px-Wert und würde den Editor sonst auf 0 drücken, wenn das Panel später schrumpft (großer Splitter, Drehung, Bildschirmtastatur). Verifiziert mit --hrow:9999px -> gekappt bei 85 %. - Nebenbei: der große Splitter war als einziges Bedienelement nicht lokalisiert (fest deutsche title/aria-label). Beide teilen sich nun `gutterTooltip`, dazu `gutterAria`/`hintGutterAria` — 9 Sprachen. Verifiziert im Browser: Legende scrollt (247 px Fenster, 607 px Inhalt); Ziehen in beiden Ausrichtungen inkl. Unter-/Obergrenze und Doppelklick-Reset; `--hcol` und `--hrow` überleben den Moduswechsel; nach Neuladen sind Auf-Zustand, Breite und beide Variablen wiederhergestellt; auf Mobil ist die Summary-Zeile aus und der Kopf-Knopf steuert. Vitest 37/37. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4061362922 |
frontend: Sprung holt keine Bildschirmtastatur mehr herauf
Nach dem Sprung erschien auf Touch-Geräten sofort die virtuelle Tastatur und nahm den halben Bildschirm — auch bei angeschlossener Bluetooth-Tastatur. Das ist kein Fehler der App: Eine Webseite erfährt nichts über verbundene Tastaturen, sie fordert nur Fokus an; alles Weitere entscheidet das OS (Android hat dafür den Schalter „Bildschirmtastatur anzeigen" unter Physische Tastatur, vielerorts an). Statt darauf zu verweisen, fordert der Sprung sie erst gar nicht an: - `jumpToLine()` setzt vor dem Fokussieren `inputmode="none"` — das unterdrückt nur die VIRTUELLE Tastatur, Hardware-Tastaturen tippen unverändert weiter. - Die Sperre fällt, sobald der Nutzer das Textfeld selbst antippt (`pointerdown` läuft vor dem Fokus). Der Sprung ist damit „hinschauen", der erste Tipp ins Textfeld „bearbeiten". - `newDoc()` hebt die Sperre ausdrücklich auf — bei einem neuen, leeren Dokument ist Tippen gemeint. Verifiziert im Browser: Sprung setzt inputmode=none bei Fokus im Textfeld und korrekter Zeilenmarkierung; Feld bleibt editierbar (nicht readOnly/disabled); eigenes pointerdown entfernt das Attribut; erneuter Sprung setzt es wieder; „Neues Dokument" fokussiert ohne Sperre. Vitest 37/37. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bbdecce75a |
frontend: Touch-Langdruck springt beim Loslassen (Fokus blieb sonst nicht)
Auf echten Touch-Geräten flackerte die Markierung im Editor nach dem langen
Druck nur kurz auf und der Fokus fiel sofort wieder heraus.
Ursache: Der Sprung lief im 500-ms-Timer, und `focus()` aus einem
Timer-Callback gilt in mobilen Browsern nicht als Nutzergeste — ein Textfeld
darf sich so nicht selbst fokussieren (sonst könnte jede Seite ungefragt die
Bildschirmtastatur aufziehen). `touchend` ist eine Nutzergeste.
- Der Timer setzt jetzt nur noch die Rückmeldung: der Zielknoten bekommt die
Klasse `armed` mit demselben Petrol-Ring wie unter dem Alt-Zeiger.
`jumpToLine()` läuft im touchend-Handler; Wischen bricht weiterhin ab.
- Zweiter Beitrag zum selben Symptom: die native Textauswahl bzw. das
Link-Callout des Browsers startet bei derselben Druckdauer und riss die
Auswahl an sich. Drei Schichten dagegen, weil keine allein überall wirkt:
contextmenu-preventDefault während des Drucks (überall), `user-select:none`
an .node für grobe Zeiger (@media hover:none and pointer:coarse) und
`-webkit-touch-callout:none` (nur iOS; Chrome kennt es nicht mehr). Die
frühere Regel `a.node{-webkit-touch-callout:none}` geht darin auf.
Verifiziert (synthetische TouchEvents): während des Drucks nur `armed` +
Petrol-Ring, Auswahl noch unverändert, Fokus noch nicht im Textfeld; beim
Loslassen Sprung, Fokus TEXTAREA, Klick unterdrückt; Wischen und kurzer Druck
springen nicht und lassen den Klick durch. Ring greift auch unterhalb einer
any-of-Gruppe. Vitest 37/37.
Einschränkung: Weder die Nutzergesten-Regel noch die nativen Langdruck-Gesten
lassen sich synthetisch auslösen — der eigentliche Beweis ist ein Test auf
echter Hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
c496850b88 |
frontend: Alt-Sprung auffindbar machen (Cursor-Rückmeldung + Legenden-Zeile)
Eine Modifier-Geste, die niemand kennt, ist keine Funktion. Zwei Maßnahmen: - Solange Alt gedrückt ist, trägt #out die Klasse `alt`: alle Knoten zeigen `cursor:alias`, der Knoten unter dem Zeiger einen Petrol-Ring. Das wirkt auch auf verlinkten Knoten, wo der einfache Klick dem Link gehört und es sonst gar keine Rückmeldung gäbe. Der `blur`-Handler ist Pflicht — bei Alt+Tab kommt kein keyup mehr an, der Modus bliebe hängen. - Die aufklappbare Legende endet mit einer abgesetzten Bedienungs-Zeile (`hint_jump` in 9 Sprachen, `.hint-op` mit gestricheltem Trenner), damit sie ihre Rolle als Notations-Legende behält. Verworfen (in D25 samt Begründung festgehalten): einfacher Klick springt auf Knoten ohne URL (~97 % der Knoten der Beispieldateien) — beiläufig auffindbar, aber dieselbe Geste täte je nach Knoten Verschiedenes; sowie ein einmaliger Hinweis nach dem ersten Sprung. Verifiziert im Browser: ohne Alt Cursor auto bzw. pointer (Link), mit Alt überall `alias`, nach Loslassen zurück, nach blur ohne keyup (Alt+Tab) zurückgesetzt; Legenden-Zeile sichtbar und abgesetzt. Vitest 37/37. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1f6988b047 |
frontend: Sprung zwischen Diagramm und Text (beide Richtungen)
Knoten und Notationszeile sind nun verknüpft. Der Parser hängte die
Zeilennummer ohnehin schon an jeden Knoten — sie wandert jetzt als data-line
ins Markup.
- Diagramm -> Text: Alt+Klick markiert die ganze Zeile im Editor (Fokus,
in Sicht gescrollt); Tastatur Alt+Enter, Touch langer Druck (500 ms,
Wischen bricht ab). Ein zugeklapptes Editor-Panel wird zuerst geöffnet.
- Text -> Diagramm: der Knoten der Cursor-Zeile bekommt weißen Halo +
Tinte-Ring und wird beim Zeilenwechsel ins Bild gescrollt.
- Alt statt einfachem Klick, weil ein Knoten mit URL als <a> den ganzen
Kasten belegt (SPEC §6 bleibt unverändert). Der Handler ruft
preventDefault() — sonst lädt Alt+Klick auf einen Link das Ziel herunter.
Neuer Tooltip-Hinweis an jedem Knoten (i18n `jumpHint`, 9 Sprachen).
- Gescrollt wird über einen Spiegel-div, nicht über Zeilenhöhe x n: lange
Zeilen brechen weich um, die naive Rechnung lag im Test bei 60 Zeilen um
bis zu 525 px daneben.
Beim Verifizieren gefunden und behoben: `ul.or .node{box-shadow:none}` ist
spezifischer als `.node.current` und schluckte den Ring überall unterhalb
einer any-of-Gruppe — die Regel braucht den #out-Präfix.
Verifiziert im Browser (Alt+Klick auf <a>- und div-Knoten, Klick aufs
Größen-Badge, Klick/Enter ohne Alt unverändert, Alt+Enter, kurzer/langer
Druck und Wischen auf Mobil, Panel-Aufklappen aus dem minimierten Zustand,
Zeile nach Sprung immer im sichtbaren Band, Kommentar-/verworfene Zeilen
heben nichts hervor). Vitest 37/37 (3 neue data-line-Tests, Snapshots
aktualisiert). Dokumentiert als D25 + SPEC §9 + frontend/CLAUDE.md + README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
cdf843e2e3 |
docs: Eigene Endung .werkbaum + fünf Beispieldateien unter docs/examples/
Mit nur einem Beispiel ließ sich das Umschalten zwischen Dokumenten (D22) nicht zeigen — jeder ?sourceUrl=-Link legt genau ein Dokument an (D23), erst mehrere nacheinander geöffnete füllen den Wähler. - Neue Endung `.werkbaum` (D24, SPEC §12): macht Notationstexte zuordenbar (spätere Editor-Zuordnung, Syntax-Highlighting, Öffnen/Speichern). Konvention, kein Vertrag — `?sourceUrl=` wertet weder Endung noch Content-Type aus, `.txt` bleibt gültig. `.gitattributes` führt `*.werkbaum` als Text (LF). - docs/example-plan.txt -> docs/examples/example-plan-0.werkbaum (Inhalt bis auf die Kopfzeile unverändert); README-Links nachgezogen. - Neu, je einen anderen Aspekt zeigend: example-plan-1 (Küche: kein Software-Plan, alternativenlastig — günstigster Pfad), example-plan-2 (Konferenz: breiter Fächer, viele Beteiligte), example-plan-3 (drei Wurzeln = drei Bäume nebeneinander, dazu ein Knoten ohne Größe für das implizite-M-Badge), example-werkbaum (Werkbaum selbst: Bestand + mögliche Weiterentwicklung, destilliert aus ROADMAP/TASKS/DECISIONS). - README.md / README.de.md: Tabelle mit den fünf Live-Links samt Hinweis, dass sie nacheinander geöffnet den Dokument-Wähler füllen. Verifiziert: alle fünf parsen headless ohne Warnungen und ohne gemischte Gates (19/26/43/28/79 Knoten, 3 Wurzeln in Beispiel 3); Vitest 34/34 grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1557a7ab0d |
frontend: Notationstext per ?sourceUrl= von einer URL laden
Der Editor kann den Text aus einer externen Textdatei beziehen: …/index.html?sourceUrl=https://example.org/plan.txt - Das Geladene wird als eigenes Dokument (D22) geführt, dessen Name die URL ist; die id leitet sich aus der URL ab, derselbe Link aktualisiert also dieses Dokument, statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente bleiben unberührt. Die volle URL steht zusätzlich im Tooltip (der Name wird in der Titelzeile elidiert). - Ist der Parameter gesetzt, wird bei jedem Laden neu geholt — die URL ist die Quelle der Wahrheit. - Nur http(s) (relative Angaben werden aufgelöst), credentials:'omit'; file:/data:/javascript: werden abgewiesen. Fremder Text ist ungefährlich: Labels werden escaped, der Parser erkennt als Link nur https?:// (SPEC §1). - Fehler (CORS, 404, Netz) landen als zeilenlose Warnung `sourceLoad` im Warnbereich und nennen CORS ausdrücklich als wahrscheinliche Ursache; der bisherige Stand bleibt stehen. i18n in allen 9 Sprachen. - Nebenbei: checkForUpdates() baut die Cache-Buster-URL nun über URL/searchParams — mit vorhandenem Query-String hätte '?t=' ein zweites '?' erzeugt. Verifiziert headless (jsdom gegen den gebauten Bundle: Erfolg, CORS-Fehler, 404, ohne Parameter, verbotenes Schema) und im Browser gegen raw.githubusercontent.com. Dokumentiert als D23 + SPEC §9 + frontend/CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
64e4e60b53 |
frontend: Dokument-Wähler — Beispiel englisch, Inline-Umbenennen, gezielter Reset
Behebt drei Punkte am Mehrfach-Dokument-Feature (D22): - Beispiel-Dokument heißt jetzt fest „Example" (englisch, nicht mehr lokalisiert „Beispiel") und trägt die reservierte id `example`. Alt-Zustände (zufällige id, „Beispiel") werden beim Laden adoptiert, sofern der Text noch unverändert ist (=== INITIAL) — echte Inhalte nie. - Umbenennen läuft inline im Menü (Textfeld: Enter bestätigt, Esc bricht ab, Blur bestätigt) statt über window.prompt, das in manchen Browser-Kontexten unterdrückt ist („Umbenennen funktioniert nicht"). - Reset (Debug-Knopf, non-prod) setzt nur noch das Beispiel-Dokument auf INITIAL/„Example" zurück und verwirft Ansichts-/Sprach-/Update-Flags; alle anderen Dokumente bleiben erhalten (vorher wurden pauschal alle werkbaum-*- Schlüssel gelöscht). i18n-Keys docDefaultName/docRenamePrompt entfallen (nicht mehr gebraucht). D22 + frontend/CLAUDE.md nachgezogen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
36d79b82c4 |
frontend: Mehrere umschaltbare Dokumente (Wähler in der Editor-Titelzeile)
Der Editor kann jetzt mehrere Notationstexte halten und zwischen ihnen
umschalten. Der Name des aktiven Dokuments ersetzt die feste Beschriftung
„Struktur (Text)" und öffnet als Dropdown den Wähler mit Wechseln, Neu,
Umbenennen und Löschen.
- Datenmodell (noch kein Backend): Dokumente als [{id,name,text}] in
localStorage (werkbaum-docs), aktives per id in werkbaum-active; der
aktive Text bleibt zusätzlich in werkbaum-src gespiegelt.
- Migration verlustfrei: fehlt werkbaum-docs, wird der bestehende Einzeltext
(oder INITIAL) in ein Dokument gepackt. Reset und Löschen des letzten
Dokuments säen das Beispiel neu.
- Jedes Dokument ist nur Text + Name (kein Strukturformat, D14) —
vorwärtskompatibel zum geplanten Backend (D13). Ansichts-State (Modus,
Zoom, Aufteilung) bleibt global.
- i18n in allen 9 Sprachen; Doc-Namen bleiben Nutzerdaten (nicht übersetzt).
- Dokumentiert als ADR D22 (DECISIONS.md) + frontend/CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
1cf60e2a75 |
frontend: Integrate self-hosted IBM Plex fonts, replace Google Fonts
IBM Plex Sans and Mono are now embedded locally (`woff2`) under `src/fonts`, licensed under SIL OFL 1.1. Removed external `<link>` to Google Fonts for enhanced privacy and compliance. Updated `style.css` with `@font-face` and unicode ranges for `latin` and `latin-ext` subsets. Added license details to documentation. |
||
|
|
0f27bab905 |
frontend/ci: Build-Hinweis in die App-Quelle, env-gesteuert; Prod-Build ohne
Der Latest-Build-Hinweis wandert vom Workflow-sed in die App (app.js, mountBuildBadge), damit ihn auch der Dev-Server zeigt — ein Post-Build-sed erreicht den Dev-Server nicht. Logik umgekehrt: Hinweis ist der Normalfall, nur die produktive Installation schaltet ihn ab. Drei Zustaende ueber Vite-Env VITE_BUILD_BADGE (Auswertung in app.js): - Dev-Server (import.meta.env.DEV) -> 🔧 "Vorschau/lokaler Entwicklungsstand" - Default `npm run build` (Env ungesetzt) -> 🚧 "latest build"; der Pages- Deploy nutzt den Default und traegt den Hinweis dadurch automatisch (die sed-Injektion entfaellt). - `npm run build:prod` (Vite-Modus prod, .env.prod: VITE_BUILD_BADGE=none) -> KEIN Badge; esbuild eliminiert den Zweig als toten Code. Verifiziert zur Laufzeit (Dev + `vite preview` auf beiden Builds): 🔧 im Dev, 🚧 im Default-Build, gar nichts im Prod-Build (Titel sauber "Werkbaum"). 34 Vitest-Tests gruen, Workflow-YAML/Shell gueltig. Doku: D16 fortgeschrieben (quellbasiert/env statt sed, Begruendung); README (de/en) mit Prod-Build-Anleitung inkl. LICENSE-/Versions-Caveats; frontend/ CLAUDE.md; .claude/launch.json bekommt einen frontend-dist-Preview (vite preview, Port 8138) zum Verifizieren gebauter Dateien. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0a8b3659ea |
frontend: Fehlertolerantes Parsen — unbekannte Statuszeichen melden
TASKS Phase 2 (Phase 2 abgeschlossen). Der Parser erfasst die Statusbox jetzt
als beliebiges Einzelzeichen `\[([^\]])\]` und validiert gegen STATUS_BY_CODE.
Unbekannte Codes (z. B. [z]) werden nicht mehr still ins Label geschluckt,
sondern als {type:'unknownStatus', line, code} in parse().warnings gemeldet;
der Knoten bleibt neutral, Folgezeilen unberührt. Mehrzeichige Klammern ([xyz])
sind keine Statusbox und bleiben im Label (kein Fehlalarm).
render() in app.js führt Parser- und Renderer-Warnungen zusammen (nach Zeile
sortiert) und zeigt sie via formatWarning. Neuer i18n-Key unknownStatusWarn in
allen 9 Sprachen.
Verifiziert: mehrere unbekannte Codes -> zeilensortierte, lokalisierte
Warnungen (DE/EN), Knoten neutral, keine Konsolenfehler. tests/parser.test.js
+5 (34 Tests grün). SPEC §4 + CLAUDE ergänzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
15699c321c |
frontend: Barrierefreiheit — aria-Labels, Fokusreihenfolge, Live-Region
TASKS Phase 2. Jeder Diagramm-Knoten bekommt in render.js (nodeAria) einen sprechenden, lokalisierten aria-label: Label + Status + Aufwand (inkl. „(angenommen)" beim impliziten M) + Zuständige + Link. Die rein visuellen Badges (Größe, Tags, ↗) sind aria-hidden, damit der Screenreader nicht „M", „anna", „↗" kryptisch doppelt vorliest. Neue a11y*-i18n-Keys in allen 9 Sprachen. Alle Knoten sind fokussierbar (tabindex=0 bzw. der Link) in Dokument-/Lese- reihenfolge; sichtbarer :focus-visible-Rahmen (Petrol). #warn ist eine Live-Region (role=status, aria-live=polite) — neue Warnungen werden angesagt. Verifiziert: aria-label folgt dem Sprachwechsel (DE/EN), Badges aria-hidden, keine visuelle Regression, keine Konsolenfehler. Renderer-Snapshots aktualisiert (29 Tests grün). SPEC §9 (Barrierefreiheit) + CLAUDE ergänzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5905712aa5 |
frontend: Renderer + Modell headless auslagern (src/model.js, src/render.js)
TASKS Phase 1. Baum-/Kostenlogik -> model.js (gateOf, needsBreakdown, visibleChildren, computeCheapSet, cheapCls), HTML-Erzeugung -> render.js (renderTreeHtml). Beide sind headless: UI-State (verworfene einblenden, günstigster Pfad) kommt als Parameter herein statt aus Globals; nur cheapPathOn bleibt UI-State in app.js. render() in app.js ist jetzt die dünne DOM-Anbindung. Snapshot-Tests (tests/render.test.js): Grundzustand, günstigster Pfad, verworfene einblenden, Geister-Knoten („Untergliederung fehlt"), plus die Zusicherung, dass der Renderer nie eine Modus-Klasse erzeugt (Modus = CSS-Container-Klasse in app.js -> ein Snapshot deckt alle drei Modi). 24 Tests grün. Byte-identisches Rendering im Browser verifiziert (18 Knoten, 6 Blätter, 3 Geister, Toggles ok, keine Konsolenfehler). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
78d1000f61 |
frontend: Projektgerüst mit Vite — Single-HTML in ES-Module + Bündelung
Erster Schritt der Modularisierung (ROADMAP „tragfähige Codebasis", TASKS Phase 1). Der bisher einzelne index.html-Prototyp wird zur Vite-App: - CSS -> src/style.css, gesamtes JS -> src/app.js (noch als ein Modul; Parser/Renderer werden in den folgenden Checkboxen herausgelöst). - index.html ist der Vite-Entry (<script type="module" src="/src/app.js">). - npm run build (vite + vite-plugin-singlefile) inlint JS+CSS+Favicon (als data:-URI) in EINE self-contained dist/index.html -> bleibt file://-tauglich. - Vitest als Testrunner eingerichtet (tests/, passWithNoTests bis Schritt 2/3). - Pages-Workflow: Node + npm ci + npm test + npm run build, deployt dist/; Favicon-sed entfällt (inline), nur ../LICENSE + Footer-Version bleiben. - .claude/launch.json auf Vite-Dev-Server (Port 8137) umgestellt. Entscheidung als D19 dokumentiert (weicht D11 „kein Build-Zwang" bewusst auf, hält aber D16 file://-Versprechen über die gebündelte Datei). README (de/en) und frontend/CLAUDE.md um Dev-/Build-/Test-Workflow ergänzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ad57707dbc |
frontend: Mobiler Splitter stufenlos + Titelzeilen-Tap maximiert
Auf dem Smartphone hatte der Splitter zwei Fehler: nach dem ersten Ziehen rastete er in eine feste Aufteilung und ließ sich nicht mehr bewegen (die Titelzeile im Snap-Rand fing jeden weiteren Zug ab), und das Tippen auf eine Titelzeile klappte das Panel nicht mehr aus (der Handler verlangte den collapsed-Zustand, den ein freier Zug aufhob). Mobil jetzt kontinuierlich statt diskret: - Grid-Zeilen-Minima --pmin-d/--pmin-e = gemessene Kopfhöhen (syncPanelMins), dadurch bleiben beide Titelzeilen immer stehen (kein Snap/Collapse). - Gutter-Drag ruft setMobileDrow() (klemmt --drow frei) — jederzeit beweglich. - Tipp auf eine Titelzeile maximiert dieses Panel (das andere = Titelzeile). - applyLayout ruft auf Mobil kein applySplit (behält --drow bei Moduswechsel); seg-Klick stoppt Propagation, damit er die Titelzeile nicht mit-maximiert. - Default: Diagramm maximiert. Desktop behält das diskrete Modell + Buttons. SPEC §9, DECISIONS D17 und frontend/CLAUDE.md nachgeführt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8e8c44bf9d |
frontend: Günstigster-Pfad-Linie mit Stationspunkten + Nesting-Fix
- gestrichelte, geschwungene Petrol-Linie (Katmull-Rom) durch die Endknoten des Pfads; kräftig hinter den Knoten, abgetönte Kopie davor - blasse, große Stationspunkte nur an echten Blättern (U-Bahn-Plan) — macht durchquerte Fremd-/Zwischenknoten eindeutig als "nicht Endknoten" erkennbar - Linie + Punkte auch im SVG/PNG-Export (hinten/vorne-Schichtung) - Overlays liegen in #out, erben dessen CSS-zoom (Punkte /zoom umgerechnet); Neuzeichnung nach render() und applyLayout() - Beispiel erweitert: Frontend (M) -> PWA | Web+Nativ (Web[/]+Android+iOS) - Layout-Fix: all-of unter einer any-of-Alternative wird nur horizontal schmal transponiert gestapelt (ul.or>li.has-and>ul.and), sonst schob der breite Fächer Alternativknoten und Elternbaum weit nach rechts - Geist-Knoten-Label "Untergliederung fehlt" -> "…" (alle 9 Sprachen), die Erklärung bleibt im Tooltip; der lange Text verbreiterte gestapelte Zellen - SPEC §5/§9 und DECISIONS D18 nachgeführt, Notiz in frontend/CLAUDE.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c8f8160eca |
frontend: Günstigster-Pfad-Hervorhebung per Inversion + implizites M
- Umschalter im Diagramm-Kopf (Default an, Zustand in werkbaum-ui persistiert) - nötige Knoten: all-of ⇒ alle Kinder, any-of ⇒ günstigste Alternative (kleinste rekursive Kosten; Gleichstand ⇒ erste); verworfene zählen nie mit - Darstellung per Inversion: nicht benötigte Knoten treten blass/entsättigt zurück (kein Zusatzrahmen an den ohnehin dichten Knoten-Ecken) - fehlende T-Shirt-Größe wird für die Kostenschätzung als M gewertet; invertiertes M-Badge (weiß mit Petrol-Rand) macht die Annahme sichtbar - neue Tooltips i18n (DE-Quelle + 8 Übersetzungen, 50 Keys × 9) - SPEC §9 (Günstigster Pfad) und DECISIONS D18 mitgeführt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
506bdccd2e |
frontend: Ein-Bereich-Ansicht + kompakte Legende/Sprachwahl auf Mobil
Auf kleinem Bildschirm (body.mobile per matchMedia, <=640px): - Immer nur ein Bereich gross (Diagramm ODER Editor); der andere schrumpft auf seine Titelzeile und dient als Umschalter (Antippen holt ihn nach vorn). Splitter und Min/Normal/Max-Buttons entfallen. - Legende von der Bereichs-Umschaltung entkoppelt: eigener Umschalter (#legendBtn) im Editor-Kopf statt der dauerhaften "AGENDA"-Zeile, damit sie zugeklappt keinen Platz kostet (neuer i18n-Key legendTooltip, 9 Spr.). - Sprachwahl schlank: nur EN + aktive Sprache + "…" (klappt alle auf); die aktive Sprache wird auch als "extra" eingeblendet, Auto-Aufklappen entfaellt. - Default Vollbild auf Mobil (nur ohne gespeicherte Nutzerwahl). Verifiziert (headless, 375px): body.mobile gesetzt, Diagramm-Default + Editor als Titelzeile, Umschalten per Tap, Legenden-Toggle, Sprachleiste DE+EN+…/Aufklappen zeigt alle 9/aktives Extra bleibt sichtbar, Vollbild an, keine Konsolenfehler; Desktop unveraendert (beide Panels, 4 Sprachen, Legendenbutton verborgen). i18n konsistent (47 Keys x 9). SPEC §9 (Kleiner Bildschirm) + DECISIONS D17 + frontend/CLAUDE.md ergaenzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7c39da7b44 |
frontend: GUI-Zustand und Editortext im Browser speichern
Ohne Backend bleiben Einstellungen und Text ueber ein Neuladen erhalten (localStorage). Gespeichert: Editortext (werkbaum-src) und GUI-Zustand (werkbaum-ui: Modus, verworfene, Split-Zustand inkl. freier Drag-Groesse --col/--drow, Zoom, Vollbild); Sprache bleibt in werkbaum-lang. saveUI()/saveSrc() haengen an allen Zustandsaenderungen (Layout, Toggle, Fenster-Buttons, Splitter-Drag, Zoom, Vollbild, Texteingabe); restoreState() stellt beim Laden alles wieder her und unterdrueckt waehrenddessen ueber restoring=true das Zurueckschreiben. Fehlender Text faellt auf das Beispiel (INITIAL) zurueck, ein leerer bleibt leer. Verifiziert (headless): Text, Modus, verworfene, Zoom, Vollbild, Custom-Split (--col) und Minimier-Zustand ueberstehen ein Reload; keine Konsolenfehler. Keine SPEC-Aenderung (Notation unberuehrt); localStorage-Schluessel in frontend/CLAUDE.md dokumentiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
20b9d89f88 |
frontend: Darstellung horizontal/vertikal/kompakt + graue Any-of-Linien
Der bisherige „vertikal"-Toggle wird zur dreistufigen Auswahl „horizontal / vertikal / kompakt", die zugleich das Seitenlayout wählt: - horizontal: Diagramm oben, Editor unten (je volle Breite), Agenda rechts im Editor aufklappbar. - vertikal: Editor links, Diagramm rechts; „all of" tritt rechts aus. - kompakt: wie vertikal, aber „all of" läuft nach unten (spart Breite); Gate nur noch über den Linienstil codiert. Zusätzlich wird das Petrol im Diagramm aufgegeben: any-of-Linien (Sammelleiste und Abzweige) sowie der Rahmen der Alternative-Knoten sind in allen Modi durchgehend gestrichelt bzw. grau (#6B7A8C). all-of bleibt durchgezogen in Tinte. Petrol nur noch für UI-Akzente und das Logo. Doku nachgezogen: SPEC §9 (Linienführung, drei Modi), DECISIONS D15, frontend/CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ded894a4da | initial, vibe-coded version, mostly frontend |