diff options
| author | Adam Soltys <[email protected]> | 2026-08-04 00:22:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-04 00:22:19 -0700 |
| commit | bba0b5f36841ef68950154fdbe278d6c249ca375 (patch) | |
| tree | f67b89e92bdc925c116967c718460466dc64be3c /src | |
| parent | ea4b6192f34562e38eeff3e5c246f90646f1b37d (diff) | |
| download | coinosv3-bba0b5f36841ef68950154fdbe278d6c249ca375.tar.gz coinosv3-bba0b5f36841ef68950154fdbe278d6c249ca375.zip | |
Paste lives in the field; the footer fits; claiming a name comes first
Paste, on the import and load-seed screens, was a bordered box floating
beside a small label — heavier than the label, detached from the field it
fills. It now sits inside the textarea, and only while that textarea is
empty: the only time it's wanted, and the only time it can't cover what
you typed. It's toggled on the node rather than through render(), since
re-rendering a textarea per keystroke rebuilds it under the caret. Beside
a one-line input (Send) the old placement was right and is untouched.
The footer measured 100dvh, which on a phone can resolve to the tall
viewport at first paint and leave the language picker just below the
fold. svh is the one that fits with the address bar showing. Short
screens also tighten their padding, which buys back the ~50px the
create/import screen was overflowing at 375x553.
And the wizard no longer asks every new user whether they have an old
coinos.io account before letting them have a name. Claiming the name is
the step; the people who do have an old account know it, and say so with
a link. That screen is a statement now, not a question they just
answered. The npub-shaped default address is no longer offered as a
button — a plain skip goes past without showing anyone that.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 49 | ||||
| -rw-r--r-- | src/i18n.js | 7 | ||||
| -rw-r--r-- | src/style.css | 25 |
3 files changed, 57 insertions, 24 deletions
@@ -510,6 +510,25 @@ function pasteBtn(apply) { }); } +// Paste, for a textarea. Beside a one-line input the button belongs in the +// input-group (see Send); above a textarea it floats next to the label as a +// box heavier than the label itself. So put it inside the field — and only +// while the field is empty, which is the only time it's wanted and the only +// time it can't cover what you typed. +function pasteInto(ta, apply) { + const btn = pasteBtn((text) => { apply(text); sync(); }); + if (!btn) return ta; + btn.classList.add('paste-in'); + btn.append(h('span', {}, t('paste'))); + const wrap = h('div', { class: 'paste-wrap' }, ta, btn); + // Toggled on the node rather than through render(): re-rendering a textarea + // on every keystroke rebuilds it under the caret. + const sync = () => wrap.classList.toggle('has-text', !!ta.value); + ta.addEventListener('input', sync); + sync(); + return wrap; +} + // ---------------------------------------------------------------- display unit // Global BTC/sats preference, persisted in localStorage across refreshes and // logouts. Every unit label on the site is clickable to toggle it. @@ -766,11 +785,8 @@ function importPane() { h( 'label', { class: 'field' }, - h('div', { class: 'row between' }, - h('span', { class: 'lab' }, t('importLabel')), - pasteBtn((text) => { ta.value = text; ui.importText = text; }) - ), - ta + h('span', { class: 'lab' }, t('importLabel')), + pasteInto(ta, (text) => { ta.value = text; ui.importText = text; }) ), optionsPanel(), h('button', { class: 'btn-primary btn-block', onClick: () => openWallet(ui.importText) }, t('openWallet')) @@ -2121,7 +2137,7 @@ function onboardScreen() { const o = ui.onb; // steps that were waiting on something advance the moment it exists if ((o.step === 'welcome' || o.step === 'signin') && ui.screen === 'wallet' && activeAccount()) { - o.step = 'legacy'; + o.step = 'username'; o.enterAddr = featureHook('namesAddress') || null; } if (o.step === 'username') { @@ -2163,10 +2179,8 @@ function onboardScreen() { disabled: !!o.legacyBusy, onClick: goLegacy, }, o.legacyBusy ? h('span', { class: 'spinner sm' }) : t('onbLegacyGo')), h('button', { class: 'btn-block', style: 'padding:14px', onClick: () => { o.step = 'username'; render(); } }, - o.wentToLegacy ? t('onbLegacyDone') : t('onbLegacyNo')), + o.wentToLegacy ? t('onbLegacyDone') : t('back')), ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null, - // No Back: the wallet exists by now, so the nostr question behind us - // would just bounce straight back here. Both choices lead forward. ]); } if (o.step === 'welcome') { @@ -2207,10 +2221,12 @@ function onboardScreen() { title(t('onbNameTitle')), h('p', { class: 'muted', style: 'margin:0' }, t('onbNameBody')), featureHook('namesClaimForm') || h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('onbNameWait'))), - addr - ? h('button', { class: 'btn-block', onClick: () => { o.step = 'avatar'; render(); } }, - t('onbKeepPre'), h('span', { class: 'onb-addr' }, addr), t('onbKeepPost')) - : null, + // A way past without naming yourself, but not one that shows the + // npub-shaped default address — nobody wants to be offered that. + addr ? h('button', { class: 'linklike small', onClick: () => { o.step = 'avatar'; render(); } }, t('onbSkipName')) : null, + // Most people are new. The ones who aren't know it, and can say so here + // rather than everyone being asked first. + h('button', { class: 'linklike small', onClick: () => { ui.onbError = ''; o.step = 'legacy'; render(); } }, t('onbHaveCoinos')), ]); } if (o.step === 'avatar') { @@ -2607,12 +2623,9 @@ function loadSeedCard() { return h( 'div', { class: 'card col' }, - h('div', { class: 'row between' }, - h('h3', { style: 'margin:0' }, t('loadSeedTitle')), - pasteBtn((text) => { seedTa.value = text; ls.value = text; }) - ), + h('h3', { style: 'margin:0' }, t('loadSeedTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('loadSeedDesc')), - seedTa, + pasteInto(seedTa, (text) => { seedTa.value = text; ls.value = text; }), h('input', { type: 'password', class: 'mono-input', placeholder: t('bip39PassphraseOpt'), autocapitalize: 'none', autocomplete: 'off', value: ls.passphrase, diff --git a/src/i18n.js b/src/i18n.js index 9b65a77..a395518 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -525,20 +525,19 @@ const STR = { onbStart: 'Get started', onbHaveWallet: 'I already have a wallet', onbSigninTitle: 'Sign in with Nostr', - onbLegacyTitle: 'Do you have an account on coinos.io?', + onbLegacyTitle: 'Move your coinos.io account', 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.', onbLegacyGo: 'Move my coinos.io account', onbLegacyNoAddr: "Your payment address isn't ready yet — try again in a moment.", onbLegacyDone: "I'm done — continue", - onbLegacyNo: "No, I'm new here", + onbHaveCoinos: 'I already have a coinos.io account', migrateClaimed: 'Welcome back — {name} is yours here now.', migrateClaimFailed: "Your balance moved, but {name} isn't free yet. Try claiming it in a minute.", migrateWorking: 'Claiming {name}…', onbNameTitle: 'Claim your username', onbNameBody: 'Your payment address — people can send you bitcoin with just this.', onbNameWait: 'Getting things ready…', - onbKeepPre: 'Use ', - onbKeepPost: ' for now', + onbSkipName: 'Skip for now', onbAvatarTitle: 'Choose your look', onbAvatarBody: 'Pick a punk or upload your own picture. You can change it anytime.', onbUpload: 'Upload a photo', diff --git a/src/style.css b/src/style.css index 5064fae..975f490 100644 --- a/src/style.css +++ b/src/style.css @@ -80,9 +80,12 @@ html, body { margin: 0 auto; 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. */ + short screens instead of floating under the content. svh, not dvh: dvh + can resolve to the tall viewport (URL bar hidden) at first paint, which + puts the footer just below the fold until you scroll — the phone browser + showing its address bar is the case that has to fit. */ min-height: 100vh; - min-height: 100dvh; + min-height: 100svh; display: flex; flex-direction: column; } @@ -578,3 +581,21 @@ body::after { .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); } +.paste-wrap { position: relative; display: block; } +.paste-wrap textarea { display: block; } +.paste-in { + position: absolute; right: 10px; bottom: 12px; display: flex; align-items: center; gap: 6px; + padding: 5px 9px; font-size: 12px; font-weight: 600; border-radius: 8px; + color: var(--muted); background: var(--paper); border: 1px solid var(--line-strong); +} +.paste-in:hover { color: var(--ink); } +.paste-wrap.has-text .paste-in { display: none; } + +/* Short screens (a small phone with its address bar showing) — buy back the + vertical room the footer needs by tightening the frame, not the content. */ +@media (max-height: 620px) { + #app { padding-top: 12px; padding-bottom: 12px; } + .card { padding: 16px; } + .footer { padding-top: 14px; padding-bottom: 0; } + .tabs { margin: 10px 0; } +} |
