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 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d9a4832f9
commit
edefce3572
2 files changed
+128
-29
No files matched your search
+20
@@ -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
|
||||
|
||||
@@ -596,22 +596,104 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) {
|
||||
* einem Zoomschritt am Anfang eines fremden Blattes.
|
||||
*/
|
||||
const keepPage = useRef<number | null>(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 = (
|
||||
<>
|
||||
<ToolButton label="Zoom out" onClick={() => 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. */}
|
||||
<button
|
||||
onClick={() => { keepPage.current = current; setScale('fit') }}
|
||||
disabled={scale === 'fit'}
|
||||
title="Fit width"
|
||||
style={{
|
||||
fontSize: 12, color: 'var(--color-text-muted)', minWidth: 52, flexShrink: 0,
|
||||
border: 'none', background: 'none',
|
||||
cursor: scale === 'fit' ? 'default' : 'pointer',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{Math.round(effScale * 100)}%
|
||||
</button>
|
||||
<ToolButton label="Zoom in" onClick={() => 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) {
|
||||
</span>
|
||||
<ToolButton label="Next page" disabled={!doc || current >= doc.numPages}
|
||||
onClick={() => goToPage(current + 1)} glyph="down" />
|
||||
|
||||
<span style={{ width: 1, height: 20, flexShrink: 0, background: 'var(--color-border)' }} />
|
||||
|
||||
<ToolButton label="Zoom out" onClick={() => 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. */}
|
||||
<button
|
||||
onClick={() => { keepPage.current = current; setScale('fit') }}
|
||||
disabled={scale === 'fit'}
|
||||
title="Fit width"
|
||||
style={{
|
||||
fontSize: 12, color: 'var(--color-text-muted)', minWidth: 52, flexShrink: 0,
|
||||
border: 'none', background: 'none',
|
||||
cursor: scale === 'fit' ? 'default' : 'pointer',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{Math.round(effScale * 100)}%
|
||||
</button>
|
||||
<ToolButton label="Zoom in" onClick={() => zoom(1)} glyph="plus" />
|
||||
{barNarrow && (
|
||||
<>
|
||||
<span style={{ width: 1, height: 20, flexShrink: 0, background: 'var(--color-border)' }} />
|
||||
{zoomGroup}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Suche im Dokument – die mittlere Spalte. Sie ist der Grund, aus dem
|
||||
@@ -786,10 +852,23 @@ export default function PdfViewer({ url, fileId, searchTerm }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 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 && <div />}
|
||||
{/* 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 && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
justifyContent: 'flex-end', minWidth: 0,
|
||||
}}>
|
||||
{zoomGroup}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Seiten */}
|
||||
|
||||
Reference in new issue
Block a user