frontend: Mobiler Splitter stufenlos + Titelzeilen-Tap maximiert
Auf dem Smartphone hatte der Splitter zwei Fehler: nach dem ersten Ziehen rastete er in eine feste Aufteilung und ließ sich nicht mehr bewegen (die Titelzeile im Snap-Rand fing jeden weiteren Zug ab), und das Tippen auf eine Titelzeile klappte das Panel nicht mehr aus (der Handler verlangte den collapsed-Zustand, den ein freier Zug aufhob). Mobil jetzt kontinuierlich statt diskret: - Grid-Zeilen-Minima --pmin-d/--pmin-e = gemessene Kopfhöhen (syncPanelMins), dadurch bleiben beide Titelzeilen immer stehen (kein Snap/Collapse). - Gutter-Drag ruft setMobileDrow() (klemmt --drow frei) — jederzeit beweglich. - Tipp auf eine Titelzeile maximiert dieses Panel (das andere = Titelzeile). - applyLayout ruft auf Mobil kein applySplit (behält --drow bei Moduswechsel); seg-Klick stoppt Propagation, damit er die Titelzeile nicht mit-maximiert. - Default: Diagramm maximiert. Desktop behält das diskrete Modell + Buttons. SPEC §9, DECISIONS D17 und frontend/CLAUDE.md nachgeführt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
779ac10cdf
commit
ad57707dbc
+12
-4
@@ -138,10 +138,18 @@ exakt der deployte Commit (`…/commit/<sha>`, im Build via `git rev-parse HEAD`
|
|||||||
(Nummerierung: D15 war bereits für den kompakten Modus vergeben, daher D16.)
|
(Nummerierung: D15 war bereits für den kompakten Modus vergeben, daher D16.)
|
||||||
|
|
||||||
## D17 — Kleiner Bildschirm: ein Bereich, kompakte Legende & Sprachwahl
|
## D17 — Kleiner Bildschirm: ein Bereich, kompakte Legende & Sprachwahl
|
||||||
Auf schmalen Viewports (≤ 640 px) ist für beide Bereiche kein Platz. Daher ist
|
Auf schmalen Viewports (≤ 640 px) ist für beide Bereiche kein Platz. Diagramm
|
||||||
immer nur **ein** Bereich groß (Diagramm oder Editor); der andere schrumpft auf
|
und Editor werden gestapelt und über den **Splitter stufenlos** geteilt (frei
|
||||||
seine Titelzeile und dient als Umschalter (Antippen bringt ihn nach vorn); die
|
ziehbar, jederzeit erneut verstellbar); die beiden **Titelzeilen bleiben immer
|
||||||
Fenster-Buttons entfallen, der **Splitter bleibt** (Teilen per Ziehen möglich).
|
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-
|
Die Legende wird von der Bereichs-
|
||||||
Umschaltung **entkoppelt** und über einen eigenen Umschalter im Editor-Kopf
|
Umschaltung **entkoppelt** und über einen eigenen Umschalter im Editor-Kopf
|
||||||
gesteuert — das dauerhafte „AGENDA“-Band kostet sonst zu viel Höhe. Die
|
gesteuert — das dauerhafte „AGENDA“-Band kostet sonst zu viel Höhe. Die
|
||||||
|
|||||||
+10
-5
@@ -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.
|
(50 %-Höhe) weiterhin auf der Knotenmitte liegt.
|
||||||
|
|
||||||
### Kleiner Bildschirm (mobil)
|
### Kleiner Bildschirm (mobil)
|
||||||
Ab schmaler Breite (≤ 640 px, z. B. Smartphone) ist immer nur **ein** Bereich
|
Ab schmaler Breite (≤ 640 px, z. B. Smartphone) werden Diagramm und Texteditor
|
||||||
groß sichtbar — Diagramm **oder** Texteditor; der andere schrumpft auf seine
|
**gestapelt** und über den **Splitter** frei geteilt: Ziehen verteilt beliebig,
|
||||||
Titelzeile und dient als Umschalter (Antippen bringt ihn nach vorn). Die
|
jederzeit erneut verstellbar; die beiden **Titelzeilen bleiben dabei immer
|
||||||
Min/Normal/Max-Buttons entfallen; geteilt wird weiterhin über den **Splitter**
|
stehen** (jedes Panel schrumpft höchstens bis auf seine volle Titelzeile — die
|
||||||
(Ziehen). Die **Legende** bekommt hier einen eigenen
|
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
|
Umschalter im Editor-Kopf (statt der dauerhaften „AGENDA“-Zeile), damit sie
|
||||||
zugeklappt keinen Platz kostet. Der **Modus-Wähler** zeigt nur das aktive Icon
|
zugeklappt keinen Platz kostet. Der **Modus-Wähler** zeigt nur das aktive Icon
|
||||||
und schaltet bei jedem Tippen reihum weiter (horizontal → kompakt → vertikal →
|
und schaltet bei jedem Tippen reihum weiter (horizontal → kompakt → vertikal →
|
||||||
|
|||||||
+8
-4
@@ -42,9 +42,13 @@ Diagramm rechts, Toggles für transponierte Ansicht und verworfene Elemente).
|
|||||||
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. Fehlender `werkbaum-src` fällt auf `INITIAL` zurück, ein
|
||||||
leerer String bleibt jedoch leer.
|
leerer String bleibt jedoch leer.
|
||||||
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) schaltet auf
|
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
||||||
Ein-Bereich-Ansicht (Diagramm **oder** Editor, Umschalten per Titelzeile),
|
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
||||||
eigenen Legenden-Umschalter (`#legendBtn`) und schlanke Sprachwahl; Default
|
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
||||||
Vollbild. Layout-CSS hängt an `body.mobile`, nicht an einer eigenen
|
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
|
`@media`-Regel — beide Seiten müssen denselben 640-px-Schwellwert nutzen
|
||||||
(SPEC §9, D17).
|
(SPEC §9, D17).
|
||||||
|
|||||||
+61
-9
@@ -561,6 +561,18 @@
|
|||||||
820-px-Regel weiter oben. Die Legende bekommt einen eigenen Umschalter
|
820-px-Regel weiter oben. Die Legende bekommt einen eigenen Umschalter
|
||||||
statt der dauerhaften „AGENDA“-Zeile. */
|
statt der dauerhaften „AGENDA“-Zeile. */
|
||||||
body.mobile .winbtns{display:none}
|
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) */
|
/* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */
|
||||||
body.mobile .sub-full{display:none}
|
body.mobile .sub-full{display:none}
|
||||||
body.mobile .sub-short{display:inline}
|
body.mobile .sub-short{display:inline}
|
||||||
@@ -1188,7 +1200,7 @@ function applyLayout(mode){
|
|||||||
out.classList.toggle('vertical', mode === 'vertikal');
|
out.classList.toggle('vertical', mode === 'vertikal');
|
||||||
out.classList.toggle('kompakt', mode === 'kompakt');
|
out.classList.toggle('kompakt', mode === 'kompakt');
|
||||||
app.classList.toggle('side', mode !== 'horizontal');
|
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 */
|
drawCheapPath(); /* Blatt-Positionen ändern sich mit dem Modus */
|
||||||
}
|
}
|
||||||
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
||||||
@@ -1222,6 +1234,25 @@ function applySplit(){
|
|||||||
}
|
}
|
||||||
/* Setzt einen Minimier-Zustand während des Ziehens nur bei Änderung. */
|
/* Setzt einen Minimier-Zustand während des Ziehens nur bei Änderung. */
|
||||||
function snapTo(state){ if(splitState!==state){ splitState = state; applySplit(); } }
|
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 => {
|
document.querySelectorAll('.winbtn').forEach(b => {
|
||||||
b.addEventListener('click', e => {
|
b.addEventListener('click', e => {
|
||||||
/* Fenster-Buttons sind maßgeblich: nicht zum Titelzeilen-Restore durchreichen */
|
/* Fenster-Buttons sind maßgeblich: nicht zum Titelzeilen-Restore durchreichen */
|
||||||
@@ -1233,11 +1264,16 @@ document.querySelectorAll('.winbtn').forEach(b => {
|
|||||||
[editorPanel, diagramPanel].forEach(panel => {
|
[editorPanel, diagramPanel].forEach(panel => {
|
||||||
panel.querySelector('.panel-head').addEventListener('click', e => {
|
panel.querySelector('.panel-head').addEventListener('click', e => {
|
||||||
if(e.target.closest('button, label, input')) return; /* Bedienelemente behalten ihre Funktion */
|
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;
|
if(!panel.classList.contains('collapsed')) return;
|
||||||
/* Klein: das getippte Panel nach vorn holen (das andere schrumpft);
|
splitState = 'normal';
|
||||||
sonst: zurück in die normale Zweiteilung. */
|
|
||||||
if(isMobile()) splitState = (panel === editorPanel) ? 'a' : 'b';
|
|
||||||
else splitState = 'normal';
|
|
||||||
applySplit();
|
applySplit();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1262,6 +1298,12 @@ const SNAP = 72;
|
|||||||
gutter.addEventListener('pointermove', e => {
|
gutter.addEventListener('pointermove', e => {
|
||||||
if(!dragging) return;
|
if(!dragging) return;
|
||||||
const rect = app.getBoundingClientRect();
|
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')){
|
if(app.classList.contains('side')){
|
||||||
const w = e.clientX - rect.left, maxw = rect.width - 14;
|
const w = e.clientX - rect.left, maxw = rect.width - 14;
|
||||||
if(w <= SNAP) snapTo('b'); /* Editor auf Titelzeile */
|
if(w <= SNAP) snapTo('b'); /* Editor auf Titelzeile */
|
||||||
@@ -1290,8 +1332,12 @@ function endDrag(e){
|
|||||||
}
|
}
|
||||||
gutter.addEventListener('pointerup', endDrag);
|
gutter.addEventListener('pointerup', endDrag);
|
||||||
gutter.addEventListener('pointercancel', endDrag);
|
gutter.addEventListener('pointercancel', endDrag);
|
||||||
/* Doppelklick auf den Splitter stellt die normale Aufteilung wieder her. */
|
/* Doppelklick auf den Splitter stellt die normale Aufteilung wieder her
|
||||||
gutter.addEventListener('dblclick', () => { splitState = 'normal'; applySplit(); });
|
(auf Mobil: Diagramm maximiert). */
|
||||||
|
gutter.addEventListener('dblclick', () => {
|
||||||
|
if(isMobile()){ clearCollapse(); setMobileDrow(mobileMaxDrow(), true); return; }
|
||||||
|
splitState = 'normal'; applySplit();
|
||||||
|
});
|
||||||
|
|
||||||
/* ---------- Zoom für das Diagramm ---------- */
|
/* ---------- Zoom für das Diagramm ---------- */
|
||||||
/* CSS-`zoom` skaliert die Layout-Box, dadurch greifen die Scrollbalken
|
/* CSS-`zoom` skaliert die Layout-Box, dadurch greifen die Scrollbalken
|
||||||
@@ -1904,6 +1950,7 @@ const LAYOUT_MODES = ['horizontal','kompakt','vertikal'];
|
|||||||
seg.addEventListener('click', e => {
|
seg.addEventListener('click', e => {
|
||||||
if(!isMobile()) return;
|
if(!isMobile()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation(); /* nicht zum Titelzeilen-Maximieren durchreichen */
|
||||||
const cur = document.querySelector('input[name="layout"]:checked').value;
|
const cur = document.querySelector('input[name="layout"]:checked').value;
|
||||||
const next = LAYOUT_MODES[(LAYOUT_MODES.indexOf(cur) + 1) % LAYOUT_MODES.length];
|
const next = LAYOUT_MODES[(LAYOUT_MODES.indexOf(cur) + 1) % LAYOUT_MODES.length];
|
||||||
const r = document.querySelector('input[name="layout"][value="' + next + '"]');
|
const r = document.querySelector('input[name="layout"][value="' + next + '"]');
|
||||||
@@ -1915,8 +1962,13 @@ function applyMobile(){
|
|||||||
document.body.classList.toggle('mobile', m);
|
document.body.classList.toggle('mobile', m);
|
||||||
if(m) collapseLangsel(); /* Sprachleiste eingeklappt starten (Overlay-Logik) */
|
if(m) collapseLangsel(); /* Sprachleiste eingeklappt starten (Overlay-Logik) */
|
||||||
if(!m) return;
|
if(!m) return;
|
||||||
/* Nur ein Panel sichtbar: normal/custom passen nicht — auf Diagramm (b). */
|
/* Freie Aufteilung (kontinuierlich): kein Desktop-Collapse. Ist noch keine
|
||||||
if(splitState !== 'a' && splitState !== 'b'){ splitState = 'b'; applySplit(); }
|
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. */
|
/* Default Vollbild auf kleinem Bildschirm — nur ohne gespeicherte Wahl. */
|
||||||
if(!hadStoredUI && !document.body.classList.contains('fullscreen')){
|
if(!hadStoredUI && !document.body.classList.contains('fullscreen')){
|
||||||
document.body.classList.add('fullscreen');
|
document.body.classList.add('fullscreen');
|
||||||
|
|||||||
Reference in New Issue
Block a user