/*! trentpower.fr · authored source */ /* trentpower.fr verify modal — action menu Role: renders the four-row action menu opened by the footer "verify" button. handoff layer whose primary action is the dedicated /verify/ page — the modal never duplicates that page's record. peer of the language gate and access modals; same `.shell` / `.shell-close` / `.modal-shell-scrim` family, same focus trap / esc / scrim-click lifecycle exposed by overlay.js as window.TP_OVERLAY. Hierarchy: verify is the user-facing trust hub; integrity is the deeper technical archive linked from /verify/. the modal therefore leads with "verify this page" (primary, hands off to /verify/) and does not surface /integrity/ as a peer action — that promotion would split the trust layer and weaken the verify destination. Source: edited here as verify-modal.template.js; compiled to /js/verify-modal.js by generate_site.py, which substitutes the edition literal so the citation string matches the active release. Data: window.TP_VERIFICATION_MAP, built by generate_verification_map.py, is the single record source shared with /verify/. loaded as a sibling script on every active html so the modal carries a full citation, source link and slug on every page. Constraints: - no fetch; csp connect-src 'none' is preserved - no inline event handlers; csp script-src 'self' plus nonces holds - no tracking, no third-party scripts - no floating toast — the row itself becomes the confirmation */ (function () { 'use strict'; var EDITION = '2026-05-02'; // this file's own UI vocabulary, both editions. the modal is built // by script, so its labels live here; they are picked off the page's // . no translation runtime, no window.I18N. keys not // listed here (e.g. per-page citation titles) fall through to the // caller's fallback, which resolves from the page's own title. the // string set must stay in sync with content//shared.yml under // the `verify_modal:` block; the YAML is the editorial source of // truth surfaced in /editorial/editorial-copy-review/. var LABELS = { en: { 'verify_modal.title': 'Verify, cite or read the source.', 'verify_modal.menu_label': 'Page verification actions', 'verify_modal.row.source_verb': 'View this page’s source', 'verify_modal.row.source_meta': 'Cmd + U', 'verify_modal.row.source_meta_touch': 'Source mirror · HTML edition', 'verify_modal.row.copy_verb': 'Copy citation', 'verify_modal.row.copy_copied': 'Copied to clipboard', 'verify_modal.row.page_verb': 'Verify this page', 'verify_modal.row.page_meta': 'Publication record · Verification', 'verify_modal.row.print_verb': 'Print this page', 'verify_modal.row.print_meta': 'Cmd + P', 'verify_modal.row.print_meta_touch': 'Print or export PDF', 'verify_modal.close': 'Close', 'cite.site_label': 'Personal Site', 'cite.edition_label': 'Edition', // page_title.* survives from the previous overlay so the modal // can present a curated per-page title in the kicker line. the // verification map's title is "client strategy & growth systems // · trent power" — too long for a single line — so each page // carries an editorial short title here. 'cite.overlay.page_title.home': 'Client Strategy & Growth Systems', 'cite.overlay.page_title.privacy': 'Privacy statement', 'cite.overlay.page_title.security': 'Security posture', 'cite.overlay.page_title.integrity': 'Integrity record', 'cite.overlay.page_title.verify': 'Verify page', 'cite.overlay.page_title.source': 'Source reader', 'cite.overlay.page_title.source-reader': 'Source reader', 'cite.overlay.page_title.acknowledgments': 'Security acknowledgements', 'cite.overlay.page_title.integrity-verify-locally': 'Verify locally', 'cite.overlay.page_title.releases': 'Release archive', 'cite.overlay.page_title.release-archive': 'Release archive', 'cite.overlay.page_title.forbidden': 'Access not available', 'cite.overlay.page_title.not-found': 'Page not found', 'cite.overlay.page_title.server-error': 'Temporary server error', 'cite.overlay.page_title.maintenance': 'Down for maintenance', 'cite.overlay.page_title.sw-reset': 'Service worker reset' }, fr: { 'verify_modal.title': 'Vérifier, citer ou lire la source.', 'verify_modal.menu_label': 'Actions de vérification de la page', 'verify_modal.row.source_verb': 'Voir la source de cette page', 'verify_modal.row.source_meta': 'Cmd + U', 'verify_modal.row.source_meta_touch': 'Miroir source · édition HTML', 'verify_modal.row.copy_verb': 'Copier la citation', 'verify_modal.row.copy_copied': 'Copié dans le presse-papiers', 'verify_modal.row.page_verb': 'Vérifier cette page', 'verify_modal.row.page_meta': 'Dossier de publication · vérification', 'verify_modal.row.print_verb': 'Imprimer cette page', 'verify_modal.row.print_meta': 'Cmd + P', 'verify_modal.row.print_meta_touch': 'Imprimer ou exporter en PDF', 'verify_modal.close': 'Fermer', 'cite.site_label': 'Site personnel', 'cite.edition_label': 'Édition', 'cite.overlay.page_title.home': 'Stratégie client et systèmes de croissance', 'cite.overlay.page_title.privacy': 'Notice de confidentialité', 'cite.overlay.page_title.security': 'Posture de sécurité', 'cite.overlay.page_title.integrity': 'Notice d’intégrité', 'cite.overlay.page_title.verify': 'Page de vérification', 'cite.overlay.page_title.source': 'Lecteur de code source', 'cite.overlay.page_title.source-reader': 'Lecteur de code source', 'cite.overlay.page_title.acknowledgments': 'Remerciements de sécurité', 'cite.overlay.page_title.integrity-verify-locally': 'Vérifier localement', 'cite.overlay.page_title.releases': 'Archive des versions', 'cite.overlay.page_title.release-archive': 'Archive des versions', 'cite.overlay.page_title.forbidden': 'Accès non disponible', 'cite.overlay.page_title.not-found': 'Page introuvable', 'cite.overlay.page_title.server-error': 'Erreur serveur temporaire', 'cite.overlay.page_title.maintenance': 'Maintenance en cours', 'cite.overlay.page_title.sw-reset': 'Réinitialisation du service worker' } }; function getLang() { return document.documentElement.lang === 'fr' ? 'fr' : 'en'; } function tt(key, fallback) { var bag = LABELS[getLang()] || LABELS.en; var v = bag[key]; return (typeof v === 'string') ? v : (fallback || ''); } // ─── path normalisation: same shape as /verify/'s normalisepath ──────── function normalisePath(raw) { if (!raw) return '/'; raw = String(raw).trim(); if (!raw) return '/'; if (raw.charAt(0) !== '/') raw = '/' + raw; raw = raw.replace(/\/index\.html$/, '/'); if (raw.indexOf('.') === -1 && raw.charAt(raw.length - 1) !== '/') raw += '/'; return raw; } function currentRecord() { var map = (typeof window !== 'undefined' && window.TP_VERIFICATION_MAP) || {}; var p = normalisePath(location.pathname || '/'); return map[p] || null; } // ─── slug + citation derivation ──────────────────────────────────────── // the source row deep-links into the navigable source reader at // /source/view/, which reads the directory mirror under /source/ // and renders each page's authored source in a readable shell. // we pass the current page path as ?path=, exactly as // the /en-au/source/ and /fr/source/ index pages do for every // page in their list. /source// directly is a 403 (no // directory listing) — that was the bug this fixes. function currentSlug() { var p = normalisePath(location.pathname || '/'); return p.replace(/^\//, ''); } function sourceHref() { return '/source/view/?path=' + encodeURIComponent(normalisePath(location.pathname || '/')); } // the dedicated /verify/ page is bilingual — route to the edition // matching the page the visitor is on. /verify/ (single-tree) is // retired in favour of /en-au/verify/ and /fr/verifier/. function verifyPageHref() { var base = getLang() === 'fr' ? '/fr/verifier/' : '/en-au/verify/'; return base + '?path=' + encodeURIComponent(normalisePath(location.pathname || '/')); } // integrityHref() removed in the trust-hierarchy refactor; /integrity/ // is reachable from /verify/'s related-records panel, not from this // modal. retained here as a comment so the omission is intentional. // ─── citation fallback (when route is not in the verification map) ───── function pageTitle() { var raw = document.title || ''; if (raw.indexOf('|') !== -1) { return raw.replace(/^[^|]+\|\s*/, '').trim().replace(/\s*&\s*/g, ' and '); } return raw.replace(/\s*[—-]\s*Trent Power$/, '').trim(); } function canonicalUrl() { var link = document.querySelector('link[rel="canonical"]'); return link ? link.href : location.href; } // citation format matches the brief and the A-verify reference: // "trent power. {title}. personal site. edition {edition}. {url}". // the verification map's `citation` field carries this same string // baked at build time; we prefer it. fallback rebuilds it on the // client when the record is missing (legacy / non-mapped routes). function fallbackCitation() { return 'Trent Power. ' + pageTitle() + '. ' + tt('cite.site_label', 'Personal Site') + '. ' + tt('cite.edition_label', 'Edition') + ' ' + EDITION + '. ' + canonicalUrl(); } // citation preview · middle-truncation that keeps the canonical // bookends ("trent power · …" and "· edition YYYY-MM-DD") and // squeezes the middle title to fit. previously the meta rendered // preview shows only the edition so the line fits on one desktop row. // the full citation continues to live in data-citation for the // clipboard write; only the displayed preview uses this shorter shape. function citationPreview() { return tt('cite.edition_label', 'Edition') + ' ' + EDITION; } // resolve the editorial short title for this page. priority: // 1. explicit `` override // 2. i18n key `cite.overlay.page_title.` (canonical // route — translates per language, kept short so it fits the // modal's centred kicker line) // 3. verification-map record title (curated, but generic in // places — "… · trent power" suffix included) // 4. document.title minus the "— trent power" suffix as a final // fallback so the modal never blanks. function resolveTitle(rec) { var explicit = document.body && document.body.dataset && document.body.dataset.citationTitle; if (explicit) return explicit; var page = (document.body && document.body.dataset && document.body.dataset.page) || ''; if (page) { var key = 'cite.overlay.page_title.' + page; var translated = tt(key, ''); if (translated) return translated; } return (rec && rec.title) || pageTitle() || tt('cite.site_label', 'Personal Site'); } // ─── dom helpers ─────────────────────────────────────────────────────── function el(tag, attrs, text) { var n = document.createElement(tag); if (attrs) for (var k in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, k) && attrs[k] != null) { n.setAttribute(k, attrs[k]); } } if (text != null) n.textContent = text; return n; } // svg helper · createElementNS so the shield anchor renders as a real // svg subtree (createElement('svg') would emit an HTMLUnknownElement // that browsers refuse to paint). attrs only — no text content. used // for the verify-modal header anchor; no innerHTML, no trusted-types // policy needed. var SVG_NS = 'http://www.w3.org/2000/svg'; function svgEl(tag, attrs) { var n = document.createElementNS(SVG_NS, tag); if (attrs) for (var k in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, k) && attrs[k] != null) { n.setAttribute(k, attrs[k]); } } return n; } function safeHref(href) { if (!href) return '#'; if (href.charAt(0) === '/' || /^https?:\/\//.test(href)) return href; return '#'; } // ─── build modal dom lazily ──────────────────────────────────────────── // four rows, top-down: // 1 · verify this page (primary, italic oxblood, deep-links /verify/) // 2 · view this page's source (handoff to /source/view/) // 3 · copy citation (button, inline "copied" feedback) // 4 · print this page (sync gesture, no toast) // each row is a real or