/*! trentpower.fr · authored source */ /* app-enhance source. edit here, not in app-enhance.js. the build header is prepended at build time by generate_site.py. loaded after first paint by app.js via requestidlecallback + document.createElement('script') — the script url passes through the tp-i18n trusted types policy. carries the project-overlay lifecycle (focus trap, click-outside, escape) and the print-time document.title swap. none of this is needed for first paint, language detection or accessibility, so it does not enter the lcp critical path. */ (function () { 'use strict'; // shared globals come from /app.js: window.I18N, window.LANG_CYCLE. // /app.js exports those before scheduling this bundle. var I18N = window.I18N || null; // ═══════════════════════════════════════════════════ // overlay , accessible, focus-trapped, reusable // the same lifecycle (blur backdrop, scroll lock, inert siblings, // focus trap, escape close, click-outside close) is used by both // the homepage's "view project" modal and the cite-this-page // overlay rendered by cite.js. exposed via window.TP_OVERLAY so // cite.js can register its own (overlay, trigger) pair without // duplicating the lifecycle. // ═══════════════════════════════════════════════════ var siteContent = document.getElementById('main'); var supportsInert = 'inert' in document.documentElement; function setInert(el, state) { if (!el) return; if (state) { if (supportsInert) el.setAttribute('inert', ''); el.setAttribute('aria-hidden', 'true'); } else { if (supportsInert) el.removeAttribute('inert'); el.removeAttribute('aria-hidden'); } } function focusableIn(el) { return el.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'); } var activeOverlay = null; var activeTrigger = null; // history integration · whether this opener pushed a state. only set // when opts.hash is supplied. cleared whenever an overlay closes. var pushedHistoryState = false; function openOverlay(overlay, trigger, opts) { if (!overlay) return; activeOverlay = overlay; activeTrigger = trigger || null; overlay.setAttribute('aria-hidden', 'false'); overlay.classList.add('active'); document.body.style.overflow = 'hidden'; setInert(siteContent, true); if (trigger) trigger.setAttribute('aria-expanded', 'true'); var focusable = focusableIn(overlay); if (focusable.length > 0) focusable[0].focus(); // optional history integration. caller passes opts.hash (e.g. '#cite') // and the overlay becomes back-button-closable + deep-linkable. the // opts.fromPopstate flag lets the popstate listener re-open without // pushing an extra entry. pushedHistoryState = false; if (opts && opts.hash && !opts.fromPopstate && typeof history !== 'undefined' && typeof history.pushState === 'function') { try { history.pushState({ tp_overlay: opts.hash.replace(/^#/, '') }, '', opts.hash); pushedHistoryState = true; } catch (_) { /* sandboxed iframe etc. — silently no-op */ } } } function closeOverlay(opts) { if (!activeOverlay) return; var overlay = activeOverlay; var trigger = activeTrigger; overlay.classList.remove('active'); overlay.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; setInert(siteContent, false); if (trigger) { trigger.setAttribute('aria-expanded', 'false'); trigger.focus(); } activeOverlay = null; activeTrigger = null; // if we pushed a history entry on open and this close is user- // initiated (× / esc / click-outside, not a popstate-driven close), // pop our entry so the url reverts cleanly. wrapped in try because // history.back() can fail when the page is the only entry. if (pushedHistoryState && !(opts && opts.fromPopstate) && typeof history !== 'undefined' && typeof history.back === 'function') { try { history.back(); } catch (_) { /* no prior entry — stay put */ } } pushedHistoryState = false; } // popstate · browser back/forward. if an overlay is open and the new // location no longer carries our state, close from popstate so we // don't double-pop the history. Open-on-forward isn't supported in // this version — initial-hash auto-open is handled by the overlay's // own init code (cite.template.js). window.addEventListener('popstate', function () { if (activeOverlay) { var st = history.state; var stillOpen = st && st.tp_overlay; if (!stillOpen) closeOverlay({ fromPopstate: true }); } }); document.addEventListener('keydown', function (e) { if (!activeOverlay) return; if (e.key === 'Escape') { closeOverlay(); return; } if (e.key === 'Tab') { var focusable = focusableIn(activeOverlay); if (focusable.length === 0) return; var first = focusable[0]; var last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } }); // expose for cite.js (and any future overlay). window.TP_OVERLAY = { open: openOverlay, close: closeOverlay }; // project "view project" modal , wires its trigger / close button into // the shared lifecycle. pages without #modal silently skip. var modal = document.getElementById('modal'); var btn = document.getElementById('access-btn'); var closeBtn = document.getElementById('modal-close'); if (modal && btn && closeBtn) { btn.addEventListener('click', function () { openOverlay(modal, btn); }); closeBtn.addEventListener('click', closeOverlay); modal.addEventListener('click', function (e) { if (e.target === modal) closeOverlay(); }); } // ═══════════════════════════════════════════════════ // print , temporarily swap document.title so the browser's // "save as pdf" dialogue suggests a clean filename, then restore // on afterprint so the on-screen tab title is unaffected. // Reveal-classed elements are forced visible defensively (the // live screen layout is hidden by the print.css direct-child rule, // but the visibility flag is harmless on screen and protects // against future regressions). // ═══════════════════════════════════════════════════ var savedDocTitle = null; function onBeforePrint() { var lang = document.documentElement.lang || 'en'; var t = (I18N && I18N[lang]) || (I18N && I18N.en) || null; var page = (document.body && document.body.getAttribute('data-page')) || 'home'; var printTitle = (page === 'privacy' && t && t.privacy && t.privacy.print && t.privacy.print.doc_title) || (page === 'integrity' && t && t.integrity && t.integrity.print && t.integrity.print.doc_title) || (page === 'security' && t && t.security && t.security.print && t.security.print.doc_title) || (page === 'source' && t && t.source && t.source.print && t.source.print.doc_title) || (page === 'releases' && t && t.releases && t.releases.print && t.releases.print.doc_title) || (page === 'forbidden' && t && t.error && t.error['403'] && t.error['403'].print && t.error['403'].print.doc_title) || (page === 'not-found' && t && t.error && t.error['404'] && t.error['404'].print && t.error['404'].print.doc_title) || (page === 'server-error' && t && t.error && t.error['500'] && t.error['500'].print && t.error['500'].print.doc_title) || (page === 'verify' && t && t.verify && t.verify.doc_title) || (t && t.print && t.print.doc_title); if (printTitle) { savedDocTitle = document.title; document.title = printTitle; } var revealSelector = '.principle, .trajectory-item, .project-card, ' + '.hero-name, .hero-statement, .hero-body, .trust-mark'; document.querySelectorAll(revealSelector).forEach(function (el) { el.classList.add('visible'); }); document.documentElement.classList.add('is-printing'); } function onAfterPrint() { if (savedDocTitle !== null) { document.title = savedDocTitle; savedDocTitle = null; } document.documentElement.classList.remove('is-printing'); } if (window.matchMedia) { try { window.matchMedia('print').addEventListener('change', function (e) { if (e.matches) onBeforePrint(); else onAfterPrint(); }); } catch (_) { /* older browsers fall back to onbeforeprint */ } } window.addEventListener('beforeprint', onBeforePrint); window.addEventListener('afterprint', onAfterPrint); // ═══════════════════════════════════════════════════ // POST-LCP FULL-FONT upgrade // /fonts-full.css carries the full editorial @font-face declarations // and a `.fonts-loaded` :root override that flips --serif / --sans / // --mono to put the full families first. loaded as a same-origin // stylesheet (csp `style-src 'self'` allows it; no trusted types // gate applies — `require-trusted-types-for 'script'` only governs // script-URL sinks). subset and full font share identical glyph // metrics so the swap is glyph-by-glyph with cls = 0. // ═══════════════════════════════════════════════════ (function _loadFullFonts() { if (document.querySelector('link[data-tp-fonts-full]')) return; var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/fonts-full.css'; link.setAttribute('data-tp-fonts-full', ''); // wait until every pending font load has resolved (loaded or hit // its font-display timeout), then run the class flip inside the // next paint frame so the recalc rides the browser's normal paint // pipeline rather than appearing as a free-standing reflow. function paintFlip() { if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(function () { document.documentElement.classList.add('fonts-loaded'); }); } else { document.documentElement.classList.add('fonts-loaded'); } } link.addEventListener('load', function () { if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') { document.fonts.ready.then(paintFlip); } else { setTimeout(paintFlip, 1500); } }); document.head.appendChild(link); })(); // quiet hello to anyone reading the console. // one line, once per page load. no tracking, no network, no ui. var canLog = window.console && typeof console.info === "function"; if (canLog) { console.info("trentpower.fr · static, signed, inspectable · ctrl+u still works"); } })();