feat: Dokumenten-Menü neu — Brotkrume im App-Kopf, Stand-Knöpfe im Editor-Kopf (D81)

Der Dokument-Wähler steht jetzt als gerahmter Chip "Werkbaum › Name" im
App-Kopf und ist damit auch auf dem Telefon aus beiden Bereichen erreichbar;
sein Name übernimmt dort die Untertitel-Zeile. Das Menü gruppiert nach
Dokumentart (docKind, headless getestet), trägt Umbenennen/Löschen/
Wiederherstellen als Symbole je Zeile und scrollt als Ganzes, statt auf
kleinen Bildschirmen abgeschnitten zu werden. Die Editor-Titelzeile heißt
wieder "Text-Editor" und trägt die Stand-Funktionen des aktiven Dokuments:
Speichern (Strg+S), Stand sichern, frühere Stände, Neu laden (Original/URL/
Datei) und den neuen kurzen Teilen-Knopf (vorher "Auf den Server legen" im
Menü). subtitleShort ist ersatzlos ausgebaut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-26 22:21:11 +02:00
co-authored by Claude Fable 5
parent fdca934cf0
commit 4be6ecb984
12 changed files with 620 additions and 236 deletions
+2 -2
View File
@@ -97,8 +97,8 @@ selbst** und sehen die Änderungen der anderen, ohne neu zu laden:
https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/<uuid>
```
Angelegt wird so ein Dokument über den Eintrag **„Auf den Server legen"** im
Dokumenten-Menü: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt
Angelegt wird so ein Dokument über den **Teilen-Knopf** in der Titelzeile des
Text-Editors: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt
den Link in die Adresszeile und in die Zwischenablage. Diesen Link weitergeben —
die Adresse ist die Einladung, und wer sie kennt, kommt hinein.
+2 -2
View File
@@ -92,8 +92,8 @@ itself** and sees the others' changes without reloading:
https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/<uuid>
```
To create such a document, open the document menu and pick **"Put on the
server"**: it uploads the active plan, switches to it, and puts the link in the
To create such a document, press the **Share** button in the text editor's
title bar: it uploads the active plan, switches to it, and puts the link in the
address bar and on your clipboard. Share that link — the address is the
invitation, and knowing it is what grants access.
+3
View File
@@ -19,6 +19,9 @@ reverse.
## 2026-08-26
- The document picker moved into the app header: `Werkbaum name` opens the menu — on the phone it is reachable from both panes, and the name takes over the subtitle line
- The document menu groups by kind — included, yours, sources — carries rename, delete and restore on each row, and scrolls instead of being cut off on small screens
- The editor title bar is labelled `Text editor` again and holds the document's own buttons: save, snapshot, history, reload from the source, and a short `Share` button that puts the plan on a server
- The address bar follows the document you switch to: `?live=` and `?sourceUrl=` name what is in front of you, so a reload brings back the same plan
- Switching to a server document in the picker now really opens it live — before it only showed its last state
- Fix: a foreign change to a server document could land in the text of a local document you had switched to
+105
View File
@@ -6901,3 +6901,108 @@ getippt und umgeschaltet, kommt beim Server an. 514 Tests, davon 13 neue in
`tests/docurl.test.js`; Gegenproben: fremde Parameter mitwerfen → genau die
zwei danach benannten Zusicherungen fallen, `encodeURIComponent` statt der
sparsamen Maskierung → genau die sieben, die die Lesbarkeit festhalten.
## D81 — Dokumenten-Menü neu: Brotkrume im App-Kopf, Stand-Knöpfe in der Editor-Titelzeile
Das Dokumenten-Menü war gewachsen, ohne je gestaltet zu sein: Der Dokumentname
in der Editor-Titelzeile war Auslöser eines Dropdowns, sah aber nicht nach
Menü aus, und das Menü mischte Dokumentwahl, Dateifunktionen und Verwaltung in
einem Knopf-Wust am Fuß. Auf dem Telefon (Brave) war es zudem **unten
abgeschnitten** — sieben Aktions-Knöpfe unter einer Liste, verankert in einer
Titelzeile in der unteren Bildschirmhälfte. Einer Design-Runde mit vier
Mockup-Richtungen (Knopf & Menü am Ort, Schublade, Dokument im App-Kopf,
Palette) folgte die Entscheidung des Nutzers für **C — Dokument im App-Kopf**.
**Der Name gehört über beide Bereiche, nicht in eine Panel-Zeile.** Das
Dokument bestimmt Text UND Diagramm; im App-Kopf steht es als Brotkrume
**„Werkbaum Name"** — ein gerahmter Chip mit Pfeil, der endlich nach Menü
aussieht und lange URL-Namen mit Ellipse kürzt (voller Name im Tooltip, wie
gehabt). Nebengewinn auf dem Telefon: Der Kopf ist in beiden Bereichen
sichtbar, die Dokumentwahl ist also auch bei Diagramm-vorn erreichbar — vorher
musste man erst in den Textbereich wechseln (D17). Der Name übernimmt dort die
Zeile des Untertitels; es kommt keine Fläche hinzu (die harte Randbedingung
der Design-Runde: größerer App-Header und ständige Werkzeugleiste waren in
einer früheren Runde genau daran gescheitert).
**Die Editor-Titelzeile bekommt ihre Beschriftung zurück — „Text-Editor"**
(Nutzer-Vorgabe; vorher „Struktur (Text)", zuletzt ganz vom Wähler verdrängt,
D22) — **und trägt die Stand-Funktionen des aktiven Dokuments** als Knöpfe:
- **Speichern** (Als Datei speichern, Strg+S — der Blitz samt Haken von D74
sitzt jetzt hier statt am Dokumentnamen: der Knopf ist die Geste);
- **Stand jetzt sichern** und **Frühere Stände** (unverändert, D54);
- **Neu laden** — kontextabhängig: bei mitgelieferten Dokumenten „Original
wiederherstellen" (ausgegraut, solange unverändert), bei URL-Dokumenten
frisch holen (die URL ist die Quelle der Wahrheit, D23), bei Dateien mit
gemerktem Handle neu aus der Datei lesen (D72); sonst verborgen. Die
Abweichungs-Prüfung hängt am **input-Ereignis**, nicht nur am
Dokumentwechsel — beim Bauen gefunden: Nach dem ersten Tippen blieb der
Knopf ausgegraut, weil die Abweichung beim Tippen entsteht;
- **Teilen** — der neue, kurze Name für „Auf den Server legen"
(D76-Nachtrag 8, Nutzer: „prägnanter und kurzer"): legt das Dokument auf
einen Werkbaum-Server und startet die gemeinsame Bearbeitung; entfällt, wo
es schon liegt (id `live:…`). Aus dem Menü heraus, in die Titelzeile hinein
— es ist eine Funktion des aktiven Dokuments, keine Dokumentverwaltung.
**Das Menü selbst: Gruppen nach Dokumentart, Aktionen an der Zeile.** Drei
Gruppen — **Mitgeliefert · Eigene · Quellen** (Server- und URL-Dokumente);
die Art steckt in der id und ist als `docKind()` headless in docurl.js
(Hausregel D54-Nachtrag 3, getestet). Umbenennen, Löschen und — bei
abweichenden mitgelieferten — Wiederherstellen hängen als **Symbole an der
Zeile des jeweiligen Dokuments**, immer sichtbar (Touch kennt kein Hover; die
Hover-only-Variante des Mockups hätte dort versagt). Damit wirken sie auf
**jedes** Dokument, nicht mehr nur auf das aktive, und das Menü bleibt nach
einer Verwaltungs-Aktion **offen** — wer aufräumt, räumt meist weiter. Kein
Knopf im Knopf: Die Zeile ist ein `div`, Wählen-Knopf und Aktions-Knöpfe sind
Geschwister (verschachtelte interaktive Elemente sind ungültiges HTML). Unten
bleiben zwei Befehle als gewöhnliche Menüzeilen: „+ Neues Dokument" und
„Datei öffnen…".
**Gegen das Abschneiden auf dem Telefon: scrollen, nicht klappen.** Das Menü
bekommt `max-height` und scrollt **als Ganzes**; die vom Nutzer alternativ
vorgeschlagenen auf-/zuklappbaren Gruppen sind verworfen — sie brauchten
einen gemerkten Klapp-Zustand und versteckten Dokumente hinter einem zweiten
Klick, während das Scrollen zustandslos ist und nichts versteckt. Dazu kommt,
dass das Menü jetzt **oben** hängt statt in der Mitte des Bildschirms — der
Platz darunter ist das Mehrfache des alten.
**Drei Messbefunde beim Bauen, alle in derselben Sitzung behoben:**
- **Das Menü spannte auf Mobil nur 155 px** statt der vollen Breite: Bezug
war die Marken-Gruppe, und die ist nur so breit, wie die Werkzeuge rechts
ihr lassen. Dieselbe Verlegung wie beim Neuigkeiten-Popup (D58): Bezug ist
die ganze Kopfzeile; die Marken-Gruppe löst sich auf Mobil per
`display:contents` auf, damit die Chip-Zeile per `order` unter Marke und
Werkzeuge umbricht — gemessen danach 335 px, `elementFromPoint` trifft
(die D50-Prüfung).
- **Marke und Werkzeuge brachen in zwei Zeilen** (Kopf: drei Zeilen = neue
Fläche): 164 + 164 px bei 335 px Breite. Wie in D17-Nachtrag 5 weichen die
**Lücken, nicht die Knöpfe** — nachgemessen mit eingeblendetem
Neuigkeiten-Zähler UND Build-Hinweis (die D17-Lektion, mit dem versteckten
Element zu messen): Worst Case 325 von 335 px, eine Zeile, Kopf 63 px.
Bei 320 px bricht es ehrlich um (Marke und Werkzeuge passen physisch nicht
nebeneinander) — dieselbe Philosophie wie die 440-px-Schwelle (D50).
- **Der Neu-laden-Knopf blieb nach dem Tippen ausgegraut** (siehe oben).
**Was mitgeht:** Der Collapsed-Editor zeigt jetzt „TEXT-EDITOR" (senkrecht im
Seitenmodus) statt des Dokumentnamens; der Klick-Wächter am alten Auslöser
(minimierter Editor) entfällt — im App-Kopf gibt es nichts wiederherzustellen.
Ein offenes Menü wechselt die Sprache mit (wie das Neuigkeiten-Popup). Der
Untertitel-Kurztext (`subtitleShort`, 9 Sprachen) ist ersatzlos ausgebaut —
seine Zeile gehört jetzt dem Dokumentnamen.
**Werkzeuggrenzen, wieder dieselbe Sorte wie D25/D17-Nachtrag 4:** Nach einem
programmatischen Resize lieferte das Browser-Pane Miniatur-Screenshots einer
Seite, die sich selbst nachweislich korrekt maß (1280 × 860), und die
`matchMedia`-Umschaltung von `body.mobile` feuerte nicht — beides
Umgebungs-Artefakte, kein Befund; entschieden haben die Messwerte im
Dokument, nicht die Bilder.
**Nachgemessen** (518 Tests, davon 4 neue für `docKind`; Browser Desktop und
375/320 px): Chip im Kopf öffnet das gruppierte Menü, `elementFromPoint`
trifft es auf allen Breiten; Neues Dokument geht in die Inline-Benennung mit
markiertem Vorschlag und landet unter „Eigene"; ein bearbeitetes Beispiel
zeigt das Zeilen-Restore-Symbol und aktiviert Neu laden, Wiederherstellen
stellt Text und Knopfzustände zurück, ohne das Menü zu schließen; Löschen
eines nicht-aktiven Dokuments lässt den Editor unangetastet; auf dem Telefon
ist der Wähler aus dem Diagramm-Bereich erreichbar und der Wechsel rendert
183 Knoten, langer Name füllt die 335-px-Chipzeile mit Ellipse.
+6 -4
View File
@@ -561,10 +561,12 @@ Ab schmaler Breite (≤ 640 px, z. B. Smartphone) ist **immer genau ein Bereich*
zu sehen — Diagramm **oder** Text, jeweils über die ganze Fläche. Oben links in
der Titelzeile schaltet ein **Knopf** auf den anderen um; er zeigt das Ziel
(Textzeilen bzw. Baum), denn den Zustand hat man ohnehin vor sich. Es gibt hier
weder Splitter noch Min/Normal/Max-Buttons — es ist nichts zu teilen. Der
Bereichstitel „DIAGRAMM" entfällt; die Zeile wird für Umschalter und Aktionen
gebraucht. Der **Dokumenten-Wähler** steht direkt neben dem Umschalter und ist
damit genau dann zu sehen, wenn der Text vorn ist. Die übrigen
weder Splitter noch Min/Normal/Max-Buttons — es ist nichts zu teilen. Die
Bereichstitel („DIAGRAMM", „TEXT-EDITOR") entfallen; die Zeile wird für
Umschalter und Aktionen gebraucht. Der **Dokumenten-Wähler** steht als
Brotkrume im App-Kopf („Werkbaum Name", D81) und ist damit aus **beiden**
Bereichen erreichbar; sein Name übernimmt hier die Zeile des Untertitels —
es kommt keine Fläche hinzu. Die übrigen
**Aktions-Knöpfe** bleiben rechts in der Titelzeile und werden mit
umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen
komprimierten Form. Der sichtbare Bereich wird gemerkt; ein Sprung zwischen
+10 -8
View File
@@ -71,7 +71,7 @@
- [^] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS) %% no pointer, no tooltip
- [^] #ed.tipwindow: The same window at the pointer and on keyboard focus (XS) %% a title has no paragraphs and never shows on focus
- [^] #ed.docs: Several documents in one editor (M)
- [^] #ed.docs.picker: Switcher in the editor title bar (S)
- [x] #ed.docs.picker: Breadcrumb picker in the app header (S) %% Werkbaum name, see D81
- [^] #ed.docs.url: Load a document from ?sourceUrl= (S)
- [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
- [^] #ed.jump: Jump between diagram and text (S)
@@ -151,7 +151,7 @@
| [?] #col.live.crdt: Text CRDT — the plan is plain text, so it fits (L)
| [?] #col.live.ot: Operational transformation (XL)
- [^] #col.live.ask: On a real overlap, ask whose version wins (S)
- [^] #col.live.upload: Put a local plan on the server, from the menu (S)
- [x] #col.live.upload: Share: put a local plan on the server (S) %% a button in the editor title bar, see D81
- [?] #col.live.cursors: Cursors and who else is here (S)
- [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough
- [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent
@@ -531,8 +531,10 @@
without changing anything.
#ed.docs.picker
The name of the active document replaces the panel title and opens the menu
for switching, creating, renaming and deleting.
The app header reads "Werkbaum > name": the chip opens the menu, grouped by
kind (shipped, yours, sources), with rename, delete and restore on each row.
It scrolls as a whole instead of being cut off on small screens, and on the
phone it is reachable from both panes.
#ed.docs.url
A link can carry the address of a text file, which is then fetched on every
@@ -901,10 +903,10 @@
for the same result.
#col.live.upload
Without it the only way onto the server was a curl command. The local plan
stays where it is: whoever entrusts their only copy to a server should not
lose it in the act. The picker names the host beside a server document, so
the two entries are told apart.
Without it the only way onto the server was a curl command. A share button
in the editor title bar; the local plan stays where it is: whoever entrusts
their only copy to a server should not lose it in the act. The picker names
the host beside a server document, so the two entries are told apart.
#col.live.cursors
Seeing where the others are. A by-product of the merge layer rather than a
+18 -6
View File
@@ -120,7 +120,7 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
**Schattenkopie**, `scheduleLivePush()` schickt nach 0,6 s Ruhe das Diff
(D79 — die Wartezeit IST die gefuehlte Verzoegerung),
`runFeed()` hält einen Abruf offen (nur im sichtbaren Tab), `putOnServer()`
legt das aktive Dokument an („Auf den Server legen"). Die Basis-Adresse
legt das aktive Dokument an („Teilen" in der Editor-Titelzeile, D81). Die Basis-Adresse
bestimmt `serverBase()` (live.js): `?server=` vor dem offenen Dokument vor
der eigenen Herkunft — die Vorgabe stimmt damit ohne Konfiguration, weil das
Backend produktiv hinter derselben Domain liegt. Zwei Fallen, beide
@@ -153,9 +153,20 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
fehlendem `werkbaum-docs` den bestehenden `werkbaum-src` (oder `INITIAL`) in
**ein** Dokument; `initDocs()` (Aufruf **nach** `applyLang`) holt den aktiven
Text in den Editor. `saveSrc()` schreibt den Editortext ins aktive Dokument.
Der Wähler ist ein Dropdown in der Editor-Titelzeile (`#docTrigger`/`#docMenu`,
ersetzt die frühere feste „Struktur (Text)"-Beschriftung); Wechseln/Neu/
Umbenennen/Löschen in `switchDoc/newDoc/renameDoc/deleteDoc`. Jedes Dokument ist
Der Wähler ist eine **Brotkrume im App-Kopf** (`#docTrigger`/`#docMenu`,
„Werkbaum Name", D81); das Menü gruppiert nach Dokumentart (`docKind()` in
docurl.js, headless getestet) und trägt Umbenennen/Löschen/Wiederherstellen
als Symbole **je Zeile**`renameDoc/deleteDoc/restoreDoc` nehmen deshalb
eine **id**, nicht das aktive Dokument, und Verwaltungs-Aktionen lassen das
Menü offen. Die Editor-Titelzeile heißt wieder „Text-Editor" und trägt die
Stand-Knöpfe (`saveBtn`/`snapAddBtn`/`snapBtn`/`reloadBtn`/`shareBtn` in der
`.standgroup`); ihren Zustand setzt `updateDocButtons()` — aufgerufen aus
`updateDocName()` UND am `input`-Ereignis, denn die Abweichung vom
Auslieferungsstand (Neu-laden-Knopf) entsteht beim Tippen. Auf Mobil löst
sich die Marken-Gruppe per `display:contents` auf (Menü-Bezug ist die ganze
Kopfzeile, wie beim Neuigkeiten-Popup) — wer dort Elemente ergänzt, misst
die eine Marken-Zeile mit Neuigkeiten-Zähler UND Build-Badge nach
(Worst Case 325 von 335 px). Jedes Dokument ist
nur Text + Name (kein Strukturformat, D14) — vorwärtskompatibel zum Backend
(D13). Ansichts-State (`werkbaum-ui`) bleibt global über alle Dokumente. Ein
leerer Editortext bleibt leer.
@@ -577,8 +588,9 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
`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):**
`#docMenu` und `.dlmenu` hängen als absolut positionierte Kinder im
`.panel-head`. Ein `overflow` dort macht ihn zum Scroll-Container und klippt
`.dlmenu` und das Snapshot-Menü hängen als absolut positionierte Kinder im
`.panel-head` (`#docMenu` seit D81 im App-Kopf — dort gilt dasselbe). Ein
`overflow` dort macht ihn zum Scroll-Container und klippt
sie; `overflow-x` hebt dabei das `visible` der **anderen** Achse auf `auto`,
geklippt wird also nach unten — genau dorthin, wo die Menüs aufklappen. Ein
Scroll-Container klippt unabhängig davon, ob er überläuft, der Fehler trifft
+59 -30
View File
@@ -18,7 +18,13 @@
<header>
<div class="header-row">
<h1 style="display:flex;align-items:center;gap:11px">
<!-- Marke und aktives Dokument als Brotkrume (D81): „Werkbaum name".
Das Dokument bestimmt Text UND Diagramm — sein Name gehört über beide
Bereiche, nicht in eine Panel-Zeile. Der Chip ist zugleich der
Auslöser des Dokumenten-Menüs; die Editor-Titelzeile wird damit frei
für die Stand-Funktionen und bekommt ihre Beschriftung zurück. -->
<div class="brandgroup">
<h1>
<svg width="34" height="34" viewBox="0 0 64 64" aria-hidden="true">
<rect x="22" y="5" width="20" height="12" rx="3.5" fill="#243447"/>
<path d="M32 17 V22 M14 22 H50 M14 22 V27 M50 22 V27" fill="none" stroke="#243447" stroke-width="3"/>
@@ -29,6 +35,25 @@
</svg>
<span data-i18n-title="brandTooltip" title="„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).">Werk<span style="color:var(--or)">baum</span></span>
</h1>
<span class="crumb-sep" aria-hidden="true"></span>
<span class="docwrap">
<span class="doctrigger" id="docTrigger" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false" data-i18n-title="docSwitchTooltip" data-i18n-aria="docSwitchTooltip" title="Dokument wählen oder verwalten" aria-label="Dokument wählen oder verwalten">
<span class="docname" id="docName"></span>
<svg class="doc-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</span>
<!-- Ein Menü, nach Dokumentart gruppiert; Umbenennen/Löschen/
Wiederherstellen hängen als Symbole an der Zeile des jeweiligen
Dokuments (immer sichtbar — Touch kennt kein Hover). Scrollt als
Ganzes, statt auf kleinen Bildschirmen abgeschnitten zu werden. -->
<div class="docmenu" id="docMenu" role="menu" data-i18n-aria="docMenuAria" aria-label="Dokumente" hidden>
<div class="docmenu-list" id="docList"></div>
<div class="docmenu-cmds">
<button type="button" class="doccmd" id="docNew"><span class="doccmd-glyph" aria-hidden="true"></span><span data-i18n="docNew">Neues Dokument</span></button>
<button type="button" class="doccmd" id="docOpenFile"><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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg><span data-i18n="docOpenFile">Datei öffnen…</span></button>
</div>
</div>
</span>
</div>
<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,
@@ -58,43 +83,29 @@
</div>
</div>
</div>
<p><span class="sub-full" data-i18n="subtitle">Werkbaum Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree/Requirements-Baum Diagramm-Editor.</span><span class="sub-short" data-i18n="subtitleShort">WBS, Feature- &amp; Requirements-Bäume als Text</span></p>
<!-- Auf dem Telefon entfällt der Untertitel — der Dokumentname übernimmt die Zeile (D81). -->
<p class="sub-full" data-i18n="subtitle">Werkbaum Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree/Requirements-Baum Diagramm-Editor.</p>
</header>
<div class="app" id="app">
<div class="panel editor">
<div class="panel-head">
<!-- Gegenstück zum Knopf im Diagramm-Kopf (D17-Nachtrag). Der
Dokumenten-Wähler steht direkt daneben und ist damit genau dann zu
sehen, wenn der Text vorn ist — er gehört zu diesem Bereich. -->
<!-- Gegenstück zum Knopf im Diagramm-Kopf (D17-Nachtrag). -->
<button type="button" class="copybtn panetoggle" id="paneToDiagram" data-i18n-title="paneToDiagram" data-i18n-aria="paneToDiagram" title="Zum Diagramm wechseln" aria-label="Zum Diagramm wechseln">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="4" rx="1"/><rect x="2.5" y="17" width="6" height="4" rx="1"/><rect x="15.5" y="17" width="6" height="4" rx="1"/><path d="M12 7V13M5.5 13H18.5M5.5 13V17M18.5 13V17"/></svg>
</button>
<!-- Dokument-Wähler und Snapshot-Knopf stehen zusammen; die Gruppe trägt
das `margin-right:auto` und ist der Bezug für das Snapshot-Menü. -->
<span class="docgroup">
<span class="doctrigger" id="docTrigger" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false" data-i18n-title="docSwitchTooltip" data-i18n-aria="docSwitchTooltip" title="Dokument wählen oder verwalten" aria-label="Dokument wählen oder verwalten">
<span class="docname" id="docName"></span>
<svg class="doc-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</span>
<div class="docmenu" id="docMenu" role="menu" data-i18n-aria="docMenuAria" aria-label="Dokumente" hidden>
<div class="docmenu-list" id="docList"></div>
<div class="docmenu-actions">
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button>
<button type="button" class="docact" id="docOpenFile" data-i18n="docOpenFile">Datei öffnen…</button>
<button type="button" class="docact" id="docSaveFile" data-i18n="docSaveFile">Als Datei speichern</button>
<button type="button" class="docact" id="docToServer" data-i18n="docToServer">Auf den Server legen</button>
<button type="button" class="docact" id="docRename" data-i18n="docRename">Umbenennen</button>
<button type="button" class="docact" id="docRestore" data-i18n="docRestore" hidden>Original wiederherstellen</button>
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
</div>
</div>
<span class="snapwrap">
<button type="button" class="copybtn snapbtn" id="snapBtn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="snapTooltip" data-i18n-aria="snapTooltip" title="Frühere Stände dieses Dokuments" aria-label="Frühere Stände dieses Dokuments">
<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="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3 4v4h4"/><path d="M12 7.5V12l3 1.8"/></svg>
<!-- Die Beschriftung ist zurück (D81) — der Dokument-Wähler steht jetzt
als Brotkrume im App-Kopf. -->
<span data-i18n="editorTitle">Text-Editor</span>
<!-- Stand-Funktionen des AKTIVEN Dokuments (D81): Speichern, Stand jetzt
sichern, frühere Stände, aus der Quelle neu laden — dazu Teilen. Die
Gruppe trägt das `margin-right:auto` und ist der Bezug für das
Snapshot-Menü. -->
<span class="standgroup">
<button type="button" class="copybtn" id="saveBtn" data-i18n-title="docSaveFile" data-i18n-aria="docSaveFile" title="Als Datei speichern (Strg+S)" aria-label="Als Datei speichern (Strg+S)">
<svg class="ic-main" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button>
<div class="snapmenu" id="snapMenu" role="menu" data-i18n-aria="snapMenuAria" aria-label="Frühere Stände" hidden></div>
</span>
<!-- Von Hand sichern, z. B. vor einer größeren Änderung. Kamera statt
Plus: ein Plus neben dem Dokument-Namen läse sich als „neues
Dokument“ (D54-Nachtrag). -->
@@ -102,6 +113,24 @@
<svg class="ic-main" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9a2 2 0 0 1 2-2h2.2l1.3-2h6.9l1.3 2H19a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/><circle cx="12" cy="13" r="3.1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button>
<span class="snapwrap">
<button type="button" class="copybtn snapbtn" id="snapBtn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="snapTooltip" data-i18n-aria="snapTooltip" title="Frühere Stände dieses Dokuments" aria-label="Frühere Stände dieses Dokuments">
<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="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3 4v4h4"/><path d="M12 7.5V12l3 1.8"/></svg>
</button>
<div class="snapmenu" id="snapMenu" role="menu" data-i18n-aria="snapMenuAria" aria-label="Frühere Stände" hidden></div>
</span>
<!-- Aus der Quelle neu laden bzw. Original wiederherstellen — sichtbar
nur, wo es eine Quelle gibt (mitgeliefert, URL, gemerkte Datei);
Tooltip setzt updateDocButtons() je nach Fall. -->
<button type="button" class="copybtn" id="reloadBtn" 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="M1.7 5v5.5h5.5"/><path d="M4 15a8.5 8.5 0 1 0 .5-7.5L1.7 10.5"/></svg>
</button>
<!-- „Teilen" (D81, kurz für „Auf den Server legen", D76-Nachtrag 8):
legt das Dokument auf einen Werkbaum-Server und startet die
gemeinsame Bearbeitung. Entfällt, wo es schon liegt. -->
<button type="button" class="copybtn" id="shareBtn" data-i18n-title="docToServer" data-i18n-aria="docToServer" title="Teilen auf einen Werkbaum-Server legen und gemeinsam bearbeiten" aria-label="Teilen auf einen Werkbaum-Server legen und gemeinsam bearbeiten" 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="M16 16.5l-4-4-4 4M12 13v8"/><path d="M20.4 17.6A5 5 0 0 0 18 8.5h-1.3A8 8 0 1 0 3 15.7"/></svg>
</button>
</span>
<button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
@@ -124,7 +153,7 @@
einen waagerechten Balken (D49). Die Einrückung trägt hier die
Hierarchie — ein weicher Umbruch setzt die Fortsetzung an den linken
Rand und lässt die Ebene verschwinden. -->
<textarea id="src" wrap="off" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
<textarea id="src" wrap="off" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Text-Editor"></textarea>
</div>
<div class="hint-gutter" id="hintGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="hintGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Editor und Legende größenverändern" hidden></div>
<div class="agenda" id="agenda">
+222 -88
View File
@@ -8,7 +8,7 @@ import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomple
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
from './snapshots.js';
import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.js';
import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch } from './docurl.js';
import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch, docKind } from './docurl.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. */
@@ -918,6 +918,11 @@ src.addEventListener('keydown', e => {
src.addEventListener('input', render);
src.addEventListener('input', saveSrc);
src.addEventListener('input', scheduleLivePush); /* Server-Dokument: Diff nach 1,5 s Ruhe (D76) */
/* Der Neu-laden-Knopf (D81) hängt bei mitgelieferten Dokumenten an der
Abweichung vom Auslieferungsstand und die entsteht beim TIPPEN, nicht
erst beim Dokumentwechsel. Der Vergleich ist ein String-Vergleich mit
frühem Ausstieg, je Tastendruck unbedenklich. */
src.addEventListener('input', () => updateDocButtons());
/* ---------- Sprung zwischen Diagramm und Text (D25) ----------
Jeder Knoten trägt seine Zeilennummer als `data-line` (render.js).
@@ -2202,7 +2207,6 @@ document.addEventListener('click', e => {
const I18N = {
de: {
subtitle:"Werkbaum PSP / Lean Pathfinding · Editor für Projektstrukturpläne (auch Feature-Tree- & Requirements-Editor)",
subtitleShort:"PSP-Editor mit Lean Pathfinding",
imprint:"Impressum",
privacy:"Datenschutz",
legendTooltip:"Legende ein-/ausblenden",
@@ -2232,7 +2236,7 @@ const I18N = {
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung mindestens {size} angenommen",
fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite",
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
editorTitle:"Text-Editor", diagramTitle:"Diagramm",
docSwitchTooltip:"Dokument wählen oder verwalten", docMenuAria:"Dokumente",
/* Frühere Stände (D54) — alle 10 Minuten, nur bei Änderung. */
snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände",
@@ -2245,8 +2249,11 @@ const I18N = {
docDeleteConfirm:"Dokument „{name}“ löschen?",
docRestore:"Original wiederherstellen",
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern",
docToServer:"Auf den Server legen",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern (Strg+S)",
docToServer:"Teilen auf einen Werkbaum-Server legen und gemeinsam bearbeiten",
docReload:"Aus der Quelle neu laden lokale Änderungen gehen verloren",
docReloadConfirm:"„{name}“ aus der Quelle neu laden? Lokale Änderungen gehen verloren.",
docGroupShipped:"Mitgeliefert", docGroupOwn:"Eigene", docGroupSources:"Quellen",
docToServerAsk:"Adresse des Werkbaum-Servers (z. B. https://werkbaum.example):",
docToServerDone:"Auf dem Server — der Link steht in der Adresszeile und in der Zwischenablage.",
docToServerFailed:"Nicht auf den Server gelegt: {error}",
@@ -2315,7 +2322,6 @@ const I18N = {
},
en: {
subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)",
subtitleShort:"WBS editor with Lean Pathfinding",
imprint:"Imprint (Impressum)",
privacy:"Privacy",
legendTooltip:"Show/hide legend",
@@ -2344,7 +2350,7 @@ const I18N = {
implicitSizeTooltip:"No size given assumed at least {size} for the cost estimate",
fullscreenTooltip:"Full screen panels use the full window width",
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
editorTitle:"Structure (text)", diagramTitle:"Diagram",
editorTitle:"Text editor", diagramTitle:"Diagram",
docSwitchTooltip:"Choose or manage document", docMenuAria:"Documents",
snapTooltip:"Earlier states of this document", snapMenuAria:"Earlier states",
snapNone:"No earlier states yet one is kept on demand and every 10 minutes, once something has changed.",
@@ -2356,8 +2362,11 @@ const I18N = {
docDeleteConfirm:"Delete document “{name}”?",
docRestore:"Restore original",
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
docOpenFile:"Open file…", docSaveFile:"Save as file",
docToServer:"Put on the server",
docOpenFile:"Open file…", docSaveFile:"Save as file (Ctrl+S)",
docToServer:"Share put on a Werkbaum server and edit together",
docReload:"Reload from the source local changes are lost",
docReloadConfirm:"Reload “{name}” from its source? Local changes are lost.",
docGroupShipped:"Included", docGroupOwn:"Yours", docGroupSources:"Sources",
docToServerAsk:"Address of the Werkbaum server (e.g. https://werkbaum.example):",
docToServerDone:"On the server — the link is in the address bar and on the clipboard.",
docToServerFailed:"Not put on the server: {error}",
@@ -2426,7 +2435,6 @@ const I18N = {
},
es: {
subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)",
subtitleShort:"Editor EDT con Lean Pathfinding",
imprint:"Aviso legal (Impressum)",
privacy:"Privacidad",
legendTooltip:"Mostrar u ocultar la leyenda",
@@ -2455,7 +2463,7 @@ const I18N = {
implicitSizeTooltip:"Sin tamaño indicado: se asume al menos {size} para el cálculo de costes",
fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana",
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
editorTitle:"Editor de texto", diagramTitle:"Diagrama",
docSwitchTooltip:"Elegir o gestionar documento", docMenuAria:"Documentos",
snapTooltip:"Estados anteriores de este documento", snapMenuAria:"Estados anteriores",
snapNone:"Aún no hay estados anteriores: se guarda uno al pulsar el botón y cada 10 minutos, cuando algo ha cambiado.",
@@ -2467,8 +2475,11 @@ const I18N = {
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
docRestore:"Restaurar original",
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo",
docToServer:"Poner en el servidor",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo (Ctrl+S)",
docToServer:"Compartir poner en un servidor Werkbaum y editar en conjunto",
docReload:"Recargar desde la fuente los cambios locales se pierden",
docReloadConfirm:"¿Recargar «{name}» desde su fuente? Los cambios locales se pierden.",
docGroupShipped:"Incluidos", docGroupOwn:"Propios", docGroupSources:"Fuentes",
docToServerAsk:"Dirección del servidor Werkbaum (p. ej. https://werkbaum.example):",
docToServerDone:"En el servidor: el enlace está en la barra de direcciones y en el portapapeles.",
docToServerFailed:"No se pudo poner en el servidor: {error}",
@@ -2537,7 +2548,6 @@ const I18N = {
},
fr: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)",
subtitleShort:"Éditeur WBS avec Lean Pathfinding",
imprint:"Mentions légales (Impressum)",
privacy:"Confidentialité",
legendTooltip:"Afficher/masquer la légende",
@@ -2566,7 +2576,7 @@ const I18N = {
implicitSizeTooltip:"Aucune taille indiquée au moins {size} supposé pour l'estimation des coûts",
fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre",
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
editorTitle:"Éditeur de texte", diagramTitle:"Diagramme",
docSwitchTooltip:"Choisir ou gérer le document", docMenuAria:"Documents",
snapTooltip:"États antérieurs de ce document", snapMenuAria:"États antérieurs",
snapNone:"Pas encore d’état antérieur : un état est conservé sur demande et toutes les 10 minutes, dès que quelque chose a changé.",
@@ -2578,8 +2588,11 @@ const I18N = {
docDeleteConfirm:"Supprimer le document « {name} » ?",
docRestore:"Restaurer loriginal",
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier",
docToServer:"Mettre sur le serveur",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier (Ctrl+S)",
docToServer:"Partager déposer sur un serveur Werkbaum et éditer à plusieurs",
docReload:"Recharger depuis la source les modifications locales sont perdues",
docReloadConfirm:"Recharger « {name} » depuis sa source ? Les modifications locales seront perdues.",
docGroupShipped:"Fournis", docGroupOwn:"Personnels", docGroupSources:"Sources",
docToServerAsk:"Adresse du serveur Werkbaum (p. ex. https://werkbaum.example) :",
docToServerDone:"Sur le serveur — le lien est dans la barre d'adresse et dans le presse-papiers.",
docToServerFailed:"Non déposé sur le serveur : {error}",
@@ -2648,7 +2661,6 @@ const I18N = {
},
pl: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)",
subtitleShort:"Edytor WBS z Lean Pathfinding",
imprint:"Nota prawna (Impressum)",
privacy:"Prywatność",
legendTooltip:"Pokaż/ukryj legendę",
@@ -2677,7 +2689,7 @@ const I18N = {
implicitSizeTooltip:"Nie podano rozmiaru do szacowania kosztów przyjęto co najmniej {size}",
fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna",
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
editorTitle:"Edytor tekstu", diagramTitle:"Diagram",
docSwitchTooltip:"Wybierz lub zarządzaj dokumentem", docMenuAria:"Dokumenty",
snapTooltip:"Wcześniejsze stany tego dokumentu", snapMenuAria:"Wcześniejsze stany",
snapNone:"Brak wcześniejszych stanów zapisywany jest na żądanie i co 10 minut, gdy coś się zmieni.",
@@ -2689,8 +2701,11 @@ const I18N = {
docDeleteConfirm:"Usunąć dokument „{name}”?",
docRestore:"Przywróć oryginał",
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik",
docToServer:"Umieść na serwerze",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik (Ctrl+S)",
docToServer:"Udostępnij umieść na serwerze Werkbaum i edytujcie wspólnie",
docReload:"Wczytaj ponownie ze źródła lokalne zmiany przepadną",
docReloadConfirm:"Wczytać „{name}” ponownie ze źródła? Lokalne zmiany przepadną.",
docGroupShipped:"Dołączone", docGroupOwn:"Własne", docGroupSources:"Źródła",
docToServerAsk:"Adres serwera Werkbaum (np. https://werkbaum.example):",
docToServerDone:"Na serwerze — link jest w pasku adresu i w schowku.",
docToServerFailed:"Nie umieszczono na serwerze: {error}",
@@ -2759,7 +2774,6 @@ const I18N = {
},
ru: {
subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)",
subtitleShort:"Редактор СДР с Lean Pathfinding",
imprint:"Выходные данные (Impressum)",
privacy:"Конфиденциальность",
legendTooltip:"Показать/скрыть легенду",
@@ -2788,7 +2802,7 @@ const I18N = {
implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}",
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
editorTitle:"Текстовый редактор", diagramTitle:"Диаграмма",
docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы",
snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния",
snapNone:"Прежних состояний пока нет — они сохраняются по нажатию кнопки и каждые 10 минут, если что-то изменилось.",
@@ -2800,8 +2814,11 @@ const I18N = {
docDeleteConfirm:"Удалить документ «{name}»?",
docRestore:"Восстановить оригинал",
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл",
docToServer:"Положить на сервер",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл (Ctrl+S)",
docToServer:"Поделиться положить на сервер Werkbaum и редактировать вместе",
docReload:"Перезагрузить из источника – локальные изменения будут потеряны",
docReloadConfirm:"Перезагрузить «{name}» из источника? Локальные изменения будут потеряны.",
docGroupShipped:"Встроенные", docGroupOwn:"Свои", docGroupSources:"Источники",
docToServerAsk:"Адрес сервера Werkbaum (например, https://werkbaum.example):",
docToServerDone:"На сервере — ссылка в адресной строке и в буфере обмена.",
docToServerFailed:"Не удалось положить на сервер: {error}",
@@ -2870,7 +2887,6 @@ const I18N = {
},
hi: {
subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)",
subtitleShort:"WBS संपादक Lean Pathfinding के साथ",
imprint:"प्रकाशन विवरण (Impressum)",
privacy:"गोपनीयता",
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
@@ -2899,7 +2915,7 @@ const I18N = {
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया",
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
editorTitle:"टेक्स्ट संपादक", diagramTitle:"आरेख",
docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़",
snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ",
snapNone:"अभी कोई पिछली स्थिति नहीं — बटन दबाने पर और कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।",
@@ -2911,8 +2927,11 @@ const I18N = {
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
docRestore:"मूल पुनर्स्थापित करें",
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें",
docToServer:"सर्वर पर रखें",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें (Ctrl+S)",
docToServer:"साझा करें – Werkbaum सर्वर पर रखें और साथ मिलकर संपादित करें",
docReload:"स्रोत से फिर लोड करें – स्थानीय बदलाव खो जाएँगे",
docReloadConfirm:"“{name}” को स्रोत से फिर लोड करें? स्थानीय बदलाव खो जाएँगे।",
docGroupShipped:"साथ आए", docGroupOwn:"आपके", docGroupSources:"स्रोत",
docToServerAsk:"Werkbaum सर्वर का पता (जैसे https://werkbaum.example):",
docToServerDone:"सर्वर पर है — लिंक पता-पट्टी में और क्लिपबोर्ड में है।",
docToServerFailed:"सर्वर पर नहीं रखा जा सका: {error}",
@@ -2981,7 +3000,6 @@ const I18N = {
},
zh: {
subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)",
subtitleShort:"WBS 编辑器和 Lean Pathfinding",
imprint:"法律声明(Impressum",
privacy:"隐私",
legendTooltip:"显示/隐藏图例",
@@ -3010,7 +3028,7 @@ const I18N = {
liveConflictTheirs:"采用对方的",
liveConflictMine:"保留我的",
riskTooltip:"高风险 – 工作量尚不明确。",
editorTitle:"结构(文本)", diagramTitle:"图表",
editorTitle:"文本编辑器", diagramTitle:"图表",
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态",
snapNone:"暂无早期状态——可随时手动保存;内容有变动时,也会每 10 分钟保存一次。",
@@ -3022,8 +3040,11 @@ const I18N = {
docDeleteConfirm:"删除文档“{name}”?",
docRestore:"恢复原始版本",
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
docOpenFile:"打开文件…", docSaveFile:"另存为文件",
docToServer:"放到服务器上",
docOpenFile:"打开文件…", docSaveFile:"另存为文件Ctrl+S",
docToServer:"共享——放到 Werkbaum 服务器上共同编辑",
docReload:"从来源重新加载——本地更改将丢失",
docReloadConfirm:"从来源重新加载“{name}”?本地更改将丢失。",
docGroupShipped:"自带", docGroupOwn:"自己的", docGroupSources:"来源",
docToServerAsk:"Werkbaum 服务器地址(例如 https://werkbaum.example):",
docToServerDone:"已在服务器上——链接在地址栏和剪贴板中。",
docToServerFailed:"未能放到服务器上:{error}",
@@ -3092,7 +3113,6 @@ const I18N = {
},
ja: {
subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)",
subtitleShort:"WBS エディター & Lean Pathfinding",
imprint:"運営者情報(Impressum",
privacy:"プライバシー",
legendTooltip:"凡例を表示/非表示",
@@ -3121,7 +3141,7 @@ const I18N = {
liveConflictTheirs:"相手の版",
liveConflictMine:"自分の版",
riskTooltip:"高リスク – 規模はまだ不明。",
editorTitle:"構造(テキスト", diagramTitle:"ダイアグラム",
editorTitle:"テキストエディター", diagramTitle:"ダイアグラム",
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態",
snapNone:"以前の状態はまだありません — ボタンを押したとき、および変更があれば 10 分ごとに保存されます。",
@@ -3133,8 +3153,11 @@ const I18N = {
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
docRestore:"オリジナルを復元",
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存",
docToServer:"サーバーに置く",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存 (Ctrl+S)",
docToServer:"共有 – Werkbaum サーバーに置いて共同編集",
docReload:"ソースから再読み込み – ローカルの変更は失われます",
docReloadConfirm:"「{name}」をソースから再読み込みしますか?ローカルの変更は失われます。",
docGroupShipped:"同梱", docGroupOwn:"自分の", docGroupSources:"ソース",
docToServerAsk:"Werkbaum サーバーのアドレス(例: https://werkbaum.example:",
docToServerDone:"サーバーにあります — リンクはアドレス欄とクリップボードにあります。",
docToServerFailed:"サーバーに置けませんでした: {error}",
@@ -3268,6 +3291,9 @@ function applyLang(l){
offen, muss es die neue Sprache jetzt bekommen (auch die Datumsangaben,
die `toLocaleDateString` liefert). */
if(newsMenu && !newsMenu.hidden) renderNewsMenu();
/* Dasselbe fürs Dokumenten-Menü (D81): Gruppen-Überschriften und
Aktions-Tooltips kommen aus renderDocMenu. */
if(docMenu && !docMenu.hidden) renderDocMenu();
try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
}
const langsel = document.querySelector('.langsel');
@@ -3513,7 +3539,11 @@ function restoreState(){
restoring = false;
}
/* ---------- Dokument-Wähler: UI (Dropdown in der Editor-Titelzeile) ---------- */
/* ---------- Dokument-Wähler: Brotkrume im App-Kopf (D81) ----------
Werkbaum name" der Chip öffnet das Menü, das nach Dokumentart gruppiert
(docKind, docurl.js) und je Zeile Umbenennen/Löschen/Wiederherstellen trägt.
Die Stand-Funktionen des aktiven Dokuments (Speichern, Stände, Neu laden,
Teilen) stehen als Knöpfe in der Editor-Titelzeile. */
const docTrigger = document.getElementById('docTrigger');
const docMenu = document.getElementById('docMenu');
const docNameEl = document.getElementById('docName');
@@ -3529,6 +3559,7 @@ function updateDocName(){
? d.source + '\n' + t('docSwitchTooltip')
: t('docSwitchTooltip');
}
updateDocButtons(); /* die Stand-Knöpfe der Titelzeile folgen mit (D81) */
}
let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */
/* Kam das Umbenennen von einem frisch angelegten Dokument? Dann geht es danach
@@ -3540,8 +3571,16 @@ function serverHostOf(id){
if(!roh.startsWith('live:')) return null;
try{ return new URL(roh.slice(5)).host; }catch(_){ return null; }
}
function renderDocMenu(){
docList.innerHTML = docs.map(d => {
/* Kleine Zeilen-Icons (Feather-Stil wie überall): Stift, Papierkorb,
Wiederherstellen. Als Konstanten, damit der Renderer lesbar bleibt. */
const IC_RENAME = '<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="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>';
const IC_DELETE = '<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="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>';
const IC_RESTORE = '<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="M1.7 5v5.5h5.5"/><path d="M4 15a8.5 8.5 0 1 0 .5-7.5L1.7 10.5"/></svg>';
/* Eine Menüzeile: Wählen-Knopf + Zeilen-Aktionen als GESCHWISTER ein Knopf
im Knopf wäre ungültiges HTML. Die Aktionen sind immer sichtbar (Touch
kennt kein Hover): Umbenennen und Löschen überall, Wiederherstellen nur an
einem mitgelieferten Dokument, das vom Auslieferungsstand abweicht. */
function docRowHtml(d){
if(d.id === renamingId){
/* Inline-Umbenennen direkt im Menü (kein window.prompt das ist in
manchen Browser-Kontexten unterdrückt). Wert/Handler unten in JS. */
@@ -3554,10 +3593,35 @@ function renderDocMenu(){
verriete den Unterschied. */
const host = serverHostOf(d.id);
const zusatz = host ? `<span class="docitem-where">${esc(host)}</span>` : '';
return `<button type="button" class="docitem" role="menuitemradio" data-id="${d.id}" ` +
`aria-checked="${d.id === activeId ? 'true' : 'false'}">` +
const shipped = shippedStateOf(d.id);
const abweichend = shipped && (d.text !== shipped.text || d.name !== shipped.name);
const iconBtn = (act, label, svg, extra) =>
`<button type="button" class="dociconbtn${extra || ''}" data-act="${act}" ` +
`title="${esc(label)}" aria-label="${esc(label)}">${svg}</button>`;
return `<div class="docitem" data-id="${esc(d.id)}">` +
`<button type="button" class="docpick" role="menuitemradio" ` +
`aria-checked="${d.id === activeId ? 'true' : 'false'}" title="${esc(d.source || '')}">` +
`<span class="doccheck" aria-hidden="true">✓</span>` +
`<span class="docitem-name">${esc(d.name)}</span>${zusatz}</button>`;
`<span class="docitem-name">${esc(d.name)}</span>${zusatz}</button>` +
`<span class="docacts">` +
(abweichend ? iconBtn('restore', t('docRestore'), IC_RESTORE) : '') +
iconBtn('rename', t('docRename'), IC_RENAME) +
iconBtn('delete', t('docDelete'), IC_DELETE, ' docdelbtn') +
`</span></div>`;
}
function renderDocMenu(){
/* Gruppen nach Dokumentart (D81): mitgeliefert · eigene · Quellen (Server
und URL). Leere Gruppen erscheinen nicht; die Reihenfolge ist fest. */
const GRUPPEN = [
{kinds: ['shipped'], label: t('docGroupShipped')},
{kinds: ['own'], label: t('docGroupOwn')},
{kinds: ['server', 'url'], label: t('docGroupSources')},
];
docList.innerHTML = GRUPPEN.map(g => {
const liste = docs.filter(d => g.kinds.includes(docKind(d.id, SHIPPED_IDS)));
if(!liste.length) return '';
return `<div class="docsec" aria-hidden="true">${esc(g.label)}</div>` +
liste.map(docRowHtml).join('');
}).join('');
if(renamingId){
const inp = docList.querySelector('.docrename');
@@ -3584,42 +3648,92 @@ function shippedStateOf(id){
if(id === WERKBAUM_ID) return {name: WERKBAUM_NAME, text: WERKBAUM_DOC};
return null;
}
function updateRestoreBtn(){
const SHIPPED_IDS = [EXAMPLE_ID, WERKBAUM_ID]; /* für docKind (Menü-Gruppen, D81) */
/* Die Stand-Knöpfe der Editor-Titelzeile folgen dem aktiven Dokument (D81):
Teilen" entfällt, wo es schon auf einem Server liegt (sonst entstünde ein
zweites, gleichnamiges Dokument dort erkennbar an der id `live:…`, nicht
am laufenden liveState). Neu laden" erscheint nur, wo es eine Quelle gibt:
mitgeliefert (Original wiederherstellen), URL oder gemerkte Datei. Der
Speichern-Tooltip nennt die Zieldatei des gemerkten Handles (D72, Stufe 2)
Dateinamen sind Daten, kein i18n. */
function updateDocButtons(){
const d = activeDoc();
const teilen = document.getElementById('shareBtn');
if(teilen) teilen.hidden = !d || String(d.id).startsWith('live:');
const neu = document.getElementById('reloadBtn');
if(neu){
const shipped = d && shippedStateOf(d.id);
const btn = document.getElementById('docRestore');
/* Immer zeigen, wenn ein MITGELIEFERTES Dokument aktiv ist nur ausgegraut,
solange es dem Auslieferungsstand entspricht. Ganz versteckt war der
Eintrag nicht auffindbar (Nutzer: ich sehe keinen Reset-Button"). */
btn.hidden = !shipped;
btn.disabled = !!shipped && d.text === shipped.text && d.name === shipped.name;
/* Auf den Server legen" entfällt, wo es schon liegt. Sonst entstünde ein
zweites, gleichnamiges Dokument auf demselben Server im Wähler nicht zu
unterscheiden. Erkennbar an der id (`live:…`), nicht am laufenden
`liveState`: Auch ein Server-Dokument, das gerade nicht das aktive ist,
liegt bereits dort. */
const zumServer = document.getElementById('docToServer');
if(zumServer) zumServer.hidden = !!d && String(d.id).startsWith('live:');
if(shipped){
neu.hidden = false;
neu.disabled = d.text === shipped.text && d.name === shipped.name;
neu.title = t('docRestore');
neu.setAttribute('aria-label', t('docRestore'));
} else if(d && (d.source || fileHandles.has(d.id))){
neu.hidden = false; neu.disabled = false;
neu.title = t('docReload');
neu.setAttribute('aria-label', t('docReload'));
} else {
neu.hidden = true;
}
}
const speichern = document.getElementById('saveBtn');
if(speichern && d){
const h = fileHandles.get(d.id);
speichern.title = t('docSaveFile') + (h ? '\n' + h.name : '');
}
}
function restoreDoc(){
const d = activeDoc();
/* Wiederherstellen gilt jetzt je Zeile des Menüs UND für das aktive
Dokument dem Neu-laden-Knopf der Titelzeile. */
function restoreDoc(id){
const d = docs.find(x => x.id === id);
const shipped = d && shippedStateOf(d.id);
if(!shipped) return;
if(!window.confirm(t('docRestoreConfirm', {name: d.name}))) return;
d.text = shipped.text;
d.name = shipped.name;
if(d.id === activeId){
foldOverrides.clear();
loadActiveIntoEditor();
}
persistDocs();
closeDocMenu();
if(!docMenu.hidden) renderDocMenu();
updateDocButtons();
}
/* Aus der Quelle neu laden (D81): mitgeliefert Original wiederherstellen;
URL-Dokument frisch holen (die URL ist die Quelle der Wahrheit, D23);
Datei mit gemerktem Handle neu aus der Datei lesen (Chromium). Scheitert
das Datei-Lesen (Berechtigung verweigert, Datei weg), bleibt der Stand
stehen der verweigerte Dialog IST die Antwort. */
async function reloadDoc(){
const d = activeDoc();
if(!d) return;
if(shippedStateOf(d.id)){ restoreDoc(d.id); return; }
if(!window.confirm(t('docReloadConfirm', {name: d.name}))) return;
if(d.source){
try{
d.text = await fetchRemote(d.source);
sourceWarning = null;
loadActiveIntoEditor();
persistDocs();
}catch(err){
sourceWarning = {type:'sourceLoad', url: d.source, error: (err && err.message) || String(err)};
render();
}
return;
}
const h = fileHandles.get(d.id);
if(!h) return;
try{
let perm = await h.queryPermission({mode: 'read'});
if(perm === 'prompt') perm = await h.requestPermission({mode: 'read'});
if(perm !== 'granted') return;
d.text = await (await h.getFile()).text();
loadActiveIntoEditor();
persistDocs();
}catch(_){}
}
function openDocMenu(){
renderDocMenu(); updateRestoreBtn();
/* Wohin Als Datei speichern" schreiben würde: der Dateiname des gemerkten
Handles als Tooltip (D72, Stufe 2) Dateinamen sind Daten, kein i18n. */
const h = fileHandles.get(activeId);
document.getElementById('docSaveFile').title = h ? h.name : '';
renderDocMenu();
docMenu.hidden = false; docTrigger.setAttribute('aria-expanded', 'true');
}
function closeDocMenu(){ renamingId = null; renameIsNew = false; docMenu.hidden = true; docTrigger.setAttribute('aria-expanded', 'false'); }
@@ -3975,9 +4089,9 @@ function finishNewDoc(){
closeDocMenu();
src.focus();
}
function renameDoc(){
if(!activeDoc()) return;
renamingId = activeId; /* aktives Dokument inline umbenennen */
function renameDoc(id){
if(!docs.some(x => x.id === id)) return;
renamingId = id; /* Zeilen-Aktion (D81): jedes Dokument, nicht nur das aktive */
renameIsNew = false;
renderDocMenu();
}
@@ -3992,8 +4106,8 @@ function commitRename(){
finishNewDoc();
}
function cancelRename(){ renamingId = null; renderDocMenu(); finishNewDoc(); }
function deleteDoc(){
const d = activeDoc();
function deleteDoc(id){
const d = docs.find(x => x.id === id);
if(!d) return;
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
if(liveState && liveState.id === d.id) stopLive(); /* dito fürs Server-Dokument (D76) */
@@ -4001,11 +4115,15 @@ function deleteDoc(){
docs = docs.filter(x => x.id !== d.id);
if(snaps[d.id]){ delete snaps[d.id]; persistSnaps(snaps, localStorage); } /* mit dem Dokument gehen seine Stände (D54) */
if(!docs.length) docs = [{ id: EXAMPLE_ID, name: EXAMPLE_NAME, text: INITIAL }];
/* Zeilen-Aktion (D81): Nur wenn das AKTIVE Dokument geht, wechselt der
Editor; das Menü bleibt offen wer aufräumt, räumt meist weiter. */
if(d.id === activeId){
activeId = docs[0].id;
foldOverrides.clear();
loadActiveIntoEditor();
}
persistDocs();
closeDocMenu();
if(!docMenu.hidden) renderDocMenu();
}
/* ---------- Lokale Dateien öffnen und speichern (D72, Stufe 1) ----------
Der klassische Weg, der in jedem Browser läuft: Datei-Input zum Öffnen,
@@ -4230,12 +4348,12 @@ async function saveLocalFile(){
} else {
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name));
}
closeDocMenu();
updateDocButtons(); /* ein frisch gemerktes Handle gehört in den Tooltip */
/* Stilles In-Place-Speichern braucht eine sichtbare Antwort sonst wirkt
die Geste tot. Haus-Idiom flashBtn (Petrol-Blitz, D54), am Dokumentnamen:
Der ist das, was gespeichert wurde. Dialog und Download sind selbst
sichtbar und brauchen keinen. */
if(inPlace) flashBtn(docTrigger);
die Geste tot. Haus-Idiom flashBtn (Petrol-Blitz samt Haken, D54), am
Speichern-Knopf selbst: Er ist die Geste (auch für Strg+S, D74). Dialog
und Download sind selbst sichtbar und brauchen keinen. */
if(inPlace) flashBtn(document.getElementById('saveBtn'));
}
/* Strg+S / Cmd+S speichert als Datei (D74) die Geste, die jeder zuerst
versucht. Ohne preventDefault öffnet der Browser Seite speichern": genau
@@ -4771,7 +4889,6 @@ async function putOnServer(){
const basis = serverBaseOrAsk();
if(!basis) return;
closeDocMenu();
try{
const doc = await fetchJson(live.documentsUrl(basis), {
method: 'POST',
@@ -4804,9 +4921,6 @@ async function putOnServer(){
}
docTrigger.addEventListener('click', e => {
/* Desktop: ist der Editor minimiert, stellt ein Klick ihn wieder her
(Bubbling zur Titelzeile) statt das Menü zu öffnen. */
if(!isMobile() && editorPanel.classList.contains('collapsed')) return;
e.stopPropagation();
toggleDocMenu();
});
@@ -4814,22 +4928,39 @@ docTrigger.addEventListener('keydown', e => {
if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); toggleDocMenu(); }
else if(e.key === 'Escape') closeDocMenu();
});
/* Esc IM Menü schließt es und gibt den Fokus an den Chip zurück. */
docMenu.addEventListener('keydown', e => {
if(e.key === 'Escape' && !renamingId){ closeDocMenu(); docTrigger.focus(); }
});
/* Eine Delegation für beides: Zeilen-Aktion (data-act am Icon-Knopf) oder
Wählen (.docpick). Nach einer Aktion bleibt das Menü offen wer verwaltet,
verwaltet meist weiter; Wählen schließt. */
docList.addEventListener('click', e => {
const btn = e.target.closest('.docitem');
const akt = e.target.closest('[data-act]');
if(akt){
e.stopPropagation();
const zeile = akt.closest('.docitem');
const id = zeile && zeile.dataset.id;
if(!id) return;
if(akt.dataset.act === 'rename') renameDoc(id);
else if(akt.dataset.act === 'delete') deleteDoc(id);
else if(akt.dataset.act === 'restore') restoreDoc(id);
return;
}
const btn = e.target.closest('.docpick');
if(!btn) return;
e.stopPropagation();
switchDoc(btn.dataset.id);
switchDoc(btn.closest('.docitem').dataset.id);
closeDocMenu();
});
/* `newDoc()` zeichnet das Menü selbst neu es geht direkt ins Umbenennen
(D51), und ein zweiter Durchlauf baute das Eingabefeld nur noch einmal auf. */
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); });
document.getElementById('docOpenFile').addEventListener('click', e => { e.stopPropagation(); openLocalFile(); });
document.getElementById('docSaveFile').addEventListener('click', e => { e.stopPropagation(); saveLocalFile(); });
document.getElementById('docToServer').addEventListener('click', e => { e.stopPropagation(); putOnServer(); });
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); });
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); });
document.getElementById('docRestore').addEventListener('click', e => { e.stopPropagation(); restoreDoc(); });
/* Die Stand-Knöpfe der Editor-Titelzeile (D81). */
document.getElementById('saveBtn').addEventListener('click', saveLocalFile);
document.getElementById('shareBtn').addEventListener('click', putOnServer);
document.getElementById('reloadBtn').addEventListener('click', reloadDoc);
/* Klick außerhalb schließt das Menü. */
document.addEventListener('click', e => {
if(!docMenu.hidden && !docMenu.contains(e.target) && !docTrigger.contains(e.target)) closeDocMenu();
@@ -5010,6 +5141,9 @@ try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
if(hasFsAccess) handlesReady = idbLoadHandles(); /* gemerkte Datei-Handles zurückholen (D72, Stufe 2) */
/* Erst mit den Handles wissen Speichern-Tooltip und Neu-laden-Knopf, ob das
aktive Dokument eine gemerkte Datei hat (D81). */
handlesReady.then(() => updateDocButtons());
/* PWA-Dateihandling (D73): Die installierte App registriert sich über das
Manifest (`file_handlers`) für .werkbaum/.txt; ein Doppelklick im
+14
View File
@@ -30,6 +30,20 @@ export function docParam(id){
return null;
}
/* Zu welcher Gruppe des Dokumenten-Menüs gehört ein Dokument? (D81)
Die Art steckt wie bei docParam in der id: mitgeliefert (feste ids),
Server (`live:`) und URL (`url:`) sind Quellen von außen, alles andere ist
ein eigenes Dokument im Browser (Datei-Dokumente eingeschlossen ohne
File-System-Access-Handle sind sie von eigenen nicht zu unterscheiden,
und das Handle lebt nur in Chromium). */
export function docKind(id, shippedIds){
const roh = String(id == null ? '' : id);
if((shippedIds || []).indexOf(roh) >= 0) return 'shipped';
if(roh.startsWith('live:')) return 'server';
if(roh.startsWith('url:')) return 'url';
return 'own';
}
/* Der neue Query-String zu `search` (mit oder ohne führendes `?`) für das
Dokument `id`.
+136 -77
View File
@@ -78,6 +78,95 @@
header{max-width:1200px;width:100%;margin:0 auto 16px;flex:0 0 auto}
.header-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.header-tools{display:flex;align-items:center;gap:12px}
/* Marke + aktives Dokument als Brotkrume (D81): Werkbaum name". Die
Gruppe darf schrumpfen (min-width:0), damit der Chip lange Namen mit
Ellipse kürzt, statt die Werkzeuge rechts zu verdrängen. */
.brandgroup{display:flex;align-items:center;gap:10px;min-width:0;flex:1 1 auto}
.crumb-sep{color:var(--muted);opacity:.6;font-size:1.05rem;flex:0 0 auto;user-select:none}
.docwrap{position:relative;display:inline-flex;min-width:0}
/* Der Auslöser sieht jetzt nach Menü aus: gerahmter Chip mit Pfeil die
frühere nackte Beschriftung in der Panel-Zeile war als Knopf nicht zu
erkennen (der Anlass von D81). */
.doctrigger{
display:inline-flex;align-items:center;gap:6px;cursor:pointer;min-width:0;
border:1px solid rgba(36,52,71,.2);border-radius:8px;background:var(--card);
padding:4px 10px;color:var(--ink);
font-family:'IBM Plex Mono',monospace;font-size:.78rem;letter-spacing:0;
}
.doctrigger:hover,.doctrigger[aria-expanded="true"]{border-color:var(--or);color:var(--or)}
.doctrigger:focus-visible{outline:2px solid var(--or);outline-offset:2px}
/* Rückmeldung von Teilen" (putOnServer, D76/D81): Petrol-Blitz samt Haken
am Dokumentnamen er ist das, was jetzt auf dem Server liegt. */
.doctrigger.done{border-color:var(--or);color:var(--or)}
.doctrigger.done::before{content:"✓ ";flex:0 0 auto}
.docname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;max-width:min(44vw,520px)}
.doc-caret{width:13px;height:13px;flex:0 0 auto;opacity:.7}
/* Das Dokumenten-Menü hängt am Chip im App-Kopf. Es scrollt als GANZES
(max-height), statt nur in der Liste auf dem Telefon wurde das alte
Menü unten abgeschnitten (D81); erwogen waren auf-/zuklappbare Gruppen,
das Scrollen ist die zustandslose Antwort. Kein `overflow` am Vorfahren
nötig der App-Kopf ist keiner (D50 gilt hier nicht). */
.docmenu{
position:absolute;top:calc(100% + 6px);left:0;z-index:60;
width:min(360px,calc(100vw - 24px));max-height:min(72vh,560px);overflow:auto;
background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:10px;
padding:6px;box-shadow:0 8px 24px rgba(36,52,71,.22);
font-family:'IBM Plex Sans',sans-serif;font-weight:400;letter-spacing:0;
text-align:left;color:var(--ink);
}
.docmenu[hidden]{display:none}
.docmenu-list{display:flex;flex-direction:column;gap:2px}
/* Gruppen nach Dokumentart (docKind, D81): Mitgeliefert · Eigene · Quellen. */
.docsec{
margin:8px 4px 2px;padding-top:7px;border-top:1px solid rgba(36,52,71,.1);
font-family:'IBM Plex Mono',monospace;font-size:.62rem;letter-spacing:.1em;
text-transform:uppercase;color:var(--muted);
}
.docmenu-list .docsec:first-child{margin-top:2px;padding-top:0;border-top:0}
/* Eine Zeile = Wählen-Knopf + Zeilen-Aktionen. Kein Knopf im Knopf
(ungültiges HTML) die Zeile ist ein div, beides sind Geschwister. */
.docitem{display:flex;align-items:center;gap:2px;border-radius:7px}
.docpick{
display:flex;align-items:center;gap:8px;flex:1 1 auto;min-width:0;
border:0;background:transparent;cursor:pointer;text-align:left;
padding:7px 9px;border-radius:7px;color:var(--ink);
font-family:inherit;font-size:.86rem;
}
.docpick:hover{background:rgba(15,118,110,.08)}
.docpick[aria-checked="true"]{color:var(--or);font-weight:600}
.docpick .doccheck{flex:0 0 auto;width:14px;text-align:center;opacity:0}
.docpick[aria-checked="true"] .doccheck{opacity:1}
.docitem-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.docitem.editing{cursor:default;padding:0 0 0 9px;gap:8px}
.docrename{
flex:1 1 auto;min-width:0;
border:1px solid var(--or);border-radius:6px;
padding:5px 7px;margin:2px 4px 2px 0;color:var(--ink);background:var(--card);
font-family:inherit;font-size:.86rem;outline:none;
}
/* Zeilen-Aktionen immer sichtbar (Touch kennt kein Hover), aber leise. */
.docacts{display:inline-flex;gap:2px;flex:0 0 auto;padding-right:4px}
.dociconbtn{
display:grid;place-items:center;width:26px;height:26px;padding:0;
border:0;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;
}
.dociconbtn svg{width:14px;height:14px;display:block}
.dociconbtn:hover{background:rgba(36,52,71,.08);color:var(--ink)}
.docdelbtn:hover{background:rgba(216,151,168,.28);color:#B4415E}
/* Befehle unten: gewöhnliche Menüzeilen statt eines Knopf-Wusts. */
.docmenu-cmds{
display:flex;flex-direction:column;gap:2px;margin-top:6px;padding-top:6px;
border-top:1px solid rgba(36,52,71,.12);
}
.doccmd{
display:flex;align-items:center;gap:8px;width:100%;
border:0;background:transparent;cursor:pointer;text-align:left;
padding:7px 9px;border-radius:7px;color:var(--ink);
font-family:inherit;font-size:.86rem;
}
.doccmd:hover{background:rgba(15,118,110,.08)}
.doccmd svg{width:14px;height:14px;flex:0 0 auto;color:var(--muted)}
.doccmd-glyph{width:14px;text-align:center;color:var(--muted);font-weight:600;flex:0 0 auto}
.fsbtn{
display:inline-flex;align-items:center;justify-content:center;
width:30px;height:29px;padding:0;cursor:pointer;
@@ -155,11 +244,10 @@
body.fullscreen header,
body.fullscreen .app,
body.fullscreen .site-footer{max-width:none}
h1{font-size:1.25rem;font-weight:600;letter-spacing:-.01em}
h1{font-size:1.25rem;font-weight:600;letter-spacing:-.01em;display:flex;align-items:center;gap:11px}
/* Build-Hinweis hinter dem Titel (nur nicht-produktive Builds, siehe app.js) */
.build-badge{margin-left:-.5em;font-size:.58em;line-height:1;vertical-align:middle;cursor:help;user-select:none;text-decoration:none}
header p{margin-top:4px;color:var(--muted);font-size:.9rem}
.sub-short{display:none} /* Kurzfassung nur auf kleinem Bildschirm */
.langsel{
display:inline-flex;flex:0 0 auto;
border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden;
@@ -444,64 +532,21 @@
position:absolute!important;left:-10000px!important;top:0!important;
width:360px!important;height:400px!important;
}
/* Dokument-Wähler: der Name des aktiven Dokuments in der Editor-Titelzeile ist
zugleich der Auslöser eines Dropdowns zum Wechseln/Anlegen/Umbenennen/Löschen
(localStorage-Dokumente, noch kein Backend). */
/* Wähler + Snapshot-Knopf als eine Gruppe: Sie trägt das `margin-right:auto`
(schiebt die übrigen Knöpfe nach rechts) und ist der Positionsbezug des
Snapshot-Menüs dadurch klappt es unter seinem Knopf auf, ohne dass JS
rechnen muss. Am Wähler selbst darf das auto-Margin nicht mehr stehen,
sonst schöbe es den Snapshot-Knopf von ihm weg. */
.docgroup{
display:inline-flex;align-items:center;gap:4px;
margin-right:auto;max-width:62%;min-width:0;
}
/* Stand-Gruppe (D81): Speichern, Kamera, frühere Stände, Neu laden, Teilen
die Funktionen des AKTIVEN Dokuments, links neben der Beschriftung. Die
Gruppe trägt das `margin-right:auto` (schiebt die übrigen Knöpfe nach
rechts) und ist über `.snapwrap` der Positionsbezug des Snapshot-Menüs.
Der Dokument-Wähler selbst steht seit D81 als Brotkrume im App-Kopf. */
.standgroup{display:inline-flex;align-items:center;gap:4px;margin-right:auto}
/* Die Beschriftung Text-Editor" bekommt KEIN auto-Margin (die allgemeine
`.panel-head>span`-Regel gäbe ihr eins) den Platz nimmt die Stand-Gruppe. */
.panel-head>span[data-i18n="editorTitle"]{margin-right:0}
/* Bezug des Menüs ist der KNOPF, nicht die Gruppe: An der Gruppe ausgerichtet
(`right:0`) begänne es 190 px weiter links und liefe aus dem Panel heraus
nachgemessen bei der ersten Fassung. */
.snapwrap{position:relative;display:inline-flex;flex:0 0 auto}
.doctrigger{
display:inline-flex;align-items:center;gap:5px;cursor:pointer;
min-width:0;color:var(--muted);
padding:2px 6px;margin-left:-6px;border-radius:6px;
text-transform:none;letter-spacing:0;font-family:'IBM Plex Mono',monospace;
}
.doctrigger:hover,.doctrigger[aria-expanded="true"]{color:var(--or)}
.doctrigger:focus-visible{outline:2px solid var(--or);outline-offset:2px}
/* Rückmeldung fürs stille In-Place-Speichern (Strg+S, D74): Petrol-Blitz
samt Haken am Dokumentnamen das Haus-Idiom von flashBtn (D54). */
.doctrigger.done{color:var(--or)}
.doctrigger.done::before{content:"✓ ";flex:0 0 auto}
.docname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.doc-caret{width:13px;height:13px;flex:0 0 auto;opacity:.7}
.panel.collapsed .doc-caret{display:none}
.docmenu{
position:absolute;top:calc(100% + 4px);left:12px;z-index:50;min-width:210px;
background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:10px;
padding:6px;box-shadow:0 8px 24px rgba(36,52,71,.22);
text-transform:none;letter-spacing:0;font-family:'IBM Plex Sans',sans-serif;
}
.docmenu[hidden]{display:none}
.docmenu-list{display:flex;flex-direction:column;gap:2px;max-height:260px;overflow:auto}
.docitem{
display:flex;align-items:center;gap:8px;width:100%;
border:0;background:transparent;cursor:pointer;text-align:left;
padding:7px 9px;border-radius:7px;color:var(--ink);
font-family:inherit;font-size:.86rem;
}
.docitem:hover{background:rgba(15,118,110,.08)}
.docitem[aria-checked="true"]{color:var(--or);font-weight:600}
.docitem .doccheck{flex:0 0 auto;width:14px;text-align:center;opacity:0}
.docitem[aria-checked="true"] .doccheck{opacity:1}
.docitem.editing{cursor:default}
.docrename{
flex:1 1 auto;min-width:0;
border:1px solid var(--or);border-radius:6px;
padding:5px 7px;color:var(--ink);background:var(--card);
font-family:inherit;font-size:.86rem;outline:none;
}
/* Snapshot-Menü (D54): wie das Dokument-Menü, aber am rechten Rand seiner
Gruppe ausgerichtet also unter dem Knopf. */
/* Snapshot-Menü (D54): am linken Rand seiner Gruppe ausgerichtet also
unter dem Knopf. */
.snapbtn{flex:0 0 auto}
.snapmenu{
position:absolute;top:calc(100% + 6px);left:0;z-index:50;min-width:190px;
@@ -521,21 +566,6 @@
.snapitem:hover{background:rgba(15,118,110,.08)}
.snapitem .snapsize{margin-left:auto;color:var(--muted);font-size:.74rem}
.snapempty{padding:7px 9px;color:var(--muted);font-size:.8rem}
.docmenu-actions{
display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;padding-top:6px;
border-top:1px solid rgba(36,52,71,.12);
}
.docact{
flex:1 1 auto;border:1px solid rgba(36,52,71,.2);border-radius:7px;
background:var(--card);color:var(--muted);cursor:pointer;
padding:5px 8px;font-family:inherit;font-size:.78rem;white-space:nowrap;
}
.docact:hover{border-color:var(--or);color:var(--or)}
/* Original wiederherstellen" (D22-Nachtrag): sichtbar, sobald ein
mitgeliefertes Dokument aktiv ist; ausgegraut, solange es unverändert ist. */
.docact:disabled{opacity:.45;cursor:default}
.docact:disabled:hover{border-color:rgba(36,52,71,.2);color:var(--muted)}
.docdel:hover{border-color:#D897A8;color:#B4415E}
.togglebtn[aria-pressed="true"]{background:var(--or);color:#fff;border-color:var(--or)}
.togglebtn[aria-pressed="true"]:hover{color:#fff}
/* Kein Zeilenumbruch (D49, `wrap="off"` am Element): Die Einrückung trägt die
@@ -1364,7 +1394,7 @@
body.mobile .panel.right .panel-head>span[data-i18n="diagramTitle"]{display:none}
/* Umschalter oben links; er übernimmt vom entfallenen Titel das
`margin-right:auto`, damit die Aktions-Knöpfe rechts stehen bleiben. Im
Editor-Kopf tut das weiterhin der Dokumenten-Wähler daneben. */
Editor-Kopf tut das die Stand-Gruppe daneben (D81). */
body.mobile .panetoggle{display:inline-flex}
body.mobile .panel.right .panetoggle{margin-right:auto}
/* Die Titelzeile muss EINZEILIG bleiben (D17): Bricht sie um, wächst der
@@ -1409,10 +1439,39 @@
body.mobile textarea,
body.mobile .lineno{font-size:.64rem}
body.mobile textarea{padding:10px 12px}
/* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */
body.mobile .sub-full{display:none}
body.mobile .sub-short{display:inline}
body.mobile header p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Der Dokumentname übernimmt die Untertitel-Zeile (D81): Die Brotkrume
bricht unter die Marke um und bekommt dort die volle Breite lange
URL-Namen brauchen sie. Der Untertitel entfällt; es kommt keine Fläche
hinzu. Das " ohne „Werkbaum" davor sagte nichts und entfällt mit. */
body.mobile header p{display:none}
/* Die Marken-Gruppe löst sich auf (display:contents): Sie ist nur so breit,
wie die Werkzeuge rechts ihr lassen (~170 px) Chip-Zeile und Menü
brauchen aber die volle Kopfbreite. Als direkte Kinder der Kopfzeile
bricht der Chip per order:3 unter Marke und Werkzeuge um, und das Menü
nimmt die Kopfzeile als Bezug (dieselbe Verlegung wie beim
Neuigkeiten-Popup, D58). */
body.mobile .brandgroup{display:contents}
body.mobile .crumb-sep{display:none}
/* Marke und Werkzeuge müssen sich EINE Zeile teilen (sonst wächst der Kopf
auf drei Zeilen neue Fläche, die es nicht geben darf). Wie in den
Titelzeilen (D17-Nachtrag 5) weichen die Lücken, nicht die Knöpfe:
gemessen 164 px Marke + 164 px Werkzeuge bei 335 px Breite. */
body.mobile .header-row{position:relative;flex-wrap:wrap;row-gap:4px;column-gap:8px}
body.mobile .header-tools{gap:6px}
/* Nachgemessen mit Neuigkeiten-Zähler UND Build-Hinweis (die beiden
Gelegenheits-Breiten, D17-Lektion: mit dem versteckten Element messen):
ohne diese zwei Handgriffe stand die Summe exakt auf der Kante. Die
Sprachpille bleibt über 29 px Fingergröße. */
body.mobile .langsel button{padding:5px 8px}
body.mobile header h1{gap:8px}
body.mobile .docwrap{flex:1 1 100%;order:3;position:static}
body.mobile .doctrigger{max-width:100%}
body.mobile .docname{max-width:none}
body.mobile .docmenu{left:0;right:0;width:auto}
/* Die Editor-Beschriftung entfällt wie der Bereichstitel DIAGRAMM" die
Zeile wird für Umschalter und Stand-Knöpfe gebraucht; das auto-Margin
trägt die Stand-Gruppe. */
body.mobile .panel.editor .panel-head>span[data-i18n="editorTitle"]{display:none}
/* Editor immer gestapelt (Textarea oben, Legende darunter) */
body.mobile .editor-body{flex-direction:column;align-items:stretch}
body.mobile .agenda{border-left:0;border-top:1px dashed rgba(36,52,71,.18)}
@@ -1526,4 +1585,4 @@
margin-left: auto; padding-left: 10px;
font-size: .82em; color: var(--muted); white-space: nowrap;
}
.docitem[aria-checked="true"] .docitem-where{ color: inherit; opacity: .75; }
.docpick[aria-checked="true"] .docitem-where{ color: inherit; opacity: .75; }
+25 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { docParam, docSearch, LIVE_PARAM, SOURCE_PARAM } from '../src/docurl.js';
import { docParam, docSearch, docKind, LIVE_PARAM, SOURCE_PARAM } from '../src/docurl.js';
const LIVE = 'live:https://werkbaum.example/api/v1/documents/44753df1';
const URLDOC = 'url:https://example.org/plan.werkbaum';
@@ -80,3 +80,27 @@ describe('docSearch — die Adresszeile folgt dem aktiven Dokument', () => {
expect(docSearch('', 'example')).toBe('');
});
});
describe('docKind — welche Gruppe des Dokumenten-Menüs? (D81)', () => {
const SHIPPED = ['example', 'werkbaum'];
it('mitgelieferte Dokumente erkennt die feste id', () => {
expect(docKind('example', SHIPPED)).toBe('shipped');
expect(docKind('werkbaum', SHIPPED)).toBe('shipped');
});
it('Server- und URL-Dokumente sind Quellen von außen', () => {
expect(docKind(LIVE, SHIPPED)).toBe('server');
expect(docKind(URLDOC, SHIPPED)).toBe('url');
});
it('alles andere ist ein eigenes Dokument — auch aus Dateien geöffnete', () => {
expect(docKind('k3f9x1', SHIPPED)).toBe('own');
expect(docKind('Unbenannt', SHIPPED)).toBe('own');
});
it('verträgt fehlende Argumente, ohne zu werfen', () => {
expect(docKind(null, SHIPPED)).toBe('own');
expect(docKind('example')).toBe('own'); /* ohne Liste ist nichts mitgeliefert */
});
});