@port60/template-kit 0.17.0 → 0.19.0
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 +1 -1
- package/src/commands/dev.mjs +3 -1
- package/src/vendor/contract/v1/behaviours.json +12 -1
- package/src/vendor/contract/v1/context.json +2 -2
- package/src/vendor/contract/v1/islands.json +3 -3
- package/src/vendor/contract/v1/manifest.schema.json +1 -1
- package/src/vendor/validator/behaviors-runtime.js +1 -1
- package/src/vendor/validator/focus.mjs +49 -0
- package/src/vendor/validator/preview.mjs +36 -9
- package/src/vendor/validator/validate.mjs +19 -18
- package/starter/manifest.json +2 -2
- package/starter/sections/homeHero.liquid +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@port60/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready: every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/dev.mjs
CHANGED
|
@@ -132,7 +132,9 @@ export async function dev(args) {
|
|
|
132
132
|
fixtureImageBase,
|
|
133
133
|
previewContent,
|
|
134
134
|
contentImageOrigins: imageOriginsOf(previewContent),
|
|
135
|
-
surface: surfaceFor(req.url ?? '/')
|
|
135
|
+
surface: surfaceFor(req.url ?? '/'),
|
|
136
|
+
// ?focus=donate|volunteer|none: what leads, so the hero shows each widget and its buttons.
|
|
137
|
+
focus: url.searchParams.get('focus') ?? undefined
|
|
136
138
|
});
|
|
137
139
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
138
140
|
res.end(html);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "The behaviour catalogue: engine-attached JavaScript a template opts into with data attributes on its own markup. Templates never ship code; they author complete static markup and declare behaviours in supports.behaviors. The engine adds the p60-js class to the root element when its script runs; any CSS that hides pre-behaviour content MUST be scoped under .p60-js so the no-JS render stays complete. All behaviours honour prefers-reduced-motion centrally. State classes are toggled by the engine; their appearance is entirely the template's CSS.",
|
|
2
|
+
"description": "The behaviour catalogue: engine-attached JavaScript a template opts into with data attributes on its own markup. Templates never ship code; they author complete static markup and declare behaviours in supports.behaviors. The engine adds the p60-js class to the root element when its script runs; any CSS that hides pre-behaviour content MUST be scoped under .p60-js so the no-JS render stays complete. All behaviours honour prefers-reduced-motion centrally. State classes are toggled by the engine; their appearance is entirely the template's CSS. Each entry's primaryAttribute is the opt-in attribute: the validator derives its declaration checks from it and the reference documents it, so the catalogue is the single source.",
|
|
3
3
|
"behaviours": [
|
|
4
4
|
{
|
|
5
5
|
"name": "reveal",
|
|
6
6
|
"status": "available",
|
|
7
7
|
"label": "Reveal on scroll",
|
|
8
|
+
"primaryAttribute": "data-p60-reveal",
|
|
8
9
|
"description": "Adds is-revealed when the element first enters the viewport; the template's CSS does the actual motion. data-p60-reveal-group on a container staggers its children by stamping --p60-reveal-index on each (use it in a transition-delay calc). Reduced motion: is-revealed applies immediately on load.",
|
|
9
10
|
"attributes": [
|
|
10
11
|
{ "attr": "data-p60-reveal", "value": "optional variant name for the CSS to key on" },
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"name": "counter",
|
|
18
19
|
"status": "available",
|
|
19
20
|
"label": "Animated counter",
|
|
21
|
+
"primaryAttribute": "data-p60-count",
|
|
20
22
|
"description": "Counts the element's figure up from zero when first revealed. The static text IS the final value, typed exactly as it should read (\"£20\", \"95%\", \"1,200\", \"2.5k\"): the engine animates only the first run of digits and keeps the prefix, suffix, grouping and decimal places as typed, restoring the exact text on the last frame. A numeric attribute value (legacy) overrides the target; the text still sets the format. Reduced motion: the final figure shows immediately.",
|
|
21
23
|
"attributes": [
|
|
22
24
|
{ "attr": "data-p60-count", "value": "none: the element's own text is the target and the format; or a numeric target (legacy)" },
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
"name": "progress",
|
|
30
32
|
"status": "available",
|
|
31
33
|
"label": "Progress bar sweep",
|
|
34
|
+
"primaryAttribute": "data-p60-progress",
|
|
32
35
|
"description": "Animates the marked element's width from zero to its authored width when first revealed. The authored width (inline style or CSS) is the target and the no-JS render. Reduced motion: the authored width stands untouched.",
|
|
33
36
|
"attributes": [
|
|
34
37
|
{ "attr": "data-p60-progress", "value": "none; the element's own width is the target" }
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
"name": "countdown",
|
|
41
44
|
"status": "available",
|
|
42
45
|
"label": "Countdown",
|
|
46
|
+
"primaryAttribute": "data-p60-countdown",
|
|
43
47
|
"description": "Replaces the element's content with a live compact remainder to data-p60-until (\"3d 12h 45m\"). The static content is the authored date text (the no-JS render). On expiry the engine adds is-elapsed and swaps to data-p60-elapsed-text when provided. Countdowns keep ticking under reduced motion: they are information, not decoration. v1 unit labels are compact and language-neutral (d/h/m/s).",
|
|
44
48
|
"attributes": [
|
|
45
49
|
{ "attr": "data-p60-countdown", "value": "segments: dhms, dhm (default) or hm" },
|
|
@@ -53,6 +57,7 @@
|
|
|
53
57
|
"name": "accordion",
|
|
54
58
|
"status": "available",
|
|
55
59
|
"label": "Accordion",
|
|
60
|
+
"primaryAttribute": "data-p60-accordion",
|
|
56
61
|
"description": "On a container of native details elements: exclusive-open (opening one closes the others) plus smooth height animation. data-p60-accordion=\"multi\" keeps independent opening. Built on details/summary so the no-JS render is already functional and semantics are free. Reduced motion: no height animation, instant toggle.",
|
|
57
62
|
"attributes": [
|
|
58
63
|
{ "attr": "data-p60-accordion", "value": "none for exclusive-open; \"multi\" for independent" }
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
"name": "stickyHeader",
|
|
65
70
|
"status": "available",
|
|
66
71
|
"label": "Sticky header condense",
|
|
72
|
+
"primaryAttribute": "data-p60-sticky-header",
|
|
67
73
|
"description": "State classes for scroll-aware headers: past a small threshold the engine adds is-condensed; with the \"auto-hide\" value, scrolling down past 160px adds is-hidden and scrolling up removes it. The engine only ever toggles classes (rAF-throttled), what condensing or hiding looks like, and whether it animates, is entirely the template's CSS (gate transitions behind prefers-reduced-motion yourself; the classes always apply because a condensed header is layout, not decoration).",
|
|
68
74
|
"attributes": [
|
|
69
75
|
{ "attr": "data-p60-sticky-header", "value": "none for condense-only; \"auto-hide\" adds the hide-on-scroll-down pair" }
|
|
@@ -75,6 +81,7 @@
|
|
|
75
81
|
"name": "stickyCta",
|
|
76
82
|
"status": "available",
|
|
77
83
|
"label": "Sticky call-to-action bar",
|
|
84
|
+
"primaryAttribute": "data-p60-sticky-cta",
|
|
78
85
|
"description": "Adds is-stuck once the visitor scrolls past the threshold (the attribute value in px, default 480) and removes it above. The bar should DUPLICATE an action that already exists in the page (a donate button, a ticket link); because of that, this is the one behaviour whose element may be hidden unconditionally in CSS rather than .p60-js-scoped: a no-JS visitor loses nothing; the original action is still on the page.",
|
|
79
86
|
"attributes": [
|
|
80
87
|
{ "attr": "data-p60-sticky-cta", "value": "optional scroll threshold in px (default 480)" }
|
|
@@ -86,6 +93,7 @@
|
|
|
86
93
|
"name": "lightbox",
|
|
87
94
|
"status": "available",
|
|
88
95
|
"label": "Lightbox",
|
|
96
|
+
"primaryAttribute": "data-p60-lightbox",
|
|
89
97
|
"description": "On a group container: each data-p60-lightbox-item is an anchor whose href is the full image (the no-JS render simply navigates to it, already functional). The engine intercepts the click and opens an injected overlay (.p60-lightbox) with the image, a caption from data-p60-caption or the thumbnail's alt, a close button, backdrop and Escape close, arrow-key prev/next within the group, and a focus trap. Reduced motion: no transition on the overlay (the engine sets none; style the steady state).",
|
|
90
98
|
"attributes": [
|
|
91
99
|
{ "attr": "data-p60-lightbox", "value": "none; the group container" },
|
|
@@ -100,6 +108,7 @@
|
|
|
100
108
|
"name": "tabs",
|
|
101
109
|
"status": "available",
|
|
102
110
|
"label": "Tabs",
|
|
111
|
+
"primaryAttribute": "data-p60-tabs",
|
|
103
112
|
"description": "data-p60-tab=\"key\" on each tab control and data-p60-panel=\"key\" on each panel, inside a data-p60-tabs container. The engine wires the tablist/tab/tabpanel roles, aria-selected, roving tabindex, arrow/Home/End keys, toggles is-active on the active pair and hides inactive panels with the hidden attribute, so the no-JS render shows every panel stacked and complete, with no CSS scoping needed. The first tab (or the one authored with is-active) starts active.",
|
|
104
113
|
"attributes": [
|
|
105
114
|
{ "attr": "data-p60-tabs", "value": "none; the container" },
|
|
@@ -113,6 +122,7 @@
|
|
|
113
122
|
"name": "carousel",
|
|
114
123
|
"status": "available",
|
|
115
124
|
"label": "Carousel",
|
|
125
|
+
"primaryAttribute": "data-p60-carousel",
|
|
116
126
|
"description": "On a container with one data-p60-slide per child: the engine manages is-active across slides, injects a dots nav (.p60-carousel-dots with one button per slide), wires optional template-provided data-p60-carousel-prev/data-p60-carousel-next controls, adds swipe, keyboard arrows, pause on hover and focus, and hides inactive slides from assistive tech. Value \"auto\" enables auto-advance (data-p60-interval ms, min 3000, default 6000); reduced motion disables auto-advance while controls keep working. The engine stamps --p60-carousel-index on the container for track-translate designs; class-based fade on is-active is the simplest pattern. CSS hiding non-active slides MUST be .p60-js-scoped so the no-JS render shows all slides.",
|
|
117
127
|
"attributes": [
|
|
118
128
|
{ "attr": "data-p60-carousel", "value": "none for manual; \"auto\" for auto-advance" },
|
|
@@ -129,6 +139,7 @@
|
|
|
129
139
|
"name": "nav",
|
|
130
140
|
"status": "available",
|
|
131
141
|
"label": "Navigation",
|
|
142
|
+
"primaryAttribute": "data-p60-nav",
|
|
132
143
|
"description": "Your menu, made robust. You design the menu (markup, every pixel, the breakpoint); the engine supplies the interaction CSS :hover cannot: data-p60-nav on each nav root (several allowed), data-p60-nav-item on each group holding a data-p60-nav-toggle control and a data-p60-nav-menu panel (nesting allowed). Hover-capable pointers get hover intent (a short delay to open, a grace period to close) so crossing the gap between a toggle and its panel never closes it; keyboard, touch and pen toggle on the control (Enter/click, ArrowDown opens and focuses the first link; a mouse click on an already-hovered parent link navigates); Escape closes and returns focus to the toggle; a click outside or focus moving away closes; one item stays open per level and aria-expanded is kept honest. The burger: data-p60-nav-burger on a button whose aria-controls (or the attribute value) names the stacked panel; the engine toggles is-open on both, closes on Escape (returning focus), on a real navigation inside the panel, on an outside click, and when the burger disappears at a wider viewport. State is is-open on the item, the panel and the burger; under .p60-js show a panel only on is-open and keep your :hover/:focus-within rules scoped to html:not(.p60-js) for the no-JS render. Fit steps: when the root's row wraps or overflows, the engine steps is-fit-1, is-fit-2, is-fit-3 onto the root until it fits, re-measured on resize and once fonts settle, never on a stacked (column) nav; the platform's defaults shrink the member_menu pill at each step (the Port60 ID wordmark, then the text, leaving the person icon with its accessible label), and a template maps the same classes to anything else it wants to give up before the burger.",
|
|
133
144
|
"attributes": [
|
|
134
145
|
{ "attr": "data-p60-nav", "value": "none; a nav root, several allowed" },
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"section",
|
|
32
32
|
"layout"
|
|
33
33
|
],
|
|
34
|
-
"description": "What
|
|
34
|
+
"description": "What the primary_action_widget island becomes: 'donate' (default, the donation widget), 'volunteer' (the volunteer sign-up) or 'none' (the charity chose no widget in the hero). The charity's one switch (docs/volunteering.md 'Site focus'); everything below follows from it."
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "site.actions",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"availableIn": [
|
|
47
47
|
"section"
|
|
48
48
|
],
|
|
49
|
-
"description": "homeHero only: the leading action { kind, label, href }, the same as site.actions.primary. Render it as a widget with {% island '
|
|
49
|
+
"description": "homeHero only: the leading action { kind, label, href }, the same as site.actions.primary. Render it as a widget with {% island 'primary_action_widget' %} when section.actionStyle is 'widget'; that island becomes this action's widget."
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"path": "section.secondary",
|
|
@@ -283,10 +283,10 @@
|
|
|
283
283
|
]
|
|
284
284
|
},
|
|
285
285
|
{
|
|
286
|
-
"name": "
|
|
287
|
-
"label": "Primary action",
|
|
286
|
+
"name": "primary_action_widget",
|
|
287
|
+
"label": "Primary action widget",
|
|
288
288
|
"status": "available",
|
|
289
|
-
"description": "The
|
|
289
|
+
"description": "The widget that leads the site. It IS the donation widget when the charity chose giving to lead, the volunteer sign-up when volunteering leads, and nothing when the charity chose no widget in the hero; site.focus tells you which ('donate', 'volunteer' or 'none'). Place {% island 'primary_action_widget' %} in the home hero's widget slot INSTEAD of donation_widget whenever your template declares supports.focus: the platform decides which widget renders, the template never does, and the charity's switch moves it without a template change. Both widgets wear .action-card for shared treatment; .donate-card and .volunteer-card carry kind-specific styling. A template that wants a different layout per kind may branch on section.primary.kind and place donation_widget or volunteer_signup itself, but then it owns the switch.",
|
|
290
290
|
"params": [],
|
|
291
291
|
"stylingApi": [
|
|
292
292
|
".action-card",
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"volunteer"
|
|
186
186
|
]
|
|
187
187
|
},
|
|
188
|
-
"description": "The site-focus kinds this template can LEAD with (docs/volunteering.md). Declaring 'volunteer' means the home hero places the
|
|
188
|
+
"description": "The site-focus kinds this template can LEAD with (docs/volunteering.md). Declaring 'volunteer' means the home hero places the primary_action_widget island (the widget that becomes the donation widget or the volunteer sign-up as the charity chooses) or branches on section.primary.kind, so the volunteer sign-up can take the widget slot; the picker marks templates that cannot lead with volunteering. Templates that predate the setting keep rendering the donation widget, since donate is the default."
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
},
|
|
@@ -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 H=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var q=Object.prototype.hasOwnProperty;var I=(n,l)=>{for(var e in l)x(n,e,{get:l[e],enumerable:!0})},C=(n,l,e,o)=>{if(l&&typeof l=="object"||typeof l=="function")for(let t of P(l))!q.call(n,t)&&t!==e&&x(n,t,{get:()=>l[t],enumerable:!(o=H(l,t))||o.enumerable});return n};var F=n=>C(x({},"__esModule",{value:!0}),n);var Q={};I(Q,{initBehaviors:()=>J});var L=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;function b(n,l){let e=`data-p60-bound-${l}`;return n.hasAttribute(e)?!0:(n.setAttribute(e,""),!1)}function T(n,l){if(n.length===0)return;if(L()||!("IntersectionObserver"in window)){n.forEach(l);return}let e=new IntersectionObserver(o=>{for(let t of o)t.isIntersecting&&(e.unobserve(t.target),l(t.target))},{rootMargin:"0px 0px -10% 0px",threshold:.15});n.forEach(o=>e.observe(o))}function O(n){let l=Array.from(n.querySelectorAll("[data-p60-reveal-group]")).filter(o=>!b(o,"reveal-group"));for(let o of l)Array.from(o.children).forEach((t,a)=>{t.style.setProperty("--p60-reveal-index",String(a)),t.classList.add("p60-reveal-child")});T(l,o=>o.classList.add("is-revealed"));let e=Array.from(n.querySelectorAll("[data-p60-reveal]")).filter(o=>!b(o,"reveal"));T(e,o=>o.classList.add("is-revealed"))}var B=/\d[\d, ]*(?:\.\d+)?/;function D(n){let l=Array.from(n.querySelectorAll("[data-p60-count]")).filter(e=>!b(e,"count"));T(l,e=>{e.classList.add("is-revealed");let o=e.textContent??"",t=B.exec(o);if(!t)return;let a=t[0].replace(/[, ]+$/,""),c=o.slice(0,t.index),r=o.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),k=1-Math.pow(1-y,3);e.textContent=y<1?c+p.format(g*k)+r:o,y<1&&requestAnimationFrame(h)};requestAnimationFrame(h)})}function _(n){let l=Array.from(n.querySelectorAll("[data-p60-progress]")).filter(e=>!b(e,"progress"));if(!L()){for(let e of l){let o=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=o}T(l,e=>{e.classList.add("is-revealed"),e.style.width=e.dataset.p60ProgressTarget??""})}}function R(n){let l=Array.from(n.querySelectorAll("[data-p60-countdown]")).filter(e=>!b(e,"countdown"));for(let e of l){let o=Date.parse(e.getAttribute("data-p60-until")??"");if(!Number.isFinite(o))continue;let t=e.getAttribute("data-p60-countdown")||"dhm",a=t==="dhms",c=()=>{let i=o-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 V(n){let l=Array.from(n.querySelectorAll("[data-p60-accordion]")).filter(e=>!b(e,"accordion"));for(let e of l){let o=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&&o)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 $(n){let l=Array.from(n.querySelectorAll("[data-p60-carousel]")).filter(e=>!b(e,"carousel"));for(let e of l){let o=Array.from(e.querySelectorAll("[data-p60-slide]"));if(o.length<2)continue;let t=Math.max(0,o.findIndex(m=>m.classList.contains("is-active"))),a=!1,c=[],r=document.createElement("nav");r.className="p60-carousel-dots",r.setAttribute("aria-label","Slides"),o.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+o.length)%o.length,o.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 Y(n){let l=Array.from(n.querySelectorAll("[data-p60-sticky-header]")).filter(e=>!b(e,"sticky-header"));for(let e of l){let o=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),o&&(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 K(n){let l=Array.from(n.querySelectorAll("[data-p60-sticky-cta]")).filter(e=>!b(e,"sticky-cta"));for(let e of l){let o=Number.parseInt(e.getAttribute("data-p60-sticky-cta")??"480",10)||480,t=!1,a=()=>{e.classList.toggle("is-stuck",window.scrollY>o),t=!1};window.addEventListener("scroll",()=>{t||(t=!0,requestAnimationFrame(a))},{passive:!0}),a()}}function X(n){let l=Array.from(n.querySelectorAll("[data-p60-lightbox]")).filter(e=>!b(e,"lightbox"));for(let e of l){let o=Array.from(e.querySelectorAll("a[data-p60-lightbox-item]"));if(o.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+o.length)%o.length;let g=o[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};o.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()};o.forEach((f,g)=>f.addEventListener("click",u=>{u.preventDefault(),s(g,f)}))}}function z(n){let l=Array.from(n.querySelectorAll("[data-p60-tabs]")).filter(e=>!b(e,"tabs"));for(let e of l){let o=Array.from(e.querySelectorAll("[data-p60-tab]")),t=Array.from(e.querySelectorAll("[data-p60-panel]"));if(o.length===0||t.length===0)continue;let a=r=>t.find(i=>i.getAttribute("data-p60-panel")===r.getAttribute("data-p60-tab"));o[0].closest("[data-p60-tabs] > *")?.setAttribute("role","tablist");let c=(r,i=!1)=>{for(let d of o){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()};o.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(o[(i-1+o.length)%o.length],!0),s==="ArrowRight"&&c(o[(i+1)%o.length],!0),s==="Home"&&c(o[0],!0),s==="End"&&c(o[o.length-1],!0)})}),t.forEach(r=>r.setAttribute("role","tabpanel")),c(o.find(r=>r.classList.contains("is-active"))??o[0])}}var W=60,U=280,M=()=>window.matchMedia("(hover: hover)").matches,j='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',A=new Set,N=!1;function S(n){n.openTimer!==null&&(window.clearTimeout(n.openTimer),n.openTimer=null),n.closeTimer!==null&&(window.clearTimeout(n.closeTimer),n.closeTimer=null)}function E(n,l=!1){S(n);for(let e of Array.from(A))e!==n&&n.item.contains(e.item)&&E(e);n.item.classList.remove("is-open"),n.toggle.setAttribute("aria-expanded","false"),n.openedBy=null,A.delete(n),l&&n.toggle.focus()}function w(n,l){S(n);for(let e of Array.from(A))e!==n&&e.nav===n.nav&&!e.item.contains(n.item)&&E(e);n.item.classList.add("is-open"),n.toggle.setAttribute("aria-expanded","true"),n.openedBy=l,A.add(n)}function G(n){for(let e of Array.from(n.querySelectorAll("[data-p60-nav]"))){let o=Array.from(e.querySelectorAll("[data-p60-nav-item]")).filter(t=>t.closest("[data-p60-nav]")===e&&!b(t,"nav-item"));for(let t of o){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)},U)))}),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(j)?.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(n.querySelectorAll("[data-p60-nav-burger]")).filter(e=>!b(e,"nav-burger"));for(let e of l){let o=e.getAttribute("aria-controls")||e.getAttribute("data-p60-nav-burger")||"",t=o?document.getElementById(o):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)})}N||(N=!0,document.addEventListener("click",e=>{for(let o of Array.from(A))(!(e.target instanceof Node)||!o.item.contains(e.target))&&E(o)}))}function J(n=document){document.documentElement.classList.add("p60-js"),O(n),D(n),_(n),R(n),V(n),$(n),Y(n),K(n),X(n),z(n),G(n)}return F(Q);})();
|
|
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);})();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The site focus for the PREVIEW: a JavaScript twin of charity-site's lib/focus.ts, kept in step by
|
|
2
|
+
// hand (the validator is plain JS and is vendored into the kit). Same rules: the leading action is
|
|
3
|
+
// the widget in the hero; the other action is the one button in the header and beside the hero; the
|
|
4
|
+
// leading action never appears twice. The preview assumes volunteering is OPEN, so the pairing shows.
|
|
5
|
+
export const FOCUS_CHOICES = ['donate', 'volunteer', 'none'];
|
|
6
|
+
|
|
7
|
+
const ACTION = {
|
|
8
|
+
donate: { kind: 'donate', label: 'Donate', href: '/donate' },
|
|
9
|
+
volunteer: { kind: 'volunteer', label: 'Volunteer', href: '/volunteer' }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** `?focus=` from the dev server, or nothing: 'donate' is the platform default. */
|
|
13
|
+
export function normaliseFocus(value) {
|
|
14
|
+
return FOCUS_CHOICES.includes(value) ? value : 'donate';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The resolved surfaces for one preview render, the shape of site.actions on a live site. */
|
|
18
|
+
export function previewActions(focus) {
|
|
19
|
+
if (focus === 'volunteer') {
|
|
20
|
+
return { primary: ACTION.volunteer, secondary: ACTION.donate, widget: 'volunteer', header: ACTION.donate, hero: ACTION.donate };
|
|
21
|
+
}
|
|
22
|
+
if (focus === 'none') {
|
|
23
|
+
// Buttons only (a Custom setting with no widget): the hero button leads with giving.
|
|
24
|
+
return { primary: ACTION.donate, secondary: ACTION.volunteer, widget: null, header: ACTION.donate, hero: ACTION.donate };
|
|
25
|
+
}
|
|
26
|
+
return { primary: ACTION.donate, secondary: ACTION.volunteer, widget: 'donate', header: ACTION.volunteer, hero: ACTION.volunteer };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The home hero's resolved fields, as the platform computes them (lib/focus.ts withResolvedActions). */
|
|
30
|
+
export function withResolvedActions(content, actions) {
|
|
31
|
+
const style = content.actionStyle ?? content.givingStyle;
|
|
32
|
+
const buttonMode = style === 'button' || actions.widget === null;
|
|
33
|
+
return {
|
|
34
|
+
...content,
|
|
35
|
+
actionStyle: buttonMode ? 'button' : 'widget',
|
|
36
|
+
primary: actions.primary,
|
|
37
|
+
secondary: actions.secondary,
|
|
38
|
+
action: buttonMode && actions.hero === null && style === 'button' ? actions.primary : actions.hero
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The site tree with the focus applied: site.focus, site.actions, and the nav's one button. */
|
|
43
|
+
export function applyFocus(site, actions) {
|
|
44
|
+
const swapCta = (items) => Array.isArray(items)
|
|
45
|
+
? items.map((item) => (item && item.cta ? (actions.header ? { ...item, label: actions.header.label, href: actions.header.href } : null) : item)).filter(Boolean)
|
|
46
|
+
: items;
|
|
47
|
+
const nav = site.nav ? { ...site.nav, items: swapCta(site.nav.items), derived: swapCta(site.nav.derived) } : site.nav;
|
|
48
|
+
return { ...site, focus: actions.widget ?? 'none', actions, nav };
|
|
49
|
+
}
|
|
@@ -17,6 +17,7 @@ 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
19
|
import { buildSiteFixture, applyPreviewContent } from './site-context.mjs';
|
|
20
|
+
import { normaliseFocus, previewActions, withResolvedActions, applyFocus } from './focus.mjs';
|
|
20
21
|
|
|
21
22
|
// Fixture imagery resolved for the SEALED studio render (p60fixture: refs become inline-SVG data
|
|
22
23
|
// URIs the network-dead CSP can show). The dev preview may instead resolve them to the platform
|
|
@@ -143,8 +144,16 @@ function islandSkeleton(name, ctx = {}, fx = STUDIO_FX) {
|
|
|
143
144
|
<label class="newsletter-label">Email address</label><div class="newsletter-fields"><input class="newsletter-email" type="email" disabled><button class="newsletter-submit" type="button" disabled>Join our newsletter</button></div>
|
|
144
145
|
<label class="newsletter-consent"><input type="checkbox" disabled><span>Email me about our work and appeals.</span></label>
|
|
145
146
|
</form>`;
|
|
146
|
-
case '
|
|
147
|
-
|
|
147
|
+
case 'primary_action_widget':
|
|
148
|
+
// What this island becomes follows the preview's focus (fixtures.focus, from ?focus= on the
|
|
149
|
+
// dev server): the volunteer sign-up, nothing, or the default, the donation widget.
|
|
150
|
+
if (fixtures.focus === 'volunteer') {
|
|
151
|
+
return islandSkeleton('volunteer_signup', ctx, fx).replace('data-p60-preview-island="volunteer_signup"', 'data-p60-preview-island="primary_action_widget"');
|
|
152
|
+
}
|
|
153
|
+
if (fixtures.focus === 'none') {
|
|
154
|
+
return `<div class="p60-preview-empty" data-p60-preview-island="primary_action_widget">${previewNote(name)}<p>No widget here: this charity leads with buttons only (site.focus is 'none').</p></div>`;
|
|
155
|
+
}
|
|
156
|
+
return islandSkeleton('donation_widget', ctx, fx).replace('data-p60-preview-island="donation_widget"', 'data-p60-preview-island="primary_action_widget"');
|
|
148
157
|
case 'volunteer_signup':
|
|
149
158
|
return `<section class="donate-card vol-card" data-p60-preview-island="volunteer_signup">
|
|
150
159
|
${previewNote(name)}
|
|
@@ -359,18 +368,27 @@ const SURFACES = {
|
|
|
359
368
|
/** The routed dev preview's surface names (plus 'home'). */
|
|
360
369
|
export const PREVIEW_SURFACES = ['home', ...Object.keys(SURFACES)];
|
|
361
370
|
|
|
362
|
-
function surfaceBar(active) {
|
|
371
|
+
function surfaceBar(active, focus = 'donate') {
|
|
372
|
+
const withFocus = (href) => (focus === 'donate' ? href : `${href}${href.includes('?') ? '&' : '?'}focus=${focus}`);
|
|
363
373
|
const link = (name, href) =>
|
|
364
|
-
`<a href="${href}"${name === active ? ' style="font-weight:700;text-decoration:underline"' : ''}>${name}</a>`;
|
|
374
|
+
`<a href="${withFocus(href)}"${name === active ? ' style="font-weight:700;text-decoration:underline"' : ''}>${name}</a>`;
|
|
365
375
|
const links = [
|
|
366
376
|
link('home', '/'), link('events', '/events'), link('event', '/events?event=fixture'),
|
|
367
377
|
link('services', '/services'), link('service', '/services?service=fixture'),
|
|
368
378
|
link('donate', '/donate'), link('articles', '/articles'), link('article', '/articles/fixture'),
|
|
369
379
|
link('campaigns', '/campaigns'), link('campaign', '/campaigns/fixture'), link('course', '/courses'),
|
|
370
|
-
`<a href="/model" style="margin-left:auto;font-weight:700">site.content model →</a>`,
|
|
380
|
+
`<a href="${withFocus('/model')}" style="margin-left:auto;font-weight:700">site.content model →</a>`,
|
|
371
381
|
];
|
|
382
|
+
// What leads: the charity's switch, here as three links, so a developer sees the hero widget as
|
|
383
|
+
// the donation widget, as the volunteer sign-up, or absent, with the buttons following each time.
|
|
384
|
+
const focusLink = (value, label) =>
|
|
385
|
+
`<a href="${value === 'donate' ? '/' : `/?focus=${value}`}"${value === focus ? ' style="font-weight:700;text-decoration:underline"' : ''}>${label}</a>`;
|
|
386
|
+
const focusLinks = [focusLink('donate', 'giving'), focusLink('volunteer', 'volunteering'), focusLink('none', 'buttons only')];
|
|
372
387
|
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">
|
|
373
388
|
<strong style="letter-spacing:.06em;text-transform:uppercase;font-size:10px">Surfaces</strong>${links.join('')}
|
|
389
|
+
<span style="flex-basis:100%;height:0"></span>
|
|
390
|
+
<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>
|
|
374
392
|
</nav>`;
|
|
375
393
|
}
|
|
376
394
|
|
|
@@ -420,12 +438,17 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
420
438
|
// imagery to the platform CDN instead of inline-SVG art (the dev-richer half of the split).
|
|
421
439
|
const artOptions = options.fixtureImageBase ? { imageBase: options.fixtureImageBase } : null;
|
|
422
440
|
let fx = artOptions ? resolveFixtureArt(contextContract.fixtures, artOptions) : STUDIO_FX;
|
|
441
|
+
// What leads (docs/volunteering.md 'Site focus'): the dev server passes ?focus=; the studio
|
|
442
|
+
// renders the platform default. The island skeletons read it from the fixtures they are handed.
|
|
443
|
+
const focus = normaliseFocus(options.focus);
|
|
444
|
+
const actions = previewActions(focus);
|
|
445
|
+
fx = { ...fx, focus };
|
|
423
446
|
// The one content tree (content model v1): about composed from this manifest's declared
|
|
424
447
|
// sections, dev preview-content overlaid when the kit passes it (validated there), imagery
|
|
425
448
|
// resolved exactly like the rest of the fixtures.
|
|
426
|
-
const site = resolveFixtureArt(
|
|
449
|
+
const site = applyFocus(resolveFixtureArt(
|
|
427
450
|
applyPreviewContent(buildSiteFixture(manifest), options.previewContent ?? null),
|
|
428
|
-
artOptions ?? {});
|
|
451
|
+
artOptions ?? {}), actions);
|
|
429
452
|
const brand = site.brand ?? fx.brand;
|
|
430
453
|
// With a content override, the TREE is the source of truth for every fixture view: the routed
|
|
431
454
|
// platform skeletons and island skeletons re-derive their slices from the overridden site, so
|
|
@@ -481,8 +504,12 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
481
504
|
const entry = catalogueByType.get(type);
|
|
482
505
|
const source = files[`sections/${type}.liquid`];
|
|
483
506
|
if (!entry || source == null) continue;
|
|
507
|
+
const sample = artOptions ? resolveFixtureArt(entry.sample ?? {}, artOptions) : resolveFixtureArt(entry.sample ?? {});
|
|
484
508
|
const context = {
|
|
485
|
-
|
|
509
|
+
// The home hero carries the resolved actions exactly as the platform hands them over, so a
|
|
510
|
+
// developer sees the widget AND the button that pairs with it. The catalogue sample's
|
|
511
|
+
// givingStyle is dropped here: in the preview the focus decides the style.
|
|
512
|
+
section: type === 'homeHero' ? withResolvedActions({ ...sample, givingStyle: undefined, actionStyle: undefined }, actions) : sample,
|
|
486
513
|
brand,
|
|
487
514
|
site,
|
|
488
515
|
...(fx.sections?.[type] ?? {})
|
|
@@ -580,7 +607,7 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
580
607
|
<style>${themeCss}</style>
|
|
581
608
|
</head>
|
|
582
609
|
<body ${attrs}>
|
|
583
|
-
${options.surface ? surfaceBar(surface) : ''}
|
|
610
|
+
${options.surface ? surfaceBar(surface, focus) : ''}
|
|
584
611
|
${bodyHtml}
|
|
585
612
|
${runtime ? `<script>${runtime}\np60Behaviors.initBehaviors();</script>` : ''}
|
|
586
613
|
</body>
|
|
@@ -28,6 +28,19 @@ const Ajv = Ajv2020.default ?? Ajv2020;
|
|
|
28
28
|
|
|
29
29
|
export { dialect as contractDialect, sectionCatalogue, islandRegistry, contextContract, behaviourCatalogue };
|
|
30
30
|
|
|
31
|
+
// The behaviour to opt-in-attribute map is DERIVED from the catalogue (FR-5): `primaryAttribute` on
|
|
32
|
+
// each entry feeds the checks below, the generated reference and the kit, so adding a behaviour is
|
|
33
|
+
// one catalogue entry plus its runtime initialiser. The trailing word boundary keeps the original
|
|
34
|
+
// semantics: a companion attribute that starts with the primary one (data-p60-reveal-group,
|
|
35
|
+
// data-p60-nav-item) counts as the behaviour in use.
|
|
36
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
export const BEHAVIOUR_PRIMARY_ATTR = Object.fromEntries(
|
|
38
|
+
behaviourCatalogue.behaviours.map((b) => [b.name, b.primaryAttribute])
|
|
39
|
+
);
|
|
40
|
+
const PRIMARY_ATTR = Object.fromEntries(
|
|
41
|
+
behaviourCatalogue.behaviours.map((b) => [b.name, [new RegExp(`${escapeRegExp(b.primaryAttribute)}\\b`), b.primaryAttribute]])
|
|
42
|
+
);
|
|
43
|
+
|
|
31
44
|
// Templates are markup and attributes, NEVER code (docs/template-behaviours.md). These are hard
|
|
32
45
|
// errors over the RAW liquid source — even inside comments, because there is no legitimate reason
|
|
33
46
|
// for the tokens to appear at all. The handler pattern names real DOM event families rather than
|
|
@@ -100,7 +113,7 @@ export async function validateArtifact(files) {
|
|
|
100
113
|
newsletter: () => islands.has('newsletter_signup'),
|
|
101
114
|
i18n: () => islands.has('language_switch'),
|
|
102
115
|
search: () => islands.has('search'),
|
|
103
|
-
volunteering: () => islands.has('volunteer_signup') || islands.has('
|
|
116
|
+
volunteering: () => islands.has('volunteer_signup') || islands.has('primary_action_widget')
|
|
104
117
|
};
|
|
105
118
|
for (const capability of manifest?.requiresCapabilities ?? []) {
|
|
106
119
|
if (!capabilitySurface[capability]?.()) {
|
|
@@ -110,14 +123,15 @@ export async function validateArtifact(files) {
|
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
// Site focus honesty (docs/volunteering.md): a template that claims it can lead with
|
|
113
|
-
// volunteering must give the volunteer sign-up a way into the hero, either the
|
|
114
|
-
//
|
|
126
|
+
// volunteering must give the volunteer sign-up a way into the hero, either the
|
|
127
|
+
// primary_action_widget island (which becomes the sign-up when volunteering leads) or the
|
|
128
|
+
// volunteer_signup island placed directly.
|
|
115
129
|
{
|
|
116
130
|
const focusKinds = new Set(manifest?.supports?.focus ?? []);
|
|
117
131
|
if (focusKinds.has('volunteer')) {
|
|
118
132
|
const liquidSource = Object.entries(files).filter(([path]) => path.endsWith('.liquid')).map(([, s]) => s).join('\n');
|
|
119
|
-
if (!/island\s+['"]
|
|
120
|
-
errors.push("manifest: supports.focus lists 'volunteer' but no section places {% island '
|
|
133
|
+
if (!/island\s+['"]primary_action_widget['"]/.test(liquidSource) && !/island\s+['"]volunteer_signup['"]/.test(liquidSource)) {
|
|
134
|
+
errors.push("manifest: supports.focus lists 'volunteer' but no section places {% island 'primary_action_widget' %} (or 'volunteer_signup'), so the site could never lead with volunteering");
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
}
|
|
@@ -143,19 +157,6 @@ export async function validateArtifact(files) {
|
|
|
143
157
|
.filter(([path]) => path.endsWith('.liquid'))
|
|
144
158
|
.map(([, source]) => source)
|
|
145
159
|
.join('\n');
|
|
146
|
-
const PRIMARY_ATTR = {
|
|
147
|
-
reveal: [/data-p60-reveal\b/, 'data-p60-reveal'],
|
|
148
|
-
counter: [/data-p60-count\b/, 'data-p60-count'],
|
|
149
|
-
progress: [/data-p60-progress\b/, 'data-p60-progress'],
|
|
150
|
-
countdown: [/data-p60-countdown\b/, 'data-p60-countdown'],
|
|
151
|
-
accordion: [/data-p60-accordion\b/, 'data-p60-accordion'],
|
|
152
|
-
carousel: [/data-p60-carousel\b/, 'data-p60-carousel'],
|
|
153
|
-
stickyHeader: [/data-p60-sticky-header\b/, 'data-p60-sticky-header'],
|
|
154
|
-
stickyCta: [/data-p60-sticky-cta\b/, 'data-p60-sticky-cta'],
|
|
155
|
-
lightbox: [/data-p60-lightbox\b/, 'data-p60-lightbox'],
|
|
156
|
-
tabs: [/data-p60-tabs\b/, 'data-p60-tabs'],
|
|
157
|
-
nav: [/data-p60-nav\b/, 'data-p60-nav'],
|
|
158
|
-
};
|
|
159
160
|
for (const name of declaredBehaviours) {
|
|
160
161
|
const primary = PRIMARY_ATTR[name];
|
|
161
162
|
if (primary && !primary[0].test(liquidSource)) {
|
package/starter/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"format": "port60-liquid@1",
|
|
5
5
|
"label": "Starter",
|
|
6
6
|
"description": "The reference template for the Port60 dialect, the developer docs' worked example and the base to copy when building your own. 1.1.0 adds the layout slot: the template owns the header, navigation and footer chrome.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"member_menu",
|
|
24
24
|
"hero_carousel",
|
|
25
25
|
"map",
|
|
26
|
-
"
|
|
26
|
+
"primary_action_widget"
|
|
27
27
|
],
|
|
28
28
|
"layout": true,
|
|
29
29
|
"heroImagery": true,
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
</div>
|
|
70
70
|
<aside id="donate">
|
|
71
|
-
{%- comment -%} The site focus decides which widget leads (section.primary); the
|
|
72
|
-
{% island '
|
|
71
|
+
{%- comment -%} The site focus decides which widget leads (section.primary); this island becomes it: the donation widget or the volunteer sign-up. {%- endcomment -%}
|
|
72
|
+
{% island 'primary_action_widget' %}
|
|
73
73
|
</aside>
|
|
74
74
|
</div>
|
|
75
75
|
</section>
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
</div>
|
|
109
109
|
{% unless section.actionStyle == 'button' or section.givingStyle == 'button' %}
|
|
110
110
|
<aside id="donate">
|
|
111
|
-
{% island '
|
|
111
|
+
{% island 'primary_action_widget' %}
|
|
112
112
|
</aside>
|
|
113
113
|
{% endunless %}
|
|
114
114
|
</div>
|