From 3db829522b28eef55ab1fda3c1dac385cb3265d2 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 3 Aug 2026 08:52:59 -0700 Subject: Move money: Max on both directions, inline in the amount field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Into Spending gains a Max (fee-aware — it asks the tx builder what a sweep would actually deliver, so the mining fee can't make the board fail), and both directions now carry Max as a small affordance inside the input's right edge instead of a button eating a third of the row. It only appears when there's a balance to max. Balance face height trimmed to match its shorter content. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi --- src/features/ark.js | 41 ++++++++++++++++++++++++++++++++++------- src/style.css | 11 ++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/features/ark.js b/src/features/ark.js index 5209de8..23e158a 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1351,16 +1351,43 @@ export function arkFeature(ctx) { // Moving money between the two balances, in plain terms. This is the only // place the two rails meet, and it never names them: Saving is on-chain, // Spending is instant, and the wallet handles the rest. + // Amount field with an inline Max — a text affordance inside the input's + // right edge rather than a button eating a third of the row. + function amountField({ value, onInput, maxSat, placeholder, onMax }) { + return h('div', { class: 'amt-field' }, + h('input', { + type: 'number', inputmode: 'numeric', min: '0', placeholder, + value: value || '', + onInput, + }), + maxSat > 0 + ? h('button', { type: 'button', class: 'amt-max', onClick: onMax }, t('max')) + : null); + } + + // Boarding drains an on-chain balance, so Max must leave room for the + // mining fee: ask the tx builder what a sweep would actually deliver. + function maxBoardSat() { + try { + const feeRate = (wallet.feeRates && wallet.feeRates.halfHourFee) || 5; + const draft = wallet.buildTx({ recipients: [{ address: wallet.freshChange().address, amount: 0 }], feeRate, sendMax: true }); + return Math.max(0, (draft.outputs[0]?.amount || 0)); + } catch { + return 0; + } + } + function boardForm() { const minBoard = (ark && ark.info && ark.info.minBoardAmountSat) || 0; const canBoard = wallet.spendable >= minBoard; return h('div', { class: 'col', style: 'width:100%;gap:8px' }, h('div', { class: 'input-group' }, - h('input', { - type: 'number', inputmode: 'numeric', min: '0', + amountField({ + value: ui.arkBoardAmt, placeholder: t('arkBoardPlaceholder', { n: minBoard.toLocaleString() }), - value: ui.arkBoardAmt || '', onInput: (e) => { ui.arkBoardAmt = e.target.value; render(); }, + maxSat: canBoard ? wallet.spendable : 0, + onMax: () => { ui.arkBoardAmt = String(maxBoardSat()); render(); }, }), h('button', { class: 'btn-primary', disabled: !!ui.arkBusy || !canBoard, onClick: doArkBoard }, ui.arkBusy === 'board' ? h('span', { class: 'spinner sm' }) : t('arkBoardBtn'))), @@ -1402,13 +1429,13 @@ export function arkFeature(ctx) { const valid = sats > 0 && sats <= spendable; return h('div', { class: 'col', style: 'gap:8px' }, h('div', { class: 'input-group' }, - h('input', { - type: 'number', inputmode: 'numeric', min: '0', + amountField({ + value: ui.arkOffboardAmt, placeholder: t('arkOffboardAmtPlaceholder'), - value: ui.arkOffboardAmt || '', onInput: (e) => { ui.arkOffboardAmt = e.target.value; render(); }, + maxSat: spendable, + onMax: () => { ui.arkOffboardAmt = String(spendable); render(); }, }), - h('button', { class: 'btn-ghost', onClick: () => { ui.arkOffboardAmt = String(spendable); render(); } }, t('max')), h('button', { class: 'btn-primary', disabled: !!ui.arkBusy || !valid, onClick: () => doArkOffboard(sats), diff --git a/src/style.css b/src/style.css index 0131556..f151bf5 100644 --- a/src/style.css +++ b/src/style.css @@ -513,9 +513,18 @@ body::after { @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-face { min-height: 96px; } .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); } +/* amount input with an inline Max affordance */ +.amt-field { position: relative; display: flex; flex: 1; min-width: 0; } +.amt-field input { width: 100%; padding-right: 52px; } +.amt-max { + position: absolute; right: 6px; top: 50%; transform: translateY(-50%); + padding: 4px 8px; font-size: 12px; font-weight: 600; line-height: 1; + border: 0; border-radius: 7px; background: var(--accent-soft); color: var(--muted); cursor: pointer; +} +.amt-max:hover { background: var(--surface2); color: var(--ink); } -- cgit v1.2.3