diff options
| author | Adam Soltys <[email protected]> | 2026-09-02 20:14:16 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-02 20:14:16 -0700 |
| commit | 923308578999c959aac2230875d70e15fada1d84 (patch) | |
| tree | dd707b28087997d3fb8274c1f8d4cdf7a1a1c41c /src | |
| parent | 1053b92accee1d52050067d75f36c74a5fa4ca87 (diff) | |
| download | coinosv3-923308578999c959aac2230875d70e15fada1d84.tar.gz coinosv3-923308578999c959aac2230875d70e15fada1d84.zip | |
The drag holds its tongue until you let go — no account switch while the button or finger is still down (release is the choice, shared settle logic for mouse, touch and glide) — and the coins table's dotted meta line becomes little labeled pills: an hourglass countdown, depth, and exit cost each in their own chip, the expired one wearing the warning color
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 55 | ||||
| -rw-r--r-- | src/features/ark.js | 25 | ||||
| -rw-r--r-- | src/i18n.js | 7 | ||||
| -rw-r--r-- | src/style.css | 8 |
4 files changed, 65 insertions, 30 deletions
@@ -3385,8 +3385,38 @@ function balanceCard() { el._inited = true; }, 0); _accDir = null; // the drag is the animation here + // The settled-card check, shared by the scroll debounce and drag release. + // While a finger or mouse button is still DOWN nothing switches — hovering + // a card mid-drag is browsing, not choosing; release is the choice. + const settle = (el) => { + if (el._dragging) return; + const mid = el.scrollLeft + el.clientWidth / 2; + let best = 0, bestD = Infinity; + [...el.children].forEach((k, i) => { + const d = Math.abs(k.offsetLeft + k.offsetWidth / 2 - mid); + if (d < bestD) { bestD = d; best = i; } + }); + const view = ORDER2[best]; + if (view && view !== accountSel()) { + ui.account = view; + try { localStorage.setItem(ACCOUNT_KEY, view); } catch {} + // selecting an account lands on its history (home), any open + // payment detail belonged to the other account + ui.txDetail = null; ui.arkMoveDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; + ui.tab = 'history'; + render(); + } + }; return h('div', { class: 'bal-carousel', + onTouchstart: (e) => { const el = e.currentTarget; el._dragging = true; clearTimeout(el._settle); }, + onTouchend: (e) => { + const el = e.currentTarget; + el._dragging = false; + // momentum/snap scroll events reschedule this; a still release needs it + clearTimeout(el._settle); + el._settle = setTimeout(() => settle(el), 150); + }, // Desktop: scroll-snap has no mouse drag, so emulate one — grab the // strip and pull. Touch keeps the native pan (pointerType check); // buttons keep their clicks untouched. Mandatory snap fights raw @@ -3411,6 +3441,7 @@ function balanceCard() { const dx = ev.clientX - x0; if (!moved && Math.abs(dx) > 4) { moved = true; + el._dragging = true; el.classList.add('grabbing'); el.style.scrollSnapType = 'none'; } @@ -3425,6 +3456,7 @@ function balanceCard() { window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up); el.classList.remove('grabbing'); + el._dragging = false; if (!moved) return; el._dragged = Date.now(); // the release's click must not double as a card tap // A quick FLICK throws the card: velocity over the last ~100ms of @@ -3443,6 +3475,10 @@ function balanceCard() { const k = kids[best]; if (k) el.scrollTo({ left: k.offsetLeft + k.offsetWidth / 2 - el.clientWidth / 2, behavior: 'smooth' }); setTimeout(() => { el.style.scrollSnapType = ''; }, 400); + // the glide's own scroll events reschedule this; a release already + // at rest on the target needs the switch to fire regardless + clearTimeout(el._settle); + el._settle = setTimeout(() => settle(el), 150); }; window.addEventListener('pointermove', mv); window.addEventListener('pointerup', up); @@ -3451,24 +3487,7 @@ function balanceCard() { const el = e.target; el._lastScroll = Date.now(); clearTimeout(el._settle); - el._settle = setTimeout(() => { - const mid = el.scrollLeft + el.clientWidth / 2; - let best = 0, bestD = Infinity; - [...el.children].forEach((k, i) => { - const d = Math.abs(k.offsetLeft + k.offsetWidth / 2 - mid); - if (d < bestD) { bestD = d; best = i; } - }); - const view = ORDER2[best]; - if (view && view !== accountSel()) { - ui.account = view; - try { localStorage.setItem(ACCOUNT_KEY, view); } catch {} - // selecting an account lands on its history (home), any open - // payment detail belonged to the other account - ui.txDetail = null; ui.arkMoveDetail = null; ui.arkExitDetail = null; ui.giftDetail = null; - ui.tab = 'history'; - render(); - } - }, 120); + el._settle = setTimeout(() => settle(el), 120); }, }, ORDER2.map((v) => h('div', { class: 'card balance bal-slide', diff --git a/src/features/ark.js b/src/features/ark.js index 1145983..c8e979b 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1690,16 +1690,15 @@ export function arkFeature(ctx) { const feeRate = Math.max(1, (wallet.feeRates && wallet.feeRates.halfHourFee) || 2); const afterFee = Math.ceil(530 * feeRate); const depthOf = (v) => { try { return mgr._decoded(v).genesis.length; } catch { return null; } }; - // relative and terse — "expires in 14d" / "in 13h" — a date says less - // than a countdown, and a coin past its height says so plainly instead - // of the shifty "expires 0h" (the server still honors it; the wallet's + // relative and terse — "14d" / "13h" beside the hourglass — a date says + // less than a countdown, and a coin past its height says so plainly + // instead of the shifty "0h" (the server still honors it; the wallet's // next renewal round sweeps it up). const expiresOf = (v) => { if (!v.expiryHeight || !tip) return '—'; const blocks = v.expiryHeight - tip; if (blocks <= 0) return t('arkCoinsExpired'); - const c = blocks >= 144 ? Math.round(blocks / 144) + 'd' : Math.max(1, Math.round(blocks / 6)) + 'h'; - return t('arkCoinsExpIn', { when: c }); + return blocks >= 144 ? Math.round(blocks / 144) + 'd' : Math.max(1, Math.round(blocks / 6)) + 'h'; }; // what pulling THIS coin on-chain alone would cost: every hop's fee // child plus the claim, at today's rate (the whole-balance figure below @@ -1790,11 +1789,17 @@ export function arkFeature(ctx) { h('span', { class: 'small faint' }, ' ' + unitLabel())), h('span', { class: 'small ' + (f > 0 ? 'muted' : 'faint') }, t('arkCoinsRowRenew', { fee: f > 0 ? fmtAmount(f) : t('arkDepthFree') }))), - h('div', { class: 'small faint' }, t('arkCoinsRowMeta', { - exp: expiresOf(v), - d: d == null ? '—' : String(d), - exit: xf == null ? '—' : fmtAmount(xf), - })))); + // little labeled chips instead of a dot-separated line: each + // fact wears its own pill, wrapping freely on narrow screens + h('div', { class: 'coin-chips' }, + h('span', { + class: 'coin-chip' + (v.expiryHeight && tip && v.expiryHeight - tip <= 0 ? ' warn' : ''), + title: t('arkCoinsChipExpTitle'), + }, '⏳ ' + expiresOf(v)), + h('span', { class: 'coin-chip', title: t('arkCoinsChipDepthTitle') }, + t('arkCoinsChipDepth', { d: d == null ? '—' : String(d) })), + h('span', { class: 'coin-chip', title: t('arkCoinsChipExitTitle') }, + t('arkCoinsChipExit', { fee: xf == null ? '—' : fmtAmount(xf) }))))); })), h('p', { class: 'small faint', style: 'margin:4px 0 0' }, t('arkCoinsExpiryNote'))), h('div', { class: 'card col', style: 'gap:8px' }, diff --git a/src/i18n.js b/src/i18n.js index f2803ad..d6d5740 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -217,9 +217,12 @@ const STR = { arkCoinsTitle: 'Spending coins', arkCoinsIntro: 'Your Spending balance lives in {n} coins totalling {total}. Each can be pulled on-chain without the server’s help — at a cost that grows with the coin’s history.', arkCoinsIntroOne: 'Your Spending balance lives in a single coin of {total}. It can be pulled on-chain without the server’s help — at a cost that grows with the coin’s history.', - arkCoinsRowMeta: '{exp} · depth {d} · exit fee {exit}', arkCoinsRowRenew: 'renew {fee}', - arkCoinsExpIn: 'expires in {when}', + arkCoinsChipDepth: 'depth {d}', + arkCoinsChipExit: 'exit {fee}', + arkCoinsChipExpTitle: 'Time until this coin needs renewal', + arkCoinsChipDepthTitle: 'How many off-chain hops this coin carries', + arkCoinsChipExitTitle: 'What pulling this coin on-chain by itself would cost', arkCoinsExpired: 'expired', arkCoinsExpiryNote: 'Past its expiry a coin can be reclaimed by the server — the wallet renews on its own well before that.', arkCoinsExitTitle: 'Uncooperative exit', diff --git a/src/style.css b/src/style.css index b88fede..e5c3156 100644 --- a/src/style.css +++ b/src/style.css @@ -259,6 +259,14 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si .coin { display: flex; align-items: center; gap: 11px; padding: 11px 2px; border-top: 1px solid var(--line); } .coin:first-child { border-top: 0; } .coin input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent); } +/* per-coin fact pills: each datum wears its own little labeled chip */ +.coin-chips { display: flex; gap: 6px; flex-wrap: wrap; } +.coin-chip { + display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; + border: 1px solid var(--line); border-radius: 999px; + font-size: 11.5px; color: var(--muted); white-space: nowrap; +} +.coin-chip.warn { color: var(--accent-ink); border-color: var(--accent); } /* ---- review / summary ---- */ .summary { background: var(--surface2); border: 1px solid var(--line); border-radius: 12px; padding: 14px; } |
