domma-cms 1.14.0 → 1.15.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/CHANGELOG.md +4 -0
- package/admin/js/lib/site-settings-arrange.js +3 -3
- package/admin/js/templates/docs/usage-site-settings.html +13 -2
- package/admin/js/templates/menu-editor.html +1 -1
- package/admin/js/templates/settings.html +96 -22
- package/admin/js/views/settings.js +3 -3
- package/docs/DommaFeatures.md +2 -1
- package/docs/configuration.md +53 -8
- package/package.json +1 -1
- package/public/css/motion.css +1 -0
- package/public/js/motion-control.mjs +1 -0
- package/public/js/site.js +1 -1
- package/server/routes/api/settings.js +23 -2
- package/server/services/motionSettings.js +107 -0
- package/server/services/renderer.js +34 -6
- package/server/templates/page.html +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.15.0] - 2026-10-29
|
|
4
|
+
|
|
5
|
+
- The "Reduce motion" control has its own Site Settings tab, beside Breadcrumbs: show it, its label, and what a new visitor gets (follow their device, full motion, or calm). Placement puts it in the footer as before (a switch or a checkbox), as a floating button in a chosen corner (offsets, size and corners, Search's limits), or at the end of the navbar. A floating button sharing a corner with Search, Back to top or the theme switcher stacks clear of it; it defaults to the bottom left, clear of both. Stored as `site.json` `motion` (`server/services/motionSettings.js`); a site without it keeps reading `footer.showMotionToggle` / `motionDefault` / `motionControl`, which Site Settings still writes as a mirror, so nothing changes until the tab is saved. The visitor's choice is still the one `domma:reduced_motion` preference.
|
|
6
|
+
|
|
3
7
|
## [1.14.0] - 2026-10-29
|
|
4
8
|
|
|
5
9
|
- Page editor: **Highlight text** is on the Insert and Wrap menus (Content) - the [text] dialog with a gradient and a shine already chosen, its highlight choices first and the colours shown in the preview. Right-click a `[text]` tag for one-click Highlight, sweeping Highlight, Light up as you read, Rise in word by word, and Plain.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],branding:["brandText","brandLogo","brandIcon","brandUrl","adminBrandTitle","adminBrandLogo","adminBrandIcon"],favicon:["favicon"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerLayout","footerBlurb","footerBlocks","
|
|
2
|
-
`):"",footerTagline:o(t.footer?.tagline),footerEmail:o(t.footer?.email),footerAddress:o(t.footer?.address),footerGradFrom:o(t.footer?.gradient?.from),footerGradTo:o(t.footer?.gradient?.to),socialTwitter:o(t.social?.twitter),socialFacebook:o(t.social?.facebook),socialInstagram:o(t.social?.instagram),socialLinkedin:o(t.social?.linkedin),socialGithub:o(t.social?.github),socialYoutube:o(t.social?.youtube),smtpHost:o(t.smtp?.host),smtpPort:o(t.smtp?.port||587),smtpUser:o(t.smtp?.user),smtpPass:"",smtpSecure:t.smtp?.secure===!0,smtpInsecureTLS:t.smtp?.allowInsecureTLS===!0,smtpFromAddress:o(t.smtp?.fromAddress),smtpFromName:o(t.smtp?.fromName),bttEnabled:r.enabled!==!1,bttSmooth:r.smooth!==!1,bttLabel:o(r.label),bttThreshold:o(r.scrollThreshold??300),bttPosition:o(r.position,"bottom-right"),bttOffset:o(r.offset??32),bttBottomOffset:o(r.bottomOffset??r.offset??32),gtbEnabled:
|
|
3
|
-
`).map(
|
|
1
|
+
export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],branding:["brandText","brandLogo","brandIcon","brandUrl","adminBrandTitle","adminBrandLogo","adminBrandIcon"],favicon:["favicon"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerLayout","footerBlurb","footerBlocks","footerClass","footerSocialClass","footerLook","footerCtaText","footerCtaHighlight","footerCtaUrl","footerCtaSub","footerWatermark","footerStatus","footerFacts","footerTagline","footerEmail","footerAddress","footerGradFrom","footerGradTo","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpInsecureTLS","smtpFromAddress","smtpFromName"],pageNavigation:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset","gtbEnabled","gtbSmooth","gtbLabel","gtbThreshold","gtbPosition","gtbOffset","gtbBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion","ccSettingsLink","ccSettingsLinkText"],analytics:["gaId"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"],motion:["mtEnabled","mtLabel","mtDefault","mtControl","mtPlacement","mtTriggerStyle","mtPosition","mtOffsetX","mtOffsetY","mtIconSize","mtIconRadius","mtClass"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"],MOTION_PLACEMENTS=["footer","floating","navbar"],MOTION_LABEL="Reduce motion";const u=["system","off","on"],c=(t,r,i,e)=>{const n=Number.parseInt(t,10);return Number.isFinite(n)?Math.min(e,Math.max(i,n)):r};export function motionFromSettings(t={}){const r=t.motion&&typeof t.motion=="object"?t.motion:{},i=t.footer||{},e=b=>r[b]!==void 0&&r[b]!==null,n=e("default")?r.default:i.motionDefault,s=e("control")?r.control:i.motionControl,m=c(r.iconSize,52,32,96);return{mtEnabled:(e("enabled")?r.enabled:i.showMotionToggle)!==!1,mtLabel:typeof r.label=="string"&&r.label.trim()?r.label.trim():MOTION_LABEL,mtDefault:u.includes(n)?n:"system",mtControl:s==="checkbox"?"checkbox":"switch",mtPlacement:MOTION_PLACEMENTS.includes(r.placement)?r.placement:"footer",mtTriggerStyle:r.triggerStyle==="icon-text"?"icon-text":"icon",mtPosition:BREADCRUMB_POSITIONS.includes(r.position)?r.position:"BL",mtOffsetX:o(c(r.offsetX,32,0,500)),mtOffsetY:o(c(r.offsetY,32,0,500)),mtIconSize:o(m),mtIconRadius:o(c(r.iconRadius,26,0,Math.floor(m/2))),mtClass:o(r.class)}}export function motionFromForm(t){const r=c(t.mtIconSize,52,32,96);return{enabled:t.mtEnabled===!0,default:u.includes(t.mtDefault)?t.mtDefault:"system",placement:MOTION_PLACEMENTS.includes(t.mtPlacement)?t.mtPlacement:"footer",control:t.mtControl==="checkbox"?"checkbox":"switch",triggerStyle:t.mtTriggerStyle==="icon-text"?"icon-text":"icon",label:o(t.mtLabel).trim().replace(/\s+/g," ").slice(0,40)||MOTION_LABEL,position:BREADCRUMB_POSITIONS.includes(t.mtPosition)?t.mtPosition:"BL",offsetX:c(t.mtOffsetX,32,0,500),offsetY:c(t.mtOffsetY,32,0,500),iconSize:r,iconRadius:c(t.mtIconRadius,26,0,Math.floor(r/2)),class:o(t.mtClass).trim()}}export function footerBlockNames(t){return[...new Set(String(t??"").split(/[\s,]+/).map(r=>r.trim().toLowerCase()).filter(Boolean))]}export function gaIdError(t){const r=o(t).trim();return!r||/^G-[A-Z0-9]{4,20}$/i.test(r)?"":"A measurement ID looks like G-XXXXXXXXXX (Google Analytics > Admin > Data streams)."}export const FOOTER_LAYOUTS=["auto","columns","row"],FOOTER_LOOKS=["","showcase"],FOOTER_FACTS_MAX=4;const d=/^(\/|#|https?:\/\/|mailto:)/,f=/^#[0-9a-f]{6}$/i;export function footerShowcaseErrors(t){const r=[],i=o(t.footerCtaUrl).trim();i&&!d.test(i)&&r.push("The footer call to action link must start with /, #, https:// or mailto: (Footer tab).");for(const[e,n]of[["from",t.footerGradFrom],["to",t.footerGradTo]]){const s=o(n).trim();s&&!f.test(s)&&r.push(`The footer gradient "${e}" colour must look like #f26b3a (Footer tab).`)}return r}const o=(t,r="")=>t==null?r:String(t),a=(t,r)=>{const i=Number.parseInt(t,10);return Number.isFinite(i)?i:r};export function homeValue(t){return/^\/[A-Za-z0-9\-_/]*$/.test(t||"")&&String(t).replace(/\/+$/,"")||"/"}export function formFromSettings(t={}){const r=t.backToTop||{},i=t.goToBottom||{},e=t.cookieConsent||{},n=t.breadcrumbs||{};return{title:o(t.title),tagline:o(t.tagline),siteUrl:o(t.baseUrl),adminHome:homeValue(t.adminHome),spacerSize:o(t.layoutOptions?.spacerSize??40),bodyClass:o(t.bodyClass),brandText:o(t.brand?.text),brandLogo:o(t.brand?.logo),brandIcon:o(t.brand?.icon),brandUrl:o(t.brand?.url,"/"),favicon:o(t.favicon),adminBrandTitle:o(t.adminBrand?.title),adminBrandLogo:o(t.adminBrand?.logo),adminBrandIcon:o(t.adminBrand?.icon),seoTitle:o(t.seo?.defaultTitle),seoSeparator:o(t.seo?.titleSeparator," | "),seoDescription:o(t.seo?.defaultDescription),footerCopyright:o(t.footer?.copyright),footerLayout:FOOTER_LAYOUTS.includes(t.footer?.layout)?t.footer.layout:"auto",footerBlurb:o(t.footer?.blurb),footerBlocks:Array.isArray(t.footer?.blocks)?t.footer.blocks.filter(s=>typeof s=="string").join(", "):"",footerClass:o(t.footer?.class),footerSocialClass:o(t.footer?.socialClass),footerLook:FOOTER_LOOKS.includes(t.footer?.look)?t.footer.look:"",footerCtaText:o(t.footer?.cta?.text),footerCtaHighlight:o(t.footer?.cta?.highlight),footerCtaUrl:o(t.footer?.cta?.url),footerCtaSub:o(t.footer?.cta?.sub),footerWatermark:o(t.footer?.watermark),footerStatus:o(t.footer?.status),footerFacts:Array.isArray(t.footer?.facts)?t.footer.facts.filter(s=>typeof s=="string").join(`
|
|
2
|
+
`):"",footerTagline:o(t.footer?.tagline),footerEmail:o(t.footer?.email),footerAddress:o(t.footer?.address),footerGradFrom:o(t.footer?.gradient?.from),footerGradTo:o(t.footer?.gradient?.to),socialTwitter:o(t.social?.twitter),socialFacebook:o(t.social?.facebook),socialInstagram:o(t.social?.instagram),socialLinkedin:o(t.social?.linkedin),socialGithub:o(t.social?.github),socialYoutube:o(t.social?.youtube),smtpHost:o(t.smtp?.host),smtpPort:o(t.smtp?.port||587),smtpUser:o(t.smtp?.user),smtpPass:"",smtpSecure:t.smtp?.secure===!0,smtpInsecureTLS:t.smtp?.allowInsecureTLS===!0,smtpFromAddress:o(t.smtp?.fromAddress),smtpFromName:o(t.smtp?.fromName),bttEnabled:r.enabled!==!1,bttSmooth:r.smooth!==!1,bttLabel:o(r.label),bttThreshold:o(r.scrollThreshold??300),bttPosition:o(r.position,"bottom-right"),bttOffset:o(r.offset??32),bttBottomOffset:o(r.bottomOffset??r.offset??32),gtbEnabled:i.enabled===!0,gtbSmooth:i.smooth!==!1,gtbLabel:o(i.label),gtbThreshold:o(i.scrollThreshold??300),gtbPosition:o(i.position,"bottom-right"),gtbOffset:o(i.offset??32),gtbBottomOffset:o(i.bottomOffset??i.offset??32),ccEnabled:e.enabled!==!1,ccMessage:o(e.message),ccAcceptAll:o(e.acceptAllText,"Accept All"),ccRejectAll:o(e.rejectAllText,"Reject All"),ccCustomize:o(e.customizeText,"Customize"),ccSavePrefs:o(e.savePreferencesText,"Save Preferences"),ccPrivacyText:o(e.privacyPolicyText,"Privacy Policy"),ccPrivacyUrl:o(e.privacyPolicyUrl),ccCookieText:o(e.cookiePolicyText,"Cookie Policy"),ccCookieUrl:o(e.cookiePolicyUrl),ccPosition:o(e.position,"bottom"),ccLayout:o(e.layout,"bar"),ccTheme:o(e.theme,"dark"),ccShowFunctional:e.showFunctional!==!1,ccShowAnalytics:e.showAnalytics!==!1,ccShowMarketing:e.showMarketing!==!1,ccVersion:o(e.consentVersion,"1.0"),ccSettingsLink:e.showSettingsLink!==!1,ccSettingsLinkText:o(e.settingsLinkText,"Cookie settings"),gaId:o(t.googleAnalytics?.measurementId),bcEnabled:n.enabled===!0,bcHomeLabel:o(n.homeLabel,"Home"),bcPosition:BREADCRUMB_POSITIONS.includes(n.position)?n.position:"TL",bcOffsetX:o(n.offsetX??8),bcOffsetY:o(n.offsetY??8),...motionFromSettings(t)}}export function payloadFromForm(t,r={},{adminHomeReady:i=!0}={}){const e=s=>o(s).trim(),n=motionFromForm(t);return{title:e(t.title),tagline:e(t.tagline),baseUrl:e(t.siteUrl).replace(/\/+$/,""),...i&&{adminHome:e(t.adminHome)||"/"},layoutOptions:{spacerSize:a(t.spacerSize,40)||40},bodyClass:e(t.bodyClass),brand:{...r.brand,text:e(t.brandText),logo:e(t.brandLogo),icon:e(t.brandIcon),url:e(t.brandUrl)||"/"},favicon:e(t.favicon),adminBrand:{...r.adminBrand,title:e(t.adminBrandTitle),logo:e(t.adminBrandLogo),icon:e(t.adminBrandIcon)},seo:{defaultTitle:e(t.seoTitle),titleSeparator:o(t.seoSeparator)||" | ",defaultDescription:e(t.seoDescription)},footer:{copyright:e(t.footerCopyright),layout:FOOTER_LAYOUTS.includes(t.footerLayout)?t.footerLayout:"auto",blurb:e(t.footerBlurb),blocks:footerBlockNames(t.footerBlocks),links:r.footer?.links||[],showMotionToggle:n.enabled,motionDefault:n.default,motionControl:n.control,class:e(t.footerClass),socialClass:e(t.footerSocialClass),look:FOOTER_LOOKS.includes(t.footerLook)?t.footerLook:"",cta:{text:e(t.footerCtaText),highlight:e(t.footerCtaHighlight),url:e(t.footerCtaUrl),sub:e(t.footerCtaSub)},watermark:e(t.footerWatermark),status:e(t.footerStatus),facts:String(t.footerFacts??"").split(`
|
|
3
|
+
`).map(s=>s.trim()).filter(Boolean).slice(0,FOOTER_FACTS_MAX),tagline:e(t.footerTagline),email:e(t.footerEmail),address:e(t.footerAddress),gradient:{from:e(t.footerGradFrom),to:e(t.footerGradTo)}},social:{twitter:e(t.socialTwitter),facebook:e(t.socialFacebook),instagram:e(t.socialInstagram),linkedin:e(t.socialLinkedin),github:e(t.socialGithub),youtube:e(t.socialYoutube)},smtp:{host:e(t.smtpHost),port:a(t.smtpPort,587)||587,user:e(t.smtpUser),pass:o(t.smtpPass),secure:t.smtpSecure===!0,allowInsecureTLS:t.smtpInsecureTLS===!0,fromAddress:e(t.smtpFromAddress),fromName:e(t.smtpFromName)},backToTop:{enabled:t.bttEnabled===!0,scrollThreshold:a(t.bttThreshold,300),position:o(t.bttPosition)||"bottom-right",offset:a(t.bttOffset,32),bottomOffset:a(t.bttBottomOffset,32),label:e(t.bttLabel),smooth:t.bttSmooth===!0},goToBottom:{enabled:t.gtbEnabled===!0,scrollThreshold:a(t.gtbThreshold,300),position:o(t.gtbPosition)||"bottom-right",offset:a(t.gtbOffset,32),bottomOffset:a(t.gtbBottomOffset,32),label:e(t.gtbLabel),smooth:t.gtbSmooth===!0},cookieConsent:{enabled:t.ccEnabled===!0,message:e(t.ccMessage),acceptAllText:e(t.ccAcceptAll),rejectAllText:e(t.ccRejectAll),customizeText:e(t.ccCustomize),savePreferencesText:e(t.ccSavePrefs),privacyPolicyText:e(t.ccPrivacyText),privacyPolicyUrl:e(t.ccPrivacyUrl),cookiePolicyText:e(t.ccCookieText),cookiePolicyUrl:e(t.ccCookieUrl),position:o(t.ccPosition)||"bottom",layout:o(t.ccLayout)||"bar",theme:o(t.ccTheme)||"dark",showFunctional:t.ccShowFunctional===!0,showAnalytics:t.ccShowAnalytics===!0,showMarketing:t.ccShowMarketing===!0,consentVersion:e(t.ccVersion)||"1.0",showSettingsLink:t.ccSettingsLink===!0,settingsLinkText:e(t.ccSettingsLinkText)||"Cookie settings"},googleAnalytics:{measurementId:e(t.gaId).toUpperCase()},breadcrumbs:{enabled:t.bcEnabled===!0,homeLabel:e(t.bcHomeLabel)||"Home",position:BREADCRUMB_POSITIONS.includes(t.bcPosition)?t.bcPosition:"TL",offsetX:a(t.bcOffsetX,8),offsetY:a(t.bcOffsetY,8)},motion:n}}export function siteUrlError(t){const r=o(t).trim().replace(/\/+$/,"");return r?/^https?:\/\//i.test(r)?/^https?:\/\/([a-z0-9.\-]+|\[[0-9a-f:.]+\])(:\d{1,5})?$/i.test(r)?"":"Just the address, like https://example.com - no page path after it.":"Start with https:// (or http://).":""}const l=t=>typeof t=="boolean"?t:o(t);export function snapshot(t){return JSON.stringify(Object.keys(t).sort().map(r=>[r,l(t[r])]))}export function changedTabs(t,r){const i=[];for(const[e,n]of Object.entries(TAB_FIELDS))n.some(s=>l(t[s])!==l(r[s]))&&i.push(e);return i}export function draftOf(t){return Object.fromEntries(Object.entries(t).filter(([r])=>!SECRET_FIELDS.includes(r)))}const p={footerMotionToggle:"mtEnabled",footerMotionDefault:"mtDefault",footerMotionControl:"mtControl"};export function formFromDraft(t,r={}){const i={...r};for(const[n,s]of Object.entries(p))n in i&&!(s in i)&&(i[s]=i[n]),delete i[n];const e={...t};for(const[n,s]of Object.entries(i))n in e&&!SECRET_FIELDS.includes(n)&&(e[n]=s);return e}export function settingsProblems(t){const r=[];return o(t.smtpHost).trim()?o(t.smtpFromAddress).trim()||r.push({tone:"warn",tab:"email",text:"No From address - mail has no sender and many servers refuse it."}):r.push({tone:"danger",tab:"email",text:"No SMTP host - mail goes to a throwaway test inbox and reaches no one (password resets, form notifications)."}),(t.ccEnabled===!0||o(t.gaId).trim())&&!o(t.ccPrivacyUrl).trim()&&r.push({tone:"warn",tab:"cookies",text:"The cookie banner is on but links to no privacy policy."}),r}export function describeCacheKey(t){const r=/^page:(.*?):m\d+:role([^:]*):/.exec(String(t||""));return r?{path:r[1]||"/",role:r[2]||"anon",page:!0}:{path:String(t||""),role:"",page:!1}}export function expiresIn(t,r=Date.now()){if(t==null)return"no expiry";const i=t-r;return i<=0?"expired":i<6e4?`${Math.round(i/1e3)}s`:i<36e5?`${Math.round(i/6e4)}m`:`${Math.round(i/36e5)}h`}
|
|
@@ -42,10 +42,21 @@
|
|
|
42
42
|
</tr>
|
|
43
43
|
<tr>
|
|
44
44
|
<td>Footer</td>
|
|
45
|
-
<td>Copyright text, layout, blurb,
|
|
46
|
-
|
|
45
|
+
<td>Copyright text, layout, blurb, footer blocks and social links.
|
|
46
|
+
Footer links are menus - see <a href="#/docs/usage/navigation">Navigation</a>.
|
|
47
47
|
The <a href="#showcase-footer">Showcase footer</a> card adds a big call to action and more.</td>
|
|
48
48
|
</tr>
|
|
49
|
+
<tr>
|
|
50
|
+
<td>Reduce motion</td>
|
|
51
|
+
<td>The accessibility control that lets visitors turn animations off: whether it is shown,
|
|
52
|
+
its label, and what a visitor who has not chosen gets (follow their device, full motion,
|
|
53
|
+
or calm). <strong>Placement</strong> puts it in the footer (a switch or a checkbox), as a
|
|
54
|
+
floating button in a corner you pick - with its size, corners and distance from the
|
|
55
|
+
edges - or at the end of the navbar (floating where there is no navbar, or on a phone
|
|
56
|
+
when the navbar shows only its menu button). A floating button sharing a corner with Search,
|
|
57
|
+
Back to top or the theme switcher stacks clear of it. Hiding the control does not take
|
|
58
|
+
the choice away from visitors who already made it.</td>
|
|
59
|
+
</tr>
|
|
49
60
|
<tr>
|
|
50
61
|
<td>Email</td>
|
|
51
62
|
<td>The SMTP server the site sends mail through (form notifications, password resets,
|
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
</div>
|
|
292
292
|
<div class="form-group">
|
|
293
293
|
<label>Staggered entrance
|
|
294
|
-
<span data-help-title="Staggered entrance" data-help="The items arrive one after another when the menu is drawn, like a row of cards revealing. A dropdown or footer column staggers in on its own when it opens. Visitors who have asked for less motion (their device setting, or the site's motion
|
|
294
|
+
<span data-help-title="Staggered entrance" data-help="The items arrive one after another when the menu is drawn, like a row of cards revealing. A dropdown or footer column staggers in on its own when it opens. Visitors who have asked for less motion (their device setting, or the site's Reduce motion control) see the items straight away."></span></label>
|
|
295
295
|
<label style="display:flex;align-items:center;gap:.5rem;font-weight:400;cursor:pointer">
|
|
296
296
|
<input type="checkbox" id="me-entrance-enabled" name="entranceEnabled" style="width:auto;margin:0">
|
|
297
297
|
Bring the items in one after another
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<button type="button" class="tab-item">Cookie consent <i class="stx-dot" data-bind-hidden="!tabs.value.cookies" title="Changed"></i></button>
|
|
26
26
|
<button type="button" class="tab-item">Analytics <i class="stx-dot" data-bind-hidden="!tabs.value.analytics" title="Changed"></i></button>
|
|
27
27
|
<button type="button" class="tab-item">Breadcrumbs <i class="stx-dot" data-bind-hidden="!tabs.value.breadcrumbs" title="Changed"></i></button>
|
|
28
|
+
<button type="button" class="tab-item">Reduce motion <i class="stx-dot" data-bind-hidden="!tabs.value.motion" title="Changed"></i></button>
|
|
28
29
|
<button type="button" class="tab-item">Cache</button>
|
|
29
30
|
</div>
|
|
30
31
|
|
|
@@ -203,28 +204,8 @@
|
|
|
203
204
|
<span data-help="Blocks (Content › Blocks) shown side by side at the top of the footer on every page, by name, in order - for example wp-footer-1, wp-footer-2. Import from WordPress puts a site's footer widget areas here. Leave it empty for none; a name that is not a block is left out."></span></label>
|
|
204
205
|
<input id="stx-footer-blocks" type="text" class="form-input" maxlength="400" placeholder="wp-footer-1, wp-footer-2" data-model="footerBlocks.value">
|
|
205
206
|
</div>
|
|
206
|
-
<
|
|
207
|
-
<
|
|
208
|
-
<span data-help="An accessibility switch in the footer that lets visitors turn animations off. Turning this off only hides the switch - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></label>
|
|
209
|
-
<input id="stx-motion" type="checkbox" class="form-switch" data-model="footerMotionToggle.value">
|
|
210
|
-
</div>
|
|
211
|
-
<div class="stx-setting">
|
|
212
|
-
<label class="form-label" for="stx-motion-default">Reduced motion for new visitors
|
|
213
|
-
<span data-help="What a visitor who has not used the switch gets. Follow their device honours the system setting (the accessible default). Off shows every animation until they ask for less; On starts calm. A visitor's own choice always wins."></span></label>
|
|
214
|
-
<select id="stx-motion-default" class="form-input" data-model="footerMotionDefault.value">
|
|
215
|
-
<option value="system">Follow their device</option>
|
|
216
|
-
<option value="off">Off - full motion</option>
|
|
217
|
-
<option value="on">On - calm</option>
|
|
218
|
-
</select>
|
|
219
|
-
</div>
|
|
220
|
-
<div class="stx-setting">
|
|
221
|
-
<label class="form-label" for="stx-motion-control">The control
|
|
222
|
-
<span data-help="How the Reduce motion control looks in the footer."></span></label>
|
|
223
|
-
<select id="stx-motion-control" class="form-input" data-model="footerMotionControl.value">
|
|
224
|
-
<option value="switch">A switch</option>
|
|
225
|
-
<option value="checkbox">A checkbox</option>
|
|
226
|
-
</select>
|
|
227
|
-
</div>
|
|
207
|
+
<p class="stx-wide stx-hint">The “Reduce motion” control now has a tab of its own:
|
|
208
|
+
<button type="button" class="stx-inline-link" data-on-click="goTab('motion')">Reduce motion</button>.</p>
|
|
228
209
|
<div>
|
|
229
210
|
<label class="form-label" for="stx-footer-class">Footer CSS classes
|
|
230
211
|
<span data-help-title="Footer CSS classes" data-help="Your own class names, separated by spaces, added to the site footer. Style them in Custom CSS. The footer's menus take classes of their own in Menus."></span></label>
|
|
@@ -584,6 +565,91 @@
|
|
|
584
565
|
</div>
|
|
585
566
|
</div>
|
|
586
567
|
|
|
568
|
+
<!-- Reduce motion -->
|
|
569
|
+
<div class="tab-panel">
|
|
570
|
+
<div class="stx-card-columns">
|
|
571
|
+
<div class="stx-card">
|
|
572
|
+
<div class="stx-card-title"><span data-icon="pause" data-icon-size="16" aria-hidden="true"></span>The control
|
|
573
|
+
<span data-help-title="Reduce motion" data-help="An accessibility control that lets visitors turn animations off. Turning it off only hides the control - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></div>
|
|
574
|
+
<div class="stx-grid">
|
|
575
|
+
<div class="stx-wide stx-setting"><label class="form-label" for="stx-mt-on">Show it</label>
|
|
576
|
+
<input id="stx-mt-on" type="checkbox" class="form-switch" data-model="mtEnabled.value"></div>
|
|
577
|
+
<div class="stx-wide"><label class="form-label" for="stx-mt-label">Label
|
|
578
|
+
<span data-help="The words on the control, and what a screen reader announces. Up to 40 characters."></span></label>
|
|
579
|
+
<input id="stx-mt-label" type="text" class="form-input" maxlength="40" placeholder="Reduce motion" data-model="mtLabel.value"></div>
|
|
580
|
+
<div class="stx-wide stx-setting">
|
|
581
|
+
<label class="form-label" for="stx-motion-default">Reduced motion for new visitors
|
|
582
|
+
<span data-help="What a visitor who has not used the control gets. Follow their device honours the system setting (the accessible default). Off shows every animation until they ask for less; On starts calm. A visitor's own choice always wins."></span></label>
|
|
583
|
+
<select id="stx-motion-default" class="form-input" data-model="mtDefault.value">
|
|
584
|
+
<option value="system">Follow their device</option>
|
|
585
|
+
<option value="off">Off - full motion</option>
|
|
586
|
+
<option value="on">On - calm</option>
|
|
587
|
+
</select>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="stx-wide stx-setting" data-bind-hidden="mtPlacement.value !== 'footer'">
|
|
590
|
+
<label class="form-label" for="stx-motion-control">Footer style
|
|
591
|
+
<span data-help="How the Reduce motion control looks in the footer."></span></label>
|
|
592
|
+
<select id="stx-motion-control" class="form-input" data-model="mtControl.value">
|
|
593
|
+
<option value="switch">A switch</option>
|
|
594
|
+
<option value="checkbox">A checkbox</option>
|
|
595
|
+
</select>
|
|
596
|
+
</div>
|
|
597
|
+
<div class="stx-wide">
|
|
598
|
+
<label class="form-label" for="stx-mt-class">CSS classes
|
|
599
|
+
<span data-help="Your own class names, separated by spaces, added to the control wherever it sits. Style them in Custom CSS."></span></label>
|
|
600
|
+
<input id="stx-mt-class" type="text" class="form-input" placeholder="motion-quiet" autocomplete="off" spellcheck="false" data-model="mtClass.value">
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="stx-card">
|
|
605
|
+
<div class="stx-card-title"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>Placement
|
|
606
|
+
<span data-help-title="Placement" data-help="Footer puts it at the foot of every page, as it always was. Floating button is a disc fixed to a corner of the window, kept clear of Search, Back to top and the theme switcher. Navbar adds it at the end of the navbar - on a page with no navbar, or on a phone when the navbar shows only its menu button, it floats instead."></span></div>
|
|
607
|
+
<div class="stx-grid">
|
|
608
|
+
<div class="stx-wide">
|
|
609
|
+
<span class="form-label">Where it goes</span>
|
|
610
|
+
<div class="stx-segments" role="group" aria-label="Where it goes">
|
|
611
|
+
<button type="button" data-on-click="motionPlacement('footer')" data-bind-class="mtPlacement.value === 'footer' && 'is-on'">Footer</button>
|
|
612
|
+
<button type="button" data-on-click="motionPlacement('floating')" data-bind-class="mtPlacement.value === 'floating' && 'is-on'">Floating button</button>
|
|
613
|
+
<button type="button" data-on-click="motionPlacement('navbar')" data-bind-class="mtPlacement.value === 'navbar' && 'is-on'">Navbar</button>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="stx-setting stx-wide" data-bind-hidden="mtPlacement.value === 'footer'">
|
|
617
|
+
<label class="form-label" for="stx-mt-style">Button
|
|
618
|
+
<span data-help="An icon on its own (the label is still read out by screen readers), or the icon with the label beside it."></span></label>
|
|
619
|
+
<select id="stx-mt-style" class="form-input" data-model="mtTriggerStyle.value">
|
|
620
|
+
<option value="icon">Icon</option>
|
|
621
|
+
<option value="icon-text">Icon and label</option>
|
|
622
|
+
</select>
|
|
623
|
+
</div>
|
|
624
|
+
<div class="stx-wide" data-bind-hidden="mtPlacement.value !== 'floating'">
|
|
625
|
+
<span class="form-label">Corner</span>
|
|
626
|
+
<div class="stx-corners" role="group" aria-label="Corner">
|
|
627
|
+
<button type="button" data-on-click="motionCorner('TL')" data-bind-class="mtPosition.value === 'TL' && 'is-on'">↖ Top left</button>
|
|
628
|
+
<button type="button" data-on-click="motionCorner('TR')" data-bind-class="mtPosition.value === 'TR' && 'is-on'">Top right ↗</button>
|
|
629
|
+
<button type="button" data-on-click="motionCorner('BL')" data-bind-class="mtPosition.value === 'BL' && 'is-on'">↙ Bottom left</button>
|
|
630
|
+
<button type="button" data-on-click="motionCorner('BR')" data-bind-class="mtPosition.value === 'BR' && 'is-on'">Bottom right ↘</button>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
<div class="stx-pair stx-wide" data-bind-hidden="mtPlacement.value !== 'floating'">
|
|
634
|
+
<div><label class="form-label" for="stx-mt-x">From the side (px)</label>
|
|
635
|
+
<input id="stx-mt-x" type="number" class="form-input" min="0" max="500" placeholder="32" data-model="mtOffsetX.value"></div>
|
|
636
|
+
<div><label class="form-label" for="stx-mt-y">From the top/bottom (px)</label>
|
|
637
|
+
<input id="stx-mt-y" type="number" class="form-input" min="0" max="500" placeholder="32" data-model="mtOffsetY.value"></div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="stx-pair stx-wide" data-bind-hidden="mtPlacement.value !== 'floating' || mtTriggerStyle.value !== 'icon'">
|
|
640
|
+
<div><label class="form-label" for="stx-mt-size">Size <span class="stx-readout" data-bind-text="mtSizeText.value"></span>
|
|
641
|
+
<span data-help="How big the disc is (32-96 px). The glyph inside scales with it. 44 px and up is comfortable to tap on a phone."></span></label>
|
|
642
|
+
<input id="stx-mt-size" type="range" min="32" max="96" step="2" data-model="mtIconSize.value"></div>
|
|
643
|
+
<div><label class="form-label" for="stx-mt-radius">Corners <span class="stx-readout" data-bind-text="mtRadiusText.value"></span>
|
|
644
|
+
<span data-help="0 is a square; the top of the range is a circle, and it is always half the size - so it moves when the size does."></span></label>
|
|
645
|
+
<input id="stx-mt-radius" type="range" min="0" step="1" data-model="mtIconRadius.value"></div>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
<p class="stx-hint" data-bind-hidden="mtPlacement.value !== 'floating'">If Search, Back to top or the theme switcher sits in the same corner, the disc stacks clear of it, further from the edge.</p>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
587
653
|
<!-- Cache -->
|
|
588
654
|
<div class="tab-panel">
|
|
589
655
|
<div class="stx-card">
|
|
@@ -662,6 +728,14 @@
|
|
|
662
728
|
.stx-corners button:nth-child(-n+2) { border-bottom: 1px solid var(--dm-border); }
|
|
663
729
|
.stx-corners button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
|
|
664
730
|
.stx-corners button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
731
|
+
.stx-segments { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
|
|
732
|
+
.stx-segments button { border: 0; background: none; color: var(--dm-text); padding: .45rem .8rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
|
|
733
|
+
.stx-segments button + button { border-left: 1px solid var(--dm-border); }
|
|
734
|
+
.stx-segments button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
|
|
735
|
+
.stx-segments button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
736
|
+
.stx-readout { color: var(--dm-text-muted); font-weight: 400; font-variant-numeric: tabular-nums; }
|
|
737
|
+
.stx-pair input[type="range"] { width: 100%; }
|
|
738
|
+
.stx-inline-link { background: none; border: 0; padding: 0; font: inherit; color: var(--dm-primary); text-decoration: underline; cursor: pointer; }
|
|
665
739
|
.stx-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
|
|
666
740
|
.stx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
|
|
667
741
|
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as D,apiRequest as k}from"../api.js";import{placeTools as $e}from"../lib/sidebar-grouping.js";import{addBannerAction as fe}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{openMenuAt as V,sidebarCount as he}from"../lib/tool-kit.js";import{changedTabs as De,describeCacheKey as W,draftOf as Be,expiresIn as He,footerShowcaseErrors as je,formFromDraft as qe,formFromSettings as Fe,gaIdError as ve,homeValue as Ue,payloadFromForm as _e,siteUrlError as be,settingsProblems as Ke,snapshot as ge,TAB_FIELDS as ze}from"../lib/site-settings-arrange.js";import{pickMedia as Ne}from"../lib/media-picker.js";import{faviconGenerator as Ge}from"../lib/favicon-generator.js";import{makeIconInput as Ve}from"../lib/shortcode-modal.js";import{fmtDateTime as We}from"../lib/dates.js";const Y=/^\/settings(\?.*)?$/,ye="#/settings",y="settings.draft",Ye=`
|
|
2
2
|
<div class="stx-problem">
|
|
3
3
|
<span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
|
|
4
4
|
<span>{{text}}</span>
|
|
5
5
|
<button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
|
|
6
|
-
</div>`,
|
|
6
|
+
</div>`,Je=`
|
|
7
7
|
<div class="stx-row" data-id="{{id}}" tabindex="0">
|
|
8
8
|
<span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
|
|
9
9
|
<span class="stx-chip">{{expires}}</span>
|
|
10
10
|
<button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
|
|
11
|
-
</div>`,We=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","cache"];async function Ye(l,u){const[r,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const o of v||[]){if(!o||o.hidden||o.restricted)continue;const m=typeof o.url=="string"?o.url:"";m.startsWith("#/")&&m.length>2&&!n.has(m.slice(1))&&n.set(m.slice(1),`${d?`${d} \u203A `:""}${o.text||m}`),i(o.items,o.url?d:o.text||d)}};i(Ie(r?.items||[],k||[]),""),n.has(u)||n.set(u,`${u} (not in the sidebar)`),l.replaceChildren();for(const[v,d]of n){const o=document.createElement("option");o.value=v,o.textContent=d,l.appendChild(o)}}let f=null,ye=!1;function Je(){ye||!window.R?.use||(ye=!0,R.use((l,u,r)=>!(Q.test(u?.path||"")&&!Q.test(l?.path||""))||!f?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(f&&!Q.test(l?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Je(),f){const e=f;f=null,e.dispose()}const u=[],r=e=>(u.push(e),e);let k=!1;const n=l.get(0).querySelector(".stx")||l.get(0),i=()=>!k&&n.isConnected,v=E.loader(l.get(0),{type:"dots"});let d;try{d=await B.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!n.isConnected)return;const o=M.observable(d||{}),m=De(d||{}),s={};for(const[e,t]of Object.entries(m))s[e]=M.observable(t);const D=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),X=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),b=M.observable(m),q=M.computed(()=>be(b.value)),x=M.computed(()=>be(D())!==q.value),T=M.observable(!1),L=M.observable(""),Z=M.observable(!1);B.plugins.policy().then(e=>{i()&&(Z.value=e?.signedOnly===!1)}).catch(()=>{});const F=M.observable(!1),ee=M.computed(()=>{const e=new Set(Pe(D(),b.value));return Object.fromEntries(Object.keys(Ue).map(t=>[t,e.has(t)]))}),U=M.observable([]);r(M.effect(()=>{U.value=Fe(D()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const te=M.observable(""),O=M.observable(!1),A=M.observable(""),P=M.observable(""),p=M.observable(null),_=M.observable([]),K=M.observable(""),N=M.observable(!0),z=M.observable(""),ae=M.observable([]);r(M.effect(()=>{const e=K.value.trim().toLowerCase(),t=Date.now();ae.value=[..._.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(c=>String(c).toLowerCase().includes(e))).map(a=>{const c=J(a.key);return{id:a.key,path:c.path,meta:[c.page?`as ${c.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:je(a.expiresAt,t)}})}));const se=Ke(s.favicon,s.brandLogo,i),we={...se.data,...s,saving:T,dirty:x,tabs:ee,problems:U,testTo:te,testing:O,testResult:A,testTone:P,cacheQuery:K,cacheLoading:N,cacheError:z,cacheRows:ae,errorText:L,problemsText:M.computed(()=>U.value.length?"yes":""),siteUrlError:M.computed(()=>ve(s.siteUrl.value)),gaIdError:M.computed(()=>he(s.gaId.value)),gaForcesBanner:M.computed(()=>!!String(s.gaId.value||"").trim()&&s.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(s.smtpHost.value||"").trim()),mayAllowInsecureTLS:Z,dirtyEmail:M.computed(()=>ee.value.email),seoLength:M.computed(()=>{const e=String(s.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>p.value?.enabled===!0),cacheState:M.computed(()=>{const e=p.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=p.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=p.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>p.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>K.value.trim()?"Nothing matches.":p.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function G(){if(T.peek())return;const e=X(),t=b.peek(),a=ve(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const c=he(e.gaId);if(c){E.toast(`Google Analytics: ${c}`,{type:"error"});return}const y=Be(e)[0];if(y){E.toast(y,{type:"error"});return}T.value=!0,L.value="";const W=String(e.title).trim()!==String(t.title).trim();try{await B.settings.save(qe(e,o.peek(),{adminHomeReady:F.peek()}));const j=await B.settings.get().catch(()=>null);if(!i())return;j&&(o.value=j);const Re={...e,smtpPass:""};s.smtpPass.value="",b.value=Re;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(W||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(pe=>e[pe]!==t[pe]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),fe(ge),E.toast("Settings saved.",{type:"success"})}catch(j){L.value=j.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{T.value=!1}}async function ke(){await E.confirm("Throw away your unsaved settings?")&&(ne(b.peek()),L.value="")}function ne(e){for(const[t,a]of Object.entries(e))s[t]&&(s[t].value=a)}async function Ee(){if(!O.peek()){O.value=!0,P.value="",A.value="Sending\u2026";try{const e=await C("/settings/test-email",{method:"POST",body:JSON.stringify({to:te.peek().trim()||void 0})});if(!i())return;A.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;A.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{O.value=!1}}}async function $(){N.value=!0;try{const[e,t]=await Promise.all([C("/cache/status").catch(()=>null),C("/cache/keys")]);if(!i())return;p.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{N.value=!1}}async function Se(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await C("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Me(){const e=p.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{p.value=await C("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),fe(ge)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const oe=e=>_.peek().find(t=>t.key===e);function Ce(e){const t=oe(e);if(!t)return[];const a=J(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Ge;const g=n.querySelector(".stx-list");g.innerHTML=Ve;const xe=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Te=M.applyBindings(we,n,{methods:{...se.methods,save:G,discard:ke,sendTest:Ee,refreshCache:$,clearCache:Se,toggleCache:Me,pickBrandImage:async e=>{const t=await _e({title:"Choose a branding image",current:s[e].peek()});t&&i()&&(s[e].value=t)},clearBrandImage:e=>{s[e].value=""},corner:e=>{s.bcPosition.value=e}}});r(()=>Te.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=Ne("Choose an icon",s[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const c=()=>{s[t].value=a.input.value};a.input.addEventListener("input",c),r(()=>a.input.removeEventListener("input",c)),r(M.effect(()=>{a.input.value=s[t].value})),e.appendChild(a.el)}r(Ae(n,{position:"bottom"}));const Le=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=We.indexOf(t.dataset.go);a>=0&&xe.activate(a)};n.querySelector(".stx-problems").addEventListener("click",Le);const V=n.querySelector("#stx-admin-home");Ye(V,He(s.adminHome.peek())).then(()=>{i()&&(V.value=s.adminHome.peek(),F.value=!0)}).catch(()=>{});const Oe=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Ce(e.dataset.id)});r(()=>Oe?.destroy?.());const re=e=>{const t=oe(e.dataset.id),a=t&&J(t.key);a?.page?window.open(a.path,"_blank","noopener"):Y(e)},ie=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){Y(a);return}re(t)},ce=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Y(t)):e.key==="Enter"&&e.target===t&&re(t))};g.addEventListener("click",ie),g.addEventListener("keydown",ce),r(()=>{g.removeEventListener("click",ie),g.removeEventListener("keydown",ce)}),me(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),me(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>G()});const le=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),G())};document.addEventListener("keydown",le,!0),r(()=>document.removeEventListener("keydown",le,!0));const ue=()=>{if(x.peek())try{S.set(w,{form:$e(X()),base:q.peek(),at:new Date().toISOString()})}catch{}},de=e=>{x.peek()&&(ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",de),r(()=>window.removeEventListener("beforeunload",de)),f={dirty:()=>x.peek(),dispose(){k=!0,ue(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),$();let h=null;try{h=S.get(w)}catch{}if(h?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=h.base&&h.base!==q.peek(),t=h.at?` (${ze(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(a)&&i()){const c={...b.peek()};for(const[y,W]of Object.entries(h.form))y in c&&y!=="smtpPass"&&(c[y]=W);ne(c),F.peek()&&(V.value=s.adminHome.peek())}}}};
|
|
11
|
+
</div>`,we=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","motion","cache"];async function Qe(c,u){const[o,w]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),s=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const r of v||[]){if(!r||r.hidden||r.restricted)continue;const p=typeof r.url=="string"?r.url:"";p.startsWith("#/")&&p.length>2&&!s.has(p.slice(1))&&s.set(p.slice(1),`${d?`${d} \u203A `:""}${r.text||p}`),i(r.items,r.url?d:r.text||d)}};i($e(o?.items||[],w||[]),""),s.has(u)||s.set(u,`${u} (not in the sidebar)`),c.replaceChildren();for(const[v,d]of s){const r=document.createElement("option");r.value=v,r.textContent=d,c.appendChild(r)}}let f=null,ke=!1;function Xe(){ke||!window.R?.use||(ke=!0,R.use((c,u,o)=>!(Y.test(u?.path||"")&&!Y.test(c?.path||""))||!f?.dirty()?o():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(s=>{s?o():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:c}={})=>{if(f&&!Y.test(c?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(c){if(Xe(),f){const e=f;f=null,e.dispose()}const u=[],o=e=>(u.push(e),e);let w=!1;const s=c.get(0).querySelector(".stx")||c.get(0),i=()=>!w&&s.isConnected,v=E.loader(c.get(0),{type:"dots"});let d;try{d=await D.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!s.isConnected)return;const r=M.observable(d||{}),p=Fe(d||{}),n={};for(const[e,t]of Object.entries(p))n[e]=M.observable(t);const B=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),J=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),b=M.observable(p),j=M.computed(()=>ge(b.value)),x=M.computed(()=>ge(B())!==j.value),C=M.observable(!1),T=M.observable(""),Q=M.observable(!1);D.plugins.policy().then(e=>{i()&&(Q.value=e?.signedOnly===!1)}).catch(()=>{});const q=M.observable(!1),X=M.computed(()=>{const e=new Set(De(B(),b.value));return Object.fromEntries(Object.keys(ze).map(t=>[t,e.has(t)]))}),F=M.observable([]);o(M.effect(()=>{F.value=Ke(B()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const Z=M.observable(""),L=M.observable(!1),O=M.observable(""),P=M.observable(""),m=M.observable(null),U=M.observable([]),_=M.observable(""),K=M.observable(!0),z=M.observable(""),ee=M.observable([]);o(M.effect(()=>{const e=_.value.trim().toLowerCase(),t=Date.now();ee.value=[...U.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(l=>String(l).toLowerCase().includes(e))).map(a=>{const l=W(a.key);return{id:a.key,path:l.path,meta:[l.page?`as ${l.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:He(a.expiresAt,t)}})}));const te=Ge(n.favicon,n.brandLogo,i),Ee={...te.data,...n,saving:C,dirty:x,tabs:X,problems:F,testTo:Z,testing:L,testResult:O,testTone:P,cacheQuery:_,cacheLoading:K,cacheError:z,cacheRows:ee,errorText:T,problemsText:M.computed(()=>F.value.length?"yes":""),siteUrlError:M.computed(()=>be(n.siteUrl.value)),gaIdError:M.computed(()=>ve(n.gaId.value)),gaForcesBanner:M.computed(()=>!!String(n.gaId.value||"").trim()&&n.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(n.smtpHost.value||"").trim()),mtSizeText:M.computed(()=>`${n.mtIconSize.value} px`),mtRadiusText:M.computed(()=>`${n.mtIconRadius.value} px`),mayAllowInsecureTLS:Q,dirtyEmail:M.computed(()=>X.value.email),seoLength:M.computed(()=>{const e=String(n.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>m.value?.enabled===!0),cacheState:M.computed(()=>{const e=m.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=m.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=m.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>m.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>_.value.trim()?"Nothing matches.":m.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function N(){if(C.peek())return;const e=J(),t=b.peek(),a=be(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const l=ve(e.gaId);if(l){E.toast(`Google Analytics: ${l}`,{type:"error"});return}const me=je(e)[0];if(me){E.toast(me,{type:"error"});return}C.value=!0,T.value="";const Oe=String(e.title).trim()!==String(t.title).trim();try{await D.settings.save(_e(e,r.peek(),{adminHomeReady:q.peek()}));const A=await D.settings.get().catch(()=>null);if(!i())return;A&&(r.value=A);const Pe={...e,smtpPass:""};n.smtpPass.value="",b.value=Pe;try{S.remove?S.remove(y):S.set(y,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(Oe||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(pe=>e[pe]!==t[pe]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),he(ye),E.toast("Settings saved.",{type:"success"})}catch(A){T.value=A.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{C.value=!1}}async function Se(){await E.confirm("Throw away your unsaved settings?")&&(ae(b.peek()),T.value="")}function ae(e){for(const[t,a]of Object.entries(e))n[t]&&(n[t].value=a)}async function Me(){if(!L.peek()){L.value=!0,P.value="",O.value="Sending\u2026";try{const e=await k("/settings/test-email",{method:"POST",body:JSON.stringify({to:Z.peek().trim()||void 0})});if(!i())return;O.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;O.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{L.value=!1}}}async function $(){K.value=!0;try{const[e,t]=await Promise.all([k("/cache/status").catch(()=>null),k("/cache/keys")]);if(!i())return;m.value=e,U.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{K.value=!1}}async function xe(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await k("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Ce(){const e=m.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{m.value=await k("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),he(ye)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const ne=e=>U.peek().find(t=>t.key===e);function Te(e){const t=ne(e);if(!t)return[];const a=W(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}s.querySelector(".stx-problems").innerHTML=Ye;const g=s.querySelector(".stx-list");g.innerHTML=Je;const se=E.tabs(s.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Ie=M.applyBindings(Ee,s,{methods:{...te.methods,save:N,discard:Se,sendTest:Me,refreshCache:$,clearCache:xe,toggleCache:Ce,pickBrandImage:async e=>{const t=await Ne({title:"Choose a branding image",current:n[e].peek()});t&&i()&&(n[e].value=t)},clearBrandImage:e=>{n[e].value=""},corner:e=>{n.bcPosition.value=e},motionCorner:e=>{n.mtPosition.value=e},motionPlacement:e=>{n.mtPlacement.value=e},goTab:e=>{const t=we.indexOf(e);t>=0&&se.activate(t)}}}),oe=s.querySelector("#stx-mt-radius");o(M.effect(()=>{const e=Math.floor((Number.parseInt(n.mtIconSize.value,10)||52)/2);oe.max=String(e);const t=Number.parseInt(n.mtIconRadius.peek(),10)||0;t>e?n.mtIconRadius.value=String(e):oe.value=String(t)})),o(()=>Ie.dispose());for(const e of s.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=Ve("Choose an icon",n[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const l=()=>{n[t].value=a.input.value};a.input.addEventListener("input",l),o(()=>a.input.removeEventListener("input",l)),o(M.effect(()=>{a.input.value=n[t].value})),e.appendChild(a.el)}o(Ae(s,{position:"bottom"}));const Re=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=we.indexOf(t.dataset.go);a>=0&&se.activate(a)};s.querySelector(".stx-problems").addEventListener("click",Re);const G=s.querySelector("#stx-admin-home");Qe(G,Ue(n.adminHome.peek())).then(()=>{i()&&(G.value=n.adminHome.peek(),q.value=!0)}).catch(()=>{});const Le=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Te(e.dataset.id)});o(()=>Le?.destroy?.());const re=e=>{const t=ne(e.dataset.id),a=t&&W(t.key);a?.page?window.open(a.path,"_blank","noopener"):V(e)},ie=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){V(a);return}re(t)},ce=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&re(t))};g.addEventListener("click",ie),g.addEventListener("keydown",ce),o(()=>{g.removeEventListener("click",ie),g.removeEventListener("keydown",ce)}),fe(c,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),fe(c,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>N()});const le=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),N())};document.addEventListener("keydown",le,!0),o(()=>document.removeEventListener("keydown",le,!0));const ue=()=>{if(x.peek())try{S.set(y,{form:Be(J()),base:j.peek(),at:new Date().toISOString()})}catch{}},de=e=>{x.peek()&&(ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",de),o(()=>window.removeEventListener("beforeunload",de)),f={dirty:()=>x.peek(),dispose(){w=!0,ue(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s),$();let h=null;try{h=S.get(y)}catch{}if(h?.form){try{S.remove?S.remove(y):S.set(y,null)}catch{}const e=h.base&&h.base!==j.peek(),t=h.at?` (${We(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;await E.confirm(a)&&i()&&(ae(qe(b.peek(),h.form)),q.peek()&&(G.value=n.adminHome.peek()))}}};
|
package/docs/DommaFeatures.md
CHANGED
|
@@ -1099,8 +1099,9 @@ Where: `server/services/presetCollections.js` (`PRESETS`, `seedAll()`), `server/
|
|
|
1099
1099
|
- **Site Settings** - One screen with one Save covering nine tabs:
|
|
1100
1100
|
- General: title, tagline, Site URL, admin home, default spacer size.
|
|
1101
1101
|
- SEO defaults: default title, separator, meta description.
|
|
1102
|
-
- Footer: copyright,
|
|
1102
|
+
- Footer: copyright, layout, blurb, footer blocks, social icons for X, Facebook, Instagram, LinkedIn, GitHub and YouTube.
|
|
1103
1103
|
- Showcase footer (Footer tab): `footer.look: showcase` (glowing top line, more room), `footer.cta` (a big call to action, some words in a gradient, optional link and sub-line), `footer.watermark` (a huge faint name behind the footer that drifts on scroll), `footer.email` / `footer.address` under the brand, `footer.status` (green dot) and up to four `footer.facts`, `footer.tagline` in the bottom bar, `footer.gradient` `{from, to}`. Columns layout only; each part shows when it has words. Drawn by `public/js/footer-showcase.mjs` + `footer-showcase.css`.
|
|
1104
|
+
- Reduce motion: on/off, label, the default for new visitors; in the footer (switch or checkbox), a floating button (corner, offsets, size, corners) or the navbar.
|
|
1104
1105
|
- Email: SMTP host, port, user, password and TLS; From address and name; send a test email.
|
|
1105
1106
|
- Back to top: on/off, label, threshold, position, offsets, smooth scrolling.
|
|
1106
1107
|
- Cookie consent: message, position, layout, colours, categories, policy links, button text, consent version.
|
package/docs/configuration.md
CHANGED
|
@@ -137,7 +137,6 @@ The main site-wide configuration. This file is editable from the **Settings** ad
|
|
|
137
137
|
"copyright": "© 2026 My Company",
|
|
138
138
|
"layout": "auto",
|
|
139
139
|
"blurb": "",
|
|
140
|
-
"showMotionToggle": true,
|
|
141
140
|
"links": [
|
|
142
141
|
{ "text": "Privacy Policy", "url": "/privacy" }
|
|
143
142
|
]
|
|
@@ -195,7 +194,8 @@ The main site-wide configuration. This file is editable from the **Settings** ad
|
|
|
195
194
|
| `theme`, `adminTheme`, `fontFamily`, `fontSize`, `autoTheme`, `baseTheme` | **Mirror only.** These moved to `config/theme.json` (see below) and are rewritten here on every theme save so plugins and fleet tooling that read them keep working. Editing them here has no effect. |
|
|
196
195
|
| `smtp` | SMTP connection details for outgoing email (password is stripped from GET responses) - see **Outgoing email** below |
|
|
197
196
|
| `social` | Social profile URLs; empty strings are ignored by the frontend |
|
|
198
|
-
| `footer` | Footer copyright,
|
|
197
|
+
| `footer` | Footer copyright, layout, blocks and classes - see below |
|
|
198
|
+
| `motion` | The "Reduce motion" control: shown, default, footer/floating/navbar placement - see **Reduce motion** below |
|
|
199
199
|
| `backToTop` | Scroll-to-top button configuration |
|
|
200
200
|
| `cookieConsent` | Cookie consent banner configuration |
|
|
201
201
|
| `googleAnalytics` | `{measurementId: "G-…"}` - Google Analytics 4, set at Site Settings > Analytics. See **Google Analytics** below |
|
|
@@ -285,15 +285,60 @@ the host is empty you get, in order of how likely you are to see it:
|
|
|
285
285
|
| `layout` | string | `auto` (default), `columns` or `row`. **Columns**: the brand (the navbar's text, logo or icon), the blurb and the social links beside one column per group of the `footer-primary` menu (a top-level item with sub-items; its text is the heading, a link only if it has a URL), over a bottom bar of copyright, the `footer-legal` menu and the motion control. **Row**: everything on one wrapped row, as before. **Auto**: columns when the footer menu has groups, else a row. Site Settings > Footer > Layout. |
|
|
286
286
|
| `blurb` | string | A sentence or two under the brand in the columns layout. Plain text. |
|
|
287
287
|
| `blocks` | array | Block names (`content/blocks/<name>.html`) drawn side by side at the top of the footer on every page, in order - up to 8, lower-case letters, digits and hyphens. Rendered by the server (`buildFooterBlocks()` in `renderer.js`, both render paths) as `[block template="…" /]`, so sanitised like a page; a name that is not a block is left out. Site Settings > Footer > Footer blocks; Import from WordPress adds a site's footer widget areas here. |
|
|
288
|
-
| `showMotionToggle` | boolean
|
|
288
|
+
| `showMotionToggle`, `motionDefault`, `motionControl` | boolean, string, string | **Mirror of `motion`** (below) - `enabled`, `default` and `control`. Still honoured key by key where `motion` says nothing, so a site that predates the block renders as before; Site Settings rewrites them on every save for older readers and fleet tooling. A save that sends only these keys moves the `motion` block with them. |
|
|
289
289
|
| `class` | string | Your own CSS classes on the site footer (`#site-footer`). Site Settings > Footer. |
|
|
290
290
|
| `socialClass` | string | Your own CSS classes on every social media icon link in the footer. |
|
|
291
291
|
| `links` | array | Legacy single footer link list. Migrated to `config/menus/` on first boot (see [Multi-menu system](#multi-menu-system)); kept only so the migration can read it. |
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
### Reduce motion
|
|
294
|
+
|
|
295
|
+
The accessibility control that lets visitors turn animations off - **Site Settings > Reduce motion**.
|
|
296
|
+
`motion` in `site.json`, normalised by `server/services/motionSettings.js` (whitelisted and clamped: it is
|
|
297
|
+
written into every page as `window.__CMS_SITE__.motion`, on both render paths) and drawn by
|
|
298
|
+
`public/js/motion-control.mjs`.
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
"motion": {
|
|
302
|
+
"enabled": true,
|
|
303
|
+
"default": "system",
|
|
304
|
+
"placement": "footer",
|
|
305
|
+
"control": "switch",
|
|
306
|
+
"triggerStyle": "icon",
|
|
307
|
+
"label": "Reduce motion",
|
|
308
|
+
"position": "BL",
|
|
309
|
+
"offsetX": 32,
|
|
310
|
+
"offsetY": 32,
|
|
311
|
+
"iconSize": 52,
|
|
312
|
+
"iconRadius": 26,
|
|
313
|
+
"class": ""
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
| `motion` field | Type | Description |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `enabled` | boolean | Show the control. **Defaults to `true`**. |
|
|
320
|
+
| `default` | string | Reduced motion for a visitor who has not chosen: `system` (follow their device, the default), `off` (full motion) or `on` (calm). Their own choice always wins. Feeds the `{{motionDefault}}` bootstrap in `page.html`. |
|
|
321
|
+
| `placement` | string | `footer` (default - the bottom bar in the columns layout), `floating` (a disc fixed to a corner of the window) or `navbar` (a button at the end of the navbar; on a page with no navbar, or on a phone when the navbar is set to show only its menu button, it floats instead). |
|
|
322
|
+
| `control` | string | The footer's control: `switch` (default) or `checkbox`. |
|
|
323
|
+
| `triggerStyle` | string | Floating and navbar: `icon` (default; the label is still the accessible name) or `icon-text`. |
|
|
324
|
+
| `label` | string | The words on the control - trimmed, at most 40 characters. Default `Reduce motion`. |
|
|
325
|
+
| `position` | string | Floating: the corner - `TL`, `TR`, `BL` (default) or `BR`. |
|
|
326
|
+
| `offsetX`, `offsetY` | number | Floating: px from the side and from the top/bottom, 0-500 (default 32). |
|
|
327
|
+
| `iconSize`, `iconRadius` | number | Floating, icon only: the disc's size (32-96 px, default 52) and corner radius (0 to half the size, default 26) - Search's limits. |
|
|
328
|
+
| `class` | string | Your own CSS classes on the control, wherever it sits. |
|
|
329
|
+
|
|
330
|
+
With no `motion` block (or a key missing from it) the footer keys above decide, so every existing site
|
|
331
|
+
keeps its footer switch with no migration. The defaults are what the footer switch always was.
|
|
332
|
+
|
|
333
|
+
The floating disc defaults to the bottom left, clear of Search (right-centre) and Back to top
|
|
334
|
+
(bottom right). Put it in a corner something else already holds - Search's disc, Back to top or Go to
|
|
335
|
+
bottom, the theme switcher's disc, the breadcrumb pill - and it stacks clear of it, further from the edge;
|
|
336
|
+
the other control does not move. It never animates itself.
|
|
337
|
+
|
|
338
|
+
Hiding the control (`enabled: false`) hides the control, not the capability. A visitor who already
|
|
339
|
+
used it keeps their stored `domma:reduced_motion` preference (the one stored preference, whatever the
|
|
340
|
+
placement), and anyone whose browser sends `prefers-reduced-motion: reduce` still gets the calmer
|
|
341
|
+
site - they just lose the ability to override it.
|
|
297
342
|
|
|
298
343
|
### SEO and indexing
|
|
299
344
|
|
|
@@ -788,7 +833,7 @@ navbar, footer, `[menu]` and floating overlays (not the admin sidebar).
|
|
|
788
833
|
|
|
789
834
|
Items are numbered per list, so a dropdown or footer column staggers in on its own when it opens;
|
|
790
835
|
items past the twentieth share its delay. Visitors who asked for less motion (the OS setting or the
|
|
791
|
-
|
|
836
|
+
Reduce motion control) see the items at once.
|
|
792
837
|
|
|
793
838
|
### Item highlighting
|
|
794
839
|
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.dm-motion-fab{position:fixed;z-index:9997;width:52px;height:52px;border-radius:50%;border:1px solid var(--dm-border, rgba(0, 0, 0, .12));background:var(--dm-surface, #fff);color:var(--dm-text, #111);box-shadow:0 4px 16px #00000024;display:flex;align-items:center;justify-content:center;gap:8px;padding:0;cursor:pointer;white-space:nowrap;font:inherit;transition:none;animation:none}.dm-motion-fab:hover,.dm-motion-fab[aria-pressed=true]{background:var(--dm-primary, #5b8cff);border-color:var(--dm-primary, #5b8cff);color:var(--dm-primary-text, #fff)}.dm-motion-fab:focus-visible,.dm-motion-nav:focus-visible{outline:2px solid var(--dm-primary, #5b8cff);outline-offset:2px}.dm-motion-fab .dm-motion-icon,.dm-motion-fab svg{width:var(--dm-motion-glyph, 20px);height:var(--dm-motion-glyph, 20px);flex-shrink:0;pointer-events:none}.dm-motion-fab--pill{width:auto;height:44px;border-radius:999px;padding:0 18px 0 14px}.dm-motion-fab-label{font-size:.875rem;font-weight:500;pointer-events:none}.dm-motion-nav{display:inline-flex;align-items:center;gap:6px;background:none;border:0;padding:6px;border-radius:var(--dm-radius, 6px);color:inherit;cursor:pointer;font:inherit;font-size:var(--dm-font-size-sm, 14px);transition:none}.dm-motion-nav:hover{background:var(--dm-hover-bg, rgba(128, 128, 128, .14))}.dm-motion-nav[aria-pressed=true]{background:color-mix(in srgb,currentColor 16%,transparent)}.dm-motion-nav svg,.dm-motion-nav .dm-motion-icon{width:18px;height:18px;flex-shrink:0;pointer-events:none}@media print{.dm-motion-fab{display:none}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{applyClasses as f}from"/public/js/css-classes.mjs";const x="(prefers-reduced-motion: reduce)";export function reducedMotionOn(t={}){const e=globalThis.S?.get?.("reduced_motion");return e!=null?!!e:t.default==="on"?!0:t.default==="off"?!1:!!(window.matchMedia&&window.matchMedia(x).matches)}function s(t){globalThis.S?.set?.("reduced_motion",t),window.location.reload()}function C(t){const e=document.createElement("span");return e.className="dm-motion-icon",e.setAttribute("data-icon",t?"play":"pause"),e.setAttribute("aria-hidden","true"),e}function v(t,e){const i=t.control==="checkbox",o=document.createElement("input");o.type="checkbox",o.className=i?"footer-motion-checkbox":"form-switch-input",o.id="dm-motion-switch",o.checked=e,o.addEventListener("change",()=>s(o.checked));const n=document.createElement("span");n.className=i?"footer-motion-label":"form-switch-label",n.textContent=t.label;const a=document.createElement("label");return a.className=i?"footer-motion-check":"form-switch footer-motion-switch",a.title=t.label,a.append(o,n),a}function p(t,e,i,o){const n=document.createElement("button");if(n.type="button",n.className=i,n.id="dm-motion-toggle",n.setAttribute("aria-pressed",String(e)),n.setAttribute("aria-label",t.label),n.title=e?`${t.label}: on`:`${t.label}: off`,n.appendChild(C(e)),t.triggerStyle==="icon-text"){const a=document.createElement("span");a.className=o,a.textContent=t.label,n.appendChild(a)}return n.addEventListener("click",()=>s(!e)),n}const h=[".site-search-floating-trigger","#back-to-top-btn","#go-to-bottom-btn",".dm-theme-switcher:not(.dm-ts-inline)","nav.dm-breadcrumbs"].join(","),r=12;function E(t){const e=t.getBoundingClientRect();if(getComputedStyle(t).opacity!=="0"||t.offsetParent!==null)return e;const i=Math.min(e.top,t.offsetTop),o=Math.min(e.left,t.offsetLeft);return{top:i,left:o,right:Math.max(e.right,t.offsetLeft+t.offsetWidth),bottom:Math.max(e.bottom,t.offsetTop+t.offsetHeight),width:e.width,height:e.height}}const S=(t,e)=>t.left<e.right+r&&e.left<t.right+r&&t.top<e.bottom+r&&e.top<t.bottom+r;function g(t,e){const i=e.position==="TL"||e.position==="BL",o=e.position==="TL"||e.position==="TR";t.style.left=i?`${e.offsetX}px`:"auto",t.style.right=i?"auto":`${e.offsetX}px`;let n=e.offsetY;const a=()=>{t.style.top=o?`${n}px`:"auto",t.style.bottom=o?"auto":`${n}px`};a();const l=[...document.querySelectorAll(h)].filter(c=>c!==t&&getComputedStyle(c).position==="fixed"&&getComputedStyle(c).display!=="none");for(let c=0;c<l.length+1;c++){const w=t.getBoundingClientRect(),d=l.map(E).find(m=>m.width&&m.height&&S(w,m));if(!d)break;n=Math.round(o?d.bottom+r:window.innerHeight-d.top+r),a()}}function b(t,e){const i=t.triggerStyle==="icon-text",o=p(t,e,`dm-motion-fab${i?" dm-motion-fab--pill":""}`,"dm-motion-fab-label");return i||(o.style.width=`${t.iconSize}px`,o.style.height=`${t.iconSize}px`,o.style.borderRadius=`${t.iconRadius}px`,o.style.setProperty("--dm-motion-glyph",`${Math.round(t.iconSize*20/52)}px`)),f(o,t.class),o}function y(t,e){const i=()=>{t.isConnected&&g(t,e)};let o=0;const n=()=>{cancelAnimationFrame(o),o=requestAnimationFrame(i)};return new MutationObserver(a=>{a.some(l=>[...l.addedNodes].some(c=>c.nodeType===1&&c.matches?.(h)))&&n()}).observe(document.body,{childList:!0}),window.addEventListener("resize",n),window.addEventListener("load",n,{once:!0}),i}const u=t=>{window.Domma?.icons?.scan&&window.Domma.icons.scan(t)};function k(t,e){const i=b(t,e);return document.body.appendChild(i),y(i,t)(),i}function L(t,e,i){let o=t.querySelector(".navbar-actions");o||(o=document.createElement("div"),o.className="navbar-actions",(t.querySelector(".navbar-container")||t).appendChild(o));const n=p(e,i,"dm-motion-nav","dm-motion-nav-label");f(n,e.class);let a=null,l=null;const c=()=>{if(getComputedStyle(o).display!=="none"){a?.remove(),n.isConnected||(o.appendChild(n),u(n));return}n.remove(),a||(a=b(e,i),l=y(a,e)),a.isConnected||(document.body.appendChild(a),u(a),l())};return c(),window.addEventListener("resize",c),n.isConnected?n:a}export function mountMotionControl(t,e={}){if(!t||t.enabled===!1||document.getElementById("dm-motion-toggle")||document.getElementById("dm-motion-switch"))return null;const i=reducedMotionOn(t);let o=null;if(t.placement==="footer"){if(!e.footer)return null;o=v(t,i),f(o,t.class),e.footer.appendChild(o)}else t.placement==="navbar"&&e.navbar?o=L(e.navbar,t,i):o=k(t,i);return u(o),o}
|
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{applyMobileMenu as ae,initCarouselPopup as ie}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as re}from"/public/js/smooth-caret.js";import{decodeBase64Json as W}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as U,applyColour as G,applyPill as ce,applyFloat as le,applyHighlight as V,applyItemHighlight as de,itemHighlightAttrs as me,isCurrentItem as K,menuAlign as ue,applyClasses as P,cssClasses as Y,applyEntrance as X}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as pe}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as fe}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as he}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as ge}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as Q,markInvalidFields as Z,showValidationPopup as ee}from"/public/js/forms.js?v=20261025-forms-files-total";import{buildWizardSteps as ye}from"/public/js/form-steps.mjs";import{applyFormClasses as te,applySubmitButtons as ve,applyWizardButtons as be}from"/public/js/form-submit.mjs";import{captureFileFields as oe,destroyDropzones as we,fileBlueprint as Ce,restoreFileFields as Se,setFileLimits as xe,splitFiles as ke,submitBody as Le,totalTooLarge as $e,wireFileFields as Ee}from"/public/js/form-files.mjs";document.addEventListener("click",u=>{const i=u.target?.closest?.(".dm-banner__dismiss");i&&(u.preventDefault(),i.closest(".dm-banner")?.remove())}),$(()=>{re(document);const u=window.__CMS_NAV__||{},i=window.__CMS_SITE__||{};if(i.autoTheme?.enabled){let t=function(e){const n=(e||"07:00").split(":");return+n[0]*60+(+n[1]||0)},s=function(){const e=new Date,n=e.getHours()*60+e.getMinutes();return n>=t(l.dayStart)&&n<t(l.nightStart)?l.dayTheme:l.nightTheme},o=function(e){const n=l.apply&&l.apply[e]||{theme:e,custom:""};Domma.theme.set(n.theme);for(const m of[...document.body.classList])m.startsWith("dm-custom-")&&document.body.classList.remove(m);n.custom&&document.body.classList.add(n.custom)};const l=i.autoTheme;o(s()),setInterval(()=>o(s()),6e4)}const L=$("#site-navbar");if(L.length&&u.brand){let s=function(p,v=0){return Array.isArray(p)?p.filter(f=>!(f&&(f.type==="spacer"||f.type==="theme-switcher"))).filter(f=>!(f&&f.type==="separator"&&v===0)).map(f=>f&&f.type==="separator"?{divider:!0}:Array.isArray(f?.items)?{...f,items:s(f.items,v+1)}:f):p};const l={...u.brand},t=l.size&&l.size!=="md"?` navbar-brand-${l.size}`:"";if(l.logo||l.icon||l.tagline){let p="";l.logo?p+=`<img src="${l.logo}" class="navbar-brand-logo" alt="${l.text||""}">`:l.icon&&(p+=`<span data-icon="${l.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),l.text&&(p+=`<span class="navbar-brand-text${t}">${l.text}</span>`),l.tagline&&(p+=`<small class="navbar-brand-tagline">${l.tagline}</small>`),l.html=p}else t&&l.text&&(l.html=`<span class="navbar-brand-text${t}">${l.text}</span>`);const o=s(u.items||[]),e=u.position==="floating",n=u.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:l,items:o,variant:u.variant||"dark",position:e?"static":u.position||"sticky",collapsible:!0,appearOnHover:!!u.appearOnHover}),Domma.icons.scan("#site-navbar");const m=L.get(0);ae(m,u.mobile);const b=()=>{if(!n)return;const v=m.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");v&&!v.classList.contains("open")&&v.querySelector(":scope > .navbar-dropdown-toggle")?.click()};m.classList.add(`dm-nav-align-${ue(u)||"centre"}`),n&&L.get(0).classList.add("dm-nav-vertical",u.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const a=u.dock||{};n&&!e&&Ae(m,u.side==="right"),!n&&a.hideOnScroll&&Te(m),V(m,u),X(m,u),P(m,u.class),e&&(le(L.get(0),u),ge(L.get(0).parentNode||document));const c=m.querySelector(".navbar-toggle"),g=m.querySelector(".navbar-collapse");if(c&&g){let p=null;const v=()=>{g.classList.contains("show")&&c.click()},f=()=>{p&&(p.remove(),p=null),document.body.classList.remove("dm-drawer-open")},d=()=>{p||(p=document.createElement("div"),p.className="dm-navbar-backdrop",p.addEventListener("click",v),document.body.appendChild(p),document.body.classList.add("dm-drawer-open"))};c.addEventListener("click",()=>{requestAnimationFrame(()=>{g.classList.contains("show")?(b(),d()):f()})}),g.addEventListener("click",y=>{y.target.closest("a")&&requestAnimationFrame(f)}),document.addEventListener("keydown",y=>{y.key==="Escape"&&g.classList.contains("show")&&v()})}const w=m.querySelector(".navbar-nav");if(w){const p=Array.from(w.children);let v=0;for(const f of u.items||[])if(!(f&&f.type==="separator")){if(f&&f.type==="spacer"){const d=document.createElement("li"),y=["xs","sm","md","lg","xl","flex"].includes(f.size)?f.size:"md";d.className=`dm-menu-spacer dm-menu-spacer--${y}`,d.setAttribute("aria-hidden","true"),w.insertBefore(d,p[v]||null);continue}if(f&&f.type==="theme-switcher"){const d=document.createElement("li");d.className="dm-menu-theme-switcher";const y=document.createElement("span");y.dataset.themeSwitcher="menu",y.dataset.position="inline",d.appendChild(y),w.insertBefore(d,p[v]||null);continue}v++}window.CMSThemeSwitcher?.scan(w)}m.querySelectorAll("[data-path]").forEach(p=>{const v=p.dataset.path.split(".").map(Number),f=v.length>1,d=v.reduce((C,D)=>C?.items?.[D],{items:o});if(!d)return;d.colour&&G(p,d.colour),d.pill&&!f&&ce(p,d.pill),de(p,d),P(p.closest("li"),d.class);const y=U(d.badge);y&&p.appendChild(y),K(d.url,location.pathname)&&(p.classList.add("active"),p.setAttribute("aria-current","page"))}),pe(m,u,o),b()}const h=$("#site-footer");if(h.length){let e=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},n=function(r){return e(r).replace(/'/g,"'")},m=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;if(r&&r.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const N=n(r.url||"#"),k=e(r.text||""),M=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(m).join("")}</ul>`:"",j=me(r),ne=[j.hover&&` data-menu-item-hover="${n(j.hover)}"`,j.active&&` data-menu-item-active="${n(j.active)}"`].filter(Boolean).join(""),se=j.accent?` style="--dm-menu-highlight:${n(j.accent)}"`:"",J=Y(r.class).join(" ");return`<li${J?` class="${n(J)}"`:""}><a href="${N}"${ne}${se}>${k}</a>${M}</li>`},a=function(){if(!i.footer)return"";const r=Object.keys(t).filter(k=>l[k]);if(!r.length)return"";const N=Y(i.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(k=>{const{label:M,svg:j}=t[k];return`<a href="${l[k]}" target="_blank" rel="noopener noreferrer" aria-label="${M}" class="footer-social-link${N?` ${n(N)}`:""}">${j}</a>`}).join("")}</div>`},p=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${m(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const N=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(m).join("")}</ul>`:"",k=w(r)?`<span class="cms-footer-col-title">${e(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${n(r.url||"#")}">${e(r.text||"")}</a>`,M=["cms-footer-col",...Y(r.class)].join(" ");return`<li class="${n(M)}">${k}${N}</li>`},d=function(r,N,k=!1){return(r||[]).forEach(M=>{M&&(M.type==="separator"||M.type==="spacer"||M.type==="theme-switcher")||(k&&w(M)||N.push(M),Array.isArray(M.items)&&M.items.length&&d(M.items,N))}),N};const l=i.social||{},t={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const o=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},b=(()=>{const r=i.cookieConsent;if(!r||r.showSettingsLink===!1)return"";const N=k=>(k||[]).some(M=>M&&(M.url==="#cookie-settings"||N(M.items)));return N(o.primary)||N(o.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${e(r.settingsLinkText||"Cookie settings")}</a></li>`})(),c=["columns","row"].includes(i.footer?.layout)?i.footer.layout:"auto",g=i.footer?.look==="showcase"||c==="columns"||c==="auto"&&o.primary.some(r=>Array.isArray(r?.items)&&r.items.length),w=r=>g&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(g){const r=u.brand||{},N=r.text||i.title||"",k=r.logo?`<img src="${n(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",M=String(i.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${N||k?`<a class="cms-footer-brand-name" href="${n(r.url||"/")}">${k}<span>${e(N)}</span></a>`:""}${M?`<p class="cms-footer-blurb">${e(M)}</p>`:""}${a()}</div>`,o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${o.primary.map(p).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',o.copyright&&(s+=`<p class="cms-footer-copyright">${e(o.copyright)}</p>`),(o.legal.length||b)&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${o.legal.map(m).join("")}${b}</ul></nav>`),s+="</div>"}else o.copyright&&(s+=`<p>${e(o.copyright)}</p>`),o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${o.primary.map(m).join("")}</ul></nav>`),(o.legal.length||b)&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${o.legal.map(m).join("")}${b}</ul></nav>`),s+=a();s+="</div>",h.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const v=h.get(0).querySelector(":scope > .cms-footer-blocks");v?.remove(),h.html(s),v&&h.get(0).prepend(v),window.CMSThemeSwitcher?.scan(h.get(0));const f=h.get(0).querySelector(".cms-footer-brand-icon");f&&u.brand?.icon&&f.setAttribute("data-icon",u.brand.icon),P(h.get(0),i.footer?.class),g&&he(h.get(0),i);const y=[...d(o.primary,[],!0),...d(o.legal,[])];h.get(0).querySelectorAll(".footer-nav a").forEach((r,N)=>{const k=y[N];if(!k)return;k.colour&&G(r,k.colour);const M=U(k.badge);M&&r.appendChild(M),K(k.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),V(h.get(0).querySelector(".footer-nav--primary"),{highlight:o.primaryHighlight}),V(h.get(0).querySelector(".footer-nav--legal"),{highlight:o.legalHighlight}),X(h.get(0).querySelector(".footer-nav--primary"),{entrance:o.primaryEntrance}),X(h.get(0).querySelector(".footer-nav--legal"),{entrance:o.legalEntrance}),P(h.get(0).querySelector(".footer-nav--primary"),o.primaryClass),P(h.get(0).querySelector(".footer-nav--legal"),o.legalClass);const D=i.footer?.showMotionToggle!==!1,z=S.get("reduced_motion"),B=i.footer?.motionDefault,O=z!=null?!!z:B==="on"?!0:B==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),A=i.footer?.motionControl==="checkbox",I=h.get(0).querySelector(".cms-footer-bottom")||h.get(0).querySelector(".footer-inner");if(D&&I){const r=document.createElement("input");r.type="checkbox",r.className=A?"footer-motion-checkbox":"form-switch-input",r.id="dm-motion-switch",r.checked=O,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const N=document.createElement("span");N.className=A?"footer-motion-label":"form-switch-label",N.textContent="Reduce motion";const k=document.createElement("label");k.className=A?"footer-motion-check":"form-switch footer-motion-switch",k.title="Reduce motion",k.appendChild(r),k.appendChild(N),I.appendChild(k)}}const q=document.getElementById("site-sidebar");q&&_e(q),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const x=$(".page-body");if(x.length){x.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),x.find(".tabs").each(function(){Domma.elements.tabs(this)}),x.find(".carousel").each(function(){const t={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},s=a=>{const c=String(a||"").trim();return c?t[c.toLowerCase()]?`var(${t[c.toLowerCase()]})`:CSS.supports("color",c)?c:"":""},o=s(this.dataset.background),e=s(this.dataset.arrowBackground),n=s(this.dataset.arrowColor);o&&(this.style.setProperty("--dm-carousel-background",o),this.classList.add("dm-carousel-custom-background")),e&&this.style.setProperty("--dm-carousel-arrow-background",e),n&&this.style.setProperty("--dm-carousel-arrow-color",n);const m=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${m}`);const b={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const a=Number(this.dataset.height);Number.isFinite(a)&&a>0&&(b.height=a)}if(this.dataset.animationDuration){const a=parseInt(this.dataset.animationDuration,10);Number.isFinite(a)&&(b.animationDuration=a)}this.dataset.animationEasing&&(b.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(a=>a.cloneNode(!0)),this._dmCarouselOpts=b),this._dmCarousel=Domma.elements.carousel(this,b),ie(this)}),x.find(".dm-countdown").each(function(){const t={autoStart:!0},s=String(this.dataset.format||"").trim(),o=String(this.dataset.to||"").trim(),e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(o),n=e?new Date(+e[1],+e[2]-1,+e[3]).getTime():o?new Date(o).getTime():NaN;Number.isFinite(n)?t.duration=Math.max(0,n-Date.now()):this.dataset.duration&&(t.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const m=/^dd:/i.test(s);t.format=m?"hh:mm:ss":s||(t.duration>=36e5?"hh:mm:ss":"mm:ss");const b=this,a=g=>String(g).padStart(2,"0"),c=g=>{const w=Math.max(0,Math.floor(g/1e3)),p=b.querySelector(".dm-timer-time");p&&(p.textContent=`${a(Math.floor(w/86400))}:${a(Math.floor(w%86400/3600))}:${a(Math.floor(w%3600/60))}:${a(w%60)}`)};m&&(t.onTick=c),Domma.elements.timer(this,t),m&&c(t.duration||0)}),x.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),x.find(".dm-progression").each(function(){const t=Array.from(this.querySelectorAll(".dm-progression-item")).map(e=>{const n={title:e.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:e.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return e.dataset.date&&(n.date=e.dataset.date),e.dataset.status&&(n.status=e.dataset.status),e.dataset.eventIcon&&(n.icon=e.dataset.eventIcon),e.dataset.entryId&&(n._entryId=e.dataset.entryId),n}),s=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",o=Domma.elements.progression(this,{layout:s,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...t.length?{items:t}:{}});if((this.dataset.mode||"timeline")==="timeline"){const e={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((n,m)=>{const b=t[m];if(!b)return;const a=n.querySelector(".dm-progression-title");if(b.icon&&a){const g=document.createElement("span");g.dataset.icon=b.icon,g.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",g.setAttribute("aria-hidden","true"),a.prepend(g)}const c=e[b.status];if(c&&a){const g=document.createElement("span");g.className=`badge badge-${c.tone} cms-timeline-status`,g.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const w=document.createElement("span");w.dataset.icon=c.icon,w.dataset.iconSize="14",w.setAttribute("aria-hidden","true"),g.append(w,document.createTextNode(c.label)),a.after(g)}})}Domma.icons.scan(this),t.length&&(this._dmCtx={instance:o,items:t})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const l=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const s=t.dataset.revealMode||"stagger",o=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||(o.startsWith("swoosh")?700:400),n=parseInt(t.dataset.revealStagger,10)||(o.startsWith("swoosh")?90:60),m=parseInt(t.dataset.revealDelay,10)||0,b=t.dataset.revealDirection==="rtl",a=t.dataset.revealReplay==="true",c=Array.from(t.children);if(l&&o.startsWith("swoosh"))return;const g={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)",swoosh:"translateY(36px) scale(0.96)"},w=d=>o!=="swoosh-sides"?g[o]||"":c.length>1&&d===0?"translateX(-64px) rotate(-1.5deg)":c.length>1&&d===c.length-1?"translateX(64px) rotate(1.5deg)":"translateY(40px) scale(0.94)",p=o.startsWith("swoosh"),v=p?"cubic-bezier(.2,.8,.2,1)":"ease",f=(d,y)=>{d.style.opacity="0",d.style.transform=w(y),p&&(d.style.filter="blur(10px)")};c.forEach((d,y)=>{f(d,y),d.style.transition=`opacity ${e}ms ${v}, transform ${e}ms ${v}`+(p?`, filter ${e}ms ${v}`:"");const C=b?c.length-1-y:y;d.style.transitionDelay=s==="stagger"?`${m+C*n}ms`:`${m}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const d=new IntersectionObserver(y=>{y.forEach(C=>{if(C.isIntersecting){if(C.target.offsetWidth,C.target.style.opacity="1",C.target.style.transform="",p&&(C.target.style.filter=""),!a){d.unobserve(C.target);const D=C.target;D.addEventListener("transitionend",function z(B){B.target!==D||B.propertyName!=="opacity"||(D.removeEventListener("transitionend",z),D.style.transition="",D.style.transitionDelay="")})}}else a&&C.boundingClientRect.top>0&&f(C.target,c.indexOf(C.target))})},{threshold:.1});c.forEach(y=>d.observe(y))}))}),fe(document.querySelector(".page-body")||document),x.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),x.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,s=document.getElementById(t);if(!s)return;const o=E.slideover({title:s.dataset.soTitle||"",size:s.dataset.soSize||"md",position:s.dataset.soPosition||"right"});s.style.display="",o.setContent(s),o.open()})})}if(typeof $.setup=="function"){const l=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const s=W(t.dataset.config);Object.assign(l,s)}catch{}}),Object.keys(l).length>0){const t={};for(const[s,o]of Object.entries(l)){const e=o?.events?.click,{confirm:n,toast:m,alert:b,prompt:a,...c}=e||{};if(n||m||b||a){$(s).on("click",async function(N){if(N.preventDefault(),n&&!await E.confirm(n))return;let k=null;if(!(a&&(k=await E.prompt(a,{inputPlaceholder:c.promptPlaceholder||"",inputValue:c.promptDefault||""}),k===null))){{const M=$(c.target||this);c.toggleClass&&M.toggleClass(c.toggleClass),c.addClass&&M.addClass(c.addClass),c.removeClass&&M.removeClass(c.removeClass),k!==null&&(c.setText&&M.text(k),c.setVal&&M.val(k),c.setAttr&&M.attr(c.setAttr,k))}c.href&&(window.location.href=c.href),m&&E.toast(m,{type:c.toastType||"success"}),b&&E.alert(b)}});const{target:w,toggleClass:p,addClass:v,removeClass:f,setText:d,setVal:y,setAttr:C,href:D,toastType:z,promptPlaceholder:B,promptDefault:O,...A}=c,I={...o.events};Object.keys(A).length?I.click=w?{target:w,...A}:A:delete I.click;const r={...o,events:I};Object.keys(I).length||delete r.events,Object.keys(r).length&&(t[s]=r)}else t[s]=o}$.setup(t)}}x.length&&R(x.get(0))}),window.wireCTAButtons=R;function R(u){u.querySelectorAll(".dm-cta-trigger").forEach(i=>{i.__ctaWired||(i.__ctaWired=!0,i.addEventListener("click",async()=>{const L=i.dataset.action,h=i.dataset.entry,q=i.dataset.confirm;let x=S.get("auth_token");if(!x){E.toast("Please log in to perform this action.",{type:"warning"});return}if(q&&!await E.confirm(q))return;const l=Array.from(i.childNodes).map(s=>s.cloneNode(!0));i.disabled=!0,i.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${L}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(i.dataset.scope?{entryId:h,scope:i.dataset.scope}:{entryId:h})});try{let s=await t(x);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const n=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(n.ok){const{token:m}=await n.json();S.set("auth_token",m),x=m,s=await t(x)}}}const o=await s.json().catch(()=>({}));if(!s.ok||o.success===!1)throw new Error(o.error||o.message||`Error ${s.status}`);E.toast(o.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{i.disabled=!1,i.textContent="",l.forEach(s=>i.appendChild(s)),Domma.icons.scan(i)}}))})}(function(){const i=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!i.length)return;const L=S.get("auth_token");function h(x,l){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(l){s.appendChild(document.createTextNode("Please "));const o=document.createElement("a");o.href="/login",o.textContent="sign in",s.appendChild(o),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=x;return t.appendChild(s),t}function q(x,l){const t=document.createElement("template");t.innerHTML=l||"",x.replaceWith(t.content)}i.forEach(async x=>{const l=x.dataset.collectionAttrs;if(!l)return;if(!L){x.replaceChildren(h("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:l})});try{let s=L,o=await t(s);if(o.status===401){const n=S.get("auth_refresh_token");if(n){const m=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:n})});if(m.ok){const{token:b}=await m.json();S.set("auth_token",b),s=b,o=await t(s)}}}if(!o.ok){const{error:n}=await o.json().catch(()=>({}));x.replaceChildren(h(n||"Unable to load entries.",!1));return}const{html:e}=await o.json();q(x,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{x.replaceChildren(h("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),R(document.body)},100)})(),(function(){const i=document.querySelectorAll("[data-collection-table]");!i.length||typeof T>"u"||!T.create||i.forEach(L=>{let h;try{h=W(L.dataset.payload)}catch{return}const{columns:q,rows:x,search:l,sortable:t,exportable:s,pageSize:o,empty:e,ctaConfig:n,slug:m,exportCfg:b}=h;if(!q?.length)return;if(n){const g=q.findIndex(w=>w.key==="_cta");g!==-1&&(q[g]={key:"_cta",title:"",render:(w,p)=>{const v=document.createElement("button");if(v.className=`btn btn-${n.style||"primary"} dm-cta-trigger`,v.dataset.action=n.action||"",n.scope&&(v.dataset.scope=n.scope),v.dataset.entry=p._entryId||"",n.confirm&&(v.dataset.confirm=n.confirm),n.icon){const f=document.createElement("span");f.dataset.icon=n.icon,v.appendChild(f),v.appendChild(document.createTextNode(" "))}return v.appendChild(document.createTextNode(n.label||"Run")),v}})}const a="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=a,L.replaceChildren(c),T.create("#"+a,{data:x,columns:q,search:l,sortable:t,exportable:s,pageSize:o,emptyMessage:e}),n&&R(c),L._dmTable={columns:q,rows:x,slug:m,exportCfg:b,render(g){T.create("#"+a,{data:g,columns:q,search:l,sortable:t,exportable:s,pageSize:o,emptyMessage:e}),n&&R(c)}}})})(),(function(){const i=document.querySelectorAll("[data-form-inline]");if(!i.length||typeof F>"u")return;function L(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function h(t,s){const o={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const n=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),m=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":n?"chooser":e.type,b={...e.formConfig||{}};b.span==="full"&&s&&(b.span=s);const a={type:m,required:L(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...b}};e.minLength!==void 0&&(a.minLength=e.minLength),e.maxLength!==void 0&&(a.maxLength=e.maxLength),e.min!==void 0&&(a.min=e.min),e.max!==void 0&&(a.max=e.max),n&&(e.variant&&(a.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(a.multiple=!0),e.density&&(a.density=e.density),e.columns&&(a.columns=e.columns),e.accent&&(a.accent=e.accent),e.accentStyle&&(a.accentStyle=e.accentStyle),e.glow&&(a.glow=!0),e.glowColour&&(a.glowColour=e.glowColour),e.shadow&&(a.shadow=e.shadow),e.shadowColour&&(a.shadowColour=e.shadowColour)),o[e.name]=e.type==="file"?Ce(e,a):a}),o}function q(t){const s={};return(t||[]).forEach(o=>{if(!(!o.name||o.type==="page-break"||o.type==="spacer")&&(o.type==="select"||o.type==="multiselect")&&o.required){const e=(o.options||[])[0];e&&(s[o.name]=typeof e=="object"?e.value:e)}}),s}function x(t,s){(s||[]).forEach(o=>{if(o.type!=="date"||!o.name)return;const e=t.querySelector(`[name="${o.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function l(t,s,o){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=o?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}i.forEach(t=>{let s;try{s=W(t.dataset.formInline)}catch{return}t.querySelector(":scope > .cms-form-embed-summary")?.remove();const o=s.fields||[],e=s.settings||{},n=Number(e.columns)===3?3:2,m=e.layout||"grid",b=o.some(f=>f.type==="page-break"),a=new Map;xe(t,s.fileLimits,a),Ee(t,o);const c=async f=>{if(t.dataset.triggerBlocked==="true"){const d=t._formLogicRuntime,y=d&&d.submitBlock?d.submitBlock().reasons:[];l(t,y.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const d=t.querySelector(".fb-form-honeypot"),y=d?.querySelector('input[type="text"]')||null,C=d?.querySelector('[name="_t"]')||null,D=Object.assign({},f);delete D._t,d?.dataset.hpName&&delete D[d.dataset.hpName],y!==null&&(D._hp=y.value),C!==null&&(D._t=C.value);const z=new Map(a);oe(t,z);const B=ke(D,o,z),O=$e(t,B.files);if(O){l(t,O,!0);const I=new Error(O);throw I.formSubmitFailed=!0,I}const A=await H.post(`/api/forms/submit/${s.slug}`,B.files.size?Le(B.values,B.files).body:B.values);if(A&&A.error){l(t,A.error,!0);const I=new Error(A.error);throw I.formSubmitFailed=!0,I}if(A?.redirect){window.location.href=A.redirect;return}for(we(t);t.firstChild;)t.removeChild(t.firstChild);l(t,A?.message||e.successMessage||"Thank you for your submission.",!1)}catch(d){throw d.formSubmitFailed||l(t,d.message||"Submission failed. Please try again.",!0),d}},g=Date.now();function w(f){const d=f.querySelector("form");if(!d||d.querySelector(".fb-form-honeypot"))return;const y=document.createElement("div");y.className="fb-form-honeypot",y.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=d.querySelector('[name="website"]')?"_website":"website",y.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",y.appendChild(C);const D=document.createElement("input");D.name="_t",D.type="hidden",D.value=g,y.appendChild(D),d.appendChild(y)}function p(f){if(!window.FormLogicEngine||!o.some(y=>y.logic||Array.isArray(y.triggers)&&y.triggers.length))return null;const d=new window.FormLogicEngine.FormLogicRuntime(s,t,f?{baseValues:f}:void 0);if(d.init(),t._formLogicRuntime=d,t._formLogicObserved)return d;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const y=new MutationObserver(function(C){for(const D of C)for(const z of D.removedNodes)if(z===t||z.nodeType===1&&z.contains&&z.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),y.disconnect();return}});y.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=y}return d}let v=!1;if(t.addEventListener("dm-form-end",f=>{const d=f.detail?.validation;if(d&&!d.valid){Z(t,d.missing),ee(d);return}v||(v=!0,c(f.detail?.values||{}).catch(()=>{}).finally(()=>{v=!1}))}),b&&F.wizard){const f=ye(o,s.title),d=f.map(A=>({title:A.title,description:A.description,fields:h(A.fields,n)})),y=f.map(A=>A.fields);let C=0,D=null,z=null;const B=A=>{const I=window.FormLogicEngine;if(!I||!I.validateFields)return;const r=I.validateFields(t,y[C]||[]);r.valid||(A.preventDefault(),A.stopImmediatePropagation(),Z(t,r.missing),ee(r))},O=()=>{const A=y[C]||o;be(t,e),te(t,e,A),x(t,A),Q(t,A),Se(t,a),e.honeypot!==!1&&w(t),D&&D.destroy(),D=p(z?.getStepData?z.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(I=>I.addEventListener("click",B,!0))};z=F.wizard(t,{schema:{steps:d},layout:m,columns:n,finishText:e.submitText||"Finish",onSubmit:c,onStepChange:A=>{oe(t,a),C=A,setTimeout(O,0)}}),t.addEventListener("dm-form-trigger",A=>{if(A.detail?.type!=="goto-step"||!z?.goToStep)return;const I=Number(A.detail.action.step);!Number.isFinite(I)||I<0||I>=d.length||I!==C&&z.goToStep(I)}),Promise.resolve(z).then(O)}else if(F.render){const f=F.render(t,h(o,n),q(o),{submitText:e.submitText||"Submit",layout:m,columns:n,onSubmit:c});Promise.resolve(f).then(function(){ve(t,e,m),te(t,e,o),x(t,o),Q(t,o),e.honeypot!==!1&&w(t),p()})}})})();function Ae(u,i){const L=document.body,h=L.classList,q=new Set((L.dataset.railSite||"").split(" ").filter(Boolean)),x=()=>window.matchMedia("(min-width: 993px)").matches;u.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(n=>{const m=n.textContent.trim();m&&(n.setAttribute("title",m),n.querySelector("svg, [data-icon]")||(n.dataset.initial=m.charAt(0).toUpperCase()))});const l=document.createElement("button");l.type="button",l.className="dm-nav-collapse-toggle";const t=()=>{const n=h.contains("dm-vnav-collapsed");l.setAttribute("aria-pressed",String(n)),l.setAttribute("aria-label",n?"Expand menu":"Collapse menu"),l.title=l.getAttribute("aria-label");const m=i?"chevrons-left":"chevrons-right",b=i?"chevrons-right":"chevrons-left";l.innerHTML=`<span data-icon="${n?m:b}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(l)},s=n=>{h.toggle("dm-vnav-collapsed",n),S.set("nav_collapsed",n),t()};l.addEventListener("click",()=>s(!h.contains("dm-vnav-collapsed"))),t();const o=u.querySelector(".navbar-container")||u;o.insertBefore(l,o.firstChild?.nextSibling||null);const e=n=>{h.toggle("dm-vnav-autohide",n),h.toggle("dm-vnav-collapsible",!n&&q.has("collapsible")),n?h.remove("dm-vnav-collapsed"):q.has("collapsible")&&S.get("nav_collapsed")===!0&&h.add("dm-vnav-collapsed"),u.classList.remove("dm-nav-revealed"),t()};u.addEventListener("pointerdown",n=>{h.contains("dm-vnav-autohide")&&n.pointerType!=="mouse"&&!u.classList.contains("dm-nav-revealed")&&(u.classList.add("dm-nav-revealed"),n.preventDefault())}),document.addEventListener("pointerdown",n=>{u.contains(n.target)||u.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const n=h.contains("dm-vnav-autohide"),m=h.contains("dm-vnav-collapsed"),b=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:m?"Expand menu":"Collapse to icons",icon:m?"maximize":"minimize",disabled:!x()||n||!q.has("collapsible"),action:()=>s(!m)},{label:n?"Keep on screen":"Hide when not in use",icon:n?"eye":"eye-off",disabled:!x(),action:()=>{S.set("nav_autohide",!n),e(!n)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!b,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(q.has("autohide")),h.toggle("dm-vnav-collapsed",!q.has("autohide")&&q.has("start-collapsed")),t()}}]}})}function Te(u){u.classList.add("dm-nav-scroll-hide");let i=window.scrollY,L=!1;window.addEventListener("scroll",()=>{L||(L=!0,requestAnimationFrame(()=>{L=!1;const h=window.scrollY,q=u.matches(":focus-within")||u.querySelector(".navbar-dropdown.open, .navbar-collapse.show");h>i&&h>u.offsetHeight&&!q?u.classList.add("dm-nav-scrolled-away"):h<i&&u.classList.remove("dm-nav-scrolled-away"),i=h}))},{passive:!0})}function _e(u){let i={};try{i=JSON.parse(u.dataset.contents||"{}")}catch{}const L=u.closest(".site-main"),h=document.querySelector(".site-content"),q=new Set(String(i.exclude||"").split(",").map(a=>a.trim().toLowerCase()).filter(Boolean)),x=a=>!a.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),l=a=>(h?[...h.querySelectorAll(a)]:[]).filter(x).filter(c=>!q.has(c.textContent.trim().toLowerCase())),t=i.headings==="h2"?l("h2"):i.headings==="h3"?l("h3"):i.headings==="h2h3"?l("h2, h3"):l("h2").length?l("h2"):l("h3");if(!t.length){L?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(a=>a.id)),o=document.createElement("nav");o.className="dm-contents",o.setAttribute("aria-label",i.title||"Page contents"),i.hover&&(o.dataset.menuHover=i.hover),i.selected&&(o.dataset.menuActive=i.selected),i.accent&&o.style.setProperty("--dm-menu-highlight",i.accent.startsWith("#")?i.accent:`var(--dm-${i.accent})`),i.font&&(o.style.fontFamily=i.font),i.fontSize&&(o.style.fontSize=i.fontSize),i.fontWeight&&(o.style.fontWeight=i.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const n=document.createElement("p");n.className="dm-contents-title",n.textContent=i.title||"",e.appendChild(n),o.appendChild(e);const m=document.createElement("ol");m.className="dm-contents-list"+(i.numbered?" is-numbered":"");const b=[];for(const a of t){if(!a.id){const w=(a.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let p=w;for(let v=2;s.has(p);v++)p=`${w}-${v}`;s.add(p),a.id=p}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${a.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${a.id}`,g.textContent=a.textContent.trim(),c.appendChild(g),m.appendChild(c),b.push([a,g])}if(o.appendChild(m),u.replaceChildren(o),i.scrollSpy!==!1&&"IntersectionObserver"in window){const a=new Map,c=()=>{const w=b.find(([p])=>a.get(p))||null;b.forEach(([,p])=>{const v=w&&p===w[1];p.classList.toggle("is-active",!!v),v?p.setAttribute("aria-current","location"):p.removeAttribute("aria-current")})},g=new IntersectionObserver(w=>{w.forEach(p=>a.set(p.target,p.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});b.forEach(([w])=>g.observe(w))}if(L?.classList.contains("contents-collapsible")){const a=document.createElement("button");a.type="button",a.className="dm-contents-toggle";const c=()=>{const g=L.classList.contains("contents-collapsed");a.setAttribute("aria-expanded",String(!g)),a.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),a.title=a.getAttribute("aria-label"),a.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(a)};a.addEventListener("click",()=>{S.set("contents_collapsed",L.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(a,e.firstChild)}if(L?.classList.contains("contents-floating")&&i.movable){o.classList.add("is-movable");const a=S.get("contents_position");a&&Number.isFinite(a.x)&&Object.assign(u.style,{left:`${a.x}px`,top:`${a.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",c=>{if(c.target.closest("button"))return;const g=u.getBoundingClientRect(),w=c.clientX-g.left,p=c.clientY-g.top,v=d=>{const y=Math.max(0,Math.min(window.innerWidth-g.width,d.clientX-w)),C=Math.max(0,Math.min(window.innerHeight-40,d.clientY-p));Object.assign(u.style,{left:`${y}px`,top:`${C}px`,right:"auto",bottom:"auto"})},f=()=>{document.removeEventListener("pointermove",v),S.set("contents_position",{x:parseInt(u.style.left,10),y:parseInt(u.style.top,10)})};document.addEventListener("pointermove",v),document.addEventListener("pointerup",f,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const a=L?.classList.contains("contents-collapsible"),c=L?.classList.contains("contents-collapsed"),g=b.find(([,w])=>w.classList.contains("is-active"));return[{header:"Contents"},...a?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>u.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const w=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(w),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),L?.classList.contains("contents-autohide")&&(u.addEventListener("pointerdown",a=>{a.pointerType!=="mouse"&&!u.classList.contains("is-revealed")&&(u.classList.add("is-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{u.contains(a.target)||u.classList.remove("is-revealed")}))}
|
|
1
|
+
import{applyMobileMenu as oe,initCarouselPopup as ne}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as se}from"/public/js/smooth-caret.js";import{decodeBase64Json as W}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as J,applyColour as U,applyPill as ae,applyFloat as ie,applyHighlight as V,applyItemHighlight as re,itemHighlightAttrs as ce,isCurrentItem as G,menuAlign as le,applyClasses as j,cssClasses as Y,applyEntrance as X}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as de}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as me}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as ue}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as pe}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as K,markInvalidFields as Q,showValidationPopup as Z}from"/public/js/forms.js?v=20261025-forms-files-total";import{buildWizardSteps as fe}from"/public/js/form-steps.mjs";import{mountMotionControl as he}from"/public/js/motion-control.mjs?v=20261029-motion-under-switcher";import{applyFormClasses as ee,applySubmitButtons as ge,applyWizardButtons as ye}from"/public/js/form-submit.mjs";import{captureFileFields as te,destroyDropzones as ve,fileBlueprint as be,restoreFileFields as we,setFileLimits as Ce,splitFiles as Se,submitBody as xe,totalTooLarge as ke,wireFileFields as Le}from"/public/js/form-files.mjs";document.addEventListener("click",u=>{const r=u.target?.closest?.(".dm-banner__dismiss");r&&(u.preventDefault(),r.closest(".dm-banner")?.remove())}),$(()=>{se(document);const u=window.__CMS_NAV__||{},r=window.__CMS_SITE__||{};if(r.autoTheme?.enabled){let s=function(o){const c=(o||"07:00").split(":");return+c[0]*60+(+c[1]||0)},n=function(){const o=new Date,c=o.getHours()*60+o.getMinutes();return c>=s(t.dayStart)&&c<s(t.nightStart)?t.dayTheme:t.nightTheme},e=function(o){const c=t.apply&&t.apply[o]||{theme:o,custom:""};Domma.theme.set(c.theme);for(const f of[...document.body.classList])f.startsWith("dm-custom-")&&document.body.classList.remove(f);c.custom&&document.body.classList.add(c.custom)};const t=r.autoTheme;e(n()),setInterval(()=>e(n()),6e4)}const L=$("#site-navbar");if(L.length&&u.brand){let n=function(m,w=0){return Array.isArray(m)?m.filter(d=>!(d&&(d.type==="spacer"||d.type==="theme-switcher"))).filter(d=>!(d&&d.type==="separator"&&w===0)).map(d=>d&&d.type==="separator"?{divider:!0}:Array.isArray(d?.items)?{...d,items:n(d.items,w+1)}:d):m};const t={...u.brand},s=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let m="";t.logo?m+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(m+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(m+=`<span class="navbar-brand-text${s}">${t.text}</span>`),t.tagline&&(m+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=m}else s&&t.text&&(t.html=`<span class="navbar-brand-text${s}">${t.text}</span>`);const e=n(u.items||[]),o=u.position==="floating",c=u.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:t,items:e,variant:u.variant||"dark",position:o?"static":u.position||"sticky",collapsible:!0,appearOnHover:!!u.appearOnHover}),Domma.icons.scan("#site-navbar");const f=L.get(0);oe(f,u.mobile);const a=()=>{if(!c)return;const w=f.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");w&&!w.classList.contains("open")&&w.querySelector(":scope > .navbar-dropdown-toggle")?.click()};f.classList.add(`dm-nav-align-${le(u)||"centre"}`),c&&L.get(0).classList.add("dm-nav-vertical",u.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const h=u.dock||{};c&&!o&&$e(f,u.side==="right"),!c&&h.hideOnScroll&&Ee(f),V(f,u),X(f,u),j(f,u.class),o&&(ie(L.get(0),u),pe(L.get(0).parentNode||document));const l=f.querySelector(".navbar-toggle"),y=f.querySelector(".navbar-collapse");if(l&&y){let m=null;const w=()=>{y.classList.contains("show")&&l.click()},d=()=>{m&&(m.remove(),m=null),document.body.classList.remove("dm-drawer-open")},p=()=>{m||(m=document.createElement("div"),m.className="dm-navbar-backdrop",m.addEventListener("click",w),document.body.appendChild(m),document.body.classList.add("dm-drawer-open"))};l.addEventListener("click",()=>{requestAnimationFrame(()=>{y.classList.contains("show")?(a(),p()):d()})}),y.addEventListener("click",v=>{v.target.closest("a")&&requestAnimationFrame(d)}),document.addEventListener("keydown",v=>{v.key==="Escape"&&y.classList.contains("show")&&w()})}const C=f.querySelector(".navbar-nav");if(C){const m=Array.from(C.children);let w=0;for(const d of u.items||[])if(!(d&&d.type==="separator")){if(d&&d.type==="spacer"){const p=document.createElement("li"),v=["xs","sm","md","lg","xl","flex"].includes(d.size)?d.size:"md";p.className=`dm-menu-spacer dm-menu-spacer--${v}`,p.setAttribute("aria-hidden","true"),C.insertBefore(p,m[w]||null);continue}if(d&&d.type==="theme-switcher"){const p=document.createElement("li");p.className="dm-menu-theme-switcher";const v=document.createElement("span");v.dataset.themeSwitcher="menu",v.dataset.position="inline",p.appendChild(v),C.insertBefore(p,m[w]||null);continue}w++}window.CMSThemeSwitcher?.scan(C)}f.querySelectorAll("[data-path]").forEach(m=>{const w=m.dataset.path.split(".").map(Number),d=w.length>1,p=w.reduce((A,i)=>A?.items?.[i],{items:e});if(!p)return;p.colour&&U(m,p.colour),p.pill&&!d&&ae(m,p.pill),re(m,p),j(m.closest("li"),p.class);const v=J(p.badge);v&&m.appendChild(v),G(p.url,location.pathname)&&(m.classList.add("active"),m.setAttribute("aria-current","page"))}),de(f,u,e),a()}let k=null;const x=$("#site-footer");if(x.length){let o=function(i){return String(i??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},c=function(i){return o(i).replace(/'/g,"'")},f=function(i){if(i&&i.type==="separator")return'<li class="footer-sep"><hr></li>';if(i&&i.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(i.size)?i.size:"md"}" aria-hidden="true"></li>`;if(i&&i.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const M=c(i.url||"#"),q=o(i.text||""),g=Array.isArray(i.items)&&i.items.length?`<ul>${i.items.map(f).join("")}</ul>`:"",D=ce(i),N=[D.hover&&` data-menu-item-hover="${c(D.hover)}"`,D.active&&` data-menu-item-active="${c(D.active)}"`].filter(Boolean).join(""),O=D.accent?` style="--dm-menu-highlight:${c(D.accent)}"`:"",R=Y(i.class).join(" ");return`<li${R?` class="${c(R)}"`:""}><a href="${M}"${N}${O}>${q}</a>${g}</li>`},h=function(){if(!r.footer)return"";const i=Object.keys(s).filter(q=>t[q]);if(!i.length)return"";const M=Y(r.footer.socialClass).join(" ");return`<div class="footer-social">${i.map(q=>{const{label:g,svg:D}=s[q];return`<a href="${t[q]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link${M?` ${c(M)}`:""}">${D}</a>`}).join("")}</div>`},m=function(i){if(!i||i.type==="separator"||i.type==="spacer")return"";if(i.type==="theme-switcher")return`<li class="cms-footer-col">${f(i).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const M=Array.isArray(i.items)&&i.items.length?`<ul>${i.items.map(f).join("")}</ul>`:"",q=C(i)?`<span class="cms-footer-col-title">${o(i.text||"")}</span>`:`<a class="cms-footer-col-title" href="${c(i.url||"#")}">${o(i.text||"")}</a>`,g=["cms-footer-col",...Y(i.class)].join(" ");return`<li class="${c(g)}">${q}${M}</li>`},p=function(i,M,q=!1){return(i||[]).forEach(g=>{g&&(g.type==="separator"||g.type==="spacer"||g.type==="theme-switcher")||(q&&C(g)||M.push(g),Array.isArray(g.items)&&g.items.length&&p(g.items,M))}),M};const t=r.social||{},s={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let n='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},a=(()=>{const i=r.cookieConsent;if(!i||i.showSettingsLink===!1)return"";const M=q=>(q||[]).some(g=>g&&(g.url==="#cookie-settings"||M(g.items)));return M(e.primary)||M(e.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${o(i.settingsLinkText||"Cookie settings")}</a></li>`})(),l=["columns","row"].includes(r.footer?.layout)?r.footer.layout:"auto",y=r.footer?.look==="showcase"||l==="columns"||l==="auto"&&e.primary.some(i=>Array.isArray(i?.items)&&i.items.length),C=i=>y&&!i.url&&Array.isArray(i.items)&&i.items.length>0;if(y){const i=u.brand||{},M=i.text||r.title||"",q=i.logo?`<img src="${c(i.logo)}" class="cms-footer-brand-logo" alt="">`:i.icon?'<span class="cms-footer-brand-icon"></span>':"",g=String(r.footer?.blurb||"").trim();n='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',n+=`<div class="cms-footer-brand">${M||q?`<a class="cms-footer-brand-name" href="${c(i.url||"/")}">${q}<span>${o(M)}</span></a>`:""}${g?`<p class="cms-footer-blurb">${o(g)}</p>`:""}${h()}</div>`,e.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${e.primary.map(m).join("")}</ul></nav>`),n+='</div><div class="cms-footer-bottom">',e.copyright&&(n+=`<p class="cms-footer-copyright">${o(e.copyright)}</p>`),(e.legal.length||a)&&(n+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${e.legal.map(f).join("")}${a}</ul></nav>`),n+="</div>"}else e.copyright&&(n+=`<p>${o(e.copyright)}</p>`),e.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(f).join("")}</ul></nav>`),(e.legal.length||a)&&(n+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(f).join("")}${a}</ul></nav>`),n+=h();n+="</div>",x.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const w=x.get(0).querySelector(":scope > .cms-footer-blocks");w?.remove(),x.html(n),w&&x.get(0).prepend(w),window.CMSThemeSwitcher?.scan(x.get(0));const d=x.get(0).querySelector(".cms-footer-brand-icon");d&&u.brand?.icon&&d.setAttribute("data-icon",u.brand.icon),j(x.get(0),r.footer?.class),y&&ue(x.get(0),r);const v=[...p(e.primary,[],!0),...p(e.legal,[])];x.get(0).querySelectorAll(".footer-nav a").forEach((i,M)=>{const q=v[M];if(!q)return;q.colour&&U(i,q.colour);const g=J(q.badge);g&&i.appendChild(g),G(q.url,location.pathname)&&(i.classList.add("is-active"),i.setAttribute("aria-current","page"))}),V(x.get(0).querySelector(".footer-nav--primary"),{highlight:e.primaryHighlight}),V(x.get(0).querySelector(".footer-nav--legal"),{highlight:e.legalHighlight}),X(x.get(0).querySelector(".footer-nav--primary"),{entrance:e.primaryEntrance}),X(x.get(0).querySelector(".footer-nav--legal"),{entrance:e.legalEntrance}),j(x.get(0).querySelector(".footer-nav--primary"),e.primaryClass),j(x.get(0).querySelector(".footer-nav--legal"),e.legalClass),k=x.get(0).querySelector(".cms-footer-bottom")||x.get(0).querySelector(".footer-inner")}he(r.motion||{enabled:r.footer?.showMotionToggle!==!1,default:r.footer?.motionDefault,placement:"footer",control:r.footer?.motionControl,label:"Reduce motion"},{footer:k,navbar:document.querySelector("#site-navbar .navbar-container")?.closest("#site-navbar")||null});const I=document.getElementById("site-sidebar");I&&Ae(I),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const b=$(".page-body");if(b.length){b.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),b.find(".tabs").each(function(){Domma.elements.tabs(this)}),b.find(".carousel").each(function(){const s={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},n=h=>{const l=String(h||"").trim();return l?s[l.toLowerCase()]?`var(${s[l.toLowerCase()]})`:CSS.supports("color",l)?l:"":""},e=n(this.dataset.background),o=n(this.dataset.arrowBackground),c=n(this.dataset.arrowColor);e&&(this.style.setProperty("--dm-carousel-background",e),this.classList.add("dm-carousel-custom-background")),o&&this.style.setProperty("--dm-carousel-arrow-background",o),c&&this.style.setProperty("--dm-carousel-arrow-color",c);const f=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${f}`);const a={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const h=Number(this.dataset.height);Number.isFinite(h)&&h>0&&(a.height=h)}if(this.dataset.animationDuration){const h=parseInt(this.dataset.animationDuration,10);Number.isFinite(h)&&(a.animationDuration=h)}this.dataset.animationEasing&&(a.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(h=>h.cloneNode(!0)),this._dmCarouselOpts=a),this._dmCarousel=Domma.elements.carousel(this,a),ne(this)}),b.find(".dm-countdown").each(function(){const s={autoStart:!0},n=String(this.dataset.format||"").trim(),e=String(this.dataset.to||"").trim(),o=/^(\d{4})-(\d{2})-(\d{2})$/.exec(e),c=o?new Date(+o[1],+o[2]-1,+o[3]).getTime():e?new Date(e).getTime():NaN;Number.isFinite(c)?s.duration=Math.max(0,c-Date.now()):this.dataset.duration&&(s.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const f=/^dd:/i.test(n);s.format=f?"hh:mm:ss":n||(s.duration>=36e5?"hh:mm:ss":"mm:ss");const a=this,h=y=>String(y).padStart(2,"0"),l=y=>{const C=Math.max(0,Math.floor(y/1e3)),m=a.querySelector(".dm-timer-time");m&&(m.textContent=`${h(Math.floor(C/86400))}:${h(Math.floor(C%86400/3600))}:${h(Math.floor(C%3600/60))}:${h(C%60)}`)};f&&(s.onTick=l),Domma.elements.timer(this,s),f&&l(s.duration||0)}),b.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),b.find(".dm-progression").each(function(){const s=Array.from(this.querySelectorAll(".dm-progression-item")).map(o=>{const c={title:o.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:o.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return o.dataset.date&&(c.date=o.dataset.date),o.dataset.status&&(c.status=o.dataset.status),o.dataset.eventIcon&&(c.icon=o.dataset.eventIcon),o.dataset.entryId&&(c._entryId=o.dataset.entryId),c}),n=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",e=Domma.elements.progression(this,{layout:n,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...s.length?{items:s}:{}});if((this.dataset.mode||"timeline")==="timeline"){const o={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((c,f)=>{const a=s[f];if(!a)return;const h=c.querySelector(".dm-progression-title");if(a.icon&&h){const y=document.createElement("span");y.dataset.icon=a.icon,y.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",y.setAttribute("aria-hidden","true"),h.prepend(y)}const l=o[a.status];if(l&&h){const y=document.createElement("span");y.className=`badge badge-${l.tone} cms-timeline-status`,y.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const C=document.createElement("span");C.dataset.icon=l.icon,C.dataset.iconSize="14",C.setAttribute("aria-hidden","true"),y.append(C,document.createTextNode(l.label)),h.after(y)}})}Domma.icons.scan(this),s.length&&(this._dmCtx={instance:e,items:s})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const t=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(s=>{const n=s.dataset.revealMode||"stagger",e=s.dataset.revealAnimation||"slide-up",o=parseInt(s.dataset.revealDuration,10)||(e.startsWith("swoosh")?700:400),c=parseInt(s.dataset.revealStagger,10)||(e.startsWith("swoosh")?90:60),f=parseInt(s.dataset.revealDelay,10)||0,a=s.dataset.revealDirection==="rtl",h=s.dataset.revealReplay==="true",l=Array.from(s.children);if(t&&e.startsWith("swoosh"))return;const y={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)",swoosh:"translateY(36px) scale(0.96)"},C=p=>e!=="swoosh-sides"?y[e]||"":l.length>1&&p===0?"translateX(-64px) rotate(-1.5deg)":l.length>1&&p===l.length-1?"translateX(64px) rotate(1.5deg)":"translateY(40px) scale(0.94)",m=e.startsWith("swoosh"),w=m?"cubic-bezier(.2,.8,.2,1)":"ease",d=(p,v)=>{p.style.opacity="0",p.style.transform=C(v),m&&(p.style.filter="blur(10px)")};l.forEach((p,v)=>{d(p,v),p.style.transition=`opacity ${o}ms ${w}, transform ${o}ms ${w}`+(m?`, filter ${o}ms ${w}`:"");const A=a?l.length-1-v:v;p.style.transitionDelay=n==="stagger"?`${f+A*c}ms`:`${f}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const p=new IntersectionObserver(v=>{v.forEach(A=>{if(A.isIntersecting){if(A.target.offsetWidth,A.target.style.opacity="1",A.target.style.transform="",m&&(A.target.style.filter=""),!h){p.unobserve(A.target);const i=A.target;i.addEventListener("transitionend",function M(q){q.target!==i||q.propertyName!=="opacity"||(i.removeEventListener("transitionend",M),i.style.transition="",i.style.transitionDelay="")})}}else h&&A.boundingClientRect.top>0&&d(A.target,l.indexOf(A.target))})},{threshold:.1});l.forEach(v=>p.observe(v))}))}),me(document.querySelector(".page-body")||document),b.find(".card[data-collapsible]").each(function(){const s=this.querySelector(".card-header");s&&s.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),b.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const s=this.dataset.soTarget,n=document.getElementById(s);if(!n)return;const e=E.slideover({title:n.dataset.soTitle||"",size:n.dataset.soSize||"md",position:n.dataset.soPosition||"right"});n.style.display="",e.setContent(n),e.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(s=>{try{const n=W(s.dataset.config);Object.assign(t,n)}catch{}}),Object.keys(t).length>0){const s={};for(const[n,e]of Object.entries(t)){const o=e?.events?.click,{confirm:c,toast:f,alert:a,prompt:h,...l}=o||{};if(c||f||a||h){$(n).on("click",async function(R){if(R.preventDefault(),c&&!await E.confirm(c))return;let z=null;if(!(h&&(z=await E.prompt(h,{inputPlaceholder:l.promptPlaceholder||"",inputValue:l.promptDefault||""}),z===null))){{const B=$(l.target||this);l.toggleClass&&B.toggleClass(l.toggleClass),l.addClass&&B.addClass(l.addClass),l.removeClass&&B.removeClass(l.removeClass),z!==null&&(l.setText&&B.text(z),l.setVal&&B.val(z),l.setAttr&&B.attr(l.setAttr,z))}l.href&&(window.location.href=l.href),f&&E.toast(f,{type:l.toastType||"success"}),a&&E.alert(a)}});const{target:C,toggleClass:m,addClass:w,removeClass:d,setText:p,setVal:v,setAttr:A,href:i,toastType:M,promptPlaceholder:q,promptDefault:g,...D}=l,N={...e.events};Object.keys(D).length?N.click=C?{target:C,...D}:D:delete N.click;const O={...e,events:N};Object.keys(N).length||delete O.events,Object.keys(O).length&&(s[n]=O)}else s[n]=e}$.setup(s)}}b.length&&P(b.get(0))}),window.wireCTAButtons=P;function P(u){u.querySelectorAll(".dm-cta-trigger").forEach(r=>{r.__ctaWired||(r.__ctaWired=!0,r.addEventListener("click",async()=>{const L=r.dataset.action,k=r.dataset.entry,x=r.dataset.confirm;let I=S.get("auth_token");if(!I){E.toast("Please log in to perform this action.",{type:"warning"});return}if(x&&!await E.confirm(x))return;const b=Array.from(r.childNodes).map(s=>s.cloneNode(!0));r.disabled=!0,r.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${L}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(r.dataset.scope?{entryId:k,scope:r.dataset.scope}:{entryId:k})});try{let s=await t(I);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const o=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(o.ok){const{token:c}=await o.json();S.set("auth_token",c),I=c,s=await t(I)}}}const n=await s.json().catch(()=>({}));if(!s.ok||n.success===!1)throw new Error(n.error||n.message||`Error ${s.status}`);E.toast(n.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{r.disabled=!1,r.textContent="",b.forEach(s=>r.appendChild(s)),Domma.icons.scan(r)}}))})}(function(){const r=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!r.length)return;const L=S.get("auth_token");function k(I,b){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(b){s.appendChild(document.createTextNode("Please "));const n=document.createElement("a");n.href="/login",n.textContent="sign in",s.appendChild(n),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=I;return t.appendChild(s),t}function x(I,b){const t=document.createElement("template");t.innerHTML=b||"",I.replaceWith(t.content)}r.forEach(async I=>{const b=I.dataset.collectionAttrs;if(!b)return;if(!L){I.replaceChildren(k("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:b})});try{let s=L,n=await t(s);if(n.status===401){const o=S.get("auth_refresh_token");if(o){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(c.ok){const{token:f}=await c.json();S.set("auth_token",f),s=f,n=await t(s)}}}if(!n.ok){const{error:o}=await n.json().catch(()=>({}));I.replaceChildren(k(o||"Unable to load entries.",!1));return}const{html:e}=await n.json();x(I,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{I.replaceChildren(k("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),P(document.body)},100)})(),(function(){const r=document.querySelectorAll("[data-collection-table]");!r.length||typeof T>"u"||!T.create||r.forEach(L=>{let k;try{k=W(L.dataset.payload)}catch{return}const{columns:x,rows:I,search:b,sortable:t,exportable:s,pageSize:n,empty:e,ctaConfig:o,slug:c,exportCfg:f}=k;if(!x?.length)return;if(o){const l=x.findIndex(y=>y.key==="_cta");l!==-1&&(x[l]={key:"_cta",title:"",render:(y,C)=>{const m=document.createElement("button");if(m.className=`btn btn-${o.style||"primary"} dm-cta-trigger`,m.dataset.action=o.action||"",o.scope&&(m.dataset.scope=o.scope),m.dataset.entry=C._entryId||"",o.confirm&&(m.dataset.confirm=o.confirm),o.icon){const w=document.createElement("span");w.dataset.icon=o.icon,m.appendChild(w),m.appendChild(document.createTextNode(" "))}return m.appendChild(document.createTextNode(o.label||"Run")),m}})}const a="col-table-"+Math.random().toString(36).slice(2,7),h=document.createElement("div");h.id=a,L.replaceChildren(h),T.create("#"+a,{data:I,columns:x,search:b,sortable:t,exportable:s,pageSize:n,emptyMessage:e}),o&&P(h),L._dmTable={columns:x,rows:I,slug:c,exportCfg:f,render(l){T.create("#"+a,{data:l,columns:x,search:b,sortable:t,exportable:s,pageSize:n,emptyMessage:e}),o&&P(h)}}})})(),(function(){const r=document.querySelectorAll("[data-form-inline]");if(!r.length||typeof F>"u")return;function L(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function k(t,s){const n={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const o=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),c=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":o?"chooser":e.type,f={...e.formConfig||{}};f.span==="full"&&s&&(f.span=s);const a={type:c,required:L(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...f}};e.minLength!==void 0&&(a.minLength=e.minLength),e.maxLength!==void 0&&(a.maxLength=e.maxLength),e.min!==void 0&&(a.min=e.min),e.max!==void 0&&(a.max=e.max),o&&(e.variant&&(a.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(a.multiple=!0),e.density&&(a.density=e.density),e.columns&&(a.columns=e.columns),e.accent&&(a.accent=e.accent),e.accentStyle&&(a.accentStyle=e.accentStyle),e.glow&&(a.glow=!0),e.glowColour&&(a.glowColour=e.glowColour),e.shadow&&(a.shadow=e.shadow),e.shadowColour&&(a.shadowColour=e.shadowColour)),n[e.name]=e.type==="file"?be(e,a):a}),n}function x(t){const s={};return(t||[]).forEach(n=>{if(!(!n.name||n.type==="page-break"||n.type==="spacer")&&(n.type==="select"||n.type==="multiselect")&&n.required){const e=(n.options||[])[0];e&&(s[n.name]=typeof e=="object"?e.value:e)}}),s}function I(t,s){(s||[]).forEach(n=>{if(n.type!=="date"||!n.name)return;const e=t.querySelector(`[name="${n.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function b(t,s,n){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=n?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}r.forEach(t=>{let s;try{s=W(t.dataset.formInline)}catch{return}t.querySelector(":scope > .cms-form-embed-summary")?.remove();const n=s.fields||[],e=s.settings||{},o=Number(e.columns)===3?3:2,c=e.layout||"grid",f=n.some(w=>w.type==="page-break"),a=new Map;Ce(t,s.fileLimits,a),Le(t,n);const h=async w=>{if(t.dataset.triggerBlocked==="true"){const d=t._formLogicRuntime,p=d&&d.submitBlock?d.submitBlock().reasons:[];b(t,p.join(" ")||"This form cannot be submitted yet.",!0);const v=new Error("Blocked by a form trigger");throw v.formSubmitFailed=!0,v}try{const d=t.querySelector(".fb-form-honeypot"),p=d?.querySelector('input[type="text"]')||null,v=d?.querySelector('[name="_t"]')||null,A=Object.assign({},w);delete A._t,d?.dataset.hpName&&delete A[d.dataset.hpName],p!==null&&(A._hp=p.value),v!==null&&(A._t=v.value);const i=new Map(a);te(t,i);const M=Se(A,n,i),q=ke(t,M.files);if(q){b(t,q,!0);const D=new Error(q);throw D.formSubmitFailed=!0,D}const g=await H.post(`/api/forms/submit/${s.slug}`,M.files.size?xe(M.values,M.files).body:M.values);if(g&&g.error){b(t,g.error,!0);const D=new Error(g.error);throw D.formSubmitFailed=!0,D}if(g?.redirect){window.location.href=g.redirect;return}for(ve(t);t.firstChild;)t.removeChild(t.firstChild);b(t,g?.message||e.successMessage||"Thank you for your submission.",!1)}catch(d){throw d.formSubmitFailed||b(t,d.message||"Submission failed. Please try again.",!0),d}},l=Date.now();function y(w){const d=w.querySelector("form");if(!d||d.querySelector(".fb-form-honeypot"))return;const p=document.createElement("div");p.className="fb-form-honeypot",p.setAttribute("aria-hidden","true");const v=document.createElement("input");v.name=d.querySelector('[name="website"]')?"_website":"website",p.dataset.hpName=v.name,v.type="text",v.tabIndex=-1,v.autocomplete="url",v.placeholder="https://",p.appendChild(v);const A=document.createElement("input");A.name="_t",A.type="hidden",A.value=l,p.appendChild(A),d.appendChild(p)}function C(w){if(!window.FormLogicEngine||!n.some(p=>p.logic||Array.isArray(p.triggers)&&p.triggers.length))return null;const d=new window.FormLogicEngine.FormLogicRuntime(s,t,w?{baseValues:w}:void 0);if(d.init(),t._formLogicRuntime=d,t._formLogicObserved)return d;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const p=new MutationObserver(function(v){for(const A of v)for(const i of A.removedNodes)if(i===t||i.nodeType===1&&i.contains&&i.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),p.disconnect();return}});p.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=p}return d}let m=!1;if(t.addEventListener("dm-form-end",w=>{const d=w.detail?.validation;if(d&&!d.valid){Q(t,d.missing),Z(d);return}m||(m=!0,h(w.detail?.values||{}).catch(()=>{}).finally(()=>{m=!1}))}),f&&F.wizard){const w=fe(n,s.title),d=w.map(g=>({title:g.title,description:g.description,fields:k(g.fields,o)})),p=w.map(g=>g.fields);let v=0,A=null,i=null;const M=g=>{const D=window.FormLogicEngine;if(!D||!D.validateFields)return;const N=D.validateFields(t,p[v]||[]);N.valid||(g.preventDefault(),g.stopImmediatePropagation(),Q(t,N.missing),Z(N))},q=()=>{const g=p[v]||n;ye(t,e),ee(t,e,g),I(t,g),K(t,g),we(t,a),e.honeypot!==!1&&y(t),A&&A.destroy(),A=C(i?.getStepData?i.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(D=>D.addEventListener("click",M,!0))};i=F.wizard(t,{schema:{steps:d},layout:c,columns:o,finishText:e.submitText||"Finish",onSubmit:h,onStepChange:g=>{te(t,a),v=g,setTimeout(q,0)}}),t.addEventListener("dm-form-trigger",g=>{if(g.detail?.type!=="goto-step"||!i?.goToStep)return;const D=Number(g.detail.action.step);!Number.isFinite(D)||D<0||D>=d.length||D!==v&&i.goToStep(D)}),Promise.resolve(i).then(q)}else if(F.render){const w=F.render(t,k(n,o),x(n),{submitText:e.submitText||"Submit",layout:c,columns:o,onSubmit:h});Promise.resolve(w).then(function(){ge(t,e,c),ee(t,e,n),I(t,n),K(t,n),e.honeypot!==!1&&y(t),C()})}})})();function $e(u,r){const L=document.body,k=L.classList,x=new Set((L.dataset.railSite||"").split(" ").filter(Boolean)),I=()=>window.matchMedia("(min-width: 993px)").matches;u.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(o=>{const c=o.textContent.trim();c&&(o.setAttribute("title",c),o.querySelector("svg, [data-icon]")||(o.dataset.initial=c.charAt(0).toUpperCase()))});const b=document.createElement("button");b.type="button",b.className="dm-nav-collapse-toggle";const t=()=>{const o=k.contains("dm-vnav-collapsed");b.setAttribute("aria-pressed",String(o)),b.setAttribute("aria-label",o?"Expand menu":"Collapse menu"),b.title=b.getAttribute("aria-label");const c=r?"chevrons-left":"chevrons-right",f=r?"chevrons-right":"chevrons-left";b.innerHTML=`<span data-icon="${o?c:f}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(b)},s=o=>{k.toggle("dm-vnav-collapsed",o),S.set("nav_collapsed",o),t()};b.addEventListener("click",()=>s(!k.contains("dm-vnav-collapsed"))),t();const n=u.querySelector(".navbar-container")||u;n.insertBefore(b,n.firstChild?.nextSibling||null);const e=o=>{k.toggle("dm-vnav-autohide",o),k.toggle("dm-vnav-collapsible",!o&&x.has("collapsible")),o?k.remove("dm-vnav-collapsed"):x.has("collapsible")&&S.get("nav_collapsed")===!0&&k.add("dm-vnav-collapsed"),u.classList.remove("dm-nav-revealed"),t()};u.addEventListener("pointerdown",o=>{k.contains("dm-vnav-autohide")&&o.pointerType!=="mouse"&&!u.classList.contains("dm-nav-revealed")&&(u.classList.add("dm-nav-revealed"),o.preventDefault())}),document.addEventListener("pointerdown",o=>{u.contains(o.target)||u.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const o=k.contains("dm-vnav-autohide"),c=k.contains("dm-vnav-collapsed"),f=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:c?"Expand menu":"Collapse to icons",icon:c?"maximize":"minimize",disabled:!I()||o||!x.has("collapsible"),action:()=>s(!c)},{label:o?"Keep on screen":"Hide when not in use",icon:o?"eye":"eye-off",disabled:!I(),action:()=>{S.set("nav_autohide",!o),e(!o)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!f,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(x.has("autohide")),k.toggle("dm-vnav-collapsed",!x.has("autohide")&&x.has("start-collapsed")),t()}}]}})}function Ee(u){u.classList.add("dm-nav-scroll-hide");let r=window.scrollY,L=!1;window.addEventListener("scroll",()=>{L||(L=!0,requestAnimationFrame(()=>{L=!1;const k=window.scrollY,x=u.matches(":focus-within")||u.querySelector(".navbar-dropdown.open, .navbar-collapse.show");k>r&&k>u.offsetHeight&&!x?u.classList.add("dm-nav-scrolled-away"):k<r&&u.classList.remove("dm-nav-scrolled-away"),r=k}))},{passive:!0})}function Ae(u){let r={};try{r=JSON.parse(u.dataset.contents||"{}")}catch{}const L=u.closest(".site-main"),k=document.querySelector(".site-content"),x=new Set(String(r.exclude||"").split(",").map(a=>a.trim().toLowerCase()).filter(Boolean)),I=a=>!a.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),b=a=>(k?[...k.querySelectorAll(a)]:[]).filter(I).filter(h=>!x.has(h.textContent.trim().toLowerCase())),t=r.headings==="h2"?b("h2"):r.headings==="h3"?b("h3"):r.headings==="h2h3"?b("h2, h3"):b("h2").length?b("h2"):b("h3");if(!t.length){L?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(a=>a.id)),n=document.createElement("nav");n.className="dm-contents",n.setAttribute("aria-label",r.title||"Page contents"),r.hover&&(n.dataset.menuHover=r.hover),r.selected&&(n.dataset.menuActive=r.selected),r.accent&&n.style.setProperty("--dm-menu-highlight",r.accent.startsWith("#")?r.accent:`var(--dm-${r.accent})`),r.font&&(n.style.fontFamily=r.font),r.fontSize&&(n.style.fontSize=r.fontSize),r.fontWeight&&(n.style.fontWeight=r.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const o=document.createElement("p");o.className="dm-contents-title",o.textContent=r.title||"",e.appendChild(o),n.appendChild(e);const c=document.createElement("ol");c.className="dm-contents-list"+(r.numbered?" is-numbered":"");const f=[];for(const a of t){if(!a.id){const y=(a.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let C=y;for(let m=2;s.has(C);m++)C=`${y}-${m}`;s.add(C),a.id=C}const h=document.createElement("li");h.className=`dm-contents-item dm-contents-${a.tagName.toLowerCase()}`;const l=document.createElement("a");l.href=`#${a.id}`,l.textContent=a.textContent.trim(),h.appendChild(l),c.appendChild(h),f.push([a,l])}if(n.appendChild(c),u.replaceChildren(n),r.scrollSpy!==!1&&"IntersectionObserver"in window){const a=new Map,h=()=>{const y=f.find(([C])=>a.get(C))||null;f.forEach(([,C])=>{const m=y&&C===y[1];C.classList.toggle("is-active",!!m),m?C.setAttribute("aria-current","location"):C.removeAttribute("aria-current")})},l=new IntersectionObserver(y=>{y.forEach(C=>a.set(C.target,C.isIntersecting)),h()},{rootMargin:"-80px 0px -60% 0px"});f.forEach(([y])=>l.observe(y))}if(L?.classList.contains("contents-collapsible")){const a=document.createElement("button");a.type="button",a.className="dm-contents-toggle";const h=()=>{const l=L.classList.contains("contents-collapsed");a.setAttribute("aria-expanded",String(!l)),a.setAttribute("aria-label",l?"Show the contents":"Hide the contents"),a.title=a.getAttribute("aria-label"),a.innerHTML=`<span data-icon="${l?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(a)};a.addEventListener("click",()=>{S.set("contents_collapsed",L.classList.toggle("contents-collapsed")),h()}),h(),e.insertBefore(a,e.firstChild)}if(L?.classList.contains("contents-floating")&&r.movable){n.classList.add("is-movable");const a=S.get("contents_position");a&&Number.isFinite(a.x)&&Object.assign(u.style,{left:`${a.x}px`,top:`${a.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",h=>{if(h.target.closest("button"))return;const l=u.getBoundingClientRect(),y=h.clientX-l.left,C=h.clientY-l.top,m=d=>{const p=Math.max(0,Math.min(window.innerWidth-l.width,d.clientX-y)),v=Math.max(0,Math.min(window.innerHeight-40,d.clientY-C));Object.assign(u.style,{left:`${p}px`,top:`${v}px`,right:"auto",bottom:"auto"})},w=()=>{document.removeEventListener("pointermove",m),S.set("contents_position",{x:parseInt(u.style.left,10),y:parseInt(u.style.top,10)})};document.addEventListener("pointermove",m),document.addEventListener("pointerup",w,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const a=L?.classList.contains("contents-collapsible"),h=L?.classList.contains("contents-collapsed"),l=f.find(([,y])=>y.classList.contains("is-active"));return[{header:"Contents"},...a?[{label:h?"Show the contents":"Collapse to a button",icon:h?"list":"minimize",action:()=>u.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!l,action:async()=>{const y=`${location.origin}${location.pathname}#${l[0].id}`;try{await _.copyToClipboard(y),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),L?.classList.contains("contents-autohide")&&(u.addEventListener("pointerdown",a=>{a.pointerType!=="mouse"&&!u.classList.contains("is-revealed")&&(u.classList.add("is-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{u.contains(a.target)||u.classList.remove("is-revealed")}))}
|
|
@@ -20,6 +20,7 @@ import {fileURLToPath} from 'url';
|
|
|
20
20
|
import * as cache from '../../services/cache/index.js';
|
|
21
21
|
import {normaliseAdminPath} from '../../services/roles.js';
|
|
22
22
|
import {cssClassProblems} from '../../../public/js/css-classes.mjs';
|
|
23
|
+
import {motionFooterMirror, normaliseMotionSettings} from '../../services/motionSettings.js';
|
|
23
24
|
import {invalidateMeasurementId, measurementIdOf, normaliseMeasurementId} from '../../services/googleAnalytics.js';
|
|
24
25
|
|
|
25
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -124,7 +125,9 @@ export async function settingsRoutes(fastify) {
|
|
|
124
125
|
// Designer classes on <body>, every page (Site Settings > Appearance).
|
|
125
126
|
'bodyClass',
|
|
126
127
|
// Google Analytics (Site Settings > Analytics) - services/googleAnalytics.js.
|
|
127
|
-
'googleAnalytics'
|
|
128
|
+
'googleAnalytics',
|
|
129
|
+
// The "Reduce motion" control (Site Settings > Reduce motion) - services/motionSettings.js.
|
|
130
|
+
'motion'
|
|
128
131
|
]);
|
|
129
132
|
const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
|
|
130
133
|
if (unknownKeys.length > 0) {
|
|
@@ -141,7 +144,7 @@ export async function settingsRoutes(fastify) {
|
|
|
141
144
|
}
|
|
142
145
|
// Designer classes - the same rules as menus and forms (css-classes.mjs).
|
|
143
146
|
for (const [label, value] of [['Body CSS classes', data.bodyClass], ['Footer CSS classes', data.footer?.class],
|
|
144
|
-
['Social link CSS classes', data.footer?.socialClass]]) {
|
|
147
|
+
['Social link CSS classes', data.footer?.socialClass], ['Reduce motion CSS classes', data.motion?.class]]) {
|
|
145
148
|
const problems = cssClassProblems(value);
|
|
146
149
|
if (problems.length) return reply.status(400).send({error: `${label}: ${problems[0]}.`});
|
|
147
150
|
}
|
|
@@ -184,6 +187,24 @@ export async function settingsRoutes(fastify) {
|
|
|
184
187
|
merged[k] = v;
|
|
185
188
|
}
|
|
186
189
|
}
|
|
190
|
+
// The Reduce motion control: stored whitelisted, with the footer keys it used to live
|
|
191
|
+
// under kept in step for older readers. A client that still writes only those footer
|
|
192
|
+
// keys (fleet tooling) moves the block with them, so its change is not overruled.
|
|
193
|
+
if ('motion' in data) {
|
|
194
|
+
if (data.motion !== null && (typeof data.motion !== 'object' || Array.isArray(data.motion))) {
|
|
195
|
+
return reply.status(400).send({error: 'Reduce motion: send an object.'});
|
|
196
|
+
}
|
|
197
|
+
merged.motion = normaliseMotionSettings(merged.motion, merged.footer);
|
|
198
|
+
merged.footer = {...merged.footer, ...motionFooterMirror(merged.motion)};
|
|
199
|
+
} else if (merged.motion && data.footer && typeof data.footer === 'object') {
|
|
200
|
+
const legacy = {showMotionToggle: 'enabled', motionDefault: 'default', motionControl: 'control'};
|
|
201
|
+
const moved = Object.entries(legacy).filter(([k]) => k in data.footer);
|
|
202
|
+
if (moved.length) {
|
|
203
|
+
const next = {...merged.motion};
|
|
204
|
+
for (const [k, key] of moved) next[key] = k === 'showMotionToggle' ? data.footer[k] !== false : data.footer[k];
|
|
205
|
+
merged.motion = normaliseMotionSettings(next, merged.footer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
187
208
|
// Never erase smtp.pass with the empty string that the GET endpoint injects for safety
|
|
188
209
|
if (merged.smtp && merged.smtp.pass === '' && existing.smtp?.pass) {
|
|
189
210
|
merged.smtp = {...merged.smtp, pass: existing.smtp.pass};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Reduce motion" control - `site.json` `motion` (Site Settings > Reduce motion).
|
|
3
|
+
*
|
|
4
|
+
* One pure normaliser, shared by the renderer (both render paths: the page's
|
|
5
|
+
* bootstrap default and the object site.js draws the control from) and the
|
|
6
|
+
* settings route (what is stored). Whitelisted and clamped rather than merged,
|
|
7
|
+
* because the result is written into the source of every public page.
|
|
8
|
+
*
|
|
9
|
+
* Until it had a tab of its own the control lived in the footer settings: `footer.showMotionToggle`,
|
|
10
|
+
* `footer.motionDefault` and `footer.motionControl`. A site with no `motion`
|
|
11
|
+
* block - or one missing a key - falls back to those, key by key, so every
|
|
12
|
+
* existing site renders exactly as before with no migration. The admin keeps
|
|
13
|
+
* writing them as a mirror for older readers and fleet tooling.
|
|
14
|
+
*
|
|
15
|
+
* No I/O here; tested in tests/services/motion-settings.test.js.
|
|
16
|
+
*/
|
|
17
|
+
import {cssClasses} from '../../public/js/css-classes.mjs';
|
|
18
|
+
|
|
19
|
+
/** Today's behaviour: shown, in the footer, as a switch, following the device. */
|
|
20
|
+
export const MOTION_DEFAULTS = Object.freeze({
|
|
21
|
+
enabled: true,
|
|
22
|
+
default: 'system',
|
|
23
|
+
placement: 'footer',
|
|
24
|
+
control: 'switch',
|
|
25
|
+
triggerStyle: 'icon',
|
|
26
|
+
label: 'Reduce motion',
|
|
27
|
+
// Bottom left: clear of Search (right-centre) and Back to top (bottom right)
|
|
28
|
+
// at their defaults. public/js/motion-control.mjs stacks it clear of them
|
|
29
|
+
// anyway when a site moves one into the same corner.
|
|
30
|
+
position: 'BL',
|
|
31
|
+
offsetX: 32,
|
|
32
|
+
offsetY: 32,
|
|
33
|
+
iconSize: 52,
|
|
34
|
+
iconRadius: 26,
|
|
35
|
+
class: ''
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const MOTION_DEFAULT_CHOICES = ['system', 'off', 'on'];
|
|
39
|
+
export const MOTION_PLACEMENTS = ['footer', 'floating', 'navbar'];
|
|
40
|
+
export const MOTION_CONTROLS = ['switch', 'checkbox'];
|
|
41
|
+
export const MOTION_TRIGGER_STYLES = ['icon', 'icon-text'];
|
|
42
|
+
/** The breadcrumb corners - the Placement card's corner picker. */
|
|
43
|
+
export const MOTION_POSITIONS = ['TL', 'TR', 'BL', 'BR'];
|
|
44
|
+
export const MOTION_LABEL_MAX = 40;
|
|
45
|
+
/** The same limits as Search's floating disc (services/search.js). */
|
|
46
|
+
export const MOTION_ICON_SIZE_MIN = 32;
|
|
47
|
+
export const MOTION_ICON_SIZE_MAX = 96;
|
|
48
|
+
export const MOTION_OFFSET_MAX = 500;
|
|
49
|
+
|
|
50
|
+
const pick = (value, list, fallback) => (list.includes(value) ? value : fallback);
|
|
51
|
+
|
|
52
|
+
function num(value, fallback, min, max) {
|
|
53
|
+
const n = typeof value === 'number' ? value : (typeof value === 'string' && value.trim() ? Number(value) : NaN);
|
|
54
|
+
if (!Number.isFinite(n)) return fallback;
|
|
55
|
+
return Math.min(max, Math.max(min, Math.round(n)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The settings a page and the admin act on.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} [raw] - site.json `motion` (absent or partial is fine)
|
|
62
|
+
* @param {object} [footer] - site.json `footer`, for the keys the control used to live under
|
|
63
|
+
* @returns {object} every key of MOTION_DEFAULTS, nothing else
|
|
64
|
+
*/
|
|
65
|
+
export function normaliseMotionSettings(raw, footer) {
|
|
66
|
+
const m = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
67
|
+
const f = footer && typeof footer === 'object' ? footer : {};
|
|
68
|
+
const has = (k) => m[k] !== undefined && m[k] !== null;
|
|
69
|
+
|
|
70
|
+
const enabledRaw = has('enabled') ? m.enabled : f.showMotionToggle;
|
|
71
|
+
const defaultRaw = has('default') ? m.default : f.motionDefault;
|
|
72
|
+
const controlRaw = has('control') ? m.control : f.motionControl;
|
|
73
|
+
|
|
74
|
+
const label = typeof m.label === 'string' && m.label.trim()
|
|
75
|
+
? m.label.trim().replace(/\s+/g, ' ').slice(0, MOTION_LABEL_MAX)
|
|
76
|
+
: MOTION_DEFAULTS.label;
|
|
77
|
+
|
|
78
|
+
// The radius is clamped against the size (half of it is already a circle) - Search's rule.
|
|
79
|
+
const iconSize = num(m.iconSize, MOTION_DEFAULTS.iconSize, MOTION_ICON_SIZE_MIN, MOTION_ICON_SIZE_MAX);
|
|
80
|
+
const maxRadius = Math.floor(iconSize / 2);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
// Absent means shown, as footer.showMotionToggle always did.
|
|
84
|
+
enabled: enabledRaw !== false,
|
|
85
|
+
default: pick(defaultRaw, MOTION_DEFAULT_CHOICES, MOTION_DEFAULTS.default),
|
|
86
|
+
placement: pick(m.placement, MOTION_PLACEMENTS, MOTION_DEFAULTS.placement),
|
|
87
|
+
control: pick(controlRaw, MOTION_CONTROLS, MOTION_DEFAULTS.control),
|
|
88
|
+
triggerStyle: pick(m.triggerStyle, MOTION_TRIGGER_STYLES, MOTION_DEFAULTS.triggerStyle),
|
|
89
|
+
label,
|
|
90
|
+
position: pick(m.position, MOTION_POSITIONS, MOTION_DEFAULTS.position),
|
|
91
|
+
offsetX: num(m.offsetX, MOTION_DEFAULTS.offsetX, 0, MOTION_OFFSET_MAX),
|
|
92
|
+
offsetY: num(m.offsetY, MOTION_DEFAULTS.offsetY, 0, MOTION_OFFSET_MAX),
|
|
93
|
+
iconSize,
|
|
94
|
+
iconRadius: Math.min(maxRadius, num(m.iconRadius, MOTION_DEFAULTS.iconRadius, 0, maxRadius)),
|
|
95
|
+
class: cssClasses(typeof m.class === 'string' ? m.class : '').join(' ')
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The settings for a site.json. */
|
|
100
|
+
export function motionSettingsOf(site) {
|
|
101
|
+
return normaliseMotionSettings(site?.motion, site?.footer);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The footer keys older readers look at, kept in step with a `motion` block. */
|
|
105
|
+
export function motionFooterMirror(settings) {
|
|
106
|
+
return {showMotionToggle: settings.enabled, motionDefault: settings.default, motionControl: settings.control};
|
|
107
|
+
}
|
|
@@ -16,6 +16,7 @@ import {getProjectForPage} from './projects.js';
|
|
|
16
16
|
import {resolveContextMenusForPage} from './contextMenus.js';
|
|
17
17
|
import {buildCustomThemeStyleTag, buildOverrideStyleTag, getThemeTokenMap, listThemeIds, loadThemeConfig, resolveThemeClasses} from './themeSettings.js';
|
|
18
18
|
import {getSearchSettings} from './search.js';
|
|
19
|
+
import {motionSettingsOf} from './motionSettings.js';
|
|
19
20
|
import {isToolEnabled} from './tools.js';
|
|
20
21
|
import {consentSettingsFor, measurementIdOf} from './googleAnalytics.js';
|
|
21
22
|
import {fmtDateTime} from './dates.js';
|
|
@@ -209,10 +210,30 @@ export async function buildFooterBlocks(site, ctx = {}) {
|
|
|
209
210
|
return cols.length ? `<div class="cms-footer-blocks">${cols.join('')}</div>` : '';
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
const MOTION_ASSET_V = '20261029-motion-under-switcher';
|
|
214
|
+
|
|
212
215
|
/** The site's reduced-motion default for visitors who have not chosen: 'system', 'off' or 'on'. */
|
|
213
216
|
function motionDefaultOf(site) {
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
return motionSettingsOf(site).default;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The "Reduce motion" control (site.json `motion`, Site Settings > Reduce motion):
|
|
222
|
+
* the whitelisted settings for window.__CMS_SITE__.motion - site.js draws the
|
|
223
|
+
* control from them (public/js/motion-control.mjs) - and its stylesheet. The
|
|
224
|
+
* old footer keys still count where `motion` says nothing (motionSettings.js).
|
|
225
|
+
*
|
|
226
|
+
* Both render paths need this - the standing trap in this file.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} site - site.json
|
|
229
|
+
* @returns {{siteFields: {motion: object}, headTag: string}}
|
|
230
|
+
*/
|
|
231
|
+
export function buildMotionAssets(site) {
|
|
232
|
+
const motion = motionSettingsOf(site);
|
|
233
|
+
return {
|
|
234
|
+
siteFields: {motion},
|
|
235
|
+
headTag: motion.enabled ? `<link rel="stylesheet" href="/public/css/motion.css?v=${MOTION_ASSET_V}">` : ''
|
|
236
|
+
};
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
/** The override a page's [navbar] tag left in its html (the first one), or null. */
|
|
@@ -450,6 +471,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
450
471
|
|
|
451
472
|
const themeView = await buildThemeView(page);
|
|
452
473
|
const searchAssets = buildSearchAssets();
|
|
474
|
+
const motionAssets = buildMotionAssets(site);
|
|
453
475
|
const footerBlocksHtml = preset.footer !== false ? await buildFooterBlocks(site, {user: opts.user || null, urlPath: menuCtx.urlPath, project: menuCtx.project, tags: opts.cacheTags}) : '';
|
|
454
476
|
const switcherAssets = buildThemeSwitcherAssets(themeView.cfg, {
|
|
455
477
|
html: `${page.html || ''}${overlayMenus}${footerBlocksHtml}`,
|
|
@@ -547,6 +569,8 @@ export async function renderPage(page, opts = {}) {
|
|
|
547
569
|
footerScript: footerJson ? `window.__CMS_FOOTER__ = ${footerJson};` : '',
|
|
548
570
|
ctxMenusScript: ctxMenusScript,
|
|
549
571
|
ctxMenusModule: ctxMenusModule,
|
|
572
|
+
// Every `<` escaped, not only `</script>`: a label holding `<!--<script` would
|
|
573
|
+
// otherwise put the parser in script-data-escaped state and swallow the page.
|
|
550
574
|
siteJson: JSON.stringify(Object.assign(
|
|
551
575
|
{
|
|
552
576
|
title: site.title,
|
|
@@ -561,13 +585,14 @@ export async function renderPage(page, opts = {}) {
|
|
|
561
585
|
site.backToTop?.enabled ? { backToTop: site.backToTop } : {},
|
|
562
586
|
site.goToBottom?.enabled ? { goToBottom: site.goToBottom } : {},
|
|
563
587
|
consentAssets.siteFields,
|
|
588
|
+
motionAssets.siteFields,
|
|
564
589
|
autoTheme ? {autoTheme} : {}
|
|
565
|
-
)).replace(
|
|
590
|
+
)).replace(/</g, '\\u003c'),
|
|
566
591
|
headInject: [injection.head, navbarFontLink].filter(Boolean).join('\n'),
|
|
567
592
|
// Theme overrides land BEFORE content/custom.css: the overrides are
|
|
568
593
|
// generated, custom.css is hand-written, and the hand-written one stays
|
|
569
594
|
// the last word.
|
|
570
|
-
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
595
|
+
headInjectLate: [injection.headLate, searchAssets.headTag, motionAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
571
596
|
.filter(Boolean).join('\n'),
|
|
572
597
|
bodyEndInject: [
|
|
573
598
|
// Draft banner - first, so it is the topmost fixed element and a
|
|
@@ -1256,6 +1281,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
1256
1281
|
// other is the standing trap in this file.
|
|
1257
1282
|
const themeView = await buildThemeView({});
|
|
1258
1283
|
const searchAssets = buildSearchAssets();
|
|
1284
|
+
const motionAssets = buildMotionAssets(site);
|
|
1259
1285
|
const footerBlocksHtml = await buildFooterBlocks(site, {user, urlPath: seoMeta.urlPath || '/', project: menuCtx.project ?? null});
|
|
1260
1286
|
const switcherAssets = buildThemeSwitcherAssets(themeView.cfg, {
|
|
1261
1287
|
html: `${renderedFragment || ''}${overlayMenus}${footerBlocksHtml}`,
|
|
@@ -1340,6 +1366,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
1340
1366
|
footerScript: footerJson ? `window.__CMS_FOOTER__ = ${footerJson};` : '',
|
|
1341
1367
|
ctxMenusScript: ctxMenusScript,
|
|
1342
1368
|
ctxMenusModule: ctxMenusModule,
|
|
1369
|
+
// Every `<` escaped, as on the page path above.
|
|
1343
1370
|
siteJson: JSON.stringify({
|
|
1344
1371
|
title: site.title,
|
|
1345
1372
|
footer: site.footer
|
|
@@ -1349,13 +1376,14 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
1349
1376
|
}
|
|
1350
1377
|
: site.footer,
|
|
1351
1378
|
social: site.social || null,
|
|
1379
|
+
...motionAssets.siteFields,
|
|
1352
1380
|
...(bare ? {} : consentAssets.siteFields)
|
|
1353
|
-
}).replace(
|
|
1381
|
+
}).replace(/</g, '\\u003c'),
|
|
1354
1382
|
headInject: [bare ? '' : injection.head, navbarFontLink].filter(Boolean).join('\n'),
|
|
1355
1383
|
// Theme overrides land BEFORE content/custom.css: the overrides are
|
|
1356
1384
|
// generated, custom.css is hand-written, and the hand-written one stays
|
|
1357
1385
|
// the last word.
|
|
1358
|
-
headInjectLate: [bare ? '' : injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
1386
|
+
headInjectLate: [bare ? '' : injection.headLate, searchAssets.headTag, motionAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
1359
1387
|
.filter(Boolean).join('\n'),
|
|
1360
1388
|
bodyEndInject: [
|
|
1361
1389
|
overlayMenus,
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
_stored = JSON.parse(localStorage.getItem('domma:reduced_motion'));
|
|
113
113
|
} catch (e) {
|
|
114
114
|
}
|
|
115
|
-
// No choice of the visitor's yet: the site's default (Site Settings >
|
|
115
|
+
// No choice of the visitor's yet: the site's default (Site Settings > Reduce motion) -
|
|
116
116
|
// 'on' or 'off' decide it; 'system' (or nothing) leaves it to the device.
|
|
117
117
|
if (_stored === null || _stored === undefined) {
|
|
118
118
|
var _def = '{{motionDefault}}';
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
<script src="/public/js/form-logic-engine.js?v=20261025-dropzone-disable" defer></script>
|
|
189
189
|
|
|
190
190
|
<!-- Site initialisation -->
|
|
191
|
-
<script src="/public/js/site.js?v=20261029-
|
|
191
|
+
<script src="/public/js/site.js?v=20261029-reduce-motion" type="module"></script>
|
|
192
192
|
<script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
|
|
193
193
|
{{ctxMenusModule}}
|
|
194
194
|
|