Swipes take a longer drag and slide the reader across (#49)

At a flat 60px, a thumb scrolling slightly on the diagonal moved on to the next item by
accident. A swipe now has to cover a quarter of the reader's width, and never less than 100px,
and the reader follows the finger while it is down, so it is plain before letting go whether it
will move on. It slides off on a swipe and the next item slides in from the other side; a short
one springs back. The CHANGELOG also gets back the [0.8.3] link a stray edit had broken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 13:04:30 +00:00
parent 2930be37ad
commit 868dd673f3
4 changed files with 43 additions and 7 deletions

View File

@@ -674,7 +674,7 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
/* Three panes, as the original had: feeds beside, items above, the item below. */
#split{
display:grid;flex:1;min-height:0;
display:grid;flex:1;min-height:0;overflow:hidden; /* the reader sliding on a swipe */
grid-template-columns:minmax(0,1fr) 270px;
grid-template-rows:minmax(90px,var(--listh,60%)) 7px 1fr;
grid-template-areas:"list files" "grab grab" "detail detail";

View File

@@ -5,7 +5,9 @@
// the document because the panes are rebuilt every time a feed renders.
const PULL = 70; // px down, from the list's top, that counts as a pull
const SWIPE = 60; // px across that counts as a swipe
// A quarter of the reader's width across, and never less than 100px: at a flat 60px a thumb
// scrolling slightly on the diagonal jumped to the next item by accident (issue #49).
const swipeMin = () => Math.max(100, ($('#detail')?.clientWidth || 0) / 4);
let touch: {x: number, y: number, t: number, pane: 'list' | 'detail', dx: number, dy: number} | null = null;
@@ -60,11 +62,34 @@ document.addEventListener('touchmove', ev => {
pullShow(touch.dy);
if(ev.cancelable) ev.preventDefault(); // or the list rubber-bands under the finger as well
}else if(Math.abs(touch.dy) > 10 && Math.abs(touch.dy) > Math.abs(touch.dx)){
touch = null; // scrolling the text, not swiping; the first few px
slide(0); touch = null; // scrolling the text, not swiping; the first few px
// decide nothing, being mostly jitter
}else if(Math.abs(touch.dx) > 10){
// The reader follows the finger, so it is plain before letting go whether this will move on;
// with nothing that way it only gives a little.
const i = S.entries.findIndex(x => x.guid === S.sel);
const end = touch.dx < 0 && i >= S.entries.length - 1;
slide(end ? touch.dx / 4 : touch.dx, false);
}
}, {passive: false});
/// Moves the reader across by dx, sliding there unless told not to (when following a finger).
function slide(dx: number, animate = true){
const d = $('#detail'); if(!d) return;
d.style.transition = animate ? 'transform .16s ease-out, opacity .16s ease-out' : 'none';
d.style.transform = dx ? `translateX(${dx}px)` : '';
d.style.opacity = dx ? String(Math.max(.4, 1 - Math.abs(dx) / (d.clientWidth || 1))) : '';
}
/// The reader slides off the way the finger went, the next item comes in from the other side.
/// Timers, not transitionend, because with reduced motion the stylesheet turns transitions off
/// and transitionend never comes.
function slideOn(dir: number, go: () => void){
const w = $('#detail')?.clientWidth || 0;
slide(dir * w);
setTimeout(() => { go(); slide(-dir * w, false); requestAnimationFrame(() => requestAnimationFrame(() => slide(0))); }, 160);
}
document.addEventListener('touchend', () => {
const g = touch; touch = null;
if(!g) return;
@@ -74,9 +99,11 @@ document.addEventListener('touchend', () => {
return;
}
// Across, mostly sideways, and not a slow drag while selecting text.
if(Math.abs(g.dx) < SWIPE || Math.abs(g.dx) < 2 * Math.abs(g.dy) || Date.now() - g.t > 800) return;
const i = S.entries.findIndex(x => x.guid === S.sel);
if(g.dx < 0){ if(i < S.entries.length - 1) stepEntry(1); return; }
if(i > 0) stepEntry(-1); else showDetail(null);
if(Math.abs(g.dx) < swipeMin() || Math.abs(g.dx) < 2 * Math.abs(g.dy) || Date.now() - g.t > 800
|| (g.dx < 0 && i >= S.entries.length - 1)) return slide(0);
if(g.dx < 0) return slideOn(-1, () => stepEntry(1));
if(i > 0) return slideOn(1, () => stepEntry(-1));
slide(0); showDetail(null);
});
document.addEventListener('touchcancel', () => { if(touch?.pane === 'list') pullShow(0); touch = null; });
document.addEventListener('touchcancel', () => { if(touch?.pane === 'list') pullShow(0); else slide(0); touch = null; });