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> 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 Angelegt wird so ein Dokument über den **Teilen-Knopf** in der Titelzeile des
Dokumenten-Menü: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt 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 — den Link in die Adresszeile und in die Zwischenablage. Diesen Link weitergeben —
die Adresse ist die Einladung, und wer sie kennt, kommt hinein. 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> 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 To create such a document, press the **Share** button in the text editor's
server"**: it uploads the active plan, switches to it, and puts the link in the 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 address bar and on your clipboard. Share that link — the address is the
invitation, and knowing it is what grants access. invitation, and knowing it is what grants access.
+3
View File
@@ -19,6 +19,9 @@ reverse.
## 2026-08-26 ## 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 - 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 - 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 - 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 `tests/docurl.test.js`; Gegenproben: fremde Parameter mitwerfen → genau die
zwei danach benannten Zusicherungen fallen, `encodeURIComponent` statt der zwei danach benannten Zusicherungen fallen, `encodeURIComponent` statt der
sparsamen Maskierung → genau die sieben, die die Lesbarkeit festhalten. 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 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 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 (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 weder Splitter noch Min/Normal/Max-Buttons — es ist nichts zu teilen. Die
Bereichstitel „DIAGRAMM" entfällt; die Zeile wird für Umschalter und Aktionen Bereichstitel („DIAGRAMM", „TEXT-EDITOR") entfallen; die Zeile wird für
gebraucht. Der **Dokumenten-Wähler** steht direkt neben dem Umschalter und ist Umschalter und Aktionen gebraucht. Der **Dokumenten-Wähler** steht als
damit genau dann zu sehen, wenn der Text vorn ist. Die übrigen 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 **Aktions-Knöpfe** bleiben rechts in der Titelzeile und werden mit
umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen
komprimierten Form. Der sichtbare Bereich wird gemerkt; ein Sprung zwischen 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.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.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: 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.url: Load a document from ?sourceUrl= (S)
- [^] #ed.docs.restore: Restore a shipped document from the menu (XS) - [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
- [^] #ed.jump: Jump between diagram and text (S) - [^] #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.crdt: Text CRDT — the plan is plain text, so it fits (L)
| [?] #col.live.ot: Operational transformation (XL) | [?] #col.live.ot: Operational transformation (XL)
- [^] #col.live.ask: On a real overlap, ask whose version wins (S) - [^] #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.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.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 - [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent
@@ -531,8 +531,10 @@
without changing anything. without changing anything.
#ed.docs.picker #ed.docs.picker
The name of the active document replaces the panel title and opens the menu The app header reads "Werkbaum > name": the chip opens the menu, grouped by
for switching, creating, renaming and deleting. 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 #ed.docs.url
A link can carry the address of a text file, which is then fetched on every A link can carry the address of a text file, which is then fetched on every
@@ -901,10 +903,10 @@
for the same result. for the same result.
#col.live.upload #col.live.upload
Without it the only way onto the server was a curl command. The local plan Without it the only way onto the server was a curl command. A share button
stays where it is: whoever entrusts their only copy to a server should not in the editor title bar; the local plan stays where it is: whoever entrusts
lose it in the act. The picker names the host beside a server document, so their only copy to a server should not lose it in the act. The picker names
the two entries are told apart. the host beside a server document, so the two entries are told apart.
#col.live.cursors #col.live.cursors
Seeing where the others are. A by-product of the merge layer rather than a 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 **Schattenkopie**, `scheduleLivePush()` schickt nach 0,6 s Ruhe das Diff
(D79 — die Wartezeit IST die gefuehlte Verzoegerung), (D79 — die Wartezeit IST die gefuehlte Verzoegerung),
`runFeed()` hält einen Abruf offen (nur im sichtbaren Tab), `putOnServer()` `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 bestimmt `serverBase()` (live.js): `?server=` vor dem offenen Dokument vor
der eigenen Herkunft — die Vorgabe stimmt damit ohne Konfiguration, weil das der eigenen Herkunft — die Vorgabe stimmt damit ohne Konfiguration, weil das
Backend produktiv hinter derselben Domain liegt. Zwei Fallen, beide 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 fehlendem `werkbaum-docs` den bestehenden `werkbaum-src` (oder `INITIAL`) in
**ein** Dokument; `initDocs()` (Aufruf **nach** `applyLang`) holt den aktiven **ein** Dokument; `initDocs()` (Aufruf **nach** `applyLang`) holt den aktiven
Text in den Editor. `saveSrc()` schreibt den Editortext ins aktive Dokument. Text in den Editor. `saveSrc()` schreibt den Editortext ins aktive Dokument.
Der Wähler ist ein Dropdown in der Editor-Titelzeile (`#docTrigger`/`#docMenu`, Der Wähler ist eine **Brotkrume im App-Kopf** (`#docTrigger`/`#docMenu`,
ersetzt die frühere feste „Struktur (Text)"-Beschriftung); Wechseln/Neu/ „Werkbaum Name", D81); das Menü gruppiert nach Dokumentart (`docKind()` in
Umbenennen/Löschen in `switchDoc/newDoc/renameDoc/deleteDoc`. Jedes Dokument ist 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 nur Text + Name (kein Strukturformat, D14) — vorwärtskompatibel zum Backend
(D13). Ansichts-State (`werkbaum-ui`) bleibt global über alle Dokumente. Ein (D13). Ansichts-State (`werkbaum-ui`) bleibt global über alle Dokumente. Ein
leerer Editortext bleibt leer. 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 `docs/CHANGELOG.md` geschieht für den Benutzer unsichtbar (Regel in der
Wurzel-CLAUDE.md). 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 `.dlmenu` und das Snapshot-Menü hängen als absolut positionierte Kinder im
`.panel-head`. Ein `overflow` dort macht ihn zum Scroll-Container und klippt `.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`, 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 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 Scroll-Container klippt unabhängig davon, ob er überläuft, der Fehler trifft
+59 -30
View File
@@ -18,7 +18,13 @@
<header> <header>
<div class="header-row"> <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"> <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"/> <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"/> <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> </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> <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>
<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"> <div class="header-tools">
<!-- Neuigkeiten (D58): der Stern aus dem Diagramm-Kopf, jetzt permanent. <!-- Neuigkeiten (D58): der Stern aus dem Diagramm-Kopf, jetzt permanent.
Sein Popup zeigt die Änderungen der letzten Tage (aus der git-Historie, Sein Popup zeigt die Änderungen der letzten Tage (aus der git-Historie,
@@ -58,43 +83,29 @@
</div> </div>
</div> </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> </header>
<div class="app" id="app"> <div class="app" id="app">
<div class="panel editor"> <div class="panel editor">
<div class="panel-head"> <div class="panel-head">
<!-- Gegenstück zum Knopf im Diagramm-Kopf (D17-Nachtrag). Der <!-- Gegenstück zum Knopf im Diagramm-Kopf (D17-Nachtrag). -->
Dokumenten-Wähler steht direkt daneben und ist damit genau dann zu
sehen, wenn der Text vorn ist — er gehört zu diesem Bereich. -->
<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"> <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> <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> </button>
<!-- Dokument-Wähler und Snapshot-Knopf stehen zusammen; die Gruppe trägt <!-- Die Beschriftung ist zurück (D81) — der Dokument-Wähler steht jetzt
das `margin-right:auto` und ist der Bezug für das Snapshot-Menü. --> als Brotkrume im App-Kopf. -->
<span class="docgroup"> <span data-i18n="editorTitle">Text-Editor</span>
<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"> <!-- Stand-Funktionen des AKTIVEN Dokuments (D81): Speichern, Stand jetzt
<span class="docname" id="docName"></span> sichern, frühere Stände, aus der Quelle neu laden — dazu Teilen. Die
<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> Gruppe trägt das `margin-right:auto` und ist der Bezug für das
</span> Snapshot-Menü. -->
<div class="docmenu" id="docMenu" role="menu" data-i18n-aria="docMenuAria" aria-label="Dokumente" hidden> <span class="standgroup">
<div class="docmenu-list" id="docList"></div> <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)">
<div class="docmenu-actions"> <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>
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button> <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 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>
</button> </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 <!-- Von Hand sichern, z. B. vor einer größeren Änderung. Kamera statt
Plus: ein Plus neben dem Dokument-Namen läse sich als „neues Plus: ein Plus neben dem Dokument-Namen läse sich als „neues
Dokument“ (D54-Nachtrag). --> 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-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> <svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button> </button>
<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> </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"> <button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <svg class="ic-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 einen waagerechten Balken (D49). Die Einrückung trägt hier die
Hierarchie — ein weicher Umbruch setzt die Fortsetzung an den linken Hierarchie — ein weicher Umbruch setzt die Fortsetzung an den linken
Rand und lässt die Ebene verschwinden. --> 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>
<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="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"> <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 } import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
from './snapshots.js'; from './snapshots.js';
import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.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 /* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
vite.config.js). Zur Laufzeit gibt es kein git und keinen Server, der vite.config.js). Zur Laufzeit gibt es kein git und keinen Server, der
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */ 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', render);
src.addEventListener('input', saveSrc); src.addEventListener('input', saveSrc);
src.addEventListener('input', scheduleLivePush); /* Server-Dokument: Diff nach 1,5 s Ruhe (D76) */ 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) ---------- /* ---------- Sprung zwischen Diagramm und Text (D25) ----------
Jeder Knoten trägt seine Zeilennummer als `data-line` (render.js). Jeder Knoten trägt seine Zeilennummer als `data-line` (render.js).
@@ -2202,7 +2207,6 @@ document.addEventListener('click', e => {
const I18N = { const I18N = {
de: { de: {
subtitle:"Werkbaum PSP / Lean Pathfinding · Editor für Projektstrukturpläne (auch Feature-Tree- & Requirements-Editor)", subtitle:"Werkbaum PSP / Lean Pathfinding · Editor für Projektstrukturpläne (auch Feature-Tree- & Requirements-Editor)",
subtitleShort:"PSP-Editor mit Lean Pathfinding",
imprint:"Impressum", imprint:"Impressum",
privacy:"Datenschutz", privacy:"Datenschutz",
legendTooltip:"Legende ein-/ausblenden", legendTooltip:"Legende ein-/ausblenden",
@@ -2232,7 +2236,7 @@ const I18N = {
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung mindestens {size} angenommen", implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung mindestens {size} angenommen",
fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite", fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite",
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).", 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", docSwitchTooltip:"Dokument wählen oder verwalten", docMenuAria:"Dokumente",
/* Frühere Stände (D54) — alle 10 Minuten, nur bei Änderung. */ /* Frühere Stände (D54) — alle 10 Minuten, nur bei Änderung. */
snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände", snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände",
@@ -2245,8 +2249,11 @@ const I18N = {
docDeleteConfirm:"Dokument „{name}“ löschen?", docDeleteConfirm:"Dokument „{name}“ löschen?",
docRestore:"Original wiederherstellen", docRestore:"Original wiederherstellen",
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.", docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern", docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern (Strg+S)",
docToServer:"Auf den Server legen", 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):", docToServerAsk:"Adresse des Werkbaum-Servers (z. B. https://werkbaum.example):",
docToServerDone:"Auf dem Server — der Link steht in der Adresszeile und in der Zwischenablage.", docToServerDone:"Auf dem Server — der Link steht in der Adresszeile und in der Zwischenablage.",
docToServerFailed:"Nicht auf den Server gelegt: {error}", docToServerFailed:"Nicht auf den Server gelegt: {error}",
@@ -2315,7 +2322,6 @@ const I18N = {
}, },
en: { en: {
subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)", subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)",
subtitleShort:"WBS editor with Lean Pathfinding",
imprint:"Imprint (Impressum)", imprint:"Imprint (Impressum)",
privacy:"Privacy", privacy:"Privacy",
legendTooltip:"Show/hide legend", legendTooltip:"Show/hide legend",
@@ -2344,7 +2350,7 @@ const I18N = {
implicitSizeTooltip:"No size given assumed at least {size} for the cost estimate", implicitSizeTooltip:"No size given assumed at least {size} for the cost estimate",
fullscreenTooltip:"Full screen panels use the full window width", fullscreenTooltip:"Full screen panels use the full window width",
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).", 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", docSwitchTooltip:"Choose or manage document", docMenuAria:"Documents",
snapTooltip:"Earlier states of this document", snapMenuAria:"Earlier states", 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.", 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}”?", docDeleteConfirm:"Delete document “{name}”?",
docRestore:"Restore original", docRestore:"Restore original",
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.", docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
docOpenFile:"Open file…", docSaveFile:"Save as file", docOpenFile:"Open file…", docSaveFile:"Save as file (Ctrl+S)",
docToServer:"Put on the server", 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):", 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.", docToServerDone:"On the server — the link is in the address bar and on the clipboard.",
docToServerFailed:"Not put on the server: {error}", docToServerFailed:"Not put on the server: {error}",
@@ -2426,7 +2435,6 @@ const I18N = {
}, },
es: { es: {
subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)", 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)", imprint:"Aviso legal (Impressum)",
privacy:"Privacidad", privacy:"Privacidad",
legendTooltip:"Mostrar u ocultar la leyenda", 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", 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", 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).", 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", docSwitchTooltip:"Elegir o gestionar documento", docMenuAria:"Documentos",
snapTooltip:"Estados anteriores de este documento", snapMenuAria:"Estados anteriores", 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.", 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}»?", docDeleteConfirm:"¿Eliminar el documento «{name}»?",
docRestore:"Restaurar original", docRestore:"Restaurar original",
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.", docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo", docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo (Ctrl+S)",
docToServer:"Poner en el servidor", 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):", 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.", 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}", docToServerFailed:"No se pudo poner en el servidor: {error}",
@@ -2537,7 +2548,6 @@ const I18N = {
}, },
fr: { fr: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)", 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)", imprint:"Mentions légales (Impressum)",
privacy:"Confidentialité", privacy:"Confidentialité",
legendTooltip:"Afficher/masquer la légende", 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", 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", 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).", 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", docSwitchTooltip:"Choisir ou gérer le document", docMenuAria:"Documents",
snapTooltip:"États antérieurs de ce document", snapMenuAria:"États antérieurs", 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é.", 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} » ?", docDeleteConfirm:"Supprimer le document « {name} » ?",
docRestore:"Restaurer loriginal", docRestore:"Restaurer loriginal",
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.", docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier", docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier (Ctrl+S)",
docToServer:"Mettre sur le serveur", 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) :", 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.", 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}", docToServerFailed:"Non déposé sur le serveur : {error}",
@@ -2648,7 +2661,6 @@ const I18N = {
}, },
pl: { pl: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)", 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)", imprint:"Nota prawna (Impressum)",
privacy:"Prywatność", privacy:"Prywatność",
legendTooltip:"Pokaż/ukryj legendę", legendTooltip:"Pokaż/ukryj legendę",
@@ -2677,7 +2689,7 @@ const I18N = {
implicitSizeTooltip:"Nie podano rozmiaru do szacowania kosztów przyjęto co najmniej {size}", implicitSizeTooltip:"Nie podano rozmiaru do szacowania kosztów przyjęto co najmniej {size}",
fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna", fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna",
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).", 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", docSwitchTooltip:"Wybierz lub zarządzaj dokumentem", docMenuAria:"Dokumenty",
snapTooltip:"Wcześniejsze stany tego dokumentu", snapMenuAria:"Wcześniejsze stany", 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.", 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}”?", docDeleteConfirm:"Usunąć dokument „{name}”?",
docRestore:"Przywróć oryginał", docRestore:"Przywróć oryginał",
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.", docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik", docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik (Ctrl+S)",
docToServer:"Umieść na serwerze", 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):", docToServerAsk:"Adres serwera Werkbaum (np. https://werkbaum.example):",
docToServerDone:"Na serwerze — link jest w pasku adresu i w schowku.", docToServerDone:"Na serwerze — link jest w pasku adresu i w schowku.",
docToServerFailed:"Nie umieszczono na serwerze: {error}", docToServerFailed:"Nie umieszczono na serwerze: {error}",
@@ -2759,7 +2774,6 @@ const I18N = {
}, },
ru: { ru: {
subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)", subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)",
subtitleShort:"Редактор СДР с Lean Pathfinding",
imprint:"Выходные данные (Impressum)", imprint:"Выходные данные (Impressum)",
privacy:"Конфиденциальность", privacy:"Конфиденциальность",
legendTooltip:"Показать/скрыть легенду", legendTooltip:"Показать/скрыть легенду",
@@ -2788,7 +2802,7 @@ const I18N = {
implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}", implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}",
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна", fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма", editorTitle:"Текстовый редактор", diagramTitle:"Диаграмма",
docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы", docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы",
snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния", snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния",
snapNone:"Прежних состояний пока нет — они сохраняются по нажатию кнопки и каждые 10 минут, если что-то изменилось.", snapNone:"Прежних состояний пока нет — они сохраняются по нажатию кнопки и каждые 10 минут, если что-то изменилось.",
@@ -2800,8 +2814,11 @@ const I18N = {
docDeleteConfirm:"Удалить документ «{name}»?", docDeleteConfirm:"Удалить документ «{name}»?",
docRestore:"Восстановить оригинал", docRestore:"Восстановить оригинал",
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.", docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл", docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл (Ctrl+S)",
docToServer:"Положить на сервер", docToServer:"Поделиться положить на сервер Werkbaum и редактировать вместе",
docReload:"Перезагрузить из источника – локальные изменения будут потеряны",
docReloadConfirm:"Перезагрузить «{name}» из источника? Локальные изменения будут потеряны.",
docGroupShipped:"Встроенные", docGroupOwn:"Свои", docGroupSources:"Источники",
docToServerAsk:"Адрес сервера Werkbaum (например, https://werkbaum.example):", docToServerAsk:"Адрес сервера Werkbaum (например, https://werkbaum.example):",
docToServerDone:"На сервере — ссылка в адресной строке и в буфере обмена.", docToServerDone:"На сервере — ссылка в адресной строке и в буфере обмена.",
docToServerFailed:"Не удалось положить на сервер: {error}", docToServerFailed:"Не удалось положить на сервер: {error}",
@@ -2870,7 +2887,6 @@ const I18N = {
}, },
hi: { hi: {
subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)", subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)",
subtitleShort:"WBS संपादक Lean Pathfinding के साथ",
imprint:"प्रकाशन विवरण (Impressum)", imprint:"प्रकाशन विवरण (Impressum)",
privacy:"गोपनीयता", privacy:"गोपनीयता",
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ", legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
@@ -2899,7 +2915,7 @@ const I18N = {
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया", implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया",
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं", fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख", editorTitle:"टेक्स्ट संपादक", diagramTitle:"आरेख",
docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़", docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़",
snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ", snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ",
snapNone:"अभी कोई पिछली स्थिति नहीं — बटन दबाने पर और कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।", snapNone:"अभी कोई पिछली स्थिति नहीं — बटन दबाने पर और कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।",
@@ -2911,8 +2927,11 @@ const I18N = {
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?", docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
docRestore:"मूल पुनर्स्थापित करें", docRestore:"मूल पुनर्स्थापित करें",
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।", docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें", docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें (Ctrl+S)",
docToServer:"सर्वर पर रखें", docToServer:"साझा करें – Werkbaum सर्वर पर रखें और साथ मिलकर संपादित करें",
docReload:"स्रोत से फिर लोड करें – स्थानीय बदलाव खो जाएँगे",
docReloadConfirm:"“{name}” को स्रोत से फिर लोड करें? स्थानीय बदलाव खो जाएँगे।",
docGroupShipped:"साथ आए", docGroupOwn:"आपके", docGroupSources:"स्रोत",
docToServerAsk:"Werkbaum सर्वर का पता (जैसे https://werkbaum.example):", docToServerAsk:"Werkbaum सर्वर का पता (जैसे https://werkbaum.example):",
docToServerDone:"सर्वर पर है — लिंक पता-पट्टी में और क्लिपबोर्ड में है।", docToServerDone:"सर्वर पर है — लिंक पता-पट्टी में और क्लिपबोर्ड में है।",
docToServerFailed:"सर्वर पर नहीं रखा जा सका: {error}", docToServerFailed:"सर्वर पर नहीं रखा जा सका: {error}",
@@ -2981,7 +3000,6 @@ const I18N = {
}, },
zh: { zh: {
subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)", subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)",
subtitleShort:"WBS 编辑器和 Lean Pathfinding",
imprint:"法律声明(Impressum", imprint:"法律声明(Impressum",
privacy:"隐私", privacy:"隐私",
legendTooltip:"显示/隐藏图例", legendTooltip:"显示/隐藏图例",
@@ -3010,7 +3028,7 @@ const I18N = {
liveConflictTheirs:"采用对方的", liveConflictTheirs:"采用对方的",
liveConflictMine:"保留我的", liveConflictMine:"保留我的",
riskTooltip:"高风险 – 工作量尚不明确。", riskTooltip:"高风险 – 工作量尚不明确。",
editorTitle:"结构(文本)", diagramTitle:"图表", editorTitle:"文本编辑器", diagramTitle:"图表",
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档", docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态", snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态",
snapNone:"暂无早期状态——可随时手动保存;内容有变动时,也会每 10 分钟保存一次。", snapNone:"暂无早期状态——可随时手动保存;内容有变动时,也会每 10 分钟保存一次。",
@@ -3022,8 +3040,11 @@ const I18N = {
docDeleteConfirm:"删除文档“{name}”?", docDeleteConfirm:"删除文档“{name}”?",
docRestore:"恢复原始版本", docRestore:"恢复原始版本",
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。", docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
docOpenFile:"打开文件…", docSaveFile:"另存为文件", docOpenFile:"打开文件…", docSaveFile:"另存为文件Ctrl+S",
docToServer:"放到服务器上", docToServer:"共享——放到 Werkbaum 服务器上共同编辑",
docReload:"从来源重新加载——本地更改将丢失",
docReloadConfirm:"从来源重新加载“{name}”?本地更改将丢失。",
docGroupShipped:"自带", docGroupOwn:"自己的", docGroupSources:"来源",
docToServerAsk:"Werkbaum 服务器地址(例如 https://werkbaum.example):", docToServerAsk:"Werkbaum 服务器地址(例如 https://werkbaum.example):",
docToServerDone:"已在服务器上——链接在地址栏和剪贴板中。", docToServerDone:"已在服务器上——链接在地址栏和剪贴板中。",
docToServerFailed:"未能放到服务器上:{error}", docToServerFailed:"未能放到服务器上:{error}",
@@ -3092,7 +3113,6 @@ const I18N = {
}, },
ja: { ja: {
subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)", subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)",
subtitleShort:"WBS エディター & Lean Pathfinding",
imprint:"運営者情報(Impressum", imprint:"運営者情報(Impressum",
privacy:"プライバシー", privacy:"プライバシー",
legendTooltip:"凡例を表示/非表示", legendTooltip:"凡例を表示/非表示",
@@ -3121,7 +3141,7 @@ const I18N = {
liveConflictTheirs:"相手の版", liveConflictTheirs:"相手の版",
liveConflictMine:"自分の版", liveConflictMine:"自分の版",
riskTooltip:"高リスク – 規模はまだ不明。", riskTooltip:"高リスク – 規模はまだ不明。",
editorTitle:"構造(テキスト", diagramTitle:"ダイアグラム", editorTitle:"テキストエディター", diagramTitle:"ダイアグラム",
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント", docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態", snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態",
snapNone:"以前の状態はまだありません — ボタンを押したとき、および変更があれば 10 分ごとに保存されます。", snapNone:"以前の状態はまだありません — ボタンを押したとき、および変更があれば 10 分ごとに保存されます。",
@@ -3133,8 +3153,11 @@ const I18N = {
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?", docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
docRestore:"オリジナルを復元", docRestore:"オリジナルを復元",
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。", docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存", docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存 (Ctrl+S)",
docToServer:"サーバーに置く", docToServer:"共有 – Werkbaum サーバーに置いて共同編集",
docReload:"ソースから再読み込み – ローカルの変更は失われます",
docReloadConfirm:"「{name}」をソースから再読み込みしますか?ローカルの変更は失われます。",
docGroupShipped:"同梱", docGroupOwn:"自分の", docGroupSources:"ソース",
docToServerAsk:"Werkbaum サーバーのアドレス(例: https://werkbaum.example:", docToServerAsk:"Werkbaum サーバーのアドレス(例: https://werkbaum.example:",
docToServerDone:"サーバーにあります — リンクはアドレス欄とクリップボードにあります。", docToServerDone:"サーバーにあります — リンクはアドレス欄とクリップボードにあります。",
docToServerFailed:"サーバーに置けませんでした: {error}", docToServerFailed:"サーバーに置けませんでした: {error}",
@@ -3268,6 +3291,9 @@ function applyLang(l){
offen, muss es die neue Sprache jetzt bekommen (auch die Datumsangaben, offen, muss es die neue Sprache jetzt bekommen (auch die Datumsangaben,
die `toLocaleDateString` liefert). */ die `toLocaleDateString` liefert). */
if(newsMenu && !newsMenu.hidden) renderNewsMenu(); 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(_){} try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
} }
const langsel = document.querySelector('.langsel'); const langsel = document.querySelector('.langsel');
@@ -3513,7 +3539,11 @@ function restoreState(){
restoring = false; 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 docTrigger = document.getElementById('docTrigger');
const docMenu = document.getElementById('docMenu'); const docMenu = document.getElementById('docMenu');
const docNameEl = document.getElementById('docName'); const docNameEl = document.getElementById('docName');
@@ -3529,6 +3559,7 @@ function updateDocName(){
? d.source + '\n' + t('docSwitchTooltip') ? d.source + '\n' + t('docSwitchTooltip')
: 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) */ let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */
/* Kam das Umbenennen von einem frisch angelegten Dokument? Dann geht es danach /* 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; if(!roh.startsWith('live:')) return null;
try{ return new URL(roh.slice(5)).host; }catch(_){ return null; } try{ return new URL(roh.slice(5)).host; }catch(_){ return null; }
} }
function renderDocMenu(){ /* Kleine Zeilen-Icons (Feather-Stil wie überall): Stift, Papierkorb,
docList.innerHTML = docs.map(d => { 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){ if(d.id === renamingId){
/* Inline-Umbenennen direkt im Menü (kein window.prompt das ist in /* Inline-Umbenennen direkt im Menü (kein window.prompt das ist in
manchen Browser-Kontexten unterdrückt). Wert/Handler unten in JS. */ manchen Browser-Kontexten unterdrückt). Wert/Handler unten in JS. */
@@ -3554,10 +3593,35 @@ function renderDocMenu(){
verriete den Unterschied. */ verriete den Unterschied. */
const host = serverHostOf(d.id); const host = serverHostOf(d.id);
const zusatz = host ? `<span class="docitem-where">${esc(host)}</span>` : ''; const zusatz = host ? `<span class="docitem-where">${esc(host)}</span>` : '';
return `<button type="button" class="docitem" role="menuitemradio" data-id="${d.id}" ` + const shipped = shippedStateOf(d.id);
`aria-checked="${d.id === activeId ? 'true' : 'false'}">` + 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="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(''); }).join('');
if(renamingId){ if(renamingId){
const inp = docList.querySelector('.docrename'); const inp = docList.querySelector('.docrename');
@@ -3584,42 +3648,92 @@ function shippedStateOf(id){
if(id === WERKBAUM_ID) return {name: WERKBAUM_NAME, text: WERKBAUM_DOC}; if(id === WERKBAUM_ID) return {name: WERKBAUM_NAME, text: WERKBAUM_DOC};
return null; 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 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 shipped = d && shippedStateOf(d.id);
const btn = document.getElementById('docRestore'); if(shipped){
/* Immer zeigen, wenn ein MITGELIEFERTES Dokument aktiv ist nur ausgegraut, neu.hidden = false;
solange es dem Auslieferungsstand entspricht. Ganz versteckt war der neu.disabled = d.text === shipped.text && d.name === shipped.name;
Eintrag nicht auffindbar (Nutzer: ich sehe keinen Reset-Button"). */ neu.title = t('docRestore');
btn.hidden = !shipped; neu.setAttribute('aria-label', t('docRestore'));
btn.disabled = !!shipped && d.text === shipped.text && d.name === shipped.name; } else if(d && (d.source || fileHandles.has(d.id))){
neu.hidden = false; neu.disabled = false;
/* Auf den Server legen" entfällt, wo es schon liegt. Sonst entstünde ein neu.title = t('docReload');
zweites, gleichnamiges Dokument auf demselben Server im Wähler nicht zu neu.setAttribute('aria-label', t('docReload'));
unterscheiden. Erkennbar an der id (`live:…`), nicht am laufenden } else {
`liveState`: Auch ein Server-Dokument, das gerade nicht das aktive ist, neu.hidden = true;
liegt bereits dort. */
const zumServer = document.getElementById('docToServer');
if(zumServer) zumServer.hidden = !!d && String(d.id).startsWith('live:');
} }
function restoreDoc(){ }
const d = activeDoc(); const speichern = document.getElementById('saveBtn');
if(speichern && d){
const h = fileHandles.get(d.id);
speichern.title = t('docSaveFile') + (h ? '\n' + h.name : '');
}
}
/* 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); const shipped = d && shippedStateOf(d.id);
if(!shipped) return; if(!shipped) return;
if(!window.confirm(t('docRestoreConfirm', {name: d.name}))) return; if(!window.confirm(t('docRestoreConfirm', {name: d.name}))) return;
d.text = shipped.text; d.text = shipped.text;
d.name = shipped.name; d.name = shipped.name;
if(d.id === activeId){
foldOverrides.clear(); foldOverrides.clear();
loadActiveIntoEditor(); loadActiveIntoEditor();
}
persistDocs(); 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(){ function openDocMenu(){
renderDocMenu(); updateRestoreBtn(); renderDocMenu();
/* 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 : '';
docMenu.hidden = false; docTrigger.setAttribute('aria-expanded', 'true'); docMenu.hidden = false; docTrigger.setAttribute('aria-expanded', 'true');
} }
function closeDocMenu(){ renamingId = null; renameIsNew = false; docMenu.hidden = true; docTrigger.setAttribute('aria-expanded', 'false'); } function closeDocMenu(){ renamingId = null; renameIsNew = false; docMenu.hidden = true; docTrigger.setAttribute('aria-expanded', 'false'); }
@@ -3975,9 +4089,9 @@ function finishNewDoc(){
closeDocMenu(); closeDocMenu();
src.focus(); src.focus();
} }
function renameDoc(){ function renameDoc(id){
if(!activeDoc()) return; if(!docs.some(x => x.id === id)) return;
renamingId = activeId; /* aktives Dokument inline umbenennen */ renamingId = id; /* Zeilen-Aktion (D81): jedes Dokument, nicht nur das aktive */
renameIsNew = false; renameIsNew = false;
renderDocMenu(); renderDocMenu();
} }
@@ -3992,8 +4106,8 @@ function commitRename(){
finishNewDoc(); finishNewDoc();
} }
function cancelRename(){ renamingId = null; renderDocMenu(); finishNewDoc(); } function cancelRename(){ renamingId = null; renderDocMenu(); finishNewDoc(); }
function deleteDoc(){ function deleteDoc(id){
const d = activeDoc(); const d = docs.find(x => x.id === id);
if(!d) return; if(!d) return;
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return; if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
if(liveState && liveState.id === d.id) stopLive(); /* dito fürs Server-Dokument (D76) */ 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); 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(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 }]; 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; activeId = docs[0].id;
foldOverrides.clear(); foldOverrides.clear();
loadActiveIntoEditor(); loadActiveIntoEditor();
}
persistDocs(); persistDocs();
closeDocMenu(); if(!docMenu.hidden) renderDocMenu();
} }
/* ---------- Lokale Dateien öffnen und speichern (D72, Stufe 1) ---------- /* ---------- Lokale Dateien öffnen und speichern (D72, Stufe 1) ----------
Der klassische Weg, der in jedem Browser läuft: Datei-Input zum Öffnen, Der klassische Weg, der in jedem Browser läuft: Datei-Input zum Öffnen,
@@ -4230,12 +4348,12 @@ async function saveLocalFile(){
} else { } else {
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name)); 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 /* Stilles In-Place-Speichern braucht eine sichtbare Antwort sonst wirkt
die Geste tot. Haus-Idiom flashBtn (Petrol-Blitz, D54), am Dokumentnamen: die Geste tot. Haus-Idiom flashBtn (Petrol-Blitz samt Haken, D54), am
Der ist das, was gespeichert wurde. Dialog und Download sind selbst Speichern-Knopf selbst: Er ist die Geste (auch für Strg+S, D74). Dialog
sichtbar und brauchen keinen. */ und Download sind selbst sichtbar und brauchen keinen. */
if(inPlace) flashBtn(docTrigger); if(inPlace) flashBtn(document.getElementById('saveBtn'));
} }
/* Strg+S / Cmd+S speichert als Datei (D74) die Geste, die jeder zuerst /* Strg+S / Cmd+S speichert als Datei (D74) die Geste, die jeder zuerst
versucht. Ohne preventDefault öffnet der Browser Seite speichern": genau versucht. Ohne preventDefault öffnet der Browser Seite speichern": genau
@@ -4771,7 +4889,6 @@ async function putOnServer(){
const basis = serverBaseOrAsk(); const basis = serverBaseOrAsk();
if(!basis) return; if(!basis) return;
closeDocMenu();
try{ try{
const doc = await fetchJson(live.documentsUrl(basis), { const doc = await fetchJson(live.documentsUrl(basis), {
method: 'POST', method: 'POST',
@@ -4804,9 +4921,6 @@ async function putOnServer(){
} }
docTrigger.addEventListener('click', e => { 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(); e.stopPropagation();
toggleDocMenu(); toggleDocMenu();
}); });
@@ -4814,22 +4928,39 @@ docTrigger.addEventListener('keydown', e => {
if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); toggleDocMenu(); } if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); toggleDocMenu(); }
else if(e.key === 'Escape') closeDocMenu(); 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 => { 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; if(!btn) return;
e.stopPropagation(); e.stopPropagation();
switchDoc(btn.dataset.id); switchDoc(btn.closest('.docitem').dataset.id);
closeDocMenu(); closeDocMenu();
}); });
/* `newDoc()` zeichnet das Menü selbst neu es geht direkt ins Umbenennen /* `newDoc()` zeichnet das Menü selbst neu es geht direkt ins Umbenennen
(D51), und ein zweiter Durchlauf baute das Eingabefeld nur noch einmal auf. */ (D51), und ein zweiter Durchlauf baute das Eingabefeld nur noch einmal auf. */
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); }); document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); });
document.getElementById('docOpenFile').addEventListener('click', e => { e.stopPropagation(); openLocalFile(); }); document.getElementById('docOpenFile').addEventListener('click', e => { e.stopPropagation(); openLocalFile(); });
document.getElementById('docSaveFile').addEventListener('click', e => { e.stopPropagation(); saveLocalFile(); }); /* Die Stand-Knöpfe der Editor-Titelzeile (D81). */
document.getElementById('docToServer').addEventListener('click', e => { e.stopPropagation(); putOnServer(); }); document.getElementById('saveBtn').addEventListener('click', saveLocalFile);
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); }); document.getElementById('shareBtn').addEventListener('click', putOnServer);
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); }); document.getElementById('reloadBtn').addEventListener('click', reloadDoc);
document.getElementById('docRestore').addEventListener('click', e => { e.stopPropagation(); restoreDoc(); });
/* Klick außerhalb schließt das Menü. */ /* Klick außerhalb schließt das Menü. */
document.addEventListener('click', e => { document.addEventListener('click', e => {
if(!docMenu.hidden && !docMenu.contains(e.target) && !docTrigger.contains(e.target)) closeDocMenu(); 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 */ applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */ initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
if(hasFsAccess) handlesReady = idbLoadHandles(); /* gemerkte Datei-Handles zurückholen (D72, Stufe 2) */ 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 /* PWA-Dateihandling (D73): Die installierte App registriert sich über das
Manifest (`file_handlers`) für .werkbaum/.txt; ein Doppelklick im Manifest (`file_handlers`) für .werkbaum/.txt; ein Doppelklick im
+14
View File
@@ -30,6 +30,20 @@ export function docParam(id){
return null; 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 /* Der neue Query-String zu `search` (mit oder ohne führendes `?`) für das
Dokument `id`. 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{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-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.header-tools{display:flex;align-items:center;gap:12px} .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{ .fsbtn{
display:inline-flex;align-items:center;justify-content:center; display:inline-flex;align-items:center;justify-content:center;
width:30px;height:29px;padding:0;cursor:pointer; width:30px;height:29px;padding:0;cursor:pointer;
@@ -155,11 +244,10 @@
body.fullscreen header, body.fullscreen header,
body.fullscreen .app, body.fullscreen .app,
body.fullscreen .site-footer{max-width:none} 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-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} .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} header p{margin-top:4px;color:var(--muted);font-size:.9rem}
.sub-short{display:none} /* Kurzfassung nur auf kleinem Bildschirm */
.langsel{ .langsel{
display:inline-flex;flex:0 0 auto; display:inline-flex;flex:0 0 auto;
border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden; 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; position:absolute!important;left:-10000px!important;top:0!important;
width:360px!important;height:400px!important; width:360px!important;height:400px!important;
} }
/* Dokument-Wähler: der Name des aktiven Dokuments in der Editor-Titelzeile ist /* Stand-Gruppe (D81): Speichern, Kamera, frühere Stände, Neu laden, Teilen
zugleich der Auslöser eines Dropdowns zum Wechseln/Anlegen/Umbenennen/Löschen die Funktionen des AKTIVEN Dokuments, links neben der Beschriftung. Die
(localStorage-Dokumente, noch kein Backend). */ Gruppe trägt das `margin-right:auto` (schiebt die übrigen Knöpfe nach
/* Wähler + Snapshot-Knopf als eine Gruppe: Sie trägt das `margin-right:auto` rechts) und ist über `.snapwrap` der Positionsbezug des Snapshot-Menüs.
(schiebt die übrigen Knöpfe nach rechts) und ist der Positionsbezug des Der Dokument-Wähler selbst steht seit D81 als Brotkrume im App-Kopf. */
Snapshot-Menüs dadurch klappt es unter seinem Knopf auf, ohne dass JS .standgroup{display:inline-flex;align-items:center;gap:4px;margin-right:auto}
rechnen muss. Am Wähler selbst darf das auto-Margin nicht mehr stehen, /* Die Beschriftung Text-Editor" bekommt KEIN auto-Margin (die allgemeine
sonst schöbe es den Snapshot-Knopf von ihm weg. */ `.panel-head>span`-Regel gäbe ihr eins) den Platz nimmt die Stand-Gruppe. */
.docgroup{ .panel-head>span[data-i18n="editorTitle"]{margin-right:0}
display:inline-flex;align-items:center;gap:4px;
margin-right:auto;max-width:62%;min-width:0;
}
/* Bezug des Menüs ist der KNOPF, nicht die Gruppe: An der Gruppe ausgerichtet /* 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 (`right:0`) begänne es 190 px weiter links und liefe aus dem Panel heraus
nachgemessen bei der ersten Fassung. */ nachgemessen bei der ersten Fassung. */
.snapwrap{position:relative;display:inline-flex;flex:0 0 auto} .snapwrap{position:relative;display:inline-flex;flex:0 0 auto}
.doctrigger{ /* Snapshot-Menü (D54): am linken Rand seiner Gruppe ausgerichtet also
display:inline-flex;align-items:center;gap:5px;cursor:pointer; unter dem Knopf. */
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. */
.snapbtn{flex:0 0 auto} .snapbtn{flex:0 0 auto}
.snapmenu{ .snapmenu{
position:absolute;top:calc(100% + 6px);left:0;z-index:50;min-width:190px; 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:hover{background:rgba(15,118,110,.08)}
.snapitem .snapsize{margin-left:auto;color:var(--muted);font-size:.74rem} .snapitem .snapsize{margin-left:auto;color:var(--muted);font-size:.74rem}
.snapempty{padding:7px 9px;color:var(--muted);font-size:.8rem} .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"]{background:var(--or);color:#fff;border-color:var(--or)}
.togglebtn[aria-pressed="true"]:hover{color:#fff} .togglebtn[aria-pressed="true"]:hover{color:#fff}
/* Kein Zeilenumbruch (D49, `wrap="off"` am Element): Die Einrückung trägt die /* 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} body.mobile .panel.right .panel-head>span[data-i18n="diagramTitle"]{display:none}
/* Umschalter oben links; er übernimmt vom entfallenen Titel das /* Umschalter oben links; er übernimmt vom entfallenen Titel das
`margin-right:auto`, damit die Aktions-Knöpfe rechts stehen bleiben. Im `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 .panetoggle{display:inline-flex}
body.mobile .panel.right .panetoggle{margin-right:auto} body.mobile .panel.right .panetoggle{margin-right:auto}
/* Die Titelzeile muss EINZEILIG bleiben (D17): Bricht sie um, wächst der /* Die Titelzeile muss EINZEILIG bleiben (D17): Bricht sie um, wächst der
@@ -1409,10 +1439,39 @@
body.mobile textarea, body.mobile textarea,
body.mobile .lineno{font-size:.64rem} body.mobile .lineno{font-size:.64rem}
body.mobile textarea{padding:10px 12px} body.mobile textarea{padding:10px 12px}
/* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */ /* Der Dokumentname übernimmt die Untertitel-Zeile (D81): Die Brotkrume
body.mobile .sub-full{display:none} bricht unter die Marke um und bekommt dort die volle Breite lange
body.mobile .sub-short{display:inline} URL-Namen brauchen sie. Der Untertitel entfällt; es kommt keine Fläche
body.mobile header p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 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) */ /* Editor immer gestapelt (Textarea oben, Legende darunter) */
body.mobile .editor-body{flex-direction:column;align-items:stretch} 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)} 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; margin-left: auto; padding-left: 10px;
font-size: .82em; color: var(--muted); white-space: nowrap; 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 { 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 LIVE = 'live:https://werkbaum.example/api/v1/documents/44753df1';
const URLDOC = 'url:https://example.org/plan.werkbaum'; const URLDOC = 'url:https://example.org/plan.werkbaum';
@@ -80,3 +80,27 @@ describe('docSearch — die Adresszeile folgt dem aktiven Dokument', () => {
expect(docSearch('', 'example')).toBe(''); 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 */
});
});