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 */