Swipe to open and close the menu on phones
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1dcf2707f3
commit
138a4e5d77
2 changed files with 72 additions and 2 deletions
|
|
@ -667,6 +667,76 @@ document.addEventListener("submit", async (e) => {
|
|||
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
|
||||
|
||||
// --- swipe the menu open and shut (phones) --------------------------------
|
||||
// A mostly-sideways swipe drags the drawer with the finger; letting go snaps
|
||||
// it open or shut by distance or speed. Vertical scrolling is left alone.
|
||||
// Move and end listeners go on the touched element itself: touch events keep
|
||||
// going to it even if a re-render detaches it mid-swipe, and then they no
|
||||
// longer bubble up to the document.
|
||||
|
||||
(function swipeMenu() {
|
||||
const phone = matchMedia("(max-width: 900px)");
|
||||
let drag = null;
|
||||
|
||||
const reset = () => {
|
||||
$("#side").style.transform = $("#side").style.transition = "";
|
||||
$("#scrim").style.opacity = "";
|
||||
$("#app").classList.remove("dragging");
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", (e) => {
|
||||
if (drag) release();
|
||||
if (!phone.matches || e.touches.length !== 1 || !$("#modal").hidden || e.target.closest("input, textarea, select")) return;
|
||||
const t = e.touches[0];
|
||||
drag = { x: t.clientX, y: t.clientY, at: e.timeStamp, open: $("#app").classList.contains("open"), axis: null, dx: 0, target: e.target };
|
||||
drag.target.addEventListener("touchmove", move, { passive: false });
|
||||
drag.target.addEventListener("touchend", finish);
|
||||
drag.target.addEventListener("touchcancel", finish);
|
||||
}, { passive: true });
|
||||
|
||||
function release() {
|
||||
drag.target.removeEventListener("touchmove", move);
|
||||
drag.target.removeEventListener("touchend", finish);
|
||||
drag.target.removeEventListener("touchcancel", finish);
|
||||
if (drag.axis === "x") reset();
|
||||
drag = null;
|
||||
}
|
||||
|
||||
function move(e) {
|
||||
if (!drag) return;
|
||||
const t = e.touches[0];
|
||||
const dx = t.clientX - drag.x, dy = t.clientY - drag.y;
|
||||
if (!drag.axis) {
|
||||
if (Math.hypot(dx, dy) < 10) return;
|
||||
const sideways = Math.abs(dx) > Math.abs(dy) * 1.3 && (drag.open ? dx < 0 : dx > 0);
|
||||
drag.axis = sideways ? "x" : "y";
|
||||
if (!sideways) return;
|
||||
drag.width = $("#side").offsetWidth;
|
||||
$("#side").style.transition = "none";
|
||||
$("#app").classList.add("dragging");
|
||||
}
|
||||
if (drag.axis !== "x") return;
|
||||
e.preventDefault();
|
||||
drag.dx = dx;
|
||||
// 0 is fully open, -width fully shut.
|
||||
const offset = Math.max(-drag.width, Math.min(0, drag.open ? dx : dx - drag.width));
|
||||
$("#side").style.transform = `translateX(${offset}px)`;
|
||||
$("#scrim").style.opacity = String(1 + offset / drag.width);
|
||||
}
|
||||
|
||||
function finish(e) {
|
||||
if (!drag) return;
|
||||
const swiped = drag.axis === "x";
|
||||
const speed = drag.dx / Math.max(1, e.timeStamp - drag.at); // px per ms
|
||||
const open = drag.open
|
||||
? !(drag.dx < -drag.width * 0.35 || speed < -0.4)
|
||||
: drag.dx > drag.width * 0.35 || speed > 0.4;
|
||||
// Hand back to the stylesheet; its transition carries on from here.
|
||||
release();
|
||||
if (swiped) setMenu(open);
|
||||
}
|
||||
})();
|
||||
|
||||
(async function boot() {
|
||||
try {
|
||||
C = await api("/api/content");
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
html, body { margin: 0; height: 100%; overscroll-behavior-x: none; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
|
|
@ -305,7 +305,7 @@ table.parts td:last-child { text-align: right; font-variant-numeric: tabular-num
|
|||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#app.open #side { transform: none; box-shadow: 0 0 40px rgba(0, 0, 0, .5); }
|
||||
#app.open #scrim { display: block; position: fixed; inset: 0; z-index: 15; background: rgba(0, 0, 0, .5); }
|
||||
#app.open #scrim, #app.dragging #scrim { display: block; position: fixed; inset: 0; z-index: 15; background: rgba(0, 0, 0, .5); }
|
||||
html.menu-open, html.menu-open body { overflow: hidden; }
|
||||
.menu-btn { display: block; }
|
||||
#top { padding: 8px max(16px, env(safe-area-inset-right)) 8px max(16px, env(safe-area-inset-left)); flex-wrap: wrap; gap: 8px; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue