Testing347's picture
Update assets/site.js
7540be3 verified
/* assets/site.js
SILENTPATTERN — shared behaviors (Vanta, modals, lab navigator)
IMPORTANT: This file must be pure JavaScript (no <script> tags inside).
*/
(function () {
"use strict";
function q(id) { return document.getElementById(id); }
function isShown(modal) {
return modal && !modal.classList.contains("modal-hidden");
}
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
function handler(e) {
if (e.key === "Tab") {
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
} else if (e.key === "Escape") {
// Close only THIS modal
toggleModal(modal, false);
}
}
modal.addEventListener("keydown", handler);
modal._focusHandler = handler;
}
function untrapFocus(modal) {
if (modal && modal._focusHandler) {
modal.removeEventListener("keydown", modal._focusHandler);
delete modal._focusHandler;
}
}
function toggleModal(modal, show) {
if (!modal) return;
if (show) {
modal.classList.remove("modal-hidden");
modal.classList.add("modal-visible");
modal.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
// Ensure focus + focus trap
setTimeout(() => {
try { modal.focus(); } catch {}
trapFocus(modal);
}, 0);
} else {
modal.classList.remove("modal-visible");
modal.classList.add("modal-hidden");
modal.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
untrapFocus(modal);
}
}
function initVanta() {
const el = q("vanta-bg");
if (!el || !window.VANTA || !window.VANTA.NET) return null;
const fx = window.VANTA.NET({
el: "#vanta-bg",
mouseControls: true,
touchControls: true,
gyroControls: false,
minHeight: 200.0,
minWidth: 200.0,
scale: 1.0,
scaleMobile: 1.0,
color: 0x4f46e5,
backgroundColor: 0x020617,
points: 12.0,
maxDistance: 20.0,
spacing: 15.0
});
window.addEventListener("resize", () => {
try { fx.resize(); } catch {}
});
return fx;
}
function setupAccessModal() {
const accessModal = q("access-modal");
const accessBtn = q("access-btn");
const accessCta = q("access-cta");
const closeAccessModal = q("close-access-modal");
const form = q("access-form");
function openAccess() {
toggleModal(accessModal, true);
setTimeout(() => {
const name = q("name");
if (name) name.focus();
}, 50);
}
if (accessBtn) accessBtn.addEventListener("click", openAccess);
if (accessCta) accessCta.addEventListener("click", openAccess);
if (closeAccessModal) closeAccessModal.addEventListener("click", () => toggleModal(accessModal, false));
if (accessModal) accessModal.addEventListener("click", (e) => {
if (e.target === accessModal) toggleModal(accessModal, false);
});
if (form) {
form.addEventListener("submit", async (e) => {
e.preventDefault();
const name = (q("name")?.value || "").trim();
const email = (q("email")?.value || "").trim();
const institution = (q("institution")?.value || "").trim();
const purpose = (q("purpose")?.value || "").trim();
if (!name || !email || !institution || !purpose) {
alert("Please fill in all fields.");
return;
}
// Optional server post; safe fallback
try {
const res = await fetch("/api/access", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, institution, purpose, page: location.pathname })
});
if (!res.ok) throw new Error("Request not accepted.");
alert("Request received. You will be contacted after review.");
} catch {
alert("Request received. You will be contacted after review.");
}
form.reset();
toggleModal(accessModal, false);
});
}
return { accessModal, openAccess };
}
function setupLabNavigator(dossiers, defaultKey) {
const labNav = q("lab-navigator");
const labNavBtn = q("lab-nav-btn");
const labNavClose = q("lab-nav-close");
const dossierTitle = q("dossier-title");
const dossierSubtitle = q("dossier-subtitle");
const dossierStatus = q("dossier-status");
const dossierBody = q("dossier-body");
const dossierEvidence = q("dossier-evidence");
const dossierPrimary = q("dossier-primary");
const dossierSecondary = q("dossier-secondary");
const dossierMeta = q("dossier-meta");
function openLabNav() { toggleModal(labNav, true); }
function closeLabNav() { toggleModal(labNav, false); }
if (labNavBtn) labNavBtn.addEventListener("click", openLabNav);
if (labNavClose) labNavClose.addEventListener("click", closeLabNav);
if (labNav) {
labNav.addEventListener("click", (e) => {
const shouldClose = e.target && e.target.getAttribute("data-lab-close") === "true";
if (shouldClose) closeLabNav();
});
}
function renderDossier(key) {
const d = dossiers && dossiers[key];
if (!d) return;
if (dossierTitle) dossierTitle.textContent = d.title || "Lab Dossier";
if (dossierSubtitle) dossierSubtitle.textContent = d.subtitle || "";
if (dossierStatus) dossierStatus.textContent = d.status || "READY";
if (dossierBody) dossierBody.textContent = d.body || "";
if (dossierEvidence) {
dossierEvidence.innerHTML = "";
const items = Array.isArray(d.evidence) ? d.evidence : [];
if (items.length) {
items.forEach((item) => {
const li = document.createElement("li");
li.textContent = item;
dossierEvidence.appendChild(li);
});
} else {
const li = document.createElement("li");
li.className = "text-gray-500";
li.textContent = "No evidence items provided.";
dossierEvidence.appendChild(li);
}
}
if (dossierPrimary) {
dossierPrimary.textContent = d.primary?.label || "Open";
dossierPrimary.onclick = typeof d.primary?.action === "function" ? d.primary.action : null;
}
if (dossierSecondary) {
dossierSecondary.textContent = d.secondary?.label || "View Note";
dossierSecondary.onclick = typeof d.secondary?.action === "function" ? d.secondary.action : null;
}
if (dossierMeta) {
const u = d.updated || "—";
dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`;
}
}
// Bind dossier node clicks (if nodes exist on the page)
document.querySelectorAll(".lab-node").forEach((btn) => {
btn.addEventListener("click", () => {
const key = btn.getAttribute("data-dossier");
renderDossier(key);
});
});
// Global ESC behavior: close open modals if present
document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
const accessModal = q("access-modal");
if (isShown(labNav)) closeLabNav();
if (isShown(accessModal)) toggleModal(accessModal, false);
});
renderDossier(defaultKey || "start");
return { openLabNav, closeLabNav, renderDossier, labNav };
}
// Export public API (library only; no auto-init to avoid duplication)
window.SilentPattern = {
toggleModal,
initVanta,
setupAccessModal,
setupLabNavigator
};
})();