frontend: Mehrere umschaltbare Dokumente (Wähler in der Editor-Titelzeile)

Der Editor kann jetzt mehrere Notationstexte halten und zwischen ihnen
umschalten. Der Name des aktiven Dokuments ersetzt die feste Beschriftung
„Struktur (Text)" und öffnet als Dropdown den Wähler mit Wechseln, Neu,
Umbenennen und Löschen.

- Datenmodell (noch kein Backend): Dokumente als [{id,name,text}] in
  localStorage (werkbaum-docs), aktives per id in werkbaum-active; der
  aktive Text bleibt zusätzlich in werkbaum-src gespiegelt.
- Migration verlustfrei: fehlt werkbaum-docs, wird der bestehende Einzeltext
  (oder INITIAL) in ein Dokument gepackt. Reset und Löschen des letzten
  Dokuments säen das Beispiel neu.
- Jedes Dokument ist nur Text + Name (kein Strukturformat, D14) —
  vorwärtskompatibel zum geplanten Backend (D13). Ansichts-State (Modus,
  Zoom, Aufteilung) bleibt global.
- i18n in allen 9 Sprachen; Doc-Namen bleiben Nutzerdaten (nicht übersetzt).
- Dokumentiert als ADR D22 (DECISIONS.md) + frontend/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-23 11:48:31 +02:00
co-authored by Claude Opus 4.8
parent 5a0a06b185
commit 36d79b82c4
5 changed files with 293 additions and 11 deletions
+34
View File
@@ -329,3 +329,37 @@ Not eine deutsche Oberfläche. Die Erkennung greift auch **nach dem Reset**
(CLAUDE: neue UI-Texte zuerst auf Deutsch) — das betrifft die Autoren-/Pflege­ (CLAUDE: neue UI-Texte zuerst auf Deutsch) — das betrifft die Autoren-/Pflege­
seite und ist unabhängig vom Anzeige-Default für Besucher. Eine bewusste seite und ist unabhängig vom Anzeige-Default für Besucher. Eine bewusste
Sprachwahl überschreibt die Erkennung dauerhaft (Persistenz in `werkbaum-lang`). Sprachwahl überschreibt die Erkennung dauerhaft (Persistenz in `werkbaum-lang`).
## D22 — Mehrere Dokumente client-seitig, Wähler in der Editor-Titelzeile
Der Editor kann mehrere Notationstexte halten, zwischen denen umgeschaltet wird
(z. B. verschiedene Projekte/Bäume). Umgesetzt **ohne Backend** (noch keins,
D13): die Dokumente liegen als `[{id, name, text}]` im localStorage
(`werkbaum-docs`), das aktive per `id` in `werkbaum-active`. Der aktive Text
wird zusätzlich in `werkbaum-src` gespiegelt (Abwärtskompatibilität + Migration).
**Platzierung: Dropdown in der Editor-Titelzeile.** Der Name des aktiven
Dokuments **ersetzt** die statische Beschriftung „Struktur (Text)" und ist
zugleich der Auslöser eines Dropdowns zum Wechseln, Anlegen (` Neu`),
Umbenennen und Löschen. Begründung: Der Wähler bestimmt, *welchen Text* man
bearbeitet — er gehört auf das Textpanel, nicht ins Diagramm. Verworfen:
**Kopfzeile oben** (schon eng, besonders mobil) und **Tab-Leiste** (kostet eine
ganze Zeile Höhe, D17, und skaliert nicht über ~5 Dokumente). Ein Dropdown
skaliert und passt zum bereits etablierten Overlay-Idiom (Sprache/Download, D17).
Auf kleinem Bildschirm öffnet dasselbe Menü als absolut positioniertes Overlay
unter der Titelzeile.
**Vereinbarkeit mit D14 (Text ist das führende Format):** Jedes Dokument ist nur
ein Notationstext plus `name` (Metadatum) — **kein** erfundenes Strukturformat.
Damit ist das Modell vorwärtskompatibel zum geplanten Persistenz-/Taiga-Backend
(D13/D14: „Text als Ganzes + explizite Metadaten"): „mehrere Dokumente" bildet
1:1 auf „mehrere gespeicherte Notationstexte mit Name/id" ab; der localStorage-
Array ist der client-seitige Platzhalter, bis das Backend existiert.
**Migration & Reset:** Fehlt `werkbaum-docs`, wird der bestehende Einzeltext
(oder `INITIAL`) verlustfrei in **ein** Dokument gepackt (Name in der erkannten
UI-Sprache, `docDefaultName`). Der Reset löscht wie bisher alle `werkbaum-*`-
Schlüssel → beim nächsten Laden greift dieselbe Migration. Wird das letzte
Dokument gelöscht, entsteht wieder das Beispiel-Dokument (INITIAL). Die
GUI-Ansichts-Einstellungen (Modus, Zoom, Aufteilung; `werkbaum-ui`) bleiben
bewusst **global** über alle Dokumente — pro-Dokument-Ansichtszustand wäre eine
spätere Erweiterung.
+16 -5
View File
@@ -81,13 +81,24 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
(`a11y*`) in **allen 9 Sprachen** anlegen. Knoten sind `tabindex="0"` (`a11y*`) in **allen 9 Sprachen** anlegen. Knoten sind `tabindex="0"`
(Fokus = Lesereihenfolge), `#warn` ist eine Live-Region. (Fokus = Lesereihenfolge), `#warn` ist eine Live-Region.
- Zustand wird im `localStorage` gehalten (noch kein Backend): `werkbaum-lang` - Zustand wird im `localStorage` gehalten (noch kein Backend): `werkbaum-lang`
(Sprache), `werkbaum-src` (Editortext), `werkbaum-ui` (JSON: Modus, (Sprache), `werkbaum-docs` (JSON-Array der Dokumente `[{id,name,text}]`),
verworfene, günstigster Pfad, Split-Zustand inkl. `--col`/`--drow`, Zoom, `werkbaum-active` (id des aktiven Dokuments), `werkbaum-src` (Spiegel des
Vollbild). Neue aktiven Texts, Abwärtskompatibilität), `werkbaum-ui` (JSON: Modus, verworfene,
günstigster Pfad, Split-Zustand inkl. `--col`/`--drow`, Zoom, Vollbild). Neue
GUI-Einstellungen in `saveUI()`/`restoreState()` mitführen; `saveUI` liefert GUI-Einstellungen in `saveUI()`/`restoreState()` mitführen; `saveUI` liefert
während `restoring===true` nichts, damit das Wiederherstellen nicht sofort während `restoring===true` nichts, damit das Wiederherstellen nicht sofort
zurückschreibt. Fehlender `werkbaum-src` fällt auf `INITIAL` zurück, ein zurückschreibt.
leerer String bleibt jedoch leer. - Dokumente (D22): mehrere umschaltbare Notationstexte. `loadDocs()` migriert bei
fehlendem `werkbaum-docs` den bestehenden `werkbaum-src` (oder `INITIAL`) in
**ein** Dokument; `initDocs()` (Aufruf **nach** `applyLang`, damit der
Standardname in der erkannten UI-Sprache steht) 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 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; das letzte gelöschte Dokument wird als `INITIAL` neu gesät.
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt - Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
+12 -1
View File
@@ -46,7 +46,18 @@
<div class="app" id="app"> <div class="app" id="app">
<div class="panel editor"> <div class="panel editor">
<div class="panel-head"> <div class="panel-head">
<span data-i18n="editorTitle">Struktur (Text)</span> <span class="doctrigger" id="docTrigger" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false" data-i18n-title="docSwitchTooltip" data-i18n-aria="docSwitchTooltip" title="Dokument wählen oder verwalten" aria-label="Dokument wählen oder verwalten">
<span class="docname" id="docName"></span>
<svg class="doc-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</span>
<div class="docmenu" id="docMenu" role="menu" data-i18n-aria="docMenuAria" aria-label="Dokumente" hidden>
<div class="docmenu-list" id="docList"></div>
<div class="docmenu-actions">
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button>
<button type="button" class="docact" id="docRename" data-i18n="docRename">Umbenennen</button>
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
</div>
</div>
<button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren"> <button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> <svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
+187 -4
View File
@@ -572,6 +572,10 @@ const I18N = {
fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite", fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite",
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).", brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm", editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
docSwitchTooltip:"Dokument wählen oder verwalten", docMenuAria:"Dokumente",
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
docDefaultName:"Beispiel", docNewName:"Unbenannt",
docRenamePrompt:"Dokument umbenennen:", docDeleteConfirm:"Dokument „{name}“ löschen?",
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren", copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren", copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren",
downloadDiagramTooltip:"Diagramm als SVG-Datei herunterladen (z. B. für LibreOffice: Einfügen → Bild)", downloadDiagramTooltip:"Diagramm als SVG-Datei herunterladen (z. B. für LibreOffice: Einfügen → Bild)",
@@ -613,6 +617,10 @@ const I18N = {
fullscreenTooltip:"Full screen panels use the full window width", fullscreenTooltip:"Full screen panels use the full window width",
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).", brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
editorTitle:"Structure (text)", diagramTitle:"Diagram", editorTitle:"Structure (text)", diagramTitle:"Diagram",
docSwitchTooltip:"Choose or manage document", docMenuAria:"Documents",
docNew:"New document", docRename:"Rename", docDelete:"Delete",
docDefaultName:"Example", docNewName:"Untitled",
docRenamePrompt:"Rename document:", docDeleteConfirm:"Delete document “{name}”?",
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard", copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard", copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard",
downloadDiagramTooltip:"Download diagram as an SVG file (e.g. for LibreOffice: Insert → Image)", downloadDiagramTooltip:"Download diagram as an SVG file (e.g. for LibreOffice: Insert → Image)",
@@ -654,6 +662,10 @@ const I18N = {
fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana", 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).", 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:"Estructura (texto)", diagramTitle:"Diagrama",
docSwitchTooltip:"Elegir o gestionar documento", docMenuAria:"Documentos",
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
docDefaultName:"Ejemplo", docNewName:"Sin título",
docRenamePrompt:"Renombrar documento:", docDeleteConfirm:"¿Eliminar el documento «{name}»?",
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles", copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles", copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles",
downloadDiagramTooltip:"Descargar el diagrama como archivo SVG (p. ej. para LibreOffice: Insertar → Imagen)", downloadDiagramTooltip:"Descargar el diagrama como archivo SVG (p. ej. para LibreOffice: Insertar → Imagen)",
@@ -695,6 +707,10 @@ const I18N = {
fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre", fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre",
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).", brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
editorTitle:"Structure (texte)", diagramTitle:"Diagramme", editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
docSwitchTooltip:"Choisir ou gérer le document", docMenuAria:"Documents",
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
docDefaultName:"Exemple", docNewName:"Sans titre",
docRenamePrompt:"Renommer le document :", docDeleteConfirm:"Supprimer le document « {name} » ?",
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers", copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers", copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers",
downloadDiagramTooltip:"Télécharger le diagramme en fichier SVG (p. ex. pour LibreOffice : Insertion → Image)", downloadDiagramTooltip:"Télécharger le diagramme en fichier SVG (p. ex. pour LibreOffice : Insertion → Image)",
@@ -736,6 +752,10 @@ const I18N = {
fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna", fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna",
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).", brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram", editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
docSwitchTooltip:"Wybierz lub zarządzaj dokumentem", docMenuAria:"Dokumenty",
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
docDefaultName:"Przykład", docNewName:"Bez nazwy",
docRenamePrompt:"Zmień nazwę dokumentu:", docDeleteConfirm:"Usunąć dokument „{name}”?",
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka", copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka", copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka",
downloadDiagramTooltip:"Pobierz diagram jako plik SVG (np. dla LibreOffice: Wstaw → Obraz)", downloadDiagramTooltip:"Pobierz diagram jako plik SVG (np. dla LibreOffice: Wstaw → Obraz)",
@@ -777,6 +797,10 @@ const I18N = {
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна", fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма", editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы",
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
docDefaultName:"Пример", docNewName:"Без названия",
docRenamePrompt:"Переименовать документ:", docDeleteConfirm:"Удалить документ «{name}»?",
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена", copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена", copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
downloadDiagramTooltip:"Скачать диаграмму как файл SVG (напр. для LibreOffice: Вставка → Изображение)", downloadDiagramTooltip:"Скачать диаграмму как файл SVG (напр. для LibreOffice: Вставка → Изображение)",
@@ -818,6 +842,10 @@ const I18N = {
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं", fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख", editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़",
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
docDefaultName:"उदाहरण", docNewName:"बिना शीर्षक",
docRenamePrompt:"दस्तावेज़ का नाम बदलें:", docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें", copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें", copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
downloadDiagramTooltip:"आरेख को SVG फ़ाइल के रूप में डाउनलोड करें (जैसे LibreOffice: सम्मिलित करें → छवि)", downloadDiagramTooltip:"आरेख को SVG फ़ाइल के रूप में डाउनलोड करें (जैसे LibreOffice: सम्मिलित करें → छवि)",
@@ -859,6 +887,10 @@ const I18N = {
ghostTooltip:"从 M 号起,元素应进一步细分。", ghostTooltip:"从 M 号起,元素应进一步细分。",
riskTooltip:"高风险 – 工作量尚不明确。", riskTooltip:"高风险 – 工作量尚不明确。",
editorTitle:"结构(文本)", diagramTitle:"图表", editorTitle:"结构(文本)", diagramTitle:"图表",
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
docNew:"新建文档", docRename:"重命名", docDelete:"删除",
docDefaultName:"示例", docNewName:"未命名",
docRenamePrompt:"重命名文档:", docDeleteConfirm:"删除文档“{name}”?",
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板", copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板", copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
downloadDiagramTooltip:"将图表下载为 SVG 文件(例如用于 LibreOffice:插入 → 图像)", downloadDiagramTooltip:"将图表下载为 SVG 文件(例如用于 LibreOffice:插入 → 图像)",
@@ -900,6 +932,10 @@ const I18N = {
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。", ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
riskTooltip:"高リスク – 規模はまだ不明。", riskTooltip:"高リスク – 規模はまだ不明。",
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム", editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
docDefaultName:"例", docNewName:"無題",
docRenamePrompt:"ドキュメントの名前を変更:", docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー", copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー", copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
downloadDiagramTooltip:"ダイアグラムを SVG ファイルとしてダウンロード(例:LibreOffice の 挿入 → 画像)", downloadDiagramTooltip:"ダイアグラムを SVG ファイルとしてダウンロード(例:LibreOffice の 挿入 → 画像)",
@@ -1026,11 +1062,56 @@ fsBtn.addEventListener('click', () => {
Vollbild) und der Editortext bleiben per localStorage über Neuladen Vollbild) und der Editortext bleiben per localStorage über Neuladen
erhalten. Sprache liegt weiterhin in 'werkbaum-lang'. */ erhalten. Sprache liegt weiterhin in 'werkbaum-lang'. */
const LS_UI = 'werkbaum-ui', LS_SRC = 'werkbaum-src'; const LS_UI = 'werkbaum-ui', LS_SRC = 'werkbaum-src';
const LS_DOCS = 'werkbaum-docs', LS_ACTIVE = 'werkbaum-active';
let restoring = false; /* unterdrückt Speichern während des Wiederherstellens */ let restoring = false; /* unterdrückt Speichern während des Wiederherstellens */
let hadStoredUI = false; /* gab es beim Laden schon gespeicherte GUI-Einstellungen? */ let hadStoredUI = false; /* gab es beim Laden schon gespeicherte GUI-Einstellungen? */
/* ---------- Dokumente (mehrere umschaltbare Notationstexte) ----------
Noch kein Backend: mehrere Dokumente liegen als [{id,name,text}] im
localStorage (LS_DOCS), das aktive per id in LS_ACTIVE. Jedes Dokument ist
nur ein Notationstext + Name (Metadatum) — kein eigenes Strukturformat (D14).
Der aktive Text wird zusätzlich in LS_SRC gespiegelt (Abwärtskompatibilität
+ Migration bestehender Einzeltexte). */
let docs = []; /* [{id, name, text}] */
let activeId = null;
function uid(){ return 'd' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
function activeDoc(){ return docs.find(d => d.id === activeId) || null; }
function uniqueName(base){
const taken = new Set(docs.map(d => d.name));
if(!taken.has(base)) return base;
let i = 2;
while(taken.has(base + ' ' + i)) i++;
return base + ' ' + i;
}
function persistDocs(){
try{
localStorage.setItem(LS_DOCS, JSON.stringify(docs));
localStorage.setItem(LS_ACTIVE, activeId || '');
const d = activeDoc();
if(d) localStorage.setItem(LS_SRC, d.text); /* Spiegel für Fallback/Migration */
}catch(_){}
}
/* Aus dem localStorage laden; bei fehlender Dokumentenliste den bestehenden
Einzeltext (oder INITIAL) als erstes Dokument migrieren. */
function loadDocs(){
let arr = null;
try{ arr = JSON.parse(localStorage.getItem(LS_DOCS) || 'null'); }catch(_){}
if(!Array.isArray(arr) || !arr.length ||
!arr.every(d => d && typeof d.id === 'string' && typeof d.text === 'string')){
let legacy = null;
try{ legacy = localStorage.getItem(LS_SRC); }catch(_){}
arr = [{ id: uid(), name: t('docDefaultName'), text: (legacy !== null) ? legacy : INITIAL }];
}
docs = arr;
let a = null;
try{ a = localStorage.getItem(LS_ACTIVE); }catch(_){}
activeId = docs.some(d => d.id === a) ? a : docs[0].id;
}
function saveSrc(){ function saveSrc(){
if(restoring) return; if(restoring) return;
try{ localStorage.setItem(LS_SRC, src.value); }catch(_){} const d = activeDoc();
if(d) d.text = src.value;
persistDocs();
} }
function saveUI(){ function saveUI(){
if(restoring) return; if(restoring) return;
@@ -1050,11 +1131,11 @@ function saveUI(){
} }
function restoreState(){ function restoreState(){
restoring = true; restoring = true;
let savedSrc = null, ui = null; let ui = null;
try{ savedSrc = localStorage.getItem(LS_SRC); }catch(_){}
try{ ui = JSON.parse(localStorage.getItem(LS_UI) || 'null'); }catch(_){} try{ ui = JSON.parse(localStorage.getItem(LS_UI) || 'null'); }catch(_){}
hadStoredUI = !!ui; hadStoredUI = !!ui;
src.value = (savedSrc !== null) ? savedSrc : INITIAL; /* leerer String bleibt leer */ /* Editortext gehört jetzt den Dokumenten (initDocs, nach applyLang), damit der
Standard-Dokumentname in der erkannten UI-Sprache steht. */
const mode = (ui && ui.mode) || 'horizontal'; const mode = (ui && ui.mode) || 'horizontal';
const modeEl = document.querySelector('input[name="layout"][value="' + mode + '"]'); const modeEl = document.querySelector('input[name="layout"][value="' + mode + '"]');
if(modeEl) modeEl.checked = true; if(modeEl) modeEl.checked = true;
@@ -1081,6 +1162,107 @@ function restoreState(){
restoring = false; restoring = false;
} }
/* ---------- Dokument-Wähler: UI (Dropdown in der Editor-Titelzeile) ---------- */
const docTrigger = document.getElementById('docTrigger');
const docMenu = document.getElementById('docMenu');
const docNameEl = document.getElementById('docName');
const docList = document.getElementById('docList');
function updateDocName(){
const d = activeDoc();
if(docNameEl) docNameEl.textContent = d ? d.name : '';
}
function renderDocMenu(){
docList.innerHTML = docs.map(d =>
`<button type="button" class="docitem" role="menuitemradio" data-id="${d.id}" ` +
`aria-checked="${d.id === activeId ? 'true' : 'false'}">` +
`<span class="doccheck" aria-hidden="true">✓</span>` +
`<span class="docitem-name">${esc(d.name)}</span></button>`
).join('');
}
function openDocMenu(){ renderDocMenu(); docMenu.hidden = false; docTrigger.setAttribute('aria-expanded', 'true'); }
function closeDocMenu(){ docMenu.hidden = true; docTrigger.setAttribute('aria-expanded', 'false'); }
function toggleDocMenu(){ docMenu.hidden ? openDocMenu() : closeDocMenu(); }
/* Beim Wechseln/Anlegen/Löschen zuerst den aktuellen Editortext ins aktive
Dokument sichern, dann das Ziel laden und neu rendern. */
function flushActive(){ const d = activeDoc(); if(d) d.text = src.value; }
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : ''; render(); updateDocName(); }
function switchDoc(id){
if(id === activeId) return;
flushActive();
activeId = id;
loadActiveIntoEditor();
persistDocs();
}
function newDoc(){
flushActive();
const d = { id: uid(), name: uniqueName(t('docNewName')), text: '' };
docs.push(d);
activeId = d.id;
loadActiveIntoEditor();
persistDocs();
src.focus();
}
function renameDoc(){
const d = activeDoc();
if(!d) return;
const name = window.prompt(t('docRenamePrompt'), d.name);
if(name === null) return;
const trimmed = name.trim();
if(!trimmed) return;
d.name = trimmed;
persistDocs();
updateDocName();
renderDocMenu();
}
function deleteDoc(){
const d = activeDoc();
if(!d) return;
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
docs = docs.filter(x => x.id !== d.id);
if(!docs.length) docs = [{ id: uid(), name: t('docDefaultName'), text: INITIAL }];
activeId = docs[0].id;
loadActiveIntoEditor();
persistDocs();
closeDocMenu();
}
/* Dokumente laden + aktiven Text in den Editor holen. Nach applyLang aufgerufen,
damit der migrierte Standardname in der erkannten UI-Sprache steht. */
function initDocs(){
restoring = true;
loadDocs();
const d = activeDoc();
src.value = d ? d.text : '';
restoring = false;
persistDocs(); /* migrierte/geladene Liste festschreiben (stabil über Reload) */
updateDocName();
render();
}
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();
});
docTrigger.addEventListener('keydown', e => {
if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); toggleDocMenu(); }
else if(e.key === 'Escape') closeDocMenu();
});
docList.addEventListener('click', e => {
const btn = e.target.closest('.docitem');
if(!btn) return;
e.stopPropagation();
switchDoc(btn.dataset.id);
closeDocMenu();
});
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); renderDocMenu(); });
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); });
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); });
/* Klick außerhalb schließt das Menü. */
document.addEventListener('click', e => {
if(!docMenu.hidden && !docMenu.contains(e.target) && !docTrigger.contains(e.target)) closeDocMenu();
});
/* ---------- Kleiner Bildschirm: nur ein Panel, schlanke Sprachwahl, Vollbild ---------- */ /* ---------- Kleiner Bildschirm: nur ein Panel, schlanke Sprachwahl, Vollbild ---------- */
const mqMobile = window.matchMedia('(max-width:640px)'); const mqMobile = window.matchMedia('(max-width:640px)');
function isMobile(){ return mqMobile.matches; } function isMobile(){ return mqMobile.matches; }
@@ -1162,6 +1344,7 @@ function detectLang(){
let startLang = 'de'; 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) */
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */ applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
/* ---------- Build-Hinweis (Vorschau/Dev + „latest build") ---------- /* ---------- Build-Hinweis (Vorschau/Dev + „latest build") ----------
+44 -1
View File
@@ -239,7 +239,7 @@
display:flex;flex-direction:column;min-height:0;min-width:0; display:flex;flex-direction:column;min-height:0;min-width:0;
} }
.panel-head{ .panel-head{
flex:0 0 auto; flex:0 0 auto;position:relative;
padding:10px 16px; padding:10px 16px;
display:flex;justify-content:space-between;align-items:center;gap:14px; display:flex;justify-content:space-between;align-items:center;gap:14px;
font-family:'IBM Plex Mono',monospace; font-family:'IBM Plex Mono',monospace;
@@ -308,6 +308,49 @@
.dlmenu{display:inline-flex;gap:3px} .dlmenu{display:inline-flex;gap:3px}
.dltrigger{display:none} .dltrigger{display:none}
.legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */ .legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */
/* 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). */
.doctrigger{
display:inline-flex;align-items:center;gap:5px;cursor:pointer;
margin-right:auto;max-width:60%;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}
.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}
.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)}
.docdel:hover{border-color:#D897A8;color:#B4415E}
.togglebtn[aria-pressed="true"]{background:var(--or);color:#fff;border-color:var(--or)} .togglebtn[aria-pressed="true"]{background:var(--or);color:#fff;border-color:var(--or)}
.togglebtn[aria-pressed="true"]:hover{color:#fff} .togglebtn[aria-pressed="true"]:hover{color:#fff}
textarea{ textarea{