@port60/template-kit 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/commands/content.mjs +15 -4
- package/src/commands/dev.mjs +37 -21
- package/src/lib/previewOptions.mjs +70 -0
- package/src/vendor/contract/v1/behaviours.json +235 -44
- package/src/vendor/contract/v1/manifest.schema.json +3 -0
- package/src/vendor/validator/behaviors-runtime.js +1 -1
- package/src/vendor/validator/fonts.mjs +53 -0
- package/src/vendor/validator/icons.mjs +37 -0
- package/src/vendor/validator/platform-base.css +23 -0
- package/src/vendor/validator/preview.mjs +116 -40
- package/src/vendor/validator/site-context.mjs +134 -8
- package/src/vendor/validator/validate.mjs +10 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/build-behaviors-runtime.mjs — do not edit by hand.
|
|
2
|
-
"use strict";var p60Behaviors=(()=>{var x=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var C=(o,l)=>{for(var e in l)x(o,e,{get:l[e],enumerable:!0})},F=(o,l,e,n)=>{if(l&&typeof l=="object"||typeof l=="function")for(let t of q(l))!I.call(o,t)&&t!==e&&x(o,t,{get:()=>l[t],enumerable:!(n=P(l,t))||n.enumerable});return o};var O=o=>F(x({},"__esModule",{value:!0}),o);var ee={};C(ee,{initBehaviors:()=>Z});var L=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;function b(o,l){let e=`data-p60-bound-${l}`;return o.hasAttribute(e)?!0:(o.setAttribute(e,""),!1)}var N=.15;function T(o,l){if(o.length===0)return;if(L()||!("IntersectionObserver"in window)){o.forEach(l);return}let e=new IntersectionObserver(n=>{for(let t of n){if(!t.isIntersecting)continue;(t.boundingClientRect.height>=window.innerHeight||t.intersectionRatio>=N-.001)&&(e.unobserve(t.target),l(t.target))}},{rootMargin:"0px 0px -10% 0px",threshold:[0,N]});o.forEach(n=>e.observe(n))}function R(o){let l=Array.from(o.querySelectorAll("[data-p60-reveal-group]")).filter(n=>!b(n,"reveal-group"));for(let n of l)Array.from(n.children).forEach((t,a)=>{t.style.setProperty("--p60-reveal-index",String(a)),t.classList.add("p60-reveal-child")});T(l,n=>n.classList.add("is-revealed"));let e=Array.from(o.querySelectorAll("[data-p60-reveal]")).filter(n=>!b(n,"reveal"));T(e,n=>n.classList.add("is-revealed"))}var B=/\d[\d, ]*(?:\.\d+)?/;function _(o){let l=Array.from(o.querySelectorAll("[data-p60-count]")).filter(e=>!b(e,"count"));T(l,e=>{e.classList.add("is-revealed");let n=e.textContent??"",t=B.exec(n);if(!t)return;let a=t[0].replace(/[, ]+$/,""),c=n.slice(0,t.index),r=n.slice(t.index+a.length),i=/[, ]/.test(a),d=a.includes(".")?a.length-a.indexOf(".")-1:0,s=Number.parseFloat(a.replace(/[, ]/g,"")),f=Number.parseFloat(e.getAttribute("data-p60-count")??""),g=Number.isFinite(f)?f:s;if(!Number.isFinite(g)||L())return;let u=Math.min(Number.parseInt(e.getAttribute("data-p60-count-duration")??"1200",10)||1200,4e3),p=new Intl.NumberFormat(document.documentElement.lang||void 0,{useGrouping:i,minimumFractionDigits:d,maximumFractionDigits:d}),m=performance.now(),h=v=>{let y=Math.min((v-m)/u,1),H=1-Math.pow(1-y,3);e.textContent=y<1?c+p.format(g*H)+r:n,y<1&&requestAnimationFrame(h)};requestAnimationFrame(h)})}function D(o){let l=Array.from(o.querySelectorAll("[data-p60-progress]")).filter(e=>!b(e,"progress"));if(!L()){for(let e of l){let n=e.style.width||getComputedStyle(e).width;e.style.transition="none",e.style.width="0px",e.offsetWidth,e.style.transition="width 1.1s cubic-bezier(0.22, 1, 0.36, 1)",e.dataset.p60ProgressTarget=n}T(l,e=>{e.classList.add("is-revealed"),e.style.width=e.dataset.p60ProgressTarget??""})}}function V(o){let l=Array.from(o.querySelectorAll("[data-p60-countdown]")).filter(e=>!b(e,"countdown"));for(let e of l){let n=Date.parse(e.getAttribute("data-p60-until")??"");if(!Number.isFinite(n))continue;let t=e.getAttribute("data-p60-countdown")||"dhm",a=t==="dhms",c=()=>{let i=n-Date.now();if(i<=0){e.classList.add("is-elapsed");let m=e.getAttribute("data-p60-elapsed-text");m&&(e.textContent=m),window.clearInterval(r);return}let d=Math.floor(i/1e3),s=Math.floor(d/86400),f=Math.floor(d%86400/3600),g=Math.floor(d%3600/60),u=d%60,p=[];t.includes("d")&&s>0&&p.push(`${s}d`),p.push(`${f}h`,`${g}m`),a&&p.push(`${u}s`),e.textContent=p.join(" ")},r=window.setInterval(c,a?1e3:15e3);c()}}function $(o){let l=Array.from(o.querySelectorAll("[data-p60-accordion]")).filter(e=>!b(e,"accordion"));for(let e of l){let n=e.getAttribute("data-p60-accordion")!=="multi",t=Array.from(e.querySelectorAll("details"));for(let a of t){a.addEventListener("toggle",()=>{if(a.classList.toggle("is-open",a.open),a.open&&n)for(let i of t)i!==a&&i.open&&(i.open=!1)}),a.open&&a.classList.add("is-open");let r=a.querySelector("summary")?.nextElementSibling;r&&!L()&&a.addEventListener("toggle",()=>{if(!a.open)return;let i=r.scrollHeight;r.style.overflow="hidden",r.animate([{height:"0px",opacity:"0"},{height:`${i}px`,opacity:"1"}],{duration:260,easing:"ease-out"}).addEventListener("finish",()=>{r.style.overflow=""})})}}}function Y(o){let l=Array.from(o.querySelectorAll("[data-p60-carousel]")).filter(e=>!b(e,"carousel"));for(let e of l){let n=Array.from(e.querySelectorAll("[data-p60-slide]"));if(n.length<2)continue;let t=Math.max(0,n.findIndex(m=>m.classList.contains("is-active"))),a=!1,c=[],r=document.createElement("nav");r.className="p60-carousel-dots",r.setAttribute("aria-label","Slides"),n.forEach((m,h)=>{let v=document.createElement("button");v.type="button",v.className="p60-carousel-dot",v.setAttribute("aria-label",`Go to slide ${h+1}`),v.addEventListener("click",()=>i(h,!0)),c.push(v),r.appendChild(v)}),e.appendChild(r),e.setAttribute("aria-roledescription","carousel"),e.hasAttribute("tabindex")||e.setAttribute("tabindex","0");let i=(m,h=!1)=>{t=(m+n.length)%n.length,n.forEach((v,y)=>{v.classList.toggle("is-active",y===t),y===t?(v.removeAttribute("aria-hidden"),v.removeAttribute("inert")):(v.setAttribute("aria-hidden","true"),v.setAttribute("inert",""))}),c.forEach((v,y)=>{v.classList.toggle("p60-carousel-dot--active",y===t),v.setAttribute("aria-current",y===t?"true":"false")}),e.style.setProperty("--p60-carousel-index",String(t)),h&&p()};e.querySelectorAll("[data-p60-carousel-prev]").forEach(m=>m.addEventListener("click",()=>i(t-1,!0))),e.querySelectorAll("[data-p60-carousel-next]").forEach(m=>m.addEventListener("click",()=>i(t+1,!0))),e.addEventListener("keydown",m=>{m.key==="ArrowLeft"&&i(t-1,!0),m.key==="ArrowRight"&&i(t+1,!0)});let d=0,s=0;e.addEventListener("pointerdown",m=>{d=m.clientX,s=m.clientY}),e.addEventListener("pointerup",m=>{let h=m.clientX-d,v=m.clientY-s;Math.abs(h)>40&&Math.abs(h)>Math.abs(v)&&i(t+(h<0?1:-1),!0)}),e.addEventListener("mouseenter",()=>{a=!0}),e.addEventListener("mouseleave",()=>{a=!1}),e.addEventListener("focusin",()=>{a=!0}),e.addEventListener("focusout",()=>{a=!1});let f,g=e.getAttribute("data-p60-carousel")==="auto"&&!L(),u=Math.max(Number.parseInt(e.getAttribute("data-p60-interval")??"6000",10)||6e3,3e3),p=()=>{f!==void 0&&window.clearInterval(f),g&&(f=window.setInterval(()=>{!a&&!document.hidden&&i(t+1)},u))};i(t),p()}}function K(o){let l=Array.from(o.querySelectorAll("[data-p60-sticky-header]")).filter(e=>!b(e,"sticky-header"));for(let e of l){let n=e.getAttribute("data-p60-sticky-header")==="auto-hide",t=window.scrollY,a=!1,c=()=>{let r=window.scrollY;e.classList.toggle("is-condensed",r>24),n&&(r>160&&r>t?e.classList.add("is-hidden"):e.classList.remove("is-hidden")),t=r,a=!1};window.addEventListener("scroll",()=>{a||(a=!0,requestAnimationFrame(c))},{passive:!0}),c()}}function X(o){let l=Array.from(o.querySelectorAll("[data-p60-sticky-cta]")).filter(e=>!b(e,"sticky-cta"));for(let e of l){let n=Number.parseInt(e.getAttribute("data-p60-sticky-cta")??"480",10)||480,t=!1,a=()=>{e.classList.toggle("is-stuck",window.scrollY>n),t=!1};window.addEventListener("scroll",()=>{t||(t=!0,requestAnimationFrame(a))},{passive:!0}),a()}}function z(o){let l=Array.from(o.querySelectorAll("[data-p60-lightbox]")).filter(e=>!b(e,"lightbox"));for(let e of l){let n=Array.from(e.querySelectorAll("a[data-p60-lightbox-item]"));if(n.length===0)continue;let t=null,a=0,c=null,r=()=>{t?.remove(),t=null,e.classList.remove("is-open"),document.removeEventListener("keydown",d),c?.focus()},i=f=>{a=(f+n.length)%n.length;let g=n[a];if(!t)return;let u=t.querySelector(".p60-lightbox-img"),p=t.querySelector(".p60-lightbox-caption");u&&(u.src=g.href);let m=g.getAttribute("data-p60-caption")??g.querySelector("img")?.alt??"";p&&(p.textContent=m),u&&(u.alt=m)},d=f=>{if(f.key==="Escape"&&r(),f.key==="ArrowLeft"&&i(a-1),f.key==="ArrowRight"&&i(a+1),f.key==="Tab"&&t){let g=Array.from(t.querySelectorAll("button")),u=g.indexOf(document.activeElement),p=f.shiftKey?u<=0?g.length-1:u-1:u>=g.length-1?0:u+1;g[p]?.focus(),f.preventDefault()}},s=(f,g)=>{c=g,t=document.createElement("div"),t.className="p60-lightbox",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),L()&&(t.style.transition="none"),t.innerHTML='<figure><img class="p60-lightbox-img" alt=""><figcaption class="p60-lightbox-caption"></figcaption></figure>';let u=(m,h,v)=>{let y=document.createElement("button");return y.type="button",y.className=m,y.setAttribute("aria-label",h),y.addEventListener("click",v),t.appendChild(y),y};n.length>1&&(u("p60-lightbox-prev","Previous image",()=>i(a-1)),u("p60-lightbox-next","Next image",()=>i(a+1)));let p=u("p60-lightbox-close","Close",r);t.addEventListener("click",m=>{m.target===t&&r()}),document.body.appendChild(t),e.classList.add("is-open"),document.addEventListener("keydown",d),i(f),p.focus()};n.forEach((f,g)=>f.addEventListener("click",u=>{u.preventDefault(),s(g,f)}))}}function U(o){let l=Array.from(o.querySelectorAll("[data-p60-tabs]")).filter(e=>!b(e,"tabs"));for(let e of l){let n=Array.from(e.querySelectorAll("[data-p60-tab]")),t=Array.from(e.querySelectorAll("[data-p60-panel]"));if(n.length===0||t.length===0)continue;let a=r=>t.find(i=>i.getAttribute("data-p60-panel")===r.getAttribute("data-p60-tab"));n[0].closest("[data-p60-tabs] > *")?.setAttribute("role","tablist");let c=(r,i=!1)=>{for(let d of n){let s=d===r;d.classList.toggle("is-active",s),d.setAttribute("aria-selected",s?"true":"false"),d.setAttribute("tabindex",s?"0":"-1")}for(let d of t){let s=d===a(r);d.classList.toggle("is-active",s),d.hidden=!s}i&&r.focus()};n.forEach((r,i)=>{r.setAttribute("role","tab"),r.addEventListener("click",d=>{d.preventDefault(),c(r)}),r.addEventListener("keydown",d=>{let s=d.key;s==="ArrowLeft"&&c(n[(i-1+n.length)%n.length],!0),s==="ArrowRight"&&c(n[(i+1)%n.length],!0),s==="Home"&&c(n[0],!0),s==="End"&&c(n[n.length-1],!0)})}),t.forEach(r=>r.setAttribute("role","tabpanel")),c(n.find(r=>r.classList.contains("is-active"))??n[0])}}var W=60,j=280,M=()=>window.matchMedia("(hover: hover)").matches,G='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',A=new Set,k=!1;function S(o){o.openTimer!==null&&(window.clearTimeout(o.openTimer),o.openTimer=null),o.closeTimer!==null&&(window.clearTimeout(o.closeTimer),o.closeTimer=null)}function E(o,l=!1){S(o);for(let e of Array.from(A))e!==o&&o.item.contains(e.item)&&E(e);o.item.classList.remove("is-open"),o.toggle.setAttribute("aria-expanded","false"),o.openedBy=null,A.delete(o),l&&o.toggle.focus()}function w(o,l){S(o);for(let e of Array.from(A))e!==o&&e.nav===o.nav&&!e.item.contains(o.item)&&E(e);o.item.classList.add("is-open"),o.toggle.setAttribute("aria-expanded","true"),o.openedBy=l,A.add(o)}function J(o){for(let e of Array.from(o.querySelectorAll("[data-p60-nav]"))){let n=Array.from(e.querySelectorAll("[data-p60-nav-item]")).filter(t=>t.closest("[data-p60-nav]")===e&&!b(t,"nav-item"));for(let t of n){let a=s=>s.closest("[data-p60-nav-item]")===t,c=Array.from(t.querySelectorAll("[data-p60-nav-toggle]")).find(a)??null,r=Array.from(t.querySelectorAll("[data-p60-nav-menu]")).find(a)??null;if(!c||!r)continue;let i={nav:e,item:t,toggle:c,menu:r,openedBy:null,lastPointer:"",openTimer:null,closeTimer:null},d=()=>A.has(i);c.setAttribute("aria-expanded","false"),r.id&&!c.hasAttribute("aria-controls")&&c.setAttribute("aria-controls",r.id),t.addEventListener("mouseenter",()=>{M()&&(i.closeTimer!==null&&(window.clearTimeout(i.closeTimer),i.closeTimer=null),!d()&&i.openTimer===null&&(i.openTimer=window.setTimeout(()=>{i.openTimer=null,w(i,"hover")},W)))}),t.addEventListener("mouseleave",()=>{M()&&(i.openTimer!==null&&(window.clearTimeout(i.openTimer),i.openTimer=null),d()&&i.openedBy==="hover"&&i.closeTimer===null&&(i.closeTimer=window.setTimeout(()=>{i.closeTimer=null,E(i)},j)))}),c.addEventListener("pointerdown",s=>{i.lastPointer=s.pointerType??""}),c.addEventListener("click",s=>{let f=s.detail===0||i.lastPointer==="touch"||i.lastPointer==="pen"||!M();if(!d()){s.preventDefault(),s.stopPropagation(),w(i,"toggle");return}!f&&i.openedBy==="hover"||(s.preventDefault(),s.stopPropagation(),E(i))}),c.addEventListener("keydown",s=>{s.key==="ArrowDown"&&(s.preventDefault(),d()||w(i,"toggle"),r.querySelector(G)?.focus())}),t.addEventListener("keydown",s=>{s.key==="Escape"&&d()&&(s.stopPropagation(),E(i,!0))}),t.addEventListener("focusin",s=>{s.target instanceof Node&&r.contains(s.target)&&!d()&&w(i,"focus")}),t.addEventListener("focusout",s=>{let f=s.relatedTarget;f instanceof Node&&!t.contains(f)&&d()&&E(i)})}if(!b(e,"nav-fit")){let a=u=>{for(let p=1;p<=3;p++)e.classList.toggle(`is-fit-${p}`,p<=u)},c=u=>{let p=u.getBoundingClientRect();return p.width>0||p.height>0?[u]:Array.from(u.children).flatMap(c)},r=u=>{let p=-1/0;for(let m of u.flatMap(c)){let h=m.getBoundingClientRect();if(p!==-1/0&&h.top>=p-2)return!0;p=Math.max(p,h.bottom)}return!1},i=()=>{if(e.scrollWidth>e.clientWidth+1||r(Array.from(e.children)))return!1;let u=e.parentElement;return!(u&&getComputedStyle(u).flexWrap==="wrap"&&r(Array.from(u.children)))},d=0,s=()=>{if(d=performance.now(),e.offsetParent===null||!getComputedStyle(e).flexDirection.startsWith("row")){a(0);return}a(0);let u=0;for(;!i()&&u<3;)a(++u);d=performance.now()},f=!1,g=()=>{f||(f=!0,requestAnimationFrame(()=>{f=!1,s()}))};window.addEventListener("resize",g,{passive:!0}),document.fonts?.ready.then(g).catch(()=>{}),document.fonts?.addEventListener("loadingdone",g),"ResizeObserver"in window&&new ResizeObserver(()=>{performance.now()-d>120&&g()}).observe(e),s()}}let l=Array.from(o.querySelectorAll("[data-p60-nav-burger]")).filter(e=>!b(e,"nav-burger"));for(let e of l){let n=e.getAttribute("aria-controls")||e.getAttribute("data-p60-nav-burger")||"",t=n?document.getElementById(n):null;if(!t)continue;let a=()=>t.classList.contains("is-open"),c=r=>{if(t.classList.toggle("is-open",r),e.classList.toggle("is-open",r),e.setAttribute("aria-expanded",String(r)),!r)for(let i of Array.from(A))t.contains(i.item)&&E(i)};e.setAttribute("aria-expanded","false"),e.addEventListener("click",r=>{r.stopPropagation(),c(!a())}),t.addEventListener("click",r=>{r.target instanceof Element&&r.target.closest("a")&&c(!1)}),document.addEventListener("keydown",r=>{r.key==="Escape"&&a()&&(c(!1),e.focus())}),document.addEventListener("click",r=>{a()&&r.target instanceof Node&&!t.contains(r.target)&&!e.contains(r.target)&&c(!1)}),window.addEventListener("resize",()=>{a()&&e.offsetParent===null&&c(!1)})}k||(k=!0,document.addEventListener("click",e=>{for(let n of Array.from(A))(!(e.target instanceof Node)||!n.item.contains(e.target))&&E(n)}))}var Q=[["reveal",R],["counter",_],["progress",D],["countdown",V],["accordion",$],["carousel",Y],["stickyHeader",K],["stickyCta",X],["lightbox",z],["tabs",U],["nav",J]];function Z(o=document){document.documentElement.classList.add("p60-js");for(let[l,e]of Q)try{e(o)}catch(n){console.warn(`[p60] behaviour "${l}" failed to initialise; the others still run`,n)}}return O(ee);})();
|
|
2
|
+
"use strict";var p60Behaviors=(()=>{var N=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var F=(i,d)=>{for(var e in d)N(i,e,{get:d[e],enumerable:!0})},O=(i,d,e,n)=>{if(d&&typeof d=="object"||typeof d=="function")for(let t of B(d))!R.call(i,t)&&t!==e&&N(i,t,{get:()=>d[t],enumerable:!(n=C(d,t))||n.enumerable});return i};var _=i=>O(N({},"__esModule",{value:!0}),i);var se={};F(se,{initBehaviors:()=>ie});var M=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;function v(i,d){let e=`data-p60-bound-${d}`;return i.hasAttribute(e)?!0:(i.setAttribute(e,""),!1)}var q=.15;function H(i,d){if(i.length===0)return;if(M()||!("IntersectionObserver"in window)){i.forEach(d);return}let e=new IntersectionObserver(n=>{for(let t of n){if(!t.isIntersecting)continue;(t.boundingClientRect.height>=window.innerHeight||t.intersectionRatio>=q-.001)&&(e.unobserve(t.target),d(t.target))}},{rootMargin:"0px 0px -10% 0px",threshold:[0,q]});i.forEach(n=>e.observe(n))}function D(i){let d=Array.from(i.querySelectorAll("[data-p60-reveal-group]")).filter(n=>!v(n,"reveal-group"));for(let n of d)Array.from(n.children).forEach((t,r)=>{t.style.setProperty("--p60-reveal-index",String(r)),t.classList.add("p60-reveal-child")});H(d,n=>n.classList.add("is-revealed"));let e=Array.from(i.querySelectorAll("[data-p60-reveal]")).filter(n=>!v(n,"reveal"));H(e,n=>n.classList.add("is-revealed"))}var $=/\d[\d, ]*(?:\.\d+)?/;function V(i){let d=Array.from(i.querySelectorAll("[data-p60-count]")).filter(e=>!v(e,"count"));H(d,e=>{e.classList.add("is-revealed");let n=e.textContent??"",t=$.exec(n);if(!t)return;let r=t[0].replace(/[, ]+$/,""),l=n.slice(0,t.index),o=n.slice(t.index+r.length),s=/[, ]/.test(r),c=r.includes(".")?r.length-r.indexOf(".")-1:0,a=Number.parseFloat(r.replace(/[, ]/g,"")),u=Number.parseFloat(e.getAttribute("data-p60-count")??""),m=Number.isFinite(u)?u:a;if(!Number.isFinite(m)||M())return;let f=Math.min(Number.parseInt(e.getAttribute("data-p60-count-duration")??"1200",10)||1200,4e3),p=new Intl.NumberFormat(document.documentElement.lang||void 0,{useGrouping:s,minimumFractionDigits:c,maximumFractionDigits:c}),b=performance.now(),E=y=>{let A=Math.min((y-b)/f,1),g=1-Math.pow(1-A,3);e.textContent=A<1?l+p.format(m*g)+o:n,A<1&&requestAnimationFrame(E)};requestAnimationFrame(E)})}function Y(i){let d=Array.from(i.querySelectorAll("[data-p60-progress]")).filter(e=>!v(e,"progress"));if(!M()){for(let e of d){let n=e.style.width||getComputedStyle(e).width;e.style.transition="none",e.style.width="0px",e.offsetWidth,e.style.transition="width 1.1s cubic-bezier(0.22, 1, 0.36, 1)",e.dataset.p60ProgressTarget=n}H(d,e=>{e.classList.add("is-revealed"),e.style.width=e.dataset.p60ProgressTarget??""})}}function z(i){let d=Array.from(i.querySelectorAll("[data-p60-countdown]")).filter(e=>!v(e,"countdown"));for(let e of d){let n=Date.parse(e.getAttribute("data-p60-until")??"");if(!Number.isFinite(n))continue;let t=e.getAttribute("data-p60-countdown")||"dhm",r=t==="dhms",l=()=>{let s=n-Date.now();if(s<=0){e.classList.add("is-elapsed");let b=e.getAttribute("data-p60-elapsed-text");b&&(e.textContent=b),window.clearInterval(o);return}let c=Math.floor(s/1e3),a=Math.floor(c/86400),u=Math.floor(c%86400/3600),m=Math.floor(c%3600/60),f=c%60,p=[];t.includes("d")&&a>0&&p.push(`${a}d`),p.push(`${u}h`,`${m}m`),r&&p.push(`${f}s`),e.textContent=p.join(" ")},o=window.setInterval(l,r?1e3:15e3);l()}}function K(i){let d=Array.from(i.querySelectorAll("[data-p60-accordion]")).filter(e=>!v(e,"accordion"));for(let e of d){let n=e.getAttribute("data-p60-accordion")!=="multi",t=Array.from(e.querySelectorAll("details"));for(let r of t){r.addEventListener("toggle",()=>{if(r.classList.toggle("is-open",r.open),r.open&&n)for(let s of t)s!==r&&s.open&&(s.open=!1)}),r.open&&r.classList.add("is-open");let o=r.querySelector("summary")?.nextElementSibling;o&&!M()&&r.addEventListener("toggle",()=>{if(!r.open)return;let s=o.scrollHeight;o.style.overflow="hidden",o.animate([{height:"0px",opacity:"0"},{height:`${s}px`,opacity:"1"}],{duration:260,easing:"ease-out"}).addEventListener("finish",()=>{o.style.overflow=""})})}}}function U(i){let d=Array.from(i.querySelectorAll("[data-p60-carousel]")).filter(e=>!v(e,"carousel"));for(let e of d){let n=Array.from(e.querySelectorAll("[data-p60-slide]"));if(n.length<2)continue;let t=Math.max(0,n.findIndex(g=>g.classList.contains("is-active"))),r=[],l=document.createElement("nav");l.className="p60-carousel-dots",l.setAttribute("aria-label","Slides"),n.forEach((g,L)=>{let h=document.createElement("button");h.type="button",h.className="p60-carousel-dot",h.setAttribute("aria-label",`Go to slide ${L+1}`),h.addEventListener("click",()=>o(L,!0)),r.push(h),l.appendChild(h)}),e.appendChild(l),e.setAttribute("aria-roledescription","carousel"),e.hasAttribute("tabindex")||e.setAttribute("tabindex","0");let o=(g,L=!1)=>{t=(g+n.length)%n.length,n.forEach((h,x)=>{h.classList.toggle("is-active",x===t),x===t?(h.removeAttribute("aria-hidden"),h.removeAttribute("inert")):(h.setAttribute("aria-hidden","true"),h.setAttribute("inert",""))}),r.forEach((h,x)=>{h.classList.toggle("p60-carousel-dot--active",x===t),h.setAttribute("aria-current",x===t?"true":"false")}),e.style.setProperty("--p60-carousel-index",String(t)),L&&E()};e.querySelectorAll("[data-p60-carousel-prev]").forEach(g=>g.addEventListener("click",()=>o(t-1,!0))),e.querySelectorAll("[data-p60-carousel-next]").forEach(g=>g.addEventListener("click",()=>o(t+1,!0))),e.addEventListener("keydown",g=>{g.key==="ArrowLeft"&&o(t-1,!0),g.key==="ArrowRight"&&o(t+1,!0)});let s=0,c=0;e.addEventListener("pointerdown",g=>{s=g.clientX,c=g.clientY}),e.addEventListener("pointerup",g=>{let L=g.clientX-s,h=g.clientY-c;Math.abs(L)>40&&Math.abs(L)>Math.abs(h)&&o(t+(L<0?1:-1),!0)});let a=!1,u=!1,m,f=e.getAttribute("data-p60-carousel")==="auto"&&!M(),p=Math.max(Number.parseInt(e.getAttribute("data-p60-interval")??"6000",10)||6e3,3e3),b=()=>{m!==void 0&&window.clearInterval(m),m=void 0},E=()=>{b(),!(!f||u||M())&&(m=window.setInterval(()=>{!a&&!document.hidden&&o(t+1)},p))},y=null,A=g=>{u=g,e.classList.toggle("is-paused",u),y&&(y.setAttribute("aria-label",u?"Start slide rotation":"Stop slide rotation"),y.classList.toggle("p60-carousel-pause--stopped",u)),E()};f&&(y=document.createElement("button"),y.type="button",y.className="p60-carousel-pause",y.setAttribute("aria-label","Stop slide rotation"),y.addEventListener("click",()=>A(!u)),l.insertBefore(y,l.firstChild)),e.addEventListener("mouseenter",()=>{a=!0}),e.addEventListener("mouseleave",()=>{a=!1}),e.addEventListener("focusin",g=>{g.target!==y&&m!==void 0&&A(!0)}),window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener?.("change",g=>{g.matches&&A(!0)}),o(t),E()}}function X(i){let d=Array.from(i.querySelectorAll("[data-p60-sticky-header]")).filter(e=>!v(e,"sticky-header"));for(let e of d){let n=e.getAttribute("data-p60-sticky-header")==="auto-hide",t=window.scrollY,r=!1,l=()=>{let o=window.scrollY;e.classList.toggle("is-condensed",o>24),n&&(o>160&&o>t?e.classList.add("is-hidden"):e.classList.remove("is-hidden")),t=o,r=!1};window.addEventListener("scroll",()=>{r||(r=!0,requestAnimationFrame(l))},{passive:!0}),l()}}function W(i){let d=Array.from(i.querySelectorAll("[data-p60-sticky-cta]")).filter(e=>!v(e,"sticky-cta"));for(let e of d){let n=Number.parseInt(e.getAttribute("data-p60-sticky-cta")??"480",10)||480,t=!1,r=()=>{e.classList.toggle("is-stuck",window.scrollY>n),t=!1};window.addEventListener("scroll",()=>{t||(t=!0,requestAnimationFrame(r))},{passive:!0}),r()}}function j(i){let d=Array.from(i.querySelectorAll("[data-p60-lightbox]")).filter(e=>!v(e,"lightbox"));for(let e of d){let n=Array.from(e.querySelectorAll("a[data-p60-lightbox-item]"));if(n.length===0)continue;let t=null,r=0,l=null,o=()=>{t?.remove(),t=null,e.classList.remove("is-open"),document.removeEventListener("keydown",c),l?.focus()},s=u=>{r=(u+n.length)%n.length;let m=n[r];if(!t)return;let f=t.querySelector(".p60-lightbox-img"),p=t.querySelector(".p60-lightbox-caption");f&&(f.src=m.href);let b=m.getAttribute("data-p60-caption")??m.querySelector("img")?.alt??"";p&&(p.textContent=b),f&&(f.alt=b)},c=u=>{if(u.key==="Escape"&&o(),u.key==="ArrowLeft"&&s(r-1),u.key==="ArrowRight"&&s(r+1),u.key==="Tab"&&t){let m=Array.from(t.querySelectorAll("button")),f=m.indexOf(document.activeElement),p=u.shiftKey?f<=0?m.length-1:f-1:f>=m.length-1?0:f+1;m[p]?.focus(),u.preventDefault()}},a=(u,m)=>{l=m,t=document.createElement("div"),t.className="p60-lightbox",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),M()&&(t.style.transition="none"),t.innerHTML='<figure><img class="p60-lightbox-img" alt=""><figcaption class="p60-lightbox-caption"></figcaption></figure>';let f=(b,E,y)=>{let A=document.createElement("button");return A.type="button",A.className=b,A.setAttribute("aria-label",E),A.addEventListener("click",y),t.appendChild(A),A};n.length>1&&(f("p60-lightbox-prev","Previous image",()=>s(r-1)),f("p60-lightbox-next","Next image",()=>s(r+1)));let p=f("p60-lightbox-close","Close",o);t.addEventListener("click",b=>{b.target===t&&o()}),document.body.appendChild(t),e.classList.add("is-open"),document.addEventListener("keydown",c),s(u),p.focus()};n.forEach((u,m)=>u.addEventListener("click",f=>{f.preventDefault(),a(m,u)}))}}function G(i){let d=Array.from(i.querySelectorAll("[data-p60-tabs]")).filter(e=>!v(e,"tabs"));for(let e of d){let n=Array.from(e.querySelectorAll("[data-p60-tab]")),t=Array.from(e.querySelectorAll("[data-p60-panel]"));if(n.length===0||t.length===0)continue;let r=o=>t.find(s=>s.getAttribute("data-p60-panel")===o.getAttribute("data-p60-tab"));n[0].closest("[data-p60-tabs] > *")?.setAttribute("role","tablist");let l=(o,s=!1)=>{for(let c of n){let a=c===o;c.classList.toggle("is-active",a),c.setAttribute("aria-selected",a?"true":"false"),c.setAttribute("tabindex",a?"0":"-1")}for(let c of t){let a=c===r(o);c.classList.toggle("is-active",a),c.hidden=!a}s&&o.focus()};n.forEach((o,s)=>{o.setAttribute("role","tab"),o.addEventListener("click",c=>{c.preventDefault(),l(o)}),o.addEventListener("keydown",c=>{let a=c.key;a==="ArrowLeft"&&l(n[(s-1+n.length)%n.length],!0),a==="ArrowRight"&&l(n[(s+1)%n.length],!0),a==="Home"&&l(n[0],!0),a==="End"&&l(n[n.length-1],!0)})}),t.forEach(o=>o.setAttribute("role","tabpanel")),l(n.find(o=>o.classList.contains("is-active"))??n[0])}}var J=60,Q=280,k=()=>window.matchMedia("(hover: hover)").matches,Z='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',T=new Set,P=!1;function I(i){i.openTimer!==null&&(window.clearTimeout(i.openTimer),i.openTimer=null),i.closeTimer!==null&&(window.clearTimeout(i.closeTimer),i.closeTimer=null)}function w(i,d=!1){I(i);for(let e of Array.from(T))e!==i&&i.item.contains(e.item)&&w(e);i.item.classList.remove("is-open"),i.toggle.setAttribute("aria-expanded","false"),i.openedBy=null,T.delete(i),d&&i.toggle.focus()}function S(i,d){I(i);for(let e of Array.from(T))e!==i&&e.nav===i.nav&&!e.item.contains(i.item)&&w(e);i.item.classList.add("is-open"),i.toggle.setAttribute("aria-expanded","true"),i.openedBy=d,T.add(i)}function ee(i){for(let e of Array.from(i.querySelectorAll("[data-p60-nav]"))){let n=Array.from(e.querySelectorAll("[data-p60-nav-item]")).filter(t=>t.closest("[data-p60-nav]")===e&&!v(t,"nav-item"));for(let t of n){let r=a=>a.closest("[data-p60-nav-item]")===t,l=Array.from(t.querySelectorAll("[data-p60-nav-toggle]")).find(r)??null,o=Array.from(t.querySelectorAll("[data-p60-nav-menu]")).find(r)??null;if(!l||!o)continue;let s={nav:e,item:t,toggle:l,menu:o,openedBy:null,lastPointer:"",openTimer:null,closeTimer:null},c=()=>T.has(s);l.setAttribute("aria-expanded","false"),o.id&&!l.hasAttribute("aria-controls")&&l.setAttribute("aria-controls",o.id),t.addEventListener("mouseenter",()=>{k()&&(s.closeTimer!==null&&(window.clearTimeout(s.closeTimer),s.closeTimer=null),!c()&&s.openTimer===null&&(s.openTimer=window.setTimeout(()=>{s.openTimer=null,S(s,"hover")},J)))}),t.addEventListener("mouseleave",()=>{k()&&(s.openTimer!==null&&(window.clearTimeout(s.openTimer),s.openTimer=null),c()&&s.openedBy==="hover"&&s.closeTimer===null&&(s.closeTimer=window.setTimeout(()=>{s.closeTimer=null,w(s)},Q)))}),l.addEventListener("pointerdown",a=>{s.lastPointer=a.pointerType??""}),l.addEventListener("click",a=>{let u=a.detail===0||s.lastPointer==="touch"||s.lastPointer==="pen"||!k();if(!c()){a.preventDefault(),a.stopPropagation(),S(s,"toggle");return}!u&&s.openedBy==="hover"||(a.preventDefault(),a.stopPropagation(),w(s))}),l.addEventListener("keydown",a=>{a.key==="ArrowDown"&&(a.preventDefault(),c()||S(s,"toggle"),o.querySelector(Z)?.focus())}),t.addEventListener("keydown",a=>{a.key==="Escape"&&c()&&(a.stopPropagation(),w(s,!0))}),t.addEventListener("focusin",a=>{a.target instanceof Node&&o.contains(a.target)&&!c()&&S(s,"focus")}),t.addEventListener("focusout",a=>{let u=a.relatedTarget;u instanceof Node&&!t.contains(u)&&c()&&w(s)})}if(!v(e,"nav-fit")){let r=f=>{for(let p=1;p<=3;p++)e.classList.toggle(`is-fit-${p}`,p<=f)},l=f=>{let p=f.getBoundingClientRect();return p.width>0||p.height>0?[f]:Array.from(f.children).flatMap(l)},o=f=>{let p=-1/0;for(let b of f.flatMap(l)){let E=b.getBoundingClientRect();if(p!==-1/0&&E.top>=p-2)return!0;p=Math.max(p,E.bottom)}return!1},s=()=>{if(e.scrollWidth>e.clientWidth+1||o(Array.from(e.children)))return!1;let f=e.parentElement;return!(f&&getComputedStyle(f).flexWrap==="wrap"&&o(Array.from(f.children)))},c=0,a=()=>{if(c=performance.now(),e.offsetParent===null||!getComputedStyle(e).flexDirection.startsWith("row")){r(0);return}r(0);let f=0;for(;!s()&&f<3;)r(++f);c=performance.now()},u=!1,m=()=>{u||(u=!0,requestAnimationFrame(()=>{u=!1,a()}))};window.addEventListener("resize",m,{passive:!0}),document.fonts?.ready.then(m).catch(()=>{}),document.fonts?.addEventListener("loadingdone",m),"ResizeObserver"in window&&new ResizeObserver(()=>{performance.now()-c>120&&m()}).observe(e),a()}}let d=Array.from(i.querySelectorAll("[data-p60-nav-burger]")).filter(e=>!v(e,"nav-burger"));for(let e of d){let n=e.getAttribute("aria-controls")||e.getAttribute("data-p60-nav-burger")||"",t=n?document.getElementById(n):null;if(!t)continue;let r=()=>t.classList.contains("is-open"),l=o=>{if(t.classList.toggle("is-open",o),e.classList.toggle("is-open",o),e.setAttribute("aria-expanded",String(o)),!o)for(let s of Array.from(T))t.contains(s.item)&&w(s)};e.setAttribute("aria-expanded","false"),e.addEventListener("click",o=>{o.stopPropagation(),l(!r())}),t.addEventListener("click",o=>{o.target instanceof Element&&o.target.closest("a")&&l(!1)}),document.addEventListener("keydown",o=>{o.key==="Escape"&&r()&&(l(!1),e.focus())}),document.addEventListener("click",o=>{r()&&o.target instanceof Node&&!t.contains(o.target)&&!e.contains(o.target)&&l(!1)}),window.addEventListener("resize",()=>{r()&&e.offsetParent===null&&l(!1)})}P||(P=!0,document.addEventListener("click",e=>{for(let n of Array.from(T))(!(e.target instanceof Node)||!n.item.contains(e.target))&&w(n)}))}function te(i){let d=Array.from(i.querySelectorAll("[data-p60-reading-progress]")).filter(e=>!v(e,"reading-progress"));for(let e of d){let n=e.getAttribute("data-p60-reading-target"),t=n?document.getElementById(n):null;if(n&&!t)continue;e.setAttribute("role","progressbar"),e.setAttribute("aria-valuemin","0"),e.setAttribute("aria-valuemax","100"),!e.hasAttribute("aria-label")&&!e.hasAttribute("aria-labelledby")&&e.setAttribute("aria-label","Reading progress");let r=!1,l=()=>{let s=window.innerHeight,c=0,a=Math.max(document.documentElement.scrollHeight-s,0);if(t){let m=t.getBoundingClientRect();c=m.top+window.scrollY,a=Math.max(m.height-s,0)}let u=a===0?window.scrollY>=c?1:0:Math.min(Math.max((window.scrollY-c)/a,0),1);e.style.setProperty("--p60-reading-progress",u.toFixed(4)),e.setAttribute("aria-valuenow",String(Math.round(u*100))),e.classList.toggle("is-started",u>0),e.classList.toggle("is-complete",u>=1),r=!1},o=()=>{r||(r=!0,requestAnimationFrame(l))};window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o,{passive:!0}),l()}}function ne(i){let d=Array.from(i.querySelectorAll("[data-p60-show-more]")).filter(e=>!v(e,"show-more"));for(let e of d){let n=Math.max(Number.parseInt(e.getAttribute("data-p60-show-more")??"6",10)||6,1),t=Array.from(e.querySelectorAll("[data-p60-show-more-item]")),r=e.querySelector("[data-p60-show-more-toggle]")??(e.id?document.querySelector(`[data-p60-show-more-toggle][aria-controls="${e.id}"]`):null);if(!r)continue;let l=t.slice(n);if(l.length===0){r.hidden=!0;continue}let o=r.textContent??"",s=r.getAttribute("data-p60-show-more-toggle")??"";e.id&&!r.hasAttribute("aria-controls")&&r.setAttribute("aria-controls",e.id);let c=a=>{for(let u of l)u.hidden=!a;e.classList.toggle("is-expanded",a),r.setAttribute("aria-expanded",a?"true":"false"),s&&(r.textContent=a?s:o)};r.addEventListener("click",()=>c(r.getAttribute("aria-expanded")!=="true")),c(e.classList.contains("is-expanded"))}}function oe(i){let d=Array.from(i.querySelectorAll("[data-p60-scrollspy]")).filter(e=>!v(e,"scrollspy"));for(let e of d){let n=Number.parseInt(e.getAttribute("data-p60-scrollspy")??"96",10)||96,t=[];for(let s of Array.from(e.querySelectorAll('a[href^="#"]'))){let c=decodeURIComponent((s.getAttribute("href")??"").slice(1)),a=c?document.getElementById(c):null;a&&t.push({link:s,target:a})}if(t.length===0)continue;let r=!1,l=()=>{let s=null,c=-1/0;for(let{link:a,target:u}of t){let m=u.getBoundingClientRect().top;m<=n&&m>c&&(c=m,s=a)}for(let{link:a}of t){let u=a===s;a.classList.toggle("is-current",u),u?a.setAttribute("aria-current","location"):a.removeAttribute("aria-current")}r=!1},o=()=>{r||(r=!0,requestAnimationFrame(l))};window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o,{passive:!0}),window.addEventListener("hashchange",o),l()}}var re=[["reveal",D],["counter",V],["progress",Y],["countdown",z],["accordion",K],["carousel",U],["stickyHeader",X],["stickyCta",W],["lightbox",j],["tabs",G],["readingProgress",te],["showMore",ne],["scrollspy",oe],["nav",ee]];function ie(i=document){document.documentElement.classList.add("p60-js");for(let[d,e]of re)try{e(i)}catch(n){console.warn(`[p60] behaviour "${d}" failed to initialise; the others still run`,n)}}return _(se);})();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Font-slot resolution for the DEV preview, the same rules as the engine's fonts.ts: the template
|
|
2
|
+
// declares font knobs (slot, default family, the weights its type system uses); a chosen family
|
|
3
|
+
// becomes a --p60s-<key> variable with an honest fallback stack and ONE stylesheet URL for every
|
|
4
|
+
// chosen family at the slot's weights, clamped to the family's real weights. Served from the
|
|
5
|
+
// catalogue's provider (Bunny Fonts, the privacy-first mirror of the Google catalogue).
|
|
6
|
+
import catalogue from '../contract/v1/fonts.json' with { type: 'json' };
|
|
7
|
+
|
|
8
|
+
const byName = new Map(catalogue.families.map((f) => [f.name, f]));
|
|
9
|
+
|
|
10
|
+
export const FONT_PROVIDER_ORIGIN = new URL(catalogue.provider.cssBase).origin;
|
|
11
|
+
|
|
12
|
+
/** A catalogue-valid family name, else null. */
|
|
13
|
+
export function knownFamily(name) {
|
|
14
|
+
return typeof name === 'string' && byName.has(name) ? name : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The CSS font-family value for a slot: quoted family plus its honest generic fallback. */
|
|
18
|
+
export function fontStackFor(name) {
|
|
19
|
+
const family = byName.get(name);
|
|
20
|
+
return family ? `'${family.name}', ${family.stack}` : `'${name}', sans-serif`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function nearestWeight(target, available) {
|
|
24
|
+
let best = available[0];
|
|
25
|
+
for (const w of available) {
|
|
26
|
+
const d = Math.abs(w - target);
|
|
27
|
+
const bestD = Math.abs(best - target);
|
|
28
|
+
if (d < bestD || (d === bestD && w > best)) best = w;
|
|
29
|
+
}
|
|
30
|
+
return best;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One css2 URL covering every chosen family at its slot's clamped weights, or null. */
|
|
34
|
+
export function fontCssHref(slots) {
|
|
35
|
+
const weightsByFamily = new Map();
|
|
36
|
+
for (const slot of slots) {
|
|
37
|
+
const family = byName.get(slot.family);
|
|
38
|
+
if (!family) continue;
|
|
39
|
+
const set = weightsByFamily.get(family.name) ?? new Set();
|
|
40
|
+
const asked = slot.weights?.length > 0 ? slot.weights : [400, 700];
|
|
41
|
+
for (const weight of asked) set.add(nearestWeight(weight, family.weights));
|
|
42
|
+
weightsByFamily.set(family.name, set);
|
|
43
|
+
}
|
|
44
|
+
if (weightsByFamily.size === 0) return null;
|
|
45
|
+
const parts = [...weightsByFamily.entries()].map(([name, weights]) =>
|
|
46
|
+
`family=${name.replace(/ /g, '+')}:wght@${[...weights].sort((a, b) => a - b).join(';')}`);
|
|
47
|
+
return `${catalogue.provider.cssBase}?${parts.join('&')}&display=swap`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A template's own Google Fonts URL, rewritten to the mirror (same css2 API). */
|
|
51
|
+
export function toProvider(url) {
|
|
52
|
+
return String(url).replace('https://fonts.googleapis.com/', `${FONT_PROVIDER_ORIGIN}/`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Icon resolution for the preview, the same rule as the engine's icons.ts: content carries a
|
|
2
|
+
// Lucide icon NAME ("heart-handshake"), the platform supplies `iconSvg` on items[] entries, and
|
|
3
|
+
// the template renders it through the dialect's `raw` filter. Unknown names resolve to '' so a
|
|
4
|
+
// template's `{% if item.iconSvg %}` branch falls back to the plain name, exactly as it does live.
|
|
5
|
+
import * as lucide from 'lucide-static';
|
|
6
|
+
|
|
7
|
+
const table = lucide;
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
|
|
10
|
+
/** "heart-handshake" / "heart_handshake" / "HeartHandshake" -> "HeartHandshake" (the lucide-static key). */
|
|
11
|
+
function pascal(name) {
|
|
12
|
+
return name.trim().split(/[-_\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Resolve a Lucide icon name to its SVG string, or '' when blank or unknown. */
|
|
16
|
+
export function iconSvg(name) {
|
|
17
|
+
if (!name || typeof name !== 'string') return '';
|
|
18
|
+
const key = name.trim();
|
|
19
|
+
const cached = cache.get(key);
|
|
20
|
+
if (cached !== undefined) return cached;
|
|
21
|
+
const svg = table[pascal(key)];
|
|
22
|
+
const out = typeof svg === 'string' ? svg : '';
|
|
23
|
+
cache.set(key, out);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Add a resolved iconSvg to any items[] entries that carry an icon NAME; unchanged otherwise. */
|
|
28
|
+
export function withResolvedIcons(content) {
|
|
29
|
+
if (!content || typeof content !== 'object' || !Array.isArray(content.items)) return content;
|
|
30
|
+
const items = content.items.map((item) => {
|
|
31
|
+
if (item && typeof item === 'object' && typeof item.icon === 'string' && item.icon) {
|
|
32
|
+
return { ...item, iconSvg: iconSvg(item.icon) };
|
|
33
|
+
}
|
|
34
|
+
return item;
|
|
35
|
+
});
|
|
36
|
+
return { ...content, items };
|
|
37
|
+
}
|
|
@@ -4709,3 +4709,26 @@ textarea.p60-form-control {
|
|
|
4709
4709
|
border-radius: 10px; margin: 0 0 10px; }
|
|
4710
4710
|
.impact-map-popup-card .impact-map-card-image { width: calc(100% + 32px); margin: -14px -16px 10px;
|
|
4711
4711
|
border-radius: calc(var(--radius-sm) - 1px) calc(var(--radius-sm) - 1px) 0 0; }
|
|
4712
|
+
|
|
4713
|
+
/* ---- Public listings (docs/reference/public-listings.md) ----
|
|
4714
|
+
Every list is one page at a time; these are the shared "more" affordances: the SSR pager on
|
|
4715
|
+
the campaigns index, the events filter row's gap, and the volunteer chooser's "Show more" row. */
|
|
4716
|
+
.listing-pager {
|
|
4717
|
+
display: flex;
|
|
4718
|
+
flex-wrap: wrap;
|
|
4719
|
+
align-items: center;
|
|
4720
|
+
justify-content: center;
|
|
4721
|
+
gap: 1rem;
|
|
4722
|
+
margin-top: 1.75rem;
|
|
4723
|
+
}
|
|
4724
|
+
.listing-pager-status {
|
|
4725
|
+
color: var(--muted);
|
|
4726
|
+
font-size: 0.9rem;
|
|
4727
|
+
}
|
|
4728
|
+
.event-filters-gap {
|
|
4729
|
+
flex: 1 1 auto;
|
|
4730
|
+
}
|
|
4731
|
+
.vol-more {
|
|
4732
|
+
justify-content: center;
|
|
4733
|
+
text-align: center;
|
|
4734
|
+
}
|
|
@@ -16,7 +16,9 @@ import sectionCatalogue from '../contract/v1/sections.json' with { type: 'json'
|
|
|
16
16
|
import islandRegistry from '../contract/v1/islands.json' with { type: 'json' };
|
|
17
17
|
import contextContract from '../contract/v1/context.json' with { type: 'json' };
|
|
18
18
|
import { resolveFixtureArt } from './fixture-art.mjs';
|
|
19
|
-
import { buildSiteFixture, applyPreviewContent } from './site-context.mjs';
|
|
19
|
+
import { buildSiteFixture, applyPreviewContent, pageComposition, PAGE_KEYS } from './site-context.mjs';
|
|
20
|
+
import { withResolvedIcons } from './icons.mjs';
|
|
21
|
+
import { FONT_PROVIDER_ORIGIN, fontCssHref, fontStackFor, knownFamily, toProvider } from './fonts.mjs';
|
|
20
22
|
import { normaliseFocus, previewActions, withResolvedActions, applyFocus } from './focus.mjs';
|
|
21
23
|
|
|
22
24
|
// Fixture imagery resolved for the SEALED studio render (p60fixture: refs become inline-SVG data
|
|
@@ -351,6 +353,13 @@ function courseListingSkeleton(fx = STUDIO_FX) {
|
|
|
351
353
|
</div></section>`;
|
|
352
354
|
}
|
|
353
355
|
|
|
356
|
+
/** The platform's own About body, for a template that does not declare the about page: the
|
|
357
|
+
* composition's sections listed as skeletons, the way the platform renders them itself live. */
|
|
358
|
+
function aboutSkeleton(fx, composition = []) {
|
|
359
|
+
const rows = composition.map((s) => `<article class="event-card"><div class="event-card-body"><h3>${escapeHtml(s.type)}</h3><p class="event-card-when">rendered by the platform</p></div></article>`).join('');
|
|
360
|
+
return `${surfaceDivider('about')}<section class="section"><div class="container"><h1>About us</h1>${rows}</div></section>`;
|
|
361
|
+
}
|
|
362
|
+
|
|
354
363
|
// surface → { pageTemplate to prefer when the theme declares it, builtin skeleton }
|
|
355
364
|
const SURFACES = {
|
|
356
365
|
events: { template: 'events', builtin: eventsListingSkeleton },
|
|
@@ -366,14 +375,14 @@ const SURFACES = {
|
|
|
366
375
|
};
|
|
367
376
|
|
|
368
377
|
/** The routed dev preview's surface names (plus 'home'). */
|
|
369
|
-
export const PREVIEW_SURFACES = ['home', ...Object.keys(SURFACES)];
|
|
378
|
+
export const PREVIEW_SURFACES = ['home', 'about', ...Object.keys(SURFACES)];
|
|
370
379
|
|
|
371
|
-
function surfaceBar(active, focus = 'donate') {
|
|
380
|
+
function surfaceBar(active, focus = 'donate', looks = [], activeLook = null) {
|
|
372
381
|
const withFocus = (href) => (focus === 'donate' ? href : `${href}${href.includes('?') ? '&' : '?'}focus=${focus}`);
|
|
373
382
|
const link = (name, href) =>
|
|
374
383
|
`<a href="${withFocus(href)}"${name === active ? ' style="font-weight:700;text-decoration:underline"' : ''}>${name}</a>`;
|
|
375
384
|
const links = [
|
|
376
|
-
link('home', '/'), link('events', '/events'), link('event', '/events?event=fixture'),
|
|
385
|
+
link('home', '/'), link('about', '/about'), link('events', '/events'), link('event', '/events?event=fixture'),
|
|
377
386
|
link('services', '/services'), link('service', '/services?service=fixture'),
|
|
378
387
|
link('donate', '/donate'), link('articles', '/articles'), link('article', '/articles/fixture'),
|
|
379
388
|
link('campaigns', '/campaigns'), link('campaign', '/campaigns/fixture'), link('course', '/courses'),
|
|
@@ -384,11 +393,21 @@ function surfaceBar(active, focus = 'donate') {
|
|
|
384
393
|
const focusLink = (value, label) =>
|
|
385
394
|
`<a href="${value === 'donate' ? '/' : `/?focus=${value}`}"${value === focus ? ' style="font-weight:700;text-decoration:underline"' : ''}>${label}</a>`;
|
|
386
395
|
const focusLinks = [focusLink('donate', 'giving'), focusLink('volunteer', 'volunteering'), focusLink('none', 'buttons only')];
|
|
396
|
+
// Looks: the author's one-click bundles, switchable here the way a charity switches them in
|
|
397
|
+
// Appearance; ?look= on any URL, and ?p60s-<key>=<value> for a single knob.
|
|
398
|
+
const lookLink = (name, href) =>
|
|
399
|
+
`<a href="${href}"${name === activeLook ? ' style="font-weight:700;text-decoration:underline"' : ''}>${escapeHtml(name)}</a>`;
|
|
400
|
+
const lookLinks = looks.length
|
|
401
|
+
? [lookLink(null, '/').replace('>null<', '>defaults<'), ...looks.map((l) => lookLink(l.name, `/?look=${encodeURIComponent(l.name)}`))]
|
|
402
|
+
: [];
|
|
387
403
|
return `<nav class="p60-preview-surfaces" aria-label="Preview surfaces" style="position:sticky;top:0;z-index:99;display:flex;gap:12px;flex-wrap:wrap;padding:8px 14px;font:12px/1.4 system-ui,sans-serif;background:#0b1220;color:#e6e9f2;opacity:.94">
|
|
388
404
|
<strong style="letter-spacing:.06em;text-transform:uppercase;font-size:10px">Surfaces</strong>${links.join('')}
|
|
389
405
|
<span style="flex-basis:100%;height:0"></span>
|
|
390
406
|
<strong style="letter-spacing:.06em;text-transform:uppercase;font-size:10px">Leads with</strong>${focusLinks.join('')}
|
|
391
|
-
<span style="opacity:.7">the hero widget becomes the donation widget, the volunteer sign-up, or nothing; the buttons follow</span
|
|
407
|
+
<span style="opacity:.7">the hero widget becomes the donation widget, the volunteer sign-up, or nothing; the buttons follow</span>${lookLinks.length ? `
|
|
408
|
+
<span style="flex-basis:100%;height:0"></span>
|
|
409
|
+
<strong style="letter-spacing:.06em;text-transform:uppercase;font-size:10px">Looks</strong>${lookLinks.join('')}
|
|
410
|
+
<span style="opacity:.7">?look=name, or ?p60s-key=value for one knob</span>` : ''}
|
|
392
411
|
</nav>`;
|
|
393
412
|
}
|
|
394
413
|
|
|
@@ -402,23 +421,37 @@ function partsToHtml(html, contentHtml, ctx = {}, fx = STUDIO_FX) {
|
|
|
402
421
|
return out;
|
|
403
422
|
}
|
|
404
423
|
|
|
405
|
-
/**
|
|
406
|
-
*
|
|
407
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Knob values → the same body attributes / CSS vars TemplateHost stamps, with the same guards: an
|
|
426
|
+
* override (a Look, or ?p60s-<key>=) wins over the default; a select value that is not one of the
|
|
427
|
+
* knob's options falls back to the default; a font must be a catalogue family. Font slots come
|
|
428
|
+
* back too, so the dev preview can load the one stylesheet production would (the studio stays
|
|
429
|
+
* sealed and renders the fallback stacks).
|
|
430
|
+
*/
|
|
431
|
+
function knobValues(manifest, overrides = {}) {
|
|
408
432
|
const attrs = [];
|
|
409
433
|
const vars = [];
|
|
434
|
+
const fontSlots = [];
|
|
410
435
|
for (const knob of manifest?.settings?.schema ?? []) {
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
436
|
+
const raw = overrides[knob.key] ?? knob.default;
|
|
437
|
+
if (raw == null || raw === '') continue;
|
|
438
|
+
const value = String(raw);
|
|
413
439
|
if (knob.kind === 'color') {
|
|
414
|
-
if (/^#[0-9a-fA-F]{3,8}$/.test(
|
|
440
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(value)) vars.push(`--p60s-${knob.key}: ${value};`);
|
|
415
441
|
} else if (knob.kind === 'font') {
|
|
416
|
-
|
|
442
|
+
const family = knownFamily(value) ?? knownFamily(knob.default);
|
|
443
|
+
if (family) {
|
|
444
|
+
fontSlots.push({ family, weights: knob.weights ?? [] });
|
|
445
|
+
vars.push(`--p60s-${knob.key}: ${fontStackFor(family)};`);
|
|
446
|
+
}
|
|
417
447
|
} else {
|
|
418
|
-
|
|
448
|
+
const options = Array.isArray(knob.options) ? knob.options : null;
|
|
449
|
+
const chosen = options && !options.includes(value) ? knob.default : raw;
|
|
450
|
+
if (chosen == null || chosen === '') continue;
|
|
451
|
+
attrs.push(`data-p60s-${knob.key}="${escapeHtml(String(chosen))}"`);
|
|
419
452
|
}
|
|
420
453
|
}
|
|
421
|
-
return { attrs: attrs.join(' '), vars: vars.join(' ') };
|
|
454
|
+
return { attrs: attrs.join(' '), vars: vars.join(' '), fontSlots };
|
|
422
455
|
}
|
|
423
456
|
|
|
424
457
|
/**
|
|
@@ -446,8 +479,15 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
446
479
|
// The one content tree (content model v1): about composed from this manifest's declared
|
|
447
480
|
// sections, dev preview-content overlaid when the kit passes it (validated there), imagery
|
|
448
481
|
// resolved exactly like the rest of the fixtures.
|
|
482
|
+
const surface = options.surface ?? 'home';
|
|
483
|
+
// The section-based pages: home and about compose from supports.pages and the catalogue's page
|
|
484
|
+
// assignment (or the preview-content `pages` block, the admin-authored composition previewed).
|
|
485
|
+
// The tree's `about` is the composition of the page being rendered, as it is live.
|
|
486
|
+
const pageOf = (page) => pageComposition(manifest, page, options.previewContent);
|
|
487
|
+
const treePage = PAGE_KEYS.includes(surface) ? surface : 'home';
|
|
488
|
+
const baseSite = buildSiteFixture(manifest, { page: treePage });
|
|
449
489
|
const site = applyFocus(resolveFixtureArt(
|
|
450
|
-
applyPreviewContent(
|
|
490
|
+
applyPreviewContent({ ...baseSite, content: { ...baseSite.content, about: pageOf(treePage) } }, options.previewContent ?? null),
|
|
451
491
|
artOptions ?? {}), actions);
|
|
452
492
|
const brand = site.brand ?? fx.brand;
|
|
453
493
|
// With a content override, the TREE is the source of truth for every fixture view: the routed
|
|
@@ -481,7 +521,6 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
481
521
|
};
|
|
482
522
|
}
|
|
483
523
|
|
|
484
|
-
const surface = options.surface ?? 'home';
|
|
485
524
|
let contentHtml;
|
|
486
525
|
if (surface !== 'home' && SURFACES[surface]) {
|
|
487
526
|
// A routed platform surface: the theme's own page template when it ships one, else the
|
|
@@ -498,32 +537,54 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
498
537
|
} else {
|
|
499
538
|
contentHtml = def.builtin(fx);
|
|
500
539
|
}
|
|
540
|
+
} else if (surface === 'about' && !(manifest?.supports?.pages ?? []).includes('about')) {
|
|
541
|
+
// The template declares no about page: live, the platform's own About body renders inside the
|
|
542
|
+
// template's layout, so the preview shows that as a skeleton rather than the home composition.
|
|
543
|
+
contentHtml = aboutSkeleton(fx, pageOf('about'));
|
|
501
544
|
} else {
|
|
545
|
+
// Render one page composition: each section's Liquid over its content (the sample, or the
|
|
546
|
+
// preview-content override), icons resolved on items like the engine does, an unsupported
|
|
547
|
+
// type omitted, never an error (the contract).
|
|
548
|
+
const renderComposition = async (composition) => {
|
|
549
|
+
const out = [];
|
|
550
|
+
for (const { type, content } of composition) {
|
|
551
|
+
const entry = catalogueByType.get(type);
|
|
552
|
+
const source = files[`sections/${type}.liquid`];
|
|
553
|
+
if (!entry || source == null) continue;
|
|
554
|
+
const base = artOptions ? resolveFixtureArt(content ?? {}, artOptions) : resolveFixtureArt(content ?? {});
|
|
555
|
+
const context = {
|
|
556
|
+
// The home hero carries the resolved actions exactly as the platform hands them over, so a
|
|
557
|
+
// developer sees the widget AND the button that pairs with it. The catalogue sample's
|
|
558
|
+
// givingStyle is dropped here: in the preview the focus decides the style.
|
|
559
|
+
section: withResolvedIcons(type === 'homeHero' ? withResolvedActions({ ...base, givingStyle: undefined, actionStyle: undefined }, actions) : base),
|
|
560
|
+
brand,
|
|
561
|
+
site,
|
|
562
|
+
...(fx.sections?.[type] ?? {})
|
|
563
|
+
};
|
|
564
|
+
const rendered = await liquid.parseAndRender(source, context);
|
|
565
|
+
// Island skeletons see the SAME context the section rendered with — that is what lets the
|
|
566
|
+
// hero carousel skeleton hydrate from the section's own photo fixtures.
|
|
567
|
+
out.push(partsToHtml(rendered, '', context, fx));
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
};
|
|
502
571
|
const sectionsHtml = [];
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
brand,
|
|
514
|
-
site,
|
|
515
|
-
...(fx.sections?.[type] ?? {})
|
|
516
|
-
};
|
|
517
|
-
const rendered = await liquid.parseAndRender(source, context);
|
|
518
|
-
// Island skeletons see the SAME context the section rendered with — that is what lets the
|
|
519
|
-
// hero carousel skeleton hydrate from the section's own photo fixtures.
|
|
520
|
-
sectionsHtml.push(partsToHtml(rendered, '', context, fx));
|
|
572
|
+
if (surface === 'about') {
|
|
573
|
+
sectionsHtml.push(...(await renderComposition(pageOf('about'))));
|
|
574
|
+
} else {
|
|
575
|
+
sectionsHtml.push(...(await renderComposition(pageOf('home'))));
|
|
576
|
+
// The studio's single document (no surface requested) keeps showing everything the template
|
|
577
|
+
// ships: the about composition follows the home one under a divider.
|
|
578
|
+
if (!options.surface && (manifest?.supports?.pages ?? []).includes('about')) {
|
|
579
|
+
const about = await renderComposition(pageOf('about'));
|
|
580
|
+
if (about.length) sectionsHtml.push(`<div class="p60-preview-divider" role="note">page: about</div>`, ...about);
|
|
581
|
+
}
|
|
521
582
|
}
|
|
522
583
|
|
|
523
584
|
// Declared page templates render too (over their page fixtures) — the loop an author lives in
|
|
524
585
|
// covers every surface they ship, not just home sections. The routed dev preview ALSO serves
|
|
525
586
|
// each at its own path; this keeps the studio's single document complete.
|
|
526
|
-
for (const page of manifest?.supports?.pageTemplates ?? []) {
|
|
587
|
+
for (const page of surface === 'about' ? [] : (manifest?.supports?.pageTemplates ?? [])) {
|
|
527
588
|
const source = files[`pages/${page}.liquid`];
|
|
528
589
|
const fixture = fx.pages?.[page];
|
|
529
590
|
if (source == null || fixture == null) continue;
|
|
@@ -540,7 +601,7 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
540
601
|
const rendered = await liquid.parseAndRender(files['layout.liquid'], {
|
|
541
602
|
brand,
|
|
542
603
|
site,
|
|
543
|
-
nav: fx.layout.nav,
|
|
604
|
+
nav: options.previewContent?.nav ? site.nav : fx.layout.nav,
|
|
544
605
|
socials: fx.layout.socials ?? [],
|
|
545
606
|
worship: manifest?.supports?.worship ? (fx.layout.worship ?? null) : null,
|
|
546
607
|
locale: fx.layout.locale
|
|
@@ -551,7 +612,16 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
551
612
|
}
|
|
552
613
|
|
|
553
614
|
const themeCss = files['assets/theme.css'] ?? '';
|
|
554
|
-
const { attrs, vars } =
|
|
615
|
+
const { attrs, vars, fontSlots } = knobValues(manifest, options.knobs ?? {});
|
|
616
|
+
// Webfonts: the DEV preview loads the same stylesheets production would (the template's own
|
|
617
|
+
// manifest.fonts through the provider mirror, plus one for the chosen font knobs) so type is
|
|
618
|
+
// judged for real locally; the studio render stays network-dead and shows the fallback stacks.
|
|
619
|
+
const webfonts = options.webfonts
|
|
620
|
+
? [...(manifest?.fonts ?? []).map(toProvider), ...(fontCssHref(fontSlots) ? [fontCssHref(fontSlots)] : [])]
|
|
621
|
+
: [];
|
|
622
|
+
const fontLinks = webfonts.length
|
|
623
|
+
? `<link rel="preconnect" href="${FONT_PROVIDER_ORIGIN}" crossorigin>\n${webfonts.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('\n')}`
|
|
624
|
+
: '';
|
|
555
625
|
|
|
556
626
|
// The kit's dev server passes the platform's own behaviour runtime (a self-contained bundle) so
|
|
557
627
|
// authors see their carousels, reveals and tabs living locally. The document stays network-dead
|
|
@@ -573,10 +643,15 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
573
643
|
for (const origin of options.contentImageOrigins ?? []) {
|
|
574
644
|
imgOrigins.add(origin);
|
|
575
645
|
}
|
|
646
|
+
// The author's own photographs beside the template (a preview/ folder the dev server serves)
|
|
647
|
+
// are same-origin; the studio never admits them, there is no such folder to serve there.
|
|
648
|
+
if (options.localImages) imgOrigins.add("'self'");
|
|
576
649
|
const imgSrc = ['data:', ...imgOrigins].join(' ');
|
|
650
|
+
const styleSrc = webfonts.length ? `'unsafe-inline' ${FONT_PROVIDER_ORIGIN}` : "'unsafe-inline'";
|
|
651
|
+
const fontSrc = webfonts.length ? ` font-src ${FONT_PROVIDER_ORIGIN};` : '';
|
|
577
652
|
const csp = runtime
|
|
578
|
-
? `default-src 'none'; style-src
|
|
579
|
-
: `default-src 'none'; style-src
|
|
653
|
+
? `default-src 'none'; style-src ${styleSrc}; img-src ${imgSrc};${fontSrc} script-src 'unsafe-inline';`
|
|
654
|
+
: `default-src 'none'; style-src ${styleSrc}; img-src ${imgSrc};${fontSrc}`;
|
|
580
655
|
const motionApproximation = runtime ? '' : `
|
|
581
656
|
@media (prefers-reduced-motion: no-preference) {
|
|
582
657
|
[data-p60-carousel] > [data-p60-slide], .p60-preview-slide { animation: p60-preview-crossfade 8s infinite; }
|
|
@@ -591,6 +666,7 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
591
666
|
<!-- Network-dead by design: the template's CSS can style, never fetch. -->
|
|
592
667
|
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
593
668
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
669
|
+
${fontLinks}
|
|
594
670
|
<title>${escapeHtml(manifest?.label ?? manifest?.name ?? 'Template preview')} — studio preview</title>
|
|
595
671
|
<style>${vars ? `:root { ${vars} }` : ''}
|
|
596
672
|
.p60-preview-badge { display: inline-flex; align-items: center; width: fit-content; margin: 0 0 8px;
|
|
@@ -607,7 +683,7 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
607
683
|
<style>${themeCss}</style>
|
|
608
684
|
</head>
|
|
609
685
|
<body ${attrs}>
|
|
610
|
-
${options.surface ? surfaceBar(surface, focus) : ''}
|
|
686
|
+
${options.surface ? surfaceBar(surface, focus, manifest?.looks ?? [], options.look ?? null) : ''}
|
|
611
687
|
${bodyHtml}
|
|
612
688
|
${runtime ? `<script>${runtime}\np60Behaviors.initBehaviors();</script>` : ''}
|
|
613
689
|
</body>
|