diff options
| author | Adam Soltys <[email protected]> | 2026-08-04 00:50:56 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-04 00:50:56 -0700 |
| commit | c57c8dc347a024b51734f9bc5c8a348181b08754 (patch) | |
| tree | de3336a0cbf93343e8888826794c1e8a54d60cae | |
| parent | 40fa5fccd8d5e02d38a7b7080390a4abd50a446e (diff) | |
| download | coinosv3-c57c8dc347a024b51734f9bc5c8a348181b08754.tar.gz coinosv3-c57c8dc347a024b51734f9bc5c8a348181b08754.zip | |
Park the send field above the phone keyboard; wizard whenever walletless
The recipient candidate list was hiding under the on-screen keyboard:
scroll the field to the top on focus and when fresh candidates land,
give the list scroll room to get there, and let the keyboard resize the
viewport (interactive-widget) so the scroll has somewhere to go.
The wizard now shows whenever the device has no wallet — the one-time
onboarded flag is gone; deleting every wallet brings the wizard back.
Co-Authored-By: Claude Fable 5 <[email protected]>
| -rw-r--r-- | build.js | 2 | ||||
| -rw-r--r-- | src/app.js | 26 | ||||
| -rw-r--r-- | src/style.css | 7 |
3 files changed, 28 insertions, 7 deletions
@@ -245,7 +245,7 @@ export async function buildHtml({ minify = true, pwa = minify, features = proces <html lang="en"> <head> <meta charset="utf-8"> -<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> +<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, interactive-widget=resizes-content"> <meta name="color-scheme" content="light dark"> <title>Coinos</title> <link rel="icon" href="${FAVICON}"> @@ -2124,18 +2124,17 @@ function vaultScreen() { // ================================================================ onboarding // First visit on a fresh device: a full-screen wizard — welcome, nostr // yes/no (sign in or silently mint a wallet), username, avatar, confetti — -// then a three-stop coach-mark tour of the home screen. One time only. -const ONBOARDED_KEY = 'btc-wallet-onboarded'; +// then a three-stop coach-mark tour of the home screen. Shown whenever the +// device has no wallet — deleting every wallet brings it back. // The wizard mints the wallet on its first step, so "an account exists" stops // meaning "wizard done". The current step is persisted so a refresh (or the // legacy-migration round trip) resumes exactly where it left off instead of // dropping into the half-set-up wallet. const ONB_STEP_KEY = 'btc-wallet-onb-step'; function onbInProgress() { - try { return !localStorage.getItem(ONBOARDED_KEY) && !!localStorage.getItem(ONB_STEP_KEY); } catch { return false; } + try { return !!localStorage.getItem(ONB_STEP_KEY); } catch { return false; } } function shouldOnboard() { - try { if (localStorage.getItem(ONBOARDED_KEY)) return false; } catch {} return !ui.onbSkip && accounts.length === 0 && !hasVault(); } @@ -2165,6 +2164,8 @@ function onboardScreen() { if (!ui.onb) { let saved = null; try { saved = localStorage.getItem(ONB_STEP_KEY); } catch {} + // a saved step past wallet creation only makes sense if the wallet is here + if (saved && saved !== 'welcome' && saved !== 'signin' && !activeAccount()) saved = 'welcome'; ui.onb = { step: saved || 'welcome' }; if (saved === 'signin') ui.nostrLoginOpen = true; // reopen the login card } @@ -2311,7 +2312,7 @@ function onboardScreen() { h('div', { class: 'check-badge onb-check' }, '✓'), h('p', { class: 'muted', style: 'margin:0;text-align:center' }, t('onbDoneBody')), h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: () => { - try { localStorage.setItem(ONBOARDED_KEY, '1'); localStorage.removeItem(ONB_STEP_KEY); } catch {} + try { localStorage.removeItem(ONB_STEP_KEY); } catch {} ui.onb = null; ui.tour = 0; ui.tab = 'receive'; @@ -2947,7 +2948,17 @@ function destReady(a) { // Recipient search under the destination input: usernames and npubs resolve // to candidates with avatars; picking one runs the same path as pasting. const sendSearch = { rows: null }; -const sendSearcher = makeSearcher((q, rows) => { sendSearch.rows = rows; render(); }); +// With the phone keyboard up, the space under the recipient field is scarce — +// park the field at the top of the view so the candidate list gets what's +// left. Runs on focus and again whenever fresh candidates land (the list +// growing is what pushes things under the keyboard). The .send-suggest +// bottom margin (style.css) guarantees there's page below to scroll into. +function parkSendField() { + if (!matchMedia('(pointer: coarse)').matches) return; + const el = document.activeElement; + if (el && el.isConnected && el.tagName === 'INPUT') el.scrollIntoView({ block: 'start', behavior: 'smooth' }); +} +const sendSearcher = makeSearcher((q, rows) => { sendSearch.rows = rows; render(); if (rows.length) setTimeout(parkSendField, 50); }); // One recipient: address + amount. Max is only offered for a single recipient. function recipientRow(s, r, i) { @@ -2974,6 +2985,9 @@ function recipientRow(s, r, i) { const addrInput = h('input', { type: 'text', class: 'mono-input grow', placeholder: i === 0 ? t('destPlaceholder') : 'bc1q…', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address, + // the delay lets the keyboard start opening (and any render() swap of + // this input) before parkSendField measures the live activeElement + onFocus: () => setTimeout(parkSendField, 300), onInput: (e) => { const v = e.target.value; r.address = v; syncCheck(); diff --git a/src/style.css b/src/style.css index 2b9c969..89662c0 100644 --- a/src/style.css +++ b/src/style.css @@ -102,6 +102,13 @@ a { color: var(--accent-ink); text-decoration: none; } a:hover { text-decoration: underline; } summary { cursor: pointer; } +/* breathing room when an input is scrolled to the top for the keyboard */ +input { scroll-margin-top: 12px; } +/* room below the recipient candidates so the field can scroll to the top + even when the page would otherwise be too short (the margin sits under + the keyboard, never seen) */ +@media (pointer: coarse) { .send-suggest { margin-bottom: 45vh; } } + /* ---- layout helpers ---- */ .row { display: flex; align-items: center; gap: 10px; } .between { justify-content: space-between; } |
