aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-23 23:17:19 -0700
committerAdam Soltys <[email protected]>2026-06-23 23:17:19 -0700
commit06e48290ca5e6da7866715cc26c4e3fdf3e03b91 (patch)
tree998ba58087dd40d839fc919635b3b2e328cfc24a
parentd5e55c9c577feb6f8000bad3c37045a2cd7c9b4c (diff)
downloadcoinosv3-06e48290ca5e6da7866715cc26c4e3fdf3e03b91.tar.gz
coinosv3-06e48290ca5e6da7866715cc26c4e3fdf3e03b91.zip
Show full recipient address below the input for verification
The address input scrolls/truncates on mobile, hiding the ends. Render the full address as wrapping text beneath it with the first/last 6 chars emphasized, so paste/type/scan can be verified without scrolling. Updated imperatively on input so the field keeps focus. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
-rw-r--r--src/app.js31
-rw-r--r--src/style.css2
2 files changed, 31 insertions, 2 deletions
diff --git a/src/app.js b/src/app.js
index 286372e..54592a7 100644
--- a/src/app.js
+++ b/src/app.js
@@ -964,18 +964,42 @@ async function broadcastScanned() {
render();
}
+// Full address as wrapping nodes, first/last 6 chars emphasized — readable
+// without horizontally scrolling the input. Returns DOM nodes for in-place
+// updates (the address input doesn't re-render on every keystroke).
+function addrVerifyNodes(a) {
+ const n = 6;
+ if (!a) return [];
+ if (a.length <= n * 2) return [document.createTextNode(a)];
+ return [
+ h('span', { class: 'hl' }, a.slice(0, n)),
+ document.createTextNode(a.slice(n, -n)),
+ h('span', { class: 'hl' }, a.slice(-n)),
+ ];
+}
+
// One recipient: address + amount. Max is only offered for a single recipient.
function recipientRow(s, r, i) {
const single = s.recipients.length === 1;
const maxOn = single && s.max;
- return h(
+
+ // Updated imperatively on input (and on render) so paste, typing, and scan
+ // all reflect immediately without disrupting the input's focus/cursor.
+ const check = h('div', { class: 'addr-check' });
+ const syncCheck = () => {
+ const a = r.address.trim();
+ check.replaceChildren(...addrVerifyNodes(a));
+ check.style.display = a ? '' : 'none';
+ };
+
+ const row = h(
'div',
{ class: 'col gap6' },
h('div', { class: 'input-group' },
h('input', {
type: 'text', class: 'mono-input grow', placeholder: 'bc1q…',
autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address,
- onInput: (e) => (r.address = e.target.value),
+ onInput: (e) => { r.address = e.target.value; syncCheck(); },
}),
i === 0 && canScan() && h('button', {
type: 'button', class: 'btn-sm', title: t('scanQr'), onClick: scanIntoSend,
@@ -983,6 +1007,7 @@ function recipientRow(s, r, i) {
}),
!single && h('button', { type: 'button', class: 'btn-sm', title: t('remove'), onClick: () => { s.recipients.splice(i, 1); render(); } }, '✕')
),
+ check,
h('div', { class: 'input-group' },
h('input', {
type: 'number', step: unit === 'sats' ? '1' : '0.00000001', min: '0',
@@ -1008,6 +1033,8 @@ function recipientRow(s, r, i) {
single && h('button', { type: 'button', class: s.max ? 'btn-primary' : '', onClick: () => { s.max = !s.max; render(); } }, t('max'))
)
);
+ syncCheck();
+ return row;
}
function sendForm() {
diff --git a/src/style.css b/src/style.css
index 69c3169..61a80af 100644
--- a/src/style.css
+++ b/src/style.css
@@ -150,6 +150,8 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si
border-radius: 11px; padding: 12px 14px; word-break: break-all; text-align: center;
}
.path { font-family: var(--mono); font-size: 12px; color: var(--faint); }
+.addr-check { font-family: var(--mono); font-size: 12px; line-height: 1.5; color: var(--muted); word-break: break-all; padding: 0 2px; }
+.addr-check .hl { color: var(--ink); font-weight: 700; }
/* ---- mnemonic grid ---- */
.words { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }