domma-cms 0.71.0 → 0.73.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/CLAUDE.md +7 -0
- 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/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/activity-throbber.js +1 -0
- package/admin/js/lib/admin-theme.js +1 -0
- package/admin/js/lib/menu-rows.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -1
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/templates/settings.html +8 -0
- package/admin/js/templates/theme.html +0 -3
- package/admin/js/views/menu-editor.js +16 -16
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -1
- package/package.json +2 -2
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -1
- package/server/routes/api/auth.js +16 -3
- package/server/routes/api/settings.js +9 -0
- package/server/routes/api/sidebar.js +7 -2
- package/server/routes/api/theme.js +22 -3
- package/server/server.js +10 -8
- package/server/services/adminHome.js +87 -0
- package/server/services/hooks.js +30 -6
- package/server/services/menus.js +36 -6
- package/server/services/plugins.js +93 -6
- package/server/services/renderer.js +24 -10
- package/server/services/roles.js +14 -3
- package/server/services/sidebar-migration.js +82 -8
- package/server/services/themeSettings.js +210 -10
- package/server/templates/page.html +13 -4
- package/admin/js/lib/sidebar-grouping.test.js +0 -1
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{makeBadgeEl as N,applyColour as M,applyPill as B,applyFloat as R,applyHighlight as z,applyItemHighlight as P,itemHighlightAttrs as V,isCurrentItem as O,menuAlign as J}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{initFloatingMenus as W}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as j,markInvalidFields as G,showValidationPopup as X}from"/public/js/forms.js?v=20260821-wizard-grid";import{buildWizardSteps as Y}from"/public/js/form-steps.mjs";$(()=>{const b=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let e=function(a){const t=(a||"07:00").split(":");return+t[0]*60+(+t[1]||0)},n=function(){const a=new Date,t=a.getHours()*60+a.getMinutes();return t>=e(o.dayStart)&&t<e(o.nightStart)?o.dayTheme:o.nightTheme};const o=g.autoTheme;Domma.theme.set(n()),setInterval(()=>Domma.theme.set(n()),6e4)}const k=$("#site-navbar");if(k.length&&b.brand){let n=function(i,l=0){return Array.isArray(i)?i.filter(s=>!(s&&s.type==="spacer")).filter(s=>!(s&&s.type==="separator"&&l===0)).map(s=>s&&s.type==="separator"?{divider:!0}:Array.isArray(s?.items)?{...s,items:n(s.items,l+1)}:s):i},a=function(i){return Array.isArray(i)?i.filter(l=>!(l&&(l.type==="separator"||l.type==="spacer"))):[]};const o={...b.brand},e=o.size&&o.size!=="md"?` navbar-brand-${o.size}`:"";if(o.logo||o.icon||o.tagline){let i="";o.logo?i+=`<img src="${o.logo}" class="navbar-brand-logo" alt="${o.text||""}">`:o.icon&&(i+=`<span data-icon="${o.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),o.text&&(i+=`<span class="navbar-brand-text${e}">${o.text}</span>`),o.tagline&&(i+=`<small class="navbar-brand-tagline">${o.tagline}</small>`),o.html=i}else e&&o.text&&(o.html=`<span class="navbar-brand-text${e}">${o.text}</span>`);const t=b.position==="floating",d=b.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:o,items:n(b.items||[]),variant:b.variant||"dark",position:t?"static":b.position||"sticky",collapsible:!0,appearOnHover:!!b.appearOnHover}),Domma.icons.scan("#site-navbar");const h=k.get(0);h.classList.add(`dm-nav-align-${J(b)||"centre"}`),d&&k.get(0).classList.add("dm-nav-vertical",b.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(h,b),t&&(R(k.get(0),b),W(k.get(0).parentNode||document));const y=h.querySelector(".navbar-toggle"),f=h.querySelector(".navbar-collapse");if(y&&f){let i=null;const l=()=>{f.classList.contains("show")&&y.click()},s=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},r=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",l),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{f.classList.contains("show")?r():s()})}),f.addEventListener("click",c=>{c.target.closest("a")&&requestAnimationFrame(s)}),document.addEventListener("keydown",c=>{c.key==="Escape"&&f.classList.contains("show")&&l()})}const u=h.querySelector(".navbar-nav");if(u){const i=Array.from(u.children);let l=0;for(const s of b.items||[])if(!(s&&s.type==="separator")){if(s&&s.type==="spacer"){const r=document.createElement("li"),c=["xs","sm","md","lg","xl","flex"].includes(s.size)?s.size:"md";r.className=`dm-menu-spacer dm-menu-spacer--${c}`,r.setAttribute("aria-hidden","true"),u.insertBefore(r,i[l]||null);continue}l++}}const w=a(b.items||[]);h.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const l=w[parseInt(i.dataset.index,10)];if(!l)return;const s=i.dataset.subindex!=null,r=s&&Array.isArray(l.items)?l.items[parseInt(i.dataset.subindex,10)]:l;if(!r)return;r.colour&&M(i,r.colour),r.pill&&!s&&B(i,r.pill),P(i,r);const c=N(r.badge);c&&i.appendChild(c),O(r.url,location.pathname)&&(i.classList.add("active"),i.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let t=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},d=function(r){return t(r).replace(/'/g,"'")},h=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>`;const c=d(r.url||"#"),m=t(r.text||""),v=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"",I=V(r),L=[I.hover&&` data-menu-item-hover="${d(I.hover)}"`,I.active&&` data-menu-item-active="${d(I.active)}"`].filter(Boolean).join(""),x=I.accent?` style="--dm-menu-highlight:${d(I.accent)}"`:"";return`<li><a href="${c}"${L}${x}>${m}</a>${v}</li>`},y=function(r,c){return(r||[]).forEach(m=>{m&&(m.type==="separator"||m.type==="spacer")||(c.push(m),Array.isArray(m.items)&&m.items.length&&y(m.items,c))}),c};const o=g.social||{},e={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 a=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(a.copyright&&(n+=`<p>${t(a.copyright)}</p>`),a.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary"><ul>${a.primary.map(h).join("")}</ul></nav>`),a.legal.length&&(n+=`<nav class="footer-nav footer-nav--legal"><ul>${a.legal.map(h).join("")}</ul></nav>`),g.footer){const r=Object.keys(e).filter(c=>o[c]);r.length&&(n+='<div class="footer-social">',r.forEach(c=>{const{label:m,svg:v}=e[c];n+=`<a href="${o[c]}" target="_blank" rel="noopener noreferrer" aria-label="${m}" class="footer-social-link">${v}</a>`}),n+="</div>")}n+="</div>",C.html(n);const f=[...y(a.primary,[]),...y(a.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,c)=>{const m=f[c];if(!m)return;m.colour&&M(r,m.colour);const v=N(m.badge);v&&r.appendChild(v),O(m.url,location.pathname)&&(r.classList.add("is-active"),r.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});const w=g.footer?.showMotionToggle!==!1,i=S.get("reduced_motion"),l=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),s=C.get(0).querySelector(".footer-inner");if(w&&s){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=l,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const c=document.createElement("span");c.className="form-switch-label",c.textContent="Reduce motion";const m=document.createElement("label");m.className="form-switch footer-motion-switch",m.title="Reduce motion",m.appendChild(r),m.appendChild(c),s.appendChild(m)}}$("#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 p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const o={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 e=parseInt(this.dataset.animationDuration,10);Number.isFinite(e)&&(o.animationDuration=e)}this.dataset.animationEasing&&(o.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(e=>e.cloneNode(!0)),this._dmCarouselOpts=o),this._dmCarousel=Domma.elements.carousel(this,o)}),p.find(".dm-countdown").each(function(){const o={autoStart:!0};this.dataset.to&&(o.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(o.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(o.format=this.dataset.format),Domma.elements.timer(this,o)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(a=>{const t={title:a.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:a.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return a.dataset.date&&(t.date=a.dataset.date),a.dataset.status&&(t.status=a.dataset.status),a.dataset.entryId&&(t._entryId=a.dataset.entryId),t}),e=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",n=Domma.elements.progression(this,{layout:e,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...o.length?{items:o}:{}});o.length&&(this._dmCtx={instance:n,items:o})});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(o=>{const e=o.dataset.revealMode||"stagger",n=o.dataset.revealAnimation||"slide-up",a=parseInt(o.dataset.revealDuration,10)||400,t=parseInt(o.dataset.revealStagger,10)||60,d=parseInt(o.dataset.revealDelay,10)||0,h=o.dataset.revealDirection==="rtl",y=Array.from(o.children),f={"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((u,w)=>{u.style.opacity="0",u.style.transform=f[n]||"",u.style.transition=`opacity ${a}ms ease, transform ${a}ms ease`;const i=h?y.length-1-w:w;u.style.transitionDelay=e==="stagger"?`${d+i*t}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(w=>{w.forEach(i=>{i.isIntersecting&&(i.target.offsetWidth,i.target.style.opacity="1",i.target.style.transform="none",u.unobserve(i.target))})},{threshold:.1});y.forEach(w=>u.observe(w))}))}),p.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,e=document.getElementById(o);if(!e)return;const n=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",n.setContent(e),n.open()})})}if(typeof $.setup=="function"){const o=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(e=>{try{const n=atob(e.dataset.config),a=JSON.parse(n);Object.assign(o,a)}catch{}}),Object.keys(o).length>0){const e={};for(const[n,a]of Object.entries(o)){const t=a?.events?.click,{confirm:d,toast:h,alert:y,prompt:f,...u}=t||{};d||h||y||f?($(n).on("click",async function(i){if(i.preventDefault(),d&&!await E.confirm(d))return;let l=null;if(!(f&&(l=await E.prompt(f,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),l===null))){if(u.target){const s=$(u.target);u.toggleClass&&s.toggleClass(u.toggleClass),u.addClass&&s.addClass(u.addClass),u.removeClass&&s.removeClass(u.removeClass),l!==null&&(u.setText&&s.text(l),u.setVal&&s.val(l),u.setAttr&&s.attr(u.setAttr,l))}u.href&&(window.location.href=u.href),h&&E.toast(h,{type:u.toastType||"success"}),y&&E.alert(y)}}),Object.keys(u).length&&(e[n]={...a,events:{...a.events,click:u}})):e[n]=a}$.setup(e)}}p.length&&D(p.get(0))});function D(b){b.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const k=g.dataset.action,C=g.dataset.entry,_=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(_&&!await E.confirm(_))return;const o=Array.from(g.childNodes).map(n=>n.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const e=n=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({entryId:C})});try{let n=await e(p);if(n.status===401){const t=S.get("auth_refresh_token");if(t){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),p=h,n=await e(p)}}}const a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||a.message||`Error ${n.status}`);E.toast(a.message||"Action completed.",{type:"success"})}catch(n){E.toast(n.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",o.forEach(n=>g.appendChild(n)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const k=S.get("auth_token");function C(p,o){const e=document.createElement("div");e.className="dm-collection-display dm-collection-empty";const n=document.createElement("p");if(o){n.appendChild(document.createTextNode("Please "));const a=document.createElement("a");a.href="/login",a.textContent="sign in",n.appendChild(a),n.appendChild(document.createTextNode(" to view your entries."))}else n.textContent=p;return e.appendChild(n),e}function _(p,o){const e=document.createElement("template");e.innerHTML=o||"",p.replaceWith(e.content)}g.forEach(async p=>{const o=p.dataset.collectionAttrs;if(!o)return;if(!k){p.replaceChildren(C("",!0));return}const e=n=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({attrs:o})});try{let n=k,a=await e(n);if(a.status===401){const d=S.get("auth_refresh_token");if(d){const h=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(h.ok){const{token:y}=await h.json();S.set("auth_token",y),n=y,a=await e(n)}}}if(!a.ok){const{error:d}=await a.json().catch(()=>({}));p.replaceChildren(C(d||"Unable to load entries.",!1));return}const{html:t}=await a.json();_(p,t)}catch{p.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(),D(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(k=>{let C;try{C=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:_,rows:p,search:o,sortable:e,exportable:n,pageSize:a,empty:t,ctaConfig:d,slug:h,exportCfg:y}=C;if(!_?.length)return;if(d){const w=_.findIndex(i=>i.key==="_cta");w!==-1&&(_[w]={key:"_cta",title:"",render:(i,l)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=l._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const r=document.createElement("span");r.dataset.icon=d.icon,s.appendChild(r),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const f="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=f,k.replaceChildren(u),T.create("#"+f,{data:p,columns:_,search:o,sortable:e,exportable:n,pageSize:a,emptyMessage:t}),d&&D(u),k._dmTable={columns:_,rows:p,slug:h,exportCfg:y,render(w){T.create("#"+f,{data:w,columns:_,search:o,sortable:e,exportable:n,pageSize:a,emptyMessage:t}),d&&D(u)}}})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function k(e){const n=e&&e.logic;return!!(n&&(n.visibility||n.requirement))}function C(e,n){const a={};return(e||[]).forEach(t=>{if(t.type==="page-break"||t.type==="spacer"||!t.name)return;const d=t.type==="chooser"||!!(t.variant&&["select","radio","checkbox-group"].includes(t.type)),h=t.type==="checkbox"?"boolean":t.type==="date"||t.type==="icon"||t.type==="image"?"string":d?"chooser":t.type,y={...t.formConfig||{}};y.span==="full"&&n&&(y.span=n);const f={type:h,required:k(t)?!1:t.required,label:t.label,options:t.options,formConfig:{...t.placeholder&&{placeholder:t.placeholder},...t.helper&&{hint:t.helper},...y}};d&&(t.variant&&(f.variant=t.variant),(t.multiple||t.type==="checkbox-group")&&(f.multiple=!0),t.density&&(f.density=t.density),t.columns&&(f.columns=t.columns),t.accent&&(f.accent=t.accent),t.accentStyle&&(f.accentStyle=t.accentStyle),t.glow&&(f.glow=!0),t.glowColour&&(f.glowColour=t.glowColour),t.shadow&&(f.shadow=t.shadow),t.shadowColour&&(f.shadowColour=t.shadowColour)),a[t.name]=f}),a}function _(e){const n={};return(e||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const t=(a.options||[])[0];t&&(n[a.name]=typeof t=="object"?t.value:t)}}),n}function p(e,n){(n||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const t=e.querySelector(`[name="${a.name}"]`);t&&t.type!=="date"&&(t.type="date")})}function o(e,n,a){let t=e.querySelector(".cms-form-message");t||(t=document.createElement("p"),t.className="cms-form-message",e.appendChild(t)),t.textContent=n,t.style.cssText=a?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}g.forEach(e=>{let n;try{n=JSON.parse(atob(e.dataset.formInline))}catch{return}const a=n.fields||[],t=n.settings||{},d=Number(t.columns)===3?3:2,h=t.layout||"grid",y=a.some(l=>l.type==="page-break"),f=async l=>{if(e.dataset.triggerBlocked==="true"){const s=e._formLogicRuntime,r=s&&s.submitBlock?s.submitBlock().reasons:[];o(e,r.join(" ")||"This form cannot be submitted yet.",!0);const c=new Error("Blocked by a form trigger");throw c.formSubmitFailed=!0,c}try{const s=e.querySelector(".fb-form-honeypot"),r=s?.querySelector('input[type="text"]')||null,c=s?.querySelector('[name="_t"]')||null,m=Object.assign({},l);delete m._t,s?.dataset.hpName&&delete m[s.dataset.hpName],r!==null&&(m._hp=r.value),c!==null&&(m._t=c.value);const v=await H.post(`/api/forms/submit/${n.slug}`,m);if(v&&v.error){o(e,v.error,!0);const I=new Error(v.error);throw I.formSubmitFailed=!0,I}if(v?.redirect){window.location.href=v.redirect;return}for(;e.firstChild;)e.removeChild(e.firstChild);o(e,v?.message||t.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||o(e,s.message||"Submission failed. Please try again.",!0),s}},u=Date.now();function w(l){const s=l.querySelector("form");if(!s||s.querySelector(".fb-form-honeypot"))return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const c=document.createElement("input");c.name=s.querySelector('[name="website"]')?"_website":"website",r.dataset.hpName=c.name,c.type="text",c.tabIndex=-1,c.autocomplete="url",c.placeholder="https://",r.appendChild(c);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=u,r.appendChild(m),s.appendChild(r)}function i(l){if(!window.FormLogicEngine||!a.some(r=>r.logic||Array.isArray(r.triggers)&&r.triggers.length))return null;const s=new window.FormLogicEngine.FormLogicRuntime(n,e,l?{baseValues:l}:void 0);if(s.init(),e._formLogicRuntime=s,e._formLogicObserved)return s;if(e._formLogicObserved=!0,e.parentNode&&typeof MutationObserver<"u"){const r=new MutationObserver(function(c){for(const m of c)for(const v of m.removedNodes)if(v===e||v.nodeType===1&&v.contains&&v.contains(e)){e._formLogicRuntime&&e._formLogicRuntime.destroy(),r.disconnect();return}});r.observe(e.parentNode,{childList:!0,subtree:!1}),e._formLogicObserver=r}return s}if(y&&F.wizard){const l=Y(a,n.title),s=l.map(x=>({title:x.title,description:x.description,fields:C(x.fields,d)})),r=l.map(x=>x.fields);let c=0,m=null,v=null;const I=x=>{const A=window.FormLogicEngine;if(!A||!A.validateFields)return;const q=A.validateFields(e,r[c]||[]);q.valid||(x.preventDefault(),x.stopImmediatePropagation(),G(e,q.missing),X(q))},L=()=>{const x=r[c]||a;p(e,x),j(e,x),t.honeypot!==!1&&w(e),m&&m.destroy(),m=i(v?.getStepData?v.getStepData():void 0),e.querySelectorAll(".wizard-next, .wizard-submit").forEach(A=>A.addEventListener("click",I,!0))};v=F.wizard(e,{schema:{steps:s},layout:h,columns:d,finishText:t.submitText||"Finish",onSubmit:f,onStepChange:x=>{c=x,setTimeout(L,0)}}),e.addEventListener("dm-form-trigger",x=>{if(x.detail?.type!=="goto-step"||!v?.goToStep)return;const A=Number(x.detail.action.step);!Number.isFinite(A)||A<0||A>=s.length||A!==c&&v.goToStep(A)}),Promise.resolve(v).then(L)}else if(F.render){const l=F.render(e,C(a,d),_(a),{submitText:t.submitText||"Submit",layout:h,columns:d,onSubmit:f});Promise.resolve(l).then(function(){if(h==="grid"&&t.submitSpan==="full"){const s=e.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}p(e,a),j(e,a),t.honeypot!==!1&&w(e),i()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
1
|
+
import{makeBadgeEl as N,applyColour as M,applyPill as B,applyFloat as R,applyHighlight as z,applyItemHighlight as P,itemHighlightAttrs as V,isCurrentItem as O,menuAlign as J}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{initFloatingMenus as W}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as j,markInvalidFields as G,showValidationPopup as X}from"/public/js/forms.js?v=20260821-wizard-grid";import{buildWizardSteps as Y}from"/public/js/form-steps.mjs";$(()=>{const b=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let t=function(e){const c=(e||"07:00").split(":");return+c[0]*60+(+c[1]||0)},o=function(){const e=new Date,c=e.getHours()*60+e.getMinutes();return c>=t(a.dayStart)&&c<t(a.nightStart)?a.dayTheme:a.nightTheme},n=function(e){const c=a.apply&&a.apply[e]||{theme:e,custom:""};Domma.theme.set(c.theme);for(const p of[...document.body.classList])p.startsWith("dm-custom-")&&document.body.classList.remove(p);c.custom&&document.body.classList.add(c.custom)};const a=g.autoTheme;n(o()),setInterval(()=>n(o()),6e4)}const k=$("#site-navbar");if(k.length&&b.brand){let o=function(i,d=0){return Array.isArray(i)?i.filter(s=>!(s&&s.type==="spacer")).filter(s=>!(s&&s.type==="separator"&&d===0)).map(s=>s&&s.type==="separator"?{divider:!0}:Array.isArray(s?.items)?{...s,items:o(s.items,d+1)}:s):i},n=function(i){return Array.isArray(i)?i.filter(d=>!(d&&(d.type==="separator"||d.type==="spacer"))):[]};const a={...b.brand},t=a.size&&a.size!=="md"?` navbar-brand-${a.size}`:"";if(a.logo||a.icon||a.tagline){let i="";a.logo?i+=`<img src="${a.logo}" class="navbar-brand-logo" alt="${a.text||""}">`:a.icon&&(i+=`<span data-icon="${a.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),a.text&&(i+=`<span class="navbar-brand-text${t}">${a.text}</span>`),a.tagline&&(i+=`<small class="navbar-brand-tagline">${a.tagline}</small>`),a.html=i}else t&&a.text&&(a.html=`<span class="navbar-brand-text${t}">${a.text}</span>`);const e=b.position==="floating",c=b.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:a,items:o(b.items||[]),variant:b.variant||"dark",position:e?"static":b.position||"sticky",collapsible:!0,appearOnHover:!!b.appearOnHover}),Domma.icons.scan("#site-navbar");const p=k.get(0);p.classList.add(`dm-nav-align-${J(b)||"centre"}`),c&&k.get(0).classList.add("dm-nav-vertical",b.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(p,b),e&&(R(k.get(0),b),W(k.get(0).parentNode||document));const y=p.querySelector(".navbar-toggle"),h=p.querySelector(".navbar-collapse");if(y&&h){let i=null;const d=()=>{h.classList.contains("show")&&y.click()},s=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},r=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",d),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?r():s()})}),h.addEventListener("click",l=>{l.target.closest("a")&&requestAnimationFrame(s)}),document.addEventListener("keydown",l=>{l.key==="Escape"&&h.classList.contains("show")&&d()})}const u=p.querySelector(".navbar-nav");if(u){const i=Array.from(u.children);let d=0;for(const s of b.items||[])if(!(s&&s.type==="separator")){if(s&&s.type==="spacer"){const r=document.createElement("li"),l=["xs","sm","md","lg","xl","flex"].includes(s.size)?s.size:"md";r.className=`dm-menu-spacer dm-menu-spacer--${l}`,r.setAttribute("aria-hidden","true"),u.insertBefore(r,i[d]||null);continue}d++}}const w=n(b.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const d=w[parseInt(i.dataset.index,10)];if(!d)return;const s=i.dataset.subindex!=null,r=s&&Array.isArray(d.items)?d.items[parseInt(i.dataset.subindex,10)]:d;if(!r)return;r.colour&&M(i,r.colour),r.pill&&!s&&B(i,r.pill),P(i,r);const l=N(r.badge);l&&i.appendChild(l),O(r.url,location.pathname)&&(i.classList.add("active"),i.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let e=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},c=function(r){return e(r).replace(/'/g,"'")},p=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>`;const l=c(r.url||"#"),m=e(r.text||""),v=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(p).join("")}</ul>`:"",L=V(r),I=[L.hover&&` data-menu-item-hover="${c(L.hover)}"`,L.active&&` data-menu-item-active="${c(L.active)}"`].filter(Boolean).join(""),x=L.accent?` style="--dm-menu-highlight:${c(L.accent)}"`:"";return`<li><a href="${l}"${I}${x}>${m}</a>${v}</li>`},y=function(r,l){return(r||[]).forEach(m=>{m&&(m.type==="separator"||m.type==="spacer")||(l.push(m),Array.isArray(m.items)&&m.items.length&&y(m.items,l))}),l};const a=g.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 o='<div class="footer-inner container">';const n=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(n.copyright&&(o+=`<p>${e(n.copyright)}</p>`),n.primary.length&&(o+=`<nav class="footer-nav footer-nav--primary"><ul>${n.primary.map(p).join("")}</ul></nav>`),n.legal.length&&(o+=`<nav class="footer-nav footer-nav--legal"><ul>${n.legal.map(p).join("")}</ul></nav>`),g.footer){const r=Object.keys(t).filter(l=>a[l]);r.length&&(o+='<div class="footer-social">',r.forEach(l=>{const{label:m,svg:v}=t[l];o+=`<a href="${a[l]}" target="_blank" rel="noopener noreferrer" aria-label="${m}" class="footer-social-link">${v}</a>`}),o+="</div>")}o+="</div>",C.html(o);const h=[...y(n.primary,[]),...y(n.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,l)=>{const m=h[l];if(!m)return;m.colour&&M(r,m.colour);const v=N(m.badge);v&&r.appendChild(v),O(m.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:n.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:n.legalHighlight});const w=g.footer?.showMotionToggle!==!1,i=S.get("reduced_motion"),d=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),s=C.get(0).querySelector(".footer-inner");if(w&&s){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=d,r.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 m=document.createElement("label");m.className="form-switch footer-motion-switch",m.title="Reduce motion",m.appendChild(r),m.appendChild(l),s.appendChild(m)}}$("#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 a={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)&&(a.animationDuration=t)}this.dataset.animationEasing&&(a.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(t=>t.cloneNode(!0)),this._dmCarouselOpts=a),this._dmCarousel=Domma.elements.carousel(this,a)}),f.find(".dm-countdown").each(function(){const a={autoStart:!0};this.dataset.to&&(a.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(a.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(a.format=this.dataset.format),Domma.elements.timer(this,a)}),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 a=Array.from(this.querySelectorAll(".dm-progression-item")).map(n=>{const e={title:n.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:n.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return n.dataset.date&&(e.date=n.dataset.date),n.dataset.status&&(e.status=n.dataset.status),n.dataset.entryId&&(e._entryId=n.dataset.entryId),e}),t=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",o=Domma.elements.progression(this,{layout:t,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...a.length?{items:a}:{}});a.length&&(this._dmCtx={instance:o,items:a})});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(a=>{const t=a.dataset.revealMode||"stagger",o=a.dataset.revealAnimation||"slide-up",n=parseInt(a.dataset.revealDuration,10)||400,e=parseInt(a.dataset.revealStagger,10)||60,c=parseInt(a.dataset.revealDelay,10)||0,p=a.dataset.revealDirection==="rtl",y=Array.from(a.children),h={"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((u,w)=>{u.style.opacity="0",u.style.transform=h[o]||"",u.style.transition=`opacity ${n}ms ease, transform ${n}ms ease`;const i=p?y.length-1-w:w;u.style.transitionDelay=t==="stagger"?`${c+i*e}ms`:`${c}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(w=>{w.forEach(i=>{i.isIntersecting&&(i.target.offsetWidth,i.target.style.opacity="1",i.target.style.transform="none",u.unobserve(i.target))})},{threshold:.1});y.forEach(w=>u.observe(w))}))}),f.find(".card[data-collapsible]").each(function(){const a=this.querySelector(".card-header");a&&a.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const a=this.dataset.soTarget,t=document.getElementById(a);if(!t)return;const o=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",o.setContent(t),o.open()})})}if(typeof $.setup=="function"){const a=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const o=atob(t.dataset.config),n=JSON.parse(o);Object.assign(a,n)}catch{}}),Object.keys(a).length>0){const t={};for(const[o,n]of Object.entries(a)){const e=n?.events?.click,{confirm:c,toast:p,alert:y,prompt:h,...u}=e||{};c||p||y||h?($(o).on("click",async function(i){if(i.preventDefault(),c&&!await E.confirm(c))return;let d=null;if(!(h&&(d=await E.prompt(h,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),d===null))){if(u.target){const s=$(u.target);u.toggleClass&&s.toggleClass(u.toggleClass),u.addClass&&s.addClass(u.addClass),u.removeClass&&s.removeClass(u.removeClass),d!==null&&(u.setText&&s.text(d),u.setVal&&s.val(d),u.setAttr&&s.attr(u.setAttr,d))}u.href&&(window.location.href=u.href),p&&E.toast(p,{type:u.toastType||"success"}),y&&E.alert(y)}}),Object.keys(u).length&&(t[o]={...n,events:{...n.events,click:u}})):t[o]=n}$.setup(t)}}f.length&&D(f.get(0))});function D(b){b.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const k=g.dataset.action,C=g.dataset.entry,_=g.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(_&&!await E.confirm(_))return;const a=Array.from(g.childNodes).map(o=>o.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const t=o=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({entryId:C})});try{let o=await t(f);if(o.status===401){const e=S.get("auth_refresh_token");if(e){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(c.ok){const{token:p}=await c.json();S.set("auth_token",p),f=p,o=await t(f)}}}const n=await o.json().catch(()=>({}));if(!o.ok)throw new Error(n.error||n.message||`Error ${o.status}`);E.toast(n.message||"Action completed.",{type:"success"})}catch(o){E.toast(o.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",a.forEach(o=>g.appendChild(o)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const k=S.get("auth_token");function C(f,a){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const o=document.createElement("p");if(a){o.appendChild(document.createTextNode("Please "));const n=document.createElement("a");n.href="/login",n.textContent="sign in",o.appendChild(n),o.appendChild(document.createTextNode(" to view your entries."))}else o.textContent=f;return t.appendChild(o),t}function _(f,a){const t=document.createElement("template");t.innerHTML=a||"",f.replaceWith(t.content)}g.forEach(async f=>{const a=f.dataset.collectionAttrs;if(!a)return;if(!k){f.replaceChildren(C("",!0));return}const t=o=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({attrs:a})});try{let o=k,n=await t(o);if(n.status===401){const c=S.get("auth_refresh_token");if(c){const p=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:c})});if(p.ok){const{token:y}=await p.json();S.set("auth_token",y),o=y,n=await t(o)}}}if(!n.ok){const{error:c}=await n.json().catch(()=>({}));f.replaceChildren(C(c||"Unable to load entries.",!1));return}const{html:e}=await n.json();_(f,e)}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(),D(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(k=>{let C;try{C=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:_,rows:f,search:a,sortable:t,exportable:o,pageSize:n,empty:e,ctaConfig:c,slug:p,exportCfg:y}=C;if(!_?.length)return;if(c){const w=_.findIndex(i=>i.key==="_cta");w!==-1&&(_[w]={key:"_cta",title:"",render:(i,d)=>{const s=document.createElement("button");if(s.className=`btn btn-${c.style||"primary"} dm-cta-trigger`,s.dataset.action=c.action||"",s.dataset.entry=d._entryId||"",c.confirm&&(s.dataset.confirm=c.confirm),c.icon){const r=document.createElement("span");r.dataset.icon=c.icon,s.appendChild(r),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(c.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=h,k.replaceChildren(u),T.create("#"+h,{data:f,columns:_,search:a,sortable:t,exportable:o,pageSize:n,emptyMessage:e}),c&&D(u),k._dmTable={columns:_,rows:f,slug:p,exportCfg:y,render(w){T.create("#"+h,{data:w,columns:_,search:a,sortable:t,exportable:o,pageSize:n,emptyMessage:e}),c&&D(u)}}})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function k(t){const o=t&&t.logic;return!!(o&&(o.visibility||o.requirement))}function C(t,o){const n={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const c=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":c?"chooser":e.type,y={...e.formConfig||{}};y.span==="full"&&o&&(y.span=o);const h={type:p,required:k(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...y}};c&&(e.variant&&(h.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(h.multiple=!0),e.density&&(h.density=e.density),e.columns&&(h.columns=e.columns),e.accent&&(h.accent=e.accent),e.accentStyle&&(h.accentStyle=e.accentStyle),e.glow&&(h.glow=!0),e.glowColour&&(h.glowColour=e.glowColour),e.shadow&&(h.shadow=e.shadow),e.shadowColour&&(h.shadowColour=e.shadowColour)),n[e.name]=h}),n}function _(t){const o={};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&&(o[n.name]=typeof e=="object"?e.value:e)}}),o}function f(t,o){(o||[]).forEach(n=>{if(n.type!=="date"||!n.name)return;const e=t.querySelector(`[name="${n.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function a(t,o,n){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=n?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const n=o.fields||[],e=o.settings||{},c=Number(e.columns)===3?3:2,p=e.layout||"grid",y=n.some(d=>d.type==="page-break"),h=async d=>{if(t.dataset.triggerBlocked==="true"){const s=t._formLogicRuntime,r=s&&s.submitBlock?s.submitBlock().reasons:[];a(t,r.join(" ")||"This form cannot be submitted yet.",!0);const l=new Error("Blocked by a form trigger");throw l.formSubmitFailed=!0,l}try{const s=t.querySelector(".fb-form-honeypot"),r=s?.querySelector('input[type="text"]')||null,l=s?.querySelector('[name="_t"]')||null,m=Object.assign({},d);delete m._t,s?.dataset.hpName&&delete m[s.dataset.hpName],r!==null&&(m._hp=r.value),l!==null&&(m._t=l.value);const v=await H.post(`/api/forms/submit/${o.slug}`,m);if(v&&v.error){a(t,v.error,!0);const L=new Error(v.error);throw L.formSubmitFailed=!0,L}if(v?.redirect){window.location.href=v.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);a(t,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||a(t,s.message||"Submission failed. Please try again.",!0),s}},u=Date.now();function w(d){const s=d.querySelector("form");if(!s||s.querySelector(".fb-form-honeypot"))return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const l=document.createElement("input");l.name=s.querySelector('[name="website"]')?"_website":"website",r.dataset.hpName=l.name,l.type="text",l.tabIndex=-1,l.autocomplete="url",l.placeholder="https://",r.appendChild(l);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=u,r.appendChild(m),s.appendChild(r)}function i(d){if(!window.FormLogicEngine||!n.some(r=>r.logic||Array.isArray(r.triggers)&&r.triggers.length))return null;const s=new window.FormLogicEngine.FormLogicRuntime(o,t,d?{baseValues:d}:void 0);if(s.init(),t._formLogicRuntime=s,t._formLogicObserved)return s;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const r=new MutationObserver(function(l){for(const m of l)for(const v of m.removedNodes)if(v===t||v.nodeType===1&&v.contains&&v.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),r.disconnect();return}});r.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=r}return s}if(y&&F.wizard){const d=Y(n,o.title),s=d.map(x=>({title:x.title,description:x.description,fields:C(x.fields,c)})),r=d.map(x=>x.fields);let l=0,m=null,v=null;const L=x=>{const A=window.FormLogicEngine;if(!A||!A.validateFields)return;const q=A.validateFields(t,r[l]||[]);q.valid||(x.preventDefault(),x.stopImmediatePropagation(),G(t,q.missing),X(q))},I=()=>{const x=r[l]||n;f(t,x),j(t,x),e.honeypot!==!1&&w(t),m&&m.destroy(),m=i(v?.getStepData?v.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(A=>A.addEventListener("click",L,!0))};v=F.wizard(t,{schema:{steps:s},layout:p,columns:c,finishText:e.submitText||"Finish",onSubmit:h,onStepChange:x=>{l=x,setTimeout(I,0)}}),t.addEventListener("dm-form-trigger",x=>{if(x.detail?.type!=="goto-step"||!v?.goToStep)return;const A=Number(x.detail.action.step);!Number.isFinite(A)||A<0||A>=s.length||A!==l&&v.goToStep(A)}),Promise.resolve(v).then(I)}else if(F.render){const d=F.render(t,C(n,c),_(n),{submitText:e.submitText||"Submit",layout:p,columns:c,onSubmit:h});Promise.resolve(d).then(function(){if(p==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}f(t,n),j(t,n),e.honeypot!==!1&&w(t),i()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";var a=window.__CMS_THEME_SWITCHER__;if(!a||!Array.isArray(a.themes)||!a.themes.length)return;var
|
|
1
|
+
(function(){"use strict";var a=window.__CMS_THEME_SWITCHER__;if(!a||!Array.isArray(a.themes)||!a.themes.length)return;var i=a.position||"right-center",m=i==="left-center"||i==="bottom-left"||i==="top-left",u=i==="bottom-left"||i==="bottom-right",l=i==="top-left"||i==="top-right",c=a.offsetX!=null?a.offsetX:32,f=a.offsetY!=null?a.offsetY:32,n=document.createElement("div");n.className="dm-theme-switcher"+(m?" dm-ts-left":"")+(l?" dm-ts-top":""),n.id="dm-theme-switcher",n.setAttribute("role","navigation"),n.setAttribute("aria-label","Theme switcher");var o=document.createElement("button");o.className="dm-ts-trigger",o.setAttribute("data-tooltip","Switch theme"),o.setAttribute("aria-haspopup","true"),o.setAttribute("aria-expanded","false");var p=360/a.themes.length;o.style.background="conic-gradient("+a.themes.map(function(e,t){return(e.from||"#888")+" "+(t*p).toFixed(2)+"deg "+((t+1)*p).toFixed(2)+"deg"}).join(", ")+")";var s=document.createElement("div");s.className="dm-ts-options",s.setAttribute("role","menu");var h=a.themes.map(function(e){var t=document.createElement("button");return t.className="dm-ts-dot",t.dataset.theme=e.id,t.setAttribute("data-tooltip",e.label),t.setAttribute("aria-label",e.label),t.setAttribute("role","menuitem"),t.style.background="linear-gradient(135deg, "+(e.from||"#888")+", "+(e.to||e.from||"#888")+")",s.appendChild(t),t});n.appendChild(o),n.appendChild(s),Object.assign(n.style,{right:m?"auto":c+"px",left:m?c+"px":"auto",top:l?f+"px":u?"auto":"50vh",bottom:u?f+"px":"auto",transform:l||u?"none":"translateY(-50%)"});function b(){document.body.appendChild(n)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",b):b();function g(e,t){typeof Domma<"u"&&Domma.theme?e(Domma.theme):t>0&&setTimeout(function(){g(e,t-1)},100)}g(function(e){function t(){var r=e.get();h.forEach(function(d){d.classList.toggle("active",d.dataset.theme===r)})}h.forEach(function(r){r.addEventListener("click",function(){e.set(r.dataset.theme),document.body.className=document.body.className.split(" ").filter(function(d){return d.indexOf("dm-custom-")!==0}).join(" "),t()})}),e.onChange(t),t(),o.addEventListener("click",function(){var r=o.getAttribute("aria-expanded")==="true";o.setAttribute("aria-expanded",String(!r))})},30)})();
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
11
|
import {config, getConfig} from '../../config.js';
|
|
12
|
-
import {hooks} from '../../services/hooks.js';
|
|
13
|
-
import {
|
|
12
|
+
import {getRegisteredSidebarItems, hooks} from '../../services/hooks.js';
|
|
13
|
+
import {getMenu} from '../../services/menus.js';
|
|
14
|
+
import {resolveAdminHome} from '../../services/adminHome.js';
|
|
15
|
+
import {authenticate, checkVisibility} from '../../middleware/auth.js';
|
|
14
16
|
import {getAdminScope, getPermissionsForRoles, getRoleLevel} from '../../services/roles.js';
|
|
15
17
|
import {getEffectiveRegistry, GROUP_ORDER} from '../../services/permissionRegistry.js';
|
|
16
18
|
import {getEffectiveRoles} from '../../services/userRoles.js';
|
|
@@ -126,7 +128,18 @@ export async function authRoutes(fastify) {
|
|
|
126
128
|
const permissions = getPermissionsForRoles(roles, allResources);
|
|
127
129
|
// adminScope: null = the whole admin; otherwise {home, allow} - the
|
|
128
130
|
// screens this user's roles confine them to (see roles.getAdminScope).
|
|
129
|
-
|
|
131
|
+
const adminScope = getAdminScope(roles);
|
|
132
|
+
// adminHome: the screen `#/` opens for this user - see services/adminHome.js.
|
|
133
|
+
// The menu is read only when a site home has to be checked against it.
|
|
134
|
+
const adminHome = await resolveAdminHome({
|
|
135
|
+
scope: adminScope,
|
|
136
|
+
permissions,
|
|
137
|
+
siteHome: getConfig('site')?.adminHome,
|
|
138
|
+
registered: () => getRegisteredSidebarItems().map(e => e.item),
|
|
139
|
+
menuItems: async () => (await getMenu('admin-sidebar').catch(() => null))?.items || [],
|
|
140
|
+
canSee: (visibility) => checkVisibility(request.user, visibility)
|
|
141
|
+
});
|
|
142
|
+
return {permissions, adminScope, adminHome};
|
|
130
143
|
});
|
|
131
144
|
|
|
132
145
|
// GET /api/auth/permissions-registry - returns the full permissions registry,
|
|
@@ -11,6 +11,7 @@ import fs from 'fs/promises';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import {fileURLToPath} from 'url';
|
|
13
13
|
import * as cache from '../../services/cache/index.js';
|
|
14
|
+
import {normaliseAdminPath} from '../../services/roles.js';
|
|
14
15
|
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
@@ -41,6 +42,8 @@ export async function settingsRoutes(fastify) {
|
|
|
41
42
|
'smtp', 'footer', 'analytics', 'social', 'locale',
|
|
42
43
|
'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
|
|
43
44
|
'adminBrand',
|
|
45
|
+
// The screen `#/` (Dashboard) opens - see services/adminHome.js.
|
|
46
|
+
'adminHome',
|
|
44
47
|
// Keys that legitimately live in site.json (written elsewhere - e.g.
|
|
45
48
|
// `brand` by the menus migration) and are echoed back by GET /settings.
|
|
46
49
|
// They must be accepted here or any client that round-trips the GET
|
|
@@ -51,6 +54,12 @@ export async function settingsRoutes(fastify) {
|
|
|
51
54
|
if (unknownKeys.length > 0) {
|
|
52
55
|
return reply.status(400).send({ error: `Unknown settings keys: ${unknownKeys.join(', ')}` });
|
|
53
56
|
}
|
|
57
|
+
if ('adminHome' in data) {
|
|
58
|
+
// An admin route, or blank for the built-in dashboard.
|
|
59
|
+
const home = data.adminHome == null || data.adminHome === '' ? '/' : normaliseAdminPath(data.adminHome);
|
|
60
|
+
if (!home) return reply.status(400).send({error: 'adminHome must be an admin route such as /plugins/blog'});
|
|
61
|
+
data.adminHome = home;
|
|
62
|
+
}
|
|
54
63
|
// Merge incoming data with existing config so partial updates (e.g. just adminBrand)
|
|
55
64
|
// don't wipe unrelated keys. Nested objects get a shallow merge so sub-fields are
|
|
56
65
|
// preserved even when only some sub-fields are sent.
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* `candidate`, say) used to get a 403 here and fall back to a hardcoded tree
|
|
10
10
|
* holding a Menus link they could not open. Items gated by `visibility` are
|
|
11
11
|
* dropped on the server, the same way the public navbar drops them; items
|
|
12
|
-
* gated by `permission` are dropped by the renderer
|
|
12
|
+
* gated by `permission`, and `hidden` items, are dropped by the renderer - it
|
|
13
|
+
* needs the hidden ones to know which Tools a menu has hidden.
|
|
13
14
|
*
|
|
14
15
|
* Auth middlewares are accepted as DI options so tests can supply no-ops.
|
|
15
16
|
*/
|
|
@@ -26,7 +27,11 @@ export async function sidebarRoutes(fastify, opts = {}) {
|
|
|
26
27
|
fastify.get('/sidebar/menu', signedIn, async (request) => {
|
|
27
28
|
const menu = await getMenu(ADMIN_SIDEBAR_SLUG);
|
|
28
29
|
if (!menu) return {items: []};
|
|
29
|
-
|
|
30
|
+
// Hidden items are kept: they are how a menu hides a Tool, and the
|
|
31
|
+
// renderer needs them to place Tools before it drops them. An item
|
|
32
|
+
// this user may not see (`visibility`) arrives as a label-less hidden
|
|
33
|
+
// stub for the same reason - see restrictedStub() in menus.js.
|
|
34
|
+
return {...menu, items: filterItemsForUser(menu.items || [], request.user, {keepHidden: true})};
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
fastify.get('/sidebar/registered-items', signedIn, async (request) => {
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* GET /api/theme - the theme config (themes, fonts, overrides)
|
|
5
5
|
* PUT /api/theme - replace it
|
|
6
6
|
* GET /api/theme/catalog - theme ids and --dm-* token names the shipped
|
|
7
|
-
* stylesheet actually defines
|
|
7
|
+
* stylesheet actually defines, and the custom
|
|
8
|
+
* themes plugins provide
|
|
9
|
+
* GET /api/theme/admin - how to apply the admin's own theme (any signed-in
|
|
10
|
+
* user - everyone's admin needs one)
|
|
8
11
|
* POST /api/theme/compile - compile a candidate config to CSS without saving,
|
|
9
12
|
* so the admin previews exactly what the page gets
|
|
10
13
|
*
|
|
@@ -16,11 +19,15 @@ import {
|
|
|
16
19
|
requirePermission as defaultRequirePermission
|
|
17
20
|
} from '../../middleware/auth.js';
|
|
18
21
|
import {
|
|
22
|
+
compileCustomThemeCss,
|
|
19
23
|
compileOverrideCss,
|
|
20
24
|
getThemeTokenMap,
|
|
25
|
+
listCustomThemes,
|
|
26
|
+
resolveThemeClasses,
|
|
21
27
|
listThemeIds,
|
|
22
28
|
listThemeTokens,
|
|
23
29
|
loadThemeConfig,
|
|
30
|
+
rememberedIds,
|
|
24
31
|
saveThemeConfig,
|
|
25
32
|
validateThemeConfig
|
|
26
33
|
} from '../../services/themeSettings.js';
|
|
@@ -38,9 +45,21 @@ export async function themeRoutes(fastify, opts = {}) {
|
|
|
38
45
|
// rather than from a hardcoded list that would drift from the CSS.
|
|
39
46
|
fastify.get('/theme/catalog', canRead, async () => ({
|
|
40
47
|
themes: listThemeIds(),
|
|
41
|
-
tokens: listThemeTokens()
|
|
48
|
+
tokens: listThemeTokens(),
|
|
49
|
+
// Custom themes are listed apart: they are a base plus changes, and the
|
|
50
|
+
// public switcher cannot offer them.
|
|
51
|
+
custom: listCustomThemes().map(({id, label, baseTheme, owner}) => ({id, label, baseTheme, owner}))
|
|
42
52
|
}));
|
|
43
53
|
|
|
54
|
+
// The admin applies its theme before anything else loads. A custom admin
|
|
55
|
+
// theme is its base, its class, and the CSS behind that class.
|
|
56
|
+
fastify.get('/theme/admin', {preHandler: [authenticate]}, async () => {
|
|
57
|
+
const cfg = await loadThemeConfig();
|
|
58
|
+
const id = cfg.adminTheme || 'charcoal-dark';
|
|
59
|
+
const {dommaTheme, customClass} = resolveThemeClasses(id, cfg);
|
|
60
|
+
return {theme: id, dommaTheme, customClass, css: customClass ? compileCustomThemeCss([id]) : ''};
|
|
61
|
+
});
|
|
62
|
+
|
|
44
63
|
/*
|
|
45
64
|
* One theme's token values. Fetched on demand rather than folded into
|
|
46
65
|
* /catalog: 32 themes x ~245 tokens is a payload nobody needs on a page
|
|
@@ -62,7 +81,7 @@ export async function themeRoutes(fastify, opts = {}) {
|
|
|
62
81
|
|
|
63
82
|
fastify.post('/theme/compile', canRead, async (request, reply) => {
|
|
64
83
|
const candidate = request.body || {};
|
|
65
|
-
const errors = validateThemeConfig(candidate);
|
|
84
|
+
const errors = validateThemeConfig(candidate, {remembered: rememberedIds(await loadThemeConfig())});
|
|
66
85
|
if (errors.length) return reply.code(400).send({error: errors.join('; '), validation: errors});
|
|
67
86
|
return {css: compileOverrideCss(candidate)};
|
|
68
87
|
});
|
package/server/server.js
CHANGED
|
@@ -280,8 +280,10 @@ try {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
try {
|
|
283
|
-
const {runMigration: runSidebarMigration, ensureSidebarItem, ensureDocumentationSubmenus} = await import('./services/sidebar-migration.js');
|
|
283
|
+
const {runMigration: runSidebarMigration, ensureSidebarItem, ensureDocumentationSubmenus, ensureFolderKeys} = await import('./services/sidebar-migration.js');
|
|
284
284
|
await runSidebarMigration();
|
|
285
|
+
// Stable folder keys - what a Tool's default `folder` names.
|
|
286
|
+
await ensureFolderKeys();
|
|
285
287
|
// Surface admin pages added after first boot on existing installs.
|
|
286
288
|
await ensureSidebarItem({
|
|
287
289
|
groupText: 'System',
|
|
@@ -510,19 +512,19 @@ await app.register(contactsRoutes, {prefix: '/api/contacts'});
|
|
|
510
512
|
// Registered through the same channel plugins use, deliberately. The sidebar
|
|
511
513
|
// menu (config/menus/admin-sidebar.json) is seeded once and file-existence
|
|
512
514
|
// guarded, so adding an entry there would reach new sites only - every existing
|
|
513
|
-
// site would simply never see Notes or Todo. Registered items are
|
|
514
|
-
// render time on every site
|
|
515
|
-
//
|
|
515
|
+
// site would simply never see Notes or Todo. Registered items are placed at
|
|
516
|
+
// render time on every site (placeTools() in admin/js/lib/sidebar-grouping.js):
|
|
517
|
+
// in the Tools folder by default, or wherever an admin has moved them.
|
|
516
518
|
//
|
|
517
519
|
// `permission` is what hides each one from a role that cannot use it; the
|
|
518
520
|
// renderer filters on it.
|
|
519
521
|
const {registerSidebarItem} = await import('./services/hooks.js');
|
|
520
522
|
// `countUrl` is the badge: the sidebar fetches it and shows {count, label, tone}
|
|
521
523
|
// (see fetchCounts in admin/js/lib/sidebar-renderer.js).
|
|
522
|
-
registerSidebarItem({item: {text: 'Notes', url: '#/notes', icon: 'edit-3', permission: 'notes', countUrl: '/api/notes/count'}});
|
|
523
|
-
registerSidebarItem({item: {text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'}});
|
|
524
|
-
registerSidebarItem({item: {text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'}});
|
|
525
|
-
registerSidebarItem({item: {text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'}});
|
|
524
|
+
registerSidebarItem({item: {source: 'Built in', text: 'Notes', url: '#/notes', icon: 'edit-3', permission: 'notes', countUrl: '/api/notes/count'}});
|
|
525
|
+
registerSidebarItem({item: {source: 'Built in', text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'}});
|
|
526
|
+
registerSidebarItem({item: {source: 'Built in', text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'}});
|
|
527
|
+
registerSidebarItem({item: {source: 'Built in', text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'}});
|
|
526
528
|
|
|
527
529
|
await registerPlugins(app);
|
|
528
530
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin home - the screen `#/` (Dashboard) opens.
|
|
3
|
+
*
|
|
4
|
+
* Every admin screen is a Tool, and "Dashboard" is simply whichever Tool home
|
|
5
|
+
* points at. Three sources, first answer wins:
|
|
6
|
+
*
|
|
7
|
+
* 1. A confined role's own home (roles.getAdminScope) - it is also the only
|
|
8
|
+
* place a confined user may land.
|
|
9
|
+
* 2. The site's `adminHome` (config/site.json, System > Site Settings).
|
|
10
|
+
* 3. `/` - the built-in dashboard, or the plugin view that replaces it
|
|
11
|
+
* (`views.dashboard`).
|
|
12
|
+
*
|
|
13
|
+
* The site's choice is skipped when the sidebar does not know that screen (a
|
|
14
|
+
* Tool whose plugin has been disabled) or the user lacks the permission guarding
|
|
15
|
+
* it (its sidebar entry's `permission`), so nobody lands on a dead end. The server stays the authority for data either way;
|
|
16
|
+
* this only decides where the admin opens.
|
|
17
|
+
*/
|
|
18
|
+
import {normaliseAdminPath} from './roles.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* True if `permissions` grants `resource` - the same test the sidebar uses
|
|
22
|
+
* (sidebar-renderer.js canAny), so home never opens what the sidebar hides.
|
|
23
|
+
*/
|
|
24
|
+
function holds(permissions, resource) {
|
|
25
|
+
if (!resource) return true;
|
|
26
|
+
const p = permissions || [];
|
|
27
|
+
return p.includes(resource) || ['read', 'create', 'update', 'delete'].some(a => p.includes(`${resource}.${a}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trimUrl = (url) => (typeof url === 'string' ? url.replace(/\/+$/, '') || '#/' : '');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The path, top down, to the first menu entry whose url is `url` - a Tool
|
|
34
|
+
* placeholder (`ref`) included, since it is where that Tool renders - or null.
|
|
35
|
+
*/
|
|
36
|
+
function pathTo(url, nodes, trail = []) {
|
|
37
|
+
for (const n of nodes || []) {
|
|
38
|
+
if (!n) continue;
|
|
39
|
+
if (trimUrl(n.url) === url) return [...trail, n];
|
|
40
|
+
const hit = pathTo(url, n.items, [...trail, n]);
|
|
41
|
+
if (hit) return hit;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** True if the menu hides `text` the older way: a url-less, childless hidden item of that name. */
|
|
47
|
+
function hiddenByName(text, nodes) {
|
|
48
|
+
const t = String(text || '').toLowerCase();
|
|
49
|
+
for (const n of nodes || []) {
|
|
50
|
+
if (!n) continue;
|
|
51
|
+
if (n.hidden && !n.url && !(n.items || []).length && String(n.text || '').toLowerCase() === t) return true;
|
|
52
|
+
if (hiddenByName(text, n.items)) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {object} o
|
|
59
|
+
* @param {{home: string}|null} o.scope the user's admin scope (confinement)
|
|
60
|
+
* @param {string[]} o.permissions what the user holds
|
|
61
|
+
* @param {unknown} o.siteHome site.json `adminHome`
|
|
62
|
+
* @param {object[]|Function} [o.registered] registered sidebar items (their `.item`s), or a function returning them
|
|
63
|
+
* @param {object[]|Function} [o.menuItems] the admin-sidebar menu's items, or a function returning them
|
|
64
|
+
* @param {(visibility: unknown) => boolean} [o.canSee] the sidebar's visibility test for this user
|
|
65
|
+
* @returns {Promise<string>|string} an admin route path
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveAdminHome({scope, permissions, siteHome, registered = [], menuItems = [], canSee = () => true}) {
|
|
68
|
+
if (scope?.home) return scope.home;
|
|
69
|
+
const home = normaliseAdminPath(siteHome);
|
|
70
|
+
if (!home || home === '/') return '/';
|
|
71
|
+
const url = `#${home}`;
|
|
72
|
+
const list = async (v) => (typeof v === 'function' ? await v() : v) || [];
|
|
73
|
+
// Only a screen the sidebar shows this user: an enabled Tool, or a page in
|
|
74
|
+
// the menu. Where the menu places it decides whether it is shown - a hidden
|
|
75
|
+
// entry, or a hidden or gated folder on the way to it, rules it out even
|
|
76
|
+
// for a registered Tool, exactly as the sidebar leaves it out.
|
|
77
|
+
const tool = (await list(registered)).find(i => i && !i.hidden && trimUrl(i.url) === url);
|
|
78
|
+
const items = await list(menuItems);
|
|
79
|
+
const path = pathTo(url, items);
|
|
80
|
+
if (!tool && (!path || path.at(-1).ref)) return '/'; // nothing enables it
|
|
81
|
+
if (tool && !path && hiddenByName(tool.text, items)) return '/';
|
|
82
|
+
const gates = [...(path || []), ...(tool ? [tool] : [])];
|
|
83
|
+
const open = gates.every(n => !n.hidden
|
|
84
|
+
&& holds(permissions, n.permission)
|
|
85
|
+
&& (n.visibility == null || canSee(n.visibility)));
|
|
86
|
+
return open ? home : '/';
|
|
87
|
+
}
|
package/server/services/hooks.js
CHANGED
|
@@ -71,27 +71,51 @@ export async function registerMenuLocation(slot, def) {
|
|
|
71
71
|
const _registeredSidebarItems = [];
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Register a sidebar
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* Register a Tool's sidebar entry. Entries are placed into the rendered admin
|
|
75
|
+
* sidebar at render time - they are NOT persisted into menu JSON - so plugins
|
|
76
|
+
* re-register on each server boot.
|
|
77
77
|
*
|
|
78
|
-
*
|
|
78
|
+
* `folder` is the Tool's DEFAULT place: the `key` of an admin-sidebar folder
|
|
79
|
+
* (`'tools'`, `'content'`, `'data'`, `'system'`, ...). It defaults to `'tools'`,
|
|
80
|
+
* and an admin who moves the Tool in the menu overrides it - the renderer
|
|
81
|
+
* places by `url` first (see placeTools in admin/js/lib/sidebar-grouping.js).
|
|
82
|
+
* `parent` is the older form: any node matched by its text.
|
|
83
|
+
*
|
|
84
|
+
* @param {{folder?: string, parent?: string, item: {text: string, url: string, icon?: string, permission?: string, folder?: string, items?: object[]}}} opts
|
|
79
85
|
*/
|
|
80
86
|
export function registerSidebarItem(opts) {
|
|
81
87
|
if (!opts || !opts.item || typeof opts.item.text !== 'string') {
|
|
82
88
|
throw new Error('registerSidebarItem: opts.item.text is required');
|
|
83
89
|
}
|
|
90
|
+
const {folder: itemFolder, ...item} = opts.item;
|
|
84
91
|
_registeredSidebarItems.push({
|
|
85
92
|
parent: opts.parent || null,
|
|
86
|
-
|
|
93
|
+
folder: opts.folder || itemFolder || DEFAULT_TOOL_FOLDER,
|
|
94
|
+
item
|
|
87
95
|
});
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
/** Where a Tool's sidebar entry goes when it names no folder. */
|
|
99
|
+
export const DEFAULT_TOOL_FOLDER = 'tools';
|
|
100
|
+
|
|
101
|
+
/** Sidebar URLs taken off the sidebar - a built-in Tool a plugin has taken over. */
|
|
102
|
+
const _hiddenSidebarUrls = new Set();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Leave the item with this URL out of the sidebar. For a core Tool whose entry
|
|
106
|
+
* is registered before the plugins load, so it cannot simply not be registered.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} url - e.g. '#/contacts'
|
|
109
|
+
*/
|
|
110
|
+
export function hideSidebarUrl(url) {
|
|
111
|
+
_hiddenSidebarUrls.add(url);
|
|
112
|
+
}
|
|
113
|
+
|
|
90
114
|
/**
|
|
91
115
|
* @returns {Array<{parent: string|null, item: object}>}
|
|
92
116
|
*/
|
|
93
117
|
export function getRegisteredSidebarItems() {
|
|
94
|
-
return _registeredSidebarItems.
|
|
118
|
+
return _registeredSidebarItems.filter(entry => !_hiddenSidebarUrls.has(entry?.item?.url));
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
// ---------------------------------------------------------------------------
|
package/server/services/menus.js
CHANGED
|
@@ -714,15 +714,22 @@ export function validateMenu(menu) {
|
|
|
714
714
|
return;
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
-
|
|
717
|
+
// A Tool placeholder (`ref`, admin-sidebar) takes its label from the
|
|
718
|
+
// Tool's registration unless the admin gave it one of its own.
|
|
719
|
+
if (item.ref === true && item.url && !item.text) {
|
|
720
|
+
// fine - the url is checked below like any leaf
|
|
721
|
+
} else if (!item.text || typeof item.text !== 'string') {
|
|
718
722
|
errors.push(`Item ${path}: text is required`);
|
|
719
723
|
}
|
|
720
724
|
// url is only required for LEAF items (no children). Folder items
|
|
721
725
|
// - those with their own `items` array - are pure groupings and
|
|
722
726
|
// don't need a navigation target. The admin sidebar uses this
|
|
723
727
|
// shape extensively (Content / Data / System headings etc.).
|
|
728
|
+
// A keyed item is an admin-sidebar folder (see sidebar-migration.js)
|
|
729
|
+
// even while empty - the Tools folder is, until a Tool is enabled.
|
|
724
730
|
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
|
725
|
-
|
|
731
|
+
const isKeyedFolder = typeof item.key === 'string' && item.key !== '' && !item.url;
|
|
732
|
+
if (!hasChildren && !isKeyedFolder) {
|
|
726
733
|
if (!item.url || typeof item.url !== 'string' || !URL_PREFIX_RE.test(item.url)) {
|
|
727
734
|
errors.push(`Item ${path}: url must start with /, #, http(s)://, or mailto:`);
|
|
728
735
|
}
|
|
@@ -1082,15 +1089,38 @@ export async function resolveOverlaysForPage(ctx = {}, user = null) {
|
|
|
1082
1089
|
.sort((a, b) => String(a.slug).localeCompare(String(b.slug)));
|
|
1083
1090
|
}
|
|
1084
1091
|
|
|
1085
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* The admin sidebar's stand-in for an item this user may not see: only its
|
|
1094
|
+
* url (or folder key) and `hidden`, so Tool placement still knows the admin put
|
|
1095
|
+
* a Tool there - it must not fall back into Tools for exactly the users it was
|
|
1096
|
+
* restricted from - while the label never reaches them.
|
|
1097
|
+
*/
|
|
1098
|
+
function restrictedStub(item) {
|
|
1099
|
+
const children = Array.isArray(item.items) ? item.items.map(restrictedStub).filter(Boolean) : [];
|
|
1100
|
+
if (!item.url && !item.key && !children.length) return null;
|
|
1101
|
+
return {
|
|
1102
|
+
...(item.url && {url: item.url}),
|
|
1103
|
+
...(item.key && {key: item.key}),
|
|
1104
|
+
hidden: true,
|
|
1105
|
+
// Marks a role gate, as opposed to an admin's hide, for ensureLockedTools.
|
|
1106
|
+
restricted: true,
|
|
1107
|
+
...(children.length && {items: children})
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export function filterItemsForUser(items, user, {keepHidden = false} = {}) {
|
|
1086
1112
|
const out = [];
|
|
1087
1113
|
for (const item of items) {
|
|
1088
|
-
if (item.hidden) continue;
|
|
1114
|
+
if (item.hidden && !keepHidden) continue;
|
|
1089
1115
|
if (item.visibility != null) {
|
|
1090
|
-
if (!checkVisibility(user, item.visibility))
|
|
1116
|
+
if (!checkVisibility(user, item.visibility)) {
|
|
1117
|
+
const stub = keepHidden ? restrictedStub(item) : null;
|
|
1118
|
+
if (stub) out.push(stub);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1091
1121
|
}
|
|
1092
1122
|
const children = Array.isArray(item.items)
|
|
1093
|
-
? filterItemsForUser(item.items, user)
|
|
1123
|
+
? filterItemsForUser(item.items, user, {keepHidden})
|
|
1094
1124
|
: [];
|
|
1095
1125
|
out.push({...item, items: children});
|
|
1096
1126
|
}
|