The approval box had no height limit inside a fixed mask, so a long message or many rows pushed the end of the text and the buttons out of view. The box now scrolls, long values scroll in place, and the buttons stay pinned at the bottom.
115 lines
7.7 KiB
HTML
115 lines
7.7 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Approval</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;
|
|
/* Darker acid for light backgrounds — ~5.5:1 on white. */
|
|
--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(460px, 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; letter-spacing: .1px; margin: 0 0 10px; }
|
|
.origin { display: inline-flex; align-items: center; gap: 8px; max-width: 100%;
|
|
background: var(--surface2); border: 1px solid rgb(from var(--acid) r g b / .35); color: var(--acid);
|
|
border-radius: 8px; padding: 6px 10px; margin-bottom: 12px;
|
|
font: 13px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; word-break: break-all; }
|
|
.origin .lbl { color: var(--dim); font: 11px system-ui, sans-serif; white-space: nowrap; }
|
|
.body { white-space: pre-wrap; color: var(--ink); margin-bottom: 12px; }
|
|
.rows { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; margin-bottom: 12px;
|
|
background: var(--surface2); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
|
|
.rows .k { color: var(--dim); font-size: 12px; white-space: nowrap; }
|
|
.rows .v { word-break: break-all; }
|
|
.rows .v.mono { font: 12.5px/1.4 ui-monospace, "Cascadia Code", Consolas, monospace; }
|
|
.rows .v.strong { font-weight: 650; font-size: 14px; }
|
|
/* A long message or a long list of rows scrolls inside the box; the
|
|
origin stays at the top and the buttons stay on screen. Without this a
|
|
dapp could push Approve and the end of a message out of view. */
|
|
.promptbox { max-height: calc(100vh - 64px); overflow-y: auto; }
|
|
.body { max-height: 30vh; overflow-y: auto; }
|
|
.rows .v { max-height: 40vh; overflow-y: auto; white-space: pre-wrap; }
|
|
.pact { position: sticky; bottom: -14px; background: var(--surface); padding: 8px 0 2px; }
|
|
label.chk { display: flex; align-items: center; gap: 8px; color: var(--mut); font-size: 12.5px; margin-bottom: 12px; cursor: pointer; }
|
|
label.chk select { flex: 1; padding: 5px 8px; border-radius: 6px; background: var(--surface2); color: var(--ink);
|
|
border: 1px solid var(--line); font: inherit; font-size: 12.5px; }
|
|
.pact { display: flex; gap: 6px; justify-content: flex-end; }
|
|
.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:hover { border-color: rgb(from var(--acid) r g b / .35); }
|
|
.pbtn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; font-weight: 650; }
|
|
.pbtn.danger { background: var(--danger); color: #0b0e14; border-color: transparent; font-weight: 650; }
|
|
.pbtn:focus-visible { outline: 2px solid rgb(from var(--acid) r g b / .6); outline-offset: 1px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
|
let current = null;
|
|
function finish(action, checked) {
|
|
if (!current) return;
|
|
const { reqId } = current;
|
|
const extra = document.getElementById("sel") ? document.getElementById("sel").value : "";
|
|
current = null;
|
|
document.body.innerHTML = "";
|
|
window.approval.pick(reqId, action, checked, extra);
|
|
}
|
|
window.approval.onShow((req) => {
|
|
current = req;
|
|
const rows = Array.isArray(req.rows) ? req.rows : [];
|
|
const actions = Array.isArray(req.actions) && req.actions.length ? req.actions : [{ id: "ok", label: "OK", primary: true }];
|
|
const hasCancel = actions.some((a) => a.id === "cancel");
|
|
document.body.innerHTML =
|
|
`<div class="promptmask"><div class="promptbox" role="dialog" aria-modal="true">
|
|
<div class="who"><span>🧩</span><span class="addon">${esc(req.addonName || req.addonId)}</span><span>·</span><span>asks for your approval</span></div>
|
|
<h1 class="title">${esc(req.title || "Approve?")}</h1>
|
|
${req.origin ? `<div class="origin"><span class="lbl">from</span><span>${esc(req.origin)}</span></div>` : ""}
|
|
${req.body ? `<div class="body">${esc(req.body)}</div>` : ""}
|
|
${rows.length ? `<div class="rows">${rows.map((r) => `<div class="k">${esc(r.label)}</div><div class="v${r.mono ? " mono" : ""}${r.strong ? " strong" : ""}">${esc(r.value)}</div>`).join("")}</div>` : ""}
|
|
${req.checkbox ? `<label class="chk"><input type="checkbox" id="chk"> ${esc(req.checkbox.label || "Always allow")}</label>` : ""}
|
|
${req.select ? `<label class="chk"><span>${esc(req.select.label)}</span><select id="sel">${req.select.options.map((o) => `<option value="${esc(o.value)}">${esc(o.label)}</option>`).join("")}</select></label>` : ""}
|
|
<div class="pact">
|
|
${hasCancel ? "" : `<button class="pbtn" type="button" data-id="cancel">Cancel</button>`}
|
|
${actions.map((a) => `<button class="pbtn${a.primary ? " primary" : ""}${a.danger ? " danger" : ""}" type="button" data-id="${esc(a.id)}">${esc(a.label || a.id)}</button>`).join("")}
|
|
</div>
|
|
</div></div>`;
|
|
const mask = document.querySelector(".promptmask");
|
|
mask.addEventListener("mousedown", (e) => { if (e.target === mask) finish("cancel", false); });
|
|
// Nothing but Cancel works for the first moment. A page can raise this
|
|
// overlay without a gesture and knows where the primary button will be,
|
|
// so "double-click here" put the second click on Send. A click has to
|
|
// arrive after the dialog has been on screen long enough to be seen.
|
|
const ARM_MS = 800;
|
|
const armAt = Date.now() + ARM_MS;
|
|
const acts = Array.from(document.querySelectorAll('button[data-id]')).filter((b) => b.dataset.id !== "cancel");
|
|
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-id]").forEach((b) => {
|
|
b.addEventListener("click", () => {
|
|
if (b.dataset.id !== "cancel" && Date.now() < armAt) return;
|
|
finish(b.dataset.id, !!document.getElementById("chk")?.checked);
|
|
});
|
|
});
|
|
// Focus the non-destructive default so Enter never blindly approves a
|
|
// spend; the user has to tab or click onto the primary action.
|
|
const first = document.querySelector('button[data-id="cancel"]') || document.querySelector("button");
|
|
setTimeout(() => first && first.focus(), 0);
|
|
});
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") finish("cancel", false); });
|
|
</script>
|
|
</body>
|
|
</html>
|