/* RFC-1192: the conditional cookie-consent surface. initCookieConsent() self-gates on the [data-cookie-consent-host] node that the layout renders only when the site has an entitled COOKIE_MODULES entry, renders the accept/decline surface, and persists the visitor's choice via writeConsentFlag(). Dispatches the `wg:cookie-consent` CustomEvent so consent-gated modules can react on the same page (no reload). Never re-prompt after a recorded choice — both accept and decline are durable. Never write cookies itself — the surface only records the consent flag. No innerHTML sinks — DOM is built via createElement/textContent (Trusted Types, RFC-1178). Do not style beyond the minimal fixed-position shell — theming is a site concern. RFC-1192: initial consent surface module (accept/decline -> wg_consent flag + wg:cookie-consent event). */ import { COOKIE_MODULES } from "../policy/cookies.ts"; import { readConsentFlag, writeConsentFlag } from "./cookie-write-gate.ts"; /** CustomEvent name dispatched on window after a consent choice is recorded. */ export const COOKIE_CONSENT_EVENT = "wg:cookie-consent"; const SURFACE_TEXT = { title: "Cookies auf dieser Website", body: "Diese Website setzt eigene Cookies für folgende Zwecke:", accept: "Akzeptieren", decline: "Ablehnen", } as const; /** * Self-gating init. No-ops when: no host node exists, a consent flag is * already recorded, or no catalog modules are active for the site. * `moduleIds` — the entitled COOKIE_MODULES ids resolved server-side * (orchestrator option `cookieModules`). */ export function initCookieConsent(moduleIds: readonly string[] = []): void { const host = document.querySelector("[data-cookie-consent-host]"); if (!host) return; if (readConsentFlag() !== null) return; const active = COOKIE_MODULES.filter((m) => moduleIds.includes(m.moduleId)); if (active.length === 0) return; const banner = document.createElement("div"); banner.setAttribute("data-cookie-consent", ""); banner.setAttribute("role", "dialog"); banner.setAttribute("aria-label", SURFACE_TEXT.title); banner.style.cssText = "position:fixed;inset:auto 0 0 0;z-index:9999;padding:1rem;" + "background:var(--ds-color-surface,#fff);color:var(--ds-color-text,#111);" + "border-top:1px solid currentColor;display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;"; const text = document.createElement("div"); const title = document.createElement("strong"); title.textContent = SURFACE_TEXT.title; const body = document.createElement("p"); body.textContent = SURFACE_TEXT.body; body.style.margin = "0"; const purposes = document.createElement("ul"); purposes.style.margin = "0"; for (const mod of active) { for (const cookie of mod.cookies) { const li = document.createElement("li"); li.textContent = cookie.purpose; purposes.appendChild(li); } } text.appendChild(title); text.appendChild(body); text.appendChild(purposes); banner.appendChild(text); const close = (granted: boolean) => { writeConsentFlag(granted); window.dispatchEvent(new CustomEvent(COOKIE_CONSENT_EVENT, { detail: { granted } })); banner.remove(); }; const accept = document.createElement("button"); accept.type = "button"; accept.textContent = SURFACE_TEXT.accept; accept.addEventListener("click", () => close(true), { once: true }); const decline = document.createElement("button"); decline.type = "button"; decline.textContent = SURFACE_TEXT.decline; decline.addEventListener("click", () => close(false), { once: true }); banner.appendChild(accept); banner.appendChild(decline); host.appendChild(banner); host.setAttribute("data-cookie-consent-initialized", "true"); }