domma-cms 1.6.5 → 1.6.7

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/public/js/site.js CHANGED
@@ -1 +1 @@
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},i=function(o){return e(o).replace(/'/g,"&#39;")},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()})})();
1
+ import{decodeBase64Json as N}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as B,applyColour as j,applyPill as G,applyFloat as Y,applyHighlight as O,applyItemHighlight as U,itemHighlightAttrs as K,isCurrentItem as R,menuAlign as Q,applyClasses as I,cssClasses as P}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as Z}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as V,markInvalidFields as W,showValidationPopup as J}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as ee}from"/public/js/form-steps.mjs";import{applyFormClasses as X,applySubmitButtons as te,applyWizardButtons as ae}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},o=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;o(s()),setInterval(()=>o(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},o=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-${Q(x)||"centre"}`),i&&_.get(0).classList.add("dm-nav-vertical",x.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),O(p,x),I(p,x.class),e&&(Y(_.get(0),x),Z(_.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")},a=()=>{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")?a():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 a=document.createElement("li"),d=["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md";a.className=`dm-menu-spacer dm-menu-spacer--${d}`,a.setAttribute("aria-hidden","true"),m.insertBefore(a,c[g]||null);continue}g++}}const b=o(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,a=r&&Array.isArray(g.items)?g.items[parseInt(c.dataset.subindex,10)]:g;if(!a)return;a.colour&&j(c,a.colour),a.pill&&!r&&G(c,a.pill),U(c,a),I(c.closest("li"),a.class);const d=B(a.badge);d&&c.appendChild(d),R(a.url,location.pathname)&&(c.classList.add("active"),c.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let e=function(a){return String(a??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},i=function(a){return e(a).replace(/'/g,"&#39;")},p=function(a){if(a&&a.type==="separator")return'<li class="footer-sep"><hr></li>';if(a&&a.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(a.size)?a.size:"md"}" aria-hidden="true"></li>`;const d=i(a.url||"#"),l=e(a.text||""),w=Array.isArray(a.items)&&a.items.length?`<ul>${a.items.map(p).join("")}</ul>`:"",v=K(a),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=P(a.class).join(" ");return`<li${k?` class="${i(k)}"`:""}><a href="${d}"${D}${M}>${l}</a>${w}</li>`},y=function(a,d){return(a||[]).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 o=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(o.copyright&&(s+=`<p>${e(o.copyright)}</p>`),o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${o.primary.map(p).join("")}</ul></nav>`),o.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${o.legal.map(p).join("")}</ul></nav>`),f.footer){const a=Object.keys(t).filter(l=>n[l]),d=P(f.footer.socialClass).join(" ");a.length&&(s+='<div class="footer-social">',a.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(o.primary,[]),...y(o.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((a,d)=>{const l=u[d];if(!l)return;l.colour&&j(a,l.colour);const w=B(l.badge);w&&a.appendChild(w),R(l.url,location.pathname)&&(a.classList.add("is-active"),a.setAttribute("aria-current","page"))}),O(C.get(0).querySelector(".footer-nav--primary"),{highlight:o.primaryHighlight}),O(C.get(0).querySelector(".footer-nav--legal"),{highlight:o.legalHighlight}),I(C.get(0).querySelector(".footer-nav--primary"),o.primaryClass),I(C.get(0).querySelector(".footer-nav--legal"),o.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 a=document.createElement("input");a.type="checkbox",a.className="form-switch-input",a.id="dm-motion-switch",a.checked=g,a.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(a),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(),o=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=o?new Date(+o[1],+o[2]-1,+o[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(o=>{const e={title:o.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:o.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return o.dataset.date&&(e.date=o.dataset.date),o.dataset.status&&(e.status=o.dataset.status),o.dataset.entryId&&(e._entryId=o.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",o=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 ${o}ms ease, transform ${o}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=N(t.dataset.config);Object.assign(n,s)}catch{}}),Object.keys(n).length>0){const t={};for(const[s,o]of Object.entries(n)){const e=o?.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]={...o,events:{...o.events,click:m}})):t[s]=o}$.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 o=await s.json().catch(()=>({}));if(!s.ok)throw new Error(o.error||o.message||`Error ${s.status}`);E.toast(o.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{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 o=document.createElement("a");o.href="/login",o.textContent="sign in",s.appendChild(o),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=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=_,o=await t(s);if(o.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,o=await t(s)}}}if(!o.ok){const{error:i}=await o.json().catch(()=>({}));h.replaceChildren(C(i||"Unable to load entries.",!1));return}const{html:e}=await o.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=N(_.dataset.payload)}catch{return}const{columns:A,rows:h,search:n,sortable:t,exportable:s,pageSize:o,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 a=document.createElement("span");a.dataset.icon=i.icon,r.appendChild(a),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:o,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:o,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 o={};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)),o[e.name]=u}),o}function A(t){const s={};return(t||[]).forEach(o=>{if(!(!o.name||o.type==="page-break"||o.type==="spacer")&&(o.type==="select"||o.type==="multiselect")&&o.required){const e=(o.options||[])[0];e&&(s[o.name]=typeof e=="object"?e.value:e)}}),s}function h(t,s){(s||[]).forEach(o=>{if(o.type!=="date"||!o.name)return;const e=t.querySelector(`[name="${o.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(t,s,o){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=o?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}f.forEach(t=>{let s;try{s=N(t.dataset.formInline)}catch{return}const o=s.fields||[],e=s.settings||{},i=Number(e.columns)===3?3:2,p=e.layout||"grid",y=o.some(r=>r.type==="page-break"),u=async r=>{if(t.dataset.triggerBlocked==="true"){const a=t._formLogicRuntime,d=a&&a.submitBlock?a.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 a=t.querySelector(".fb-form-honeypot"),d=a?.querySelector('input[type="text"]')||null,l=a?.querySelector('[name="_t"]')||null,w=Object.assign({},r);delete w._t,a?.dataset.hpName&&delete w[a.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(a){throw a.formSubmitFailed||n(t,a.message||"Submission failed. Please try again.",!0),a}},m=Date.now();function b(r){const a=r.querySelector("form");if(!a||a.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=a.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),a.appendChild(d)}function c(r){if(!window.FormLogicEngine||!o.some(d=>d.logic||Array.isArray(d.triggers)&&d.triggers.length))return null;const a=new window.FormLogicEngine.FormLogicRuntime(s,t,r?{baseValues:r}:void 0);if(a.init(),t._formLogicRuntime=a,t._formLogicObserved)return a;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 a}let g=!1;if(t.addEventListener("dm-form-end",r=>{const a=r.detail?.validation;if(a&&!a.valid){W(t,a.missing),J(a);return}g||(g=!0,u(r.detail?.values||{}).catch(()=>{}).finally(()=>{g=!1}))}),y&&F.wizard){const r=ee(o,s.title),a=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 z=L.validateFields(t,d[l]||[]);z.valid||(k.preventDefault(),k.stopImmediatePropagation(),W(t,z.missing),J(z))},M=()=>{const k=d[l]||o;ae(t,e),X(t,e,k),h(t,k),V(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:a},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>=a.length||L!==l&&v.goToStep(L)}),Promise.resolve(v).then(M)}else if(F.render){const r=F.render(t,C(o,i),A(o),{submitText:e.submitText||"Submit",layout:p,columns:i,onSubmit:u});Promise.resolve(r).then(function(){te(t,e,p),X(t,e,o),h(t,o),V(t,o),e.honeypot!==!1&&b(t),c()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
@@ -0,0 +1 @@
1
+ export function decodeBase64Json(e){const t=Uint8Array.from(atob(e),o=>o.charCodeAt(0));return JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(t))}
@@ -29,6 +29,7 @@
29
29
  * in step with its schema. Core has no need: seedAll() rewrites every preset's
30
30
  * schema.json on each boot, which is the same job done earlier.
31
31
  */
32
+ import {registerToolTrash, trashToolEntry} from '../../services/tool-trash.js';
32
33
  import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
33
34
  import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
34
35
  import {hooks} from '../../services/hooks.js';
@@ -248,6 +249,8 @@ export async function contactsRoutes(fastify, opts = {}) {
248
249
  const requirePermission = opts.requirePermission || defaultRequirePermission;
249
250
  const holdsManageGroups = opts.holdsManageGroups || defaultHoldsManageGroups;
250
251
 
252
+ registerToolTrash(fastify, {slug: CONTACTS_SLUG, resource: 'contacts', itemPath: '/contacts', authenticate, requirePermission, project: toContact, onPurge: entry => hooks.emit('contact:deleted', {id: entry.id, userId: entry.data.userId})});
253
+
251
254
  function userId(request) {
252
255
  return request.user?.id ?? request.user?.sub ?? null;
253
256
  }
@@ -384,18 +387,14 @@ export async function contactsRoutes(fastify, opts = {}) {
384
387
  return reply.code(400).send({ error: 'ids array is required' });
385
388
  }
386
389
 
387
- let deleted = 0;
388
- for (const id of ids) {
389
- const entry = await getEntry(CONTACTS_SLUG, id).catch(() => null);
390
- if (!entry) continue;
391
- if (uid && entry.data.userId !== uid) continue;
392
- await deleteEntry(CONTACTS_SLUG, id).catch(() => {
393
- });
394
- hooks.emit('contact:deleted', {id, userId: entry.data.userId ?? null});
395
- deleted++;
390
+ const deletedIds = [], failedIds = [];
391
+ for (const id of [...new Set(ids)]) {
392
+ const entry = await getEntry(CONTACTS_SLUG, id);
393
+ if (!entry || !uid || entry.data.userId !== uid) { failedIds.push(id); continue; }
394
+ try { await trashToolEntry(CONTACTS_SLUG, entry); deletedIds.push(id); }
395
+ catch { failedIds.push(id); }
396
396
  }
397
-
398
- return reply.send({ deleted });
397
+ return reply.send({deleted: deletedIds.length, deletedIds, failedIds});
399
398
  });
400
399
 
401
400
  /** DELETE /api/contacts/contacts/:id */
@@ -407,9 +406,8 @@ export async function contactsRoutes(fastify, opts = {}) {
407
406
  if (!entry) return reply.code(404).send({error: 'Contact not found'});
408
407
  if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Contact not found'});
409
408
 
410
- await deleteEntry(CONTACTS_SLUG, id);
411
- // Anything pointing at it (Calendar's event links) can let go now (0.79).
412
- hooks.emit('contact:deleted', {id, userId: entry.data.userId ?? null});
409
+ await trashToolEntry(CONTACTS_SLUG, entry);
410
+ // Keep related links and Pro fields for restore.
413
411
  return reply.send({ ok: true });
414
412
  });
415
413
 
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Notes API - /api/notes
3
3
  *
4
- * Was the `notes` plugin until 0.67.0. The logic is unchanged; what moved is
5
- * where it lives and how it is gated.
4
+ * Was the `notes` plugin until 0.67.0. Personal entries now use recoverable Trash; normal reads omit trashed rows.
6
5
  *
7
6
  * **Per-user scope.** Entries carry a `userId` and every read is filtered to the
8
7
  * caller's own. The plugin made this configurable (`scope: 'user' | 'site'`) and
@@ -15,9 +14,10 @@
15
14
  * `setupPlugin()` read `schema.slug`, so the live collection has always been
16
15
  * called this, and existing entries are found only if it stays exactly so.
17
16
  */
17
+ import {registerToolTrash, trashToolEntry} from '../../services/tool-trash.js';
18
18
  import {ago, clip, plural} from '../../services/sidebarBadges.js';
19
19
  import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
20
- import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
20
+ import {createEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
21
21
  import {hooks} from '../../services/hooks.js';
22
22
 
23
23
  const SLUG = 'notes-notes';
@@ -45,6 +45,8 @@ export async function notesRoutes(fastify, opts = {}) {
45
45
  const requirePermission = opts.requirePermission || defaultRequirePermission;
46
46
 
47
47
 
48
+ registerToolTrash(fastify, {slug: SLUG, resource: 'notes', itemPath: '/items', authenticate, requirePermission, project: toNote, onPurge: entry => hooks.emit('note:deleted', {id: entry.id, userId: entry.data.userId})});
49
+
48
50
  function userId(request) {
49
51
  return request.user?.id ?? request.user?.sub ?? null;
50
52
  }
@@ -149,9 +151,8 @@ export async function notesRoutes(fastify, opts = {}) {
149
151
  if (!entry) return reply.code(404).send({error: 'Note not found'});
150
152
  if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Note not found'});
151
153
 
152
- await deleteEntry(SLUG, id);
153
- // Anything pointing at it (Calendar's event links) can let go now (0.79).
154
- hooks.emit('note:deleted', {id, userId: entry.data.userId ?? null});
154
+ await trashToolEntry(SLUG, entry);
155
+ // Related links remain available if this note is restored.
155
156
  return reply.send({ success: true });
156
157
  });
157
158
 
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Todo API - /api/todo
3
3
  *
4
- * Was the `todo` plugin until 0.67.0. The logic is unchanged; what moved is
5
- * where it lives and how it is gated.
4
+ * Was the `todo` plugin until 0.67.0. Personal entries now use recoverable Trash; normal reads omit trashed rows.
6
5
  *
7
6
  * **Per-user scope.** Entries carry a `userId` and every read is filtered to the
8
7
  * caller's own. The plugin made this configurable (`scope: 'user' | 'site'`) and
@@ -15,9 +14,10 @@
15
14
  * `setupPlugin()` read `schema.slug`, so the live collection has always been
16
15
  * called this, and existing entries are found only if it stays exactly so.
17
16
  */
17
+ import {registerToolTrash, trashToolEntry} from '../../services/tool-trash.js';
18
18
  import {clip, plural} from '../../services/sidebarBadges.js';
19
19
  import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
20
- import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
20
+ import {createEntry, getEntry, listEntries, updateEntry} from '../../services/collections.js';
21
21
  import {registerCalendarSource} from '../../services/hooks.js';
22
22
  import {getPermissionsFor} from '../../services/roles.js';
23
23
 
@@ -83,6 +83,8 @@ export async function todoRoutes(fastify, opts = {}) {
83
83
  const requirePermission = opts.requirePermission || defaultRequirePermission;
84
84
 
85
85
 
86
+ registerToolTrash(fastify, {slug: SLUG, resource: 'todo', itemPath: '/items', authenticate, requirePermission, project: toTodo, onChange: tell});
87
+
86
88
  function userId(request) {
87
89
  return request.user?.id ?? request.user?.sub ?? null;
88
90
  }
@@ -213,7 +215,7 @@ export async function todoRoutes(fastify, opts = {}) {
213
215
  const uid = userId(request);
214
216
  const all = await loadAll(uid);
215
217
  const completed = all.filter(t => t.status === 'completed');
216
- await Promise.all(completed.map(t => deleteEntry(SLUG, t.id)));
218
+ await Promise.all(completed.map(async t => trashToolEntry(SLUG, await getEntry(SLUG, t.id))));
217
219
  if (completed.length) tell(uid);
218
220
  return reply.send({success: true, removed: completed.length});
219
221
  });
@@ -227,7 +229,7 @@ export async function todoRoutes(fastify, opts = {}) {
227
229
  if (!entry) return reply.code(404).send({error: 'Todo not found'});
228
230
  if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Todo not found'});
229
231
 
230
- await deleteEntry(SLUG, id);
232
+ await trashToolEntry(SLUG, entry);
231
233
  tell(entry.data.userId);
232
234
  return reply.send({ success: true });
233
235
  });
@@ -16,6 +16,7 @@ import {typedEntryData} from './entryValues.js';
16
16
  import {toCsv} from '../../public/js/collection-export.mjs';
17
17
 
18
18
  const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
19
+ const TOOL_TRASH_SLUGS = new Set(['notes-notes', 'todo-items', 'contacts-contacts']);
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // Internal helpers (schema only)
@@ -352,6 +353,7 @@ export async function validateReferences(schema, data) {
352
353
  * @param {string} [opts.search] - Substring match across all fields
353
354
  * @param {Record<string, unknown>} [opts.filter] - Structured filter; see filterEngine.js
354
355
  * @param {boolean} [opts.resolveRefs=false] - Batch-resolve reference fields to display labels
356
+ * @param {boolean} [opts.includeToolTrash=false] - Internal recovery/maintenance reads only
355
357
  * @returns {Promise<{ entries: object[], total: number, page: number, limit: number }>}
356
358
  * @throws {Error} If collection slug does not exist
357
359
  */
@@ -359,6 +361,11 @@ export async function listEntries(slug, opts = {}) {
359
361
  const schema = await getCollection(slug);
360
362
  if (!schema) throw new Error(`Collection "${slug}" not found`);
361
363
  const adapter = await getAdapter(slug);
364
+ // Personal Tool trash is retained for recovery, but must stay out of
365
+ // normal lists, badges, notifications and contact-sharing integrations.
366
+ if (TOOL_TRASH_SLUGS.has(slug) && !opts.includeToolTrash) {
367
+ opts = {...opts, filter: {...opts.filter, _toolTrashedAt_exists: false}};
368
+ }
362
369
  const result = await adapter.list(slug, opts);
363
370
  if (opts.resolveRefs) {
364
371
  const {resolveReferences} = await import('./references.js');
@@ -372,11 +379,13 @@ export async function listEntries(slug, opts = {}) {
372
379
  *
373
380
  * @param {string} slug
374
381
  * @param {string} entryId
382
+ * @param {{includeToolTrash?: boolean}} [opts] - Internal recovery/maintenance reads only
375
383
  * @returns {Promise<object|null>}
376
384
  */
377
- export async function getEntry(slug, entryId) {
385
+ export async function getEntry(slug, entryId, opts = {}) {
378
386
  const adapter = await getAdapter(slug);
379
- return adapter.get(slug, entryId);
387
+ const entry = await adapter.get(slug, entryId);
388
+ return TOOL_TRASH_SLUGS.has(slug) && entry?.data?._toolTrashedAt && !opts.includeToolTrash ? null : entry;
380
389
  }
381
390
 
382
391
  /**
@@ -1819,17 +1819,17 @@ function processEffectsBlocks(markdown) {
1819
1819
 
1820
1820
  apply('firework', (attrStr, body) => {
1821
1821
  const attrs = parseShortcodeAttrs(attrStr);
1822
+ const flag = name => attrs[name] === true || attrs[name] === 'true';
1822
1823
  const classes = ['firework'];
1823
- if (attrs.type) classes.push(`firework-${attrs.type}`);
1824
- if (attrs.colour) classes.push(`firework-${attrs.colour}`);
1825
- if (attrs.size) classes.push(`firework-${attrs.size}`);
1826
- if (attrs.continuous === 'true') classes.push('firework-continuous');
1827
- if (attrs.hover === 'true') classes.push('firework-on-hover');
1828
- if (body === null) return `<div class="${classes.join(' ')}"></div>`;
1829
- // Restore code placeholders before the nested parse (tabs-handler
1830
- // pattern) so code spans/fences render as <code>, not raw backticks.
1831
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1832
- return `<div class="${classes.join(' ')}">${innerHtml}</div>\n`;
1824
+ for (const name of ['type', 'colour', 'size']) if (attrs[name]) classes.push(`firework-${escapeAttr(attrs[name])}`);
1825
+ if (flag('continuous')) classes.push('firework-continuous');
1826
+ const hover = flag('hover');
1827
+ if (hover) classes.push('firework-on-hover');
1828
+ // Native hover fireworks need a hit target; the base decoration ignores pointer events.
1829
+ const style = hover ? ' style="pointer-events:auto;--dm-fw-size:var(--dm-fw-particle-size)"' : '';
1830
+ const particles = hover ? '' : '<div class="firework-launch"></div><div class="firework-particles"></div>';
1831
+ const innerHtml = body === null ? '' : marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1832
+ return `<div class="${classes.join(' ')}"${style}>${particles}${innerHtml}</div>\n`;
1833
1833
  });
1834
1834
 
1835
1835
  apply('fireworks', (_attrStr, body) => {
@@ -367,7 +367,7 @@ export async function renderPage(page, opts = {}) {
367
367
  ? `window.__CMS_CTXMENUS__ = ${JSON.stringify(ctxMenus).replace(/<\/script>/gi, '<\\/script>')};`
368
368
  : '';
369
369
  const ctxMenusModule = ctxMenus.length
370
- ? '<script src="/public/js/context-menus.js?v=20261009-classes" type="module"></script>'
370
+ ? '<script src="/public/js/context-menus.js?v=20261010-utf8-payloads" type="module"></script>'
371
371
  : '';
372
372
 
373
373
  const vars = {
@@ -1081,7 +1081,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
1081
1081
  ? `window.__CMS_CTXMENUS__ = ${JSON.stringify(ctxMenus).replace(/<\/script>/gi, '<\\/script>')};`
1082
1082
  : '';
1083
1083
  const ctxMenusModule = ctxMenus.length
1084
- ? '<script src="/public/js/context-menus.js?v=20261009-classes" type="module"></script>'
1084
+ ? '<script src="/public/js/context-menus.js?v=20261010-utf8-payloads" type="module"></script>'
1085
1085
  : '';
1086
1086
 
1087
1087
  const vars = {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Runs once on server boot. Idempotent - guarded by the existence of
5
5
  * config/menus/admin-sidebar.json. Seeds the admin sidebar menu with the
6
- * standard tree (Overview, Projects, Content, Data, Tools, System,
6
+ * standard tree (Dashboards, Projects, Content, Data, Tools, System,
7
7
  * Documentation) and patches config/menu-locations.json to map the
8
8
  * `admin-sidebar` slot.
9
9
  *
@@ -21,7 +21,7 @@ const DEFAULT_CONFIG_DIR = path.resolve(path.dirname(__filename), '..', '..', 'c
21
21
 
22
22
  const SEED_ITEMS = [
23
23
  {
24
- text: 'Overview', key: 'overview', icon: 'home',
24
+ text: 'Dashboards', key: 'overview', icon: 'home',
25
25
  items: [
26
26
  {text: 'Dashboard', url: '#/', icon: 'home'}
27
27
  ]
@@ -319,13 +319,14 @@ export async function ensureDocumentationSubmenus(opts = {}) {
319
319
 
320
320
  /** Keys for the folders the seed has always created, matched by their original name. */
321
321
  const SEED_FOLDER_KEYS = {
322
- overview: 'overview', projects: 'projects', content: 'content', data: 'data',
322
+ overview: 'overview', dashboards: 'overview', projects: 'projects', content: 'content', data: 'data',
323
323
  tools: 'tools', system: 'system', documentation: 'documentation'
324
324
  };
325
325
 
326
326
  /**
327
327
  * Give an existing install's top-level folders their stable `key`, and - once -
328
- * a persisted Tools folder for enabled Tools to be placed into.
328
+ * a persisted Tools folder for enabled Tools to be placed into, and - once -
329
+ * the name Dashboards for the folder that was Overview.
329
330
  *
330
331
  * - A folder is keyed only if it has no key yet and still carries one of the
331
332
  * seed's names; a folder the admin made, or renamed before keys existed, is
@@ -361,6 +362,16 @@ export async function ensureFolderKeys(opts = {}) {
361
362
  changed = true;
362
363
  }
363
364
 
365
+ // The first folder was "Overview" until 1.6.6. Renamed once (`meta.dashboardsFolder`)
366
+ // and only while it still says Overview, so a name an admin gave it - even
367
+ // Overview again, later - stays. Its key is still `overview`.
368
+ if (!menu.meta?.dashboardsFolder && items.length) {
369
+ const first = items.find(n => isFolder(n) && n.key === 'overview');
370
+ if (first && String(first.text || '').trim() === 'Overview') first.text = 'Dashboards';
371
+ menu.meta = {...(menu.meta || {}), dashboardsFolder: true};
372
+ changed = true;
373
+ }
374
+
364
375
  // An empty menu is left empty: the renderer's minimal fallback (Dashboard,
365
376
  // Menus) is what an admin needs to repair it, and a lone Tools folder would
366
377
  // stop that fallback from applying.
@@ -0,0 +1,39 @@
1
+ import {deleteEntry, getEntry, listEntries, updateEntry} from './collections.js';
2
+
3
+ /** Keep the original row (including Pro fields and related IDs) until purged. */
4
+ export async function trashToolEntry(slug, entry) {
5
+ return updateEntry(slug, entry.id, {...entry.data, _toolTrashedAt: new Date().toISOString()});
6
+ }
7
+
8
+ /** Routes remain owner-scoped, even for administrators. Restore needs update. */
9
+ export function registerToolTrash(fastify, {slug, resource, itemPath, authenticate, requirePermission, project, onChange, onPurge}) {
10
+ const uid = request => request.user?.id ?? request.user?.sub ?? null;
11
+ const may = action => ({preHandler: [authenticate, requirePermission(resource, action)]});
12
+ const own = async request => {
13
+ const entry = await getEntry(slug, request.params.id, {includeToolTrash: true});
14
+ return uid(request) && entry?.data.userId === uid(request) && entry.data._toolTrashedAt ? entry : null;
15
+ };
16
+ fastify.get('/trash', may('read'), async request => {
17
+ if (!uid(request)) return [];
18
+ const {entries} = await listEntries(slug, {limit: 100000, includeToolTrash: true});
19
+ return entries.filter(e => e.data.userId === uid(request) && e.data._toolTrashedAt)
20
+ .sort((a, b) => String(b.data._toolTrashedAt).localeCompare(String(a.data._toolTrashedAt)))
21
+ .map(e => ({...project(e), trashedAt: e.data._toolTrashedAt}));
22
+ });
23
+ fastify.post(`${itemPath}/:id/restore`, may('update'), async (request, reply) => {
24
+ const entry = await own(request);
25
+ if (!entry) return reply.code(404).send({error: 'Item not found in Trash'});
26
+ const data = {...entry.data};
27
+ delete data._toolTrashedAt;
28
+ const restored = await updateEntry(slug, entry.id, data);
29
+ await onChange?.(uid(request));
30
+ return project(restored);
31
+ });
32
+ fastify.delete('/trash/:id', may('delete'), async (request, reply) => {
33
+ const entry = await own(request);
34
+ if (!entry) return reply.code(404).send({error: 'Item not found in Trash'});
35
+ await deleteEntry(slug, entry.id);
36
+ await onPurge?.(entry);
37
+ return {success: true};
38
+ });
39
+ }
@@ -19,7 +19,7 @@
19
19
  {{#if fontLink}}{{fontLink}}{{/if}}
20
20
 
21
21
  <!-- DommaJS CSS -->
22
- <link rel="stylesheet" href="/dist/domma/domma.css">
22
+ <link rel="stylesheet" href="/dist/domma/domma.css?v=20261010-fireworks">
23
23
  <link rel="stylesheet" href="/dist/domma/grid.css">
24
24
  <link rel="stylesheet" href="/dist/domma/elements.css">
25
25
  <link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
@@ -137,8 +137,8 @@
137
137
  </script>
138
138
 
139
139
  <!-- Site initialisation -->
140
- <script src="/public/js/site.js?v=20261009-more-classes" type="module"></script>
141
- <script src="/public/js/collection-context.js?v=20260927-ctx" type="module"></script>
140
+ <script src="/public/js/site.js?v=20261010-utf8-payloads" type="module"></script>
141
+ <script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
142
142
  {{ctxMenusModule}}
143
143
 
144
144
  <!-- Floating menu panels: collapse to a hamburger, drag to move, resize.
@@ -155,7 +155,7 @@
155
155
  <script src="/public/js/forms.js?v=20261009-form-classes" type="module"></script>
156
156
 
157
157
  <!-- Core effects runtime -->
158
- <script src="/public/js/effects.js?v=20261003-characters"></script>
158
+ <script src="/public/js/effects.js?v=20261010-motion-demos"></script>
159
159
 
160
160
  <!-- Plugin body-end injection -->
161
161
  {{bodyEndInject}}