diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 3744f14..8e6bae8 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -134,3 +134,16 @@ zählt der flache CI-Klon nur einen Commit. `1.0` bleibt die Version beim lokale Öffnen (Platzhalter im Quelltext). (Nummerierung: D15 war bereits für den kompakten Modus vergeben, daher D16.) + +## D17 — Kleiner Bildschirm: ein Bereich, kompakte Legende & Sprachwahl +Auf schmalen Viewports (≤ 640 px) ist für beide Bereiche kein Platz. Daher ist +immer nur **ein** Bereich groß (Diagramm oder Editor); der andere schrumpft auf +seine Titelzeile und dient als Umschalter (Antippen bringt ihn nach vorn), +Splitter und Fenster-Buttons entfallen. Die Legende wird von der Bereichs- +Umschaltung **entkoppelt** und über einen eigenen Umschalter im Editor-Kopf +gesteuert — das dauerhafte „AGENDA“-Band kostet sonst zu viel Höhe. Die +Sprachleiste zeigt nur Englisch + aktive Sprache + „…“. Voreinstellung ist +**Vollbild** (maximale Fläche). Umgesetzt über die Klasse `body.mobile` (per +`matchMedia`), damit CSS und JS denselben Schwellwert teilen; die Vollbild- +Voreinstellung greift nur, wenn noch **keine** gespeicherte Nutzerwahl vorliegt +(eine bewusste Abschaltung bleibt so erhalten, siehe localStorage-Persistenz). diff --git a/docs/SPEC.md b/docs/SPEC.md index 62ee191..7091839 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -141,6 +141,16 @@ ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die all-of-Zwischenknoten bekommen ihn **symmetrisch**, damit ihr Abzweig (50 %-Höhe) weiterhin auf der Knotenmitte liegt. +### Kleiner Bildschirm (mobil) +Ab schmaler Breite (≤ 640 px, z. B. Smartphone) ist immer nur **ein** Bereich +groß sichtbar — Diagramm **oder** Texteditor; der andere schrumpft auf seine +Titelzeile und dient als Umschalter (Antippen bringt ihn nach vorn). Splitter +und Min/Normal/Max-Buttons entfallen. Die **Legende** bekommt hier einen eigenen +Umschalter im Editor-Kopf (statt der dauerhaften „AGENDA“-Zeile), damit sie +zugeklappt keinen Platz kostet. Die **Sprachwahl** zeigt nur Englisch und die +aktive Sprache plus „…“ (das alle Sprachen aufklappt). Voreinstellung auf +kleinem Bildschirm ist **Vollbild** (siehe D17). + ### Grafikexport des Diagramms Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen: diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index e6148d4..ca1267c 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -31,3 +31,9 @@ Diagramm rechts, Toggles für transponierte Ansicht und verworfene Elemente). 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. +- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) schaltet auf + Ein-Bereich-Ansicht (Diagramm **oder** Editor, Umschalten per Titelzeile), + eigenen Legenden-Umschalter (`#legendBtn`) und schlanke Sprachwahl; Default + Vollbild. Layout-CSS hängt an `body.mobile`, nicht an einer eigenen + `@media`-Regel — beide Seiten müssen denselben 640-px-Schwellwert nutzen + (SPEC §9, D17). diff --git a/frontend/index.html b/frontend/index.html index 236c161..b2859e6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -250,6 +250,7 @@ .copybtn.done .ic-done{display:block} .dlbtn{gap:3px} .dl-label{font-family:'IBM Plex Mono',monospace;font-size:.62rem;font-weight:600;letter-spacing:.02em} + .legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */ textarea{ display:block;width:100%;min-height:0; border:0;outline:none;resize:none; @@ -484,6 +485,36 @@ Die graue, gestrichelte Any-of-Führung kommt aus der Basis und gilt hier wie in allen Modi. Gate-Codierung: und = durchgezogen dunkel (Tinte), oder = gestrichelt grau. */ + + /* ---------- Kleiner Bildschirm (Smartphone): nur ein Panel ---------- + body.mobile wird per matchMedia (<=640px) gesetzt. Es ist immer genau + EIN Panel groß, das andere schrumpft auf seine Titelzeile und dient als + Umschalter (Tippen bringt es nach vorn). Splitter und Fenster-Buttons + entfallen; die Legende bekommt einen eigenen Umschalter statt der + dauerhaften „AGENDA“-Zeile. */ + body.mobile .gutter{display:none} + body.mobile .winbtns{display:none} + body.mobile .app, + body.mobile .app.side{ + grid-template-columns:minmax(0,1fr); + grid-template-areas:"diagram" "editor"; + } + body.mobile .app.collapse-diagram, + body.mobile .app.side.collapse-diagram{grid-template-rows:auto minmax(0,1fr)} + body.mobile .app.collapse-editor, + body.mobile .app.side.collapse-editor{grid-template-rows:minmax(0,1fr) auto} + /* 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)} + body.mobile .agenda[open]{width:auto} + /* Legende: keine dauerhafte Summary-Zeile, dafür der #legendBtn im Kopf */ + body.mobile .agenda>summary{display:none} + body.mobile .legendbtn{display:inline-flex} + /* Sprachwahl schlank: nur EN + aktuelle Sprache + „…“; „…“ klappt alle auf */ + body.mobile .langsel button[data-lang]{display:none} + body.mobile .langsel.expanded button[data-lang]{display:inline-block} + body.mobile .langsel button[data-lang="en"], + body.mobile .langsel button[data-lang].active{display:inline-block} @@ -530,6 +561,9 @@ +
@@ -973,7 +1007,12 @@ document.querySelectorAll('.winbtn').forEach(b => { [editorPanel, diagramPanel].forEach(panel => { panel.querySelector('.panel-head').addEventListener('click', e => { if(e.target.closest('button, label, input')) return; /* Bedienelemente behalten ihre Funktion */ - if(panel.classList.contains('collapsed')){ splitState = 'normal'; applySplit(); } + if(!panel.classList.contains('collapsed')) return; + /* Klein: das getippte Panel nach vorn holen (das andere schrumpft); + sonst: zurück in die normale Zweiteilung. */ + if(isMobile()) splitState = (panel === editorPanel) ? 'a' : 'b'; + else splitState = 'normal'; + applySplit(); }); }); @@ -1098,6 +1137,7 @@ const I18N = { de: { subtitle:"Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.", imprint:"Impressum", + legendTooltip:"Legende ein-/ausblenden", ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.", discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden", fullscreenTooltip:"Vollbild – Panels nutzen die ganze Fensterbreite", @@ -1130,6 +1170,7 @@ const I18N = { en: { subtitle:"Werkbaum – A text-based WBS / work breakdown structure / feature-tree diagram editor.", imprint:"Imprint (Impressum)", + legendTooltip:"Show/hide legend", ghostTooltip:"From size M upward, an item should be broken down further.", discardedTooltip:"Show/hide discarded nodes and their subtree", fullscreenTooltip:"Full screen – panels use the full window width", @@ -1162,6 +1203,7 @@ const I18N = { es: { subtitle:"Werkbaum – Un editor de diagramas basado en texto para EDT / estructura de desglose del trabajo / árbol de características.", imprint:"Aviso legal (Impressum)", + legendTooltip:"Mostrar u ocultar la leyenda", ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.", discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol", fullscreenTooltip:"Pantalla completa – los paneles usan todo el ancho de la ventana", @@ -1194,6 +1236,7 @@ const I18N = { fr: { subtitle:"Werkbaum – Un éditeur de diagrammes basé sur le texte pour WBS / organigramme des tâches / arbre de fonctionnalités.", imprint:"Mentions légales (Impressum)", + legendTooltip:"Afficher/masquer la légende", ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.", discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre", fullscreenTooltip:"Plein écran – les panneaux occupent toute la largeur de la fenêtre", @@ -1226,6 +1269,7 @@ const I18N = { pl: { subtitle:"Werkbaum – Edytor diagramów oparty na tekście: WBS / struktura podziału pracy / drzewo funkcji.", imprint:"Nota prawna (Impressum)", + legendTooltip:"Pokaż/ukryj legendę", ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.", discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem", fullscreenTooltip:"Pełny ekran – panele wykorzystują całą szerokość okna", @@ -1258,6 +1302,7 @@ const I18N = { ru: { subtitle:"Werkbaum – текстовый редактор диаграмм: СДР / структура декомпозиции работ / дерево функций.", imprint:"Выходные данные (Impressum)", + legendTooltip:"Показать/скрыть легенду", ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.", discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом", fullscreenTooltip:"Полный экран – панели занимают всю ширину окна", @@ -1290,6 +1335,7 @@ const I18N = { hi: { subtitle:"Werkbaum – टेक्स्ट-आधारित आरेख संपादक: WBS / कार्य विभाजन संरचना / फ़ीचर-ट्री।", imprint:"प्रकाशन विवरण (Impressum)", + legendTooltip:"लेजेंड दिखाएँ/छिपाएँ", ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।", discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ", fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं", @@ -1322,6 +1368,7 @@ const I18N = { zh: { subtitle:"Werkbaum – 基于文本的图表编辑器:WBS / 工作分解结构 / 功能树。", imprint:"法律声明(Impressum)", + legendTooltip:"显示/隐藏图例", brandTooltip:"「Werkbaum」大致意为‘工作之树’——即工作分解结构(WBS)之树。", fullscreenTooltip:"全屏——面板占据整个窗口宽度", discardedTooltip:"显示/隐藏已放弃的节点及其子树", @@ -1354,6 +1401,7 @@ const I18N = { ja: { subtitle:"Werkbaum – テキストベースの図エディター:WBS / 作業分解構成図 / フィーチャーツリー。", imprint:"運営者情報(Impressum)", + legendTooltip:"凡例を表示/非表示", brandTooltip:"「Werkbaum」はおおよそ『作業の木』の意味 — 作業分解構成図(WBS)のツリーです。", fullscreenTooltip:"全画面 — パネルがウィンドウ幅いっぱいを使用", discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示", @@ -1420,8 +1468,10 @@ function applyLang(l){ document.querySelectorAll('[data-i18n-aria]').forEach(el => el.setAttribute('aria-label', t(el.dataset.i18nAria))); document.getElementById('hint').innerHTML = buildHint(); document.querySelectorAll('.langsel button[data-lang]').forEach(b => b.classList.toggle('active', b.dataset.lang===l)); - /* Ist eine aufklappbare Sprache aktiv, den erweiterten Bereich offen halten. */ - if(LANG_EXTRA.includes(l)){ + /* Ist eine aufklappbare Sprache aktiv, den erweiterten Bereich offen halten. + Auf kleinem Bildschirm nicht — dort bleibt die Leiste schlank (EN + aktive + Sprache reichen; die aktive wird per CSS auch als „extra“ eingeblendet). */ + if(LANG_EXTRA.includes(l) && !isMobile()){ langsel.classList.add('expanded'); langMore.setAttribute('aria-expanded', 'true'); } @@ -1454,6 +1504,7 @@ fsBtn.addEventListener('click', () => { erhalten. Sprache liegt weiterhin in 'werkbaum-lang'. */ const LS_UI = 'werkbaum-ui', LS_SRC = 'werkbaum-src'; let restoring = false; /* unterdrückt Speichern während des Wiederherstellens */ +let hadStoredUI = false; /* gab es beim Laden schon gespeicherte GUI-Einstellungen? */ function saveSrc(){ if(restoring) return; try{ localStorage.setItem(LS_SRC, src.value); }catch(_){} @@ -1478,6 +1529,7 @@ function restoreState(){ let savedSrc = null, ui = null; try{ savedSrc = localStorage.getItem(LS_SRC); }catch(_){} try{ ui = JSON.parse(localStorage.getItem(LS_UI) || 'null'); }catch(_){} + hadStoredUI = !!ui; src.value = (savedSrc !== null) ? savedSrc : INITIAL; /* leerer String bleibt leer */ const mode = (ui && ui.mode) || 'horizontal'; const modeEl = document.querySelector('input[name="layout"][value="' + mode + '"]'); @@ -1501,10 +1553,37 @@ function restoreState(){ restoring = false; } +/* ---------- Kleiner Bildschirm: nur ein Panel, schlanke Sprachwahl, Vollbild ---------- */ +const mqMobile = window.matchMedia('(max-width:640px)'); +function isMobile(){ return mqMobile.matches; } +const legendBtn = document.getElementById('legendBtn'); +const agenda = document.querySelector('.agenda'); +legendBtn.addEventListener('click', () => { + agenda.open = !agenda.open; + legendBtn.classList.toggle('active', agenda.open); +}); +agenda.addEventListener('toggle', () => legendBtn.classList.toggle('active', agenda.open)); +function applyMobile(){ + const m = isMobile(); + document.body.classList.toggle('mobile', m); + if(!m) return; + /* Nur ein Panel sichtbar: normal/custom passen nicht — auf Diagramm (b). */ + if(splitState !== 'a' && splitState !== 'b'){ splitState = 'b'; applySplit(); } + /* Default Vollbild auf kleinem Bildschirm — nur ohne gespeicherte Wahl. */ + if(!hadStoredUI && !document.body.classList.contains('fullscreen')){ + document.body.classList.add('fullscreen'); + fsBtn.classList.add('active'); + fsBtn.setAttribute('aria-pressed', 'true'); + saveUI(); + } +} +mqMobile.addEventListener('change', applyMobile); + restoreState(); /* Editortext + GUI-Zustand aus dem Browser wiederherstellen */ let startLang = 'de'; try{ startLang = localStorage.getItem('werkbaum-lang') || 'de'; }catch(_){} applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */ +applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */