aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js49
-rw-r--r--src/i18n.js7
-rw-r--r--src/style.css25
3 files changed, 57 insertions, 24 deletions
diff --git a/src/app.js b/src/app.js
index a82e1b8..276f6cd 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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; }
+}