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:
co-authored by
Claude Fable 5
parent
fdca934cf0
commit
4be6ecb984
+18
-6
@@ -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
@@ -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- & 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
@@ -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 » — l’arbre de l’organigramme 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 l’original",
|
||||
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
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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 */
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user