aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-02 20:14:16 -0700
committerAdam Soltys <[email protected]>2026-09-02 20:14:16 -0700
commit923308578999c959aac2230875d70e15fada1d84 (patch)
treedd707b28087997d3fb8274c1f8d4cdf7a1a1c41c /src
parent1053b92accee1d52050067d75f36c74a5fa4ca87 (diff)
downloadcoinosv3-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.js55
-rw-r--r--src/features/ark.js25
-rw-r--r--src/i18n.js7
-rw-r--r--src/style.css8
4 files changed, 65 insertions, 30 deletions
diff --git a/src/app.js b/src/app.js
index f752ebc..4e69eb7 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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; }