frontend: Legende scrollbar + Splitter zwischen Editor und Legende

Die Legende wurde abgeschnitten statt zu scrollen — obwohl `.hint` seit jeher
`flex:1 1 auto; min-height:0; overflow:auto` trägt.

Ursache: Chrome legt den Inhalt eines <details> in das Pseudo-Element
`::details-content`. Damit war `.hint` kein Flex-Kind von `.agenda` mehr, die
Begrenzung griff nicht und der Kasten wuchs auf seine Inhaltshöhe (gemessen
585 px in einem 282 px hohen Container) und wurde geclippt. Eine Regel für
`::details-content` wirkte im Test, ist aber Chrome-eigen — Firefox und Safari
kennen das Pseudo-Element nicht.

- Die Legende ist deshalb kein <details> mehr, sondern `div.agenda` +
  `button.agenda-summary`; der Zustand hängt an der Klasse `open`
  (`aria-expanded` am Button, `#legendBtn` und Splitter-Sichtbarkeit über
  `setAgendaOpen()` synchron). `.hint` ist wieder echtes Flex-Kind und scrollt
  in allen Browsern.
- Neuer Splitter `#hintGutter` im selben Idiom wie der große (Pointer-Capture,
  Doppelklick setzt zurück): horizontal nebeneinander -> Breite `--hcol`,
  gestapelt (side/mobil) -> Höhe `--hrow`. Beide getrennt gehalten und in
  `werkbaum-ui` persistiert (samt Auf-/Zu-Zustand), damit ein Moduswechsel die
  jeweils andere Aufteilung nicht zerstört. Grenzen 90 px bis 85 %.
- Die 85-%-Obergrenze steht zusätzlich als max-width/max-height im CSS: die
  gespeicherte Größe ist ein fester px-Wert und würde den Editor sonst auf 0
  drücken, wenn das Panel später schrumpft (großer Splitter, Drehung,
  Bildschirmtastatur). Verifiziert mit --hrow:9999px -> gekappt bei 85 %.
- Nebenbei: der große Splitter war als einziges Bedienelement nicht lokalisiert
  (fest deutsche title/aria-label). Beide teilen sich nun `gutterTooltip`,
  dazu `gutterAria`/`hintGutterAria` — 9 Sprachen.

