Vestibule/extension/content.js

128 lines
4.5 KiB
JavaScript
Executable File

// Vestibule content script — production implementation.
//
// Single responsibility: inject and manage the hidden menu overlay
// and the lock overlay. The 3px-from-top gesture is the direct
// descendant of the 2001 VB6 Form_MouseMove handler (Y <= 3).
const EDGE_PX = 3;
const HIDE_DELAY_MS = 1500;
let menuEl = null;
let hideTimer = null;
// ─── Menu construction (data-driven) ──────────────────────────────────
const MENU_ACTIONS = [
{ action: "back", label: "Back", symbol: "\u25C0", handler: () => history.back() },
{ action: "forward", label: "Forward", symbol: "\u25B6", handler: () => history.forward() },
{ action: "refresh", label: "Refresh", symbol: "\u21BB", handler: () => location.reload() },
{ action: "home", label: "Home", symbol: "\u2302", handler: () => browser.runtime.sendMessage({ type: "navigate-home" }).catch(() => {}) },
{ divider: true },
{ action: "unlock", label: "Unlock", symbol: "\uD83D\uDD12", handler: openUnlockPopup },
{ action: "admin", label: "Admin", symbol: "\u2699", handler: () => browser.runtime.sendMessage({ type: "open-admin" }).catch(() => {}) },
];
function buildMenu() {
const menu = document.createElement("div");
menu.id = "vestibule-menu";
menu.setAttribute("role", "toolbar");
menu.setAttribute("aria-label", "Vestibule controls");
MENU_ACTIONS.forEach((item) => {
if (item.divider) {
const span = document.createElement("span");
span.className = "vestibule-divider";
menu.appendChild(span);
return;
}
const btn = document.createElement("button");
btn.dataset.action = item.action;
btn.title = item.label;
btn.setAttribute("aria-label", item.label);
btn.textContent = item.symbol;
btn.addEventListener("click", () => {
item.handler();
hideMenu();
});
menu.appendChild(btn);
});
menu.addEventListener("mouseenter", () => clearTimeout(hideTimer));
menu.addEventListener("mouseleave", scheduleHide);
return menu;
}
function ensureMenu() {
if (menuEl && document.body.contains(menuEl)) return menuEl;
menuEl = buildMenu();
document.documentElement.appendChild(menuEl);
return menuEl;
}
// ─── Show / hide (step-down) ──────────────────────────────────────────
function showMenu() {
clearTimeout(hideTimer);
ensureMenu().classList.add("vestibule-visible");
}
function hideMenu() {
if (menuEl) menuEl.classList.remove("vestibule-visible");
}
function scheduleHide() {
clearTimeout(hideTimer);
hideTimer = setTimeout(hideMenu, HIDE_DELAY_MS);
}
// ─── Unlock popup ─────────────────────────────────────────────────────
function openUnlockPopup() {
browser.runtime.sendMessage({ type: "open-unlock-popup" }).catch(() => {});
}
// ─── Lock overlay ─────────────────────────────────────────────────────
function showLockOverlay() {
if (document.getElementById("vestibule-lock-overlay")) return;
const overlay = document.createElement("div");
overlay.id = "vestibule-lock-overlay";
overlay.innerHTML = `
<div class="vestibule-lock-card">
<h1>Session Reset</h1>
<p>This kiosk has been idle. Your browsing data has been cleared.</p>
<p>Click Unlock to start a new session.</p>
<button data-action="unlock">Unlock</button>
</div>
`;
overlay.addEventListener("click", (e) => {
if (!e.target.closest("button[data-action='unlock']")) return;
openUnlockPopup();
});
document.documentElement.appendChild(overlay);
}
function hideLockOverlay() {
const overlay = document.getElementById("vestibule-lock-overlay");
if (overlay) overlay.remove();
}
// ─── Event wiring ─────────────────────────────────────────────────────
document.addEventListener("mousemove", (e) => {
if (e.clientY <= EDGE_PX) showMenu();
}, { passive: true });
const RUNTIME_MESSAGE_HANDLERS = {
"show-lock-overlay": showLockOverlay,
"hide-lock-overlay": hideLockOverlay,
};
browser.runtime.onMessage.addListener((msg) => {
const handler = RUNTIME_MESSAGE_HANDLERS[msg.type];
if (handler) handler(msg);
});
console.log("[vestibule] content script loaded on", location.href);