theseus/chrome.html
Local Dev 75e468ae39 Theseus UX polish: BCDN/BCNR terminology + soft-mode prompt redesign
Terminology (user-facing labels now match the product/registry distinction):
- BCDN = Bitcoin Cash Domain Names (the product — what you view)
- BCNR = Bitcoin Cash Name Registry (the on-chain system that backs it)
Chrome badge, popover, Settings section and 'Open with…' prompt all show BCDN
for a resolved name. BCNR stays only where the registry itself is the subject.

'Open with…' prompt (collision.html):
- Larger window (640x520, was 480x340)
- Radio-select pattern with explicit Open / Cancel buttons (was auto-fire on click)
- Enter = Open, Esc = Cancel, keyboard-first
- Body copy per operator spec (no product-name-in-parens; blockchain-generic)
- Card titles kept short: 'BCNR / BCDN' + 'ICANN / IANA'

Address-bar chip:
- Says BCDN (not BCNR) for on-chain names; ICANN for web
- Dropped the '·.tld' suffix — TLD is already in the URL bar
- On collision candidates: shows BOTH chips (BCDN | ICANN) with active
  highlighted; clicking the inactive one flips the tab (via collision-switch)

Settings section renamed Naming → Registries. Copy rewritten in the same
BCDN/BCNR frame; policy labels are now 'BCDN first' / 'ICANN first' / 'Ask
each time'.

Passive 'also on BCNR' bar copy updated to BCDN.

Verified: all preload/settings/main/chrome JS + HTML parse; no secrets in
staging.
2026-08-02 13:02:47 +02:00

340 lines
21 KiB
HTML

