feat(menü): Geteilte werden verlassen, nicht gelöscht (D81-Nachtrag 5)

Eigene Aktion für die Geteilt-Gruppe (Server- und URL-Dokumente): Wort
(Verlassen), Icon (Tür mit Pfeil statt Papierkorb), Rückfrage (nur der
eigene Listeneintrag geht, Server bleibt) und normales statt rotem Hover.
Der Link wandert beim Verlassen in die Zwischenablage — der Listeneintrag
ist zugleich das Lesezeichen. Verhalten wie lokales Löschen
(removeDocLocally, gemeinsamer Kern); i18n in neun Sprachen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-27 07:28:43 +02:00
co-authored by Claude Fable 5
parent a78147e459
commit 5c8fbcde01
4 changed files with 93 additions and 8 deletions
+1
View File
@@ -19,6 +19,7 @@ reverse.
## 2026-08-27
- Shared documents are left, not deleted: their row shows a leave action with its own icon and wording — the document stays on the server, and its link lands on the clipboard
- The document menu's groups are named Included, Local and Shared now — "yours" and "sources" said less than where a document lives
- Documents are stored one key each now, with a small index: a full storage no longer blocks every save, and one corrupted entry costs one document instead of all
- Typing no longer serializes every document on each keystroke — only the active text is written, the full list on switching, closing or leaving the page
+38
View File
@@ -7208,3 +7208,41 @@ weitergegeben hat. Erwogen waren auch „Im Browser · Im Netz" (ortsbasiert)
und „Lokal · Extern"; die gewählte Form ist die knappste, die alle drei
Gruppen mit einem Wort trifft. In allen neun Sprachen nachgezogen
(en: Included · Local · Shared).
**Nachtrag 5 — Geteilte werden VERLASSEN, nicht gelöscht.** Nutzer-Frage:
Was tut das Löschen bei einem geteilten Dokument — verschwindet es vom
Server? Es verschwindet **nur lokal**: Der Client ruft nie das
Server-DELETE, das Dokument und die Arbeit aller anderen bleiben, und der
Link öffnet es jederzeit wieder. Richtig so — das Zeilen-Symbol darf nicht
die destruktivste Aktion der App sein. Aber der Benutzer konnte es nicht
wissen: Wort, Icon und Rückfrage waren dieselben wie beim echten Löschen.
**Entschieden (Nutzer): eine eigene Aktion „Verlassen"** für die ganze
Geteilt-Gruppe (Server- UND URL-Dokumente — eine Regel je Gruppe), mit
allem, was eine eigene Aktion ausmacht: das Wort (die vertraute Metapher
geteilter Ordner und Gruppen; erwogen und verworfen: „Vergessen" — deckt
URL-Dokumente besser, ist aber weniger etabliert — und „Trennen" — klingt
nach Verbindungszustand statt Listeneintrag), das Icon (Tür mit Pfeil,
Log-out-Idiom, statt Papierkorb), die Rückfrage (benennt, dass nur der
eigene Listeneintrag geht) und die Farbe (normales Hover statt der
Gefahr-Färbung des Papierkorbs — nichts Destruktives). Das **Verhalten** ist
dasselbe wie beim lokalen Löschen (`removeDocLocally()`, der gemeinsame
Kern) — für den Benutzer sind es trotzdem zwei verschiedene Dinge, und
genau das sagt die Oberfläche jetzt.
**Der Listeneintrag ist zugleich das Lesezeichen** — wer den Link nirgends
sonst hat, verlöre den Rückweg. Beim Verlassen wandert er deshalb in die
Zwischenablage (gebaut aus `docSearch('', id)`, derselben getesteten Regel
wie die Adresszeile, D80); die Rückfrage sagt es an. Neue Schlüssel
`docLeave`/`docLeaveConfirm` in neun Sprachen.
**Nachgemessen** im Browser an inszenierten Geteilten (Server- und
URL-Dokument): beide Zeilen tragen Stift + Tür statt Papierkorb, ohne rote
Hover-Färbung; die Rückfrage nennt Liste, Server und Zwischenablage; nach
dem Bestätigen liegt der `?live=`- bzw. `?sourceUrl=`-Link lesbar in der
Zwischenablage, die Zeile ist weg, das Menü bleibt offen, die
Text-Schlüssel sind abgeräumt; mitgelieferte und lokale Zeilen behalten den
Papierkorb. Werkzeug-Lehre am Rande: Ein noch offener, veralteter Test-Tab
flushte bei jedem Sichtbarkeitswechsel seinen alten Speicherstand über die
Inszenierung (Last-Writer-Wins zwischen Tabs — kein neues Verhalten, aber
eine Falle für Speicher-Tests: alte Tabs erst schließen).
+1 -1
View File
@@ -533,7 +533,7 @@
#ed.docs.picker
The app header reads "Werkbaum > name": the chip opens the menu, grouped by
kind (shipped, local, shared), with rename and delete on each row (shipped
ones keep their delivered name). Restore
ones keep their delivered name, shared ones are left, not deleted). Restore
lives on the editor title bar's reload button instead, acting only on the
open document. The menu scrolls as a whole instead of being cut off on
small screens, and on the phone it is reachable from both panes.
+53 -7
View File
@@ -2260,6 +2260,8 @@ const I18N = {
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
docNewName:"Unbenannt",
docDeleteConfirm:"Dokument „{name}“ löschen?",
docLeave:"Verlassen nur aus der eigenen Liste entfernen",
docLeaveConfirm:"„{name}“ verlassen? Es wird nur aus deiner Liste entfernt — auf dem Server bleibt es bestehen, und sein Link liegt danach in der Zwischenablage.",
docRestore:"Original wiederherstellen",
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern (Strg+S)",
@@ -2374,6 +2376,8 @@ const I18N = {
docNew:"New document", docRename:"Rename", docDelete:"Delete",
docNewName:"Untitled",
docDeleteConfirm:"Delete document “{name}”?",
docLeave:"Leave removes it from your list only",
docLeaveConfirm:"Leave “{name}”? It is only removed from your list — it stays on the server, and its link is placed on your clipboard.",
docRestore:"Restore original",
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
docOpenFile:"Open file…", docSaveFile:"Save as file (Ctrl+S)",
@@ -2488,6 +2492,8 @@ const I18N = {
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
docNewName:"Sin título",
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
docLeave:"Salir solo se quita de tu lista",
docLeaveConfirm:"¿Salir de «{name}»? Solo se quita de tu lista: sigue en el servidor y su enlace queda en el portapapeles.",
docRestore:"Restaurar original",
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo (Ctrl+S)",
@@ -2602,6 +2608,8 @@ const I18N = {
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
docNewName:"Sans titre",
docDeleteConfirm:"Supprimer le document « {name} » ?",
docLeave:"Quitter ne le retire que de votre liste",
docLeaveConfirm:"Quitter « {name} » ? Il n'est retiré que de votre liste — il reste sur le serveur et son lien est copié dans le presse-papiers.",
docRestore:"Restaurer loriginal",
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier (Ctrl+S)",
@@ -2716,6 +2724,8 @@ const I18N = {
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
docNewName:"Bez nazwy",
docDeleteConfirm:"Usunąć dokument „{name}”?",
docLeave:"Opuść usuwa tylko z twojej listy",
docLeaveConfirm:"Opuścić „{name}”? Zniknie tylko z twojej listy — na serwerze pozostaje, a jego link trafi do schowka.",
docRestore:"Przywróć oryginał",
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik (Ctrl+S)",
@@ -2830,6 +2840,8 @@ const I18N = {
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
docNewName:"Без названия",
docDeleteConfirm:"Удалить документ «{name}»?",
docLeave:"Покинуть – удаляется только из вашего списка",
docLeaveConfirm:"Покинуть «{name}»? Он исчезнет только из вашего списка — на сервере он останется, а его ссылка будет в буфере обмена.",
docRestore:"Восстановить оригинал",
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл (Ctrl+S)",
@@ -2944,6 +2956,8 @@ const I18N = {
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
docNewName:"बिना शीर्षक",
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
docLeave:"छोड़ें – केवल आपकी सूची से हटता है",
docLeaveConfirm:"„{name}“ छोड़ें? यह केवल आपकी सूची से हटेगा — सर्वर पर बना रहेगा, और इसका लिंक क्लिपबोर्ड में रख दिया जाएगा।",
docRestore:"मूल पुनर्स्थापित करें",
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें (Ctrl+S)",
@@ -3058,6 +3072,8 @@ const I18N = {
docNew:"新建文档", docRename:"重命名", docDelete:"删除",
docNewName:"未命名",
docDeleteConfirm:"删除文档“{name}”?",
docLeave:"离开——仅从你的列表中移除",
docLeaveConfirm:"离开“{name}”?它只会从你的列表中移除——仍保留在服务器上,其链接会放入剪贴板。",
docRestore:"恢复原始版本",
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
docOpenFile:"打开文件…", docSaveFile:"另存为文件(Ctrl+S",
@@ -3172,6 +3188,8 @@ const I18N = {
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
docNewName:"無題",
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
docLeave:"退出 – 自分のリストから外すだけ",
docLeaveConfirm:"「{name}」から退出しますか?自分のリストから外れるだけで、サーバーには残ります。リンクはクリップボードに入ります。",
docRestore:"オリジナルを復元",
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存 (Ctrl+S)",
@@ -3628,10 +3646,11 @@ 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.
Als Konstanten, damit der Renderer lesbar bleibt. */
/* Kleine Zeilen-Icons (Feather-Stil wie überall): Stift, Papierkorb, Tür mit
Pfeil (Verlassen). 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_LEAVE = '<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="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></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): Löschen überall, Umbenennen nur bei NICHT
@@ -3665,7 +3684,14 @@ function docRowHtml(d){
`<span class="docitem-name">${esc(d.name)}</span>${zusatz}</button>` +
`<span class="docacts">` +
(shippedStateOf(d.id) ? '' : iconBtn('rename', t('docRename'), IC_RENAME)) +
iconBtn('delete', t('docDelete'), IC_DELETE, ' docdelbtn') +
/* Geteilte (Server- wie URL-Dokumente) werden VERLASSEN, nicht gelöscht
(D81-Nachtrag 5): Die Aktion tut lokal dasselbe, ist für den Benutzer
aber eine andere dem Dokument selbst geschieht nichts, man gibt nur
seinen Listeneintrag auf. Eigenes Wort, eigenes Icon (Tür mit Pfeil),
eigene Rückfrage und bewusst NICHT die rote Gefahr-Färbung. */
(['server', 'url'].includes(docKind(d.id, SHIPPED_IDS))
? iconBtn('leave', t('docLeave'), IC_LEAVE)
: iconBtn('delete', t('docDelete'), IC_DELETE, ' docdelbtn')) +
`</span></div>`;
}
function renderDocMenu(){
@@ -4167,10 +4193,10 @@ function commitRename(){
finishNewDoc();
}
function cancelRename(){ renamingId = null; renderDocMenu(); finishNewDoc(); }
function deleteDoc(id){
const d = docs.find(x => x.id === id);
if(!d) return;
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
/* Der gemeinsame Kern von Löschen und Verlassen: den Eintrag samt lokaler
Anhängsel entfernen. Die beiden Aktionen unterscheiden sich für den
Benutzer (Wort, Icon, Rückfrage D81-Nachtrag 5), lokal tun sie dasselbe. */
function removeDocLocally(d){
if(liveState && liveState.id === d.id) stopLive(); /* dito fürs Server-Dokument (D76) */
if(fileHandles.has(d.id)){ fileHandles.delete(d.id); idbDeleteHandle(d.id); } /* mit dem Dokument geht sein Datei-Handle (D72) */
docs = docs.filter(x => x.id !== d.id);
@@ -4186,6 +4212,25 @@ function deleteDoc(id){
persistDocs();
if(!docMenu.hidden) renderDocMenu();
}
function deleteDoc(id){
const d = docs.find(x => x.id === id);
if(!d) return;
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
removeDocLocally(d);
}
/* Verlassen (D81-Nachtrag 5): für Geteilte dem Dokument auf dem Server
bzw. hinter der URL geschieht NICHTS, nur der eigene Listeneintrag geht.
Der Eintrag ist zugleich das Lesezeichen: Der Link wandert deshalb in die
Zwischenablage (die Rückfrage sagt es an) wer ihn nirgends sonst hat,
verlöre sonst den Rückweg. */
async function leaveDoc(id){
const d = docs.find(x => x.id === id);
if(!d || !['server', 'url'].includes(docKind(d.id, SHIPPED_IDS))) return;
if(!window.confirm(t('docLeaveConfirm', {name: d.name}))) return;
const link = location.origin + location.pathname + docSearch('', d.id);
try{ await navigator.clipboard.writeText(link); }catch(_){}
removeDocLocally(d);
}
/* ---------- Lokale Dateien öffnen und speichern (D72, Stufe 1) ----------
Der klassische Weg, der in jedem Browser läuft: Datei-Input zum Öffnen,
Blob-Download zum Speichern. Geöffnet wird als NEUES Dokument (D22) eine
@@ -5025,6 +5070,7 @@ docList.addEventListener('click', e => {
if(!id) return;
if(akt.dataset.act === 'rename') renameDoc(id);
else if(akt.dataset.act === 'delete') deleteDoc(id);
else if(akt.dataset.act === 'leave') leaveDoc(id);
return;
}
const btn = e.target.closest('.docpick');