Verifiziert im Browser: Legende scrollt (247 px Fenster, 607 px Inhalt);
Ziehen in beiden Ausrichtungen inkl. Unter-/Obergrenze und Doppelklick-Reset;
`--hcol` und `--hrow` überleben den Moduswechsel; nach Neuladen sind
Auf-Zustand, Breite und beide Variablen wiederhergestellt; auf Mobil ist die
Summary-Zeile aus und der Kopf-Knopf steuert. Vitest 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-27 14:02:56 +02:00
co-authored by Claude Opus 4.8
parent 4061362922
commit 6888762ac8
6 changed files with 181 additions and 21 deletions
+33
View File
@@ -586,3 +586,36 @@ Verworfen: die Tastatur zuzulassen und auf die OS-Einstellung zu verweisen (lös
es nur auf einem Gerät), sowie ein eigener Umschalter dafür (weiteres es nur auf einem Gerät), sowie ein eigener Umschalter dafür (weiteres
Bedienelement in einer engen Kopfzeile plus i18n in 9 Sprachen, für ein Bedienelement in einer engen Kopfzeile plus i18n in 9 Sprachen, für ein
Verhalten, das kaum jemand umstellen will). Verhalten, das kaum jemand umstellen will).
## D26 — Legende scrollbar: eigener Container statt `<details>`, plus Splitter
Die Legende („Agenda") im Editor-Panel war zu hoch für ihren Platz und wurde
**abgeschnitten** statt scrollbar zu sein — obwohl `.hint` seit jeher
`flex:1 1 auto; min-height:0; overflow:auto` trug.
**Ursache:** Chrome legt den Inhalt eines `<details>` seit einiger Zeit in das
Pseudo-Element **`::details-content`**. Damit ist `.hint` **kein Flex-Kind** von
`.agenda` mehr; die Flex-Begrenzung greift nicht, `.hint` wächst auf seine
Inhaltshöhe (gemessen 585 px in einem 282 px hohen Container) und wird vom
`overflow:hidden` der Agenda geclippt. Ein Gegenmittel wäre
`.agenda::details-content{display:flex;…}` (im Test wirksam: 585 → 248 px,
scrollbar), aber das Pseudo-Element ist Chrome-eigen — Firefox und Safari kennen
es nicht, dort hinge dieselbe Layout-Kette an einem anderen anonymen Kasten.
**Entscheidung: kein `<details>` mehr.** Die Legende ist ein gewöhnliches
`<div class="agenda">` mit `<button class="agenda-summary">`; der Auf-/Zu-Zustand
hängt an der Klasse `open` (`aria-expanded` am Button). Damit ist `.hint` wieder
ein echtes Flex-Kind und die Begrenzung in **allen** Browsern dieselbe. Die
native Aufklapp-Mechanik war ohnehin nur halb genutzt — mobil steuert seit D17
der `#legendBtn` im Kopf, das `toggle`-Ereignis diente nur der Synchronisierung.
**Splitter Editor|Legende.** Die Aufteilung ist nun frei ziehbar, im selben
Idiom wie der große Splitter (`pointerdown`/`pointermove`/Pointer-Capture,
Doppelklick setzt zurück). Die Ausrichtung folgt derselben Fallunterscheidung
wie dort: horizontal nebeneinander → Legendenbreite `--hcol`, gestapelt (`side`
oder mobil) → Legendenhöhe `--hrow`. **Beide Werte werden getrennt gehalten und
persistiert** (`werkbaum-ui`), sodass ein Moduswechsel die jeweils andere
Aufteilung nicht zerstört; der Auf-/Zu-Zustand der Legende wird mitgesichert.
Grenzen: mindestens 90 px, höchstens 85 % — die Obergrenze steht **zusätzlich
als `max-width`/`max-height` im CSS**, weil die gespeicherte Größe ein fester
px-Wert ist: schrumpft das Panel später (Zug am großen Splitter, Drehung,
Bildschirmtastatur), würde der Editor sonst auf 0 gedrückt.
+10
View File
@@ -225,6 +225,16 @@ Scheitert das Laden (häufigster Fall: das Ziel sendet keinen
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der `Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23. bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
### Legende im Editor-Panel
Neben dem Textfeld steht eine aufklappbare **Legende** (Notation in Kurzform,
abschließend eine Bedienungs-Zeile). Sie ist **scrollbar**, wenn ihr Inhalt
höher ist als der Platz, und über einen eigenen **Splitter** vom Textfeld
abgeteilt: horizontal nebeneinander (Legendenbreite), in den gestapelten Modi
und auf kleinem Bildschirm untereinander (Legendenhöhe). Ziehen verteilt frei,
Doppelklick stellt die Vorgabe wieder her; die Aufteilung bleibt für beide
Ausrichtungen getrennt erhalten. Die Legende belegt höchstens 85 % des Panels,
damit das Textfeld nie ganz verschwindet. Siehe D26.
### Sprung zwischen Diagramm und Text ### Sprung zwischen Diagramm und Text
Jeder Knoten kennt seine Zeilennummer im Notationstext; beide Richtungen sind Jeder Knoten kennt seine Zeilennummer im Notationstext; beide Richtungen sind
verknüpft (siehe D25): verknüpft (siehe D25):
+11
View File
@@ -153,6 +153,17 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Handler am Textfeld hebt das wieder auf (läuft vor dem Fokus). Wer sonst Handler am Textfeld hebt das wieder auf (läuft vor dem Fokus). Wer sonst
irgendwo `src.focus()` ergänzt und dabei Tippen meint, muss vorher irgendwo `src.focus()` ergänzt und dabei Tippen meint, muss vorher
`keyboardOnJump(false)` rufen — so wie `newDoc()`. `keyboardOnJump(false)` rufen — so wie `newDoc()`.
- Legende (D26): **kein `<details>`** — Chrome legt Details-Inhalt in
`::details-content`, dadurch war `.hint` kein Flex-Kind mehr und ließ sich nicht
begrenzen (Inhalt geclippt statt scrollbar). Jetzt `div.agenda` +
`button.agenda-summary`, Zustand an der Klasse `open` (`setAgendaOpen()` hält
Klasse, `aria-expanded`, `#legendBtn` und die Sichtbarkeit des Splitters
zusammen). Wer hier wieder ein natives Aufklapp-Element einsetzt, bricht das
Scrollen. Der Splitter `#hintGutter` schreibt `--hcol` (nebeneinander) bzw.
`--hrow` (gestapelt: `side` oder mobil) an `#app`; beide werden getrennt in
`werkbaum-ui` gesichert. Die 85-%-Obergrenze steht **zusätzlich** als
`max-width`/`max-height` im CSS — die gespeicherte px-Größe würde den Editor
sonst erdrücken, wenn das Panel später schrumpft.
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt - Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
+5 -4
View File
@@ -73,8 +73,9 @@
</div> </div>
<div class="editor-body"> <div class="editor-body">
<textarea id="src" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea> <textarea id="src" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
<details class="agenda"> <div class="hint-gutter" id="hintGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="hintGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Editor und Legende größenverändern" hidden></div>
<summary data-i18n="agenda">Agenda</summary> <div class="agenda" id="agenda">
<button type="button" class="agenda-summary" id="agendaToggle" aria-expanded="false" aria-controls="hint" data-i18n="agenda">Agenda</button>
<div class="hint" id="hint"> <div class="hint" id="hint">
Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.<br> Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.<br>
<code>-</code>&nbsp; Teilpaket, alle erforderlich <em>(all of)</em><br> <code>-</code>&nbsp; Teilpaket, alle erforderlich <em>(all of)</em><br>
@@ -96,11 +97,11 @@
Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende. Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende.
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten. Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
</div> </div>
</details> </div>
</div> </div>
</div> </div>
<div class="gutter" id="gutter" role="separator" aria-label="Bereiche größenverändern" title="Ziehen zum Verschieben"></div> <div class="gutter" id="gutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="gutterAria" aria-label="Bereiche größenverändern" title="Ziehen zum Verschieben, Doppelklick setzt zurück"></div>
<div class="panel right"> <div class="panel right">
<div class="panel-head"> <div class="panel-head">
+88 -5
View File
@@ -770,6 +770,8 @@ const I18N = {
downloadMenu:"Diagramm herunterladen (SVG/PNG)", downloadMenu:"Diagramm herunterladen (SVG/PNG)",
minimize:"minimieren", normal:"normal", maximize:"maximieren", minimize:"minimieren", normal:"normal", maximize:"maximieren",
agenda:"Agenda", discarded:"verworfene", agenda:"Agenda", discarded:"verworfene",
gutterTooltip:"Ziehen zum Verschieben, Doppelklick setzt zurück", gutterAria:"Bereiche größenverändern",
hintGutterAria:"Editor und Legende größenverändern",
modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor", modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor",
modeKompakt:"Kompakt alles nach unten, platzsparend", modeKompakt:"Kompakt alles nach unten, platzsparend",
modeVertikal:"Vertikal Baum nach rechts, Diagramm neben dem Editor", modeVertikal:"Vertikal Baum nach rechts, Diagramm neben dem Editor",
@@ -818,6 +820,8 @@ const I18N = {
downloadMenu:"Download diagram (SVG/PNG)", downloadMenu:"Download diagram (SVG/PNG)",
minimize:"minimize", normal:"normal", maximize:"maximize", minimize:"minimize", normal:"normal", maximize:"maximize",
agenda:"Legend", discarded:"discarded", agenda:"Legend", discarded:"discarded",
gutterTooltip:"Drag to resize, double-click resets", gutterAria:"Resize the areas",
hintGutterAria:"Resize editor and legend",
modeHorizontal:"Horizontal org chart, diagram above the editor", modeHorizontal:"Horizontal org chart, diagram above the editor",
modeKompakt:"Compact everything downward, space-saving", modeKompakt:"Compact everything downward, space-saving",
modeVertikal:"Vertical tree to the right, diagram beside the editor", modeVertikal:"Vertical tree to the right, diagram beside the editor",
@@ -866,6 +870,8 @@ const I18N = {
downloadMenu:"Descargar el diagrama (SVG/PNG)", downloadMenu:"Descargar el diagrama (SVG/PNG)",
minimize:"minimizar", normal:"normal", maximize:"maximizar", minimize:"minimizar", normal:"normal", maximize:"maximizar",
agenda:"Leyenda", discarded:"descartados", agenda:"Leyenda", discarded:"descartados",
gutterTooltip:"Arrastra para redimensionar, doble clic restablece", gutterAria:"Redimensionar las áreas",
hintGutterAria:"Redimensionar editor y leyenda",
modeHorizontal:"Horizontal organigrama, diagrama sobre el editor", modeHorizontal:"Horizontal organigrama, diagrama sobre el editor",
modeKompakt:"Compacto todo hacia abajo, ahorra espacio", modeKompakt:"Compacto todo hacia abajo, ahorra espacio",
modeVertikal:"Vertical árbol hacia la derecha, diagrama junto al editor", modeVertikal:"Vertical árbol hacia la derecha, diagrama junto al editor",
@@ -914,6 +920,8 @@ const I18N = {
downloadMenu:"Télécharger le diagramme (SVG/PNG)", downloadMenu:"Télécharger le diagramme (SVG/PNG)",
minimize:"réduire", normal:"normal", maximize:"agrandir", minimize:"réduire", normal:"normal", maximize:"agrandir",
agenda:"Légende", discarded:"abandonnés", agenda:"Légende", discarded:"abandonnés",
gutterTooltip:"Glisser pour redimensionner, double-clic pour réinitialiser", gutterAria:"Redimensionner les zones",
hintGutterAria:"Redimensionner l'éditeur et la légende",
modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur", modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur",
modeKompakt:"Compact tout vers le bas, gain de place", modeKompakt:"Compact tout vers le bas, gain de place",
modeVertikal:"Vertical arbre vers la droite, diagramme à côté de l'éditeur", modeVertikal:"Vertical arbre vers la droite, diagramme à côté de l'éditeur",
@@ -962,6 +970,8 @@ const I18N = {
downloadMenu:"Pobierz diagram (SVG/PNG)", downloadMenu:"Pobierz diagram (SVG/PNG)",
minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj", minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj",
agenda:"Legenda", discarded:"odrzucone", agenda:"Legenda", discarded:"odrzucone",
gutterTooltip:"Przeciągnij, aby zmienić rozmiar; dwuklik przywraca", gutterAria:"Zmień rozmiar obszarów",
hintGutterAria:"Zmień rozmiar edytora i legendy",
modeHorizontal:"Poziomo schemat organizacyjny, diagram nad edytorem", modeHorizontal:"Poziomo schemat organizacyjny, diagram nad edytorem",
modeKompakt:"Kompaktowo wszystko w dół, oszczędza miejsce", modeKompakt:"Kompaktowo wszystko w dół, oszczędza miejsce",
modeVertikal:"Pionowo drzewo w prawo, diagram obok edytora", modeVertikal:"Pionowo drzewo w prawo, diagram obok edytora",
@@ -1010,6 +1020,8 @@ const I18N = {
downloadMenu:"Скачать диаграмму (SVG/PNG)", downloadMenu:"Скачать диаграмму (SVG/PNG)",
minimize:"свернуть", normal:"обычный", maximize:"развернуть", minimize:"свернуть", normal:"обычный", maximize:"развернуть",
agenda:"Легенда", discarded:"отклонённые", agenda:"Легенда", discarded:"отклонённые",
gutterTooltip:"Потяните, чтобы изменить размер; двойной щелчок сбрасывает", gutterAria:"Изменить размер областей",
hintGutterAria:"Изменить размер редактора и легенды",
modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором", modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором",
modeKompakt:"Компактно – всё вниз, экономит место", modeKompakt:"Компактно – всё вниз, экономит место",
modeVertikal:"Вертикально – дерево вправо, диаграмма рядом с редактором", modeVertikal:"Вертикально – дерево вправо, диаграмма рядом с редактором",
@@ -1058,6 +1070,8 @@ const I18N = {
downloadMenu:"आरेख डाउनलोड करें (SVG/PNG)", downloadMenu:"आरेख डाउनलोड करें (SVG/PNG)",
minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें", minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें",
agenda:"लेजेंड", discarded:"अस्वीकृत", agenda:"लेजेंड", discarded:"अस्वीकृत",
gutterTooltip:"आकार बदलने के लिए खींचें, डबल-क्लिक रीसेट करता है", gutterAria:"क्षेत्रों का आकार बदलें",
hintGutterAria:"संपादक और लेजेंड का आकार बदलें",
modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख", modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख",
modeKompakt:"सघन – सब नीचे की ओर, जगह बचाता है", modeKompakt:"सघन – सब नीचे की ओर, जगह बचाता है",
modeVertikal:"लंबवत – पेड़ दाईं ओर, संपादक के बगल में आरेख", modeVertikal:"लंबवत – पेड़ दाईं ओर, संपादक के बगल में आरेख",
@@ -1106,6 +1120,8 @@ const I18N = {
downloadMenu:"下载图表(SVG/PNG", downloadMenu:"下载图表(SVG/PNG",
minimize:"最小化", normal:"正常", maximize:"最大化", minimize:"最小化", normal:"正常", maximize:"最大化",
agenda:"图例", discarded:"已放弃", agenda:"图例", discarded:"已放弃",
gutterTooltip:"拖动可调整大小,双击可重置", gutterAria:"调整区域大小",
hintGutterAria:"调整编辑器和图例大小",
modeHorizontal:"横向——组织结构图,图表在编辑器上方", modeHorizontal:"横向——组织结构图,图表在编辑器上方",
modeKompakt:"紧凑——全部向下,节省空间", modeKompakt:"紧凑——全部向下,节省空间",
modeVertikal:"纵向——树向右展开,图表在编辑器旁边", modeVertikal:"纵向——树向右展开,图表在编辑器旁边",
@@ -1154,6 +1170,8 @@ const I18N = {
downloadMenu:"ダイアグラムをダウンロード(SVG/PNG)", downloadMenu:"ダイアグラムをダウンロード(SVG/PNG)",
minimize:"最小化", normal:"標準", maximize:"最大化", minimize:"最小化", normal:"標準", maximize:"最大化",
agenda:"凡例", discarded:"破棄", agenda:"凡例", discarded:"破棄",
gutterTooltip:"ドラッグでサイズ変更、ダブルクリックでリセット", gutterAria:"領域のサイズを変更",
hintGutterAria:"エディターと凡例のサイズを変更",
modeHorizontal:"横 — 組織図、ダイアグラムはエディターの上", modeHorizontal:"横 — 組織図、ダイアグラムはエディターの上",
modeKompakt:"コンパクト — すべて下方向、省スペース", modeKompakt:"コンパクト — すべて下方向、省スペース",
modeVertikal:"縦 — ツリーを右へ、ダイアグラムはエディターの横", modeVertikal:"縦 — ツリーを右へ、ダイアグラムはエディターの横",
@@ -1353,6 +1371,12 @@ function saveUI(){
split: splitState, split: splitState,
col: app.style.getPropertyValue('--col') || null, col: app.style.getPropertyValue('--col') || null,
drow: app.style.getPropertyValue('--drow') || null, drow: app.style.getPropertyValue('--drow') || null,
/* Legende: auf/zu + Aufteilung Editor|Legende (D26). Bewusst per
DOM-Abfrage statt über die `agenda`-Konstante die wird erst weiter
unten deklariert, und saveUI() läuft schon aus applySplit() heraus. */
agenda: !!document.querySelector('.agenda.open'),
hcol: app.style.getPropertyValue('--hcol') || null,
hrow: app.style.getPropertyValue('--hrow') || null,
zoom: zoom, zoom: zoom,
fullscreen: document.body.classList.contains('fullscreen') fullscreen: document.body.classList.contains('fullscreen')
})); }));
@@ -1387,6 +1411,10 @@ function restoreState(){
if(ui && ui.col) app.style.setProperty('--col', ui.col); if(ui && ui.col) app.style.setProperty('--col', ui.col);
if(ui && ui.drow) app.style.setProperty('--drow', ui.drow); if(ui && ui.drow) app.style.setProperty('--drow', ui.drow);
} }
/* Legende (D26): Aufteilung gilt unabhängig vom Preset des großen Splitters. */
if(ui && ui.hcol) app.style.setProperty('--hcol', ui.hcol);
if(ui && ui.hrow) app.style.setProperty('--hrow', ui.hrow);
setAgendaOpen(!!(ui && ui.agenda));
applyZoom(); applyZoom();
restoring = false; restoring = false;
} }
@@ -1595,13 +1623,68 @@ window.addEventListener('orientationchange', setAppHeight);
if(window.visualViewport){ if(window.visualViewport){
window.visualViewport.addEventListener('resize', setAppHeight); window.visualViewport.addEventListener('resize', setAppHeight);
} }
/* ---------- Legende: auf/zu + Splitter zum Editor (D26) ----------
Die Legende ist ein gewöhnlicher Container (kein <details> mehr, siehe
style.css/D26); der Auf-/Zu-Zustand hängt an der Klasse `open`. */
const legendBtn = document.getElementById('legendBtn'); const legendBtn = document.getElementById('legendBtn');
const agenda = document.querySelector('.agenda'); const agenda = document.getElementById('agenda');
legendBtn.addEventListener('click', () => { const agendaToggle = document.getElementById('agendaToggle');
agenda.open = !agenda.open; const hintGutter = document.getElementById('hintGutter');
legendBtn.classList.toggle('active', agenda.open); const editorBody = document.querySelector('.editor-body');
function agendaOpen(){ return agenda.classList.contains('open'); }
function setAgendaOpen(on){
agenda.classList.toggle('open', on);
agendaToggle.setAttribute('aria-expanded', on ? 'true' : 'false');
legendBtn.classList.toggle('active', on);
hintGutter.hidden = !on; /* ohne offene Legende gibt es nichts zu teilen */
saveUI();
}
legendBtn.addEventListener('click', () => setAgendaOpen(!agendaOpen()));
agendaToggle.addEventListener('click', () => setAgendaOpen(!agendaOpen()));
/* Splitter Editor|Legende. Im horizontalen Modus liegen sie nebeneinander
(Legendenbreite `--hcol`), in den gestapelten Modi untereinander
(Legendenhöhe `--hrow`) dieselbe Fallunterscheidung wie beim großen
Splitter. Gezogen wird von der Editorseite aus, die Legende behält also
den Abstand zum jeweils gegenüberliegenden Rand. */
const HINT_MIN = 90; /* Titelzeile + ein paar Zeilen bleiben immer da */
const HINT_MAX_SHARE = 0.85;
function editorStacked(){ return isMobile() || app.classList.contains('side'); }
let hintDragging = false;
hintGutter.addEventListener('pointerdown', e => {
hintDragging = true;
hintGutter.classList.add('dragging');
hintGutter.setPointerCapture(e.pointerId);
document.body.style.userSelect = 'none';
e.preventDefault();
});
hintGutter.addEventListener('pointermove', e => {
if(!hintDragging) return;
const b = editorBody.getBoundingClientRect();
if(editorStacked()){
const h = Math.min(Math.max(b.bottom - e.clientY, HINT_MIN), b.height * HINT_MAX_SHARE);
app.style.setProperty('--hrow', Math.round(h) + 'px');
} else {
const w = Math.min(Math.max(b.right - e.clientX, HINT_MIN), b.width * HINT_MAX_SHARE);
app.style.setProperty('--hcol', Math.round(w) + 'px');
}
});
function endHintDrag(e){
if(!hintDragging) return;
hintDragging = false;
hintGutter.classList.remove('dragging');
document.body.style.userSelect = '';
try{ hintGutter.releasePointerCapture(e.pointerId); }catch(_){}
saveUI();
}
hintGutter.addEventListener('pointerup', endHintDrag);
hintGutter.addEventListener('pointercancel', endHintDrag);
/* Doppelklick stellt die Vorgabegröße wieder her (CSS-Default greift dann). */
hintGutter.addEventListener('dblclick', () => {
app.style.removeProperty('--hcol');
app.style.removeProperty('--hrow');
saveUI();
}); });
agenda.addEventListener('toggle', () => legendBtn.classList.toggle('active', agenda.open));
/* Modus-Wähler auf kleinem Bildschirm: es ist nur das aktive Icon sichtbar, /* Modus-Wähler auf kleinem Bildschirm: es ist nur das aktive Icon sichtbar,
Tippen schaltet zum nächsten Modus (reihum). Auf normaler Größe bleibt es Tippen schaltet zum nächsten Modus (reihum). Auf normaler Größe bleibt es
der Dreier-Umschalter dort kehrt der Handler sofort zurück. */ der Dreier-Umschalter dort kehrt der Handler sofort zurück. */
+34 -12
View File
@@ -208,28 +208,48 @@
Horizontal nebeneinander, vertikal (.side) untereinander. */ Horizontal nebeneinander, vertikal (.side) untereinander. */
.editor-body{display:flex;align-items:stretch;flex:1 1 auto;min-height:0} .editor-body{display:flex;align-items:stretch;flex:1 1 auto;min-height:0}
.editor-body textarea{flex:1 1 auto;min-width:0;min-height:0} .editor-body textarea{flex:1 1 auto;min-width:0;min-height:0}
/* Legende: bewusst KEIN <details> mehr. Chrome legt den Details-Inhalt in
`::details-content`; dadurch war `.hint` kein Flex-Kind von `.agenda` mehr,
`flex:1/min-height:0` liefen ins Leere und der Inhalt wurde geclippt statt
scrollbar (D26). Als gewöhnlicher Container ist die Kette wieder dicht. */
.agenda{ .agenda{
flex:0 0 auto; flex:0 0 auto;
border-left:1px dashed rgba(36,52,71,.18); border-left:1px dashed rgba(36,52,71,.18);
display:flex;flex-direction:column; display:flex;flex-direction:column;min-height:0;min-width:0;
} }
.agenda[open]{width:300px} /* Die Aufteilung steht als feste px-Größe. Schrumpft das Panel später (Zug am
.agenda>summary{ großen Splitter, Drehung, Tastatur), begrenzt max-* die Legende, damit der
list-style:none;cursor:pointer;white-space:nowrap; Editor nicht auf 0 gedrückt wird gleiche Schranke wie beim Ziehen. */
.agenda.open{width:var(--hcol,300px);max-width:85%;overflow:hidden}
.agenda-summary{
flex:0 0 auto;
background:none;border:0;text-align:left;
cursor:pointer;white-space:nowrap;
padding:10px 16px; padding:10px 16px;
font-family:'IBM Plex Mono',monospace; font-family:'IBM Plex Mono',monospace;
font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted); font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);
display:flex;align-items:center;gap:7px; display:flex;align-items:center;gap:7px;
} }
.agenda>summary::-webkit-details-marker{display:none} .agenda-summary::before{content:'▸';display:inline-block;transition:transform .15s}
.agenda>summary::before{content:'▸';display:inline-block;transition:transform .15s} .agenda.open>.agenda-summary::before{transform:rotate(90deg)}
.agenda[open]>summary::before{transform:rotate(90deg)} .agenda-summary:focus-visible{outline:2px solid var(--or);outline-offset:-2px}
.agenda{min-height:0} .agenda:not(.open) .hint{display:none}
.agenda[open]{overflow:hidden}
.agenda .hint{border-top:0;flex:1 1 auto;min-height:0;overflow:auto} .agenda .hint{border-top:0;flex:1 1 auto;min-height:0;overflow:auto}
/* Splitter zwischen Editor und Legende dieselbe Sprache wie der große
Splitter, nur schmaler. Ohne offene Legende gibt es nichts zu teilen. */
.hint-gutter{
flex:0 0 10px;align-self:stretch;
cursor:col-resize;touch-action:none;
display:flex;align-items:center;justify-content:center;
}
.hint-gutter[hidden]{display:none}
.hint-gutter::before{content:'';width:3px;height:38px;border-radius:2px;background:rgba(36,52,71,.18)}
.hint-gutter:hover::before,.hint-gutter.dragging::before{background:var(--or)}
.side .editor-body{flex-direction:column;align-items:stretch} .side .editor-body{flex-direction:column;align-items:stretch}
.side .agenda{border-left:0;border-top:1px dashed rgba(36,52,71,.18)} .side .agenda{border-left:0;border-top:1px dashed rgba(36,52,71,.18)}
.side .agenda[open]{width:auto} .side .agenda.open{width:auto;max-width:none;height:var(--hrow,220px);max-height:85%}
.side .hint-gutter{cursor:row-resize}
.side .hint-gutter::before{width:38px;height:3px}
.panel{ .panel{
background:var(--card); background:var(--card);
@@ -713,9 +733,11 @@
/* Editor immer gestapelt (Textarea oben, Legende darunter) */ /* Editor immer gestapelt (Textarea oben, Legende darunter) */
body.mobile .editor-body{flex-direction:column;align-items:stretch} 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{border-left:0;border-top:1px dashed rgba(36,52,71,.18)}
body.mobile .agenda[open]{width:auto} body.mobile .agenda.open{width:auto;max-width:none;height:var(--hrow,220px);max-height:85%}
body.mobile .hint-gutter{cursor:row-resize}
body.mobile .hint-gutter::before{width:38px;height:3px}
/* Legende: keine dauerhafte Summary-Zeile, dafür der #legendBtn im Kopf */ /* Legende: keine dauerhafte Summary-Zeile, dafür der #legendBtn im Kopf */
body.mobile .agenda>summary{display:none} body.mobile .agenda-summary{display:none}
body.mobile .legendbtn{display:inline-flex} body.mobile .legendbtn{display:inline-flex}
/* Sprachwahl schlank: eingeklappt nur die aktive Sprache. Ein Tipp klappt die /* Sprachwahl schlank: eingeklappt nur die aktive Sprache. Ein Tipp klappt die
volle Leiste als Overlay über die Kopfzeile auf (verdeckt fsToggle usw., volle Leiste als Overlay über die Kopfzeile auf (verdeckt fsToggle usw.,