<!doctype html>
<html>
<head><meta charset="utf-8">
<style>
:root {
color-scheme: light dark; font-family: system-ui, sans-serif;
--bg:#0f1420; --surface:#1b2330; --surface2:#171e2a; --active:#263041cc;
--ink:#e7eaf1; --mut:#b9c2d0; --dim:#8b98a9; --faint:#5e6678;
--line:rgba(255,255,255,.20); --line2:rgba(255,255,255,.13); --hover:rgba(255,255,255,.08);
}
@media (prefers-color-scheme: light) {
:root {
--bg:#e9ecf1; --surface:#ffffff; --surface2:#f1f3f7; --active:#dbe1ec;
--ink:#1a1f28; --mut:#3c4453; --dim:#697280; --faint:#98a1b0;
--line:rgba(0,0,0,.18); --line2:rgba(0,0,0,.11); --hover:rgba(0,0,0,.06);
}
}
[hidden] { display: none !important; }
body { margin: 0; height: 100vh; background: var(--bg); color: var(--ink); user-select: none; overflow: hidden; }
.tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; height: 34px; }
.tab { display: flex; align-items: center; gap: 8px; max-width: 200px; min-width: 90px; padding: 6px 10px;
background: var(--surface); border: 1px solid var(--line2); border-bottom: none; border-radius: 8px 8px 0 0;
font-size: 12.5px; cursor: default; color: var(--mut); }
.tab.active { background: var(--active); color: var(--ink); }
.tab .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.tab .spin { width: 10px; height: 10px; flex: none; border: 1.5px solid #ffffff2e; border-top-color: #d6ff3d; border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.tab .x { opacity: .5; cursor: pointer; padding: 0 2px; border-radius: 4px; }
.tab .x:hover { opacity: 1; background: var(--line2); }
.newtab { padding: 4px 10px; cursor: pointer; color: var(--dim); border-radius: 6px; font-size: 16px; }
.newtab:hover { background: var(--hover); color: var(--ink); }
.bar { display: flex; gap: 6px; align-items: center; padding: 6px 10px; }
.nav { display: flex; gap: 1px; }
.ic { width: 34px; height: 32px; display: grid; place-items: center; border-radius: 8px; cursor: pointer;
color: var(--mut); background: transparent; border: none; padding: 0; }
.ic svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.ic:hover { background: var(--line2); color: var(--ink); }
.ic:active { background: var(--line); }
.ic:disabled { opacity: .28; cursor: default; background: transparent; }
/* downloads button — a small badge sits over the icon when there's activity */
.dlbtn { position: relative; }
.dlbtn.spin::before { content: ""; position: absolute; inset: 4px; border-radius: 999px;
border: 1.5px solid transparent; border-top-color: #4b7bec; animation: dlspin .9s infinite linear; }
@keyframes dlspin { to { transform: rotate(360deg); } }
.dlbadge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px;
background: #4b7bec; color: #fff; border-radius: 7px; font-size: 9.5px; font-weight: 700;
display: grid; place-items: center; line-height: 1; box-shadow: 0 0 0 2px var(--bg); }
.dlbtn.done .dlbadge { background: #4fd1a5; }
.dlbtn.err .dlbadge { background: #f6768a; }
/* indeterminate loading bar under the toolbar — collapses when idle so it
doesn't take a permanent 2px strip below the address bar */
.loadbar { height: 0; overflow: hidden; background: transparent; transition: height .12s linear; }
.loadbar.on { height: 2px; }
.loadbar.on::after { content: ""; display: block; height: 100%; width: 35%; border-radius: 2px;
background: linear-gradient(90deg, transparent, #4b7bec, #d6ff3d, transparent); animation: loadslide 1.1s infinite linear; }
@keyframes loadslide { 0% { transform: translateX(-110%); } 100% { transform: translateX(400%); } }
/* address bar */
.urlwrap { flex: 1; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 6px 0 4px; }
.urlwrap:focus-within { border-color: #4b7bec; }
/* Firefox-style padlock — always present (the "site security" button) */
.secbadge { display: grid; place-items: center; width: 28px; height: 26px; border-radius: 6px; border: none;
background: transparent; cursor: pointer; padding: 0; }
.secbadge:hover { background: var(--line2); }
.secbadge svg { width: 15px; height: 15px; }
.secbadge svg .shk { fill: none; stroke: var(--dim); stroke-width: 1.25; stroke-linecap: round; }
.secbadge svg .bdy { fill: var(--dim); }
.secbadge svg .kh { fill: var(--surface); }
.secbadge:hover svg .kh { fill: var(--surface); }
.secbadge.warn svg .shk { stroke: #f6ad55; } .secbadge.warn svg .bdy { fill: #f6ad55; }
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
#url { flex: 1; }
/* registry indicator at the END of the address bar — one or two chips */
#reg { display: inline-flex; gap: 4px; align-items: center; }
.reg { font-size: 10.5px; letter-spacing: .03em; padding: 2px 9px; border-radius: 999px; white-space: nowrap;
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; cursor: default; user-select: none; }
.reg.icann { background: rgba(76,158,255,.13); color: #4c9eff; border-color: rgba(76,158,255,.28); }
/* collision candidate: two chips shown together; inactive one is dim + clickable */
.reg.switch { cursor: pointer; opacity: .55; }
.reg.switch:hover { opacity: 1; }
.star { border: none; background: transparent; cursor: pointer; color: var(--dim); font-size: 15px; padding: 2px 4px; border-radius: 6px; }
.star:hover { background: var(--line2); color: var(--ink); } .star.on { color: #ffd23d; }
/* search box: engine icon (click = pick engine) + a wide typing area */
.searchbox { display: flex; align-items: center; width: 300px; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 12px 0 3px; }
.searchbox:focus-within { border-color: #4b7bec; }
/* dropdown button shows the SELECTED engine's favicon (emoji fallback) + a caret */
.engine-btn { display: flex; align-items: center; gap: 3px; height: 30px; border: none; cursor: pointer;
border-radius: 999px 0 0 999px; padding: 0 4px 0 8px; background: transparent; }
.engine-btn:hover { background-color: var(--hover); }
.engine-btn #engineIcon { display: grid; place-items: center; width: 16px; height: 16px; }
.engine-btn .eimg { width: 16px; height: 16px; border-radius: 3px; }
.engine-btn .esym { font-size: 14px; line-height: 1; }
.engine-btn .cv { font-size: 8px; color: var(--dim); }
#search { flex: 1; padding: 7px 4px; }
.tor { padding: 7px 10px; border-radius: 8px; border: 1px solid var(--line); background: #2b2f3a; color: var(--ink); cursor: pointer; white-space: nowrap; font-size: 12.5px; }
.tor.connecting { background: #7a5c14; } .tor.on { background: #6b3fa0; }
.logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d6ff3d; white-space: nowrap;
padding: 6px 12px; border: 1px solid #d6ff3d33; border-radius: 8px; cursor: pointer; background: rgba(214,255,61,.08); font-size: 13px; }
.logo:hover { background: rgba(214,255,61,.16); }
.logo .gear { font-size: 12px; opacity: .8; }
/* favorites bar — new-tab page only, single compact row */
.bookmarks { display: flex; align-items: center; gap: 4px; padding: 3px 10px 5px; height: 26px; overflow: hidden; }
.bm { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 3px 9px; border-radius: 6px;
background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 12px; cursor: pointer; white-space: nowrap; }
.bm:hover { background: var(--active); color: var(--ink); }
.bm .bt { overflow: hidden; text-overflow: ellipsis; }
.bm .bx { opacity: 0; cursor: pointer; font-size: 11px; } .bm:hover .bx { opacity: .55; }
.bm .bx:hover { opacity: 1; color: #f6768a; }
.bm-empty { color: var(--faint); font-size: 11.5px; white-space: nowrap; }
.tordisc { font-size: 11.5px; color: #d9c7f2; background: #2a1c40; border-top: 1px solid #6b3fa055; padding: 5px 14px; }
.tordisc a { color: #d6ff3d; }
.bcnrbar { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: var(--mut);
background: #1a2417; border-top: 1px solid #d6ff3d33; padding: 6px 14px; }
.bcnrbar .breg { font-size: 10px; letter-spacing: .03em; padding: 1px 7px; border-radius: 999px;
background: rgba(214,255,61,.14); color: #d6ff3d; border: 1px solid #d6ff3d33; }
.bcnrbar b { color: var(--ink); }
.bcnrbar .bopen { margin-left: auto; padding: 4px 11px; border-radius: 7px; border: 1px solid #d6ff3d55;
background: rgba(214,255,61,.12); color: #eaffb0; cursor: pointer; font-size: 11.5px; }
.bcnrbar .bopen:hover { background: rgba(214,255,61,.22); }
.bcnrbar .bdismiss { color: var(--dim); text-decoration: none; } .bcnrbar .bdismiss:hover { color: var(--ink); }
</style></head>
<body>
<div class="tabs" id="tabs"></div>
<div class="bar">
<div class="nav">
<button class="ic" id="back" title="Back"><svg viewBox="0 0 16 16"><path d="M10 3 L5 8 L10 13"/></svg></button>
<button class="ic" id="fwd" title="Forward"><svg viewBox="0 0 16 16"><path d="M6 3 L11 8 L6 13"/></svg></button>
<button class="ic" id="reload" title="Reload"><svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg></button>
<button class="ic" id="home" title="Home"><svg viewBox="0 0 16 16"><path d="M2.5 7.5 L8 3 L13.5 7.5"/><path d="M4 6.7 L4 13 L12 13 L12 6.7"/></svg></button>
</div>
<div class="urlwrap">
<button class="secbadge" id="secbadge" title="Site information">
<svg viewBox="0 0 16 16" aria-hidden="true"><path class="shk" d="M5.25 7.2 V5.4 a2.75 2.75 0 0 1 5.5 0 V7.2"/><rect class="bdy" x="3.25" y="7.2" width="9.5" height="6.3" rx="1.7"/><circle class="kh" cx="8" cy="10.3" r=".95"/></svg>
</button>
<input id="url" placeholder="Ask a search engine or enter web address" spellcheck="false">
<span class="reg" id="reg" hidden></span>
<button class="star" id="star" title="Save this page"></button>
</div>
<div class="searchbox">
<button id="engineBtn" class="engine-btn" title="Choose search engine"><span id="engineIcon"></span><span class="cv"></span></button>
<input id="search" placeholder="Search" spellcheck="false">
</div>
<button class="ic dlbtn" id="downloads" title="Downloads">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 V10 M4.5 6.5 L8 10 L11.5 6.5"/><path d="M3 12.5 L3 13.5 L13 13.5 L13 12.5"/></svg>
<span class="dlbadge" id="dlbadge" hidden>0</span>
</button>
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button>
</div>
<div class="loadbar" id="loadbar"></div>
<div class="bookmarks" id="bookmarks" hidden></div>
<div id="tordisc" class="tordisc" hidden>
Onion mode hides your IP from sites and your ISP (including .bch lookups).
It is <b>not full anonymity</b> — this browser can still be fingerprinted; for that use the Tor Browser.
<a href="#" id="tordismiss">got it</a>
</div>
<div id="bcnrbar" class="bcnrbar" hidden>
<span class="breg" id="bcnrreg">BCDN</span>
<span><b id="bcnrhost"></b> also exists on <b>BCDN</b> — the decentralized registry.</span>
<button id="bcnropen" class="bopen">Open on BCDN</button>
<a href="#" id="bcnrdismiss" class="bdismiss">dismiss</a>
</div>
<script>
const $ = (id) => document.getElementById(id);
const T = window.theseus;
function syncHeight() { requestAnimationFrame(() => T.setChromeHeight(document.body.scrollHeight)); }
const RELOAD_SVG = `<svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg>`;
const STOP_SVG = `<svg viewBox="0 0 16 16"><path d="M4.5 4.5 L11.5 11.5 M11.5 4.5 L4.5 11.5"/></svg>`;
function goURL() { const v = $("url").value.trim(); if (v) T.navigate(v); }
$("url").addEventListener("keydown", (e) => { if (e.key === "Enter") goURL(); });
$("search").addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = $("search").value.trim(); if (q) { T.search(q); $("search").value = ""; } } });
$("back").onclick = () => T.back();
$("fwd").onclick = () => T.forward();
$("reload").onclick = () => T.reload();
$("home").onclick = () => T.goHome();
$("logo").onclick = () => T.openSettings();
$("secbadge").onclick = () => { const r = $("secbadge").getBoundingClientRect(); T.toggleSiteInfo({ x: Math.round(r.left), y: Math.round(r.bottom + 4) }); };
// ---- search-engine picker: the magnifier button opens a custom overlay
// dropdown (real favicons); here we only track the current engine's name. ----
function setEnginePlaceholder(d) {
const cur = (d.engines || []).find((e) => e.id === d.current);
$("search").placeholder = cur ? "Search with " + cur.name : "Search";
const icon = $("engineIcon");
const sym = (cur && cur.sym) || "🔍";
if (cur && cur.favicon)
icon.innerHTML = `<img class="eimg" src="${cur.favicon.replace(/"/g, "&quot;")}" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'esym',textContent:'${sym}'}))">`;
else icon.innerHTML = `<span class="esym">${sym}</span>`;
}
$("engineBtn").onclick = () => {
const r = $("engineBtn").getBoundingClientRect();
T.toggleEnginePicker({ x: Math.round(r.left), y: Math.round(r.bottom + 4) });
};
T.getSearchEngines().then(setEnginePlaceholder);
T.onEngines && T.onEngines(setEnginePlaceholder);
// ---- downloads button: badge + click toggles the panel ----
const dlBtn = $("downloads"), dlBadge = $("dlbadge");
function renderDownloads(items) {
const list = items || [];
const active = list.filter((d) => d.state === "progressing").length;
const doneNew = list.filter((d) => d.state === "completed").length;
const failed = list.filter((d) => d.state === "interrupted").length;
dlBtn.classList.toggle("spin", active > 0);
dlBtn.classList.toggle("done", active === 0 && doneNew > 0 && failed === 0);
dlBtn.classList.toggle("err", active === 0 && failed > 0);
const badge = active || (doneNew + failed);
if (badge > 0) { dlBadge.hidden = false; dlBadge.textContent = badge > 99 ? "99+" : String(badge); }
else { dlBadge.hidden = true; }
}
dlBtn.onclick = () => {
const r = dlBtn.getBoundingClientRect();
T.toggleDownloads({ x: Math.round(r.right - 340), y: Math.round(r.bottom + 4) });
};
T.getDownloads && T.getDownloads().then(renderDownloads);
T.onDownloads && T.onDownloads(renderDownloads);
// ---- favorites bar (new-tab page only) ----
let current = { url: "", title: "" }, bookmarks = [];
function renderBookmarks() {
const box = $("bookmarks");
box.innerHTML = bookmarks.length
? bookmarks.map((b, i) => `<div class="bm" data-i="${i}" title="${(b.url||"").replace(/"/g,"&quot;")}"><span class="bt">${(b.title||b.url).replace(/</g,"&lt;")}</span><span class="bx" data-x="${i}">✕</span></div>`).join("")
: `<span class="bm-empty">No saved pages yet — click ☆ in the address bar to save one.</span>`;
box.querySelectorAll(".bm").forEach((el) => el.onclick = (e) => {
const i = Number(el.dataset.i);
if (e.target.dataset.x !== undefined) T.removeBookmark(bookmarks[i].url);
else T.navigate(bookmarks[i].url);
});
updateStar(); syncHeight();
}
function updateStar() {
const saved = !!current.url && bookmarks.some((b) => b.url === current.url);
const s = $("star"); s.textContent = saved ? "★" : "☆"; s.classList.toggle("on", saved);
s.title = saved ? "Remove from saved pages" : "Save this page";
}
$("star").onclick = () => {
if (!current.url) return;
if (bookmarks.some((b) => b.url === current.url)) T.removeBookmark(current.url);
else T.addBookmark({ title: current.title || current.url, url: current.url });
};
T.getBookmarks().then((b) => { bookmarks = b || []; renderBookmarks(); });
T.onBookmarks((b) => { bookmarks = b || []; renderBookmarks(); });
// ---- security badge + minimal registry indicator ----
function setBadge(d) {
const b = $("secbadge");
let cls = "secbadge", title = "Site information";
if (!d || d.kind === "home") title = "Theseus";
else if (d.kind === "ok") title = "Secure · BCDN via " + (d.source || "on-chain") + " — click for details";
else if (d.kind === "web") title = "Connection · ICANN — click for details";
else if (d.kind === "resolving") title = "Resolving…";
else if (d.kind === "nxdomain") { cls = "secbadge warn"; title = "Not registered on BCDN"; }
else if (d.kind === "error") { cls = "secbadge warn"; title = "Resolution error"; }
b.className = cls; b.title = title;
}
function setReg(d) {
const r = $("reg");
if (!d || d.kind === "home" || d.kind === "resolving") { r.hidden = true; r.innerHTML = ""; return; }
r.hidden = false;
r.className = "reg-wrap"; // container; individual chips carry their own classes
// Two chips when this host is a collision candidate — could be BCDN OR ICANN.
// The active one is highlighted; the other is clickable to switch.
const isCand = !!d.tld && (d.kind === "ok" || d.kind === "web") && d.bcnrNativeTld === false;
const active = d.kind === "ok" ? "bcdn" : d.kind === "web" ? "icann" : null;
const bcdn = `<span class="reg${active === "bcdn" ? "" : " switch"}" data-c="bcdn" title="${active === "bcdn" ? "Open with BCDN (current)" : "Switch to BCDN"}">BCDN</span>`;
const icann = `<span class="reg icann${active === "icann" ? "" : " switch"}" data-c="icann" title="${active === "icann" ? "Open with ICANN (current)" : "Switch to ICANN"}">ICANN</span>`;
if (isCand) {
r.innerHTML = bcdn + icann;
for (const chip of r.querySelectorAll(".reg.switch")) {
chip.onclick = () => T.collisionSwitch({ choice: chip.dataset.c, remember: "no" });
}
} else if (d.kind === "web") {
r.innerHTML = `<span class="reg icann">ICANN</span>`;
} else {
r.innerHTML = `<span class="reg">BCDN</span>`;
}
}
// ---- Tor ----
let torShown = false;
$("tor").onclick = () => { T.toggleTor(); if (!torShown) { $("tordisc").hidden = false; torShown = true; syncHeight(); } };
$("tordismiss").onclick = (e) => { e.preventDefault(); $("tordisc").hidden = true; syncHeight(); };
// ---- passive BCNR offer ----
T.onBcnrOffer((d) => {
const bar = $("bcnrbar");
if (d && d.host) { $("bcnrhost").textContent = d.host; $("bcnrreg").textContent = "BCDN"; bar.hidden = false; }
else bar.hidden = true;
syncHeight();
});
$("bcnropen").onclick = () => { $("bcnrbar").hidden = true; syncHeight(); T.switchToBcnr(); };
$("bcnrdismiss").onclick = (e) => { e.preventDefault(); $("bcnrbar").hidden = true; syncHeight(); };
T.onTor((d) => {
const b = $("tor"); b.className = "tor " + (d.state === "on" ? "on" : d.state === "connecting" ? "connecting" : "");
b.textContent = d.state === "on" ? "🧅 Tor: On" : d.state === "connecting" ? "🧅 connecting…" : "🧅 Tor: Off";
});
// ---- tabs ----
T.onTabs((d) => {
$("back").disabled = !d.canBack; $("fwd").disabled = !d.canForward;
// loading indicator: progress bar + reload↔stop button
$("loadbar").classList.toggle("on", !!d.loading);
const rb = $("reload");
if (d.loading) { rb.innerHTML = STOP_SVG; rb.title = "Stop"; rb.onclick = () => T.stop(); }
else { rb.innerHTML = RELOAD_SVG; rb.title = "Reload"; rb.onclick = () => T.reload(); }
if (document.activeElement !== $("url")) $("url").value = d.url || "";
current.url = d.url || "";
const active = d.tabs.find((t) => t.active);
current.title = active ? active.title : "";
updateStar();
const box = $("tabs");
box.innerHTML = d.tabs.map((t) =>
`<div class="tab ${t.active ? "active" : ""}" data-id="${t.id}">${t.loading ? '<span class="spin"></span>' : ''}<span class="t">${(t.title||"New Tab").replace(/</g,"&lt;")}</span><span class="x" data-close="${t.id}">✕</span></div>`).join("") +
`<span class="newtab" id="newtab">+</span>`;
box.querySelectorAll(".tab").forEach((el) => el.onclick = (e) => {
if (e.target.dataset.close) T.closeTab(Number(e.target.dataset.close));
else T.switchTab(Number(el.dataset.id));
});
$("newtab").onclick = () => T.newTab();
});
// ---- provenance ----
T.onNav((d) => {
setBadge(d); setReg(d);
if (document.activeElement !== $("url")) {
if (d.kind === "home") $("url").value = "";
else if (d.host && !$("url").value) $("url").value = d.host;
}
// Favorites bar shows only on the new-tab / home page (hides once a page loads).
$("bookmarks").hidden = !(!d || d.kind === "home");
syncHeight();
});
syncHeight();
</script>
</body>
</html>