feat: Frühere Stände eines Dokuments, alle zehn Minuten (D54)

Alle 10 Minuten wird der Text des aktiven Dokuments weggelegt — aber nur,
wenn er sich seit dem letzten Stand geändert hat. Aufgehoben werden die
letzten 20 je Dokument (Nutzer-Entscheidung; rund 3,5 Stunden, max. ~800
kB beim größten Dokument). Angeboten werden sie über einen Knopf rechts
neben dem Dokumenten-Wähler, neueste zuoberst, je mit Uhrzeit und
Zeilenzahl.

„Nur bei Änderung" heißt: gegen den letzten Stand, und solange es keinen
gibt, gegen den Text beim Aktivieren des Dokuments. Ohne das legte der
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg.

Die Dokumente sind wichtiger als ihre Stände: Beide teilen sich den
localStorage, also wirft persistSnaps() bei Überlauf den jeweils
ältesten Stand weg, bis es passt, notfalls alle — statt das Speichern
der Dokumente zu gefährden. Ein gelöschtes Dokument nimmt seine Stände
mit. Pad-Dokumente (D31) sammeln keine: Ihr Feld ist schreibgeschützt,
ein alter Stand ließe sich dort nicht einsetzen.

Zurückgeholt wird undo-fähig (replaceTextUndoable, D53) — ein Griff
daneben kostet ein Strg+Z, keine Rückfrage. Vorher wird der aktuelle
Stand weggelegt, sonst wäre gerade er das Einzige, was das Zurückholen
verlöre.

Beim Bauen korrigiert: Das Menü war an der Gruppe ausgerichtet
(right:0) und begann 190 px weiter links, also außerhalb des Panels
(gemessen: linke Kante bei -65 px). Bezug ist jetzt der Knopf.

