bcnr.installExtension is in every page's main world, and install links were honoured from any page and any frame, with no user gesture. Any site could raise the install sheet timed so that a double-click landed on Install, whose two buttons are always in the same place; an installed community extension runs in the main process at once. The call and the links now work only from theseus.x's top frame, the call needs a real click (checked in the isolated world), and Theseus's own sheets ignore every choice except Cancel for 800 ms, like the approval overlay.
187 lines
13 KiB
HTML
187 lines
13 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Dialog</title>
|
|
<style>
|
|
/* Same surface tokens as approval.html so page dialogs and add-on approvals
|
|
read as one family; tones follow the notification-card pattern (icon in a
|
|
tinted circle, tinted primary action, quiet dismiss). */
|
|
:root { color-scheme: light dark;
|
|
--surface:#1c222c; --surface2:#0f1621; --line:rgba(255,255,255,.12);
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
|
--ok:#3ddc84; --warn:#f5a524; --danger:#f6768a; --info:#8b98a9; }
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --surface:#ffffff; --surface2:#f1f4fa; --line:rgba(0,0,0,.12);
|
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid:#0AC18E;
|
|
--ok:#16a34a; --warn:#d97706; --danger:#dc2626; --info:#5c6577; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; background: transparent; }
|
|
body { font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: var(--ink); }
|
|
.promptmask { position: fixed; inset: 0; background: rgba(0,0,0,.35);
|
|
display: grid; place-items: start center; padding-top: 14px; }
|
|
.card { --tone: var(--info);
|
|
position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
|
|
padding: 16px 16px 14px 16px; width: min(440px, calc(100vw - 32px)); max-height: calc(100vh - 28px); overflow: auto;
|
|
box-shadow: 0 18px 50px #000b; animation: drop .14s ease-out; }
|
|
.card.ok { --tone: var(--ok); } .card.warn { --tone: var(--warn); } .card.danger { --tone: var(--danger); }
|
|
@keyframes drop { from { transform: translateY(-8px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
.row { display: flex; gap: 12px; align-items: flex-start; }
|
|
.icon { flex: none; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
|
|
background: rgb(from var(--tone) r g b / .16); color: var(--tone); margin-top: 1px; }
|
|
.icon svg { width: 17px; height: 17px; }
|
|
.text { flex: 1; min-width: 0; padding-right: 22px; }
|
|
.who { color: var(--dim); font-size: 11.5px; margin: 0 0 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.who .src { color: var(--mut); font-weight: 600; }
|
|
.who .src.site { font: 600 11.5px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; }
|
|
.body { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font-size: 13.5px; font-weight: 500; max-height: 40vh; overflow: auto; }
|
|
.close { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; border: none; border-radius: 7px;
|
|
background: transparent; color: var(--dim); cursor: pointer; font-size: 15px; line-height: 1; }
|
|
.close:hover { background: var(--surface2); color: var(--ink); }
|
|
input.pin { display: block; width: 100%; margin: 10px 0 0; padding: 8px 10px; border-radius: 8px;
|
|
background: var(--surface2); color: var(--ink); border: 1px solid var(--line); font: inherit; font-size: 13px; }
|
|
input.pin:focus { outline: none; border-color: rgb(from var(--tone) r g b / .7); }
|
|
.pact { display: flex; gap: 8px; align-items: center; margin: 12px 0 0 42px; }
|
|
.pbtn { padding: 7px 16px; border-radius: 8px; border: 1px solid transparent; cursor: pointer; font: inherit; font-size: 12.5px; font-weight: 600; min-width: 84px; }
|
|
.pbtn.primary { background: rgb(from var(--tone) r g b / .18); color: var(--tone); border-color: rgb(from var(--tone) r g b / .35); }
|
|
.pbtn.primary:hover { background: rgb(from var(--tone) r g b / .28); }
|
|
.card.info .pbtn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; }
|
|
.card.info .pbtn.primary:hover { filter: brightness(1.05); }
|
|
.pbtn.quiet { background: transparent; color: var(--mut); }
|
|
.pbtn.quiet:hover { background: var(--surface2); color: var(--ink); }
|
|
.pbtn:focus-visible { outline: 2px solid rgb(from var(--tone) r g b / .6); outline-offset: 1px; }
|
|
/* Theseus's own prompts: an app/extension icon in place of the tone glyph,
|
|
a headline, the explanation under it, an optional checkbox. */
|
|
.icon img.appicon { width: 30px; height: 30px; border-radius: 8px; object-fit: cover; display: block; }
|
|
.icon.plain { background: transparent; }
|
|
.title { font-size: 14px; font-weight: 650; color: var(--ink); margin: 0 0 2px; }
|
|
.detail { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--mut); font-size: 12.5px; line-height: 1.45; margin-top: 4px; max-height: 40vh; overflow: auto; }
|
|
label.chk { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--mut); font-size: 12.5px; cursor: pointer; }
|
|
label.chk input { accent-color: var(--tone); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
|
// The page only gives us a sentence; read its tone from the words so a
|
|
// "Delete…?" looks destructive and a "Saved." looks like good news.
|
|
function toneFor(msg) {
|
|
const m = String(msg || "").toLowerCase();
|
|
if (/\b(error|fail|failed|failure|incorrect|invalid|wrong|cannot|can't|couldn't|unable|denied|delete|remove|discard|permanently|undone|lost|reject)\b/.test(m)) return "danger";
|
|
if (/\b(warning|caution|required|please provide|must|expire|expiring|expired|unsaved|leave|overwrite|replace)\b/.test(m)) return "warn";
|
|
if (/\b(success|successfully|saved|completed|complete|done|created|copied|sent|installed|updated|ready)\b/.test(m)) return "ok";
|
|
return "info";
|
|
}
|
|
const ICONS = {
|
|
ok: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5 L6.5 11.5 L12.5 5"/></svg>',
|
|
warn: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.2 L14.3 13.3 H1.7 Z"/><path d="M8 6.2 V9.6"/><circle cx="8" cy="11.8" r=".6" fill="currentColor"/></svg>',
|
|
danger: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="8" cy="8" r="6.3"/><path d="M8 4.6 V8.8"/><circle cx="8" cy="11.3" r=".6" fill="currentColor"/></svg>',
|
|
info: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="8" cy="8" r="6.3"/><path d="M8 7.2 V11.4"/><circle cx="8" cy="4.9" r=".6" fill="currentColor"/></svg>',
|
|
};
|
|
let current = null;
|
|
function finish(ok) {
|
|
if (!current) return;
|
|
const { reqId, kind } = current;
|
|
const input = document.getElementById("pin");
|
|
const value = kind === "prompt" ? (ok ? (input ? input.value : "") : null) : null;
|
|
current = null;
|
|
document.body.innerHTML = "";
|
|
window.jsdialog.answer(reqId, !!ok, value);
|
|
}
|
|
// One of Theseus's own prompts: title + explanation, the caller's buttons
|
|
// (default first), optional checkbox, an icon when the caller has one.
|
|
function finishApp(response) {
|
|
if (!current) return;
|
|
const { reqId, cancelId } = current;
|
|
const chk = document.getElementById("chk");
|
|
current = null;
|
|
document.body.innerHTML = "";
|
|
window.jsdialog.answer(reqId, response !== cancelId, { response, checkboxChecked: !!(chk && chk.checked) });
|
|
}
|
|
function showApp(req) {
|
|
const tone = req.type === "error" ? "danger" : req.type === "warning" ? "warn" : toneFor(req.title + " " + req.message);
|
|
const order = [req.defaultId, ...req.buttons.map((_, i) => i).filter((i) => i !== req.defaultId)];
|
|
document.body.innerHTML =
|
|
`<div class="promptmask"><div class="card ${tone}" role="dialog" aria-modal="true">
|
|
<button class="close" id="close" type="button" title="${esc(req.buttons[req.cancelId] || "Close")}" aria-label="Close">✕</button>
|
|
<div class="row">
|
|
<div class="icon${req.iconUrl ? " plain" : ""}">${req.iconUrl ? `<img class="appicon" src="${esc(req.iconUrl)}" alt="">` : ICONS[tone]}</div>
|
|
<div class="text">
|
|
<div class="who"><span class="src app">Theseus</span> ${req.buttons.length > 1 ? "asks" : "says"}</div>
|
|
${req.title && req.title !== req.message ? `<div class="title">${esc(req.message)}</div>` : `<div class="body">${esc(req.message)}</div>`}
|
|
${req.detail ? `<div class="detail">${esc(req.detail)}</div>` : ""}
|
|
${req.checkbox ? `<label class="chk"><input type="checkbox" id="chk"${req.checkbox.checked ? " checked" : ""}> ${esc(req.checkbox.label)}</label>` : ""}
|
|
</div>
|
|
</div>
|
|
<div class="pact">
|
|
${order.map((i) => `<button class="pbtn ${i === req.defaultId ? "primary" : "quiet"}" type="button" data-i="${i}">${esc(req.buttons[i])}</button>`).join("")}
|
|
</div>
|
|
</div></div>`;
|
|
// Armed like the approval overlay: for the first moments only the
|
|
// cancel choice works, so a click aimed at whatever was under the
|
|
// pointer when the sheet appeared (a page timing a double-click) cannot
|
|
// land on Install or another affirmative button.
|
|
const ARM_MS = 800;
|
|
const armAt = Date.now() + ARM_MS;
|
|
req.armAt = armAt;
|
|
const acts = Array.from(document.querySelectorAll("button[data-i]")).filter((b) => Number(b.dataset.i) !== req.cancelId);
|
|
acts.forEach((b) => { b.disabled = true; b.style.opacity = "0.45"; });
|
|
setTimeout(() => { if (current === req) acts.forEach((b) => { b.disabled = false; b.style.opacity = ""; }); }, ARM_MS);
|
|
document.querySelectorAll("button[data-i]").forEach((b) => b.addEventListener("click", () => {
|
|
if (Number(b.dataset.i) !== req.cancelId && Date.now() < armAt) return;
|
|
finishApp(Number(b.dataset.i));
|
|
}));
|
|
document.getElementById("close").addEventListener("click", () => finishApp(req.cancelId));
|
|
const mask = document.querySelector(".promptmask");
|
|
mask.addEventListener("mousedown", (e) => { if (e.target === mask) finishApp(req.cancelId); });
|
|
setTimeout(() => { const b = document.querySelector("button.primary"); b && b.focus(); }, 0);
|
|
}
|
|
window.jsdialog.onShow((req) => {
|
|
current = req;
|
|
if (req.kind === "app") return showApp(req);
|
|
const who = req.who || { label: "This page", kind: "site" };
|
|
const verb = req.kind === "prompt" ? "asks" : "says";
|
|
const tone = toneFor(req.message);
|
|
const okLabel = req.kind === "alert" ? "OK" : tone === "danger" && /\?\s*$/.test(String(req.message || "")) ? "Yes" : "OK";
|
|
document.body.innerHTML =
|
|
`<div class="promptmask"><div class="card ${tone}" role="${req.kind === "alert" ? "alertdialog" : "dialog"}" aria-modal="true">
|
|
<button class="close" id="close" type="button" title="${req.kind === "alert" ? "OK" : "Cancel"}" aria-label="Close">✕</button>
|
|
<div class="row">
|
|
<div class="icon">${ICONS[tone]}</div>
|
|
<div class="text">
|
|
<div class="who"><span class="src ${esc(who.kind)}" title="${esc(who.label)}">${esc(who.label)}</span> ${verb}</div>
|
|
<div class="body">${esc(req.message)}</div>
|
|
${req.kind === "prompt" ? `<input class="pin" id="pin" type="text" value="${esc(req.def ?? "")}" spellcheck="false">` : ""}
|
|
</div>
|
|
</div>
|
|
<div class="pact">
|
|
<button class="pbtn primary" type="button" id="ok">${okLabel}</button>
|
|
${req.kind === "alert" ? "" : `<button class="pbtn quiet" type="button" id="cancel">Cancel</button>`}
|
|
</div>
|
|
</div></div>`;
|
|
document.getElementById("ok").addEventListener("click", () => finish(true));
|
|
const c = document.getElementById("cancel"); if (c) c.addEventListener("click", () => finish(false));
|
|
document.getElementById("close").addEventListener("click", () => finish(req.kind === "alert"));
|
|
const mask = document.querySelector(".promptmask");
|
|
mask.addEventListener("mousedown", (e) => { if (e.target === mask) finish(req.kind === "alert"); });
|
|
const input = document.getElementById("pin");
|
|
setTimeout(() => { if (input) { input.focus(); input.select(); } else document.getElementById("ok").focus(); }, 0);
|
|
});
|
|
document.addEventListener("keydown", (e) => {
|
|
if (!current) return;
|
|
if (current.kind === "app") {
|
|
if (e.key === "Escape") { e.preventDefault(); finishApp(current.cancelId); }
|
|
else if (e.key === "Enter" && !(e.target && e.target.matches("button.quiet, #close"))) {
|
|
e.preventDefault();
|
|
if (current.defaultId !== current.cancelId && Date.now() < (current.armAt || 0)) return;
|
|
finishApp(current.defaultId);
|
|
}
|
|
return;
|
|
}
|
|
if (e.key === "Escape") { e.preventDefault(); finish(current.kind === "alert"); }
|
|
else if (e.key === "Enter" && !(e.target && (e.target.id === "cancel" || e.target.id === "close"))) { e.preventDefault(); finish(true); }
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|