From ff144b0b057e8ac76a628adfa02c556d5c6214c9 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 3 Aug 2026 08:48:18 -0700 Subject: Balance card: one account at a time, swipe or dots to flip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the segmented toggle and the always-visible second balance. The card now shows the account you're in at full size with a plain-word descriptor ("Instant · free to send" / "On-chain · mining fees apply"), two dots to flip, and a sideways slide when it changes. A swipe that starts on the card switches account instead of tab; swipes anywhere else still move between Receive/Send/History. Fixes a real mobile bug found while testing: a touch sequence still fires a compatibility click at the release point, so every swipe was ALSO pressing whatever sat under the finger (which is what kept bouncing the test wallet back to the unlock screen). One click is now swallowed after a recognized swipe. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi --- src/app.js | 76 +++++++++++++++++++++++++++++++++++++++-------------------- src/i18n.js | 2 ++ src/style.css | 8 ++++++- 3 files changed, 59 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index 64d6f0f..9830800 100644 --- a/src/app.js +++ b/src/app.js @@ -2254,7 +2254,10 @@ function accountSel() { } return ui.account; } -function setAccountSel(a) { +let _accDir = null; // which way the balance face should slide in +function setAccountSel(a, dir) { + if (ui.account === a) return; + _accDir = dir || (a === 'savings' ? 'left' : 'right'); ui.account = a; try { localStorage.setItem(ACCOUNT_KEY, a); } catch {} render(); @@ -2276,36 +2279,39 @@ function balanceCard() { const saving = wallet.spendable; const hasArk = !!featureHook('arkReady'); const sel = hasArk ? accountSel() : 'savings'; - return h( - 'div', - { class: 'card balance' }, - // The account toggle IS the headline label: you're always "in" Spending - // or Savings, and everything (receive, gifts) follows. No total — the two - // are different kinds of money, and adding them up says nothing useful. - hasArk - ? h('div', { class: 'seg balance-seg' }, - h('button', { class: sel === 'spending' ? 'active' : '', onClick: () => setAccountSel('spending') }, t('spendingLabel')), - h('button', { class: sel === 'savings' ? 'active' : '', onClick: () => setAccountSel('savings') }, t('savingLabel'))) - : h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.05em' }, t('balance')), + const isSpending = sel === 'spending'; + // One balance at a time, full size: the card IS the account you're in, and + // everything (receive, gifts) follows it. Swipe or tap the dots to flip. + const dtAcc = animWindow('acct', sel, 300); + const face = h('div', { class: 'balance-face' }, + h('div', { class: 'row between', style: 'align-items:center' }, + h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.06em' }, + hasArk ? (isSpending ? t('spendingLabel') : t('savingLabel')) : t('balance')), + hasArk + ? h('div', { class: 'balance-dots' }, + h('button', { class: 'bdot' + (isSpending ? ' on' : ''), title: t('spendingLabel'), onClick: () => setAccountSel('spending') }), + h('button', { class: 'bdot' + (!isSpending ? ' on' : ''), title: t('savingLabel'), onClick: () => setAccountSel('savings') })) + : null), h('div', { class: 'amt', style: firstLoad ? 'opacity:.3' : '' }, firstLoad ? h('span', { class: 'spinner sm', style: 'margin-right:8px' }) : null, - animatedAmount('bal:' + sel, sel === 'spending' ? spending : saving), ' ', unitTag('unit')), - hasArk || pending > 0 || featLines.length - ? h( - 'div', - { class: 'split' }, - hasArk - ? h('div', {}, - h('div', { class: 'k' }, sel === 'spending' ? t('savingLabel') : t('spendingLabel')), - h('div', { class: 'v' }, animatedAmount('bal2:' + sel, sel === 'spending' ? saving : spending), ' ', unitTag())) - : null, + animatedAmount('bal:' + sel, isSpending ? spending : saving), ' ', unitTag('unit')), + hasArk + ? h('div', { class: 'small muted balance-note' }, isSpending ? t('balanceNoteSpending') : t('balanceNoteSavings')) + : null, + pending > 0 || featLines.length + ? h('div', { class: 'split' }, pending > 0 ? h('div', {}, h('div', { class: 'k' }, t('pending')), h('div', { class: 'v pending' }, fmtAmount(pending), ' ', unitTag())) : null, ...featLines.map((l) => - h('div', {}, h('div', { class: 'k' }, l.label), h('div', { class: 'v' }, fmtAmount(l.sat), ' ', unitTag()))) - ) - : null, + h('div', {}, h('div', { class: 'k' }, l.label), h('div', { class: 'v' }, fmtAmount(l.sat), ' ', unitTag())))) + : null); + applyAnim(face, 'anim-tab-' + (_accDir || 'left'), dtAcc); + _accDir = null; + return h( + 'div', + { class: 'card balance' }, + face, // "Move money" and friends: the only door between the two balances. ...featureAll('balanceActions').map((a2) => h('button', { class: 'btn-sm', style: 'margin-top:10px', onClick: a2.onClick }, a2.label)), @@ -3387,11 +3393,18 @@ applyDir(); // inside chat, profiles, takeovers, or the (offline-forced) settings tab. (() => { const ORDER = ['receive', 'send', 'history']; - let sx = 0, sy = 0, t0 = 0, live = false; + let sx = 0, sy = 0, t0 = 0, live = false, onBalance = false, swallowClick = false; + window.addEventListener('click', (e) => { + if (!swallowClick) return; + swallowClick = false; + e.stopPropagation(); + e.preventDefault(); + }, true); 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; + onBalance = !!(e.target.closest && e.target.closest('.balance')); sx = e.touches[0].clientX; sy = e.touches[0].clientY; t0 = Date.now(); @@ -3403,6 +3416,17 @@ applyDir(); 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; + // A touch sequence still fires a compatibility click at the release + // point; after a swipe that would ALSO press whatever is under the + // finger. Swallow exactly one click. + swallowClick = true; + setTimeout(() => { swallowClick = false; }, 400); + // a swipe that began on the balance card flips the account instead + if (onBalance) { + const want = dx < 0 ? 'savings' : 'spending'; + if (featureHook('arkReady')) setAccountSel(want, dx < 0 ? 'left' : 'right'); + return; + } const next = ORDER[ORDER.indexOf(ui.tab) + (dx < 0 ? 1 : -1)]; if (!next) return; _swipeDir = dx < 0 ? 'left' : 'right'; diff --git a/src/i18n.js b/src/i18n.js index 9f43888..37b59af 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -140,6 +140,8 @@ const STR = { moveToSavingBtn: 'Move to Savings', spendingLabel: 'Spending', savingLabel: 'Savings', + balanceNoteSpending: 'Instant · free to send', + balanceNoteSavings: 'On-chain · mining fees apply', movingLabel: 'Moving…', balance: 'Balance', pending: 'Pending', diff --git a/src/style.css b/src/style.css index 3a5ab97..0131556 100644 --- a/src/style.css +++ b/src/style.css @@ -387,7 +387,6 @@ hr.divider { margin: 16px 0; } .balance-move { gap: 10px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); text-align: left; } .balance-move .seg { display: flex; width: 100%; } .balance-move .seg button { flex: 1; } -.balance-seg { display: inline-flex; margin-bottom: 6px; } .send-suggest { max-height: 260px; overflow-y: auto; } .clickable { cursor: pointer; } .profile-avatar { width: 64px; height: 64px; font-size: 18px; } @@ -513,3 +512,10 @@ body::after { @keyframes mark-float { 50% { transform: translateY(-6px); } } @media (prefers-reduced-motion: reduce) { .onb-mark { animation: none; } } .onb-addr { color: var(--muted); font-family: var(--mono); font-size: 13px; } +/* balance card: one account at a time, dots to flip */ +.balance-face { min-height: 118px; } +.balance-note { margin-top: 2px; } +.balance-dots { display: flex; gap: 7px; align-items: center; } +.bdot { width: 8px; height: 8px; padding: 0; border-radius: 50%; border: 0; background: var(--line-strong); cursor: pointer; transition: background .18s, transform .18s; } +.bdot:hover { transform: scale(1.25); background: var(--muted); } +.bdot.on { background: var(--accent); transform: scale(1.15); } -- cgit v1.2.3