// Transforms the "🔑 Sign in" nav pill on every sirius.x page into a dropdown
// exposing the three wallet-onboarding options directly (New / Import /
// WizardConnect). Signed-in state is read from localStorage.'siriusProfile'
// (portal.html writes/clears it on sign-in / sign-out) — when present, the
// menu switches to "profile" mode: address + sign-out link.
//
// One include line per page:
// (path is absolute to silentmode.st root; the BCNR gateway serves the same URL).
(() => {
const NS = "silentmode.st";
// Find the .portal anchor already in the nav (the "Sign in" pill).
const anchor = document.querySelector(".topnav a.portal");
if (!anchor) return; // page has no nav-portal button; nothing to do
// Reuse the anchor's own href as the base for the dropdown links. The nav
// sets it to the right relative path per page (./portal.html at root,
// ../portal.html in subdirs). An absolute /sirius-x/portal.html would
// break under the BCNR route (sirius.x/) — the origin is different, so
// the gateway hits Sia with a key that does not exist and returns
// NoSuchKey. Deriving from the anchor keeps every context correct.
const PORTAL_URL = anchor.getAttribute("href") || "./portal.html";
// Admin lives one directory up-or-over relative to the same root — same
// trick: replace the portal.html basename with admin/ on the resolved
// portal path. If the anchor is a hash or empty for some reason, fall
// back to a same-directory link.
const ADMIN_URL = PORTAL_URL.includes("portal.html")
? PORTAL_URL.replace(/portal\.html.*$/, "admin/")
: "./admin/";
// Read profile state — portal.html sets this after successful sign-in with
// { address, tokenAddress, signedInAt }; clears on sign-out. The read has to
// run every time the button label refreshes or the menu opens, because
// sign-in can happen on this same page (portal.html) or in another tab
// *after* this script's initial run.
const readProfile = () => {
try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; }
};
// Build wrapper + button + (empty) menu around the existing pill.
const wrap = document.createElement("div");
wrap.className = "profile-wrap";
const btn = document.createElement("button");
btn.type = "button";
btn.className = anchor.className; // reuse .portal (and .here if present)
btn.setAttribute("aria-haspopup", "menu");
btn.setAttribute("aria-expanded", "false");
const menu = document.createElement("div");
menu.className = "profile-menu";
menu.setAttribute("role", "menu");
menu.hidden = true;
// Re-renders the button label AND the menu innerHTML from the current
// profile in localStorage. Called at load, on every open(), and whenever
// another tab updates the storage key.
function render() {
const profile = readProfile();
btn.textContent = profile ? "🔑 " + shortAddr(profile.address) + " ▾" : "🔑 Wallet ▾";
if (profile) {
menu.innerHTML = `
📋 My names
🛠Admin
🚪 Sign out
`;
} else {
menu.innerHTML = `
🆕 New wallet generate a fresh seed
📥 Add wallet import a seed phrase
🔗 WizardConnect Cashonize / Paytaca
🔑 Open sign-in page →
`;
}
}
render();
// Toggle + close-on-outside. Re-render on open so a sign-in that happened
// after page load (same tab: portal.html; other tab: storage event) shows
// up without a full reload.
const open = () => { render(); menu.hidden = false; btn.setAttribute("aria-expanded", "true"); };
const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
// Storage events fire only in *other* tabs, but they cover the cross-tab case.
// Same-tab: portal.html dispatches "siriusProfileChanged" on window after
// it writes/clears the key; render on that too.
window.addEventListener("storage", (e) => { if (e.key === "siriusProfile") render(); });
window.addEventListener("siriusProfileChanged", render);
btn.addEventListener("click", (e) => { e.stopPropagation(); menu.hidden ? open() : close(); });
document.addEventListener("click", (e) => { if (!wrap.contains(e.target)) close(); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
// Sign-out clears localStorage and reloads (portal.html re-reads state).
menu.addEventListener("click", (e) => {
const t = e.target.closest("[data-action]");
if (t?.dataset.action === "signout") {
e.preventDefault();
try { localStorage.removeItem("siriusProfile"); } catch {}
// Also reload the current page so any signed-in UI resets.
location.reload();
}
});
wrap.appendChild(btn);
wrap.appendChild(menu);
anchor.replaceWith(wrap);
// Inject the dropdown's CSS once. Kept inline so a single script tag is
// the whole install; consumers don't need a matching stylesheet.
if (!document.getElementById("profile-menu-css")) {
const s = document.createElement("style");
s.id = "profile-menu-css";
s.textContent = `
.topnav .profile-wrap { position: relative; margin-left: auto; }
.topnav .profile-wrap button.portal { cursor: pointer; font-family: inherit; }
.topnav .profile-menu {
position: absolute; right: 0; top: calc(100% + 6px);
min-width: 240px; z-index: 30;
background: rgba(20, 26, 36, .98); backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,.12); border-radius: 12px;
padding: 8px; display: flex; flex-direction: column; gap: 2px;
box-shadow: 0 8px 32px rgba(0,0,0,.4);
}
.topnav .profile-menu[hidden] { display: none; }
.topnav .profile-menu > a {
display: flex; flex-direction: column; gap: 2px;
padding: 8px 10px; border-radius: 8px;
color: #e7eaf1; text-decoration: none; font-size: 14px;
}
.topnav .profile-menu > a:hover { background: rgba(214,255,61,.10); color: #d6ff3d; }
.topnav .profile-menu .mi-hint {
color: #5e6678; font-size: 11.5px; font-weight: 400; letter-spacing: 0;
}
.topnav .profile-menu hr {
border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 4px 2px;
}
.topnav .profile-menu .profile-header {
padding: 6px 10px 10px; display: flex; flex-direction: column; gap: 4px;
border-bottom: 1px solid rgba(255,255,255,.08); margin-bottom: 4px;
}
.topnav .profile-menu .profile-hint {
color: #8b98a9; font-size: 11px; text-transform: uppercase; letter-spacing: .4px;
}
.topnav .profile-menu code {
font-family: ui-monospace, monospace; font-size: 11.5px;
color: #e7eaf1; word-break: break-all; background: transparent;
}
`;
document.head.appendChild(s);
}
// ---------- helpers ----------
function shortAddr(a) {
if (!a) return "wallet";
const i = a.indexOf(":");
const body = i > 0 ? a.slice(i + 1) : a;
return body.slice(0, 6) + "…" + body.slice(-4);
}
function escapeAttr(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[c]));
}
})();