diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 2cf7dad..4954fdc 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -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­ seite und ist unabhängig vom Anzeige-Default für Besucher. Eine bewusste 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. diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index a8d023d..8a84c28 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -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"` (Fokus = Lesereihenfolge), `#warn` ist eine Live-Region. - Zustand wird im `localStorage` gehalten (noch kein Backend): `werkbaum-lang` - (Sprache), `werkbaum-src` (Editortext), `werkbaum-ui` (JSON: Modus, - verworfene, günstigster Pfad, Split-Zustand inkl. `--col`/`--drow`, Zoom, - Vollbild). Neue + (Sprache), `werkbaum-docs` (JSON-Array der Dokumente `[{id,name,text}]`), + `werkbaum-active` (id des aktiven Dokuments), `werkbaum-src` (Spiegel des + 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 während `restoring===true` nichts, damit das Wiederherstellen nicht sofort - zurückschreibt. Fehlender `werkbaum-src` fällt auf `INITIAL` zurück, ein - leerer String bleibt jedoch leer. + zurückschreibt. +- 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 Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den diff --git a/frontend/index.html b/frontend/index.html index a1b71bf..69e36e6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -46,7 +46,18 @@
- Struktur (Text) + + ` + ).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 ---------- */ const mqMobile = window.matchMedia('(max-width:640px)'); function isMobile(){ return mqMobile.matches; } @@ -1162,6 +1344,7 @@ function detectLang(){ let startLang = 'de'; try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_){ startLang = detectLang(); } 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 */ /* ---------- Build-Hinweis (Vorschau/Dev + „latest build") ---------- diff --git a/frontend/src/style.css b/frontend/src/style.css index 5868b10..0af724d 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -239,7 +239,7 @@ display:flex;flex-direction:column;min-height:0;min-width:0; } .panel-head{ - flex:0 0 auto; + flex:0 0 auto;position:relative; padding:10px 16px; display:flex;justify-content:space-between;align-items:center;gap:14px; font-family:'IBM Plex Mono',monospace; @@ -308,6 +308,49 @@ .dlmenu{display:inline-flex;gap:3px} .dltrigger{display:none} .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"]:hover{color:#fff} textarea{