diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 8c633ae..393c7e3 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -138,10 +138,18 @@ exakt der deployte Commit (`…/commit/`, im Build via `git rev-parse HEAD` (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); die -Fenster-Buttons entfallen, der **Splitter bleibt** (Teilen per Ziehen möglich). +Auf schmalen Viewports (≤ 640 px) ist für beide Bereiche kein Platz. Diagramm +und Editor werden gestapelt und über den **Splitter stufenlos** geteilt (frei +ziehbar, jederzeit erneut verstellbar); die beiden **Titelzeilen bleiben immer +stehen** (Grid-Zeilen-Minima = gemessene Kopfhöhen), ein **Tipp auf eine +Titelzeile klappt dieses Panel ganz aus**. Das frühere diskrete Modell (Snap in +Minimier-Zustände 'a'/'b') führte auf dem Smartphone zu zwei Fehlern: nach dem +ersten Ziehen rastete der Splitter in eine feste Aufteilung und war nicht mehr +zu bewegen (die Titelzeile im Snap-Rand fing jeden weiteren Zug ab), und der +Titelzeilen-Tap funktionierte nur im `collapsed`-Zustand — nach einem freien Zug +also gar nicht. Deshalb auf Mobil ein **kontinuierliches** Modell ohne Snap/ +Collapse (`--drow` frei geklemmt, Tap setzt sie aufs Extrem); Desktop behält das +diskrete Modell samt Fenster-Buttons. 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 diff --git a/docs/SPEC.md b/docs/SPEC.md index eed00cb..0472514 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -145,11 +145,16 @@ ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die (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). Die -Min/Normal/Max-Buttons entfallen; geteilt wird weiterhin über den **Splitter** -(Ziehen). Die **Legende** bekommt hier einen eigenen +Ab schmaler Breite (≤ 640 px, z. B. Smartphone) werden Diagramm und Texteditor +**gestapelt** und über den **Splitter** frei geteilt: Ziehen verteilt beliebig, +jederzeit erneut verstellbar; die beiden **Titelzeilen bleiben dabei immer +stehen** (jedes Panel schrumpft höchstens bis auf seine volle Titelzeile — die +Zeilen-Minima entsprechen den gemessenen Kopfhöhen). Ein **Antippen einer +Titelzeile klappt dieses Panel ganz aus** (das andere schrumpft auf seine +Titelzeile). Es gibt hier kein diskretes Minimieren/Einrasten mehr (kein Snap, +keine Min/Normal/Max-Buttons) — die Aufteilung ist durchgehend stufenlos. +Voreinstellung ist Diagramm maximiert (Editor als Titelzeile). Die **Legende** +bekommt hier einen eigenen Umschalter im Editor-Kopf (statt der dauerhaften „AGENDA“-Zeile), damit sie zugeklappt keinen Platz kostet. Der **Modus-Wähler** zeigt nur das aktive Icon und schaltet bei jedem Tippen reihum weiter (horizontal → kompakt → vertikal → diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 7f57d88..45a74f8 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -42,9 +42,13 @@ 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 +- 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 + Kopfhöhen `--pmin-d`/`--pmin-e`, per `syncPanelMins()` gemessen), ein Tipp auf + eine Titelzeile maximiert das Panel. `applyLayout` ruft auf Mobil **kein** + `applySplit` (das würde `--drow` löschen). Dazu eigener Legenden-Umschalter + (`#legendBtn`), schlanke Sprachwahl, Download-Overlay; Default Vollbild + + Diagramm maximiert. 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 6caa06a..a729158 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -561,6 +561,18 @@ 820-px-Regel weiter oben. Die Legende bekommt einen eigenen Umschalter statt der dauerhaften „AGENDA“-Zeile. */ body.mobile .winbtns{display:none} + /* Mobil: freie Aufteilung per Splitter — beide Titelzeilen bleiben IMMER + stehen (Zeilen-Minimum --pmin ≈ Titelzeilenhöhe). Kein diskretes + Minimieren/Snap; ein zusammengeschobenes Panel schrumpft nur bis auf seine + volle Titelzeile (Körper flext auf 0, Panel ist overflow:hidden). Gilt auch + für den Seitenmodus, der auf Mobil ohnehin gestapelt ist. Der Drag setzt + --drow (JS klemmt), das Tippen auf eine Titelzeile maximiert das Panel. */ + body.mobile .app, + body.mobile .app.side{ + grid-template-columns:minmax(0,1fr); + grid-template-rows:minmax(var(--pmin-d,49px),var(--drow,60%)) 14px minmax(var(--pmin-e,44px),1fr); + grid-template-areas:"diagram" "gutter" "editor"; + } /* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */ body.mobile .sub-full{display:none} body.mobile .sub-short{display:inline} @@ -1188,7 +1200,7 @@ function applyLayout(mode){ out.classList.toggle('vertical', mode === 'vertikal'); out.classList.toggle('kompakt', mode === 'kompakt'); app.classList.toggle('side', mode !== 'horizontal'); - applySplit(); /* Preset für die neue Orientierung neu setzen */ + if(!isMobile()) applySplit(); /* Desktop: Preset neu setzen. Mobil: freie --drow-Aufteilung behalten */ drawCheapPath(); /* Blatt-Positionen ändern sich mit dem Modus */ } document.querySelectorAll('input[name="layout"]').forEach(radio => { @@ -1222,6 +1234,25 @@ function applySplit(){ } /* Setzt einen Minimier-Zustand während des Ziehens nur bei Änderung. */ function snapTo(state){ if(splitState!==state){ splitState = state; applySplit(); } } + +/* ---------- Mobil: freie Aufteilung (kontinuierlich, kein Snap/Collapse) ---------- + Beide Titelzeilen bleiben stehen: die Grid-Zeilen-Minima --pmin-d/--pmin-e + entsprechen den (gemessenen) Kopfzeilenhöhen. --drow steuert die Diagramm- + Höhe; das Tippen auf eine Titelzeile setzt sie auf ein Extrem. */ +function headPx(sel){ return Math.ceil(document.querySelector(sel).getBoundingClientRect().height); } +function syncPanelMins(){ + app.style.setProperty('--pmin-d', headPx('.panel.right .panel-head') + 'px'); + app.style.setProperty('--pmin-e', headPx('.panel.editor .panel-head') + 'px'); +} +function cssPx(name, fallback){ return parseFloat(getComputedStyle(app).getPropertyValue(name)) || fallback; } +function mobileMinDrow(){ return cssPx('--pmin-d', 49); } +function mobileMaxDrow(){ return app.getBoundingClientRect().height - 14 - cssPx('--pmin-e', 44); } +function setMobileDrow(px, save){ + const v = Math.max(mobileMinDrow(), Math.min(px, mobileMaxDrow())); + splitState = 'custom'; + app.style.setProperty('--drow', v + 'px'); + if(save) saveUI(); +} document.querySelectorAll('.winbtn').forEach(b => { b.addEventListener('click', e => { /* Fenster-Buttons sind maßgeblich: nicht zum Titelzeilen-Restore durchreichen */ @@ -1233,11 +1264,16 @@ 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(isMobile()){ + /* Tippen auf eine Titelzeile klappt DIESES Panel ganz aus (das andere + schrumpft auf seine Titelzeile) — funktioniert in jeder Aufteilung. */ + clearCollapse(); + setMobileDrow(panel === diagramPanel ? mobileMaxDrow() : 0, true); + return; + } + /* Desktop: nur ein minimiertes Panel per Titelzeile wiederherstellen. */ 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'; + splitState = 'normal'; applySplit(); }); }); @@ -1262,6 +1298,12 @@ const SNAP = 72; gutter.addEventListener('pointermove', e => { if(!dragging) return; const rect = app.getBoundingClientRect(); + if(isMobile()){ + /* Frei ziehen; die Grid-Minima (--pmin) halten beide Titelzeilen sichtbar. + Kein Snap/Collapse — so bleibt der Splitter jederzeit beweglich. */ + setMobileDrow(e.clientY - rect.top, false); + return; + } if(app.classList.contains('side')){ const w = e.clientX - rect.left, maxw = rect.width - 14; if(w <= SNAP) snapTo('b'); /* Editor auf Titelzeile */ @@ -1290,8 +1332,12 @@ function endDrag(e){ } gutter.addEventListener('pointerup', endDrag); gutter.addEventListener('pointercancel', endDrag); -/* Doppelklick auf den Splitter stellt die normale Aufteilung wieder her. */ -gutter.addEventListener('dblclick', () => { splitState = 'normal'; applySplit(); }); +/* Doppelklick auf den Splitter stellt die normale Aufteilung wieder her + (auf Mobil: Diagramm maximiert). */ +gutter.addEventListener('dblclick', () => { + if(isMobile()){ clearCollapse(); setMobileDrow(mobileMaxDrow(), true); return; } + splitState = 'normal'; applySplit(); +}); /* ---------- Zoom für das Diagramm ---------- */ /* CSS-`zoom` skaliert die Layout-Box, dadurch greifen die Scrollbalken @@ -1904,6 +1950,7 @@ const LAYOUT_MODES = ['horizontal','kompakt','vertikal']; seg.addEventListener('click', e => { if(!isMobile()) return; e.preventDefault(); + e.stopPropagation(); /* nicht zum Titelzeilen-Maximieren durchreichen */ const cur = document.querySelector('input[name="layout"]:checked').value; const next = LAYOUT_MODES[(LAYOUT_MODES.indexOf(cur) + 1) % LAYOUT_MODES.length]; const r = document.querySelector('input[name="layout"][value="' + next + '"]'); @@ -1915,8 +1962,13 @@ function applyMobile(){ document.body.classList.toggle('mobile', m); if(m) collapseLangsel(); /* Sprachleiste eingeklappt starten (Overlay-Logik) */ if(!m) return; - /* Nur ein Panel sichtbar: normal/custom passen nicht — auf Diagramm (b). */ - if(splitState !== 'a' && splitState !== 'b'){ splitState = 'b'; applySplit(); } + /* Freie Aufteilung (kontinuierlich): kein Desktop-Collapse. Ist noch keine + Aufteilung gesetzt (frisch oder Alt-Preset), Diagramm maximieren (Editor + bleibt als Titelzeile); eine gespeicherte --drow bleibt erhalten. */ + clearCollapse(); + syncPanelMins(); /* Grid-Minima = aktuelle Kopfzeilenhöhen */ + if(app.style.getPropertyValue('--drow')) splitState = 'custom'; + else setMobileDrow(mobileMaxDrow(), true); /* Default Vollbild auf kleinem Bildschirm — nur ohne gespeicherte Wahl. */ if(!hadStoredUI && !document.body.classList.contains('fullscreen')){ document.body.classList.add('fullscreen');