feat: Strg+S speichert das Dokument direkt als Datei (D74)

Die Geste, die jeder zuerst versucht, tat bisher das Falsche (Browser-Dialog
„Seite speichern"). Jetzt ruft sie saveLocalFile(): mit gemerktem Handle
dialogfrei in dieselbe Datei, sichtbar bestätigt durch einen Petrol-Haken am
Dokumentnamen; ohne Handle wie der Menü-Eintrag. Legenden-Zeile hint_save in
neun Sprachen. Nebenbefund behoben: adoptFile() wartet jetzt auf die aus
IndexedDB zurückgeholten Handles — ein Doppelklick unmittelbar nach dem
App-Start legte sonst ein Duplikat an statt dasselbe Dokument zu
aktualisieren.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 15:15:50 +02:00
co-authored by Claude Fable 5
parent 45a52c15f9
commit 72ab9746d5
5 changed files with 127 additions and 12 deletions
+2
View File
@@ -19,6 +19,8 @@ reverse.
## 2026-08-25 ## 2026-08-25
- Ctrl+S saves the document as a file — with a remembered file it writes straight back in place, and the document name flashes as confirmation
- Fix: a file double-clicked right after the app starts could open as a duplicate document instead of updating its own
- The installed app registers for `.werkbaum` files — a double-click in the file manager opens them straight into the editor - The installed app registers for `.werkbaum` files — a double-click in the file manager opens them straight into the editor
- A service worker starts the editor offline; navigations stay network-first, so the update notice keeps telling the truth - A service worker starts the editor offline; navigations stay network-first, so the update notice keeps telling the truth
- Werkbaum installs as an app: a web app manifest with standalone display and icons rendered from the brand mark - Werkbaum installs as an app: a web app manifest with standalone display and icons rendered from the brand mark
+67
View File
@@ -5476,3 +5476,70 @@ auf eine `.werkbaum`-Datei und die persistente Schreibberechtigung der
installierten App sind Betriebssystem-Dialoge und bleiben ein Handtest auf installierten App sind Betriebssystem-Dialoge und bleiben ein Handtest auf
echter Hardware; gemessen ist alles bis an diese Kante (Manifest gültig, echter Hardware; gemessen ist alles bis an diese Kante (Manifest gültig,
Consumer registriert, `adoptFile()`-Weg seit D72 geprüft). Consumer registriert, `adoptFile()`-Weg seit D72 geprüft).
## D74 — Strg+S speichert direkt: die D72-Zurückstellung ist umgekehrt
D72-Nachtrag hielt fest: „Kein Strg+S — erwogen und zurückgestellt", mit zwei
Einwänden — der Browser-Default (Seite speichern) müsste abgefangen werden,
und ohne Handle öffnete die Geste unvermittelt einen Dialog. Mit der
installierten PWA (D73) kippt die Abwägung, und der Nutzer hat es benannt:
Ein lokal geladenes Dokument soll **direkt** speicherbar sein, ohne Dialog.
Genau dafür ist Strg+S die Geste, die jeder zuerst versucht — und ohne
eigenen Handler tut sie das Schlimmstmögliche: Sie öffnet den
„Seite speichern"-Dialog des Browsers, also einen Dialog, der nicht einmal
das Dokument speichert.
**Die beiden D72-Einwände, neu bewertet:**
- **Der Browser-Default ist kein Preis, sondern der Anlass.** `preventDefault`
ist eine Zeile; was sie unterdrückt, war vorher der einzige Effekt der
Geste — und der falsche.
- **„Ohne Handle unvermittelt ein Dialog" trägt nicht mehr.** Die Geste
**heißt** Speichern; ein Speicher-Dialog auf eine Speichern-Geste ist keine
Überraschung, sondern das Verhalten jedes Editors beim ersten Strg+S.
Danach ist das Handle gemerkt und jede weitere Geste dialogfrei.
**Verhalten = der Menü-Eintrag, nur als Geste.** Strg+S (macOS auch Cmd+S)
ruft dasselbe `saveLocalFile()`: mit gemerktem Handle in dieselbe Datei
(die einmalige Schreibberechtigungs-Nachfrage des Browsers bleibt — der
Tastendruck ist die dafür nötige Nutzergeste; die installierte App kann sie
mit „Bei jedem Besuch zulassen" dauerhaft erteilen), ohne Handle der
Speichern-Dialog (Chromium) bzw. Download (Firefox/Safari). Pad-Dokumente
dürfen wie über das Menü gespeichert werden (D72: der Schreibschutz gilt dem
Textfeld, nicht dem Export). `e.repeat` ist ausgefiltert — eine gehaltene
Taste speichert einmal.
**Stilles Speichern braucht eine sichtbare Antwort.** Der In-Place-Weg zeigt
sonst nichts — die Geste wirkte tot, und niemand wüsste, ob gespeichert ist.
Rückmeldung im Haus-Idiom (`flashBtn`, D54: 1,5 s Petrol samt Haken), am
**Dokumentnamen** in der Editor-Titelzeile — der benennt, was gespeichert
wurde. Dialog und Download sind selbst sichtbar und brauchen keine.
**Die Legenden-Zeile, die D72 zur Bedingung machte, ist da:** `hint_save` in
allen neun Sprachen, als zweite Zeile der Bedienungs-Zeile am Ende der
Legende (D25-Idiom: Auffindbarkeit gehört zur Geste).
**Nebenbefund, mitbehoben: der launchQueue-Empfänger wartete nicht auf die
gemerkten Handles.** `idbLoadHandles()` läuft asynchron beim Start; ein
Doppelklick, der die App erst startet, konnte seinen Consumer **vor** dem
Laden der Handle-Map erreichen — der `isSameEntry`-Abgleich lief dann über
eine leere Map, und dieselbe Datei wurde als Duplikat angelegt statt ihr
Dokument zu aktualisieren (genau die Zusage aus D73, „dieselbe Datei landet
im selben Dokument", wäre im häufigsten PWA-Startweg gebrochen).
`adoptFile()` wartet jetzt auf das `handlesReady`-Promise, bevor es
abgleicht — an der einen Stelle, die die Map braucht, statt in jedem
Aufrufer. Der Race ist zeitabhängig und im Werkzeug nicht deterministisch
auslösbar; der Fix ist eine Ordnungszusage im Code, geprüft per Review und
dadurch, dass der Picker-Weg (der dasselbe `adoptFile()` nimmt) unverändert
funktioniert.
**Nachgemessen** am gebauten Stand (dist auf localhost, Picker gestubbt —
die Werkzeuggrenze aus D72: den nativen Dialog kann die Automatisierung
nicht bedienen, die Logik dahinter schon): Erster Strg+S ruft den
Speichern-Dialog genau einmal (richtiger `suggestedName`) und schreibt;
zweiter Strg+S schreibt **ohne** Dialog in dasselbe Handle (0 weitere
Picker-Aufrufe, 2 Schreibvorgänge). Beide waren **echte** Tastendrücke
(CDP), und kein Browser-Dialog erschien — `preventDefault` greift. Den
Petrol-Haken am Dokumentnamen hat erst ein synthetischer Strg+S nach 400 ms
gezeigt (Klasse `done`, ✓, `#0F766E`): Der Werkzeug-Umlauf ist langsamer als
die 1,5 s des Blitzes — eine Messgrenze, kein Befund. Die Legende zeigt die
neue Zeile in DE und EN.
+7
View File
@@ -88,6 +88,7 @@
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment - [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
- [^] #ed.files: Open and save .werkbaum files (S) - [^] #ed.files: Open and save .werkbaum files (S)
- [^] #ed.files.inplace: Saving writes back into the opened file (S) %% File System Access, Chromium - [^] #ed.files.inplace: Saving writes back into the opened file (S) %% File System Access, Chromium
- [x] #ed.files.ctrls: Ctrl+S saves the document as a file (XS) %% in place with a remembered handle
+ [^] #ed.fresh: Show what is new since your last visit (S) + [^] #ed.fresh: Show what is new since your last visit (S)
- [^] #ed.fresh.news: A star in the header, with the last few days (S) %% see D58 - [^] #ed.fresh.news: A star in the header, with the last few days (S) %% see D58
+ [?] #ed.percolor: A pastel colour per person (S) + [?] #ed.percolor: A pastel colour per person (S)
@@ -615,6 +616,12 @@
handle: saving writes back into the same file, and the same file reopens handle: saving writes back into the same file, and the same file reopens
into the same document. Firefox and Safari keep the plain download. into the same document. Firefox and Safari keep the plain download.
#ed.files.ctrls
The save gesture everyone tries first. Without it the browser offers to
save the page — the one dialog nobody wants. With a remembered handle it
writes straight back in place and flashes the document name as its answer;
otherwise it behaves like the menu entry.
#ed.fresh #ed.fresh
For documents that come from outside, nodes that went to production since For documents that come from outside, nodes that went to production since
your last visit get a yellow halo. New means live, not "line added" — a line your last visit get a yellow halo. New means live, not "line added" — a line
+47 -12
View File
@@ -2252,7 +2252,8 @@ const I18N = {
hint_eff:"Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.", hint_eff:"Die Knotenfarbe zeigt den effektiven Status (mit Abhängigkeiten); ist der eigene weiter, steht er als Marke unten links.",
hint_desc:"Beschreibungen: \" Zeile unter dem Knoten; Langtext hinter --- als eingerückter #id-Block — beides im Tooltip (”).", hint_desc:"Beschreibungen: \" Zeile unter dem Knoten; Langtext hinter --- als eingerückter #id-Block — beides im Tooltip (”).",
hint_fold:"Falten: - [x] > … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).", hint_fold:"Falten: - [x] > … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).",
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild; Strg+Klick auf eine Abhängigkeit :#id springt zur Zeile dieser ID." hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild; Strg+Klick auf eine Abhängigkeit :#id springt zur Zeile dieser ID.",
hint_save:"Strg+S speichert das Dokument als Datei — mit gemerkter Datei (Chromium) direkt an Ort und Stelle."
}, },
en: { en: {
subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)", subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)",
@@ -2358,7 +2359,8 @@ const I18N = {
hint_eff:"Node colour shows the effective status (with dependencies); if its own is further along, it appears as a mark at the bottom left.", hint_eff:"Node colour shows the effective status (with dependencies); if its own is further along, it appears as a mark at the bottom left.",
hint_desc:"Descriptions: a \" line below the node; long text behind --- as an indented #id block — both in the tooltip (”).", hint_desc:"Descriptions: a \" line below the node; long text behind --- as an indented #id block — both in the tooltip (”).",
hint_fold:"Folding: - [x] > … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).", hint_fold:"Folding: - [x] > … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).",
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view; Ctrl+click a dependency :#id jumps to that ID's line." hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view; Ctrl+click a dependency :#id jumps to that ID's line.",
hint_save:"Ctrl+S saves the document as a file — with a remembered file (Chromium) right back in place."
}, },
es: { es: {
subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)", subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)",
@@ -2464,7 +2466,8 @@ const I18N = {
hint_eff:"El color del nodo muestra el estado efectivo (con dependencias); si el propio va más adelante, aparece como marca abajo a la izquierda.", hint_eff:"El color del nodo muestra el estado efectivo (con dependencias); si el propio va más adelante, aparece como marca abajo a la izquierda.",
hint_desc:"Descripciones: línea \" bajo el nodo; texto largo tras --- como bloque #id sangrado — ambos en el tooltip (”).", hint_desc:"Descripciones: línea \" bajo el nodo; texto largo tras --- como bloque #id sangrado — ambos en el tooltip (”).",
hint_fold:"Plegado: - [x] > … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).", hint_fold:"Plegado: - [x] > … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).",
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista; Ctrl+clic en una dependencia :#id salta a la línea de esa ID." hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista; Ctrl+clic en una dependencia :#id salta a la línea de esa ID.",
hint_save:"Ctrl+S guarda el documento como archivo — con un archivo recordado (Chromium), directamente en el mismo lugar."
}, },
fr: { fr: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)", subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)",
@@ -2570,7 +2573,8 @@ const I18N = {
hint_eff:"La couleur du nœud montre le statut effectif (avec dépendances) ; si le sien est plus avancé, il apparaît en marque en bas à gauche.", hint_eff:"La couleur du nœud montre le statut effectif (avec dépendances) ; si le sien est plus avancé, il apparaît en marque en bas à gauche.",
hint_desc:"Descriptions : ligne \" sous le nœud ; texte long après --- en bloc #id indenté — les deux dans linfobulle (”).", hint_desc:"Descriptions : ligne \" sous le nœud ; texte long après --- en bloc #id indenté — les deux dans linfobulle (”).",
hint_fold:"Pliage : - [x] > … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).", hint_fold:"Pliage : - [x] > … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).",
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran ; Ctrl+clic sur une dépendance :#id saute à la ligne de cet ID." hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran ; Ctrl+clic sur une dépendance :#id saute à la ligne de cet ID.",
hint_save:"Ctrl+S enregistre le document comme fichier — avec un fichier mémorisé (Chromium), directement sur place."
}, },
pl: { pl: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)", subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)",
@@ -2676,7 +2680,8 @@ const I18N = {
hint_eff:"Kolor węzła pokazuje status efektywny (z zależnościami); jeśli własny jest dalej, widnieje jako znacznik u dołu po lewej.", hint_eff:"Kolor węzła pokazuje status efektywny (z zależnościami); jeśli własny jest dalej, widnieje jako znacznik u dołu po lewej.",
hint_desc:"Opisy: wiersz \" pod węzłem; dłuższy tekst za --- jako wcięty blok #id — oba w podpowiedzi (”).", hint_desc:"Opisy: wiersz \" pod węzłem; dłuższy tekst za --- jako wcięty blok #id — oba w podpowiedzi (”).",
hint_fold:"Zwijanie: - [x] > … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).", hint_fold:"Zwijanie: - [x] > … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).",
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie; Ctrl+kliknięcie zależności :#id przechodzi do wiersza tego ID." hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie; Ctrl+kliknięcie zależności :#id przechodzi do wiersza tego ID.",
hint_save:"Ctrl+S zapisuje dokument jako plik — przy zapamiętanym pliku (Chromium) bezpośrednio w tym samym miejscu."
}, },
ru: { ru: {
subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)", subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)",
@@ -2782,7 +2787,8 @@ const I18N = {
hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.", hint_eff:"Цвет узла показывает фактический статус (с учётом зависимостей); если собственный дальше, он показан меткой слева внизу.",
hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).", hint_desc:"Описания: строка \" под узлом; длинный текст после --- как блок #id с отступом — оба в подсказке (”).",
hint_fold:"Сворачивание: - [x] > … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).", hint_fold:"Сворачивание: - [x] > … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).",
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме; Ctrl+клик по зависимости :#id переходит к строке этого ID." hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме; Ctrl+клик по зависимости :#id переходит к строке этого ID.",
hint_save:"Ctrl+S сохраняет документ как файл — с запомненным файлом (Chromium) прямо на месте."
}, },
hi: { hi: {
subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)", subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)",
@@ -2888,7 +2894,8 @@ const I18N = {
hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।", hint_eff:"नोड का रंग प्रभावी स्थिति दिखाता है (निर्भरताओं सहित); यदि अपनी स्थिति आगे है, तो वह नीचे-बाएँ चिह्न के रूप में दिखती है।",
hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।", hint_desc:"विवरण: नोड के नीचे \" पंक्ति; --- के बाद #id ब्लॉक में लंबा पाठ — दोनों टूलटिप में (”)।",
hint_fold:"फ़ोल्डिंग: - [x] > … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।", hint_fold:"फ़ोल्डिंग: - [x] > … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।",
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है; :#id निर्भरता पर Ctrl+क्लिक उस ID की पंक्ति पर ले जाता है।" hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है; :#id निर्भरता पर Ctrl+क्लिक उस ID की पंक्ति पर ले जाता है।",
hint_save:"Ctrl+S दस्तावेज़ को फ़ाइल के रूप में सहेजता है — याद रखी गई फ़ाइल (Chromium) में सीधे उसी जगह।"
}, },
zh: { zh: {
subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)", subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)",
@@ -2994,7 +3001,8 @@ const I18N = {
hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。", hint_eff:"节点颜色显示实际状态(含依赖);若自身状态更靠前,会以左下角标记显示。",
hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。", hint_desc:"描述:节点下方的 \" 行;--- 之后的缩进 #id 块为长文本——均显示在提示中(”)。",
hint_fold:"折叠:- [x] > … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。", hint_fold:"折叠:- [x] > … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。",
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野;在依赖 :#id 上 Ctrl+点击可跳转到该 ID 所在的行。" hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野;在依赖 :#id 上 Ctrl+点击可跳转到该 ID 所在的行。",
hint_save:"Ctrl+S 将文档保存为文件——若已记住文件(Chromium),则直接就地写回。"
}, },
ja: { ja: {
subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)", subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)",
@@ -3100,7 +3108,8 @@ const I18N = {
hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。", hint_eff:"ノードの色は実効ステータス(依存関係込み)を示します。自身が先行している場合は左下のマークで表示されます。",
hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。", hint_desc:"説明:ノード直下の \" 行。--- 以降は #id ブロック(字下げ)で長文 — どちらもツールチップに表示(”)。",
hint_fold:"折りたたみ:- [x] > … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。", hint_fold:"折りたたみ:- [x] > … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。",
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。依存関係 :#id を Ctrl+クリックすると、その ID の行へ移動します。" hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。依存関係 :#id を Ctrl+クリックすると、その ID の行へ移動します。",
hint_save:"Ctrl+S は文書をファイルとして保存します。記憶されたファイル(Chromium)には、そのまま直接書き戻します。"
} }
}; };
let lang = 'de'; let lang = 'de';
@@ -3140,7 +3149,7 @@ function buildHint(){
${esc(t('hint_desc'))} ${esc(t('hint_desc'))}
${esc(t('hint_fold'))} ${esc(t('hint_fold'))}
<code>!!!</code>&nbsp; ${esc(t('hint_focus'))} <code>!!!</code>&nbsp; ${esc(t('hint_focus'))}
<div class="hint-op">${esc(t('hint_jump'))}</div>`; <div class="hint-op">${esc(t('hint_jump'))}<br>${esc(t('hint_save'))}</div>`;
} }
function applyLang(l){ function applyLang(l){
lang = l; lang = l;
@@ -3891,6 +3900,11 @@ fileOpenInput.addEventListener('change', async () => {
Stufe-1-Weg; die Bedienung ist in beiden Fällen dieselbe. */ Stufe-1-Weg; die Bedienung ist in beiden Fällen dieselbe. */
const hasFsAccess = typeof window.showOpenFilePicker === 'function'; const hasFsAccess = typeof window.showOpenFilePicker === 'function';
const fileHandles = new Map(); /* docId → FileSystemFileHandle */ const fileHandles = new Map(); /* docId → FileSystemFileHandle */
/* Beim Start werden die gemerkten Handles asynchron zurückgeholt; wer die
Map fürs isSameEntry-Abgleichen braucht (adoptFile), wartet auf dieses
Promise sonst legt ein Doppelklick unmittelbar nach dem App-Start ein
Duplikat an statt dasselbe Dokument zu aktualisieren (D74). */
let handlesReady = Promise.resolve();
/* Handles überleben den Neustart nur in IndexedDB (localStorage kann sie /* Handles überleben den Neustart nur in IndexedDB (localStorage kann sie
nicht halten sie sind nicht JSON-serialisierbar). Alles hier ist Komfort, nicht halten sie sind nicht JSON-serialisierbar). Alles hier ist Komfort,
keine Pflicht: Scheitert IndexedDB, funktioniert Speichern weiter über den keine Pflicht: Scheitert IndexedDB, funktioniert Speichern weiter über den
@@ -3941,6 +3955,7 @@ async function adoptFile(handle, name, text){
flushActive(); flushActive();
let targetId = null; let targetId = null;
if(handle){ if(handle){
await handlesReady; /* gemerkte Handles müssen da sein (D74) */
for(const [id, h] of fileHandles){ for(const [id, h] of fileHandles){
try{ if(await handle.isSameEntry(h) && docs.some(d => d.id === id)){ targetId = id; break; } } try{ if(await handle.isSameEntry(h) && docs.some(d => d.id === id)){ targetId = id; break; } }
catch(_){} catch(_){}
@@ -4008,13 +4023,33 @@ async function saveLocalFile(){
flushActive(); flushActive();
const d = activeDoc(); const d = activeDoc();
if(!d) return; if(!d) return;
let inPlace = false;
if(hasFsAccess){ if(hasFsAccess){
if(!(await saveToKnownFile(d))) await saveWithPicker(d); inPlace = await saveToKnownFile(d);
if(!inPlace) await saveWithPicker(d);
} else { } else {
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name)); saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name));
} }
closeDocMenu(); closeDocMenu();
/* 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);
} }
/* Strg+S / Cmd+S speichert als Datei (D74) die Geste, die jeder zuerst
versucht. Ohne preventDefault öffnet der Browser Seite speichern": genau
der Dialog, den niemand will. Ohne gemerktes Handle verhält sich die Geste
wie der Menü-Eintrag (Dialog in Chromium, Download sonst) ein
Speichern-Dialog auf eine Speichern-Geste ist keine Überraschung. Der
Tastendruck ist zugleich die Nutzergeste, die requestPermission braucht. */
document.addEventListener('keydown', e => {
if((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey
&& (e.key === 's' || e.key === 'S')){
e.preventDefault();
if(!e.repeat) saveLocalFile();
}
});
/* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */ /* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */
function initDocs(){ function initDocs(){
restoring = true; restoring = true;
@@ -4567,7 +4602,7 @@ let startLang = 'de';
try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_){ startLang = detectLang(); } try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_){ startLang = detectLang(); }
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */ applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */ initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
if(hasFsAccess) idbLoadHandles(); /* gemerkte Datei-Handles zurückholen (D72, Stufe 2) */ if(hasFsAccess) handlesReady = idbLoadHandles(); /* gemerkte Datei-Handles zurückholen (D72, Stufe 2) */
/* PWA-Dateihandling (D73): Die installierte App registriert sich über das /* PWA-Dateihandling (D73): Die installierte App registriert sich über das
Manifest (`file_handlers`) für .werkbaum/.txt; ein Doppelklick im Manifest (`file_handlers`) für .werkbaum/.txt; ein Doppelklick im
+4
View File
@@ -502,6 +502,10 @@
} }
.doctrigger:hover,.doctrigger[aria-expanded="true"]{color:var(--or)} .doctrigger:hover,.doctrigger[aria-expanded="true"]{color:var(--or)}
.doctrigger:focus-visible{outline:2px solid var(--or);outline-offset:2px} .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} .docname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.doc-caret{width:13px;height:13px;flex:0 0 auto;opacity:.7} .doc-caret{width:13px;height:13px;flex:0 0 auto;opacity:.7}
.panel.collapsed .doc-caret{display:none} .panel.collapsed .doc-caret{display:none}