hermes panel: mirror the Sign in / Sign up tab bar from the web

Theseus's sidebar panel.html had the pre-redesign flow (two primary buttons
+ Create a new account toggle + Name+password/Seed-phrase chip switcher).
Replaced with the same persistent pill tab bar the web messenger uses:
Sign in / Sign up visible together, active one filled, inactive outlined;
tapping an inactive tab swaps the form below with no back step. "Recover
with seed phrase" is a quiet link that opens its own form (with ← Back).

The Theseus-only "Open with Theseus ID" quick path now lives inside the
Sign in tab's form and hides entirely when the vault is locked — no more
disabled button + hint about unlocking the vault cluttering the signin
screen. Bumped the bundled addon to 0.2.9.
This commit is contained in:
Local Dev 2026-10-22 13:28:47 +03:00
parent 4b3fd4be42
commit c944952f04
2 changed files with 98 additions and 69 deletions

File diff suppressed because one or more lines are too long

View file

@ -257,55 +257,65 @@
</div>
</div>
<!-- ========== SIGNIN SCREEN ========== -->
<!-- ========== SIGNIN SCREEN ==========
Same tab bar as the web messenger: Sign in / Sign up persistent pill,
active tab filled. The Theseus-only "Open with Theseus ID" quick path
slots into the Sign in tab's form when the vault is unlocked. -->
<div class="screen active" id="signin">
<div class="mk" aria-hidden="true">
<img src="assets/mark.svg" alt="" width="72" height="72" style="display:block">
</div>
<h1>Hermes</h1>
<p class="lede">End-to-end encrypted DMs. Your identity is your on-chain registered name — same wallet seed as Theseus.</p>
<div class="actions">
<button class="primary" id="useVaultBtn" disabled>Open with Theseus ID</button>
<button class="secondary" id="toggleCreateBtn">Create a new account</button>
<!-- Persistent tab bar. Clicking an inactive tab swaps the form below. -->
<div id="startRow" style="display:flex;gap:0;width:100%;max-width:300px;border:1px solid var(--line);border-radius:999px;padding:3px;background:var(--panel)">
<button class="tab tab-active" id="startSignInBtn" style="flex:1;font:inherit;font-weight:600;font-size:13px;padding:8px 10px;background:var(--sky);color:var(--sky-ink);border:0;border-radius:999px;cursor:pointer">Sign in</button>
<button class="tab" id="startSignUpBtn" style="flex:1;font:inherit;font-weight:600;font-size:13px;padding:8px 10px;background:transparent;color:var(--ink);border:0;border-radius:999px;cursor:pointer">Sign up</button>
</div>
<div class="mnemonic" id="createBox">
<input id="createHandle" type="text" placeholder="alice" autocomplete="off" spellcheck="false" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<!-- Sign-in form (default). Theseus-ID button appears above when the
vault is unlocked (toggled by boot code based on canUseVault). -->
<div id="signinForm" style="display:flex;width:100%;max-width:300px;flex-direction:column;gap:8px">
<button class="primary" id="useVaultBtn" disabled style="display:none">Open with Theseus ID</button>
<input id="signinHandle" type="text" placeholder="alice" autocomplete="username" spellcheck="false" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<input id="signinPassword" type="password" placeholder="password" autocomplete="current-password" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<button class="primary" id="signinBtn">Sign in</button>
</div>
<!-- Sign-up form (hidden until Sign up tab active). -->
<div id="signupForm" style="display:none;width:100%;max-width:300px;flex-direction:column;gap:8px">
<input id="createHandle" type="text" placeholder="pick a name — alice" autocomplete="off" spellcheck="false" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<input id="createPassword" type="password" placeholder="password (min 6)" autocomplete="new-password" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<input id="createPassword2" type="password" placeholder="confirm password" autocomplete="new-password" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<label style="display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--mut);text-align:left;cursor:pointer">
<input id="createCloud" type="checkbox" style="margin-top:3px;accent-color:var(--sky)">
<span>Back up to the cloud so I can sign in on other devices with this name + password. (You can turn this off in Settings. Your password is the only key; relays never see the seed.)</span>
<span>Back up to the cloud so I can sign in on other devices with this name + password.</span>
</label>
<button class="primary" id="useCreateBtn">Create account</button>
<div class="hint">You'll get a handle like <code>alice.erm</code>. We'll generate a seed phrase — back it up in Settings.</div>
<div class="hint" style="margin-top:0">We'll generate a seed phrase — back it up in Settings after you sign in.</div>
</div>
<div style="display:flex;align-items:center;gap:10px;width:100%;max-width:300px;margin-top:8px">
<span style="flex:1;height:1px;background:var(--line)"></span>
<span style="color:var(--mut);font-size:11.5px;letter-spacing:.4px;text-transform:uppercase">Sign in</span>
<span style="flex:1;height:1px;background:var(--line)"></span>
<!-- Recover-with-seed form. Reached from the "Recover with seed phrase"
link, has its own ← Back since recovery is a one-shot flow. -->
<div id="recoverForm" style="display:none;width:100%;max-width:300px;flex-direction:column;gap:8px">
<textarea id="mnemonicInput" placeholder="nsec1… or 12/24-word mnemonic" autocomplete="off" spellcheck="false" style="font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12.5px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:10px;width:100%;min-height:86px;resize:vertical"></textarea>
<button class="primary" id="recoverBtn">Recover account</button>
<button class="secondary" id="recoverBack" style="font:inherit;background:transparent;color:var(--mut);border:0;cursor:pointer;font-size:12px;padding:4px">← Back to sign in</button>
<div class="hint">Stays in this panel, never leaves the device.</div>
</div>
<div class="signin-chips" style="display:flex;gap:6px;width:100%;max-width:300px">
<button class="chip chip-active" data-method="namepw" style="flex:1;font:inherit;font-size:12px;padding:7px 8px;background:var(--sky);color:#06111a;border:1px solid var(--sky);border-radius:6px;cursor:pointer;font-weight:600">Name + password</button>
<button class="chip" data-method="seed" style="flex:1;font:inherit;font-size:12px;padding:7px 8px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;cursor:pointer">Seed phrase</button>
<!-- Hidden legacy button so the old toggleCreateBtn handler doesn't crash. -->
<div style="display:none">
<button id="toggleCreateBtn"></button>
</div>
<div id="methodNamepw" class="method" style="width:100%;max-width:300px;display:flex;flex-direction:column;gap:8px">
<input id="signinHandle" type="text" placeholder="alice" autocomplete="username" spellcheck="false" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<input id="signinPassword" type="password" placeholder="password" autocomplete="current-password" style="font-family:inherit;font-size:13px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 10px;width:100%">
<div class="hint">Works on this device, or on any device if you turned on cloud backup. Otherwise use seed phrase.</div>
<div id="recoverLinkRow" style="margin-top:6px">
<a href="#" id="recoverLink" style="color:var(--mut);font-size:12px;text-decoration:underline">Recover with seed phrase</a>
</div>
<div id="methodSeed" class="method" style="width:100%;max-width:300px;display:none;flex-direction:column;gap:8px">
<textarea id="mnemonicInput" placeholder="nsec1… or 12/24-word mnemonic — stays in this tab, never sent to the server" autocomplete="off" spellcheck="false" style="font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12.5px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:10px;width:100%;min-height:86px;resize:vertical"></textarea>
<div class="hint">Paste your 12 or 24 words.</div>
</div>
<button class="primary" id="signinBtn" style="width:100%;max-width:300px;margin-top:4px">Sign in</button>
<div class="err" id="signinErr"></div>
<div class="hint" id="vaultHint"></div>
<div class="hint" style="margin-top:4px;max-width:300px">You'll stay signed in. Sign out from the ⋯ menu.</div>
<div class="hint" id="vaultHint" style="font-size:11px;max-width:300px;text-align:center"></div>
</div>
<!-- ========== APP SCREEN ========== -->
@ -637,6 +647,34 @@ function showSignin() {
$("signin").classList.add("active");
$("menuBtn").style.display = "none";
closeMenu();
showSigninView("signin");
}
// One of: "signin" (default), "signup", "recover". Sign in / Sign up tabs
// stay visible on the first two; recover hides them (its own ← Back).
function showSigninView(name) {
const inTabs = name === "signin" || name === "signup";
$("startRow").style.display = inTabs ? "flex" : "none";
$("signinForm").style.display = name === "signin" ? "flex" : "none";
$("signupForm").style.display = name === "signup" ? "flex" : "none";
$("recoverForm").style.display = name === "recover" ? "flex" : "none";
$("recoverLinkRow").style.display = inTabs ? "block" : "none";
const styleTab = (btn, active) => {
btn.classList.toggle("tab-active", active);
btn.style.background = active ? "var(--sky)" : "transparent";
btn.style.color = active ? "var(--sky-ink)" : "var(--ink)";
};
styleTab($("startSignInBtn"), name === "signin");
styleTab($("startSignUpBtn"), name === "signup");
err($("signinErr"), "");
const focusEl = {
signin: $("signinHandle"),
signup: $("createHandle"),
recover: $("mnemonicInput"),
}[name];
if (focusEl) setTimeout(() => focusEl.focus(), 50);
}
function openMenu() { $("menu").style.display = "block"; }
@ -650,7 +688,7 @@ async function initWithOpts(opts) {
err($("signinErr"), "");
const btn = opts.create ? $("useCreateBtn")
: opts.useVault ? $("useVaultBtn")
: $("useMnemonicBtn");
: $("recoverBtn");
const prev = btn.textContent;
btn.disabled = true; btn.textContent = "Opening…";
try {
@ -688,14 +726,18 @@ async function initWithOpts(opts) {
}
} catch {}
// Enable "Open with Theseus ID" iff the vault is unlocked AND was set up with a mnemonic.
// Enable "Open with Theseus ID" iff the vault is unlocked AND was set up
// with a mnemonic. The button hides entirely when unavailable so the
// signin form stays clean on untyped vaults.
try {
const v = await bridge.invoke("canUseVault");
state.canUseVault = !!(v && v.available);
$("useVaultBtn").disabled = !state.canUseVault;
const btn = $("useVaultBtn");
btn.disabled = !state.canUseVault;
btn.style.display = state.canUseVault ? "block" : "none";
$("vaultHint").textContent = state.canUseVault
? "Uses the same wallet seed as Theseus — nothing new to remember."
: "To use Theseus ID, unlock the password vault first (set up with a 12/24-word mnemonic).";
: "";
} catch {}
// Panel event bus — pushes from main.js.
@ -720,12 +762,16 @@ async function initWithOpts(opts) {
setInterval(refreshStatus, 7000);
})();
// ---- events ----
// ---- events: tab switcher + recover link ----
$("startSignInBtn").addEventListener("click", () => showSigninView("signin"));
$("startSignUpBtn").addEventListener("click", () => showSigninView("signup"));
$("recoverLink").addEventListener("click", (e) => { e.preventDefault(); showSigninView("recover"); });
$("recoverBack").addEventListener("click", () => showSigninView("signin"));
showSigninView("signin");
// Theseus-only quick path: unlock Hermes with the vault seed.
$("useVaultBtn").addEventListener("click", () => initWithOpts({ useVault: true }));
$("toggleCreateBtn").addEventListener("click", () => {
$("createBox").classList.toggle("show");
if ($("createBox").classList.contains("show")) $("createHandle").focus();
});
$("useCreateBtn").addEventListener("click", async () => {
const handle = $("createHandle").value.trim();
const p1 = $("createPassword").value;
@ -743,34 +789,13 @@ $("useCreateBtn").addEventListener("click", async () => {
}
});
// --- unified Sign in button + chip switcher ---
function switchMethod(name) {
for (const c of document.querySelectorAll(".signin-chips .chip")) {
const active = c.dataset.method === name;
c.classList.toggle("chip-active", active);
c.style.background = active ? "var(--sky)" : "var(--panel)";
c.style.color = active ? "#06111a" : "var(--ink)";
c.style.borderColor = active ? "var(--sky)" : "var(--line)";
c.style.fontWeight = active ? "600" : "400";
}
$("methodNamepw").style.display = name === "namepw" ? "flex" : "none";
$("methodSeed").style.display = name === "seed" ? "flex" : "none";
state._signinMethod = name;
const focusEl = name === "namepw" ? $("signinHandle") : $("mnemonicInput");
setTimeout(() => focusEl && focusEl.focus(), 50);
}
state._signinMethod = "namepw";
for (const c of document.querySelectorAll(".signin-chips .chip")) {
c.addEventListener("click", () => switchMethod(c.dataset.method));
}
$("signinBtn").addEventListener("click", async () => {
const m = state._signinMethod || "namepw";
err($("signinErr"), "");
if (m === "namepw") {
const handle = $("signinHandle").value.trim();
const password = $("signinPassword").value;
if (!handle) { err($("signinErr"), "enter your name"); return; }
if (!password) { err($("signinErr"), "enter your password"); return; }
const handle = $("signinHandle").value.trim();
const password = $("signinPassword").value;
if (!handle) { err($("signinErr"), "enter your name"); return; }
if (!password) { err($("signinErr"), "enter your password"); return; }
{
const btn = $("signinBtn"); const prev = btn.textContent;
btn.disabled = true; btn.textContent = "Signing in\u2026";
try {
@ -794,15 +819,19 @@ $("signinBtn").addEventListener("click", async () => {
} catch (e) {
err($("signinErr"), (e && e.message) || String(e));
} finally { btn.disabled = false; btn.textContent = prev; }
} else {
const mnemonic = $("mnemonicInput").value.trim();
if (!mnemonic || mnemonic.split(/\s+/).length < 12) {
err($("signinErr"), "paste a 12- or 24-word mnemonic"); return;
}
initWithOpts({ useVault: false, mnemonic });
}
});
// Submit: Recover with seed phrase
$("recoverBtn").addEventListener("click", () => {
err($("signinErr"), "");
const mnemonic = $("mnemonicInput").value.trim();
if (!mnemonic || mnemonic.split(/\s+/).length < 12) {
err($("signinErr"), "paste a 12- or 24-word mnemonic"); return;
}
initWithOpts({ useVault: false, mnemonic });
});
$("popOutBtn").addEventListener("click", () => bridge.invoke("open-messages"));
$("menuBtn").addEventListener("click", (e) => {
e.stopPropagation();