diff options
| author | Adam Soltys <[email protected]> | 2026-08-04 00:09:47 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-04 00:09:47 -0700 |
| commit | b75db0949961bb3b8c1b7e3b46e1ad0e4733ae8e (patch) | |
| tree | 3bce078cdafb521786a866b70b1af2fa4587335b | |
| parent | d8964bdf6f047d058731edfae6d07e57106661c8 (diff) | |
| download | coinosv3-b75db0949961bb3b8c1b7e3b46e1ad0e4733ae8e.tar.gz coinosv3-b75db0949961bb3b8c1b7e3b46e1ad0e4733ae8e.zip | |
One question fewer on the way in, and an 'or' that means it
The sign-in screen stacked 'Connect a signer app' directly above a
bunker/nsec field, which reads as two steps rather than two ways in.
A divider says which it is.
The wizard's second step asked whether you have a nostr account, then
sent you to a screen that asks the same thing. Step one now offers both
paths directly — Get started, or sign in with Nostr — and the middle
step is gone, along with its strings. The welcome blurb goes too: the
screen is one decision, and prose above it is something to read before
you can make it.
The footer sinks to the bottom of the window rather than trailing the
content up the page. #app is a full-height column now; the chat card
sizes itself against the viewport, so the layout test covers it at
three sizes.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
| -rw-r--r-- | src/app.js | 32 | ||||
| -rw-r--r-- | src/features/nostrlogin.js | 3 | ||||
| -rw-r--r-- | src/i18n.js | 8 | ||||
| -rw-r--r-- | src/style.css | 12 | ||||
| -rw-r--r-- | tools/chat-layout-test.js | 3 |
5 files changed, 30 insertions, 28 deletions
@@ -2120,7 +2120,7 @@ function onboardScreen() { ui.onb ||= { step: 'welcome' }; const o = ui.onb; // steps that were waiting on something advance the moment it exists - if ((o.step === 'nostr' || o.step === 'signin') && ui.screen === 'wallet' && activeAccount()) { + if ((o.step === 'welcome' || o.step === 'signin') && ui.screen === 'wallet' && activeAccount()) { o.step = 'legacy'; o.enterAddr = featureHook('namesAddress') || null; } @@ -2173,28 +2173,20 @@ function onboardScreen() { return page([ h('div', { class: 'onb-hero' }, brandHeader(false)), title(t('onbWelcomeTitle')), - h('p', { class: 'muted', style: 'margin:0;font-size:16px' }, t('onbWelcomeBody')), - h('button', { class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: () => { o.step = 'nostr'; render(); } }, t('onbStart')), - h('button', { class: 'linklike small', onClick: () => { ui.onbSkip = true; ui.onb = null; render(); } }, t('onbHaveWallet')), - ]); - } - if (o.step === 'nostr') { - return page([ - h('div', { class: 'onb-mark', html: NOSTR_MARK }), - title(t('onbNostrTitle')), - h('p', { class: 'muted', style: 'margin:0' }, t('onbNostrBody')), - h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: () => { - o.step = 'signin'; - ui.nostrLoginOpen = true; - render(); - } }, t('onbNostrYes')), - h('button', { class: 'btn-block', style: 'padding:14px', disabled: ui.onbBusy, onClick: async () => { + // Starting is the primary path; bringing a nostr account is the + // alternative, offered here rather than as a question of its own. + h('button', { class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', disabled: ui.onbBusy, onClick: async () => { ui.onbBusy = true; render(); try { await enterWallet(newMnemonic(), '', { generated: true }); } catch (e) { ui.onbError = e.message; } ui.onbBusy = false; render(); - } }, ui.onbBusy ? h('span', { class: 'spinner sm' }) : t('onbNostrNo')), + } }, ui.onbBusy ? h('span', { class: 'spinner sm' }) : t('onbStart')), + h('button', { class: 'btn-block', style: 'padding:14px', onClick: () => { + o.step = 'signin'; + ui.nostrLoginOpen = true; + render(); + } }, t('nlSignIn')), ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null, - h('button', { class: 'linklike small', onClick: () => { o.step = 'welcome'; render(); } }, t('back')), + h('button', { class: 'linklike small', onClick: () => { ui.onbSkip = true; ui.onb = null; render(); } }, t('onbHaveWallet')), ]); } if (o.step === 'signin') { @@ -2202,7 +2194,7 @@ function onboardScreen() { h('div', { class: 'onb-mark', html: NOSTR_MARK }), title(t('onbSigninTitle')), featureHook('unlockExtra') || h('div', { class: 'notice err' }, 'nostr login unavailable'), - h('button', { class: 'linklike small', onClick: () => { o.step = 'nostr'; render(); } }, t('back')), + h('button', { class: 'linklike small', onClick: () => { o.step = 'welcome'; render(); } }, t('back')), ]); } if (o.step === 'username') { diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index ced5504..97ce8be 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -199,6 +199,9 @@ export function nostrLoginFeature(ctx) { onClick: () => run(() => extensionSigner()) }, t('nlExtension')) : null, nostrConnectSection(run), + // These are alternatives, not steps: without the divider the field under + // the button reads as something you also have to fill in. + h('div', { class: 'or-split' }, h('span', {}, t('or'))), h('div', { class: 'row gap6' }, h('input', { type: 'password', placeholder: t('nlKeyOrBunker'), style: 'flex:1', diff --git a/src/i18n.js b/src/i18n.js index 99640a3..9b65a77 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -519,15 +519,11 @@ const STR = { arkLnPayFailed: 'Payment failed', arkLnRefunded: 'Payment failed — your money is back in Spending.', arkLnPaidTitle: 'Paid!', + or: 'or', nlSignIn: 'Sign in with Nostr', onbWelcomeTitle: 'Welcome to coinos', - onbWelcomeBody: 'A bitcoin wallet that lives in your browser: instant payments, encrypted chat, and your keys never leave the device. Ready to get started?', - onbStart: "Let's go", + onbStart: 'Get started', onbHaveWallet: 'I already have a wallet', - onbNostrTitle: 'Do you already have a nostr account?', - onbNostrBody: 'Sign in with it and your wallet, name, and profile come with you. No idea what nostr is? No problem — we\u2019ll set everything up fresh.', - onbNostrYes: 'Yes — sign me in', - onbNostrNo: 'No — create my wallet', onbSigninTitle: 'Sign in with Nostr', onbLegacyTitle: 'Do you have an account on coinos.io?', onbLegacyBody: 'The original coinos. We can move your balance over and point your username here — your old account keeps working, it just stops receiving payments.', diff --git a/src/style.css b/src/style.css index ea29891..5064fae 100644 --- a/src/style.css +++ b/src/style.css @@ -78,7 +78,13 @@ html, body { #app { max-width: 560px; margin: 0 auto; - padding: 24px 16px 64px; + padding: 24px 16px 24px; + /* A column the height of the window, so the footer sits at the bottom of + short screens instead of floating under the content. */ + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; } /* ---- typography ---- */ @@ -268,7 +274,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si background: var(--green-soft); color: var(--green); display: grid; place-items: center; font-size: 34px; font-weight: 700; } -.footer { margin-top: 28px; padding-bottom: 8px; } +.footer { margin-top: auto; padding-top: 28px; padding-bottom: 8px; } .footer a { color: var(--accent-ink); font-weight: 600; } /* Clickable unit labels (BTC/sats) — look like text, toggle the global unit. */ @@ -570,3 +576,5 @@ body::after { .balance-move .input-group > button { flex: 1 1 150px; } .chat-signer-off { padding: 6px 8px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface2); } .chan-pick { width: auto; min-width: 0; padding: 6px 32px 6px 10px; font-size: 13px; font-weight: 600; border-radius: 9px; background-position: right 10px center; } +.or-split { display: flex; align-items: center; gap: 10px; color: var(--faint); font-size: 13px; } +.or-split::before, .or-split::after { content: ''; flex: 1; height: 1px; background: var(--line); } diff --git a/tools/chat-layout-test.js b/tools/chat-layout-test.js index ef86291..f39b87e 100644 --- a/tools/chat-layout-test.js +++ b/tools/chat-layout-test.js @@ -115,6 +115,9 @@ try { await page.evaluate(() => { const i = document.querySelector('input[placeholder*="channel" i]'); const b = i && i.parentElement.querySelector('button'); if (b) b.click(); }); for (let i = 0; i < 60 && !(await page.$('.chan-pick')); i++) await sleep(500); + // the picker appears on the relay echo, which can beat createChannel's own + // continuation — give the field a moment to put itself away + for (let i = 0; i < 20 && (await page.$('input[placeholder*="channel" i]')); i++) await sleep(500); const two = await page.evaluate(() => { const sel = document.querySelector('.chan-pick'); return { picker: !!sel, tag: sel && sel.tagName, opts: sel ? [...sel.options].map((o) => o.textContent) : [], field: !!document.querySelector('input[placeholder*="channel" i]') }; |
