domma-cms 1.0.0 → 1.2.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/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/app.js +1 -1
- package/admin/js/lib/context-menu-editor-arrange.js +1 -1
- package/admin/js/lib/dashboard-board-arrange.js +1 -0
- package/admin/js/lib/sidebar-frequent.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/widget-render.js +1 -0
- package/admin/js/templates/context-menu-editor.html +5 -0
- package/admin/js/templates/my-dashboard.html +3 -0
- package/admin/js/templates/settings.html +15 -0
- package/admin/js/views/context-menu-editor.js +10 -5
- package/admin/js/views/dashboard-board.js +30 -0
- package/admin/js/views/dashboard.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/my-dashboard.js +1 -0
- package/package.json +2 -2
- package/public/js/context-menus.js +1 -1
- package/public/js/site.js +1 -1
- package/server/routes/api/dashboard.js +85 -0
- package/server/routes/api/settings.js +10 -1
- package/server/routes/api/widgets.js +106 -0
- package/server/server.js +2 -0
- package/server/services/contextMenus.js +6 -0
- package/server/services/markdown.js +30 -10
- package/server/services/plugins.js +15 -0
- package/server/services/renderer.js +6 -4
- package/server/services/siteGitignore.js +2 -0
- package/server/services/widgets.js +269 -0
- package/server/templates/page.html +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";const d=window.__CMS_CTXMENUS__;if(!Array.isArray(d)||!d.length||typeof Domma>"u"||!Domma.elements||!Domma.elements.contextMenu)return;const s=(e,t)=>{typeof E<"u"&&E.toast&&E.toast(e,{type:t||"info"})},
|
|
1
|
+
(function(){"use strict";const d=window.__CMS_CTXMENUS__;if(!Array.isArray(d)||!d.length||typeof Domma>"u"||!Domma.elements||!Domma.elements.contextMenu)return;const s=(e,t)=>{typeof E<"u"&&E.toast&&E.toast(e,{type:t||"info"})},m=".site-search-trigger, .site-search-floating-trigger";function f(e){return e&&e.closest?e.closest("[data-ctx], [data-collection-table]"):null}function i(e){if(!e||!e.closest)return null;const t=e.closest('input, textarea, [contenteditable=""], [contenteditable="true"]');return!t||t.disabled||t.readOnly||t.tagName==="INPUT"&&!/^(text|search|url|tel|email|password|number)$/i.test(t.type)?null:t}function u(e){const t=i(e);if(t&&typeof t.selectionStart=="number")return t.value.slice(t.selectionStart,t.selectionEnd);const n=window.getSelection();return n?n.toString():""}function y(){try{const e=performance.getEntriesByType("navigation")[0];return!!e&&e.type==="back_forward"}catch{return!1}}function p(e,t){const n=e.selectionStart,r=e.selectionEnd;e.value=e.value.slice(0,n)+t+e.value.slice(r);const o=n+t.length;e.setSelectionRange(o,o),e.dispatchEvent(new Event("input",{bubbles:!0}))}const l={reload:{run:()=>window.location.reload()},back:{run:()=>window.history.back(),disabled:()=>window.history.length<=1},forward:{run:()=>window.history.forward(),disabled:()=>!y()},print:{run:()=>window.print()},copy:{run:async e=>{const t=u(e);if(t)try{await navigator.clipboard.writeText(t)}catch{s("Your browser would not let the page copy","error")}},disabled:e=>!u(e)},cut:{run:async e=>{const t=i(e);if(!t)return;const n=u(e);if(n)try{await navigator.clipboard.writeText(n),p(t,"")}catch{s("Your browser would not let the page cut","error")}},disabled:e=>!i(e)||!u(e)},paste:{run:async e=>{const t=i(e);if(t)try{const n=await navigator.clipboard.readText();n&&p(t,n)}catch{s("Your browser blocks paste from a menu - use Ctrl+V","warning")}},disabled:e=>!i(e)},"site-search":{run:()=>{if(window.DommaSiteSearch&&window.DommaSiteSearch.open){window.DommaSiteSearch.open();return}const e=document.querySelector(m);if(e){e.click();return}s("Site search is not available on this site","warning")},disabled:()=>!window.DommaSiteSearch&&!document.querySelector(m)},"select-all":{run:e=>{const t=i(e);if(t){t.select();return}const n=e&&e.closest&&e.closest("[data-ctx], .page-body")||document.body,r=document.createRange();r.selectNodeContents(n);const o=window.getSelection();o.removeAllRanges(),o.addRange(r)}}};function w(e){if(!e||!e.type)return null;if(l[e.type])return l[e.type].run;switch(e.type){case"navigate":return e.url?()=>{window.location.href=e.url}:null;case"copy-link":return async()=>{try{await navigator.clipboard.writeText(window.location.href),s("Link copied","success")}catch{s("Could not copy the link","error")}};case"run-action":return e.slug?async t=>{try{const n=await fetch(`/api/actions/${encodeURIComponent(e.slug)}/run`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({entryId:t&&t.dataset?t.dataset.entryId:void 0,url:window.location.pathname})});if(!n.ok)throw new Error(`HTTP ${n.status}`);s("Done","success")}catch(n){s(`Action failed: ${n.message}`,"error")}}:null;case"mark-as-spam":return async t=>{const n=t&&t.closest?t.closest("[data-entry-id]"):null,r=n&&n.dataset?n.dataset.entryId:"",o=t&&t.closest?t.closest("[data-ctx]"):null;let c="";try{o&&o.dataset.ctx&&(c=(JSON.parse(atob(o.dataset.ctx))||{}).slug||"")}catch{}if(!r||!c){s("Right-click an entry in a collection display to mark it","error");return}try{const a=await fetch(`/api/collections/${encodeURIComponent(c)}/entries/${encodeURIComponent(r)}/spam`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",body:JSON.stringify({field:e.field||void 0})});if(a.status===401||a.status===403){s("You do not have permission to do that","error");return}if(!a.ok)throw new Error(`HTTP ${a.status}`);s("Marked as spam","success"),n&&n.setAttribute("data-spam","true")}catch(a){s(`Could not mark as spam: ${a.message}`,"error")}};case"open-modal":return e.block?async()=>{try{const t=await fetch(`/api/blocks/${encodeURIComponent(e.block)}/render`);if(!t.ok)throw new Error(`HTTP ${t.status}`);const{html:n}=await t.json(),r=E.modal({title:e.title||"",closeOnBackdrop:!0}),o=document.createElement("div");o.innerHTML=typeof DOMPurify<"u"?DOMPurify.sanitize(n):n,r.element.appendChild(o),r.open()}catch(t){s(`Could not open: ${t.message}`,"error")}}:null;case"search":return t=>{const n=f(t);n&&n.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,clientX:0,clientY:0}))};case"filter":case"sort":case"export":return t=>{const n=f(t);n&&n.dispatchEvent(new CustomEvent("dm-ctx-request",{bubbles:!0,detail:{...e}}))};default:return null}}const b=new Set(["search","filter","sort","export"]);function g(e){return l[e]&&l[e].disabled?l[e].disabled:b.has(e)?t=>!f(t):null}function h(e){const t=[];for(const n of e||[]){const r=n.type||"item";if(r==="divider"){t.push({type:"divider"});continue}if(r==="header"){t.push({type:"header",header:n.text||""});continue}if(Array.isArray(n.submenu)&&n.submenu.length){const a=h(n.submenu);a.length&&t.push({label:n.text,icon:n.icon,danger:!!n.danger,className:n.class||void 0,submenu:a});continue}const o=w(n.action);if(!o)continue;const c=g(n.action&&n.action.type);t.push({label:n.text,icon:n.icon||void 0,danger:!!n.danger,className:n.class||void 0,shortcut:n.shortcut||void 0,...c?{disabled:c}:{},action:o})}return t.some(n=>n.label)?t:[]}for(const e of d){const t=h(e.items);if(!t.length)continue;const n=e.target||{};if(!n.selector)continue;const r=e.theme||{};try{Domma.elements.contextMenu(n.selector,{cursor:"glyph",match:n.match||null,exclude:n.exclude||null,inherit:e.inherit===void 0?"append":e.inherit,priority:e.priority||0,items:t,accent:r.accent,surface:r.surface,radius:r.radius,shadow:r.shadow,opacity:r.opacity,density:r.density,transition:r.transition,ariaLabel:e.name||"Context menu",className:e.class||""})}catch(o){console.warn(`[context-menus] Skipped "${e.slug}": ${o.message}`)}}})();
|
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{makeBadgeEl as O,applyColour as B,applyPill as W,applyFloat as X,applyHighlight as z,applyItemHighlight as G,itemHighlightAttrs as Y,isCurrentItem as j,menuAlign as U,applyClasses as M,cssClasses as K}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as Q}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as R,markInvalidFields as P,showValidationPopup as V}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as Z}from"/public/js/form-steps.mjs";import{applyFormClasses as J,applySubmitButtons as ee,applyWizardButtons as te}from"/public/js/form-submit.mjs";$(()=>{const x=window.__CMS_NAV__||{},y=window.__CMS_SITE__||{};if(y.autoTheme?.enabled){let t=function(e){const i=(e||"07:00").split(":");return+i[0]*60+(+i[1]||0)},s=function(){const e=new Date,i=e.getHours()*60+e.getMinutes();return i>=t(n.dayStart)&&i<t(n.nightStart)?n.dayTheme:n.nightTheme},a=function(e){const i=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(i.theme);for(const p of[...document.body.classList])p.startsWith("dm-custom-")&&document.body.classList.remove(p);i.custom&&document.body.classList.add(i.custom)};const n=y.autoTheme;a(s()),setInterval(()=>a(s()),6e4)}const _=$("#site-navbar");if(_.length&&x.brand){let s=function(c,h=0){return Array.isArray(c)?c.filter(r=>!(r&&r.type==="spacer")).filter(r=>!(r&&r.type==="separator"&&h===0)).map(r=>r&&r.type==="separator"?{divider:!0}:Array.isArray(r?.items)?{...r,items:s(r.items,h+1)}:r):c},a=function(c){return Array.isArray(c)?c.filter(h=>!(h&&(h.type==="separator"||h.type==="spacer"))):[]};const n={...x.brand},t=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let c="";n.logo?c+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(c+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(c+=`<span class="navbar-brand-text${t}">${n.text}</span>`),n.tagline&&(c+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=c}else t&&n.text&&(n.html=`<span class="navbar-brand-text${t}">${n.text}</span>`);const e=x.position==="floating",i=x.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:n,items:s(x.items||[]),variant:x.variant||"dark",position:e?"static":x.position||"sticky",collapsible:!0,appearOnHover:!!x.appearOnHover}),Domma.icons.scan("#site-navbar");const p=_.get(0);p.classList.add(`dm-nav-align-${U(x)||"centre"}`),i&&_.get(0).classList.add("dm-nav-vertical",x.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(p,x),M(p,x.class),e&&(X(_.get(0),x),Q(_.get(0).parentNode||document));const g=p.querySelector(".navbar-toggle"),u=p.querySelector(".navbar-collapse");if(g&&u){let c=null;const h=()=>{u.classList.contains("show")&&g.click()},r=()=>{c&&(c.remove(),c=null),document.body.classList.remove("dm-drawer-open")},o=()=>{c||(c=document.createElement("div"),c.className="dm-navbar-backdrop",c.addEventListener("click",h),document.body.appendChild(c),document.body.classList.add("dm-drawer-open"))};g.addEventListener("click",()=>{requestAnimationFrame(()=>{u.classList.contains("show")?o():r()})}),u.addEventListener("click",l=>{l.target.closest("a")&&requestAnimationFrame(r)}),document.addEventListener("keydown",l=>{l.key==="Escape"&&u.classList.contains("show")&&h()})}const m=p.querySelector(".navbar-nav");if(m){const c=Array.from(m.children);let h=0;for(const r of x.items||[])if(!(r&&r.type==="separator")){if(r&&r.type==="spacer"){const o=document.createElement("li"),l=["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md";o.className=`dm-menu-spacer dm-menu-spacer--${l}`,o.setAttribute("aria-hidden","true"),m.insertBefore(o,c[h]||null);continue}h++}}const b=a(x.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(c=>{const h=b[parseInt(c.dataset.index,10)];if(!h)return;const r=c.dataset.subindex!=null,o=r&&Array.isArray(h.items)?h.items[parseInt(c.dataset.subindex,10)]:h;if(!o)return;o.colour&&B(c,o.colour),o.pill&&!r&&W(c,o.pill),G(c,o),M(c.closest("li"),o.class);const l=O(o.badge);l&&c.appendChild(l),j(o.url,location.pathname)&&(c.classList.add("active"),c.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let e=function(o){return String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},i=function(o){return e(o).replace(/'/g,"'")},p=function(o){if(o&&o.type==="separator")return'<li class="footer-sep"><hr></li>';if(o&&o.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(o.size)?o.size:"md"}" aria-hidden="true"></li>`;const l=i(o.url||"#"),d=e(o.text||""),w=Array.isArray(o.items)&&o.items.length?`<ul>${o.items.map(p).join("")}</ul>`:"",v=Y(o),D=[v.hover&&` data-menu-item-hover="${i(v.hover)}"`,v.active&&` data-menu-item-active="${i(v.active)}"`].filter(Boolean).join(""),q=v.accent?` style="--dm-menu-highlight:${i(v.accent)}"`:"",k=K(o.class).join(" ");return`<li${k?` class="${i(k)}"`:""}><a href="${l}"${D}${q}>${d}</a>${w}</li>`},g=function(o,l){return(o||[]).forEach(d=>{d&&(d.type==="separator"||d.type==="spacer")||(l.push(d),Array.isArray(d.items)&&d.items.length&&g(d.items,l))}),l};const n=y.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 a=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(a.copyright&&(s+=`<p>${e(a.copyright)}</p>`),a.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${a.primary.map(p).join("")}</ul></nav>`),a.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${a.legal.map(p).join("")}</ul></nav>`),y.footer){const o=Object.keys(t).filter(l=>n[l]);o.length&&(s+='<div class="footer-social">',o.forEach(l=>{const{label:d,svg:w}=t[l];s+=`<a href="${n[l]}" target="_blank" rel="noopener noreferrer" aria-label="${d}" class="footer-social-link">${w}</a>`}),s+="</div>")}s+="</div>",C.html(s);const u=[...g(a.primary,[]),...g(a.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((o,l)=>{const d=u[l];if(!d)return;d.colour&&B(o,d.colour);const w=O(d.badge);w&&o.appendChild(w),j(d.url,location.pathname)&&(o.classList.add("is-active"),o.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:a.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:a.legalHighlight}),M(C.get(0).querySelector(".footer-nav--primary"),a.primaryClass),M(C.get(0).querySelector(".footer-nav--legal"),a.legalClass);const b=y.footer?.showMotionToggle!==!1,c=S.get("reduced_motion"),h=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),r=C.get(0).querySelector(".footer-inner");if(b&&r){const o=document.createElement("input");o.type="checkbox",o.className="form-switch-input",o.id="dm-motion-switch",o.checked=h,o.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const l=document.createElement("span");l.className="form-switch-label",l.textContent="Reduce motion";const d=document.createElement("label");d.className="form-switch footer-motion-switch",d.title="Reduce motion",d.appendChild(o),d.appendChild(l),r.appendChild(d)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const f=$(".page-body");if(f.length){f.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),f.find(".tabs").each(function(){Domma.elements.tabs(this)}),f.find(".carousel").each(function(){const n={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const t=parseInt(this.dataset.animationDuration,10);Number.isFinite(t)&&(n.animationDuration=t)}this.dataset.animationEasing&&(n.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(t=>t.cloneNode(!0)),this._dmCarouselOpts=n),this._dmCarousel=Domma.elements.carousel(this,n)}),f.find(".dm-countdown").each(function(){const n={autoStart:!0},t=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),a=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=a?new Date(+a[1],+a[2]-1,+a[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?n.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(n.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const i=/^dd:/i.test(t);n.format=i?"hh:mm:ss":t||(n.duration>=36e5?"hh:mm:ss":"mm:ss");const p=this,g=m=>String(m).padStart(2,"0"),u=m=>{const b=Math.max(0,Math.floor(m/1e3)),c=p.querySelector(".dm-timer-time");c&&(c.textContent=`${g(Math.floor(b/86400))}:${g(Math.floor(b%86400/3600))}:${g(Math.floor(b%3600/60))}:${g(b%60)}`)};i&&(n.onTick=u),Domma.elements.timer(this,n),i&&u(n.duration||0)}),f.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),f.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(a=>{const e={title:a.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:a.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return a.dataset.date&&(e.date=a.dataset.date),a.dataset.status&&(e.status=a.dataset.status),a.dataset.entryId&&(e._entryId=a.dataset.entryId),e}),t=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:t,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const t=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",a=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,i=parseInt(n.dataset.revealDelay,10)||0,p=n.dataset.revealDirection==="rtl",g=Array.from(n.children),u={"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)"};g.forEach((m,b)=>{m.style.opacity="0",m.style.transform=u[s]||"",m.style.transition=`opacity ${a}ms ease, transform ${a}ms ease`;const c=p?g.length-1-b:b;m.style.transitionDelay=t==="stagger"?`${i+c*e}ms`:`${i}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const m=new IntersectionObserver(b=>{b.forEach(c=>{c.isIntersecting&&(c.target.offsetWidth,c.target.style.opacity="1",c.target.style.transform="none",m.unobserve(c.target))})},{threshold:.1});g.forEach(b=>m.observe(b))}))}),f.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,t=document.getElementById(n);if(!t)return;const s=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",s.setContent(t),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const s=atob(t.dataset.config),a=JSON.parse(s);Object.assign(n,a)}catch{}}),Object.keys(n).length>0){const t={};for(const[s,a]of Object.entries(n)){const e=a?.events?.click,{confirm:i,toast:p,alert:g,prompt:u,...m}=e||{};i||p||g||u?($(s).on("click",async function(c){if(c.preventDefault(),i&&!await E.confirm(i))return;let h=null;if(!(u&&(h=await E.prompt(u,{inputPlaceholder:m.promptPlaceholder||"",inputValue:m.promptDefault||""}),h===null))){if(m.target){const r=$(m.target);m.toggleClass&&r.toggleClass(m.toggleClass),m.addClass&&r.addClass(m.addClass),m.removeClass&&r.removeClass(m.removeClass),h!==null&&(m.setText&&r.text(h),m.setVal&&r.val(h),m.setAttr&&r.attr(m.setAttr,h))}m.href&&(window.location.href=m.href),p&&E.toast(p,{type:m.toastType||"success"}),g&&E.alert(g)}}),Object.keys(m).length&&(t[s]={...a,events:{...a.events,click:m}})):t[s]=a}$.setup(t)}}f.length&&I(f.get(0))}),window.wireCTAButtons=I;function I(x){x.querySelectorAll(".dm-cta-trigger").forEach(y=>{y.__ctaWired||(y.__ctaWired=!0,y.addEventListener("click",async()=>{const _=y.dataset.action,C=y.dataset.entry,A=y.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(A&&!await E.confirm(A))return;const n=Array.from(y.childNodes).map(s=>s.cloneNode(!0));y.disabled=!0,y.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${_}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(y.dataset.scope?{entryId:C,scope:y.dataset.scope}:{entryId:C})});try{let s=await t(f);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const i=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(i.ok){const{token:p}=await i.json();S.set("auth_token",p),f=p,s=await t(f)}}}const a=await s.json().catch(()=>({}));if(!s.ok)throw new Error(a.error||a.message||`Error ${s.status}`);E.toast(a.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{y.disabled=!1,y.textContent="",n.forEach(s=>y.appendChild(s)),Domma.icons.scan(y)}}))})}(function(){const y=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!y.length)return;const _=S.get("auth_token");function C(f,n){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const a=document.createElement("a");a.href="/login",a.textContent="sign in",s.appendChild(a),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=f;return t.appendChild(s),t}function A(f,n){const t=document.createElement("template");t.innerHTML=n||"",f.replaceWith(t.content)}y.forEach(async f=>{const n=f.dataset.collectionAttrs;if(!n)return;if(!_){f.replaceChildren(C("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=_,a=await t(s);if(a.status===401){const i=S.get("auth_refresh_token");if(i){const p=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:i})});if(p.ok){const{token:g}=await p.json();S.set("auth_token",g),s=g,a=await t(s)}}}if(!a.ok){const{error:i}=await a.json().catch(()=>({}));f.replaceChildren(C(i||"Unable to load entries.",!1));return}const{html:e}=await a.json();A(f,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{f.replaceChildren(C("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),I(document.body)},100)})(),(function(){const y=document.querySelectorAll("[data-collection-table]");!y.length||typeof T>"u"||!T.create||y.forEach(_=>{let C;try{C=JSON.parse(atob(_.dataset.payload))}catch{return}const{columns:A,rows:f,search:n,sortable:t,exportable:s,pageSize:a,empty:e,ctaConfig:i,slug:p,exportCfg:g}=C;if(!A?.length)return;if(i){const b=A.findIndex(c=>c.key==="_cta");b!==-1&&(A[b]={key:"_cta",title:"",render:(c,h)=>{const r=document.createElement("button");if(r.className=`btn btn-${i.style||"primary"} dm-cta-trigger`,r.dataset.action=i.action||"",i.scope&&(r.dataset.scope=i.scope),r.dataset.entry=h._entryId||"",i.confirm&&(r.dataset.confirm=i.confirm),i.icon){const o=document.createElement("span");o.dataset.icon=i.icon,r.appendChild(o),r.appendChild(document.createTextNode(" "))}return r.appendChild(document.createTextNode(i.label||"Run")),r}})}const u="col-table-"+Math.random().toString(36).slice(2,7),m=document.createElement("div");m.id=u,_.replaceChildren(m),T.create("#"+u,{data:f,columns:A,search:n,sortable:t,exportable:s,pageSize:a,emptyMessage:e}),i&&I(m),_._dmTable={columns:A,rows:f,slug:p,exportCfg:g,render(b){T.create("#"+u,{data:b,columns:A,search:n,sortable:t,exportable:s,pageSize:a,emptyMessage:e}),i&&I(m)}}})})(),(function(){const y=document.querySelectorAll("[data-form-inline]");if(!y.length||typeof F>"u")return;function _(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function C(t,s){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const i=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),p=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":i?"chooser":e.type,g={...e.formConfig||{}};g.span==="full"&&s&&(g.span=s);const u={type:p,required:_(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},...g}};e.minLength!==void 0&&(u.minLength=e.minLength),e.maxLength!==void 0&&(u.maxLength=e.maxLength),e.min!==void 0&&(u.min=e.min),e.max!==void 0&&(u.max=e.max),i&&(e.variant&&(u.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(u.multiple=!0),e.density&&(u.density=e.density),e.columns&&(u.columns=e.columns),e.accent&&(u.accent=e.accent),e.accentStyle&&(u.accentStyle=e.accentStyle),e.glow&&(u.glow=!0),e.glowColour&&(u.glowColour=e.glowColour),e.shadow&&(u.shadow=e.shadow),e.shadowColour&&(u.shadowColour=e.shadowColour)),a[e.name]=u}),a}function A(t){const s={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(s[a.name]=typeof e=="object"?e.value:e)}}),s}function f(t,s){(s||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(t,s,a){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=a?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}y.forEach(t=>{let s;try{s=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=s.fields||[],e=s.settings||{},i=Number(e.columns)===3?3:2,p=e.layout||"grid",g=a.some(r=>r.type==="page-break"),u=async r=>{if(t.dataset.triggerBlocked==="true"){const o=t._formLogicRuntime,l=o&&o.submitBlock?o.submitBlock().reasons:[];n(t,l.join(" ")||"This form cannot be submitted yet.",!0);const d=new Error("Blocked by a form trigger");throw d.formSubmitFailed=!0,d}try{const o=t.querySelector(".fb-form-honeypot"),l=o?.querySelector('input[type="text"]')||null,d=o?.querySelector('[name="_t"]')||null,w=Object.assign({},r);delete w._t,o?.dataset.hpName&&delete w[o.dataset.hpName],l!==null&&(w._hp=l.value),d!==null&&(w._t=d.value);const v=await H.post(`/api/forms/submit/${s.slug}`,w);if(v&&v.error){n(t,v.error,!0);const D=new Error(v.error);throw D.formSubmitFailed=!0,D}if(v?.redirect){window.location.href=v.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);n(t,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(o){throw o.formSubmitFailed||n(t,o.message||"Submission failed. Please try again.",!0),o}},m=Date.now();function b(r){const o=r.querySelector("form");if(!o||o.querySelector(".fb-form-honeypot"))return;const l=document.createElement("div");l.className="fb-form-honeypot",l.setAttribute("aria-hidden","true");const d=document.createElement("input");d.name=o.querySelector('[name="website"]')?"_website":"website",l.dataset.hpName=d.name,d.type="text",d.tabIndex=-1,d.autocomplete="url",d.placeholder="https://",l.appendChild(d);const w=document.createElement("input");w.name="_t",w.type="hidden",w.value=m,l.appendChild(w),o.appendChild(l)}function c(r){if(!window.FormLogicEngine||!a.some(l=>l.logic||Array.isArray(l.triggers)&&l.triggers.length))return null;const o=new window.FormLogicEngine.FormLogicRuntime(s,t,r?{baseValues:r}:void 0);if(o.init(),t._formLogicRuntime=o,t._formLogicObserved)return o;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const l=new MutationObserver(function(d){for(const w of d)for(const v of w.removedNodes)if(v===t||v.nodeType===1&&v.contains&&v.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),l.disconnect();return}});l.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=l}return o}let h=!1;if(t.addEventListener("dm-form-end",r=>{const o=r.detail?.validation;if(o&&!o.valid){P(t,o.missing),V(o);return}h||(h=!0,u(r.detail?.values||{}).catch(()=>{}).finally(()=>{h=!1}))}),g&&F.wizard){const r=Z(a,s.title),o=r.map(k=>({title:k.title,description:k.description,fields:C(k.fields,i)})),l=r.map(k=>k.fields);let d=0,w=null,v=null;const D=k=>{const L=window.FormLogicEngine;if(!L||!L.validateFields)return;const N=L.validateFields(t,l[d]||[]);N.valid||(k.preventDefault(),k.stopImmediatePropagation(),P(t,N.missing),V(N))},q=()=>{const k=l[d]||a;te(t,e),J(t,e,k),f(t,k),R(t,k),e.honeypot!==!1&&b(t),w&&w.destroy(),w=c(v?.getStepData?v.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(L=>L.addEventListener("click",D,!0))};v=F.wizard(t,{schema:{steps:o},layout:p,columns:i,finishText:e.submitText||"Finish",onSubmit:u,onStepChange:k=>{d=k,setTimeout(q,0)}}),t.addEventListener("dm-form-trigger",k=>{if(k.detail?.type!=="goto-step"||!v?.goToStep)return;const L=Number(k.detail.action.step);!Number.isFinite(L)||L<0||L>=o.length||L!==d&&v.goToStep(L)}),Promise.resolve(v).then(q)}else if(F.render){const r=F.render(t,C(a,i),A(a),{submitText:e.submitText||"Submit",layout:p,columns:i,onSubmit:u});Promise.resolve(r).then(function(){ee(t,e,p),J(t,e,a),f(t,a),R(t,a),e.honeypot!==!1&&b(t),c()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
1
|
+
import{makeBadgeEl as O,applyColour as B,applyPill as X,applyFloat as G,applyHighlight as z,applyItemHighlight as Y,itemHighlightAttrs as U,isCurrentItem as j,menuAlign as K,applyClasses as I,cssClasses as R}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as Q}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as P,markInvalidFields as V,showValidationPopup as J}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as Z}from"/public/js/form-steps.mjs";import{applyFormClasses as W,applySubmitButtons as ee,applyWizardButtons as te}from"/public/js/form-submit.mjs";$(()=>{const x=window.__CMS_NAV__||{},f=window.__CMS_SITE__||{};if(f.autoTheme?.enabled){let t=function(e){const i=(e||"07:00").split(":");return+i[0]*60+(+i[1]||0)},s=function(){const e=new Date,i=e.getHours()*60+e.getMinutes();return i>=t(n.dayStart)&&i<t(n.nightStart)?n.dayTheme:n.nightTheme},a=function(e){const i=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(i.theme);for(const p of[...document.body.classList])p.startsWith("dm-custom-")&&document.body.classList.remove(p);i.custom&&document.body.classList.add(i.custom)};const n=f.autoTheme;a(s()),setInterval(()=>a(s()),6e4)}const _=$("#site-navbar");if(_.length&&x.brand){let s=function(c,g=0){return Array.isArray(c)?c.filter(r=>!(r&&r.type==="spacer")).filter(r=>!(r&&r.type==="separator"&&g===0)).map(r=>r&&r.type==="separator"?{divider:!0}:Array.isArray(r?.items)?{...r,items:s(r.items,g+1)}:r):c},a=function(c){return Array.isArray(c)?c.filter(g=>!(g&&(g.type==="separator"||g.type==="spacer"))):[]};const n={...x.brand},t=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let c="";n.logo?c+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(c+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(c+=`<span class="navbar-brand-text${t}">${n.text}</span>`),n.tagline&&(c+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=c}else t&&n.text&&(n.html=`<span class="navbar-brand-text${t}">${n.text}</span>`);const e=x.position==="floating",i=x.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:n,items:s(x.items||[]),variant:x.variant||"dark",position:e?"static":x.position||"sticky",collapsible:!0,appearOnHover:!!x.appearOnHover}),Domma.icons.scan("#site-navbar");const p=_.get(0);p.classList.add(`dm-nav-align-${K(x)||"centre"}`),i&&_.get(0).classList.add("dm-nav-vertical",x.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(p,x),I(p,x.class),e&&(G(_.get(0),x),Q(_.get(0).parentNode||document));const y=p.querySelector(".navbar-toggle"),u=p.querySelector(".navbar-collapse");if(y&&u){let c=null;const g=()=>{u.classList.contains("show")&&y.click()},r=()=>{c&&(c.remove(),c=null),document.body.classList.remove("dm-drawer-open")},o=()=>{c||(c=document.createElement("div"),c.className="dm-navbar-backdrop",c.addEventListener("click",g),document.body.appendChild(c),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{u.classList.contains("show")?o():r()})}),u.addEventListener("click",d=>{d.target.closest("a")&&requestAnimationFrame(r)}),document.addEventListener("keydown",d=>{d.key==="Escape"&&u.classList.contains("show")&&g()})}const m=p.querySelector(".navbar-nav");if(m){const c=Array.from(m.children);let g=0;for(const r of x.items||[])if(!(r&&r.type==="separator")){if(r&&r.type==="spacer"){const o=document.createElement("li"),d=["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md";o.className=`dm-menu-spacer dm-menu-spacer--${d}`,o.setAttribute("aria-hidden","true"),m.insertBefore(o,c[g]||null);continue}g++}}const b=a(x.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(c=>{const g=b[parseInt(c.dataset.index,10)];if(!g)return;const r=c.dataset.subindex!=null,o=r&&Array.isArray(g.items)?g.items[parseInt(c.dataset.subindex,10)]:g;if(!o)return;o.colour&&B(c,o.colour),o.pill&&!r&&X(c,o.pill),Y(c,o),I(c.closest("li"),o.class);const d=O(o.badge);d&&c.appendChild(d),j(o.url,location.pathname)&&(c.classList.add("active"),c.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let e=function(o){return String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},i=function(o){return e(o).replace(/'/g,"'")},p=function(o){if(o&&o.type==="separator")return'<li class="footer-sep"><hr></li>';if(o&&o.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(o.size)?o.size:"md"}" aria-hidden="true"></li>`;const d=i(o.url||"#"),l=e(o.text||""),w=Array.isArray(o.items)&&o.items.length?`<ul>${o.items.map(p).join("")}</ul>`:"",v=U(o),D=[v.hover&&` data-menu-item-hover="${i(v.hover)}"`,v.active&&` data-menu-item-active="${i(v.active)}"`].filter(Boolean).join(""),M=v.accent?` style="--dm-menu-highlight:${i(v.accent)}"`:"",k=R(o.class).join(" ");return`<li${k?` class="${i(k)}"`:""}><a href="${d}"${D}${M}>${l}</a>${w}</li>`},y=function(o,d){return(o||[]).forEach(l=>{l&&(l.type==="separator"||l.type==="spacer")||(d.push(l),Array.isArray(l.items)&&l.items.length&&y(l.items,d))}),d};const n=f.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 a=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(a.copyright&&(s+=`<p>${e(a.copyright)}</p>`),a.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${a.primary.map(p).join("")}</ul></nav>`),a.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${a.legal.map(p).join("")}</ul></nav>`),f.footer){const o=Object.keys(t).filter(l=>n[l]),d=R(f.footer.socialClass).join(" ");o.length&&(s+='<div class="footer-social">',o.forEach(l=>{const{label:w,svg:v}=t[l];s+=`<a href="${n[l]}" target="_blank" rel="noopener noreferrer" aria-label="${w}" class="footer-social-link${d?` ${i(d)}`:""}">${v}</a>`}),s+="</div>")}s+="</div>",C.html(s),I(C.get(0),f.footer?.class);const u=[...y(a.primary,[]),...y(a.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((o,d)=>{const l=u[d];if(!l)return;l.colour&&B(o,l.colour);const w=O(l.badge);w&&o.appendChild(w),j(l.url,location.pathname)&&(o.classList.add("is-active"),o.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:a.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:a.legalHighlight}),I(C.get(0).querySelector(".footer-nav--primary"),a.primaryClass),I(C.get(0).querySelector(".footer-nav--legal"),a.legalClass);const b=f.footer?.showMotionToggle!==!1,c=S.get("reduced_motion"),g=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),r=C.get(0).querySelector(".footer-inner");if(b&&r){const o=document.createElement("input");o.type="checkbox",o.className="form-switch-input",o.id="dm-motion-switch",o.checked=g,o.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const d=document.createElement("span");d.className="form-switch-label",d.textContent="Reduce motion";const l=document.createElement("label");l.className="form-switch footer-motion-switch",l.title="Reduce motion",l.appendChild(o),l.appendChild(d),r.appendChild(l)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const h=$(".page-body");if(h.length){h.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),h.find(".tabs").each(function(){Domma.elements.tabs(this)}),h.find(".carousel").each(function(){const n={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const t=parseInt(this.dataset.animationDuration,10);Number.isFinite(t)&&(n.animationDuration=t)}this.dataset.animationEasing&&(n.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(t=>t.cloneNode(!0)),this._dmCarouselOpts=n),this._dmCarousel=Domma.elements.carousel(this,n)}),h.find(".dm-countdown").each(function(){const n={autoStart:!0},t=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),a=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=a?new Date(+a[1],+a[2]-1,+a[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?n.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(n.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const i=/^dd:/i.test(t);n.format=i?"hh:mm:ss":t||(n.duration>=36e5?"hh:mm:ss":"mm:ss");const p=this,y=m=>String(m).padStart(2,"0"),u=m=>{const b=Math.max(0,Math.floor(m/1e3)),c=p.querySelector(".dm-timer-time");c&&(c.textContent=`${y(Math.floor(b/86400))}:${y(Math.floor(b%86400/3600))}:${y(Math.floor(b%3600/60))}:${y(b%60)}`)};i&&(n.onTick=u),Domma.elements.timer(this,n),i&&u(n.duration||0)}),h.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),h.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(a=>{const e={title:a.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:a.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return a.dataset.date&&(e.date=a.dataset.date),a.dataset.status&&(e.status=a.dataset.status),a.dataset.entryId&&(e._entryId=a.dataset.entryId),e}),t=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:t,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const t=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",a=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,i=parseInt(n.dataset.revealDelay,10)||0,p=n.dataset.revealDirection==="rtl",y=Array.from(n.children),u={"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)"};y.forEach((m,b)=>{m.style.opacity="0",m.style.transform=u[s]||"",m.style.transition=`opacity ${a}ms ease, transform ${a}ms ease`;const c=p?y.length-1-b:b;m.style.transitionDelay=t==="stagger"?`${i+c*e}ms`:`${i}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const m=new IntersectionObserver(b=>{b.forEach(c=>{c.isIntersecting&&(c.target.offsetWidth,c.target.style.opacity="1",c.target.style.transform="none",m.unobserve(c.target))})},{threshold:.1});y.forEach(b=>m.observe(b))}))}),h.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),h.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,t=document.getElementById(n);if(!t)return;const s=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",s.setContent(t),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const s=atob(t.dataset.config),a=JSON.parse(s);Object.assign(n,a)}catch{}}),Object.keys(n).length>0){const t={};for(const[s,a]of Object.entries(n)){const e=a?.events?.click,{confirm:i,toast:p,alert:y,prompt:u,...m}=e||{};i||p||y||u?($(s).on("click",async function(c){if(c.preventDefault(),i&&!await E.confirm(i))return;let g=null;if(!(u&&(g=await E.prompt(u,{inputPlaceholder:m.promptPlaceholder||"",inputValue:m.promptDefault||""}),g===null))){if(m.target){const r=$(m.target);m.toggleClass&&r.toggleClass(m.toggleClass),m.addClass&&r.addClass(m.addClass),m.removeClass&&r.removeClass(m.removeClass),g!==null&&(m.setText&&r.text(g),m.setVal&&r.val(g),m.setAttr&&r.attr(m.setAttr,g))}m.href&&(window.location.href=m.href),p&&E.toast(p,{type:m.toastType||"success"}),y&&E.alert(y)}}),Object.keys(m).length&&(t[s]={...a,events:{...a.events,click:m}})):t[s]=a}$.setup(t)}}h.length&&q(h.get(0))}),window.wireCTAButtons=q;function q(x){x.querySelectorAll(".dm-cta-trigger").forEach(f=>{f.__ctaWired||(f.__ctaWired=!0,f.addEventListener("click",async()=>{const _=f.dataset.action,C=f.dataset.entry,A=f.dataset.confirm;let h=S.get("auth_token");if(!h){E.toast("Please log in to perform this action.",{type:"warning"});return}if(A&&!await E.confirm(A))return;const n=Array.from(f.childNodes).map(s=>s.cloneNode(!0));f.disabled=!0,f.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${_}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(f.dataset.scope?{entryId:C,scope:f.dataset.scope}:{entryId:C})});try{let s=await t(h);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const i=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(i.ok){const{token:p}=await i.json();S.set("auth_token",p),h=p,s=await t(h)}}}const a=await s.json().catch(()=>({}));if(!s.ok)throw new Error(a.error||a.message||`Error ${s.status}`);E.toast(a.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{f.disabled=!1,f.textContent="",n.forEach(s=>f.appendChild(s)),Domma.icons.scan(f)}}))})}(function(){const f=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!f.length)return;const _=S.get("auth_token");function C(h,n){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const a=document.createElement("a");a.href="/login",a.textContent="sign in",s.appendChild(a),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=h;return t.appendChild(s),t}function A(h,n){const t=document.createElement("template");t.innerHTML=n||"",h.replaceWith(t.content)}f.forEach(async h=>{const n=h.dataset.collectionAttrs;if(!n)return;if(!_){h.replaceChildren(C("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=_,a=await t(s);if(a.status===401){const i=S.get("auth_refresh_token");if(i){const p=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:i})});if(p.ok){const{token:y}=await p.json();S.set("auth_token",y),s=y,a=await t(s)}}}if(!a.ok){const{error:i}=await a.json().catch(()=>({}));h.replaceChildren(C(i||"Unable to load entries.",!1));return}const{html:e}=await a.json();A(h,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{h.replaceChildren(C("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),q(document.body)},100)})(),(function(){const f=document.querySelectorAll("[data-collection-table]");!f.length||typeof T>"u"||!T.create||f.forEach(_=>{let C;try{C=JSON.parse(atob(_.dataset.payload))}catch{return}const{columns:A,rows:h,search:n,sortable:t,exportable:s,pageSize:a,empty:e,ctaConfig:i,slug:p,exportCfg:y}=C;if(!A?.length)return;if(i){const b=A.findIndex(c=>c.key==="_cta");b!==-1&&(A[b]={key:"_cta",title:"",render:(c,g)=>{const r=document.createElement("button");if(r.className=`btn btn-${i.style||"primary"} dm-cta-trigger`,r.dataset.action=i.action||"",i.scope&&(r.dataset.scope=i.scope),r.dataset.entry=g._entryId||"",i.confirm&&(r.dataset.confirm=i.confirm),i.icon){const o=document.createElement("span");o.dataset.icon=i.icon,r.appendChild(o),r.appendChild(document.createTextNode(" "))}return r.appendChild(document.createTextNode(i.label||"Run")),r}})}const u="col-table-"+Math.random().toString(36).slice(2,7),m=document.createElement("div");m.id=u,_.replaceChildren(m),T.create("#"+u,{data:h,columns:A,search:n,sortable:t,exportable:s,pageSize:a,emptyMessage:e}),i&&q(m),_._dmTable={columns:A,rows:h,slug:p,exportCfg:y,render(b){T.create("#"+u,{data:b,columns:A,search:n,sortable:t,exportable:s,pageSize:a,emptyMessage:e}),i&&q(m)}}})})(),(function(){const f=document.querySelectorAll("[data-form-inline]");if(!f.length||typeof F>"u")return;function _(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function C(t,s){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const i=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),p=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":i?"chooser":e.type,y={...e.formConfig||{}};y.span==="full"&&s&&(y.span=s);const u={type:p,required:_(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},...y}};e.minLength!==void 0&&(u.minLength=e.minLength),e.maxLength!==void 0&&(u.maxLength=e.maxLength),e.min!==void 0&&(u.min=e.min),e.max!==void 0&&(u.max=e.max),i&&(e.variant&&(u.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(u.multiple=!0),e.density&&(u.density=e.density),e.columns&&(u.columns=e.columns),e.accent&&(u.accent=e.accent),e.accentStyle&&(u.accentStyle=e.accentStyle),e.glow&&(u.glow=!0),e.glowColour&&(u.glowColour=e.glowColour),e.shadow&&(u.shadow=e.shadow),e.shadowColour&&(u.shadowColour=e.shadowColour)),a[e.name]=u}),a}function A(t){const s={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(s[a.name]=typeof e=="object"?e.value:e)}}),s}function h(t,s){(s||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(t,s,a){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=a?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}f.forEach(t=>{let s;try{s=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=s.fields||[],e=s.settings||{},i=Number(e.columns)===3?3:2,p=e.layout||"grid",y=a.some(r=>r.type==="page-break"),u=async r=>{if(t.dataset.triggerBlocked==="true"){const o=t._formLogicRuntime,d=o&&o.submitBlock?o.submitBlock().reasons:[];n(t,d.join(" ")||"This form cannot be submitted yet.",!0);const l=new Error("Blocked by a form trigger");throw l.formSubmitFailed=!0,l}try{const o=t.querySelector(".fb-form-honeypot"),d=o?.querySelector('input[type="text"]')||null,l=o?.querySelector('[name="_t"]')||null,w=Object.assign({},r);delete w._t,o?.dataset.hpName&&delete w[o.dataset.hpName],d!==null&&(w._hp=d.value),l!==null&&(w._t=l.value);const v=await H.post(`/api/forms/submit/${s.slug}`,w);if(v&&v.error){n(t,v.error,!0);const D=new Error(v.error);throw D.formSubmitFailed=!0,D}if(v?.redirect){window.location.href=v.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);n(t,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(o){throw o.formSubmitFailed||n(t,o.message||"Submission failed. Please try again.",!0),o}},m=Date.now();function b(r){const o=r.querySelector("form");if(!o||o.querySelector(".fb-form-honeypot"))return;const d=document.createElement("div");d.className="fb-form-honeypot",d.setAttribute("aria-hidden","true");const l=document.createElement("input");l.name=o.querySelector('[name="website"]')?"_website":"website",d.dataset.hpName=l.name,l.type="text",l.tabIndex=-1,l.autocomplete="url",l.placeholder="https://",d.appendChild(l);const w=document.createElement("input");w.name="_t",w.type="hidden",w.value=m,d.appendChild(w),o.appendChild(d)}function c(r){if(!window.FormLogicEngine||!a.some(d=>d.logic||Array.isArray(d.triggers)&&d.triggers.length))return null;const o=new window.FormLogicEngine.FormLogicRuntime(s,t,r?{baseValues:r}:void 0);if(o.init(),t._formLogicRuntime=o,t._formLogicObserved)return o;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const d=new MutationObserver(function(l){for(const w of l)for(const v of w.removedNodes)if(v===t||v.nodeType===1&&v.contains&&v.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),d.disconnect();return}});d.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=d}return o}let g=!1;if(t.addEventListener("dm-form-end",r=>{const o=r.detail?.validation;if(o&&!o.valid){V(t,o.missing),J(o);return}g||(g=!0,u(r.detail?.values||{}).catch(()=>{}).finally(()=>{g=!1}))}),y&&F.wizard){const r=Z(a,s.title),o=r.map(k=>({title:k.title,description:k.description,fields:C(k.fields,i)})),d=r.map(k=>k.fields);let l=0,w=null,v=null;const D=k=>{const L=window.FormLogicEngine;if(!L||!L.validateFields)return;const N=L.validateFields(t,d[l]||[]);N.valid||(k.preventDefault(),k.stopImmediatePropagation(),V(t,N.missing),J(N))},M=()=>{const k=d[l]||a;te(t,e),W(t,e,k),h(t,k),P(t,k),e.honeypot!==!1&&b(t),w&&w.destroy(),w=c(v?.getStepData?v.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(L=>L.addEventListener("click",D,!0))};v=F.wizard(t,{schema:{steps:o},layout:p,columns:i,finishText:e.submitText||"Finish",onSubmit:u,onStepChange:k=>{l=k,setTimeout(M,0)}}),t.addEventListener("dm-form-trigger",k=>{if(k.detail?.type!=="goto-step"||!v?.goToStep)return;const L=Number(k.detail.action.step);!Number.isFinite(L)||L<0||L>=o.length||L!==l&&v.goToStep(L)}),Promise.resolve(v).then(M)}else if(F.render){const r=F.render(t,C(a,i),A(a),{submitText:e.submitText||"Submit",layout:p,columns:i,onSubmit:u});Promise.resolve(r).then(function(){ee(t,e,p),W(t,e,a),h(t,a),P(t,a),e.honeypot!==!1&&b(t),c()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
@@ -12,6 +12,12 @@ import {authenticate as defaultAuthenticate, requireAdmin as defaultRequireAdmin
|
|
|
12
12
|
import {getHealth} from '../../services/health.js';
|
|
13
13
|
import {listVersions} from '../../services/versions.js';
|
|
14
14
|
import {PRESET_COLLECTION_SLUGS} from '../../services/presetCollections.js';
|
|
15
|
+
import {canAny as widgetCanAny, isDashboardPro} from '../../services/widgets.js';
|
|
16
|
+
import {getPermissionsForRoles} from '../../services/roles.js';
|
|
17
|
+
import {getEffectiveLevel, getEffectiveRoles} from '../../services/userRoles.js';
|
|
18
|
+
import {getEffectiveRegistry} from '../../services/permissionRegistry.js';
|
|
19
|
+
// The fixed Dashboard's own rules, so a widget says exactly what its card says.
|
|
20
|
+
import {BADGE_SOURCES, activityRows, attentionFrom, chartSeries} from '../../../admin/js/lib/dashboard-arrange.js';
|
|
15
21
|
|
|
16
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
23
|
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
@@ -290,6 +296,14 @@ export async function dashboardRoutes(fastify, opts = {}) {
|
|
|
290
296
|
|
|
291
297
|
fastify.get('/dashboard/summary', guard, async (request) => {
|
|
292
298
|
const lite = request.query.lite === '1' || request.query.lite === 'true';
|
|
299
|
+
return buildSummary(lite);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The summary the fixed Dashboard reads - and, piece by piece, core's own
|
|
304
|
+
* widgets on the Dashboard Pro board (GET /dashboard/widget/:name below).
|
|
305
|
+
*/
|
|
306
|
+
async function buildSummary(lite) {
|
|
293
307
|
const warnings = [];
|
|
294
308
|
const safe = async (label, fn) => {
|
|
295
309
|
try { return await fn(); }
|
|
@@ -374,5 +388,76 @@ export async function dashboardRoutes(fastify, opts = {}) {
|
|
|
374
388
|
const activity = (await safe('activity', () => buildActivity())) || [];
|
|
375
389
|
|
|
376
390
|
return { traffic, topPages, journeys, spikes, realtime, health, activity, warnings };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Core's own widgets (services/widgets.js CORE_WIDGETS), each in its kind's
|
|
395
|
+
* shape. Dashboard Pro only - a site without it answers 404, as for every
|
|
396
|
+
* widget route. "Needs attention" asks the same sidebar badges the fixed
|
|
397
|
+
* Dashboard does, as the signed-in user, so nobody sees a Tool they cannot open.
|
|
398
|
+
*/
|
|
399
|
+
fastify.get('/dashboard/widget/:name', {preHandler: [authenticate]}, async (request, reply) => {
|
|
400
|
+
if (!isDashboardPro()) return reply.status(404).send({error: 'Not found'});
|
|
401
|
+
const name = String(request.params.name || '');
|
|
402
|
+
const admin = getEffectiveLevel(request.user) <= 1;
|
|
403
|
+
if (name !== 'attention' && !admin) return reply.status(403).send({error: 'Administrators only'});
|
|
404
|
+
const fmtN = (n) => Number(n || 0).toLocaleString('en-GB');
|
|
405
|
+
|
|
406
|
+
if (name === 'attention') {
|
|
407
|
+
const permissions = getPermissionsForRoles(getEffectiveRoles(request.user), getEffectiveRegistry().map(r => r.key));
|
|
408
|
+
const want = BADGE_SOURCES.filter(src => widgetCanAny(permissions, src.permission));
|
|
409
|
+
const got = await Promise.all(want.map(src => fastify.inject({
|
|
410
|
+
method: 'GET', url: `/api${src.countUrl}`,
|
|
411
|
+
headers: {authorization: request.headers.authorization || ''}
|
|
412
|
+
}).then(r => (r.statusCode === 200 ? r.json() : null)).catch(() => null)));
|
|
413
|
+
const badges = Object.fromEntries(want.map((src, i) => [src.key, got[i]]).filter(([, b]) => b));
|
|
414
|
+
const checks = admin ? ((await getHealth().catch(() => null))?.checks || []) : [];
|
|
415
|
+
const rows = attentionFrom(badges, checks);
|
|
416
|
+
return {
|
|
417
|
+
items: rows.map(r => ({text: `${r.title}: ${r.text}`, meta: r.meta, href: r.href, tone: r.tone})),
|
|
418
|
+
empty: 'Nothing needs you right now.'
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const s = await buildSummary(false);
|
|
423
|
+
if (name === 'traffic') {
|
|
424
|
+
const c = chartSeries(s.traffic?.series);
|
|
425
|
+
return {labels: c.labels, values: c.values, label: 'Page views'};
|
|
426
|
+
}
|
|
427
|
+
if (name === 'visitors') {
|
|
428
|
+
const delta = s.traffic?.deltaPct;
|
|
429
|
+
return {
|
|
430
|
+
value: fmtN(s.realtime?.activeSessions), label: 'on the site now',
|
|
431
|
+
sub: `${fmtN(s.traffic?.today)} page views today${delta == null ? '' : ` (${delta >= 0 ? '+' : ''}${Number(delta).toFixed(0)}% on yesterday)`}`
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (name === 'top-pages') {
|
|
435
|
+
return {
|
|
436
|
+
items: (s.topPages || []).map(p => ({
|
|
437
|
+
text: p.url, value: fmtN(p.hits),
|
|
438
|
+
meta: p.deltaPct == null ? '' : `${p.deltaPct >= 0 ? '+' : ''}${Number(p.deltaPct).toFixed(0)}% on the week before`,
|
|
439
|
+
tone: p.deltaPct == null ? '' : (p.deltaPct >= 0 ? 'ok' : 'danger')
|
|
440
|
+
})),
|
|
441
|
+
empty: 'No page views in the last week.'
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (name === 'activity') {
|
|
445
|
+
return {
|
|
446
|
+
items: activityRows(s.activity || []).map(a => ({text: a.text, meta: a.meta, href: a.href})),
|
|
447
|
+
empty: 'Nothing has changed lately.'
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (name === 'health') {
|
|
451
|
+
const LEVEL = {ok: 'ok', warn: 'warn', fail: 'danger'};
|
|
452
|
+
return {
|
|
453
|
+
items: (s.health?.checks || []).map(c => ({
|
|
454
|
+
text: String(c.label || c.id), value: String(c.value ?? ''),
|
|
455
|
+
meta: Array.isArray(c.detail) ? c.detail.slice(0, 3).join(', ') : '',
|
|
456
|
+
tone: LEVEL[c.level] || 'warn'
|
|
457
|
+
})),
|
|
458
|
+
empty: 'No health checks ran.'
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return reply.status(404).send({error: 'Not found'});
|
|
377
462
|
});
|
|
378
463
|
}
|
|
@@ -14,6 +14,7 @@ import path from 'path';
|
|
|
14
14
|
import {fileURLToPath} from 'url';
|
|
15
15
|
import * as cache from '../../services/cache/index.js';
|
|
16
16
|
import {normaliseAdminPath} from '../../services/roles.js';
|
|
17
|
+
import {cssClassProblems} from '../../../public/js/css-classes.mjs';
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
@@ -96,7 +97,9 @@ export async function settingsRoutes(fastify) {
|
|
|
96
97
|
// `brand` by the menus migration) and are echoed back by GET /settings.
|
|
97
98
|
// They must be accepted here or any client that round-trips the GET
|
|
98
99
|
// payload into a save gets a 400 for keys it never touched.
|
|
99
|
-
'brand', 'url', 'baseTheme'
|
|
100
|
+
'brand', 'url', 'baseTheme',
|
|
101
|
+
// Designer classes on <body>, every page (Site Settings > Appearance).
|
|
102
|
+
'bodyClass'
|
|
100
103
|
]);
|
|
101
104
|
const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
|
|
102
105
|
if (unknownKeys.length > 0) {
|
|
@@ -111,6 +114,12 @@ export async function settingsRoutes(fastify) {
|
|
|
111
114
|
}
|
|
112
115
|
data.baseUrl = origin;
|
|
113
116
|
}
|
|
117
|
+
// Designer classes - the same rules as menus and forms (css-classes.mjs).
|
|
118
|
+
for (const [label, value] of [['Body CSS classes', data.bodyClass], ['Footer CSS classes', data.footer?.class],
|
|
119
|
+
['Social link CSS classes', data.footer?.socialClass]]) {
|
|
120
|
+
const problems = cssClassProblems(value);
|
|
121
|
+
if (problems.length) return reply.status(400).send({error: `${label}: ${problems[0]}.`});
|
|
122
|
+
}
|
|
114
123
|
if ('adminHome' in data) {
|
|
115
124
|
// An admin route, or blank for the built-in dashboard.
|
|
116
125
|
const home = data.adminHome == null || data.adminHome === '' ? '/' : normaliseAdminPath(data.adminHome);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Pro - the widget list and the board layouts.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/dashboard/pro {enabled} - whether this site has the board (any signed-in user)
|
|
5
|
+
* GET /api/widgets the widgets this user may add (Pro)
|
|
6
|
+
* GET /api/dashboard/layout {layout, source: mine|site|built-in} (Pro)
|
|
7
|
+
* PUT /api/dashboard/layout save mine (Pro)
|
|
8
|
+
* DELETE /api/dashboard/layout go back to the site's default (Pro)
|
|
9
|
+
* GET /api/dashboard/layout/default the site default (Pro, admins)
|
|
10
|
+
* PUT /api/dashboard/layout/default set it (Pro, admins)
|
|
11
|
+
*
|
|
12
|
+
* Every route but /dashboard/pro answers 404 while no Pro Tool has switched the
|
|
13
|
+
* board on (services/widgets.js) - a free site has none of this.
|
|
14
|
+
*
|
|
15
|
+
* A layout is {version, widgets: [{id, size}]}, in board order. Mine is kept in
|
|
16
|
+
* content/dashboard/layouts/<user id>.json (ignored in a site repo - it is per
|
|
17
|
+
* user), the site default in content/dashboard/default.json (tracked).
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs/promises';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import {authenticate as defaultAuthenticate, requireAdmin as defaultRequireAdmin} from '../../middleware/auth.js';
|
|
22
|
+
import {getPermissionsForRoles} from '../../services/roles.js';
|
|
23
|
+
import {getEffectiveLevel, getEffectiveRoles} from '../../services/userRoles.js';
|
|
24
|
+
import {getEffectiveRegistry} from '../../services/permissionRegistry.js';
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_LAYOUT, getWidgets, isDashboardPro, layoutProblems, normaliseLayout, widgetsFor
|
|
27
|
+
} from '../../services/widgets.js';
|
|
28
|
+
|
|
29
|
+
const DIR = path.resolve('content', 'dashboard');
|
|
30
|
+
const LAYOUTS = path.join(DIR, 'layouts');
|
|
31
|
+
const DEFAULT_FILE = path.join(DIR, 'default.json');
|
|
32
|
+
const USER_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
|
|
33
|
+
|
|
34
|
+
async function readJson(file) {
|
|
35
|
+
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Write via a temporary file, so a crash never leaves half a layout. */
|
|
39
|
+
async function writeJson(file, data) {
|
|
40
|
+
await fs.mkdir(path.dirname(file), {recursive: true});
|
|
41
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
42
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
43
|
+
await fs.rename(tmp, file);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const userFile = (user) => (USER_ID_RE.test(String(user?.id || '')) ? path.join(LAYOUTS, `${user.id}.json`) : null);
|
|
47
|
+
|
|
48
|
+
/** The site default, or the built-in one. */
|
|
49
|
+
async function siteDefault() {
|
|
50
|
+
const stored = await readJson(DEFAULT_FILE);
|
|
51
|
+
return stored ? {layout: normaliseLayout(stored), source: 'site'} : {layout: normaliseLayout(DEFAULT_LAYOUT), source: 'built-in'};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Who is asking, in the terms widgetsFor() takes. */
|
|
55
|
+
function who(user) {
|
|
56
|
+
const permissions = getPermissionsForRoles(getEffectiveRoles(user), getEffectiveRegistry().map(r => r.key));
|
|
57
|
+
return {permissions, isAdmin: getEffectiveLevel(user) <= 1};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function widgetRoutes(fastify, opts = {}) {
|
|
61
|
+
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
62
|
+
const requireAdmin = opts.requireAdmin || defaultRequireAdmin;
|
|
63
|
+
const pro = async (request, reply) => {
|
|
64
|
+
if (!isDashboardPro()) return reply.status(404).send({error: 'Not found'});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// isAdmin: whether this user may set the site's default layout.
|
|
68
|
+
fastify.get('/dashboard/pro', {preHandler: [authenticate]}, async (request) => ({
|
|
69
|
+
enabled: isDashboardPro(),
|
|
70
|
+
isAdmin: getEffectiveLevel(request.user) <= 1
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
fastify.get('/widgets', {preHandler: [authenticate, pro]}, async (request) => widgetsFor(getWidgets(), who(request.user)));
|
|
74
|
+
|
|
75
|
+
fastify.get('/dashboard/layout', {preHandler: [authenticate, pro]}, async (request) => {
|
|
76
|
+
const file = userFile(request.user);
|
|
77
|
+
const mine = file ? await readJson(file) : null;
|
|
78
|
+
return mine ? {layout: normaliseLayout(mine), source: 'mine'} : siteDefault();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
fastify.put('/dashboard/layout', {preHandler: [authenticate, pro]}, async (request, reply) => {
|
|
82
|
+
const problems = layoutProblems(request.body);
|
|
83
|
+
if (problems.length) return reply.status(400).send({error: problems[0]});
|
|
84
|
+
const file = userFile(request.user);
|
|
85
|
+
if (!file) return reply.status(400).send({error: 'Your account cannot keep a layout.'});
|
|
86
|
+
const layout = normaliseLayout(request.body);
|
|
87
|
+
await writeJson(file, layout);
|
|
88
|
+
return {layout, source: 'mine'};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
fastify.delete('/dashboard/layout', {preHandler: [authenticate, pro]}, async (request) => {
|
|
92
|
+
const file = userFile(request.user);
|
|
93
|
+
if (file) await fs.rm(file, {force: true});
|
|
94
|
+
return siteDefault();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
fastify.get('/dashboard/layout/default', {preHandler: [authenticate, pro, requireAdmin]}, async () => siteDefault());
|
|
98
|
+
|
|
99
|
+
fastify.put('/dashboard/layout/default', {preHandler: [authenticate, pro, requireAdmin]}, async (request, reply) => {
|
|
100
|
+
const problems = layoutProblems(request.body);
|
|
101
|
+
if (problems.length) return reply.status(400).send({error: problems[0]});
|
|
102
|
+
const layout = normaliseLayout(request.body);
|
|
103
|
+
await writeJson(DEFAULT_FILE, layout);
|
|
104
|
+
return {layout, source: 'site'};
|
|
105
|
+
});
|
|
106
|
+
}
|
package/server/server.js
CHANGED
|
@@ -523,6 +523,7 @@ const {effectsRoutes} = await import('./routes/api/effects.js');
|
|
|
523
523
|
const {searchRoutes} = await import('./routes/api/search.js');
|
|
524
524
|
const {notificationsRoutes} = await import('./routes/api/notifications.js');
|
|
525
525
|
const {dashboardRoutes} = await import('./routes/api/dashboard.js');
|
|
526
|
+
const {widgetRoutes} = await import('./routes/api/widgets.js');
|
|
526
527
|
const {cacheRoutes} = await import('./routes/api/cache.js');
|
|
527
528
|
const {scaffoldRoutes} = await import('./routes/api/scaffold.js');
|
|
528
529
|
|
|
@@ -558,6 +559,7 @@ await app.register(effectsRoutes, {prefix: '/api'});
|
|
|
558
559
|
await app.register(searchRoutes, {prefix: '/api'});
|
|
559
560
|
await app.register(notificationsRoutes, {prefix: '/api'});
|
|
560
561
|
await app.register(dashboardRoutes, {prefix: '/api'});
|
|
562
|
+
await app.register(widgetRoutes, {prefix: '/api'});
|
|
561
563
|
await app.register(cacheRoutes, {prefix: '/api'});
|
|
562
564
|
await app.register(scaffoldRoutes, {prefix: '/api'});
|
|
563
565
|
// Tools promoted out of plugins/ - their own prefixes, not /api/plugins/*.
|
|
@@ -19,6 +19,7 @@ import fs from 'fs/promises';
|
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import {fileURLToPath} from 'url';
|
|
21
21
|
import * as cache from './cache/index.js';
|
|
22
|
+
import {cssClassProblems} from '../../public/js/css-classes.mjs';
|
|
22
23
|
|
|
23
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const CONFIG_DIR = path.resolve(__dirname, '..', '..', 'config');
|
|
@@ -154,6 +155,7 @@ export function validateContextMenu(menu) {
|
|
|
154
155
|
|
|
155
156
|
if (!isPlainObject(menu)) return ['Context menu must be an object'];
|
|
156
157
|
if (!validateSlug(menu.slug)) errors.push(`Invalid slug: ${menu.slug}`);
|
|
158
|
+
for (const p of cssClassProblems(menu.class)) errors.push(`class: ${p}`);
|
|
157
159
|
if (!menu.name || typeof menu.name !== 'string') errors.push('Name is required');
|
|
158
160
|
|
|
159
161
|
// --- target ---
|
|
@@ -249,6 +251,7 @@ function validateItems(items, errors, trail, depth = 0) {
|
|
|
249
251
|
if (!ITEM_TYPES.has(type)) { errors.push(`${at}.type "${type}" is not recognised`); return; }
|
|
250
252
|
if (type === 'divider') return;
|
|
251
253
|
if (!item.text || typeof item.text !== 'string') errors.push(`${at}.text is required`);
|
|
254
|
+
for (const p of cssClassProblems(item.class)) errors.push(`${at}.class: ${p}`);
|
|
252
255
|
if (type === 'header') return;
|
|
253
256
|
|
|
254
257
|
const hasSub = Array.isArray(item.submenu) && item.submenu.length;
|
|
@@ -292,6 +295,8 @@ function shape(input) {
|
|
|
292
295
|
inherit: input.inherit === undefined ? 'append' : input.inherit,
|
|
293
296
|
...(Number.isInteger(input.priority) && input.priority !== 0 && {priority: input.priority}),
|
|
294
297
|
...(isPlainObject(input.theme) && Object.keys(input.theme).length && {theme: input.theme}),
|
|
298
|
+
// The author's own classes on the panel; '' (sent to clear) is dropped.
|
|
299
|
+
...(typeof input.class === 'string' && input.class.trim() && {class: input.class.trim()}),
|
|
295
300
|
items: Array.isArray(input.items) ? input.items : []
|
|
296
301
|
};
|
|
297
302
|
const binding = normaliseBinding(input.binding);
|
|
@@ -472,6 +477,7 @@ export function toClientPayload(menu) {
|
|
|
472
477
|
inherit: menu.inherit === undefined ? 'append' : menu.inherit,
|
|
473
478
|
...(menu.priority && {priority: menu.priority}),
|
|
474
479
|
...(menu.theme && {theme: menu.theme}),
|
|
480
|
+
...(menu.class && {class: menu.class}),
|
|
475
481
|
items: menu.items || []
|
|
476
482
|
};
|
|
477
483
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Markdown Service
|
|
3
3
|
* Parses frontmatter with gray-matter and renders Markdown to HTML with marked.
|
|
4
4
|
*/
|
|
5
|
+
import {cssClasses} from '../../public/js/css-classes.mjs';
|
|
5
6
|
import matter from 'gray-matter';
|
|
6
7
|
import {marked} from 'marked';
|
|
7
8
|
import sanitizeHtml from 'sanitize-html';
|
|
@@ -358,6 +359,21 @@ function interpolateBlockTemplate(blockTemplate, e) {
|
|
|
358
359
|
});
|
|
359
360
|
}
|
|
360
361
|
|
|
362
|
+
/**
|
|
363
|
+
* The `item-class` attribute of [collection] / [view] - the designer's own
|
|
364
|
+
* classes for every entry's wrapper, as ` a b` ready to append inside a class
|
|
365
|
+
* attribute, or '' for none. Carried on ctxOpts because that already reaches
|
|
366
|
+
* every renderer. Table rows are drawn in the browser and do not take it.
|
|
367
|
+
*
|
|
368
|
+
* @param {object} ctxOpts
|
|
369
|
+
* @returns {string}
|
|
370
|
+
*/
|
|
371
|
+
function entryClassSuffix(ctxOpts) {
|
|
372
|
+
const list = cssClasses(ctxOpts?.itemClass);
|
|
373
|
+
return list.length ? ` ${escapeAttr(list.join(' '))}` : '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
361
377
|
/**
|
|
362
378
|
* Render collection entries using a reusable HTML block template.
|
|
363
379
|
* Replaces {{fieldName}}, {{_id}}, {{_createdAt}}, {{_updatedAt}} placeholders.
|
|
@@ -380,7 +396,8 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
|
|
|
380
396
|
const blockAttr = hasCss && blockName ? ` data-block="${escapeAttr(blockName)}"` : '';
|
|
381
397
|
|
|
382
398
|
const items = entries.map(e => {
|
|
383
|
-
const
|
|
399
|
+
const entryCls = entryClassSuffix(ctxOpts).trim();
|
|
400
|
+
const wrapOpen = `<div${entryCls ? ` class="${entryCls}"` : ''}${blockAttr} data-entry-id="${escapeAttr(String(e.id ?? ''))}">`;
|
|
384
401
|
const wrapClose = `</div>`;
|
|
385
402
|
let html = interpolateBlockTemplate(blockTemplate, e);
|
|
386
403
|
|
|
@@ -621,7 +638,7 @@ function renderCollectionCards(entries, visibleFields, titleField, columns, empt
|
|
|
621
638
|
const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
|
|
622
639
|
footer = `<div class="card-footer"><button class="${btnCls}" ${btnData}>${iconHtml}${escapeHtmlText(ctaOpts.label || 'Run')}</button></div>`;
|
|
623
640
|
}
|
|
624
|
-
return `<div class="card" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || ' '}</div>${footer}</div>`;
|
|
641
|
+
return `<div class="card${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || ' '}</div>${footer}</div>`;
|
|
625
642
|
}).join('\n');
|
|
626
643
|
return `<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4"${buildCtxAttr(visibleFields, entries, ctxOpts)}>\n${cards}\n</div>`;
|
|
627
644
|
}
|
|
@@ -649,7 +666,7 @@ function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaO
|
|
|
649
666
|
const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
|
|
650
667
|
ctaHtml = `<button class="${btnCls}" ${btnData}>${iconHtml}${escapeHtmlText(ctaOpts.label || 'Run')}</button>`;
|
|
651
668
|
}
|
|
652
|
-
return `<div class="dm-collection-list-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
|
|
669
|
+
return `<div class="dm-collection-list-item${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
|
|
653
670
|
}).join('\n');
|
|
654
671
|
return `<div class="dm-collection-display dm-collection-list"${buildCtxAttr(visibleFields, entries, ctxOpts)}>\n${items}\n</div>`;
|
|
655
672
|
}
|
|
@@ -674,7 +691,7 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
|
|
|
674
691
|
const bodyVal = String(e.data?.[bodyField] ?? '');
|
|
675
692
|
const bodyHtml = marked.parse(bodyVal);
|
|
676
693
|
return (
|
|
677
|
-
`<div class="accordion-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}">\n` +
|
|
694
|
+
`<div class="accordion-item${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">\n` +
|
|
678
695
|
` <h3 class="accordion-header"><button class="accordion-button" type="button">${title}` +
|
|
679
696
|
`<span class="accordion-icon" data-icon="chevron-down"></span></button></h3>\n` +
|
|
680
697
|
` <div class="accordion-body"><div class="accordion-content">${bodyHtml}</div></div>\n` +
|
|
@@ -719,7 +736,7 @@ function renderCollectionTimeline(entries, opts, ctxOpts = {}) {
|
|
|
719
736
|
const bodyHtml = bodyField
|
|
720
737
|
? marked.parse(String(e.data?.[bodyField] ?? ''))
|
|
721
738
|
: '';
|
|
722
|
-
return `<div class="dm-progression-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${bodyHtml}</div></div>`;
|
|
739
|
+
return `<div class="dm-progression-item${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${bodyHtml}</div></div>`;
|
|
723
740
|
}).join('\n');
|
|
724
741
|
|
|
725
742
|
const ctx = buildCtxAttr(
|
|
@@ -779,7 +796,7 @@ function renderCollectionCarousel(entries, opts, ctxOpts = {}) {
|
|
|
779
796
|
: '';
|
|
780
797
|
inner = img + (title || bodyHtml ? `<div class="carousel-slide-content">${title}${bodyHtml}</div>` : '');
|
|
781
798
|
}
|
|
782
|
-
return `<div class="carousel-slide" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
|
|
799
|
+
return `<div class="carousel-slide${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
|
|
783
800
|
}).join('\n');
|
|
784
801
|
|
|
785
802
|
const ctxFields = blockTemplate
|
|
@@ -872,7 +889,7 @@ function renderCollectionListgroup(entries, opts, ctxOpts = {}) {
|
|
|
872
889
|
: '';
|
|
873
890
|
inner = (title + body) || escapeHtmlText(String(e.id ?? ''));
|
|
874
891
|
}
|
|
875
|
-
return `<div class="${itemClassStr}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
|
|
892
|
+
return `<div class="${itemClassStr}${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
|
|
876
893
|
}).join('\n');
|
|
877
894
|
|
|
878
895
|
const ctxFields = blockTemplate
|
|
@@ -976,9 +993,10 @@ async function processViewBlocks(markdown, tagSet, user = null, flags = {}) {
|
|
|
976
993
|
const ctxOpts = sourceSchema
|
|
977
994
|
? {
|
|
978
995
|
slug: sourceSlug,
|
|
979
|
-
export: resolveExportConfig(sourceSchema.export, attrs, getRoleHierarchy().at(-1))
|
|
996
|
+
export: resolveExportConfig(sourceSchema.export, attrs, getRoleHierarchy().at(-1)),
|
|
997
|
+
itemClass: attrs['item-class']
|
|
980
998
|
}
|
|
981
|
-
: {slug: sourceSlug || undefined, export: {allowed: false, access: null, formats: []}};
|
|
999
|
+
: {slug: sourceSlug || undefined, export: {allowed: false, access: null, formats: []}, itemClass: attrs['item-class']};
|
|
982
1000
|
|
|
983
1001
|
// Derive visible fields from attr filter or first entry's data keys
|
|
984
1002
|
let fields;
|
|
@@ -1500,7 +1518,9 @@ export async function renderCollectionFragment(attrs, { extraFilter = null, scop
|
|
|
1500
1518
|
const defaultRole = getRoleHierarchy().at(-1);
|
|
1501
1519
|
const ctxOpts = {
|
|
1502
1520
|
slug: schema.slug,
|
|
1503
|
-
export: resolveExportConfig(schema.export, attrs, defaultRole)
|
|
1521
|
+
export: resolveExportConfig(schema.export, attrs, defaultRole),
|
|
1522
|
+
// Designer classes for every entry's wrapper (entryClassSuffix).
|
|
1523
|
+
itemClass: attrs['item-class']
|
|
1504
1524
|
};
|
|
1505
1525
|
|
|
1506
1526
|
const filter = buildShortcodeFilter(attrs);
|