diff --git a/README.de.md b/README.de.md index daf3722..ed8d566 100644 --- a/README.de.md +++ b/README.de.md @@ -97,8 +97,8 @@ selbst** und sehen die Änderungen der anderen, ohne neu zu laden: https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/ ``` -Angelegt wird so ein Dokument über den Eintrag **„Auf den Server legen"** im -Dokumenten-Menü: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt +Angelegt wird so ein Dokument über den **Teilen-Knopf** in der Titelzeile des +Text-Editors: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt den Link in die Adresszeile und in die Zwischenablage. Diesen Link weitergeben — die Adresse ist die Einladung, und wer sie kennt, kommt hinein. diff --git a/README.md b/README.md index e734fe1..a26f86c 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ itself** and sees the others' changes without reloading: https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/ ``` -To create such a document, open the document menu and pick **"Put on the -server"**: it uploads the active plan, switches to it, and puts the link in the +To create such a document, press the **Share** button in the text editor's +title bar: it uploads the active plan, switches to it, and puts the link in the address bar and on your clipboard. Share that link — the address is the invitation, and knowing it is what grants access. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4d5a17b..246c3fb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,9 @@ reverse. ## 2026-08-26 +- The document picker moved into the app header: `Werkbaum › name` opens the menu — on the phone it is reachable from both panes, and the name takes over the subtitle line +- The document menu groups by kind — included, yours, sources — carries rename, delete and restore on each row, and scrolls instead of being cut off on small screens +- The editor title bar is labelled `Text editor` again and holds the document's own buttons: save, snapshot, history, reload from the source, and a short `Share` button that puts the plan on a server - The address bar follows the document you switch to: `?live=` and `?sourceUrl=` name what is in front of you, so a reload brings back the same plan - Switching to a server document in the picker now really opens it live — before it only showed its last state - Fix: a foreign change to a server document could land in the text of a local document you had switched to diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 201b43a..da2f29a 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -6901,3 +6901,108 @@ getippt und umgeschaltet, kommt beim Server an. 514 Tests, davon 13 neue in `tests/docurl.test.js`; Gegenproben: fremde Parameter mitwerfen → genau die zwei danach benannten Zusicherungen fallen, `encodeURIComponent` statt der sparsamen Maskierung → genau die sieben, die die Lesbarkeit festhalten. + +## D81 — Dokumenten-Menü neu: Brotkrume im App-Kopf, Stand-Knöpfe in der Editor-Titelzeile +Das Dokumenten-Menü war gewachsen, ohne je gestaltet zu sein: Der Dokumentname +in der Editor-Titelzeile war Auslöser eines Dropdowns, sah aber nicht nach +Menü aus, und das Menü mischte Dokumentwahl, Dateifunktionen und Verwaltung in +einem Knopf-Wust am Fuß. Auf dem Telefon (Brave) war es zudem **unten +abgeschnitten** — sieben Aktions-Knöpfe unter einer Liste, verankert in einer +Titelzeile in der unteren Bildschirmhälfte. Einer Design-Runde mit vier +Mockup-Richtungen (Knopf & Menü am Ort, Schublade, Dokument im App-Kopf, +Palette) folgte die Entscheidung des Nutzers für **C — Dokument im App-Kopf**. + +**Der Name gehört über beide Bereiche, nicht in eine Panel-Zeile.** Das +Dokument bestimmt Text UND Diagramm; im App-Kopf steht es als Brotkrume +**„Werkbaum › Name"** — ein gerahmter Chip mit Pfeil, der endlich nach Menü +aussieht und lange URL-Namen mit Ellipse kürzt (voller Name im Tooltip, wie +gehabt). Nebengewinn auf dem Telefon: Der Kopf ist in beiden Bereichen +sichtbar, die Dokumentwahl ist also auch bei Diagramm-vorn erreichbar — vorher +musste man erst in den Textbereich wechseln (D17). Der Name übernimmt dort die +Zeile des Untertitels; es kommt keine Fläche hinzu (die harte Randbedingung +der Design-Runde: größerer App-Header und ständige Werkzeugleiste waren in +einer früheren Runde genau daran gescheitert). + +**Die Editor-Titelzeile bekommt ihre Beschriftung zurück — „Text-Editor"** +(Nutzer-Vorgabe; vorher „Struktur (Text)", zuletzt ganz vom Wähler verdrängt, +D22) — **und trägt die Stand-Funktionen des aktiven Dokuments** als Knöpfe: + +- **Speichern** (Als Datei speichern, Strg+S — der Blitz samt Haken von D74 + sitzt jetzt hier statt am Dokumentnamen: der Knopf ist die Geste); +- **Stand jetzt sichern** und **Frühere Stände** (unverändert, D54); +- **Neu laden** — kontextabhängig: bei mitgelieferten Dokumenten „Original + wiederherstellen" (ausgegraut, solange unverändert), bei URL-Dokumenten + frisch holen (die URL ist die Quelle der Wahrheit, D23), bei Dateien mit + gemerktem Handle neu aus der Datei lesen (D72); sonst verborgen. Die + Abweichungs-Prüfung hängt am **input-Ereignis**, nicht nur am + Dokumentwechsel — beim Bauen gefunden: Nach dem ersten Tippen blieb der + Knopf ausgegraut, weil die Abweichung beim Tippen entsteht; +- **Teilen** — der neue, kurze Name für „Auf den Server legen" + (D76-Nachtrag 8, Nutzer: „prägnanter und kurzer"): legt das Dokument auf + einen Werkbaum-Server und startet die gemeinsame Bearbeitung; entfällt, wo + es schon liegt (id `live:…`). Aus dem Menü heraus, in die Titelzeile hinein + — es ist eine Funktion des aktiven Dokuments, keine Dokumentverwaltung. + +**Das Menü selbst: Gruppen nach Dokumentart, Aktionen an der Zeile.** Drei +Gruppen — **Mitgeliefert · Eigene · Quellen** (Server- und URL-Dokumente); +die Art steckt in der id und ist als `docKind()` headless in docurl.js +(Hausregel D54-Nachtrag 3, getestet). Umbenennen, Löschen und — bei +abweichenden mitgelieferten — Wiederherstellen hängen als **Symbole an der +Zeile des jeweiligen Dokuments**, immer sichtbar (Touch kennt kein Hover; die +Hover-only-Variante des Mockups hätte dort versagt). Damit wirken sie auf +**jedes** Dokument, nicht mehr nur auf das aktive, und das Menü bleibt nach +einer Verwaltungs-Aktion **offen** — wer aufräumt, räumt meist weiter. Kein +Knopf im Knopf: Die Zeile ist ein `div`, Wählen-Knopf und Aktions-Knöpfe sind +Geschwister (verschachtelte interaktive Elemente sind ungültiges HTML). Unten +bleiben zwei Befehle als gewöhnliche Menüzeilen: „+ Neues Dokument" und +„Datei öffnen…". + +**Gegen das Abschneiden auf dem Telefon: scrollen, nicht klappen.** Das Menü +bekommt `max-height` und scrollt **als Ganzes**; die vom Nutzer alternativ +vorgeschlagenen auf-/zuklappbaren Gruppen sind verworfen — sie brauchten +einen gemerkten Klapp-Zustand und versteckten Dokumente hinter einem zweiten +Klick, während das Scrollen zustandslos ist und nichts versteckt. Dazu kommt, +dass das Menü jetzt **oben** hängt statt in der Mitte des Bildschirms — der +Platz darunter ist das Mehrfache des alten. + +**Drei Messbefunde beim Bauen, alle in derselben Sitzung behoben:** + +- **Das Menü spannte auf Mobil nur 155 px** statt der vollen Breite: Bezug + war die Marken-Gruppe, und die ist nur so breit, wie die Werkzeuge rechts + ihr lassen. Dieselbe Verlegung wie beim Neuigkeiten-Popup (D58): Bezug ist + die ganze Kopfzeile; die Marken-Gruppe löst sich auf Mobil per + `display:contents` auf, damit die Chip-Zeile per `order` unter Marke und + Werkzeuge umbricht — gemessen danach 335 px, `elementFromPoint` trifft + (die D50-Prüfung). +- **Marke und Werkzeuge brachen in zwei Zeilen** (Kopf: drei Zeilen = neue + Fläche): 164 + 164 px bei 335 px Breite. Wie in D17-Nachtrag 5 weichen die + **Lücken, nicht die Knöpfe** — nachgemessen mit eingeblendetem + Neuigkeiten-Zähler UND Build-Hinweis (die D17-Lektion, mit dem versteckten + Element zu messen): Worst Case 325 von 335 px, eine Zeile, Kopf 63 px. + Bei 320 px bricht es ehrlich um (Marke und Werkzeuge passen physisch nicht + nebeneinander) — dieselbe Philosophie wie die 440-px-Schwelle (D50). +- **Der Neu-laden-Knopf blieb nach dem Tippen ausgegraut** (siehe oben). + +**Was mitgeht:** Der Collapsed-Editor zeigt jetzt „TEXT-EDITOR" (senkrecht im +Seitenmodus) statt des Dokumentnamens; der Klick-Wächter am alten Auslöser +(minimierter Editor) entfällt — im App-Kopf gibt es nichts wiederherzustellen. +Ein offenes Menü wechselt die Sprache mit (wie das Neuigkeiten-Popup). Der +Untertitel-Kurztext (`subtitleShort`, 9 Sprachen) ist ersatzlos ausgebaut — +seine Zeile gehört jetzt dem Dokumentnamen. + +**Werkzeuggrenzen, wieder dieselbe Sorte wie D25/D17-Nachtrag 4:** Nach einem +programmatischen Resize lieferte das Browser-Pane Miniatur-Screenshots einer +Seite, die sich selbst nachweislich korrekt maß (1280 × 860), und die +`matchMedia`-Umschaltung von `body.mobile` feuerte nicht — beides +Umgebungs-Artefakte, kein Befund; entschieden haben die Messwerte im +Dokument, nicht die Bilder. + +**Nachgemessen** (518 Tests, davon 4 neue für `docKind`; Browser Desktop und +375/320 px): Chip im Kopf öffnet das gruppierte Menü, `elementFromPoint` +trifft es auf allen Breiten; Neues Dokument geht in die Inline-Benennung mit +markiertem Vorschlag und landet unter „Eigene"; ein bearbeitetes Beispiel +zeigt das Zeilen-Restore-Symbol und aktiviert Neu laden, Wiederherstellen +stellt Text und Knopfzustände zurück, ohne das Menü zu schließen; Löschen +eines nicht-aktiven Dokuments lässt den Editor unangetastet; auf dem Telefon +ist der Wähler aus dem Diagramm-Bereich erreichbar und der Wechsel rendert +183 Knoten, langer Name füllt die 335-px-Chipzeile mit Ellipse. diff --git a/docs/SPEC.md b/docs/SPEC.md index ac34e63..7f71a45 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -561,10 +561,12 @@ Ab schmaler Breite (≤ 640 px, z. B. Smartphone) ist **immer genau ein Bereich* zu sehen — Diagramm **oder** Text, jeweils über die ganze Fläche. Oben links in der Titelzeile schaltet ein **Knopf** auf den anderen um; er zeigt das Ziel (Textzeilen bzw. Baum), denn den Zustand hat man ohnehin vor sich. Es gibt hier -weder Splitter noch Min/Normal/Max-Buttons — es ist nichts zu teilen. Der -Bereichstitel „DIAGRAMM" entfällt; die Zeile wird für Umschalter und Aktionen -gebraucht. Der **Dokumenten-Wähler** steht direkt neben dem Umschalter und ist -damit genau dann zu sehen, wenn der Text vorn ist. Die übrigen +weder Splitter noch Min/Normal/Max-Buttons — es ist nichts zu teilen. Die +Bereichstitel („DIAGRAMM", „TEXT-EDITOR") entfallen; die Zeile wird für +Umschalter und Aktionen gebraucht. Der **Dokumenten-Wähler** steht als +Brotkrume im App-Kopf („Werkbaum › Name", D81) und ist damit aus **beiden** +Bereichen erreichbar; sein Name übernimmt hier die Zeile des Untertitels — +es kommt keine Fläche hinzu. Die übrigen **Aktions-Knöpfe** bleiben rechts in der Titelzeile und werden mit umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen komprimierten Form. Der sichtbare Bereich wird gemerkt; ein Sprung zwischen diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index eea182d..2459524 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -71,7 +71,7 @@ - [^] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS) %% no pointer, no tooltip - [^] #ed.tipwindow: The same window at the pointer and on keyboard focus (XS) %% a title has no paragraphs and never shows on focus - [^] #ed.docs: Several documents in one editor (M) - - [^] #ed.docs.picker: Switcher in the editor title bar (S) + - [x] #ed.docs.picker: Breadcrumb picker in the app header (S) %% Werkbaum › name, see D81 - [^] #ed.docs.url: Load a document from ?sourceUrl= (S) - [^] #ed.docs.restore: Restore a shipped document from the menu (XS) - [^] #ed.jump: Jump between diagram and text (S) @@ -151,7 +151,7 @@ | [?] #col.live.crdt: Text CRDT — the plan is plain text, so it fits (L) | [?] #col.live.ot: Operational transformation (XL) - [^] #col.live.ask: On a real overlap, ask whose version wins (S) - - [^] #col.live.upload: Put a local plan on the server, from the menu (S) + - [x] #col.live.upload: Share: put a local plan on the server (S) %% a button in the editor title bar, see D81 - [?] #col.live.cursors: Cursors and who else is here (S) - [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough - [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent @@ -531,8 +531,10 @@ without changing anything. #ed.docs.picker - The name of the active document replaces the panel title and opens the menu - for switching, creating, renaming and deleting. + The app header reads "Werkbaum > name": the chip opens the menu, grouped by + kind (shipped, yours, sources), with rename, delete and restore on each row. + It scrolls as a whole instead of being cut off on small screens, and on the + phone it is reachable from both panes. #ed.docs.url A link can carry the address of a text file, which is then fetched on every @@ -901,10 +903,10 @@ for the same result. #col.live.upload - Without it the only way onto the server was a curl command. The local plan - stays where it is: whoever entrusts their only copy to a server should not - lose it in the act. The picker names the host beside a server document, so - the two entries are told apart. + Without it the only way onto the server was a curl command. A share button + in the editor title bar; the local plan stays where it is: whoever entrusts + their only copy to a server should not lose it in the act. The picker names + the host beside a server document, so the two entries are told apart. #col.live.cursors Seeing where the others are. A by-product of the merge layer rather than a diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index d5dda0d..67d83e0 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -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 diff --git a/frontend/index.html b/frontend/index.html index adc83c3..45add8d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,13 @@
-

