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
1 file src/popup/App.tsx+66 −0
src/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.

view file