fix(ui): overlay scrollbars, resizer hit-test, and Linux mini wheel (#255)

Add OverlayScrollArea with shared thumb metrics and drag handling; size the
thumb against the rail track height so panel insets cannot push it past the
visible rail. Route scroll uses a stable viewport id; Genres restores scroll
and infinite-scroll observation against that viewport (merged with upstream
virtualized genres list and lazy routes behind Suspense).

Suppress queue resizer activation when the pointer targets the main-route
overlay scrollbar; disable the resizer while dragging the thumb and use a
grabbing cursor on the body.

Apply WebKitGTK smooth wheel to the mini webview as well as main; the mini
window reapplies the persisted setting after auth store hydration.
This commit is contained in:
cucadmuh
2026-04-21 23:44:28 +03:00
committed by GitHub
parent 2318f9e07a
commit b61c168430
13 changed files with 538 additions and 130 deletions
+33
View File
@@ -0,0 +1,33 @@
export type OverlayScrollbarThumbMeta = {
thumbH: number;
thumbT: number;
visible: boolean;
};
/**
* @param trackHeight — pixel height of the overlay rail (inset top/bottom).
* When shorter than the viewport, thumb size/position must use this or the
* thumbs bottom extends past the visible rail at max scroll.
*/
export function computeOverlayScrollbarThumbMeta(
el: HTMLElement | null,
trackHeight?: number,
): OverlayScrollbarThumbMeta {
if (!el) return { thumbH: 0, thumbT: 0, visible: false };
const { scrollTop, scrollHeight, clientHeight } = el;
if (scrollHeight <= clientHeight + 1) {
return { thumbH: 0, thumbT: 0, visible: false };
}
const th =
trackHeight != null && trackHeight > 0 ? Math.min(trackHeight, clientHeight) : clientHeight;
const ratio = clientHeight / scrollHeight;
const rawH = Math.round(ratio * th);
const thumbH = Math.min(th, Math.max(24, rawH));
const range = Math.max(0, th - thumbH);
const scrollRange = scrollHeight - clientHeight;
const thumbT =
scrollRange > 0
? Math.min(range, Math.max(0, Math.round((scrollTop / scrollRange) * range)))
: 0;
return { thumbH, thumbT, visible: true };
}
+66
View File
@@ -0,0 +1,66 @@
import type { PointerEvent as ReactPointerEvent } from 'react';
import { computeOverlayScrollbarThumbMeta } from './overlayScrollbarMetrics';
/**
* Drag the overlay scrollbar thumb (native bar is hidden). Maps pointer delta
* to scrollTop using the same thumb/range geometry as the visual thumb.
*/
export function bindOverlayScrollbarThumbDrag(
e: ReactPointerEvent<HTMLElement>,
scrollEl: HTMLDivElement | null,
): void {
if (e.button !== 0 || !scrollEl) return;
e.preventDefault();
e.stopPropagation();
const thumb = e.currentTarget;
const rail = thumb.parentElement;
const trackH =
rail instanceof HTMLElement && rail.clientHeight > 0 ? rail.clientHeight : scrollEl.clientHeight;
const meta = computeOverlayScrollbarThumbMeta(scrollEl, trackH);
if (!meta.visible) return;
const { scrollHeight, clientHeight } = scrollEl;
const scrollRange = scrollHeight - clientHeight;
const range = trackH - meta.thumbH;
if (range <= 1) return;
const startScroll = scrollEl.scrollTop;
const startY = e.clientY;
const pointerId = e.pointerId;
thumb.classList.add('is-thumb-dragging');
document.body.classList.add('is-overlay-scrollbar-thumb-drag');
try {
thumb.setPointerCapture(pointerId);
} catch {
thumb.classList.remove('is-thumb-dragging');
document.body.classList.remove('is-overlay-scrollbar-thumb-drag');
return;
}
const onMove = (ev: PointerEvent) => {
if (ev.pointerId !== pointerId) return;
const dy = ev.clientY - startY;
const next = startScroll + (dy / range) * scrollRange;
scrollEl.scrollTop = Math.max(0, Math.min(scrollRange, next));
};
const onUp = (ev: PointerEvent) => {
if (ev.pointerId !== pointerId) return;
thumb.classList.remove('is-thumb-dragging');
document.body.classList.remove('is-overlay-scrollbar-thumb-drag');
try {
thumb.releasePointerCapture(pointerId);
} catch {
/* ignore */
}
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointercancel', onUp);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
window.addEventListener('pointercancel', onUp);
}