f23a453c4bda
App.tsx: bottom-sheet swipe gesture on native — touchstart in bottom 40% + swipe up >60px reveals vault list (setView 'list'); touchstart in top 35% of list + swipe down dismisses (setView 'entry'); horizontal moves >30px abort so entry-swipe-nav still works
- time
- 2026-05-07 03:06 UTC · 5 months ago
- machine
- koh2
- parent
- b39a7b800795
↓ peach-browser-f23a453c.tar.gzatlas at this save
files changed1 file
src/popup/App.tsx+66 −0src/popup/App.tsx+66 −0
@@ -483,6 +483,72 @@483483 window.addEventListener("peach:back", handlePeachBack);484484 onCleanup(() => window.removeEventListener("peach:back", handlePeachBack));485485 486+ // Mobile bottom-sheet gesture: swipe up from the bottom 40% of the screen487+ // reveals the vault list; swipe down from the top of the list dismisses it.488+ // Tracked at document level so the search bar input doesn't have to be the489+ // touch target — anywhere in the lower band counts.490+ if (isNativePlatform()) {491+ let touchStartY = 0;492+ let touchStartX = 0;493+ let dragging = false;494+ let startedInBand = false;495+ const SWIPE_THRESHOLD = 60;496+ const HORIZONTAL_TOLERANCE = 30;497+498+ const onTouchStart = (e: TouchEvent) => {499+ const t = e.touches[0];500+ if (!t) return;501+ touchStartY = t.clientY;502+ touchStartX = t.clientX;503+ dragging = true;504+ const v = view();505+ if (v === "entry") {506+ // Only count starts inside the bottom 40% band (top 60% reserved507+ // for entry interactions, taps, swipes between entries).508+ startedInBand = t.clientY > window.innerHeight * 0.6;509+ } else if (v === "list") {510+ // From list: any downward drag near the top of the list dismisses it.511+ startedInBand = t.clientY < window.innerHeight * 0.35;512+ } else {513+ startedInBand = false;514+ }515+ };516+517+ const onTouchMove = (e: TouchEvent) => {518+ if (!dragging || !startedInBand) return;519+ const t = e.touches[0];520+ if (!t) return;521+ const dy = t.clientY - touchStartY;522+ const dx = Math.abs(t.clientX - touchStartX);523+ if (dx > HORIZONTAL_TOLERANCE) {524+ // Treat as a horizontal swipe — give it back to the entry view.525+ dragging = false;526+ return;527+ }528+ const v = view();529+ if (v === "entry" && dy < -SWIPE_THRESHOLD) {530+ dragging = false;531+ setView("list");532+ } else if (v === "list" && dy > SWIPE_THRESHOLD) {533+ dragging = false;534+ setView("entry");535+ }536+ };537+538+ const onTouchEnd = () => { dragging = false; startedInBand = false; };539+540+ document.addEventListener("touchstart", onTouchStart, { passive: true });541+ document.addEventListener("touchmove", onTouchMove, { passive: true });542+ document.addEventListener("touchend", onTouchEnd, { passive: true });543+ document.addEventListener("touchcancel", onTouchEnd, { passive: true });544+ onCleanup(() => {545+ document.removeEventListener("touchstart", onTouchStart);546+ document.removeEventListener("touchmove", onTouchMove);547+ document.removeEventListener("touchend", onTouchEnd);548+ document.removeEventListener("touchcancel", onTouchEnd);549+ });550+ }551+486552 // Action handlers — keyed by ShortcutAction. The dispatcher below tests487553 // each binding against the event and runs the matching handler. New488554 // shortcuts only need an entry here; the binding is data, not code.