From 037db30d96a631deec65c03433d89b944cb81052 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sun, 23 Aug 2026 07:50:34 +0200 Subject: [PATCH] feat: Debug-Kasten minimierbar; Mobil-Inhalte ~25 % kleiner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debug-Panel: Ein Klick entfernte es bisher ganz — was nichts half, weil der 15-Sekunden-Takt es sofort wieder aufbaute; auf dem Telefon verdeckte es damit dauerhaft die untere rechte Ecke. Jetzt schaltet der Klick zwischen Kasten und einem 26-px-Icon (⟳) unten rechts um. Der Zustand liegt im localStorage, nicht am Element: Das Panel wird bei jedem Takt neu bespielt, ein Zustand am DOM-Knoten wäre beim nächsten Tick weg. Über einen echten Intervall-Durchlauf geprüft. Der aufgeklappte Kasten bekommt white-space:pre-wrap — die mit \n gefügten Zeilen liefen bisher zu einem Absatz zusammen. Mobil ~25 % kleiner: MOBILE_ZOOM=0.75 als Faktor AUF den Nutzer-Zoom, nicht als neuer Anfangswert — sonst wäre die Verkleinerung nach dem ersten Zoomen weg und „Zurücksetzen" führte auf eine Größe, die auf dem Telefon zu groß ist. Die Anzeige nennt den effektiven Wert (75 %), sie soll beschreiben was man sieht. Die drei Messstellen rechnen jetzt effZoom() zurück; nachgemessen bei 0,75 liegen alle fünf Stationspunkte exakt auf ihren Blattknoten (0 px). Der Text kann kein zoom bekommen — Zeilennummern-Streifen und Spiegel messen am Textfeld (D33) und die Streifenbreite rechnet in ch. Also font-size .64rem an Textfeld und Streifen gemeinsam, Innenabstand im selben Verhältnis mit. 27 statt 19 Zeilen im Bild. Dabei aufgefallen: Der Grafikexport war schon immer zoom-abhängig falsch — die SVG-Schriftgrößen sind feste Zahlen, die Kästen gemessen. Fiel nie auf, weil 100 % der Normalfall war; mit dem Mobil-Faktor wäre es der Regelfall geworden. diagramToSvg() stellt den Zoom für die Messung jetzt kurz auf 1 (Knoten auf dem Schirm 143 px, im SVG 189,7 px — die unskalierte Größe). Desktop unverändert nachgeprüft (Zoom 1, 100 %, 13,6 px). Prod-Build enthält das Debug-Panel weiterhin gar nicht. SPEC §9, D17 (Nachtrag 2) und frontend/CLAUDE.md nachgezogen. Co-Authored-By: Claude Fable 5 --- docs/DECISIONS.md | 48 ++++++++++++++++++++++++ docs/SPEC.md | 7 +++- frontend/CLAUDE.md | 14 ++++++- frontend/src/app.js | 85 ++++++++++++++++++++++++++++-------------- frontend/src/style.css | 10 +++++ 5 files changed, 135 insertions(+), 29 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 8a0c761..e48993a 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -243,6 +243,54 @@ Mobil, die Grid-Minima `--pmin-d`/`--pmin-e`, `syncPanelMins()`, `setMobileDrow()` und das Titelzeilen-Tippen; Desktop behält Splitter, Presets und Fenster-Buttons unverändert. +**Nachtrag 2 — auf Mobil sind die Inhalte grundsätzlich ~25 % kleiner, und der +Debug-Kasten minimiert sich statt zu verschwinden.** + +**Verkleinerung als Faktor, nicht als neuer Anfangswert.** `MOBILE_ZOOM = 0.75` +multipliziert den Nutzer-Zoom (`effZoom()`), statt ihn beim ersten Start auf +0,75 zu setzen. Der Unterschied zählt: Ein Anfangswert wäre nach dem ersten +Zoomen weg, und „Zurücksetzen" führte zurück auf eine Größe, die auf dem +Telefon zu groß ist. Als Faktor bleibt die Verkleinerung eine Eigenschaft des +Geräts, und der Regler arbeitet relativ dazu weiter. Die Anzeige nennt den +**effektiven** Wert (75 % statt 100 %) — sie soll beschreiben, was man sieht, +nicht was man eingestellt hat. + +**Die drei Messstellen mussten mit.** `alignStems()`, `drawCheapPath()` und +`drawDepLinks()` rechnen gemessene Pixel durch den CSS-`zoom` zurück; sie lesen +jetzt `effZoom()` statt `zoom`. Nachgemessen bei 0,75: alle fünf +Stationspunkte liegen exakt (0 px Abweichung) auf ihren Blattknoten. + +**Der Text kann kein `zoom` bekommen.** Zeilennummern-Streifen und Spiegel +messen am Textfeld (D33), und der Streifen rechnet seine Breite in `ch` — beide +folgen der **Schriftgröße**, nicht einem Zoom auf einem Vorfahren. Also +`font-size: .64rem` (= 0,85 × 0,75) an Textfeld **und** Streifen gemeinsam; +`line-height` ist einheitenlos und skaliert mit. Der Innenabstand des +Textfelds schrumpft im selben Verhältnis mit (14/16 → 10/12 px), sonst wäre der +Rand auf 375 px unverhältnismäßig breit. Ergebnis: 27 statt 19 Zeilen im Bild. + +**Dabei aufgefallen: der Grafikexport war schon immer zoom-abhängig falsch.** +Die Schriftgrößen im Ausgabe-SVG sind feste Zahlen (14 für Labels, 9–11 für +Badges), die Kästen kommen aus der Live-Messung — bei jedem Zoom ≠ 1 passten +Text und Kasten nicht zueinander. Das fiel nie auf, weil 100 % der Normalfall +war; mit der Mobil-Verkleinerung wäre es der Regelfall geworden. `diagramToSvg()` +stellt den Zoom für die Messung deshalb kurz auf 1 und danach zurück — +derselbe Griff wie bei der `exporting`-Klasse (D25-Nachtrag), und die Funktion +läuft synchron, es wird nichts davon gezeichnet. Nachgemessen: Knoten auf dem +Schirm 143 px, im SVG 189,7 px — also die unskalierte Größe, passend zur festen +Schrift. + +**Das Debug-Panel minimiert sich jetzt, statt sich zu schließen.** Ein Klick +entfernte es bisher ganz — was nichts half, weil der 15-Sekunden-Takt es sofort +wieder aufbaute; auf dem Telefon verdeckte es damit dauerhaft die untere rechte +Ecke. Jetzt schaltet der Klick zwischen Kasten und einem 26-px-Icon (⟳) unten +rechts um. Der Zustand liegt im **localStorage**, nicht am Element: Das Panel +wird bei jedem Takt neu bespielt, ein Zustand am DOM-Knoten wäre also beim +nächsten Tick weg. Nachgeprüft über einen echten Intervall-Durchlauf und über +den `visibilitychange`-Pfad — es bleibt minimiert. (Test-Hilfe, im Prod-Build +ohnehin unterdrückt; die Gelegenheit genutzt, dem aufgeklappten Kasten +`white-space: pre-wrap` zu geben — die mit `\n` gefügten Zeilen liefen bisher +zu einem Absatz zusammen.) + ## D18 — Günstigsten Pfad per Inversion zeigen, fehlende Größe = M Der kostengünstigste Weg durch den Baum wird hervorgehoben (Umschalter im Diagramm-Kopf, Default an, Zustand persistiert). Nötig sind bei **all of** alle diff --git a/docs/SPEC.md b/docs/SPEC.md index c3fedc9..146e718 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -403,7 +403,12 @@ damit genau dann zu sehen, wenn der Text vorn ist. Die übrigen umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen komprimierten Form. Der sichtbare Bereich wird gemerkt; ein Sprung zwischen Diagramm und Text (§9) holt den nötigen Bereich selbst nach vorn. -Voreinstellung ist das Diagramm. Die **Legende** bekommt hier einen eigenen +Voreinstellung ist das Diagramm. Beide Inhalte werden hier zudem rund **25 % +kleiner** dargestellt, damit mehr Plan auf die Fläche passt; das ist ein Faktor +**auf** den Zoom, der Zoom-Regler arbeitet unverändert relativ dazu und zeigt +den effektiven Wert (auf dem Telefon also 75 %). Der **Grafikexport** ist davon +unberührt — er zeichnet immer die unskalierte Geometrie nach. 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 e27323a..19ff9b3 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -395,7 +395,19 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der Bereich selbst um: `revealEditor()` auf Text, `focusNodeOfCaret()` aufs Diagramm — **vor** dem Zentrieren. Zustand in `werkbaum-ui` (`mobilePane`), Variable oben bei `padView` deklariert (saveUI liest sie). - `applyLayout` ruft auf Mobil **kein** `applySplit`. Dazu eigener Legenden-Umschalter + `applyLayout` ruft auf Mobil **kein** `applySplit`. + **Inhalte ~25 % kleiner** (D17-Nachtrag 2): `MOBILE_ZOOM = 0.75` als **Faktor** + auf den Nutzer-Zoom — `effZoom()` ist die Wahrheit, die `applyZoom()` setzt und + die die drei Messstellen (`alignStems`, `drawCheapPath`, `drawDepLinks`) + zurückrechnen; wer dort wieder `zoom` einsetzt, verschiebt Linien und + Stationspunkte. `applyMobile()` ruft `applyZoom()` in **beiden** Zweigen, sonst + bliebe der Faktor beim Wechsel auf Desktop stehen. Der **Text** bekommt kein + `zoom`, sondern `font-size` (Streifen und Textfeld gemeinsam) — Spiegel und + `ch`-Breite hängen an der Schriftgröße (D33). **`diagramToSvg()` stellt den + Zoom für die Messung auf 1**: Die Schriftgrößen im Ausgabe-SVG sind feste + Zahlen, die Kästen gemessen — bei Zoom ≠ 1 passt sonst beides nicht zusammen + (galt schon vorher, wurde durch den Mobil-Faktor nur zum Regelfall). + 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 diff --git a/frontend/src/app.js b/frontend/src/app.js index e551904..f8751f4 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -259,7 +259,7 @@ function applyOptStairs(){ function alignStems(){ out.querySelectorAll('ul.and>li').forEach(li => li.style.removeProperty('--stem-x')); if(out.classList.contains('vertical') || out.classList.contains('kompakt')) return; - const z = zoom || 1; + const z = effZoom() || 1; /* `li.opt-group` (Treppe) hat keinen eigenen Knoten — der Stiel zielt auf den ERSTEN Knoten der Kaskade. */ out.querySelectorAll('ul.and>li.has-or, ul.and>li.opt-group').forEach(li => { @@ -277,7 +277,7 @@ function drawCheapPath(){ const leaves = [...out.querySelectorAll('.node.cheap-leaf')]; /* Dokument-Reihenfolge = Lese-Reihenfolge */ if(leaves.length < 2) return; const outRect = out.getBoundingClientRect(); - const z = zoom || 1; + const z = effZoom() || 1; if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */ const pts = leaves.map(el => { const r = el.getBoundingClientRect(); @@ -366,7 +366,7 @@ function drawDepLinks(){ if(!edges.length) return; const outRect = out.getBoundingClientRect(); if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */ - const z = zoom || 1; + const z = effZoom() || 1; const rect = el => { const r = el.getBoundingClientRect(); return {x:(r.left - outRect.left)/z, y:(r.top - outRect.top)/z, w:r.width/z, h:r.height/z, @@ -405,6 +405,15 @@ function diagramToSvg(){ Klasse statt durch Abnehmen von `.current`/`.pulse` — so reißt der Export keine laufende Puls-Animation ab und startet sie hinterher nicht neu. */ out.classList.add('exporting'); + /* Zoom für die Messung auf 1 stellen. Die Schriftgrößen im Ausgabe-SVG sind + feste Zahlen (14 für Labels, 9–11 für Badges), die Kästen kommen dagegen + aus der Messung — bei jedem Zoom ≠ 1 passten Text und Kasten also nicht + zueinander. Das fiel bisher kaum auf, weil 100 % der Normalfall war; mit + der Mobil-Verkleinerung (D17-Nachtrag 2) wäre es der Regelfall geworden. + Derselbe Griff wie bei `exporting`: kurz neutralisieren, hinterher zurück — + die Funktion läuft synchron, es wird nichts davon gezeichnet. */ + const zoomBefore = out.style.zoom; + out.style.zoom = 1; const treeRect = out.getBoundingClientRect(); const PAD = 24; const W = Math.ceil(treeRect.width) + PAD*2; @@ -581,6 +590,7 @@ function diagramToSvg(){ }); const svg = `${parts.join('')}`; + out.style.zoom = zoomBefore; out.classList.remove('exporting'); return {svg, W, H}; } @@ -1166,6 +1176,14 @@ const ZMIN = 0.3, ZMAX = 3, ZSTEP = 0.1; const ZOOM_COLLAPSE_DELAY = 3000; /* 3 Sekunden */ let zoom = 1; let zoomCollapseTimeout; +/* Auf kleinem Bildschirm wird der Inhalt grundsätzlich verkleinert (~25 %), + damit mehr Plan auf die Fläche passt (D17-Nachtrag 2). Das ist ein Faktor + AUF den Nutzer-Zoom, kein neuer Anfangswert: Wer hineinzoomt, tut das + weiterhin relativ hierzu. Der Text bekommt dieselbe Verkleinerung über die + Schriftgröße (style.css) — CSS-`zoom` verbietet sich dort, weil der + Zeilennummern-Streifen und der Spiegel am Textfeld messen (D33). */ +const MOBILE_ZOOM = 0.75; +function effZoom(){ return zoom * (isMobile() ? MOBILE_ZOOM : 1); } function resetZoomCollapseTimeout(){ const zoomctl = document.querySelector('.zoomctl'); @@ -1179,8 +1197,11 @@ function resetZoomCollapseTimeout(){ function applyZoom(){ zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(zoom * 100) / 100)); - out.style.zoom = zoom; - document.getElementById('zoomReset').textContent = Math.round(zoom * 100) + ' %'; + out.style.zoom = effZoom(); + /* Angezeigt wird der EFFEKTIVE Wert — die Anzeige soll beschreiben, was man + sieht. Auf Mobil steht dort also 75 %, und das erklärt die Verkleinerung, + statt sie als „100 %" zu behaupten. */ + document.getElementById('zoomReset').textContent = Math.round(effZoom() * 100) + ' %'; resetZoomCollapseTimeout(); saveUI(); } @@ -2959,6 +2980,7 @@ function applyMobile(){ ausgeblendet — die Regeln hängen zwar an `body.mobile`, aber ein stehengebliebener Zustand ist beim nächsten Verkleinern verwirrend. */ document.body.classList.remove('pane-diagram', 'pane-text'); + applyZoom(); /* Mobil-Faktor wieder herausrechnen (D17-Nachtrag 2) */ return; } /* Genau ein Bereich (D17-Nachtrag): kein Desktop-Collapse, keine Aufteilung. @@ -2967,6 +2989,7 @@ function applyMobile(){ clearCollapse(); app.style.removeProperty('--drow'); applyMobilePane(); + applyZoom(); /* Mobil-Faktor an-/abschalten (D17-Nachtrag 2) */ /* Default Vollbild auf kleinem Bildschirm — nur ohne gespeicherte Wahl. */ if(!hadStoredUI && !document.body.classList.contains('fullscreen')){ document.body.classList.add('fullscreen'); @@ -3127,36 +3150,44 @@ function logUpdate(msg){ localStorage.setItem('werkbaum-update-log', log.join('\n')); } -/* Debug-Panel anzeigen (Test-Hilfe; im Prod-Build unterdrückt) */ +/* Debug-Panel anzeigen (Test-Hilfe; im Prod-Build unterdrückt). + Ein Klick **minimiert** es auf ein Icon unten rechts, ein weiterer holt es + zurück. Vorher entfernte der Klick es ganz — das half nichts, weil der + 15-Sekunden-Takt es gleich wieder aufbaute; auf dem Telefon verdeckte es so + dauerhaft die untere rechte Ecke. Der Zustand liegt im localStorage, nicht am + Element: Das Panel wird bei jedem Takt neu bespielt und überlebt so auch ein + Neuladen. */ +const LS_DEBUG_MIN = 'werkbaum-update-debug-min'; +function paintUpdateDebug(panel){ + const min = localStorage.getItem(LS_DEBUG_MIN) === '1'; + panel.style.cssText = + 'position:fixed;bottom:10px;right:10px;z-index:999;' + + 'background:rgba(0,0,0,0.9);color:#0F766E;border:1px solid #0F766E;' + + 'font-family:monospace;cursor:pointer;' + + (min + ? 'width:26px;height:26px;padding:0;border-radius:50%;font-size:14px;' + + 'display:flex;align-items:center;justify-content:center;overflow:hidden;' + : 'padding:12px;border-radius:4px;font-size:11px;max-width:240px;' + + 'max-height:120px;overflow-y:auto;white-space:pre-wrap;'); + panel.title = 'Update Debug Panel – Klick zum ' + (min ? 'Aufklappen' : 'Minimieren'); + const log = localStorage.getItem('werkbaum-update-log') || ''; + panel.textContent = min ? '⟳' + : (log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge'); +} function showUpdateDebug(){ if(isProdBuild) return; let panel = document.getElementById('updateDebugPanel'); if(!panel){ panel = document.createElement('div'); panel.id = 'updateDebugPanel'; - panel.style.cssText = ` - position: fixed; - bottom: 10px; - right: 10px; - background: rgba(0,0,0,0.9); - color: #0F766E; - padding: 12px; - border-radius: 4px; - font-size: 11px; - font-family: monospace; - max-width: 240px; - max-height: 120px; - overflow-y: auto; - z-index: 999; - border: 1px solid #0F766E; - cursor: pointer; - `; - panel.title = 'Update Debug Panel – Klick zum Schließen'; document.body.appendChild(panel); - panel.addEventListener('click', () => panel.remove()); + panel.addEventListener('click', () => { + const min = localStorage.getItem(LS_DEBUG_MIN) !== '1'; + try{ localStorage.setItem(LS_DEBUG_MIN, min ? '1' : '0'); }catch(_){} + paintUpdateDebug(panel); + }); } - const log = localStorage.getItem('werkbaum-update-log') || ''; - panel.textContent = log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge'; + paintUpdateDebug(panel); } /* Reset-Button im Header neben Fullscreen (Test-Hilfe; im Prod-Build weggelassen) */ diff --git a/frontend/src/style.css b/frontend/src/style.css index d0af893..17bd737 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1078,6 +1078,16 @@ Editor-Kopf tut das weiterhin der Dokumenten-Wähler daneben. */ body.mobile .panetoggle{display:inline-flex} body.mobile .panel.right .panetoggle{margin-right:auto} + /* Inhalte rund 25 % kleiner, damit mehr Plan auf die Fläche passt + (D17-Nachtrag 2). Das Diagramm macht das über den CSS-`zoom` (Faktor auf + den Nutzer-Zoom, siehe MOBILE_ZOOM in app.js); der Text über die + Schriftgröße — `zoom` verbietet sich hier, weil Zeilennummern-Streifen und + Spiegel am Textfeld messen (D33) und der Streifen seine Breite in `ch` + rechnet. Beide müssen dieselbe Typografie tragen, sonst wandern die Zahlen + weg; `line-height` ist einheitenlos und skaliert von selbst mit. */ + body.mobile textarea, + body.mobile .lineno{font-size:.64rem} + body.mobile textarea{padding:10px 12px} /* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */ body.mobile .sub-full{display:none} body.mobile .sub-short{display:inline}