+ +
+

Werkbaum

+ + + + + + +
+

Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree/Requirements-Baum Diagramm-Editor.

- + - - - - - - - - @@ -102,6 +113,24 @@ + + + + + + + +
diff --git a/frontend/src/app.js b/frontend/src/app.js index 27d40b2..2c9f343 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -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 = ''; +const IC_DELETE = ''; +const IC_RESTORE = ''; +/* 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 `
` + + `
`; + } + /* 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 ? `${esc(host)}` : ''; + const shipped = shippedStateOf(d.id); + const abweichend = shipped && (d.text !== shipped.text || d.name !== shipped.name); + const iconBtn = (act, label, svg, extra) => + ``; + return `
` + + `` + + `` + + (abweichend ? iconBtn('restore', t('docRestore'), IC_RESTORE) : '') + + iconBtn('rename', t('docRename'), IC_RENAME) + + iconBtn('delete', t('docDelete'), IC_DELETE, ' docdelbtn') + + `
`; +} 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 `
` + - `
`; - } - /* 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 ? `${esc(host)}` : ''; - return ``; + /* 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 `` + + 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 diff --git a/frontend/src/docurl.js b/frontend/src/docurl.js index 0a296fc..fd1a17a 100644 --- a/frontend/src/docurl.js +++ b/frontend/src/docurl.js @@ -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`. diff --git a/frontend/src/style.css b/frontend/src/style.css index 5cf23aa..25a637c 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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; } diff --git a/frontend/tests/docurl.test.js b/frontend/tests/docurl.test.js index cc64573..58411f3 100644 --- a/frontend/tests/docurl.test.js +++ b/frontend/tests/docurl.test.js @@ -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 */ + }); +});