theseus/bundled-addons/consent/panel.html
Local Dev d4b9de93a6 feat(theseus): Cookie Pop-ups — consent banners answered automatically
A bundled add-on that answers cookie consent dialogs, rejecting all but
the essentials by default (or accepting, if the user prefers the banner
simply gone), so pages open without one. Built on DuckDuckGo's
autoconsent (MPL-2.0): its rule bundle covers hundreds of consent
managers, and a reject-button heuristic handles unknown banners in
reject mode. The library runs through the page-inject slot in every
http(s) frame's isolated world; the add-on hands each frame the user's
settings and the rules, and counts what was handled per site for the
panel, which also excludes a site with one click.

build-inject.js assembles inject.js from the library in node_modules
plus the glue, and copies the compact rules and licence into the add-on
so a rule update can ship through the add-on channel.

Host side: page-inject scripts get theseus.evalInPage for the few rules
that need the page's own JavaScript (they already reach the page via
contextBridge, so no new trust tier), and api.tabs is open to
page-inject add-ons as well as request-filter ones.
2026-09-27 19:55:43 +02:00

105 lines
7.8 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cookie Pop-ups</title>
<style>
:root { color-scheme: light dark;
--bg:#0e131c; --panel:#141a24; --panel2:#191f2b; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#0ac18e; --warn:#f5a524; --danger:#ff5b5b; }
@media (prefers-color-scheme: light) {
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --warn:#d97706; }
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg); color: var(--ink); font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; display: flex; flex-direction: column; }
header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
header .pill { margin-left: auto; }
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; background: rgba(139,152,169,.14); color: var(--mut); }
.pill.on { color: #0b0e14; background: var(--acid); }
.pill.off { color: #fff; background: var(--danger); }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
.card { border: 1px solid var(--line); background: var(--panel); border-radius: 14px; padding: 16px; }
.site { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
.site .host { font: 600 13px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; color: var(--mut); overflow-wrap: anywhere; }
.site .num { font-size: 40px; font-weight: 700; line-height: 1.1; color: var(--warn); font-variant-numeric: tabular-nums; }
.site .num.muted { color: var(--dim); }
.site .lbl { color: var(--mut); }
.site .sub { color: var(--dim); font-size: 11.5px; }
.btn { border: 1px solid var(--line); background: var(--panel2); color: var(--ink); padding: 9px 16px; border-radius: 10px; font: inherit; font-weight: 600; cursor: pointer; }
.btn:hover { border-color: rgb(from var(--warn) r g b / .5); }
.btn.primary { background: var(--warn); color: #0b0e14; border-color: transparent; }
.btn.wide { margin-top: 10px; min-width: 200px; }
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.row + .row { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.row .k { font-weight: 600; }
.row .d { color: var(--dim); font-size: 11.5px; }
.row .v { color: var(--mut); font-variant-numeric: tabular-nums; }
.switch { position: relative; width: 40px; height: 22px; border-radius: 11px; background: var(--dim); border: none; cursor: pointer; flex: none; transition: background .15s; }
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .15s; }
.switch.on { background: var(--acid); } .switch.on::after { left: 20px; }
.modes { display: grid; gap: 8px; margin-top: 10px; }
.mode { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel2); cursor: pointer; }
.mode.sel { border-color: rgb(from var(--warn) r g b / .6); }
.mode input { margin-top: 3px; accent-color: var(--warn); }
.mode .t { font-weight: 600; } .mode .d { color: var(--dim); font-size: 11.5px; }
.note { color: var(--dim); font-size: 11.5px; line-height: 1.45; }
h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
</style>
</head>
<body>
<header><span>🍪</span><h1>Cookie Pop-ups</h1><span class="pill" id="pill"><span class="dot"></span><span id="pillText">…</span></span></header>
<div class="body">
<div class="card site" id="siteCard"><div class="lbl">Open a site to see what was handled there.</div></div>
<div class="card">
<div class="row">
<div><div class="k">Answer cookie pop-ups for me</div><div class="d">On every site, before the banner gets in the way.</div></div>
<button class="switch" id="enabled" role="switch" aria-checked="true" title="Turn on or off"></button>
</div>
<div class="modes" id="modes">
<label class="mode"><input type="radio" name="mode" value="optOut"><span><div class="t">Reject all but essentials</div><div class="d">Declines tracking and marketing cookies wherever the pop-up allows it. Recommended.</div></span></label>
<label class="mode"><input type="radio" name="mode" value="optIn"><span><div class="t">Just make it go away</div><div class="d">Accepts the defaults. Fastest, but you keep whatever the site wants to set.</div></span></label>
</div>
<div class="row" style="margin-top:12px; padding-top:12px; border-top:1px solid var(--line)">
<div><div class="k">Pop-ups handled</div><div class="d">Since the add-on was installed.</div></div>
<div class="v" id="total">0</div>
</div>
</div>
<div class="note" id="rules"></div>
<div class="note">Built on DuckDuckGo's autoconsent rules for hundreds of consent dialogs, with a fallback that looks for reject buttons on unknown ones. Some banners still need a click; if one keeps reappearing, exclude the site here.</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" })[c]);
let state = null;
function render(s) {
state = s;
$("pill").className = "pill " + (s.enabled ? "on" : "off");
$("pillText").textContent = s.enabled ? "On" : "Off";
$("enabled").classList.toggle("on", s.enabled); $("enabled").setAttribute("aria-checked", String(s.enabled));
for (const l of document.querySelectorAll(".mode")) { const on = l.querySelector("input").value === s.mode; l.classList.toggle("sel", on); l.querySelector("input").checked = on; }
$("total").textContent = Number(s.handledTotal || 0).toLocaleString();
$("rules").textContent = s.rulesVersion ? `Rules: autoconsent ${s.rulesVersion}.` : "";
const card = $("siteCard");
if (!s.site) card.innerHTML = `<div class="lbl">Open a site to see what was handled there.</div>`;
else if (s.site.skipped) card.innerHTML = `<div class="host">${esc(s.site.host)}</div><div class="num muted">—</div><div class="lbl">Excluded: pop-ups are left alone on this site.</div><button class="btn primary wide" id="siteBtn">Handle pop-ups here again</button>`;
else {
const n = s.site.handled;
card.innerHTML = `<div class="host">${esc(s.site.host)}</div><div class="num${n ? "" : " muted"}">${n}</div>
<div class="lbl">${n === 1 ? "pop-up answered on this site" : "pop-ups answered on this site"}${s.site.cmp ? ` · ${esc(s.site.cmp)}` : ""}</div>
${s.site.failed ? `<div class="sub">${s.site.failed} attempt(s) did not complete — the banner may need a click.</div>` : ""}
<button class="btn wide" id="siteBtn">Exclude this site</button>`;
}
const b = $("siteBtn"); if (b) b.onclick = () => window.silentmode.invoke("skip-site", { host: s.site.host, skipped: !s.site.skipped }).then(render);
}
$("enabled").onclick = () => window.silentmode.invoke("set-enabled", { enabled: !(state && state.enabled) }).then(render);
$("modes").addEventListener("change", (e) => { if (e.target.name === "mode") window.silentmode.invoke("set-mode", { mode: e.target.value }).then(render); });
window.silentmode.on("state", render);
window.silentmode.invoke("state").then(render);
window.silentmode.onVisibility((v) => { if (v) window.silentmode.invoke("state").then(render); });
</script>
</body>
</html>