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