aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-02 23:14:26 -0700
committerAdam Soltys <[email protected]>2026-08-02 23:14:26 -0700
commit64823b30c167d8dd0ec8747bb4b7aa5d975e98a3 (patch)
tree1d4a8d4d388c8fc0c36f2e68368d8f9df7e4e0d3 /src
parentbae797fb402e0d9b86c43c5c1dd356cb056e2838 (diff)
downloadcoinosv3-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
Diffstat (limited to 'src')
-rw-r--r--src/app.js48
-rw-r--r--src/style.css8
2 files changed, 54 insertions, 2 deletions
diff --git a/src/app.js b/src/app.js
index ce2ed3d..073b4c4 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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; }