diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 23:14:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 23:14:26 -0700 |
| commit | 64823b30c167d8dd0ec8747bb4b7aa5d975e98a3 (patch) | |
| tree | 1d4a8d4d388c8fc0c36f2e68368d8f9df7e4e0d3 | |
| parent | bae797fb402e0d9b86c43c5c1dd356cb056e2838 (diff) | |
| download | coinosv3-64823b30c167d8dd0ec8747bb4b7aa5d975e98a3.tar.gz coinosv3-64823b30c167d8dd0ec8747bb4b7aa5d975e98a3.zip | |
Swipe between Receive / Send / History
Horizontal-dominant quick swipes move through the main tabs, sliding
the page in from the swipe's direction (the nav animation picks a
directional variant when a gesture caused it). Gated to the wallet's
own tab pages — never chat, profiles, takeovers, or offline-forced
settings — with a vertical-dominance test so scrolling never
navigates, a touchcancel reset, and overscroll-behavior-x: none so the
browser's edge-back gesture doesn't fight ours.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
| -rw-r--r-- | src/app.js | 48 | ||||
| -rw-r--r-- | src/style.css | 8 |
2 files changed, 54 insertions, 2 deletions
@@ -284,6 +284,9 @@ function uiChanged(key, val) { // its one-shot class, killing the motion. animWindow answers "how far into // the animation started by this change are we?" — callers re-apply the class // with a negative animation-delay so a rebuild RESUMES the animation. +let _navAt = -1e9; +let _navCls = 'anim-page'; +let _swipeDir = null; // set by the swipe handler just before render const _animAt = new Map(); function animWindow(key, val, durMs) { if (uiChanged(key, val)) _animAt.set(key, performance.now()); @@ -347,7 +350,12 @@ function render() { // ui field that decides which page is on screen. const navKey = [ui.screen, ui.tab, ui.chatOpen, ui.msgView, ui.msgPeer, ui.msgCommunity, ui.profilePk, ui.settingsPage, ui.addrScan, ui.arkExitPage, ui.txDetail, ui.giftMode, ui.claimStep].join('|'); - applyAnim(screen, 'anim-page', animWindow('nav', navKey, 340)); + if (uiChanged('nav', navKey)) { + _navAt = performance.now(); + _navCls = _swipeDir ? 'anim-slide-' + _swipeDir : 'anim-page'; + } + _swipeDir = null; + applyAnim(screen, _navCls, (performance.now() - _navAt) < 340 ? performance.now() - _navAt : -1); root.replaceChildren(screen, footer()); if (fpath) { const el = nodeAtPath(fpath); @@ -3171,6 +3179,44 @@ const FEATURES = buildFeatures(ctx); applyDir(); // Auto log-out: start the countdown only when the app loses focus / is hidden, // and cancel it the moment it's focused again. So it never logs out mid-use. +// Swipe left/right moves between the main tabs, the way thumbs expect. +// Horizontal-dominant, quick, and only on the wallet's own tab pages — never +// inside chat, profiles, takeovers, or the (offline-forced) settings tab. +(() => { + const ORDER = ['receive', 'send', 'history']; + let sx = 0, sy = 0, t0 = 0, live = false; + window.addEventListener('touchstart', (e) => { + live = e.touches.length === 1 && ui.screen === 'wallet' && !ui.chatOpen && !ui.profilePk + && !ui.arkExitPage && ORDER.includes(ui.tab); + if (!live) return; + sx = e.touches[0].clientX; + sy = e.touches[0].clientY; + t0 = Date.now(); + }, { passive: true }); + window.addEventListener('touchcancel', () => { live = false; }, { passive: true }); + window.addEventListener('touchend', (e) => { + if (!live) return; + live = false; + const t = e.changedTouches[0]; + const dx = t.clientX - sx, dy = t.clientY - sy; + if (Date.now() - t0 > 600 || Math.abs(dx) < 60 || Math.abs(dx) < 1.8 * Math.abs(dy)) return; + const next = ORDER[ORDER.indexOf(ui.tab) + (dx < 0 ? 1 : -1)]; + if (!next) return; + _swipeDir = dx < 0 ? 'left' : 'right'; + ui.tab = next; + // same resets as a tab-bar tap + ui.sendError = ''; + ui.revealShown = false; + ui.txDetail = null; + ui.arkMoveDetail = null; + ui.giftDetail = null; + ui.addrScan = false; + ui.bump = null; + ui.giftMode = false; + render(); + }, { passive: true }); +})(); + window.addEventListener('blur', onAppHidden); window.addEventListener('focus', onAppVisible); document.addEventListener('visibilitychange', () => { diff --git a/src/style.css b/src/style.css index f01df60..1325118 100644 --- a/src/style.css +++ b/src/style.css @@ -457,6 +457,8 @@ input[type=text], input[type=password], input[type=number], textarea, select { t body { background-image: radial-gradient(900px 420px at 50% -160px, var(--wash), transparent 70%); background-attachment: fixed; + /* our swipe-nav owns horizontal gestures; keep the browser's edge-back out */ + overscroll-behavior-x: none; } body::after { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; @@ -465,6 +467,10 @@ body::after { } @media (prefers-reduced-motion: reduce) { - .anim-page, .anim-menu, .anim-unfold, .amt-flash, .check-badge, .balance::before { animation: none !important; } + .anim-page, .anim-menu, .anim-unfold, .anim-slide-left, .anim-slide-right, .amt-flash, .check-badge, .balance::before { animation: none !important; } *, *::before, *::after { transition-duration: .01ms !important; } } +@keyframes slide-in-left { from { opacity: 0; transform: translateX(30px); } } +@keyframes slide-in-right { from { opacity: 0; transform: translateX(-30px); } } +.anim-slide-left { animation: slide-in-left .28s cubic-bezier(.16, .84, .3, 1) both; } +.anim-slide-right { animation: slide-in-right .28s cubic-bezier(.16, .84, .3, 1) both; } |
