refactor(hephaestus): wallet picker is an accordion, not a page swap
Clicking Create / Import / WizardConnect now expands the flow *inside* that option (native <details>/<summary>) instead of hiding the picker and rendering a separate view. Opening one option auto-closes the others. Chevron flips and border turns accent-orange on the open card. "Back" buttons removed — closing is a click on the same summary. - wallet.js: renderTabs() emits three <details class="wallet-opt">; the per-option flow content is populated lazily on first `toggle` and lives inside `.wallet-flow` (no more global #panel). Accordion behavior is enforced by closing sibling <details> on toggle. - login.css: .wallet-opt reworked as details-summary card; chevron rotates on [open]; .wallet-flow is the divider-topped body inside each card.
This commit is contained in:
parent
e67943ed00
commit
34b58b8637
2 changed files with 92 additions and 97 deletions
|
|
@ -36,44 +36,45 @@ h2 { font-size: 17px; margin: 0 0 6px; }
|
|||
p.lede { color: var(--muted); margin: 0 0 20px; font-size: 14px; }
|
||||
p.sub { color: var(--muted); margin: 0 0 12px; font-size: 13.5px; }
|
||||
|
||||
/* --- Wallet picker (three big options — Create / Import / Connect) --- */
|
||||
/* --- Wallet picker: accordion of three options (Create / Import / Connect) --- */
|
||||
.wallet-picker { display: grid; gap: 10px; margin: 8px 0 18px; }
|
||||
.wallet-opt {
|
||||
display: grid; grid-template-columns: 40px 1fr; gap: 14px; align-items: start;
|
||||
padding: 14px 16px; background: var(--bg); border: 1px solid var(--border);
|
||||
border-radius: 10px; cursor: pointer; text-align: left; font: inherit; color: var(--text);
|
||||
transition: border-color .1s, background .1s;
|
||||
details.wallet-opt {
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: 10px;
|
||||
overflow: hidden; transition: border-color .1s, background .1s;
|
||||
}
|
||||
.wallet-opt:hover { border-color: var(--accent); background: var(--card); }
|
||||
details.wallet-opt:hover { border-color: var(--accent); }
|
||||
details.wallet-opt[open] { border-color: var(--accent); background: var(--card); }
|
||||
details.wallet-opt > summary {
|
||||
display: grid; grid-template-columns: 40px 1fr auto; gap: 14px; align-items: start;
|
||||
padding: 14px 16px; cursor: pointer; list-style: none; user-select: none;
|
||||
}
|
||||
details.wallet-opt > summary::-webkit-details-marker { display: none; }
|
||||
details.wallet-opt > summary::marker { content: ""; }
|
||||
.wallet-opt .ico { font-size: 24px; line-height: 1; padding-top: 2px; }
|
||||
.wallet-opt .body { display: flex; flex-direction: column; gap: 4px; }
|
||||
.wallet-opt .body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.wallet-opt b { color: var(--text); font-size: 14.5px; font-weight: 600;
|
||||
display: inline-flex; align-items: center; gap: 8px; }
|
||||
display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.wallet-opt .sub { color: var(--muted); font-size: 13px; line-height: 1.5; }
|
||||
.wallet-opt .pill {
|
||||
display: inline-block; font-size: 10.5px; padding: 1px 8px; border-radius: 999px;
|
||||
background: rgba(255,140,0,.14); color: var(--accent); font-weight: 500;
|
||||
letter-spacing: 0.02em; text-transform: lowercase;
|
||||
}
|
||||
.wallet-opt .chev {
|
||||
color: var(--muted); font-size: 14px; padding-top: 4px;
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
details.wallet-opt[open] > summary .chev { transform: rotate(180deg); color: var(--accent); }
|
||||
p.fine {
|
||||
color: var(--muted); font-size: 12.5px; margin: 4px 0 0; text-align: center;
|
||||
}
|
||||
p.fine a { color: var(--muted); }
|
||||
p.fine a:hover { color: var(--text); }
|
||||
|
||||
/* --- Selected flow panel (Create/Import/Connect) --- */
|
||||
.flow { display: flex; flex-direction: column; gap: 4px; }
|
||||
.flow > label { margin-top: 12px; }
|
||||
.flow .row {
|
||||
display: flex; gap: 10px; margin-top: 18px; align-items: center;
|
||||
}
|
||||
.flow .row .primary { flex: 1; margin-top: 0; }
|
||||
.flow .row .ghost {
|
||||
background: transparent; border: 1px solid var(--border); color: var(--muted);
|
||||
padding: 10px 14px; border-radius: 8px; font: inherit; font-size: 13.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flow .row .ghost:hover { color: var(--text); border-color: var(--muted); }
|
||||
/* --- Expanded flow inside a wallet-opt --- */
|
||||
.wallet-flow { padding: 0 16px 16px 16px; border-top: 1px solid var(--border); }
|
||||
.wallet-flow .flow { display: flex; flex-direction: column; gap: 4px; padding-top: 12px; }
|
||||
.wallet-flow .flow > label { margin-top: 12px; }
|
||||
|
||||
/* --- WizardConnect coming-soon panel --- */
|
||||
.wc-supported { display: grid; gap: 8px; margin: 8px 0 14px; }
|
||||
|
|
|
|||
|
|
@ -159,115 +159,109 @@ function renderTabs() {
|
|||
<h1>Sign in with your <span class="acid">Bitcoin Cash</span> wallet</h1>
|
||||
<p class="lede">No email. No password reset. Your wallet is your identity.</p>
|
||||
<div class="wallet-picker" id="picker">
|
||||
<button class="wallet-opt" data-choice="create">
|
||||
<span class="ico">✨</span>
|
||||
<span class="body">
|
||||
<b>Create a new wallet<span class="pill">easiest</span></b>
|
||||
<span class="sub">Made here in your browser. You get a recovery phrase to write down — it is the only key.</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="wallet-opt" data-choice="import">
|
||||
<span class="ico">🔑</span>
|
||||
<span class="body">
|
||||
<b>Import / add a wallet</b>
|
||||
<span class="sub">Restore a wallet you already have from its 12- or 24-word recovery phrase.</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="wallet-opt" data-choice="connect">
|
||||
<span class="ico">🪄</span>
|
||||
<span class="body">
|
||||
<b>Connect a wallet — WizardConnect<span class="pill">most private</span></b>
|
||||
<span class="sub">Cashonize 0.9+ or Paytaca. Your keys never leave your wallet.</span>
|
||||
</span>
|
||||
</button>
|
||||
<details class="wallet-opt" data-choice="create">
|
||||
<summary>
|
||||
<span class="ico">✨</span>
|
||||
<span class="body">
|
||||
<b>Create a new wallet<span class="pill">easiest</span></b>
|
||||
<span class="sub">Made here in your browser. You get a recovery phrase to write down — it is the only key.</span>
|
||||
</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="wallet-flow"></div>
|
||||
</details>
|
||||
<details class="wallet-opt" data-choice="import">
|
||||
<summary>
|
||||
<span class="ico">🔑</span>
|
||||
<span class="body">
|
||||
<b>Import / add a wallet</b>
|
||||
<span class="sub">Restore a wallet you already have from its 12- or 24-word recovery phrase.</span>
|
||||
</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="wallet-flow"></div>
|
||||
</details>
|
||||
<details class="wallet-opt" data-choice="connect">
|
||||
<summary>
|
||||
<span class="ico">🪄</span>
|
||||
<span class="body">
|
||||
<b>Connect a wallet — WizardConnect<span class="pill">most private</span></b>
|
||||
<span class="sub">Cashonize 0.9+ or Paytaca. Your keys never leave your wallet.</span>
|
||||
</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="wallet-flow"></div>
|
||||
</details>
|
||||
</div>
|
||||
<p class="fine">Prefer old-school? <a href="/user/login?password=1">Sign in with username & password →</a></p>
|
||||
<div id="panel"></div>
|
||||
</div>
|
||||
`));
|
||||
app.querySelectorAll(".wallet-opt").forEach((b) => {
|
||||
b.addEventListener("click", () => {
|
||||
// Hide the picker while a specific panel is open
|
||||
app.querySelector("#picker").classList.add("hidden");
|
||||
app.querySelector(".fine")?.classList.add("hidden");
|
||||
renderPanel(b.dataset.choice);
|
||||
app.querySelectorAll("details.wallet-opt").forEach((d) => {
|
||||
d.addEventListener("toggle", () => {
|
||||
if (!d.open) return;
|
||||
// Accordion: close siblings
|
||||
app.querySelectorAll("details.wallet-opt").forEach((other) => {
|
||||
if (other !== d) other.open = false;
|
||||
});
|
||||
// Lazily populate this option's flow content the first time it opens
|
||||
const flow = d.querySelector(".wallet-flow");
|
||||
if (flow && !flow.dataset.ready) {
|
||||
populateFlow(d.dataset.choice, flow);
|
||||
flow.dataset.ready = "1";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backToPicker() {
|
||||
app.querySelector("#picker")?.classList.remove("hidden");
|
||||
app.querySelector(".fine")?.classList.remove("hidden");
|
||||
const panel = document.getElementById("panel");
|
||||
if (panel) panel.innerHTML = "";
|
||||
}
|
||||
|
||||
function renderPanel(kind) {
|
||||
const panel = document.getElementById("panel");
|
||||
panel.innerHTML = "";
|
||||
function populateFlow(kind, flow) {
|
||||
if (kind === "create") {
|
||||
const mnemonic = generateBip39Mnemonic();
|
||||
panel.appendChild(el(`
|
||||
flow.appendChild(el(`
|
||||
<div class="flow">
|
||||
<h2>Your new recovery phrase</h2>
|
||||
<p class="sub">Write it down. This is the only way to recover your account — we can never help you reset it.</p>
|
||||
<p class="sub"><b>Your new recovery phrase.</b> Write it down. This is the only way to recover your account — we can never help you reset it.</p>
|
||||
<div class="mnemonic-box">${mnemonic}</div>
|
||||
<label>Set a passphrase to encrypt this wallet in your browser</label>
|
||||
<input type="password" id="pw" autocomplete="new-password" placeholder="min 8 chars">
|
||||
<input type="password" class="pw" autocomplete="new-password" placeholder="min 8 chars">
|
||||
<p class="small">The passphrase never leaves your device. Lose both phrase and passphrase = account is gone.</p>
|
||||
<div class="row">
|
||||
<button class="primary" id="go">Create wallet & sign in</button>
|
||||
<button class="ghost" id="back">← Back</button>
|
||||
</div>
|
||||
<p class="warn hidden" id="err"></p>
|
||||
<button class="primary go">Create wallet & sign in</button>
|
||||
<p class="warn hidden err"></p>
|
||||
</div>
|
||||
`));
|
||||
panel.querySelector("#back").addEventListener("click", backToPicker);
|
||||
panel.querySelector("#go").addEventListener("click", async () => {
|
||||
const pw = panel.querySelector("#pw").value;
|
||||
if (pw.length < 8) return showErr(panel, "passphrase must be at least 8 chars");
|
||||
await onboardAndLogin(mnemonic, pw, panel);
|
||||
flow.querySelector(".go").addEventListener("click", async () => {
|
||||
const pw = flow.querySelector(".pw").value;
|
||||
if (pw.length < 8) return showErr(flow, "passphrase must be at least 8 chars");
|
||||
await onboardAndLogin(mnemonic, pw, flow);
|
||||
});
|
||||
} else if (kind === "import") {
|
||||
panel.appendChild(el(`
|
||||
flow.appendChild(el(`
|
||||
<div class="flow">
|
||||
<h2>Import a wallet</h2>
|
||||
<p class="sub">Paste your BIP-39 recovery phrase (12 or 24 words). Same phrase = same account.</p>
|
||||
<textarea id="mn" spellcheck="false" autocomplete="off" placeholder="word word word ..."></textarea>
|
||||
<textarea class="mn" spellcheck="false" autocomplete="off" placeholder="word word word ..."></textarea>
|
||||
<label>Set a passphrase to encrypt this wallet in your browser</label>
|
||||
<input type="password" id="pw" autocomplete="new-password" placeholder="min 8 chars">
|
||||
<div class="row">
|
||||
<button class="primary" id="go">Import & sign in</button>
|
||||
<button class="ghost" id="back">← Back</button>
|
||||
</div>
|
||||
<p class="warn hidden" id="err"></p>
|
||||
<input type="password" class="pw" autocomplete="new-password" placeholder="min 8 chars">
|
||||
<button class="primary go">Import & sign in</button>
|
||||
<p class="warn hidden err"></p>
|
||||
</div>
|
||||
`));
|
||||
panel.querySelector("#back").addEventListener("click", backToPicker);
|
||||
panel.querySelector("#go").addEventListener("click", async () => {
|
||||
const mn = panel.querySelector("#mn").value.trim().replace(/\s+/g, " ");
|
||||
const pw = panel.querySelector("#pw").value;
|
||||
if (pw.length < 8) return showErr(panel, "passphrase must be at least 8 chars");
|
||||
flow.querySelector(".go").addEventListener("click", async () => {
|
||||
const mn = flow.querySelector(".mn").value.trim().replace(/\s+/g, " ");
|
||||
const pw = flow.querySelector(".pw").value;
|
||||
if (pw.length < 8) return showErr(flow, "passphrase must be at least 8 chars");
|
||||
try { await keypairFromMnemonic(mn); }
|
||||
catch { return showErr(panel, "invalid recovery phrase"); }
|
||||
await onboardAndLogin(mn, pw, panel);
|
||||
catch { return showErr(flow, "invalid recovery phrase"); }
|
||||
await onboardAndLogin(mn, pw, flow);
|
||||
});
|
||||
} else if (kind === "connect") {
|
||||
panel.appendChild(el(`
|
||||
flow.appendChild(el(`
|
||||
<div class="flow">
|
||||
<h2>Connect a wallet</h2>
|
||||
<p class="sub">Sign the challenge with an external BCH wallet — your keys never leave it. Encrypted end-to-end over Nostr; the relay only sees ciphertext.</p>
|
||||
<div class="wc-supported">
|
||||
<div class="wc-wallet"><b>Cashonize</b><span>browser extension · v0.9+</span><a href="https://cashonize.com/" target="_blank" rel="noopener">install →</a></div>
|
||||
<div class="wc-wallet"><b>Paytaca</b><span>mobile · iOS + Android</span><a href="https://paytaca.com/" target="_blank" rel="noopener">install →</a></div>
|
||||
</div>
|
||||
<p class="note-soon"><b>Coming soon.</b> WizardConnect for message-signing is being wired in — for now, use Create or Import above (or the button below to jump back to the picker).</p>
|
||||
<div class="row">
|
||||
<button class="ghost" id="back">← Back to picker</button>
|
||||
</div>
|
||||
<p class="note-soon"><b>Coming soon.</b> WizardConnect for message-signing is being wired in — for now, use Create or Import above.</p>
|
||||
</div>
|
||||
`));
|
||||
panel.querySelector("#back").addEventListener("click", backToPicker);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue