From edefce35723800511eefc6b7883cc55933e850ed Mon Sep 17 00:00:00 2001 From: marc Date: Tue, 25 Aug 2026 13:02:06 +0200 Subject: [PATCH] feat(pdf): Strg + Mausrad zoomt, Zoomgruppe rechts aussen Die Zoomknoepfe sitzen jetzt rechts in der Leiste statt links neben der Seitenzahl - dieselbe Anordnung wie in InkNote. Die dritte Rasterspalte war ohnehin da, damit die Suche wirklich mittig sitzt; jetzt traegt sie auch etwas. Schmal bleibt der Zoom in der ersten Zeile, sonst waere die Leiste am Telefon dreizeilig geworden. Strg (oder Cmd) + Mausrad zoomt das Dokument. Der Zuhoerer wird von Hand angemeldet: React meldet Rad-Ereignisse passiv an, und passiv darf man preventDefault nicht rufen - ohne das zoomt der Browser die ganze Seite statt des Dokuments. Beim Rad wird der Punkt unter dem Zeiger festgehalten statt der Seite. Gemessen: Seite 2 bleibt ueber zwei Zoomstufen unter dem Zeiger. Co-Authored-By: Claude Opus 5 --- TESTING.md | 20 ++++ frontend/src/components/PdfViewer.tsx | 137 ++++++++++++++++++++------ 2 files changed, 128 insertions(+), 29 deletions(-) diff --git a/TESTING.md b/TESTING.md index 69fe564..2ac74ae 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1422,6 +1422,26 @@ Dateien bleiben markiert, statt die Auswahl zu verlieren. auf dem Handy bleibt der Knopf der Weg - [ ] Mit vielen Dateien (50+) auf der produktiven Instanz gegenprüfen +## PDF: Strg + Mausrad, Zoom nach rechts (24.08.2026) + +| Prüfung | Gemessen | +|---|---| +| Zoomgruppe breit | rechts außen, 1138–1262 px bei 1280 px Fenster | +| Suchfeld bleibt mittig | Mitte 640, Fenstermitte 640 | +| Strg + Rad hoch | 100 % → 125 % | +| Strg + Rad runter (2×) | → 75 % | +| Rad **ohne** Strg | unverändert 75 %, blättert nur | +| `preventDefault` gerufen | ja – sonst zoomte der Browser die ganze Seite | +| Punkt unter dem Zeiger | bleibt: Seite 2 vor und nach zwei Zoomstufen, Bildlauf 1400 → 2250 | +| Schmal (412 px) | Zoom in der **ersten** Zeile neben der Seitenzahl, zweizeilig wie vorher | + +Beim Rad wird der Punkt unter dem Zeiger festgehalten, nicht die Seite: Man +zeigt auf eine Stelle und will, dass sie dort bleibt. Die Knöpfe halten +weiterhin die Seite – dort gibt es keinen Zeiger, auf den man sich beziehen +könnte. + +- [ ] Am Trackpad gegenprüfen (Zwei-Finger-Zoom meldet sich als Strg + Rad) + ## Automatisiert geprüft (22.08.2026) Diese Punkte hat Claude per API/`curl` gegen Wegwerf-Konten durchgespielt; sie diff --git a/frontend/src/components/PdfViewer.tsx b/frontend/src/components/PdfViewer.tsx index 1b49f07..026a6c9 100644 --- a/frontend/src/components/PdfViewer.tsx +++ b/frontend/src/components/PdfViewer.tsx @@ -596,22 +596,104 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { * einem Zoomschritt am Anfang eines fremden Blattes. */ const keepPage = useRef(null) + /** + * Beim Zoomen mit dem Mausrad wird nicht die Seite festgehalten, sondern der + * Punkt unter dem Zeiger: Man zeigt auf eine Stelle im Text und will, dass + * sie dort bleibt, wo der Finger hindeutet – nicht, dass der Seitenanfang + * anspringt. + */ + const keepAnchor = useRef<{ px: number; py: number; cx: number; cy: number; from: number } | null>(null) useEffect(() => { + const anchor = keepAnchor.current + if (anchor) { + keepAnchor.current = null + const root = scrollRef.current + if (root) { + const ratio = effScale / anchor.from + root.scrollTop = Math.max(0, anchor.py * ratio - anchor.cy) + root.scrollLeft = Math.max(0, anchor.px * ratio - anchor.cx) + } + return + } const n = keepPage.current if (n === null) return keepPage.current = null goToPage(n) }, [effScale, goToPage]) - function zoom(dir: 1 | -1) { - const now = effScale - const next = dir > 0 + /** Die nächste Stufe in eine Richtung, oder undefined am Ende der Leiter. */ + function nextStep(now: number, dir: 1 | -1): number | undefined { + return dir > 0 ? zoomSteps.find(s => s > now + 0.001) : [...zoomSteps].reverse().find(s => s < now - 0.001) + } + + function zoom(dir: 1 | -1) { + const next = nextStep(effScale, dir) if (next) { keepPage.current = current; setScale(next) } } + /** + * Strg + Mausrad zoomt – wie in jedem Betrachter, und wie in InkNote. + * + * Von Hand angemeldet statt über onWheel: React meldet Rad-Ereignisse + * passiv an, und ein passiver Zuhörer darf `preventDefault` nicht rufen. + * Ohne das zoomt der **Browser** die ganze Seite, statt dass wir das + * Dokument zoomen – Leiste und Oberfläche würden mitwachsen. + */ + useEffect(() => { + const root = scrollRef.current + if (!root) return + function onWheel(e: WheelEvent) { + // Auch die Meta-Taste: Auf einem Mac ist sie die übliche. + if (!e.ctrlKey && !e.metaKey) return + e.preventDefault() + const root2 = scrollRef.current + if (!root2) return + const dir: 1 | -1 = e.deltaY < 0 ? 1 : -1 + const next = nextStep(effScale, dir) + if (!next) return + const r = root2.getBoundingClientRect() + const cx = e.clientX - r.left + const cy = e.clientY - r.top + keepAnchor.current = { + px: root2.scrollLeft + cx, py: root2.scrollTop + cy, + cx, cy, from: effScale, + } + setScale(next) + } + root.addEventListener('wheel', onWheel, { passive: false }) + return () => root.removeEventListener('wheel', onWheel) + }, [effScale]) + const pages = useMemo(() => sizes.map((_, i) => i + 1), [sizes]) + + /** Verkleinern, Prozentzahl (zugleich „einpassen"), Vergrößern. Einmal + * beschrieben, an zwei Stellen gezeigt – breit rechts außen, schmal in der + * ersten Zeile. */ + const zoomGroup = ( + <> + zoom(-1)} glyph="minus" /> + {/* Zurück zur eingepassten Größe. Bewusst kein Umschalter zwischen + „Breite" und „100 %": Seit die Einpassung bei 100 % gedeckelt ist, + sind das am Schreibtisch oft dieselbe Zahl – ein Knopf, der nichts + tut, ist schlimmer als keiner. */} + + zoom(1)} glyph="plus" /> + + ) /** * Reicht die Breite für drei Spalten? Unterhalb bricht die Leiste um, und * ein Raster ergäbe eine gequetschte Suche neben gequetschten Knöpfen. @@ -692,28 +774,12 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { = doc.numPages} onClick={() => goToPage(current + 1)} glyph="down" /> - - - - zoom(-1)} glyph="minus" /> - {/* Zurück zur eingepassten Größe. Bewusst kein Umschalter zwischen - „Breite" und „100 %": Seit die Einpassung bei 100 % gedeckelt ist, - sind das am Schreibtisch oft dieselbe Zahl – ein Knopf, der nichts - tut, ist schlimmer als keiner. */} - - zoom(1)} glyph="plus" /> + {barNarrow && ( + <> + + {zoomGroup} + + )} {/* Suche im Dokument – die mittlere Spalte. Sie ist der Grund, aus dem @@ -786,10 +852,23 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) { - {/* Die leere dritte Spalte. Sie trägt nichts und ist trotzdem nötig: - Ohne sie hätte das Raster nur zwei Spalten, und „Mitte" wäre - wieder die Mitte des Restes. */} - {!barNarrow &&
} + {/* Die dritte Spalte: der Zoom, rechtsbündig – dieselbe Anordnung wie + in InkNote. Die Spalte war ohnehin nötig, damit „Mitte" die Mitte + der Leiste ist und nicht die Mitte des Restes; jetzt trägt sie + auch etwas. + + Schmal steht der Zoom **nicht** hier, sondern oben neben der + Seitenzahl: Als eigene Zeile wäre die Leiste am Telefon dreizeilig + geworden – gemessen 190 statt 140 Pixel, und die gehen dem + Dokument ab. */} + {!barNarrow && ( +
+ {zoomGroup} +
+ )}
{/* Seiten */}