Nachgemessen (Takt für die Prüfung auf 2 s verkürzt, danach
zurückgestellt): ohne Eingabe kein Eintrag, nach einer Eingabe genau
einer, ohne weitere keiner dazu; nach 29 Änderungen 20 (gedeckelt,
Zeitstempel aufsteigend). Klick auf einen älteren Eintrag lädt ihn
(36 -> 32 Zeilen), Diagramm zeichnet neu, undo holt den vorherigen Stand
zurück. Plan: 155 Knoten, 0 Warnungen. 240 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 10:23:42 +02:00
co-authored by Claude Opus 5
parent 6935b4882d
commit 8fd595abf9
5 changed files with 274 additions and 2 deletions
+60
View File
@@ -3522,3 +3522,63 @@ Undo deshalb über `execCommand('undo')`; das steht jetzt auch in
`frontend/CLAUDE.md`. Dieselbe Lehre wie D25 (synthetische `TouchEvent`s) und
D17-Nachtrag 4 (Bildschirmtastatur): Was die Umgebung stellt, stellt der
Emulator nicht.
## D54 — Frühere Stände: alle zehn Minuten, nur bei Änderung, die letzten zwanzig
Ein Sicherheitsnetz gegen Versehen — und der Anlass war ein echtes: Der
Tab-Fehler aus D53 konnte eine ganze Auswahl löschen und nahm dabei auch noch
das Rückgängig mit. Ohne Netz war der Text dann weg.
**Aufbewahrt werden die letzten 20 je Dokument** (Nutzer-Entscheidung), also
rund 3½ Stunden bei gleichmäßigen Abständen und höchstens ~800 kB beim größten
Dokument. Erwogen und verworfen: **ausgedünnt** (alle der letzten Stunde,
stündlich für einen Tag, täglich für eine Woche — reicht weiter zurück, kostet
aber eine Ausdünn-Regel, die man beim Lesen erst verstehen muss) und
**lückenlos 24 Stunden** (bis zu 144 Stände, beim großen Plan ~5,8 MB und damit
über dem localStorage-Limit — es bräuchte doch wieder eine Notbremse). Der
Anspruch ist bewusst klein: Wer weiter zurück will, hat Git.
**„Nur bei Änderung" heißt: gegen den letzten Stand, nicht gegen das Laden.**
Gibt es noch keinen Stand, wird gegen den Text beim Aktivieren des Dokuments
verglichen (`snapBase`). Ohne das legte der erste Takt nach dem Öffnen auch ein
**unverändertes** Dokument weg — beim Herumklicken durch mehrere Dokumente
sammelte sich so Ballast, den niemand erzeugt hat. Nachgemessen: Ohne Eingabe
entsteht der Schlüssel im localStorage gar nicht erst.
**Die Dokumente sind wichtiger als ihre Stände.** Beide teilen sich den
localStorage. Läuft er über, wirft `persistSnaps()` deshalb so lange den
jeweils ältesten Stand weg, bis es passt, notfalls alle — statt eine Ausnahme
hochzureichen und damit womöglich das Speichern der **Dokumente** zu
gefährden. Ein gelöschtes Dokument nimmt seine Stände mit.
**Zurückgeholt wird undo-fähig** (`replaceTextUndoable`, D53): ein Griff
daneben kostet ein Strg+Z, keine Rückfrage — dieselbe Haltung wie beim Falten
(D38-Nachtrag 2). Vorher wird der **aktuelle** Stand weggelegt, falls er noch
nicht drin ist; sonst wäre ausgerechnet er das Einzige, was das Zurückholen
verlöre.
**Pad-Dokumente (D31) bleiben außen vor.** Ihr Textfeld ist schreibgeschützt —
ein alter Stand ließe sich dort gar nicht einsetzen, und Stände zu sammeln,
die niemand laden kann, wäre nur Ballast. Der Knopf ist dort verborgen
(`src.readOnly` bewacht Sammeln, Laden und Sichtbarkeit).
**Platzierung: rechts neben dem Dokumenten-Wähler** (Nutzer-Vorgabe), beide in
einer Gruppe, die das `margin-right:auto` trägt. Der Positionsbezug des Menüs
ist der **Knopf**, nicht die Gruppe: An der Gruppe ausgerichtet (`right:0`)
begann es 190 px weiter links und lief aus dem Panel heraus — in der ersten
Fassung gebaut, im Bild sofort zu sehen und nachgemessen (linke Kante bei
65 px). Jetzt `position:relative` an einer Hülle um den Knopf und `left:0` am
Menü: linke Kante bei 99 px, gleichauf mit dem Knopf, ganz im Panel.
Jeder Eintrag nennt **Uhrzeit und Zeilenzahl** — die Zeilenzahl sagt auf einen
Blick, welchen der ähnlich benannten Stände man greift. Neueste zuoberst,
danach sucht man zuerst.
**Nachgemessen** (Takt für die Prüfung auf 2 s verkürzt, danach zurückgestellt
und geprüft): ohne Eingabe kein Eintrag; nach einer Eingabe genau einer; ohne
weitere Eingabe kommt keiner dazu; nach 29 Änderungen sind es **20** (gedeckelt,
Zeitstempel aufsteigend, der älteste ist der kleinste Text). Menü: 20 Einträge,
neueste zuoberst. Klick auf einen älteren Eintrag lädt ihn (36 → 32 Zeilen),
schließt das Menü, das Diagramm zeichnet neu (30 Knoten) — und ein `undo` holt
den vorherigen Stand zeichengenau zurück. Der **Pad-Fall** ist durch die
`readOnly`-Wächter im Code abgedeckt, aber nicht live durchgespielt (er
bräuchte ein echtes Pad).
+7
View File
@@ -71,6 +71,7 @@
- [^] #ed.fold.click: Fold and unfold in the diagram (S)
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
- [^] #ed.fold.preset: One switch folds everything from size M downward (S) %% see D44
- [x] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
- [ ] #ed.files: Open and save .werkbaum files (S)
+ [^] #ed.fresh: Show what is new since your last visit (S)
+ [?] #ed.percolor: A pastel colour per person (S)
@@ -502,6 +503,12 @@
smaller closed, L and above open. Size rather than depth — depth only says
how finely somebody happened to write it down.
#ed.snaps
A safety net against mishaps: the text is put aside every ten minutes, but
only when it differs from the last one kept, and the last twenty are offered
beside the document picker. Loading one is a single undo step, so a wrong
pick costs nothing.
#ed.files
Open a .werkbaum file and save one. Today the leading data format is the one
thing that cannot leave the browser as a file, while the diagram can.
+10
View File
@@ -52,6 +52,9 @@
<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>
@@ -65,6 +68,13 @@
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
</div>
</div>
<span class="snapwrap">
<button type="button" class="copybtn snapbtn" id="snapBtn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="snapTooltip" data-i18n-aria="snapTooltip" title="Frühere Stände dieses Dokuments" aria-label="Frühere Stände dieses Dokuments">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3 4v4h4"/><path d="M12 7.5V12l3 1.8"/></svg>
</button>
<div class="snapmenu" id="snapMenu" role="menu" data-i18n-aria="snapMenuAria" aria-label="Frühere Stände" hidden></div>
</span>
</span>
<button type="button" class="copybtn padview" id="padViewBtn" data-view="text" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect class="frame" x="2.5" y="5" width="19" height="14" rx="2"/><line class="ic-both" x1="12" y1="5" x2="12" y2="19"/><rect class="ic-pad" x="3.5" y="6" width="8" height="12" rx="1" fill="currentColor" stroke="none"/><rect class="ic-text" x="12.5" y="6" width="8" height="12" rx="1" fill="currentColor" stroke="none"/></svg>
</button>
+162 -1
View File
@@ -1744,6 +1744,10 @@ const I18N = {
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
editorTitle:"Struktur (Text)", 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",
snapNone:"Noch keine früheren Stände gesichert wird alle 10 Minuten, sobald sich etwas geändert hat.",
snapLines:"{n} Zeilen",
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
docNewName:"Unbenannt",
docDeleteConfirm:"Dokument „{name}“ löschen?",
@@ -1829,6 +1833,9 @@ const I18N = {
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
editorTitle:"Structure (text)", 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 every 10 minutes, once something has changed.",
snapLines:"{n} lines",
docNew:"New document", docRename:"Rename", docDelete:"Delete",
docNewName:"Untitled",
docDeleteConfirm:"Delete document “{name}”?",
@@ -1914,6 +1921,9 @@ const I18N = {
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
editorTitle:"Estructura (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 cada 10 minutos, cuando algo ha cambiado.",
snapLines:"{n} líneas",
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
docNewName:"Sin título",
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
@@ -1999,6 +2009,9 @@ const I18N = {
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
editorTitle:"Structure (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é toutes les 10 minutes, dès que quelque chose a changé.",
snapLines:"{n} lignes",
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
docNewName:"Sans titre",
docDeleteConfirm:"Supprimer le document « {name} » ?",
@@ -2084,6 +2097,9 @@ const I18N = {
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
editorTitle:"Struktura (tekst)", 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 co 10 minut, gdy coś się zmieni.",
snapLines:"wierszy: {n}",
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
docNewName:"Bez nazwy",
docDeleteConfirm:"Usunąć dokument „{name}”?",
@@ -2169,6 +2185,9 @@ const I18N = {
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы",
snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния",
snapNone:"Прежних состояний пока нет — они сохраняются каждые 10 минут, если что-то изменилось.",
snapLines:"строк: {n}",
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
docNewName:"Без названия",
docDeleteConfirm:"Удалить документ «{name}»?",
@@ -2254,6 +2273,9 @@ const I18N = {
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़",
snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ",
snapNone:"अभी कोई पिछली स्थिति नहीं — कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।",
snapLines:"{n} पंक्तियाँ",
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
docNewName:"बिना शीर्षक",
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
@@ -2339,6 +2361,9 @@ const I18N = {
riskTooltip:"高风险 – 工作量尚不明确。",
editorTitle:"结构(文本)", diagramTitle:"图表",
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态",
snapNone:"暂无早期状态——内容有变动时,每 10 分钟保存一次。",
snapLines:"{n} 行",
docNew:"新建文档", docRename:"重命名", docDelete:"删除",
docNewName:"未命名",
docDeleteConfirm:"删除文档“{name}”?",
@@ -2424,6 +2449,9 @@ const I18N = {
riskTooltip:"高リスク – 規模はまだ不明。",
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態",
snapNone:"以前の状態はまだありません — 変更があれば 10 分ごとに保存されます。",
snapLines:"{n} 行",
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
docNewName:"無題",
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
@@ -2869,6 +2897,130 @@ function toggleDocMenu(){ docMenu.hidden ? openDocMenu() : closeDocMenu(); }
Dokument sichern, dann das Ziel laden und neu rendern. */
function flushActive(){ const d = activeDoc(); if(d) d.text = src.value; }
/* ---------- Frühere Stände (Snapshots, D54) ----------
Alle zehn Minuten wird der Text des aktiven Dokuments weggelegt aber nur,
wenn er sich seit dem letzten Stand geändert hat. Aufgehoben werden die
letzten 20 je Dokument (rund 3½ Stunden Arbeit bei gleichmäßigen Abständen).
Es ist ein Sicherheitsnetz gegen Versehen, kein Versionsverwaltungssystem
wer weiter zurück will, hat Git. */
const LS_SNAPS = 'werkbaum-snaps';
const SNAP_EVERY = 10 * 60 * 1000;
const SNAP_KEEP = 20;
let snaps = {}; /* {docId: [{t, text}, …]} — ältester zuerst */
let snapBase = ''; /* Text bei Dokumentwechsel; Vergleich, solange es keinen Stand gibt */
function loadSnaps(){
try{ const o = JSON.parse(localStorage.getItem(LS_SNAPS) || '{}');
snaps = o && typeof o === 'object' ? o : {}; }
catch(_){ snaps = {}; }
}
/* Der Platz im localStorage ist geteilt. Läuft er über, sollen die
**Dokumente** überleben, nicht ihre Stände der Fehlerfall wirft deshalb
Stände weg, bis es passt, notfalls alle. */
function persistSnaps(){
for(;;){
try{ localStorage.setItem(LS_SNAPS, JSON.stringify(snaps)); return true; }
catch(_){
if(!dropOldestSnap()){ try{ localStorage.removeItem(LS_SNAPS); }catch(_){} return false; }
}
}
}
function dropOldestSnap(){
let id = null, t = Infinity;
for(const k in snaps){
const l = snaps[k];
if(l && l.length && l[0].t < t){ t = l[0].t; id = k; }
}
if(id === null) return false;
snaps[id].shift();
if(!snaps[id].length) delete snaps[id];
return true;
}
/* Legt den aktuellen Text weg, wenn er neu ist. Pad-Dokumente (D31) bleiben
außen vor: Ihr Text ist schreibgeschützt, ein alter Stand ließe sich dort
gar nicht wieder einsetzen Stände zu sammeln, die niemand laden kann,
wäre nur Ballast. */
function snapshotNow(){
const d = activeDoc();
if(!d || src.readOnly) return false;
const text = src.value;
const list = snaps[d.id] || (snaps[d.id] = []);
const letzter = list.length ? list[list.length-1].text : snapBase;
if(text === letzter) return false;
list.push({t: Date.now(), text});
while(list.length > SNAP_KEEP) list.shift();
snapBase = text;
persistSnaps();
if(!snapMenu.hidden) renderSnapMenu();
return true;
}
setInterval(snapshotNow, SNAP_EVERY);
const snapBtn = document.getElementById('snapBtn');
const snapMenu = document.getElementById('snapMenu');
function snapLabel(ms){
const d = new Date(ms);
const heute = d.toDateString() === new Date().toDateString();
try{
return heute
? d.toLocaleTimeString(lang, {hour: '2-digit', minute: '2-digit'})
: d.toLocaleString(lang, {day: '2-digit', month: '2-digit',
hour: '2-digit', minute: '2-digit'});
}catch(_){ return d.toISOString().slice(0, 16).replace('T', ' '); }
}
function renderSnapMenu(){
const d = activeDoc();
const list = d ? (snaps[d.id] || []) : [];
snapMenu.innerHTML = '';
if(!list.length){
const p = document.createElement('div');
p.className = 'snapempty';
p.textContent = t('snapNone');
snapMenu.appendChild(p);
return;
}
/* Neueste zuoberst — danach sucht man zuerst. */
list.slice().reverse().forEach(s => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'snapitem';
b.setAttribute('role', 'menuitem');
b.innerHTML = '<span></span><span class="snapsize"></span>';
b.firstChild.textContent = snapLabel(s.t);
b.lastChild.textContent = t('snapLines', {n: s.text.split('\n').length});
b.addEventListener('click', e => { e.stopPropagation(); loadSnapshot(s); });
snapMenu.appendChild(b);
});
}
/* Zurückgeholt wird **undo-fähig** (D53): Ein Griff daneben kostet ein Strg+Z,
keine Rückfrage. Der aktuelle Stand wird vorher weggelegt, falls er noch
nicht drin ist sonst wäre er das Einzige, was der Griff verlöre. */
function loadSnapshot(s){
if(src.readOnly) return;
snapshotNow();
closeSnapMenu();
if(!replaceTextUndoable(s.text)) render();
snapBase = src.value;
}
function openSnapMenu(){
renderSnapMenu();
snapMenu.hidden = false;
snapBtn.setAttribute('aria-expanded', 'true');
}
function closeSnapMenu(){
snapMenu.hidden = true;
snapBtn.setAttribute('aria-expanded', 'false');
}
function updateSnapBtn(){ snapBtn.hidden = src.readOnly; if(src.readOnly) closeSnapMenu(); }
snapBtn.addEventListener('click', e => {
e.stopPropagation();
snapMenu.hidden ? openSnapMenu() : closeSnapMenu();
});
document.addEventListener('click', e => {
if(!snapMenu.hidden && !snapMenu.contains(e.target) && !snapBtn.contains(e.target)) closeSnapMenu();
});
/* ---------- Was ist neu?" (D28) ----------
Verglichen wird gegen die zuletzt **gesehene** Fassung eines Dokuments von
außen (mitgeliefert oder ?sourceUrl=), nicht gegen die letzte Auslieferung:
@@ -2918,7 +3070,11 @@ function updateFreshBtn(){
}
freshBtn.addEventListener('click', acknowledgeFresh);
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : ''; render(); updateDocName(); updateFreshBtn(); }
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
snapBase = src.value; closeSnapMenu(); updateSnapBtn();
render(); updateDocName(); updateFreshBtn(); }
function switchDoc(id){
if(id === activeId) return;
flushActive();
@@ -2977,6 +3133,7 @@ function deleteDoc(){
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
if(padSource && padSource.id === d.id) stopPad(); /* danach gibt es nichts mehr zu holen (D31) */
docs = docs.filter(x => x.id !== d.id);
if(snaps[d.id]){ delete snaps[d.id]; persistSnaps(); } /* 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();
@@ -2988,9 +3145,12 @@ function deleteDoc(){
function initDocs(){
restoring = true;
loadDocs();
loadSnaps();
const d = activeDoc();
src.value = d ? d.text : '';
snapBase = src.value;
restoring = false;
updateSnapBtn();
persistDocs(); /* migrierte/geladene Liste festschreiben (stabil über Reload) */
updateDocName();
render();
@@ -3268,6 +3428,7 @@ function updatePadLink(){
const d = activeDoc();
const isPad = !!(d && padSource && d.id === padSource.id);
src.readOnly = isPad;
updateSnapBtn(); /* Pad-Dokumente sammeln keine Stände (D54) */
src.classList.toggle('readonly', isPad);
if(srcWrap) srcWrap.classList.toggle('readonly', isPad); /* Streifen mit abtönen */
if(isPad) src.title = t('padReadonly'); else src.removeAttribute('title');
+35 -1
View File
@@ -414,9 +414,22 @@
/* 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;
}
/* 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;
margin-right:auto;max-width:60%;color:var(--muted);
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;
}
@@ -450,6 +463,27 @@
padding:5px 7px;color:var(--ink);background:var(--card);
font-family:inherit;font-size:.86rem;outline:none;
}
/* Snapshot-Menü (D54): wie das Dokument-Menü, aber am rechten Rand seiner
Gruppe ausgerichtet also unter dem Knopf. */
.snapbtn{flex:0 0 auto}
.snapmenu{
position:absolute;top:calc(100% + 6px);left:0;z-index:50;min-width:190px;
max-height:280px;overflow:auto;
display:flex;flex-direction:column;gap:2px;
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;
}
.snapmenu[hidden]{display:none}
.snapitem{
display:flex;align-items:baseline;gap:8px;width:100%;
border:0;background:transparent;cursor:pointer;text-align:left;
padding:6px 9px;border-radius:7px;color:var(--ink);
font-family:inherit;font-size:.84rem;white-space:nowrap;
}
.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);