150 lines
7.6 KiB
HTML
150 lines
7.6 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<title>Unlock</title>
|
||
|
|
<style>
|
||
|
|
:root { color-scheme: light dark;
|
||
|
|
--surface:#1c222c; --surface2:#0f1621; --line:rgba(255,255,255,.12);
|
||
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; --danger:#f6768a; }
|
||
|
|
@media (prefers-color-scheme: light) {
|
||
|
|
:root { --surface:#ffffff; --surface2:#f1f4fa; --line:rgba(0,0,0,.12);
|
||
|
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid: #0AC18E; }
|
||
|
|
}
|
||
|
|
* { 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,.45);
|
||
|
|
display: grid; place-items: start center; padding-top: 48px; }
|
||
|
|
.promptbox { background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
|
||
|
|
padding: 16px 18px 14px; width: min(380px, calc(100vw - 32px));
|
||
|
|
box-shadow: 0 20px 60px #000d; animation: pop .12s ease-out; }
|
||
|
|
@keyframes pop { from { transform: translateY(-6px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||
|
|
.who { display: flex; align-items: center; gap: 8px; color: var(--dim); font-size: 11.5px; margin-bottom: 8px; }
|
||
|
|
.who .addon { color: var(--mut); }
|
||
|
|
.title { font-size: 15px; font-weight: 650; margin: 0 0 4px; }
|
||
|
|
.reason { color: var(--mut); margin: 0 0 14px; }
|
||
|
|
.dots { display: flex; gap: 10px; justify-content: center; margin: 6px 0 14px; }
|
||
|
|
.dots i { width: 13px; height: 13px; border-radius: 50%; border: 2px solid var(--mut); }
|
||
|
|
.dots i.on { background: var(--acid); border-color: var(--acid); }
|
||
|
|
.pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 10px; }
|
||
|
|
.pad button { padding: 11px 0; font: 600 17px system-ui, sans-serif; border-radius: 9px; border: 1px solid var(--line);
|
||
|
|
background: var(--surface2); color: var(--ink); cursor: pointer; }
|
||
|
|
.pad button:hover { border-color: rgb(from var(--acid) r g b / .4); }
|
||
|
|
.pad button.muted { font-size: 13px; color: var(--mut); }
|
||
|
|
input[type=password] { width: 100%; padding: 9px 11px; border-radius: 8px; border: 1px solid var(--line);
|
||
|
|
background: var(--surface2); color: var(--ink); font: inherit; font-size: 14px; margin-bottom: 10px; }
|
||
|
|
input:focus, button:focus-visible { outline: 2px solid rgb(from var(--acid) r g b / .6); outline-offset: 1px; }
|
||
|
|
.err { color: var(--danger); font-size: 12.5px; min-height: 18px; margin-bottom: 6px; text-align: center; }
|
||
|
|
.pact { display: flex; gap: 6px; justify-content: space-between; align-items: center; }
|
||
|
|
.link { background: none; border: none; color: var(--mut); cursor: pointer; font: inherit; font-size: 12.5px; padding: 0; text-decoration: underline; }
|
||
|
|
.pbtn { padding: 7px 14px; border-radius: 7px; border: 1px solid var(--line); background: var(--surface2);
|
||
|
|
color: var(--ink); cursor: pointer; font: inherit; font-size: 12.5px; }
|
||
|
|
.pbtn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; font-weight: 650; }
|
||
|
|
.pbtn:disabled { opacity: .5; cursor: default; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<script>
|
||
|
|
// Built with createElement/textContent only: the add-on supplies the
|
||
|
|
// reason text, and it must never be able to inject markup into a prompt
|
||
|
|
// that asks for the master password.
|
||
|
|
const el = (tag, attrs = {}, ...kids) => {
|
||
|
|
const n = document.createElement(tag);
|
||
|
|
for (const [k, v] of Object.entries(attrs)) {
|
||
|
|
if (k === "class") n.className = v;
|
||
|
|
else if (k.startsWith("on")) n.addEventListener(k.slice(2), v);
|
||
|
|
else n.setAttribute(k, v);
|
||
|
|
}
|
||
|
|
for (const c of kids.flat()) if (c != null && c !== false) n.append(c instanceof Node ? c : String(c));
|
||
|
|
return n;
|
||
|
|
};
|
||
|
|
let req = null;
|
||
|
|
let mode = "pin";
|
||
|
|
let pin = "";
|
||
|
|
let busy = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
function done(cancelled) {
|
||
|
|
if (!req) return;
|
||
|
|
const id = req.reqId;
|
||
|
|
req = null;
|
||
|
|
document.body.replaceChildren();
|
||
|
|
if (cancelled) window.unlock.cancel(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function submit(value) {
|
||
|
|
if (!req || busy) return;
|
||
|
|
busy = true;
|
||
|
|
render();
|
||
|
|
const r = await window.unlock.submit(req.reqId, mode, value).catch((e) => ({ ok: false, error: e.message }));
|
||
|
|
busy = false;
|
||
|
|
if (r && r.ok) { req = null; document.body.replaceChildren(); return; }
|
||
|
|
error = (r && r.error) || "Could not unlock";
|
||
|
|
if (r && r.mode) mode = r.mode;
|
||
|
|
pin = "";
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
|
||
|
|
function press(d) {
|
||
|
|
if (busy) return;
|
||
|
|
if (d === "back") pin = pin.slice(0, -1);
|
||
|
|
else if (pin.length < 6) pin += d;
|
||
|
|
error = "";
|
||
|
|
render();
|
||
|
|
if (pin.length === 6) submit(pin);
|
||
|
|
}
|
||
|
|
|
||
|
|
function render() {
|
||
|
|
if (!req) return;
|
||
|
|
const head = [
|
||
|
|
el("div", { class: "who" }, el("span", {}, "🔒"), el("span", { class: "addon" }, req.addonName || "Theseus"), el("span", {}, "·"), el("span", {}, "asks to unlock your vault")),
|
||
|
|
el("h1", { class: "title" }, mode === "pin" ? "Enter your PIN" : "Enter your master password"),
|
||
|
|
req.reason ? el("p", { class: "reason" }, req.reason) : null,
|
||
|
|
];
|
||
|
|
let bodyEls;
|
||
|
|
if (mode === "pin") {
|
||
|
|
const dots = el("div", { class: "dots" }, ...Array.from({ length: 6 }, (_, i) => el("i", { class: i < pin.length ? "on" : "" })));
|
||
|
|
const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "back"];
|
||
|
|
const pad = el("div", { class: "pad" }, ...keys.map((k) => k === ""
|
||
|
|
? el("span")
|
||
|
|
: el("button", { type: "button", class: k === "back" ? "muted" : "", onclick: () => press(k), "aria-label": k === "back" ? "Delete" : k }, k === "back" ? "⌫" : k)));
|
||
|
|
bodyEls = [dots, el("div", { class: "err" }, error), pad,
|
||
|
|
el("div", { class: "pact" },
|
||
|
|
el("button", { class: "link", type: "button", onclick: () => { mode = "password"; error = ""; render(); } }, "Use master password"),
|
||
|
|
el("button", { class: "pbtn", type: "button", onclick: () => done(true) }, "Cancel"))];
|
||
|
|
} else {
|
||
|
|
const input = el("input", { type: "password", placeholder: "Master password", autocomplete: "current-password" });
|
||
|
|
const go = el("button", { class: "pbtn primary", type: "submit" }, busy ? "Checking…" : "Unlock");
|
||
|
|
if (busy) go.setAttribute("disabled", "");
|
||
|
|
const form = el("form", { onsubmit: (e) => { e.preventDefault(); if (input.value) submit(input.value); } }, input,
|
||
|
|
el("div", { class: "err" }, error),
|
||
|
|
el("div", { class: "pact" },
|
||
|
|
req.pinSet && !req.requireMaster && mode === "password" ? el("button", { class: "link", type: "button", onclick: () => { mode = "pin"; error = ""; render(); } }, "Use PIN") : el("span"),
|
||
|
|
el("div", { style: "display:flex;gap:6px" },
|
||
|
|
el("button", { class: "pbtn", type: "button", onclick: () => done(true) }, "Cancel"), go)));
|
||
|
|
bodyEls = [form];
|
||
|
|
setTimeout(() => input.focus(), 0);
|
||
|
|
}
|
||
|
|
document.body.replaceChildren(el("div", { class: "promptmask", onmousedown: (e) => { if (e.target.classList.contains("promptmask")) done(true); } },
|
||
|
|
el("div", { class: "promptbox", role: "dialog", "aria-modal": "true" }, ...head, ...bodyEls)));
|
||
|
|
}
|
||
|
|
|
||
|
|
window.unlock.onShow((r) => {
|
||
|
|
req = r;
|
||
|
|
mode = r.pinSet && !r.requireMaster ? "pin" : "password";
|
||
|
|
pin = "";
|
||
|
|
error = r.requireMaster && r.pinSet ? "Too many wrong PINs. Enter the master password." : "";
|
||
|
|
busy = false;
|
||
|
|
render();
|
||
|
|
});
|
||
|
|
document.addEventListener("keydown", (e) => {
|
||
|
|
if (!req) return;
|
||
|
|
if (e.key === "Escape") return done(true);
|
||
|
|
if (mode === "pin" && /^[0-9]$/.test(e.key)) press(e.key);
|
||
|
|
else if (mode === "pin" && e.key === "Backspace") press("back");
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|