feat: Neuigkeiten — der Stern wandert in die Kopfzeile und bekommt ein Popup
Der „Was ist neu?"-Knopf stand im Diagramm-Kopf und war verborgen, solange es nichts gab — er konnte also nur etwas über das offene Dokument sagen, und meistens sagte er gar nichts. Jetzt steht er permanent in der oberen Bedienleiste und öffnet ein Popup mit der Chronik der letzten Tage; je Tag führt ein Link die betroffenen Knoten im Diagramm vor (dieselbe gelbe Kranz-Ansicht wie D28, nur mit einer anderen Frage). Zwei Quellen, jede in ihrer Rolle: docs/CHANGELOG.md sagt, WAS geschehen ist (englisch — das Popup ist Oberfläche in neun Sprachen, die Commit-Betreffs sind deutsch); die git-Historie des mitgelieferten Plans sagt, WELCHE Knoten sich bewegt haben. Beides wird zur Bauzeit eingelesen (Vite-Plugin), zur Laufzeit lädt Werkbaum weiterhin nichts nach. Bernstein heißt ungesehen, Petrol heißt „wird gerade vorgeführt". Der Besuchsvergleich (D28) steht als abgesetzter Abschnitt zuoberst im Popup und trägt den „gesehen"-Knopf, den vorher der Knopf selbst war. SPEC §9 (Neuigkeiten) und D58; 20 neue Tests, davon einer auf der ausgelieferten CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
71bfb62b35
commit
36154f6f68
@@ -19,6 +19,10 @@ Integrations-Backend.
|
|||||||
Eintrag begründen, alte Einträge nie löschen. Besonders D13 (Backend-Stack)
|
Eintrag begründen, alte Einträge nie löschen. Besonders D13 (Backend-Stack)
|
||||||
und D14 (Parser-Hoheit) beachten.
|
und D14 (Parser-Hoheit) beachten.
|
||||||
- Ziele: docs/ROADMAP.md · Offene Arbeit: docs/TASKS.md (Checkboxen pflegen).
|
- Ziele: docs/ROADMAP.md · Offene Arbeit: docs/TASKS.md (Checkboxen pflegen).
|
||||||
|
- Änderungen: @docs/CHANGELOG.md — **jedes Feature und jeder behobene Fehler
|
||||||
|
bekommt dort eine Zeile**, englisch, ein Satz, unter der Überschrift des
|
||||||
|
Tages (`## JJJJ-MM-TT`). Die Datei speist das Neuigkeiten-Popup im Editor
|
||||||
|
(D58); ohne die Zeile geschieht die Änderung für den Benutzer unsichtbar.
|
||||||
|
|
||||||
## Querschnitts-Konventionen
|
## Querschnitts-Konventionen
|
||||||
- Doku auf Deutsch. Die Editor-UI ist mehrsprachig (DE/EN/ES/FR direkt,
|
- Doku auf Deutsch. Die Editor-UI ist mehrsprachig (DE/EN/ES/FR direkt,
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
What changed, newest first. This file feeds the **What's new** popup in the
|
||||||
|
editor (the star in the header, D58): every `## YYYY-MM-DD` heading opens a day,
|
||||||
|
every bullet under it becomes one note. Anything else here — like this
|
||||||
|
paragraph — is read by people, not by the build.
|
||||||
|
|
||||||
|
**English, deliberately.** The project's documentation is German (CLAUDE.md),
|
||||||
|
but this file is shown *inside the product*, whose interface speaks nine
|
||||||
|
languages. Like the shipped plan and `llms.md` it is a delivered artefact with a
|
||||||
|
worldwide audience (D22, D43). Keep the notes short — one line, what changed,
|
||||||
|
from the reader's side rather than the commit's. `Backticks` become code in the
|
||||||
|
popup; no other markup is honoured, so write plain sentences.
|
||||||
|
|
||||||
|
The node highlighting per day does **not** come from here: it is computed from
|
||||||
|
the git history of `docs/examples/werkbaum.werkbaum`. A day can therefore carry
|
||||||
|
a link without having a note (someone forgot to write one) — but never the
|
||||||
|
reverse.
|
||||||
|
|
||||||
|
## 2026-08-24
|
||||||
|
|
||||||
|
- A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram
|
||||||
|
- A warning triangle replaces the question mark as the pointer over faulty line numbers
|
||||||
|
- The node window replaces the browser tooltip everywhere — at the pointer, on keyboard focus and on touch
|
||||||
|
- A `#` button in the diagram header puts node IDs in front of the titles
|
||||||
|
- Typing `#.kc` under `#prod-stage` now expands to `#prod-stage.kc` when you leave the line
|
||||||
|
- A camera button next to the history saves a snapshot on demand
|
||||||
|
- Fix: the manual snapshot button confirmed without saving anything while nothing had changed yet
|
||||||
|
- Warning line numbers carry their message as a tooltip
|
||||||
|
- `llms.txt` now points at the notation guide, and `llms.md` is served as UTF-8
|
||||||
|
|
||||||
|
## 2026-08-23
|
||||||
|
|
||||||
|
- Every node of the shipped Werkbaum plan carries an ID and a description
|
||||||
|
- One button steps from station to station along the cheapest path
|
||||||
|
- The cheapest path shows the open front: what is done costs nothing any more
|
||||||
|
- A new document starts by asking for its name
|
||||||
|
- The text area no longer wraps lines — the indentation keeps carrying the hierarchy
|
||||||
|
- Node IDs read `#auth: Title` now, with an optional separating colon
|
||||||
|
- Fold marks moved behind the status box, so the boxes stay aligned
|
||||||
|
- Fix: the document and download menus open again on small screens
|
||||||
|
|
||||||
|
## 2026-08-22
|
||||||
|
|
||||||
|
- Node IDs (`#auth`) name a node across the whole document
|
||||||
|
- Dependencies (`:#auth,#api`) point across the tree, cycles allowed
|
||||||
|
- The node colour shows the effective status — what a node waits for holds it back
|
||||||
|
- Dependency links are drawn as thin dotted curves behind the nodes
|
||||||
|
- Subtrees fold and unfold, in the diagram and as `>` / `<` in the text
|
||||||
|
- Exactly-one groups (`=`) with a "1" plaque on the collector rail
|
||||||
|
- Node descriptions, as `"` lines or as blocks behind a `---` divider
|
||||||
|
- The cheapest path counts shared dependencies once — and says so when it has to guess
|
||||||
|
- "Restore original" brings a shipped document back to the delivered state
|
||||||
|
- `llms.md` explains the notation to AI agents
|
||||||
|
|
||||||
|
## 2026-08-16
|
||||||
|
|
||||||
|
- The example plan is called `werkbaum.werkbaum` and describes Werkbaum itself
|
||||||
|
|
||||||
|
## 2026-08-05
|
||||||
|
|
||||||
|
- Line numbers next to the text, warnings marked in the strip
|
||||||
|
- Alt+click in the text centres the node of the caret line
|
||||||
|
|
||||||
|
## 2026-07-30
|
||||||
|
|
||||||
|
- The focus mark `!!!` got a teal crown of its own
|
||||||
|
- Research on updating by itself: what an Etherpad instance can and cannot do
|
||||||
|
- An IntelliJ plugin recorded as an idea in the plan
|
||||||
|
|
||||||
|
## 2026-07-27
|
||||||
|
|
||||||
|
- `+` marks an optional node — an extra, neither required nor an alternative
|
||||||
|
- Consecutive optional leaves cascade into a staircase instead of taking a column each
|
||||||
|
- "What's new" highlights what went into production since your last visit
|
||||||
|
- A shared Etherpad can be watched, embedded and reloaded
|
||||||
|
|
||||||
|
## 2026-07-25
|
||||||
|
|
||||||
|
- The notation text can be loaded from a URL with `?sourceUrl=`
|
||||||
|
|
||||||
|
## 2026-07-23
|
||||||
|
|
||||||
|
- Several documents side by side, switched from the editor's title bar
|
||||||
|
- The interface language follows the browser, German as the fallback
|
||||||
|
- High risk `[!]` carries a warning triangle
|
||||||
|
|
||||||
|
## 2026-07-22
|
||||||
|
|
||||||
|
- IBM Plex is embedded locally — no request to Google, no IP address to third parties
|
||||||
|
- Zoom controls for the diagram
|
||||||
|
- Accessibility: spoken labels per node, focus order, live region for warnings
|
||||||
|
|
||||||
|
## 2026-07-21
|
||||||
|
|
||||||
|
- First version: text notation, parser, and a diagram in three layout modes
|
||||||
|
- Status boxes, t-shirt sizes, people tags, bare URLs and `%%` comments
|
||||||
|
- The cheapest path through the tree, with a metro-style line through the open leaves
|
||||||
|
- Print stylesheet and SVG/PNG export of the diagram
|
||||||
@@ -4077,3 +4077,111 @@ mit synthetischem `focusin` öffnete es sofort. Erst das Fronten des Tabs samt
|
|||||||
echtem Klick und echter Tab-Taste hat es bewiesen. Wie D25 (synthetische
|
echtem Klick und echter Tab-Taste hat es bewiesen. Wie D25 (synthetische
|
||||||
`TouchEvent`s), D17-Nachtrag 4 (Bildschirmtastatur) und D53 (synthetisches
|
`TouchEvent`s), D17-Nachtrag 4 (Bildschirmtastatur) und D53 (synthetisches
|
||||||
Strg+Z): Was die Umgebung stellt, stellt der Emulator nicht.
|
Strg+Z): Was die Umgebung stellt, stellt der Emulator nicht.
|
||||||
|
|
||||||
|
## D58 — Neuigkeiten: der Stern wandert in die Kopfzeile und bekommt ein Popup
|
||||||
|
Der „Was ist neu?"-Knopf (D28) stand im Diagramm-Kopf und war **verborgen,
|
||||||
|
solange es nichts gab** — er konnte also nur etwas über das gerade offene
|
||||||
|
Dokument sagen, und meistens sagte er gar nichts. Jetzt steht er permanent in
|
||||||
|
der oberen Bedienleiste, zeigt ein Popup mit der Chronik der letzten Tage, und
|
||||||
|
jeder Tag führt seine Knoten im Diagramm vor.
|
||||||
|
|
||||||
|
**Zwei Aussagen, ein Knopf.** Die **Chronik** ist allgemein („was ist am
|
||||||
|
Produkt geschehen"), der **Besuchsvergleich** persönlich („was ist seit deinem
|
||||||
|
letzten Besuch live gegangen"). Sie zu trennen hieße, zwei Knöpfe in eine Zeile
|
||||||
|
zu setzen, die D56 gerade erst auf zehn Elemente zurechtgemessen hat — und für
|
||||||
|
den Betrachter sind es ohnehin dieselbe Frage in zwei Zeitmaßstäben. Der
|
||||||
|
Besuchsvergleich steht als abgesetzter Abschnitt **zuoberst** im Popup und
|
||||||
|
trägt den „gesehen"-Knopf, den vorher der Knopf selbst war.
|
||||||
|
|
||||||
|
**Bernstein heißt ungesehen, Petrol heißt „wird vorgeführt".** Zwei Zustände,
|
||||||
|
zwei Farben, beide schon vergeben: Bernstein ist die Farbe des Strahlenkranzes
|
||||||
|
am Knoten (D28) — Knopf und Knoten sagen damit dasselbe —, Petrol die für
|
||||||
|
Interaktion (D32). Ein dritter Kanal war nicht nötig.
|
||||||
|
|
||||||
|
**Aufgeschlagen heißt gelesen.** Der Deckel wandert beim Öffnen auf den
|
||||||
|
**neuesten gelisteten Tag**, nicht auf „heute": Ein Datum aus der Uhr des
|
||||||
|
Betrachters verglichen mit einem Datum aus dem Build ginge schief, sobald die
|
||||||
|
Uhren auseinanderliegen. Der Besuchsvergleich behält seinen eigenen Knopf — er
|
||||||
|
hat eine andere Basis (den Text der zuletzt gesehenen Fassung, D28).
|
||||||
|
|
||||||
|
**Woher die Daten kommen: zwei Quellen, jede in ihrer Rolle.**
|
||||||
|
|
||||||
|
- `docs/CHANGELOG.md` → **was** geschehen ist, ein englischer Satz je Änderung.
|
||||||
|
- Die git-Historie von `docs/examples/werkbaum.werkbaum` → **welche Knoten**
|
||||||
|
sich an diesem Tag bewegt haben (neu oder mit anderem Status).
|
||||||
|
|
||||||
|
Beides wird **zur Bauzeit** eingelesen und als virtuelles Modul eingebettet
|
||||||
|
(Vite-Plugin). Zur Laufzeit gibt es kein git und keinen Server, der nachliefern
|
||||||
|
könnte (D11/D19), und nachladen würde D20 brechen. Der Preis ist benannt: Der
|
||||||
|
Dev-Server liest einmal beim Start, neue Einträge erscheinen nach einem
|
||||||
|
Neustart.
|
||||||
|
|
||||||
|
**Die Notizen kommen NICHT aus den Commit-Betreffs** — obwohl sie dort stünden
|
||||||
|
und die erste Fassung sie genau so gezogen hat (samt Filter für Bau-, Test- und
|
||||||
|
Beförderungs-Commits). Der Nutzer hat die richtige Frage gestellt: *„Sind die
|
||||||
|
Neuigkeiten nun in allen unterstützten Sprachen? Da hätte ich eine Rückfrage
|
||||||
|
erwartet."* Die Betreffs sind **deutsch** (CLAUDE.md: Doku auf Deutsch), das
|
||||||
|
Popup aber ist Produkt-Oberfläche in neun Sprachen — ein japanischer Besucher
|
||||||
|
hätte einen japanischen Rahmen um deutsche Sätze bekommen. Entschieden
|
||||||
|
(Nutzer): eine **gepflegte englische Changelog-Datei**, wie der mitgelieferte
|
||||||
|
Plan und `llms.md` ausgeliefertes Artefakt mit weltweitem Publikum (D22, D43).
|
||||||
|
|
||||||
|
Verworfen waren: **deutsch lassen und benennen** (billig, aber acht der neun
|
||||||
|
Sprachen lesen es nicht) und **die Notizen aus den Knoten-Labels des Plans
|
||||||
|
ableiten** (schon englisch, keine Pflege, Text und Hervorhebung sagten
|
||||||
|
zwangsläufig dasselbe — aber „Fold marks → fertig" ist eine Statusmeldung, kein
|
||||||
|
Satz, und Tage ohne Plan-Änderung fielen ganz weg). Der Preis der gewählten
|
||||||
|
Lösung ist ehrlich zu nennen: **eine Datei mehr, die beim Bauen eines Features
|
||||||
|
mitgeschrieben werden muss** — als Regel in CLAUDE.md festgehalten, sonst
|
||||||
|
veraltet sie still.
|
||||||
|
|
||||||
|
**Der Link je Tag nennt die Zahl der Knoten, die es HEUTE noch gibt.** Die
|
||||||
|
Schlüssel sind Label-Pfade (dieselbe Identität wie D28/D38); ein seither
|
||||||
|
umbenannter Knoten ist nicht mehr zu treffen. Gezählt wird deshalb gegen den
|
||||||
|
aktuellen Plan — im mitgelieferten Stand sind das beim 22.08. dreißig statt der
|
||||||
|
zweiunddreißig gespeicherten. Ein Link, der „32 Knoten" verspricht und 30
|
||||||
|
zeigt, wäre eine kleine Lüge an einer Stelle, an der es nichts kostet, die
|
||||||
|
Wahrheit zu sagen.
|
||||||
|
|
||||||
|
**Ein Tag kann einen Link ohne Notizen haben, aber nie umgekehrt.** Der
|
||||||
|
Beförderungs-Commit (D30) ist der Regelfall: An einem reinen Deploy-Tag bewegt
|
||||||
|
sich der Plan, ohne dass jemand einen Changelog-Eintrag schreibt. Ohne die
|
||||||
|
Vereinigung beider Quellen fiele ausgerechnet der Tag stumm unter den Tisch, an
|
||||||
|
dem etwas in Produktion gegangen ist.
|
||||||
|
|
||||||
|
**Die Vorführung ersetzt den Besuchsvergleich, statt neben ihm zu stehen.** Es
|
||||||
|
ist dieselbe Ansicht (gelber Kranz), nur mit einer anderen Frage; zwei
|
||||||
|
gleichzeitige Mengen im selben Kanal wären nicht auseinanderzuhalten.
|
||||||
|
Umgeschaltet wird dabei auf den mitgelieferten Plan — dessen Knoten sind
|
||||||
|
gemeint. **Reihenfolge beachtet:** `switchDoc()` räumt einen vorgeführten Tag
|
||||||
|
ausdrücklich weg (in einem anderen Dokument zeigten seine Schlüssel ins Leere),
|
||||||
|
also muss erst gewechselt und dann gesetzt werden. In der anderen Reihenfolge
|
||||||
|
löschte der Wechsel gerade den Tag, den man zeigen wollte — beim Bauen
|
||||||
|
hineingelaufen.
|
||||||
|
|
||||||
|
**Nicht persistiert.** Der Faltzustand steht im Text (D38), der Besuchsstand im
|
||||||
|
localStorage (D28) — eine vorgeführte Chronik ist weder das eine noch das
|
||||||
|
andere. Sie endet mit der Sitzung, mit dem Dokumentwechsel oder mit dem zweiten
|
||||||
|
Druck.
|
||||||
|
|
||||||
|
**Auf dem Telefon ist der Bezug die Werkzeugleiste, nicht der Knopf.** Das
|
||||||
|
Popup ist 351 px breit, der Knopf 30 — an ihm ausgerichtet begann es bei
|
||||||
|
**−116 px**, also außerhalb des Bildes (nachgemessen bei 375 px). `body.mobile
|
||||||
|
.newswrap{position:static}` macht die `.header-tools` zum Bezug, deren rechter
|
||||||
|
Rand auch der der Seite ist: danach 17 bis 368 px. Ein `overflow` an der
|
||||||
|
Kopfzeile als Ausweg verbietet sich — das klippt genau diese Menüs (D50).
|
||||||
|
|
||||||
|
**Backticks werden zu Code-Stücken**, und zwar **nach** dem Escapen: Was der
|
||||||
|
Ersetzung vorliegt, ist bereits harmloser Text, der Weg zu eigenem Markup
|
||||||
|
bleibt also verschlossen. Ohne die Ersetzung stünde `` `#auth` `` mit nackten
|
||||||
|
Backticks im Popup und läse sich wie ein Tippfehler.
|
||||||
|
|
||||||
|
**Nachgemessen** (Werkbaum-Plan, Dev-Server): 12 Tage im Popup, davon einer
|
||||||
|
(28.07.) ohne Notizen mit Link — der Deploy-Tag. Der Link des 24.08. hebt 12
|
||||||
|
Knoten hervor, holt den ersten in die Mitte und färbt den Knopf petrol; ein
|
||||||
|
zweiter Druck stellt die 11 Knoten des Besuchsvergleichs wieder her; „gesehen"
|
||||||
|
löscht sie und nimmt dem Knopf das Bernstein. Bei 375 px liegt das Popup
|
||||||
|
vollständig im Bild und wird an beiden Kanten getroffen (`elementFromPoint`),
|
||||||
|
die Kopfzeile bleibt einreihig. 20 neue Tests in `frontend/tests/news.test.js`,
|
||||||
|
darunter einer, der die **ausgelieferte** `docs/CHANGELOG.md` liest — ist sie
|
||||||
|
unlesbar, stünde das Popup sonst leer da, ohne dass es jemand merkt.
|
||||||
|
|||||||
+38
-2
@@ -650,12 +650,48 @@ Bei Dokumenten, die von außen kommen (mitgeliefert, per `?sourceUrl=` oder
|
|||||||
gezeigt, was sich seit dem letzten Besuch getan hat. **„Neu" heißt: neu in
|
gezeigt, was sich seit dem letzten Besuch getan hat. **„Neu" heißt: neu in
|
||||||
Produktion** — ein Knoten trägt jetzt `[^]` und tat es in der zuletzt gesehenen
|
Produktion** — ein Knoten trägt jetzt `[^]` und tat es in der zuletzt gesehenen
|
||||||
Fassung nicht. Solche Knoten bekommen einen **gelben Strahlenkranz** nach außen
|
Fassung nicht. Solche Knoten bekommen einen **gelben Strahlenkranz** nach außen
|
||||||
(die Füllung bleibt die Statusfarbe aus §4). Ein Knopf im Diagramm-Kopf nennt
|
(die Füllung bleibt die Statusfarbe aus §4). Die Anzahl steht am
|
||||||
die Anzahl und bestätigt per Klick; danach ist die aktuelle Fassung die neue
|
**Neuigkeiten-Knopf** in der Kopfzeile (siehe unten), bestätigt wird im Popup;
|
||||||
|
danach ist die aktuelle Fassung die neue
|
||||||
Vergleichsbasis. Beim ersten Ansehen eines Dokuments leuchtet nichts. Der Kranz
|
Vergleichsbasis. Beim ersten Ansehen eines Dokuments leuchtet nichts. Der Kranz
|
||||||
erscheint weder im Druck noch im Grafikexport — er hängt am persönlichen
|
erscheint weder im Druck noch im Grafikexport — er hängt am persönlichen
|
||||||
Besuchsstand. Siehe D28.
|
Besuchsstand. Siehe D28.
|
||||||
|
|
||||||
|
### Neuigkeiten (Stern in der Kopfzeile)
|
||||||
|
Ein **Stern-Knopf in der oberen Bedienleiste** ist immer sichtbar und öffnet ein
|
||||||
|
Popup mit den Änderungen der letzten Tage — je Tag ein Datum und ein paar kurze
|
||||||
|
Notizen. Er trägt zwei Aussagen, die zusammengehören:
|
||||||
|
|
||||||
|
- **Die Chronik** (allgemein): was am Produkt geschehen ist. Die Notizen stehen
|
||||||
|
in `docs/CHANGELOG.md`, die Knoten je Tag kommen aus der Versionsgeschichte
|
||||||
|
des mitgelieferten Plans; beides wird **beim Bauen** eingelesen und
|
||||||
|
eingebettet (zur Laufzeit lädt Werkbaum nichts nach, D20).
|
||||||
|
- **Der Besuchsvergleich** (persönlich): „Was ist neu?" des aktiven Dokuments
|
||||||
|
(oben) — als abgesetzter Abschnitt zuoberst im Popup, mit dem Knopf
|
||||||
|
„gesehen".
|
||||||
|
|
||||||
|
**Bernstein heißt ungesehen** — dieselben Töne wie der Strahlenkranz am Knoten,
|
||||||
|
damit Knopf und Knoten dasselbe sagen. Er färbt sich, solange es unangesehene
|
||||||
|
Tage gibt oder das aktive Dokument neue Knoten hat; die Zahl daneben ist die der
|
||||||
|
neuen Knoten. Aufgeschlagen heißt gelesen: Das Öffnen des Popups merkt den
|
||||||
|
neuesten gelisteten Tag als gesehen.
|
||||||
|
|
||||||
|
**Jeder Tag mit Knotenänderungen trägt einen Link**, der genau diese Knoten im
|
||||||
|
Diagramm in der „Was ist neu?"-Ansicht vorführt (gelber Kranz, §9) — dieselbe
|
||||||
|
Ansicht, nur mit einer anderen Frage: „was geschah am 24.08." statt „was ist
|
||||||
|
seit deinem letzten Besuch live gegangen". Dabei wird auf den mitgelieferten
|
||||||
|
Plan umgeschaltet, denn dessen Knoten sind gemeint; der Knopf steht dann in
|
||||||
|
Petrol („wird gerade vorgeführt") und ein zweiter Druck hebt es wieder auf.
|
||||||
|
Genannt wird die Zahl der Knoten, die es **heute noch gibt** — ein seither
|
||||||
|
umbenannter Knoten ist nicht mehr zu treffen, und der Link verspricht nichts,
|
||||||
|
was er nicht halten kann. Die Vorführung ist Sitzungssache und wird nicht
|
||||||
|
gemerkt.
|
||||||
|
|
||||||
|
Die **Notizen sind englisch**, auch wenn die Oberfläche in einer anderen Sprache
|
||||||
|
steht: `docs/CHANGELOG.md` ist ein ausgeliefertes Artefakt mit weltweitem
|
||||||
|
Publikum, wie der mitgelieferte Plan und `llms.md` (§13). Übersetzt ist alles
|
||||||
|
übrige — Titel, Knöpfe und die Datumsangaben. Siehe D58.
|
||||||
|
|
||||||
### Sprung zwischen Diagramm und Text
|
### Sprung zwischen Diagramm und Text
|
||||||
Jeder Knoten kennt seine Zeilennummer im Notationstext; beide Richtungen sind
|
Jeder Knoten kennt seine Zeilennummer im Notationstext; beide Richtungen sind
|
||||||
verknüpft (siehe D25):
|
verknüpft (siehe D25):
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
|
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
|
||||||
- [ ] #ed.files: Open and save .werkbaum files (S)
|
- [ ] #ed.files: Open and save .werkbaum files (S)
|
||||||
+ [^] #ed.fresh: Show what is new since your last visit (S)
|
+ [^] #ed.fresh: Show what is new since your last visit (S)
|
||||||
|
- [x] #ed.fresh.news: A star in the header, with the last few days (S) %% see D58
|
||||||
+ [?] #ed.percolor: A pastel colour per person (S)
|
+ [?] #ed.percolor: A pastel colour per person (S)
|
||||||
- [?] #ed.dates: Dates and milestones (M)
|
- [?] #ed.dates: Dates and milestones (M)
|
||||||
| [?] #ed.dates.attr: An attribute in the line (S)
|
| [?] #ed.dates.attr: An attribute in the line (S)
|
||||||
@@ -555,6 +556,12 @@
|
|||||||
your last visit get a yellow halo. New means live, not "line added" — a line
|
your last visit get a yellow halo. New means live, not "line added" — a line
|
||||||
diff would be mostly noise.
|
diff would be mostly noise.
|
||||||
|
|
||||||
|
#ed.fresh.news
|
||||||
|
A star that is always in the header opens a popup with the changes of the
|
||||||
|
last few days; every day can show its nodes in the diagram, in the very same
|
||||||
|
halo view. Notes come from docs/CHANGELOG.md, the nodes from the plan's git
|
||||||
|
history, both read at build time.
|
||||||
|
|
||||||
#ed.percolor
|
#ed.percolor
|
||||||
Give every @name a colour derived from the name itself, so the same person
|
Give every @name a colour derived from the name itself, so the same person
|
||||||
is recognizable across the tree without looking anything up.
|
is recognizable across the tree without looking anything up.
|
||||||
|
|||||||
@@ -526,6 +526,18 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
Hash-Rückfall, der Marker-Pfad ist nur zu prüfen, wenn man einen echten
|
Hash-Rückfall, der Marker-Pfad ist nur zu prüfen, wenn man einen echten
|
||||||
`commit/<sha>` in `index.html` einspritzt (danach zurücknehmen!) und
|
`commit/<sha>` in `index.html` einspritzt (danach zurücknehmen!) und
|
||||||
`window.fetch` überschreibt.
|
`window.fetch` überschreibt.
|
||||||
|
- **Neuigkeiten (D58):** `NEWS` kommt aus dem virtuellen Modul
|
||||||
|
`virtual:werkbaum-news` (Vite-Plugin in `vite.config.js`), gefüllt zur
|
||||||
|
**Bauzeit** aus `docs/CHANGELOG.md` (die Notizen) und der git-Historie des
|
||||||
|
mitgelieferten Plans (die Knoten je Tag). Die Regeln stehen in
|
||||||
|
`scripts/news.mjs` und sind getestet; `app.js` verdrahtet nur. Drei Dinge, die
|
||||||
|
beim Anfassen auffallen: Der **Dev-Server liest einmal beim Start** — neue
|
||||||
|
Changelog-Zeilen erscheinen erst nach einem Neustart, nicht per HMR. Ein
|
||||||
|
vorgeführter Tag muss **nach** `switchDoc()` gesetzt werden, denn das räumt
|
||||||
|
ihn ausdrücklich weg. Und die Backtick-Ersetzung läuft **nach** `esc()`, damit
|
||||||
|
aus einer Changelog-Zeile kein Markup werden kann. Ein Feature ohne Zeile in
|
||||||
|
`docs/CHANGELOG.md` geschieht für den Benutzer unsichtbar (Regel in der
|
||||||
|
Wurzel-CLAUDE.md).
|
||||||
- **Titelzeilen tragen die Aufklapp-Menüs — kein `overflow` daran (D50):**
|
- **Titelzeilen tragen die Aufklapp-Menüs — kein `overflow` daran (D50):**
|
||||||
`#docMenu` und `.dlmenu` hängen als absolut positionierte Kinder im
|
`#docMenu` und `.dlmenu` hängen als absolut positionierte Kinder im
|
||||||
`.panel-head`. Ein `overflow` dort macht ihn zum Scroll-Container und klippt
|
`.panel-head`. Ein `overflow` dort macht ihn zum Scroll-Container und klippt
|
||||||
|
|||||||
+15
-6
@@ -23,6 +23,17 @@
|
|||||||
<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>
|
<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>
|
</h1>
|
||||||
<div class="header-tools">
|
<div class="header-tools">
|
||||||
|
<!-- Neuigkeiten (D58): der Stern aus dem Diagramm-Kopf, jetzt permanent.
|
||||||
|
Sein Popup zeigt die Änderungen der letzten Tage (aus der git-Historie,
|
||||||
|
zur Bauzeit eingelesen); je Tag führt ein Link die betroffenen Knoten
|
||||||
|
in der „Was ist neu?"-Ansicht (D28) im Diagramm vor. -->
|
||||||
|
<span class="newswrap">
|
||||||
|
<button type="button" id="newsBtn" class="fsbtn newsbtn" aria-haspopup="dialog" aria-expanded="false" data-i18n-title="newsTooltip" data-i18n-aria="newsTooltip" title="Neuigkeiten" aria-label="Neuigkeiten">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" 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" hidden>0</span>
|
||||||
|
</button>
|
||||||
|
<div class="newsmenu" id="newsMenu" role="dialog" data-i18n-aria="newsTitle" aria-label="Neuigkeiten" hidden></div>
|
||||||
|
</span>
|
||||||
<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">
|
<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>
|
<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>
|
</button>
|
||||||
@@ -178,15 +189,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- Von Station zu Station des günstigsten Pfads (SPEC §9, D47). Kein
|
<!-- Von Station zu Station des günstigsten Pfads (SPEC §9, D47). Kein
|
||||||
Umschalter, sondern eine Bewegung: jeder Druck holt die nächste noch
|
Umschalter, sondern eine Bewegung: jeder Druck holt die nächste noch
|
||||||
offene Station in die Mitte. Verborgen, solange es keine gibt (wie
|
offene Station in die Mitte. Verborgen, solange es keine gibt. -->
|
||||||
der „Was ist neu?"-Knopf daneben). -->
|
|
||||||
<button type="button" class="copybtn" id="leanNextBtn" title="Zur nächsten Station des günstigsten Pfads" aria-label="Zur nächsten Station des günstigsten Pfads" hidden>
|
<button type="button" class="copybtn" id="leanNextBtn" title="Zur nächsten Station des günstigsten Pfads" aria-label="Zur nächsten Station des günstigsten Pfads" 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="M2.5 12h5" stroke-dasharray="2.5 2.5"/><circle cx="11" cy="12" r="3"/><path d="M15 12h6M18.5 9.5l2.5 2.5-2.5 2.5"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 12h5" stroke-dasharray="2.5 2.5"/><circle cx="11" cy="12" r="3"/><path d="M15 12h6M18.5 9.5l2.5 2.5-2.5 2.5"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="copybtn freshbtn" id="freshBtn" title="Neu in Produktion" aria-label="Neu in Produktion" hidden>
|
<!-- Der Stern stand hier bis D58 („Was ist neu?", D28) und ist in die
|
||||||
<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>
|
Kopfzeile gewandert: Er sagt jetzt nicht nur etwas über das aktive
|
||||||
<span class="freshcount" id="freshCount" aria-hidden="true">0</span>
|
Dokument, sondern über das Produkt — und ist deshalb immer da. -->
|
||||||
</button>
|
|
||||||
<!-- Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die
|
<!-- Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die
|
||||||
beiden Nachbarn. Gedrückt = alles ab Größe M abwärts ist zugeklappt. -->
|
beiden Nachbarn. Gedrückt = alles ab Größe M abwärts ist zugeklappt. -->
|
||||||
<button type="button" class="copybtn togglebtn" id="foldSmallBtn" aria-pressed="false" data-i18n-title="foldSmallTooltip" data-i18n-aria="foldSmallTooltip" title="Knoten der Größe M und kleiner zuklappen – erneut drücken klappt alle wieder auf" aria-label="Knoten der Größe M und kleiner zuklappen – erneut drücken klappt alle wieder auf">
|
<button type="button" class="copybtn togglebtn" id="foldSmallBtn" aria-pressed="false" data-i18n-title="foldSmallTooltip" data-i18n-aria="foldSmallTooltip" title="Knoten der Größe M und kleiner zuklappen – erneut drücken klappt alle wieder auf" aria-label="Knoten der Größe M und kleiner zuklappen – erneut drücken klappt alle wieder auf">
|
||||||
|
|||||||
+252
-27
@@ -6,6 +6,10 @@ import { formatWarning, warningText } from './warnings.js';
|
|||||||
import { padUrls } from './remote.js';
|
import { padUrls } from './remote.js';
|
||||||
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
||||||
from './snapshots.js';
|
from './snapshots.js';
|
||||||
|
/* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
|
||||||
|
vite.config.js). Zur Laufzeit gibt es kein git — und keinen Server, der
|
||||||
|
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */
|
||||||
|
import NEWS from 'virtual:werkbaum-news';
|
||||||
/* Werkbaum, mit Werkbaum geplant — als mitgeliefertes Dokument „Werkbank" (D27).
|
/* Werkbaum, mit Werkbaum geplant — als mitgeliefertes Dokument „Werkbank" (D27).
|
||||||
Dieselbe Datei, die auch per ?sourceUrl= geladen werden kann; `?raw` bettet
|
Dieselbe Datei, die auch per ?sourceUrl= geladen werden kann; `?raw` bettet
|
||||||
sie beim Build in die eine Ausgabedatei ein (D19), es wird nichts nachgeladen
|
sie beim Build in die eine Ausgabedatei ein (D19), es wird nichts nachgeladen
|
||||||
@@ -76,6 +80,12 @@ let sourceWarning = null;
|
|||||||
Vergleichsfassung. `freshBaseline` ist der Text, gegen den verglichen wurde;
|
Vergleichsfassung. `freshBaseline` ist der Text, gegen den verglichen wurde;
|
||||||
er wird erst beim Bestätigen fortgeschrieben. */
|
er wird erst beim Bestätigen fortgeschrieben. */
|
||||||
let freshSet = new Set(), freshDocId = null, freshBaseline = null, freshPrevRoots = null;
|
let freshSet = new Set(), freshDocId = null, freshBaseline = null, freshPrevRoots = null;
|
||||||
|
/* Führt das Neuigkeiten-Popup gerade einen Tag vor (D58)? Dann tritt dessen
|
||||||
|
Knotenmenge an die Stelle des Besuchsvergleichs — `newsDay` ist das Datum
|
||||||
|
(auch die Marke im Popup), `newsKeySet` die Label-Pfade. Sitzungssache: Der
|
||||||
|
Faltzustand steht im Text (D38), der Besuchsstand im localStorage — eine
|
||||||
|
vorgeführte Chronik ist weder das eine noch das andere. */
|
||||||
|
let newsDay = null, newsKeySet = null;
|
||||||
/* Faltung (SPEC §9, D38): `foldOverrides` sind die interaktiven Eingriffe des
|
/* Faltung (SPEC §9, D38): `foldOverrides` sind die interaktiven Eingriffe des
|
||||||
Nutzers (Schlüssel = Label-Pfad wie bei D28, damit sie das Neu-Parsen bei
|
Nutzers (Schlüssel = Label-Pfad wie bei D28, damit sie das Neu-Parsen bei
|
||||||
jedem Tastendruck überleben); sie überlagern den Anfangszustand aus den
|
jedem Tastendruck überleben); sie überlagern den Anfangszustand aus den
|
||||||
@@ -118,17 +128,26 @@ function render(){
|
|||||||
if(!plan.exact) warnings = warnings.concat([{type: 'cheapApprox'}]);
|
if(!plan.exact) warnings = warnings.concat([{type: 'cheapApprox'}]);
|
||||||
}
|
}
|
||||||
out.classList.toggle('cheap-on', cheapPathOn);
|
out.classList.toggle('cheap-on', cheapPathOn);
|
||||||
/* Die Menge MUSS aus denselben Knotenobjekten gebildet werden, die gerade
|
|
||||||
gerendert werden — `freshProdSet` liefert Knoten aus `roots`. Eine früher
|
|
||||||
berechnete Menge stammte aus einem anderen Parse-Durchlauf und träfe per
|
|
||||||
Objektidentität nie zu (D28). */
|
|
||||||
freshSet = (freshDocId === activeId && freshPrevRoots)
|
|
||||||
? freshProdSet(freshPrevRoots, roots) : new Set();
|
|
||||||
/* Faltung (D38): Anfangszustand aus den Textmarken (`!!!` holt sich mit
|
/* Faltung (D38): Anfangszustand aus den Textmarken (`!!!` holt sich mit
|
||||||
hervor), überlagert von den Sitzungs-Eingriffen des Nutzers. Wie bei
|
hervor), überlagert von den Sitzungs-Eingriffen des Nutzers. Wie bei
|
||||||
`freshSet` muss die Menge aus den gerade geparsten Knoten bestehen. */
|
`freshSet` muss die Menge aus den gerade geparsten Knoten bestehen. */
|
||||||
const initFold = initialCollapsed(roots, true);
|
const initFold = initialCollapsed(roots, true);
|
||||||
const keys = nodeKeys(roots);
|
const keys = nodeKeys(roots);
|
||||||
|
/* Die Menge MUSS aus denselben Knotenobjekten gebildet werden, die gerade
|
||||||
|
gerendert werden — `freshProdSet` liefert Knoten aus `roots`. Eine früher
|
||||||
|
berechnete Menge stammte aus einem anderen Parse-Durchlauf und träfe per
|
||||||
|
Objektidentität nie zu (D28).
|
||||||
|
Führt das Neuigkeiten-Popup gerade einen Tag vor (D58), gilt dessen
|
||||||
|
Knotenmenge STATT des Vergleichs mit dem letzten Besuch: Es ist dieselbe
|
||||||
|
Ansicht, nur mit einer anderen Frage — „was geschah am 24.08." statt
|
||||||
|
„was ist seit deinem letzten Besuch live gegangen". */
|
||||||
|
if(newsKeySet){
|
||||||
|
freshSet = new Set();
|
||||||
|
keys.forEach((key, n) => { if(newsKeySet.has(key)) freshSet.add(n); });
|
||||||
|
} else {
|
||||||
|
freshSet = (freshDocId === activeId && freshPrevRoots)
|
||||||
|
? freshProdSet(freshPrevRoots, roots) : new Set();
|
||||||
|
}
|
||||||
const collapsedSet = new Set();
|
const collapsedSet = new Set();
|
||||||
let foldSmallExact = true, foldSmallAny = false;
|
let foldSmallExact = true, foldSmallAny = false;
|
||||||
foldByLine = new Map();
|
foldByLine = new Map();
|
||||||
@@ -1353,7 +1372,7 @@ document.addEventListener('pointerdown', e => {
|
|||||||
if(e.target && e.target.closest && e.target.closest('.node') === tipNode) return;
|
if(e.target && e.target.closest && e.target.closest('.node') === tipNode) return;
|
||||||
closeNodeTip();
|
closeNodeTip();
|
||||||
}, true);
|
}, true);
|
||||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeNodeTip(); });
|
document.addEventListener('keydown', e => { if(e.key === 'Escape'){ closeNodeTip(); closeNewsMenu(); } });
|
||||||
/* Beim Scrollen des Diagramms wandert der Knoten, das `position:fixed`-Fenster
|
/* Beim Scrollen des Diagramms wandert der Knoten, das `position:fixed`-Fenster
|
||||||
nicht — es zeigte dann auf etwas anderes. Also zumachen. */
|
nicht — es zeigte dann auf etwas anderes. Also zumachen. */
|
||||||
document.querySelector('.diagram').addEventListener('scroll', closeNodeTip, {passive: true});
|
document.querySelector('.diagram').addEventListener('scroll', closeNodeTip, {passive: true});
|
||||||
@@ -1884,7 +1903,16 @@ const I18N = {
|
|||||||
agenda:"Agenda", discarded:"verworfene",
|
agenda:"Agenda", discarded:"verworfene",
|
||||||
gutterTooltip:"Ziehen zum Verschieben, Doppelklick setzt zurück", gutterAria:"Bereiche größenverändern",
|
gutterTooltip:"Ziehen zum Verschieben, Doppelklick setzt zurück", gutterAria:"Bereiche größenverändern",
|
||||||
hintGutterAria:"Editor und Legende größenverändern",
|
hintGutterAria:"Editor und Legende größenverändern",
|
||||||
freshTooltip:"Neu in Produktion seit dem letzten Ansehen: {n}. Klicken markiert sie als gesehen.",
|
freshTooltip:"Neu in Produktion seit dem letzten Ansehen: {n}.",
|
||||||
|
newsTooltip:"Neuigkeiten",
|
||||||
|
newsTitle:"Neuigkeiten",
|
||||||
|
newsEmpty:"Keine Einträge.",
|
||||||
|
newsUnseen:"Neuigkeiten aus {n} Tagen, die du noch nicht angesehen hast.",
|
||||||
|
newsSince:"Seit deinem letzten Besuch: {n} neu in Produktion.",
|
||||||
|
newsSeen:"gesehen",
|
||||||
|
newsShow:"{n} Knoten im Diagramm zeigen",
|
||||||
|
newsShowOff:"Hervorhebung aufheben",
|
||||||
|
newsShowing:"Neuigkeiten vom {d} werden im Diagramm gezeigt.",
|
||||||
modeHorizontal:"Horizontal – Organigramm, Diagramm über dem Editor",
|
modeHorizontal:"Horizontal – Organigramm, Diagramm über dem Editor",
|
||||||
modeKompakt:"Kompakt – alles nach unten, platzsparend",
|
modeKompakt:"Kompakt – alles nach unten, platzsparend",
|
||||||
modeVertikal:"Vertikal – Baum nach rechts, Diagramm neben dem Editor",
|
modeVertikal:"Vertikal – Baum nach rechts, Diagramm neben dem Editor",
|
||||||
@@ -1974,7 +2002,16 @@ const I18N = {
|
|||||||
agenda:"Legend", discarded:"discarded",
|
agenda:"Legend", discarded:"discarded",
|
||||||
gutterTooltip:"Drag to resize, double-click resets", gutterAria:"Resize the areas",
|
gutterTooltip:"Drag to resize, double-click resets", gutterAria:"Resize the areas",
|
||||||
hintGutterAria:"Resize editor and legend",
|
hintGutterAria:"Resize editor and legend",
|
||||||
freshTooltip:"New in production since you last looked: {n}. Click to mark them as seen.",
|
freshTooltip:"New in production since you last looked: {n}.",
|
||||||
|
newsTooltip:"What's new",
|
||||||
|
newsTitle:"What's new",
|
||||||
|
newsEmpty:"Nothing recorded.",
|
||||||
|
newsUnseen:"News from {n} days you haven't looked at yet.",
|
||||||
|
newsSince:"Since your last visit: {n} new in production.",
|
||||||
|
newsSeen:"seen",
|
||||||
|
newsShow:"Show {n} nodes in the diagram",
|
||||||
|
newsShowOff:"Clear the highlight",
|
||||||
|
newsShowing:"Showing the changes of {d} in the diagram.",
|
||||||
modeHorizontal:"Horizontal – org chart, diagram above the editor",
|
modeHorizontal:"Horizontal – org chart, diagram above the editor",
|
||||||
modeKompakt:"Compact – everything downward, space-saving",
|
modeKompakt:"Compact – everything downward, space-saving",
|
||||||
modeVertikal:"Vertical – tree to the right, diagram beside the editor",
|
modeVertikal:"Vertical – tree to the right, diagram beside the editor",
|
||||||
@@ -2064,7 +2101,16 @@ const I18N = {
|
|||||||
agenda:"Leyenda", discarded:"descartados",
|
agenda:"Leyenda", discarded:"descartados",
|
||||||
gutterTooltip:"Arrastra para redimensionar, doble clic restablece", gutterAria:"Redimensionar las áreas",
|
gutterTooltip:"Arrastra para redimensionar, doble clic restablece", gutterAria:"Redimensionar las áreas",
|
||||||
hintGutterAria:"Redimensionar editor y leyenda",
|
hintGutterAria:"Redimensionar editor y leyenda",
|
||||||
freshTooltip:"Nuevo en producción desde la última vez: {n}. Haz clic para marcarlo como visto.",
|
freshTooltip:"Nuevo en producción desde la última vez: {n}.",
|
||||||
|
newsTooltip:"Novedades",
|
||||||
|
newsTitle:"Novedades",
|
||||||
|
newsEmpty:"Sin entradas.",
|
||||||
|
newsUnseen:"Novedades de {n} días que aún no has visto.",
|
||||||
|
newsSince:"Desde tu última visita: {n} nuevo en producción.",
|
||||||
|
newsSeen:"visto",
|
||||||
|
newsShow:"Mostrar {n} nodos en el diagrama",
|
||||||
|
newsShowOff:"Quitar el resaltado",
|
||||||
|
newsShowing:"Se muestran en el diagrama los cambios del {d}.",
|
||||||
modeHorizontal:"Horizontal – organigrama, diagrama sobre el editor",
|
modeHorizontal:"Horizontal – organigrama, diagrama sobre el editor",
|
||||||
modeKompakt:"Compacto – todo hacia abajo, ahorra espacio",
|
modeKompakt:"Compacto – todo hacia abajo, ahorra espacio",
|
||||||
modeVertikal:"Vertical – árbol hacia la derecha, diagrama junto al editor",
|
modeVertikal:"Vertical – árbol hacia la derecha, diagrama junto al editor",
|
||||||
@@ -2154,7 +2200,16 @@ const I18N = {
|
|||||||
agenda:"Légende", discarded:"abandonnés",
|
agenda:"Légende", discarded:"abandonnés",
|
||||||
gutterTooltip:"Glisser pour redimensionner, double-clic pour réinitialiser", gutterAria:"Redimensionner les zones",
|
gutterTooltip:"Glisser pour redimensionner, double-clic pour réinitialiser", gutterAria:"Redimensionner les zones",
|
||||||
hintGutterAria:"Redimensionner l'éditeur et la légende",
|
hintGutterAria:"Redimensionner l'éditeur et la légende",
|
||||||
freshTooltip:"Nouveau en production depuis votre dernière visite : {n}. Cliquez pour marquer comme vu.",
|
freshTooltip:"Nouveau en production depuis votre dernière visite : {n}.",
|
||||||
|
newsTooltip:"Nouveautés",
|
||||||
|
newsTitle:"Nouveautés",
|
||||||
|
newsEmpty:"Aucune entrée.",
|
||||||
|
newsUnseen:"Nouveautés de {n} jours que vous n'avez pas encore consultées.",
|
||||||
|
newsSince:"Depuis votre dernière visite : {n} nouveau en production.",
|
||||||
|
newsSeen:"vu",
|
||||||
|
newsShow:"Afficher {n} nœuds dans le diagramme",
|
||||||
|
newsShowOff:"Retirer la mise en évidence",
|
||||||
|
newsShowing:"Les changements du {d} sont affichés dans le diagramme.",
|
||||||
modeHorizontal:"Horizontal – organigramme, diagramme au-dessus de l'éditeur",
|
modeHorizontal:"Horizontal – organigramme, diagramme au-dessus de l'éditeur",
|
||||||
modeKompakt:"Compact – tout vers le bas, gain de place",
|
modeKompakt:"Compact – tout vers le bas, gain de place",
|
||||||
modeVertikal:"Vertical – arbre vers la droite, diagramme à côté de l'éditeur",
|
modeVertikal:"Vertical – arbre vers la droite, diagramme à côté de l'éditeur",
|
||||||
@@ -2244,7 +2299,16 @@ const I18N = {
|
|||||||
agenda:"Legenda", discarded:"odrzucone",
|
agenda:"Legenda", discarded:"odrzucone",
|
||||||
gutterTooltip:"Przeciągnij, aby zmienić rozmiar; dwuklik przywraca", gutterAria:"Zmień rozmiar obszarów",
|
gutterTooltip:"Przeciągnij, aby zmienić rozmiar; dwuklik przywraca", gutterAria:"Zmień rozmiar obszarów",
|
||||||
hintGutterAria:"Zmień rozmiar edytora i legendy",
|
hintGutterAria:"Zmień rozmiar edytora i legendy",
|
||||||
freshTooltip:"Nowe na produkcji od ostatniego razu: {n}. Kliknij, aby oznaczyć jako zobaczone.",
|
freshTooltip:"Nowe na produkcji od ostatniego razu: {n}.",
|
||||||
|
newsTooltip:"Nowości",
|
||||||
|
newsTitle:"Nowości",
|
||||||
|
newsEmpty:"Brak wpisów.",
|
||||||
|
newsUnseen:"Nowości z {n} dni, których jeszcze nie oglądałeś.",
|
||||||
|
newsSince:"Od twojej ostatniej wizyty: {n} nowe na produkcji.",
|
||||||
|
newsSeen:"zobaczone",
|
||||||
|
newsShow:"Pokaż {n} węzłów na diagramie",
|
||||||
|
newsShowOff:"Usuń podświetlenie",
|
||||||
|
newsShowing:"Na diagramie pokazane są zmiany z {d}.",
|
||||||
modeHorizontal:"Poziomo – schemat organizacyjny, diagram nad edytorem",
|
modeHorizontal:"Poziomo – schemat organizacyjny, diagram nad edytorem",
|
||||||
modeKompakt:"Kompaktowo – wszystko w dół, oszczędza miejsce",
|
modeKompakt:"Kompaktowo – wszystko w dół, oszczędza miejsce",
|
||||||
modeVertikal:"Pionowo – drzewo w prawo, diagram obok edytora",
|
modeVertikal:"Pionowo – drzewo w prawo, diagram obok edytora",
|
||||||
@@ -2334,7 +2398,16 @@ const I18N = {
|
|||||||
agenda:"Легенда", discarded:"отклонённые",
|
agenda:"Легенда", discarded:"отклонённые",
|
||||||
gutterTooltip:"Потяните, чтобы изменить размер; двойной щелчок сбрасывает", gutterAria:"Изменить размер областей",
|
gutterTooltip:"Потяните, чтобы изменить размер; двойной щелчок сбрасывает", gutterAria:"Изменить размер областей",
|
||||||
hintGutterAria:"Изменить размер редактора и легенды",
|
hintGutterAria:"Изменить размер редактора и легенды",
|
||||||
freshTooltip:"Новое в продакшене с прошлого раза: {n}. Нажмите, чтобы отметить как просмотренное.",
|
freshTooltip:"Новое в продакшене с прошлого раза: {n}.",
|
||||||
|
newsTooltip:"Новости",
|
||||||
|
newsTitle:"Новости",
|
||||||
|
newsEmpty:"Записей нет.",
|
||||||
|
newsUnseen:"Новости за {n} дней, которые вы ещё не просматривали.",
|
||||||
|
newsSince:"С вашего последнего визита: {n} новых в продакшене.",
|
||||||
|
newsSeen:"просмотрено",
|
||||||
|
newsShow:"Показать {n} узлов на диаграмме",
|
||||||
|
newsShowOff:"Снять выделение",
|
||||||
|
newsShowing:"На диаграмме показаны изменения от {d}.",
|
||||||
modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором",
|
modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором",
|
||||||
modeKompakt:"Компактно – всё вниз, экономит место",
|
modeKompakt:"Компактно – всё вниз, экономит место",
|
||||||
modeVertikal:"Вертикально – дерево вправо, диаграмма рядом с редактором",
|
modeVertikal:"Вертикально – дерево вправо, диаграмма рядом с редактором",
|
||||||
@@ -2424,7 +2497,16 @@ const I18N = {
|
|||||||
agenda:"लेजेंड", discarded:"अस्वीकृत",
|
agenda:"लेजेंड", discarded:"अस्वीकृत",
|
||||||
gutterTooltip:"आकार बदलने के लिए खींचें, डबल-क्लिक रीसेट करता है", gutterAria:"क्षेत्रों का आकार बदलें",
|
gutterTooltip:"आकार बदलने के लिए खींचें, डबल-क्लिक रीसेट करता है", gutterAria:"क्षेत्रों का आकार बदलें",
|
||||||
hintGutterAria:"संपादक और लेजेंड का आकार बदलें",
|
hintGutterAria:"संपादक और लेजेंड का आकार बदलें",
|
||||||
freshTooltip:"पिछली बार से उत्पादन में नया: {n}. देखा गया चिह्नित करने के लिए क्लिक करें।",
|
freshTooltip:"पिछली बार से उत्पादन में नया: {n}।",
|
||||||
|
newsTooltip:"नया क्या है",
|
||||||
|
newsTitle:"नया क्या है",
|
||||||
|
newsEmpty:"कोई प्रविष्टि नहीं।",
|
||||||
|
newsUnseen:"{n} दिनों की खबरें, जो आपने अभी तक नहीं देखीं।",
|
||||||
|
newsSince:"आपकी पिछली यात्रा से: {n} उत्पादन में नए।",
|
||||||
|
newsSeen:"देखा गया",
|
||||||
|
newsShow:"आरेख में {n} नोड दिखाएँ",
|
||||||
|
newsShowOff:"हाइलाइट हटाएँ",
|
||||||
|
newsShowing:"आरेख में {d} के बदलाव दिखाए जा रहे हैं।",
|
||||||
modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख",
|
modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख",
|
||||||
modeKompakt:"सघन – सब नीचे की ओर, जगह बचाता है",
|
modeKompakt:"सघन – सब नीचे की ओर, जगह बचाता है",
|
||||||
modeVertikal:"लंबवत – पेड़ दाईं ओर, संपादक के बगल में आरेख",
|
modeVertikal:"लंबवत – पेड़ दाईं ओर, संपादक के बगल में आरेख",
|
||||||
@@ -2514,7 +2596,16 @@ const I18N = {
|
|||||||
agenda:"图例", discarded:"已放弃",
|
agenda:"图例", discarded:"已放弃",
|
||||||
gutterTooltip:"拖动可调整大小,双击可重置", gutterAria:"调整区域大小",
|
gutterTooltip:"拖动可调整大小,双击可重置", gutterAria:"调整区域大小",
|
||||||
hintGutterAria:"调整编辑器和图例大小",
|
hintGutterAria:"调整编辑器和图例大小",
|
||||||
freshTooltip:"自上次查看以来新上线:{n}。点击标记为已查看。",
|
freshTooltip:"自上次查看以来新上线:{n}。",
|
||||||
|
newsTooltip:"最新动态",
|
||||||
|
newsTitle:"最新动态",
|
||||||
|
newsEmpty:"暂无记录。",
|
||||||
|
newsUnseen:"有 {n} 天的动态你还没有看过。",
|
||||||
|
newsSince:"自你上次访问以来:{n} 项新上线。",
|
||||||
|
newsSeen:"已查看",
|
||||||
|
newsShow:"在图中显示 {n} 个节点",
|
||||||
|
newsShowOff:"取消高亮",
|
||||||
|
newsShowing:"正在图中显示 {d} 的变更。",
|
||||||
modeHorizontal:"横向——组织结构图,图表在编辑器上方",
|
modeHorizontal:"横向——组织结构图,图表在编辑器上方",
|
||||||
modeKompakt:"紧凑——全部向下,节省空间",
|
modeKompakt:"紧凑——全部向下,节省空间",
|
||||||
modeVertikal:"纵向——树向右展开,图表在编辑器旁边",
|
modeVertikal:"纵向——树向右展开,图表在编辑器旁边",
|
||||||
@@ -2604,7 +2695,16 @@ const I18N = {
|
|||||||
agenda:"凡例", discarded:"破棄",
|
agenda:"凡例", discarded:"破棄",
|
||||||
gutterTooltip:"ドラッグでサイズ変更、ダブルクリックでリセット", gutterAria:"領域のサイズを変更",
|
gutterTooltip:"ドラッグでサイズ変更、ダブルクリックでリセット", gutterAria:"領域のサイズを変更",
|
||||||
hintGutterAria:"エディターと凡例のサイズを変更",
|
hintGutterAria:"エディターと凡例のサイズを変更",
|
||||||
freshTooltip:"前回以降に本番化されたもの:{n} 件。クリックで既読にします。",
|
freshTooltip:"前回以降に本番化されたもの:{n} 件。",
|
||||||
|
newsTooltip:"更新情報",
|
||||||
|
newsTitle:"更新情報",
|
||||||
|
newsEmpty:"項目はありません。",
|
||||||
|
newsUnseen:"まだ見ていない {n} 日分の更新があります。",
|
||||||
|
newsSince:"前回の訪問以降:{n} 件が本番化されました。",
|
||||||
|
newsSeen:"確認済み",
|
||||||
|
newsShow:"図に {n} 件のノードを表示",
|
||||||
|
newsShowOff:"ハイライトを解除",
|
||||||
|
newsShowing:"図に {d} の変更を表示しています。",
|
||||||
modeHorizontal:"横 — 組織図、ダイアグラムはエディターの上",
|
modeHorizontal:"横 — 組織図、ダイアグラムはエディターの上",
|
||||||
modeKompakt:"コンパクト — すべて下方向、省スペース",
|
modeKompakt:"コンパクト — すべて下方向、省スペース",
|
||||||
modeVertikal:"縦 — ツリーを右へ、ダイアグラムはエディターの横",
|
modeVertikal:"縦 — ツリーを右へ、ダイアグラムはエディターの横",
|
||||||
@@ -2703,6 +2803,10 @@ function applyLang(l){
|
|||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
updateDocName(); /* Titelzeilen-Tooltip nach dem data-i18n-title-Durchlauf setzen */
|
updateDocName(); /* Titelzeilen-Tooltip nach dem data-i18n-title-Durchlauf setzen */
|
||||||
|
/* Das Neuigkeiten-Popup wird bei jedem Öffnen gebaut — steht es gerade
|
||||||
|
offen, muss es die neue Sprache jetzt bekommen (auch die Datumsangaben,
|
||||||
|
die `toLocaleDateString` liefert). */
|
||||||
|
if(newsMenu && !newsMenu.hidden) renderNewsMenu();
|
||||||
try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
|
try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
|
||||||
}
|
}
|
||||||
const langsel = document.querySelector('.langsel');
|
const langsel = document.querySelector('.langsel');
|
||||||
@@ -3177,19 +3281,136 @@ function acknowledgeFresh(){
|
|||||||
freshPrevRoots = null;
|
freshPrevRoots = null;
|
||||||
render(); /* render() bildet die Menge neu und meldet dem Knopf */
|
render(); /* render() bildet die Menge neu und meldet dem Knopf */
|
||||||
}
|
}
|
||||||
const freshBtn = document.getElementById('freshBtn');
|
/* ---------- Neuigkeiten (D58) ----------
|
||||||
|
Der Stern aus dem Diagramm-Kopf steht jetzt permanent in der Kopfzeile und
|
||||||
|
trägt zwei Aussagen, die zusammengehören: die **Chronik** aus der
|
||||||
|
git-Historie (`NEWS`, zur Bauzeit eingelesen) und den persönlichen
|
||||||
|
Besuchsvergleich des aktiven Dokuments (D28). Bernstein heißt „ungesehen" —
|
||||||
|
für beides. */
|
||||||
|
const LS_NEWS_SEEN = 'werkbaum-news-seen';
|
||||||
|
const newsBtn = document.getElementById('newsBtn');
|
||||||
|
const newsMenu = document.getElementById('newsMenu');
|
||||||
const freshCount = document.getElementById('freshCount');
|
const freshCount = document.getElementById('freshCount');
|
||||||
function updateFreshBtn(){
|
|
||||||
const n = freshDocId === activeId ? freshSet.size : 0;
|
function newsSeenDate(){
|
||||||
if(!freshBtn) return;
|
try{ return localStorage.getItem(LS_NEWS_SEEN) || ''; }catch(_){ return ''; }
|
||||||
freshBtn.hidden = !n;
|
|
||||||
if(!n) return;
|
|
||||||
freshCount.textContent = String(n);
|
|
||||||
const tip = t('freshTooltip', {n: n});
|
|
||||||
freshBtn.title = tip;
|
|
||||||
freshBtn.setAttribute('aria-label', tip);
|
|
||||||
}
|
}
|
||||||
freshBtn.addEventListener('click', acknowledgeFresh);
|
function unseenDays(){
|
||||||
|
const seen = newsSeenDate();
|
||||||
|
return NEWS.filter(e => e.date > seen).length;
|
||||||
|
}
|
||||||
|
/* Datum in der Sprache der Oberfläche. `T00:00` ist Pflicht: Ein blankes
|
||||||
|
`new Date('2026-08-24')` liest der Browser als UTC-Mitternacht und zeigt
|
||||||
|
westlich davon den Vortag. */
|
||||||
|
function newsDateLabel(iso){
|
||||||
|
const d = new Date(iso + 'T00:00');
|
||||||
|
try{ return d.toLocaleDateString(lang, {day: 'numeric', month: 'long', year: 'numeric'}); }
|
||||||
|
catch(_){ return iso; }
|
||||||
|
}
|
||||||
|
/* Welche Schlüssel gibt es im mitgelieferten Plan überhaupt noch? Ein Label
|
||||||
|
kann seit damals umbenannt worden sein — dann trifft sein Schlüssel nichts
|
||||||
|
mehr, und der Link verspräche etwas, das er nicht halten kann. Einmal je
|
||||||
|
Aufklappen, nicht einmal je Tag: Der Plan hat 900 Zeilen. */
|
||||||
|
function planKeySet(){
|
||||||
|
const doc = docs.find(d => d.id === WERKBAUM_ID);
|
||||||
|
return doc ? new Set(nodeKeys(parse(doc.text).roots).values()) : new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFreshBtn(){
|
||||||
|
if(!newsBtn) return;
|
||||||
|
const n = (!newsKeySet && freshDocId === activeId) ? freshSet.size : 0;
|
||||||
|
const offen = unseenDays();
|
||||||
|
freshCount.hidden = !n;
|
||||||
|
if(n) freshCount.textContent = String(n);
|
||||||
|
newsBtn.classList.toggle('unseen', !!(n || offen));
|
||||||
|
newsBtn.classList.toggle('active', !!newsDay);
|
||||||
|
const tip = newsDay ? t('newsShowing', {d: newsDateLabel(newsDay)})
|
||||||
|
: n ? t('freshTooltip', {n: n})
|
||||||
|
: offen ? t('newsUnseen', {n: offen})
|
||||||
|
: t('newsTooltip');
|
||||||
|
newsBtn.title = tip;
|
||||||
|
newsBtn.setAttribute('aria-label', tip);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNewsMenu(){
|
||||||
|
const seit = (!newsKeySet && freshDocId === activeId && freshSet.size)
|
||||||
|
? `<div class="newssince"><span>${esc(t('newsSince', {n: freshSet.size}))}</span>`
|
||||||
|
+ `<button type="button" class="newsseen" id="newsSeenBtn">${esc(t('newsSeen'))}</button></div>`
|
||||||
|
: '';
|
||||||
|
const vorhanden = NEWS.some(e => e.keys.length) ? planKeySet() : new Set();
|
||||||
|
const tage = NEWS.map(e => {
|
||||||
|
const treffer = e.keys.filter(k => vorhanden.has(k)).length;
|
||||||
|
const an = newsDay === e.date;
|
||||||
|
/* `\`#auth\`` im Changelog wird zu einem Code-Stück — die Notizen nennen
|
||||||
|
Notation, und nackte Backticks läsen sich wie ein Tippfehler. Ersetzt
|
||||||
|
wird NACH dem Escapen, damit der Weg zu eigenem Markup verschlossen
|
||||||
|
bleibt: Was hier eintrifft, ist bereits harmloser Text. */
|
||||||
|
const notiz = l => esc(l).replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
|
const lines = e.lines.length
|
||||||
|
? `<ul class="newslines">${e.lines.map(l => `<li>${notiz(l)}</li>`).join('')}</ul>` : '';
|
||||||
|
const knopf = treffer
|
||||||
|
? `<button type="button" class="newsshow" data-news-day="${esc(e.date)}"`
|
||||||
|
+ ` aria-pressed="${an ? 'true' : 'false'}">`
|
||||||
|
+ `${esc(an ? t('newsShowOff') : t('newsShow', {n: treffer}))}</button>` : '';
|
||||||
|
return `<div class="newsday"><div class="newsdate">${esc(newsDateLabel(e.date))}</div>`
|
||||||
|
+ lines + knopf + '</div>';
|
||||||
|
}).join('');
|
||||||
|
newsMenu.innerHTML = `<div class="newshead"><span>${esc(t('newsTitle'))}</span>`
|
||||||
|
+ `<button type="button" class="newsclose" id="newsCloseBtn"`
|
||||||
|
+ ` title="${esc(t('tipClose'))}" aria-label="${esc(t('tipClose'))}">×</button></div>`
|
||||||
|
+ seit + (tage || `<div class="newsempty">${esc(t('newsEmpty'))}</div>`);
|
||||||
|
}
|
||||||
|
function openNewsMenu(){
|
||||||
|
renderNewsMenu();
|
||||||
|
newsMenu.hidden = false;
|
||||||
|
newsBtn.setAttribute('aria-expanded', 'true');
|
||||||
|
/* Aufgeschlagen heißt gelesen: Der Deckel wandert auf den neuesten Tag der
|
||||||
|
Liste, nicht auf „heute" — sonst hinge er an der Uhr des Betrachters. */
|
||||||
|
if(NEWS.length){ try{ localStorage.setItem(LS_NEWS_SEEN, NEWS[0].date); }catch(_){} }
|
||||||
|
updateFreshBtn();
|
||||||
|
}
|
||||||
|
function closeNewsMenu(){
|
||||||
|
if(!newsMenu || newsMenu.hidden) return;
|
||||||
|
newsMenu.hidden = true;
|
||||||
|
newsBtn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
/* Einen Tag im Diagramm vorführen. Die Schlüssel sind Label-Pfade des
|
||||||
|
MITGELIEFERTEN Plans — steht ein anderes Dokument vorn, wird gewechselt;
|
||||||
|
ohne das zeigte die Ansicht auf nichts. */
|
||||||
|
function showNewsDay(date){
|
||||||
|
const e = NEWS.find(x => x.date === date);
|
||||||
|
if(!e) return;
|
||||||
|
/* Erst wechseln, dann setzen: `switchDoc()` räumt einen vorgeführten Tag
|
||||||
|
ausdrücklich weg — in der anderen Reihenfolge löschte es gerade den, den
|
||||||
|
wir zeigen wollen. */
|
||||||
|
if(activeId !== WERKBAUM_ID && docs.some(d => d.id === WERKBAUM_ID)) switchDoc(WERKBAUM_ID);
|
||||||
|
newsDay = date;
|
||||||
|
newsKeySet = new Set(e.keys);
|
||||||
|
render();
|
||||||
|
closeNewsMenu();
|
||||||
|
const erster = out.querySelector('.node.fresh');
|
||||||
|
if(erster) erster.scrollIntoView({block: 'center', inline: 'center'});
|
||||||
|
}
|
||||||
|
function clearNewsDay(){
|
||||||
|
if(!newsDay) return;
|
||||||
|
newsDay = null; newsKeySet = null;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
newsBtn.addEventListener('click', () => {
|
||||||
|
if(newsMenu.hidden) openNewsMenu(); else closeNewsMenu();
|
||||||
|
});
|
||||||
|
newsMenu.addEventListener('click', e => {
|
||||||
|
const tag = e.target.closest('[data-news-day]');
|
||||||
|
if(tag){ const d = tag.getAttribute('data-news-day');
|
||||||
|
if(newsDay === d){ clearNewsDay(); renderNewsMenu(); } else showNewsDay(d);
|
||||||
|
return; }
|
||||||
|
if(e.target.closest('#newsSeenBtn')){ acknowledgeFresh(); renderNewsMenu(); return; }
|
||||||
|
if(e.target.closest('#newsCloseBtn')) closeNewsMenu();
|
||||||
|
});
|
||||||
|
document.addEventListener('pointerdown', e => {
|
||||||
|
if(!newsMenu || newsMenu.hidden) return;
|
||||||
|
if(!e.target.closest('#newsMenu') && !e.target.closest('#newsBtn')) closeNewsMenu();
|
||||||
|
});
|
||||||
|
|
||||||
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
||||||
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
||||||
@@ -3201,6 +3422,10 @@ function switchDoc(id){
|
|||||||
flushActive();
|
flushActive();
|
||||||
activeId = id;
|
activeId = id;
|
||||||
foldOverrides.clear(); /* Falt-Eingriffe gelten je Dokument-Sitzung (D38) */
|
foldOverrides.clear(); /* Falt-Eingriffe gelten je Dokument-Sitzung (D38) */
|
||||||
|
/* Ein vorgeführter Neuigkeiten-Tag (D58) gehört dem mitgelieferten Plan —
|
||||||
|
in einem anderen Dokument zeigten seine Schlüssel ins Leere. Kein
|
||||||
|
`clearNewsDay()`: `loadActiveIntoEditor()` zeichnet gleich selbst. */
|
||||||
|
newsDay = null; newsKeySet = null;
|
||||||
loadActiveIntoEditor();
|
loadActiveIntoEditor();
|
||||||
persistDocs();
|
persistDocs();
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-3
@@ -87,6 +87,63 @@
|
|||||||
.fsbtn svg{display:block}
|
.fsbtn svg{display:block}
|
||||||
.fsbtn:hover{border-color:var(--or);color:var(--or)}
|
.fsbtn:hover{border-color:var(--or);color:var(--or)}
|
||||||
.fsbtn.active{background:var(--or);color:#fff;border-color:var(--or)}
|
.fsbtn.active{background:var(--or);color:#fff;border-color:var(--or)}
|
||||||
|
/* Neuigkeiten (D58). Der Knopf steht permanent in der Kopfzeile; **bernstein**
|
||||||
|
heißt „da ist etwas, das du noch nicht gesehen hast" — dieselben Töne, die
|
||||||
|
der Strahlenkranz am Knoten trägt (D28), damit Knopf und Knoten dasselbe
|
||||||
|
sagen. Petrol (`.active`) heißt etwas anderes: Es wird gerade ein
|
||||||
|
bestimmter Tag im Diagramm vorgeführt. */
|
||||||
|
.newswrap{position:relative;display:inline-flex;flex:0 0 auto}
|
||||||
|
.newsbtn{width:auto;min-width:30px;padding:0 6px;gap:2px}
|
||||||
|
.newsbtn.unseen{color:#A16207;border-color:#FACC15;background:#FEFCE8}
|
||||||
|
.newsbtn.unseen:hover{color:#854D0E;border-color:#EAB308}
|
||||||
|
.newsbtn .freshcount[hidden]{display:none}
|
||||||
|
.newsmenu{
|
||||||
|
position:absolute;top:calc(100% + 6px);right:0;z-index:60;
|
||||||
|
width:min(420px,calc(100vw - 24px));max-height:min(70vh,520px);overflow:auto;
|
||||||
|
background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:10px;
|
||||||
|
padding:10px 12px;box-shadow:0 8px 24px rgba(36,52,71,.22);
|
||||||
|
text-transform:none;letter-spacing:0;font-family:'IBM Plex Sans',sans-serif;
|
||||||
|
text-align:left;color:var(--ink);
|
||||||
|
}
|
||||||
|
.newsmenu[hidden]{display:none}
|
||||||
|
.newshead{
|
||||||
|
display:flex;align-items:center;gap:8px;
|
||||||
|
font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);
|
||||||
|
}
|
||||||
|
.newshead .newsclose{
|
||||||
|
margin-left:auto;border:0;background:transparent;cursor:pointer;
|
||||||
|
color:var(--muted);font-size:1rem;line-height:1;padding:2px 4px;border-radius:6px;
|
||||||
|
}
|
||||||
|
.newshead .newsclose:hover{color:var(--ink);background:rgba(36,52,71,.07)}
|
||||||
|
/* „Seit deinem letzten Besuch" (D28) — die persönliche Hälfte, deshalb
|
||||||
|
bernstein abgesetzt und oben, vor der allgemeinen Chronik. */
|
||||||
|
.newssince{
|
||||||
|
display:flex;align-items:center;gap:8px;flex-wrap:wrap;
|
||||||
|
margin-top:8px;padding:8px 10px;border-radius:8px;
|
||||||
|
background:#FEFCE8;border:1px solid #FACC15;color:#854D0E;font-size:.82rem;
|
||||||
|
}
|
||||||
|
.newssince .newsseen{
|
||||||
|
margin-left:auto;border:1px solid rgba(133,77,14,.35);border-radius:7px;
|
||||||
|
background:transparent;color:inherit;cursor:pointer;
|
||||||
|
padding:3px 9px;font-family:inherit;font-size:.78rem;
|
||||||
|
}
|
||||||
|
.newssince .newsseen:hover{background:rgba(133,77,14,.1)}
|
||||||
|
.newsday{margin-top:10px;padding-top:9px;border-top:1px solid rgba(36,52,71,.12)}
|
||||||
|
.newsdate{font-weight:600;font-size:.84rem}
|
||||||
|
.newslines{margin:5px 0 0;padding-left:17px;font-size:.82rem;line-height:1.55;color:var(--ink)}
|
||||||
|
.newslines li{margin:1px 0}
|
||||||
|
.newslines code{
|
||||||
|
font-family:'IBM Plex Mono',monospace;font-size:.92em;
|
||||||
|
background:rgba(36,52,71,.07);border-radius:4px;padding:0 3px;
|
||||||
|
}
|
||||||
|
.newsshow{
|
||||||
|
margin-top:6px;border:1px solid rgba(36,52,71,.2);border-radius:7px;
|
||||||
|
background:var(--card);color:var(--muted);cursor:pointer;
|
||||||
|
padding:4px 9px;font-family:inherit;font-size:.78rem;
|
||||||
|
}
|
||||||
|
.newsshow:hover{border-color:var(--or);color:var(--or)}
|
||||||
|
.newsshow[aria-pressed="true"]{background:var(--or);color:#fff;border-color:var(--or)}
|
||||||
|
.newsempty{margin-top:8px;color:var(--muted);font-size:.82rem}
|
||||||
/* Vollbild: Panels nutzen fast die ganze Fensterbreite; es bleibt ein
|
/* Vollbild: Panels nutzen fast die ganze Fensterbreite; es bleibt ein
|
||||||
schmaler Rand (halber Panel-Zwischenraum = 7 px), damit es nicht gedrängt
|
schmaler Rand (halber Panel-Zwischenraum = 7 px), damit es nicht gedrängt
|
||||||
wirkt. */
|
wirkt. */
|
||||||
@@ -263,9 +320,8 @@
|
|||||||
Splitter, nur schmaler. Ohne offene Legende gibt es nichts zu teilen. */
|
Splitter, nur schmaler. Ohne offene Legende gibt es nichts zu teilen. */
|
||||||
/* Neuheiten-Knopf im Diagramm-Kopf (D28): Sternchen + Anzahl, nur sichtbar,
|
/* Neuheiten-Knopf im Diagramm-Kopf (D28): Sternchen + Anzahl, nur sichtbar,
|
||||||
wenn es etwas gibt. Gelb wie der Strahlenkranz am Knoten. */
|
wenn es etwas gibt. Gelb wie der Strahlenkranz am Knoten. */
|
||||||
.freshbtn{position:relative;color:#A16207;border-color:#FACC15;background:#FEFCE8}
|
/* Die Zahl der neuen Knoten sitzt seit D58 am Neuigkeiten-Knopf in der
|
||||||
.freshbtn:hover{color:#854D0E;border-color:#EAB308}
|
Kopfzeile (die bernsteinfarbene Fassung steht dort bei `.newsbtn.unseen`). */
|
||||||
.freshbtn[hidden]{display:none}
|
|
||||||
.freshcount{
|
.freshcount{
|
||||||
font-family:'IBM Plex Mono',monospace;font-size:.62rem;font-weight:600;
|
font-family:'IBM Plex Mono',monospace;font-size:.62rem;font-weight:600;
|
||||||
margin-left:3px;vertical-align:2px;
|
margin-left:3px;vertical-align:2px;
|
||||||
@@ -1306,6 +1362,12 @@
|
|||||||
statt die Zeile zu verbreitern); nach der Auswahl klappt sie wieder ein
|
statt die Zeile zu verbreitern); nach der Auswahl klappt sie wieder ein
|
||||||
(JS). Das „…“ entfällt hier — die aktive Sprache ist selbst der Auslöser. */
|
(JS). Das „…“ entfällt hier — die aktive Sprache ist selbst der Auslöser. */
|
||||||
body.mobile .header-tools{position:relative}
|
body.mobile .header-tools{position:relative}
|
||||||
|
/* Das Neuigkeiten-Popup ist breiter als sein Knopf. Am Knopf ausgerichtet
|
||||||
|
(`.newswrap{position:relative}`) begänne es auf dem Telefon bei −116 px,
|
||||||
|
also außerhalb des Bildes — nachgemessen bei 375 px. Hier zählt deshalb
|
||||||
|
die **Werkzeugleiste** als Bezug: Ihr rechter Rand ist auch der der Seite.
|
||||||
|
Kein `overflow` an der Kopfzeile als Ausweg — das klippt die Menüs (D50). */
|
||||||
|
body.mobile .newswrap{position:static}
|
||||||
body.mobile .langsel button[data-lang]{display:none}
|
body.mobile .langsel button[data-lang]{display:none}
|
||||||
body.mobile .langsel button[data-lang].active{display:inline-block}
|
body.mobile .langsel button[data-lang].active{display:inline-block}
|
||||||
body.mobile .langmore{display:none}
|
body.mobile .langmore{display:none}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { parseChangelog, changedKeys, attachKeys, parseLog }
|
||||||
|
from '../../scripts/news.mjs';
|
||||||
|
|
||||||
|
/* Neuigkeiten (D58). Geprüft ist die entscheidbare Hälfte — was aus dem
|
||||||
|
Changelog eine Notiz wird und welche Knoten sich an einem Tag bewegt haben.
|
||||||
|
Was git und Dateisystem liefern, steht in `collectNews()` und bleibt Sache
|
||||||
|
des Builds. */
|
||||||
|
|
||||||
|
const st = (...paare) => new Map(paare);
|
||||||
|
|
||||||
|
describe('parseChangelog — Notizen aus docs/CHANGELOG.md', () => {
|
||||||
|
const md = [
|
||||||
|
'# Changelog', '', 'Fließtext, der niemanden interessiert.', '- keine Notiz, kein Tag offen',
|
||||||
|
'', '## 2026-08-24', '', '- Erste Notiz', '* Zweite Notiz', 'kein Aufzählungszeichen',
|
||||||
|
'', '## 2026-08-23', '', '- Ältere Notiz', '',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
it('liest Tage und Notizen, neueste zuerst', () => {
|
||||||
|
expect(parseChangelog(md)).toEqual([
|
||||||
|
{date: '2026-08-24', lines: ['Erste Notiz', 'Zweite Notiz']},
|
||||||
|
{date: '2026-08-23', lines: ['Ältere Notiz']},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die Datei erklärt oben, was sie ist — das darf nicht als Notiz enden. */
|
||||||
|
it('überliest alles vor dem ersten Tag', () => {
|
||||||
|
expect(parseChangelog(md)[0].lines).not.toContain('keine Notiz, kein Tag offen');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beendet einen Tag an der nächsten Überschrift', () => {
|
||||||
|
const x = '## 2026-08-24\n- drin\n## Anhang\n- draußen\n';
|
||||||
|
expect(parseChangelog(x)).toEqual([{date: '2026-08-24', lines: ['drin']}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt einen Tag ohne Notizen weg', () => {
|
||||||
|
expect(parseChangelog('## 2026-08-24\n\n## 2026-08-23\n- da\n'))
|
||||||
|
.toEqual([{date: '2026-08-23', lines: ['da']}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('führt zwei Abschnitte desselben Tages zusammen', () => {
|
||||||
|
expect(parseChangelog('## 2026-08-24\n- a\n\n## 2026-08-24\n- b\n')[0].lines)
|
||||||
|
.toEqual(['a', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deckelt die Tage', () => {
|
||||||
|
const x = '## 2026-08-24\n- a\n## 2026-08-23\n- b\n';
|
||||||
|
expect(parseChangelog(x, {maxDays: 1}).map(e => e.date)).toEqual(['2026-08-24']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verträgt Leeres', () => {
|
||||||
|
expect(parseChangelog('')).toEqual([]);
|
||||||
|
expect(parseChangelog(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die ausgelieferte Datei selbst: Sie ist die Quelle des Popups — ist sie
|
||||||
|
unlesbar, steht das Popup leer da, ohne dass es jemand merkt. */
|
||||||
|
it('liest die echte docs/CHANGELOG.md', () => {
|
||||||
|
const echt = parseChangelog(readFileSync(new URL('../../docs/CHANGELOG.md', import.meta.url), 'utf8'));
|
||||||
|
expect(echt.length).toBeGreaterThan(3);
|
||||||
|
expect(echt[0].date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
expect(echt.every(e => e.lines.length > 0)).toBe(true);
|
||||||
|
/* Absteigend sortiert — das Popup zeigt sie in dieser Reihenfolge. */
|
||||||
|
expect([...echt].sort((a, b) => a.date < b.date ? 1 : -1)).toEqual(echt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changedKeys — was sich im Plan bewegt hat', () => {
|
||||||
|
it('meldet neue Knoten und geänderten Status', () => {
|
||||||
|
const prev = st(['> A', 'geplant'], ['> B', 'fertig']);
|
||||||
|
const curr = st(['> A', 'fertig'], ['> B', 'fertig'], ['> C', 'idee']);
|
||||||
|
expect(changedKeys(prev, curr).sort()).toEqual(['> A', '> C']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Wie beim Erstkontakt in D28: Ohne Vergleichsfassung leuchtet nichts. */
|
||||||
|
it('gibt ohne Vorfassung nichts zurück', () => {
|
||||||
|
expect(changedKeys(null, st(['> A', 'fertig']))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Ein entfernter Knoten ist im Diagramm nicht mehr da — es gäbe nichts
|
||||||
|
hervorzuheben. */
|
||||||
|
it('meldet entfernte Knoten nicht', () => {
|
||||||
|
expect(changedKeys(st(['> A', 'fertig']), st())).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('attachKeys — Tageseinträge und Plan-Fassungen zusammenführen', () => {
|
||||||
|
const versionen = [
|
||||||
|
{date: '2026-08-22', status: st(['> A', 'geplant'])},
|
||||||
|
{date: '2026-08-23', status: st(['> A', 'fertig'])},
|
||||||
|
{date: '2026-08-24', status: st(['> A', 'prod'])},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('hängt die Knoten an den Tag', () => {
|
||||||
|
const e = attachKeys([{date: '2026-08-24', lines: ['x']}], versionen);
|
||||||
|
expect(e[0]).toEqual({date: '2026-08-24', lines: ['x'], keys: ['> A']});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gibt einem Tag ohne Plan-Änderung eine leere Menge', () => {
|
||||||
|
const e = attachKeys([{date: '2026-08-25', lines: ['x']}], versionen);
|
||||||
|
expect(e[0].keys).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Der Deploy-Tag (D30) oder ein vergessener Changelog-Eintrag: Der Link
|
||||||
|
führt trotzdem vor, was sich bewegt hat. */
|
||||||
|
it('legt für einen reinen Plan-Tag einen eigenen Eintrag an', () => {
|
||||||
|
const e = attachKeys([{date: '2026-08-24', lines: ['x']}], versionen);
|
||||||
|
expect(e.map(x => x.date)).toEqual(['2026-08-24', '2026-08-23']);
|
||||||
|
expect(e[1]).toEqual({date: '2026-08-23', lines: [], keys: ['> A']});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nimmt die erste Fassung nicht als Änderung', () => {
|
||||||
|
const e = attachKeys([], [versionen[0]]);
|
||||||
|
expect(e).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deckelt auch nach dem Zusammenführen', () => {
|
||||||
|
const e = attachKeys([{date: '2026-08-24', lines: ['x']}], versionen, {maxDays: 1});
|
||||||
|
expect(e.map(x => x.date)).toEqual(['2026-08-24']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseLog — git-Ausgabe zerlegen', () => {
|
||||||
|
const REC = '\x1e', F = '\x1f';
|
||||||
|
|
||||||
|
it('liest Kennung, Datum und Betreff', () => {
|
||||||
|
const raw = `${REC}abc${F}2026-08-24${F}feat: X${REC}def${F}2026-08-23${F}fix: Y`;
|
||||||
|
expect(parseLog(raw)).toEqual([
|
||||||
|
{sha: 'abc', date: '2026-08-24', subject: 'feat: X', files: []},
|
||||||
|
{sha: 'def', date: '2026-08-23', subject: 'fix: Y', files: []},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Mit `--name-only` hängt git die Dateinamen hinter die Formatzeile — sie
|
||||||
|
gehören zu diesem Commit, weil der Satztrenner vorn steht. */
|
||||||
|
it('sammelt die Dateinamen aus --name-only', () => {
|
||||||
|
const raw = `${REC}abc${F}2026-08-24${F}rename\n\ndocs/examples/alt.werkbaum\n`;
|
||||||
|
expect(parseLog(raw)[0].files).toEqual(['docs/examples/alt.werkbaum']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verträgt leere Ausgabe', () => {
|
||||||
|
expect(parseLog('')).toEqual([]);
|
||||||
|
expect(parseLog(REC + '\n')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+38
-1
@@ -1,6 +1,8 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import { collectNews, CHANGELOG_FILE } from '../scripts/news.mjs';
|
||||||
|
|
||||||
// Bettet das Favicon (../docs/brand/favicon.svg, außerhalb des Roots) als
|
// Bettet das Favicon (../docs/brand/favicon.svg, außerhalb des Roots) als
|
||||||
// data:-URI direkt in <link rel="icon"> ein, damit die gebaute Datei wirklich
|
// data:-URI direkt in <link rel="icon"> ein, damit die gebaute Datei wirklich
|
||||||
@@ -20,6 +22,41 @@ function inlineFavicon() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Neuigkeiten (D58): ../docs/CHANGELOG.md (die Notizen) und die git-Historie
|
||||||
|
// (die Knoten je Tag) werden zur BAUZEIT eingelesen und als virtuelles Modul
|
||||||
|
// eingebettet — zur Laufzeit gibt es kein git und keinen Server (D11/D19).
|
||||||
|
// Die Regeln stehen in ../scripts/news.mjs (getestet), hier nur die
|
||||||
|
// Beschaffung. Scheitert git (kein Repo, flacher Klon, Tarball), bleibt die
|
||||||
|
// Liste leer statt den Build zu zerreißen: Das Popup sagt dann, dass es nichts
|
||||||
|
// zu zeigen gibt. Der Dev-Server liest einmal beim Start — neue Einträge
|
||||||
|
// erscheinen nach einem Neustart.
|
||||||
|
const NEWS_ID = 'virtual:werkbaum-news';
|
||||||
|
function newsData() {
|
||||||
|
const RES = '\0' + NEWS_ID;
|
||||||
|
return {
|
||||||
|
name: 'werkbaum-news',
|
||||||
|
resolveId: id => (id === NEWS_ID ? RES : null),
|
||||||
|
async load(id) {
|
||||||
|
if (id !== RES) return null;
|
||||||
|
let news = [];
|
||||||
|
try {
|
||||||
|
const cwd = new URL('..', import.meta.url);
|
||||||
|
const { parse } = await import('./src/parser.js');
|
||||||
|
const { statusByKey } = await import('./src/model.js');
|
||||||
|
news = collectNews({
|
||||||
|
run: args => execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64e6 }),
|
||||||
|
changelog: readFileSync(new URL('../' + CHANGELOG_FILE, import.meta.url), 'utf8'),
|
||||||
|
parsePlan: text => parse(text).roots,
|
||||||
|
statusByKey,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.warn(`Neuigkeiten nicht lesbar (${e.message}) — Popup bleibt leer.`);
|
||||||
|
}
|
||||||
|
return `export default ${JSON.stringify(news)};`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Werkbaum bleibt bewusst eine einzelne, self-contained Datei (file://-tauglich,
|
// Werkbaum bleibt bewusst eine einzelne, self-contained Datei (file://-tauglich,
|
||||||
// D16). Vite dient nur als Bündler/Testrunner (D19): `vite build` inlint alle
|
// D16). Vite dient nur als Bündler/Testrunner (D19): `vite build` inlint alle
|
||||||
// Module + CSS in dist/index.html; im Dev-Server (`vite`) werden sie einzeln
|
// Module + CSS in dist/index.html; im Dev-Server (`vite`) werden sie einzeln
|
||||||
@@ -27,7 +64,7 @@ function inlineFavicon() {
|
|||||||
// ../docs/brand/ ausliefern (liegt außerhalb des Projekt-Roots frontend/).
|
// ../docs/brand/ ausliefern (liegt außerhalb des Projekt-Roots frontend/).
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
root: '.',
|
root: '.',
|
||||||
plugins: [inlineFavicon(), viteSingleFile()],
|
plugins: [inlineFavicon(), newsData(), viteSingleFile()],
|
||||||
server: { port: 8137, strictPort: true, fs: { allow: ['..'] } },
|
server: { port: 8137, strictPort: true, fs: { allow: ['..'] } },
|
||||||
build: {
|
build: {
|
||||||
// Alles inlinen -> keine externen Assets, eine Datei.
|
// Alles inlinen -> keine externen Assets, eine Datei.
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/* Neuigkeiten für das Popup (D58) — aus zwei Quellen, jede in ihrer Rolle:
|
||||||
|
*
|
||||||
|
* docs/CHANGELOG.md → WAS geschehen ist, als englischer Satz je Änderung.
|
||||||
|
* git-Historie → WELCHE Knoten des mitgelieferten Plans sich an
|
||||||
|
* diesem Tag bewegt haben (für die Vorführung im
|
||||||
|
* Diagramm, „Was ist neu?"-Ansicht, D28).
|
||||||
|
*
|
||||||
|
* Warum die Notizen nicht aus den Commit-Betreffs kommen, obwohl sie dort
|
||||||
|
* stünden: Die Betreffs sind **deutsch** (CLAUDE.md: Doku auf Deutsch), das
|
||||||
|
* Popup aber ist Produkt-Oberfläche in neun Sprachen. Der Changelog ist
|
||||||
|
* englisch wie der mitgelieferte Plan und `llms.md` — ausgelieferte Artefakte
|
||||||
|
* mit weltweitem Publikum (D22, D43). Preis: eine Datei, die beim Bauen eines
|
||||||
|
* Features mitgeschrieben werden muss.
|
||||||
|
*
|
||||||
|
* Zwei Hälften, nach der Hausregel getrennt (frontend/CLAUDE.md): Was
|
||||||
|
* ENTSCHEIDBAR ist, steht hier als reine Funktion und ist getestet
|
||||||
|
* (frontend/tests/news.test.js); was git und Dateisystem liefern, steht in
|
||||||
|
* `collectNews()` ganz unten und läuft nur zur Bauzeit (Vite-Plugin).
|
||||||
|
*
|
||||||
|
* Das Ergebnis ist ein Array, neueste zuerst:
|
||||||
|
* [{date: '2026-08-24', lines: ['…', …], keys: ['… > Notation', …]}]
|
||||||
|
* `keys` sind Label-Pfade — dieselbe Identität wie „Was ist neu?" (D28).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PLAN_FILE = 'docs/examples/werkbaum.werkbaum';
|
||||||
|
export const CHANGELOG_FILE = 'docs/CHANGELOG.md';
|
||||||
|
export const MAX_DAYS = 20; /* so weit reicht das Popup zurück */
|
||||||
|
|
||||||
|
const RE_DAY = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
|
||||||
|
const RE_ITEM = /^[-*]\s+(.*\S)\s*$/;
|
||||||
|
|
||||||
|
/* `docs/CHANGELOG.md` → Tageseinträge, neueste zuerst. Erkannt wird genau
|
||||||
|
zweierlei: eine Überschrift `## JJJJ-MM-TT` eröffnet einen Tag, eine
|
||||||
|
Aufzählungszeile darunter ist eine Notiz. Alles andere ist Fließtext für
|
||||||
|
Menschen und wird überlesen — so kann die Datei oben erklären, was sie ist,
|
||||||
|
ohne dass daraus Notizen werden. Kein Deckel je Tag: Die Datei ist gepflegt,
|
||||||
|
ihr Autor entscheidet, wie viel ein Tag hergibt. */
|
||||||
|
export function parseChangelog(md, opts){
|
||||||
|
const {maxDays = MAX_DAYS} = opts || {};
|
||||||
|
const byDay = new Map();
|
||||||
|
let day = null;
|
||||||
|
for(const raw of String(md || '').split('\n')){
|
||||||
|
const kopf = raw.match(RE_DAY);
|
||||||
|
if(kopf){ day = kopf[1]; if(!byDay.has(day)) byDay.set(day, []); continue; }
|
||||||
|
if(/^#{1,6}\s/.test(raw)){ day = null; continue; } /* andere Überschrift beendet den Tag */
|
||||||
|
if(!day) continue;
|
||||||
|
const item = raw.match(RE_ITEM);
|
||||||
|
if(item) byDay.get(day).push(item[1]);
|
||||||
|
}
|
||||||
|
return [...byDay.entries()]
|
||||||
|
.filter(([, lines]) => lines.length)
|
||||||
|
.sort((a, b) => a[0] < b[0] ? 1 : -1)
|
||||||
|
.slice(0, maxDays)
|
||||||
|
.map(([date, lines]) => ({date, lines}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Knoten, die sich zwischen zwei Fassungen des Plans bewegt haben: neu
|
||||||
|
hinzugekommen oder mit anderem Status. `prev == null` (die erste bekannte
|
||||||
|
Fassung) ergibt die leere Menge — dieselbe Zurückhaltung wie beim
|
||||||
|
Erstkontakt in D28, sonst leuchtete der halbe Plan als „Änderung von damals". */
|
||||||
|
export function changedKeys(prev, curr){
|
||||||
|
if(!prev) return [];
|
||||||
|
const out = [];
|
||||||
|
for(const [key, st] of curr){
|
||||||
|
if(!prev.has(key) || prev.get(key) !== st) out.push(key);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tageseinträge + Fassungen des Plans (chronologisch, je `{date, status}` mit
|
||||||
|
`status` = Map Schlüssel → Status-Key) → dieselben Einträge mit `keys`.
|
||||||
|
Ein Tag, an dem sich nur der Plan bewegt hat, bekommt einen **eigenen**
|
||||||
|
Eintrag ohne Zeilen — typischerweise der Deploy-Tag (D30) oder einer, an dem
|
||||||
|
der Changelog vergessen wurde. Der Link führt dort trotzdem vor, was sich
|
||||||
|
bewegt hat; ohne diese Vereinigung fiele der Tag stumm unter den Tisch. */
|
||||||
|
export function attachKeys(entries, versions, opts){
|
||||||
|
const {maxDays = MAX_DAYS} = opts || {};
|
||||||
|
const keysByDay = new Map();
|
||||||
|
for(let i = 0; i < versions.length; i++){
|
||||||
|
const k = changedKeys(i ? versions[i - 1].status : null, versions[i].status);
|
||||||
|
if(k.length) keysByDay.set(versions[i].date, k);
|
||||||
|
}
|
||||||
|
const out = entries.map(e => ({...e, keys: keysByDay.get(e.date) || []}));
|
||||||
|
const haben = new Set(out.map(e => e.date));
|
||||||
|
for(const [date, keys] of keysByDay){
|
||||||
|
if(!haben.has(date)) out.push({date, lines: [], keys});
|
||||||
|
}
|
||||||
|
return out.sort((a, b) => a.date < b.date ? 1 : -1).slice(0, maxDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- ab hier git und Dateisystem (nur zur Bauzeit) ---------- */
|
||||||
|
|
||||||
|
const SEP_FIELD = '\x1f', SEP_REC = '\x1e';
|
||||||
|
/* Der Satztrenner steht **vorn**, nicht hinten: Mit `--name-only` hängt git die
|
||||||
|
Dateinamen hinter die Formatzeile, und nur so gehören sie beim Zerlegen zum
|
||||||
|
richtigen Commit. */
|
||||||
|
export const LOG_FORMAT = `--format=${SEP_REC}%H${SEP_FIELD}%cd${SEP_FIELD}%s`;
|
||||||
|
|
||||||
|
export function parseLog(raw){
|
||||||
|
return String(raw).split(SEP_REC).filter(r => r.trim()).map(r => {
|
||||||
|
const [kopf, ...rest] = r.split('\n');
|
||||||
|
const [sha, date, subject] = kopf.split(SEP_FIELD);
|
||||||
|
return {sha, date, subject: subject || '', files: rest.filter(l => l.trim())};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* `run`, `changelog` und `parsePlan` werden hereingereicht, damit diese
|
||||||
|
Funktion ohne child_process, ohne Dateisystem und ohne den Parser prüfbar
|
||||||
|
bleibt. */
|
||||||
|
export function collectNews({run, changelog, parsePlan, statusByKey, planFile = PLAN_FILE, opts} = {}){
|
||||||
|
const entries = parseChangelog(changelog, opts);
|
||||||
|
|
||||||
|
/* Je Tag, an dem der Plan angefasst wurde, zählt sein LETZTER Stand.
|
||||||
|
`--follow` reicht über die Umbenennung hinweg (`example-werkbaum.werkbaum`
|
||||||
|
→ `werkbaum.werkbaum`, 16.08.) — ohne das begänne die Geschichte des Plans
|
||||||
|
dort, und der erste Tag danach hätte keine Vergleichsfassung. Deshalb
|
||||||
|
`--name-only`: Vor der Umbenennung heißt die Datei anders, und `git show`
|
||||||
|
braucht den Namen, den sie **in diesem Commit** trug. */
|
||||||
|
const planLog = parseLog(run(
|
||||||
|
['log', '--follow', '--name-only', '--date=short', LOG_FORMAT, '--', planFile]));
|
||||||
|
const lastOfDay = new Map(); /* neueste zuerst ⇒ der erste gewinnt */
|
||||||
|
for(const c of planLog) if(!lastOfDay.has(c.date)) lastOfDay.set(c.date, c);
|
||||||
|
const versions = [...lastOfDay.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)
|
||||||
|
.map(([date, c]) => ({
|
||||||
|
date,
|
||||||
|
status: statusByKey(parsePlan(run(['show', `${c.sha}:${c.files[0] || planFile}`]))),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return attachKeys(entries, versions, opts);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user