domma-cms 0.36.19 → 0.38.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.
@@ -1,4 +1,4 @@
1
- import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){const m=A(),f=E.loader(o.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=r=>{T.create("#users-table",{data:r,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return g+l}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(l=>[l.data?.name,l.data?.level??99]));return(i[m?.role]??99)<(i[t.role]??99)?`
1
+ import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){const m=A(),f=E.loader(o.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=r=>{T.create("#users-table",{data:r,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return g+l}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(l=>[l.data?.name,l.data?.level??99]));return!((i[m?.role]??99)<(i[t.role]??99))&&!a?"":`
2
2
  <a href="#/users/edit/${t.id}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
3
  ${a?"":`<button class="btn btn-sm btn-ghost btn-delete-user" data-id="${t.id}" data-name="${v(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
4
- `:""}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(o.get(0)),o.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const r=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==r),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(o){return String(o).replace(/"/g,"&quot;")}
4
+ `}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(o.get(0)),o.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const r=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==r),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(o){return String(o).replace(/"/g,"&quot;")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.36.19",
3
+ "version": "0.38.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";const _=document.createElement("style");_.textContent=["[data-ctx], [data-collection-table] { cursor: context-menu; }","[data-ctx] a, [data-ctx] button, [data-collection-table] a, [data-collection-table] button,","[data-ctx] input, [data-collection-table] input { cursor: pointer; }",'[data-ctx] input[type="text"], [data-collection-table] input[type="text"] { cursor: text; }',".dm-ctx-hidden { display: none !important; }",".dm-ctx-menu { position: fixed; z-index: 10000; min-width: 200px; max-width: 300px; max-height: 60vh; overflow-y: auto;background: var(--dm-surface, #1c1c30); border: 1px solid var(--dm-border, #2a2a40); border-radius: 6px;box-shadow: var(--dm-shadow-lg, 0 8px 24px rgba(0,0,0,.4)); padding: .3rem; font-size: .83rem;color: var(--dm-text, #e5e5e5); }",".dm-ctx-menu button.dm-ctx-row { display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left;background: none; border: none; color: inherit; font: inherit; padding: .35rem .6rem; border-radius: 4px; cursor: pointer; }",".dm-ctx-menu button.dm-ctx-row:hover { background: var(--dm-surface-2, rgba(255,255,255,.06)); }",".dm-ctx-heading { padding: .3rem .6rem; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;color: var(--dm-text-muted, #9a9aac); }",".dm-ctx-input { width: 100%; font-size: .8rem; padding: .3rem .5rem; border-radius: 4px;border: 1px solid var(--dm-border, #2a2a40); background: var(--dm-surface-2, rgba(255,255,255,.04)); color: inherit; cursor: text; }",".dm-ctx-chips { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: 0 0 .6rem; }",".dm-ctx-chip { display: inline-flex; align-items: center; gap: .35rem; font-size: .76rem; padding: .2rem .6rem;border-radius: 999px; background: var(--dm-surface-2, rgba(255,255,255,.08)); border: 1px solid var(--dm-border, #2a2a40); }",".dm-ctx-chip button { background: none; border: none; cursor: pointer; color: inherit; font-size: .9rem; line-height: 1; padding: 0; }",".dm-ctx-empty-note { padding: .75rem; font-size: .85rem; color: var(--dm-text-muted, #9a9aac); }"].join(`
2
+ `),document.head.appendChild(_);function v(t){if(t._dmTable)return{fields:t._dmTable.columns.filter(n=>n.key!=="_cta").map(n=>({name:n.key,label:n.title||n.key})),entries:t._dmTable.rows.map((n,e)=>({id:String(e),data:n}))};if(t._dmCtxModel)return t._dmCtxModel;try{t._dmCtxModel=JSON.parse(atob(t.dataset.ctx))}catch{t._dmCtxModel=null}return t._dmCtxModel}function M(t,n){if(n.field==="*"){const d=String(n.value).toLowerCase();return Object.values(t.data||{}).some(o=>String(o??"").toLowerCase().includes(d))}const e=String(t.data?.[n.field]??"");switch(n.op){case"eq":return e===String(n.value);case"ne":return e!==String(n.value);case"contains":return e.toLowerCase().includes(String(n.value).toLowerCase());case"gt":case"lt":{const d=parseFloat(e),o=parseFloat(n.value);if(!Number.isNaN(d)&&!Number.isNaN(o))return n.op==="gt"?d>o:d<o;const l=Date.parse(e),a=Date.parse(n.value);return!Number.isNaN(l)&&!Number.isNaN(a)?n.op==="gt"?l>a:l<a:n.op==="gt"?e>String(n.value):e<String(n.value)}default:return!0}}function g(t){const n=t._dmCtxFilters||[],e=v(t);if(!e)return;const d=e.entries.filter(l=>n.every(a=>M(l,a))),o=new Set(d.map(l=>l.id));t._dmTable?t._dmTable.render(d.map(l=>l.data)):t._dmCtx&&t._dmCtx.instance&&t._dmCtx.instance.setItems?t._dmCtx.instance.setItems(t._dmCtx.items.filter(l=>o.has(String(l._entryId)))):t._dmCtxSlides?$(t,o):t.querySelectorAll("[data-entry-id]").forEach(l=>{l.classList.toggle("dm-ctx-hidden",!o.has(l.dataset.entryId))}),T(t,n),k(t,d.length,n.length)}function $(t,n){if(t._dmCarousel&&typeof t._dmCarousel.destroy=="function")try{t._dmCarousel.destroy()}catch{}const e=document.createElement("div");e.className="carousel-track",t._dmCtxSlides.forEach(d=>{n.has(d.dataset.entryId)&&e.appendChild(d.cloneNode(!0))}),t.replaceChildren(e),e.children.length&&typeof Domma<"u"&&Domma.elements?.carousel&&(t._dmCarousel=Domma.elements.carousel(t,t._dmCarouselOpts||{}))}const D={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"};function T(t,n){let e=t._dmCtxBar;if(!n.length){e&&(e.remove(),t._dmCtxBar=null);return}e||(e=document.createElement("div"),e.className="dm-ctx-chips",t.parentNode.insertBefore(e,t),t._dmCtxBar=e),e.replaceChildren(),n.forEach((o,l)=>{const a=document.createElement("span");a.className="dm-ctx-chip";const s=String(o.value),m=s.length>24?s.slice(0,24)+"\u2026":s;a.appendChild(document.createTextNode(o.field==="*"?`search \u201C${m}\u201D`:`${o.label||o.field} ${D[o.op]||o.op} \u201C${m}\u201D`));const r=document.createElement("button");r.type="button",r.textContent="\xD7",r.title="Remove filter",r.addEventListener("click",()=>{t._dmCtxFilters.splice(l,1),g(t)}),a.appendChild(r),e.appendChild(a)});const d=document.createElement("button");d.type="button",d.className="dm-ctx-chip",d.style.cursor="pointer",d.textContent="Clear all",d.addEventListener("click",()=>{t._dmCtxFilters=[],g(t)}),e.appendChild(d)}function k(t,n,e){if(t._dmCtxEmptyNote&&(t._dmCtxEmptyNote.remove(),t._dmCtxEmptyNote=null),n||!e)return;const d=document.createElement("div");d.className="dm-ctx-empty-note",d.textContent="No entries match the current filters.",t.appendChild(d),t._dmCtxEmptyNote=d}let u=null;function x(){u&&(u.remove(),u=null,document.removeEventListener("mousedown",E,!0),document.removeEventListener("keydown",N,!0),window.removeEventListener("scroll",S,!0))}const E=t=>{u&&!u.contains(t.target)&&x()},N=t=>{t.key==="Escape"&&x()},S=t=>{u&&!u.contains(t.target)&&x()};function f(t,n){const e=document.createElement("button");return e.type="button",e.className="dm-ctx-row",e.textContent=t,e.addEventListener("click",n),e}function L(t){const n=document.createElement("div");return n.className="dm-ctx-heading",n.textContent=t,n}const h=(t,n=24)=>{const e=String(t);return e.length>n?e.slice(0,n)+"\u2026":e};function z(t,n,e,d,o){x();const l=v(e);if(!l)return;const a=document.createElement("div");a.className="dm-ctx-menu",u=a;const s=(i,c,y,p)=>{(e._dmCtxFilters=e._dmCtxFilters||[]).push({field:i,label:c,op:y,value:p}),g(e),x()};if(o&&d){const i=d.data?.[o.name]??"";a.appendChild(L(o.label||o.name)),a.appendChild(f(`Filter: = \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"eq",i))),a.appendChild(f(`Filter: \u2260 \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"ne",i)));const c=String(i);(c!==""&&!Number.isNaN(parseFloat(c))||!Number.isNaN(Date.parse(c)))&&(a.appendChild(f(`Filter: > \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"gt",i))),a.appendChild(f(`Filter: < \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"lt",i))))}else if(d){a.appendChild(L("Filter by"));for(const i of l.fields){const c=d.data?.[i.name];c==null||String(c)===""||a.appendChild(f(`${i.label||i.name}: \u201C${h(c,20)}\u201D`,()=>s(i.name,i.label,"eq",c)))}}const m=document.createElement("div");m.style.padding=".35rem .6rem";const r=document.createElement("input");r.type="text",r.className="dm-ctx-input",r.placeholder="Search\u2026",r.addEventListener("mousedown",i=>i.stopPropagation()),r.addEventListener("keydown",i=>{i.key==="Enter"&&r.value.trim()&&s("*","search","contains",r.value.trim())}),m.appendChild(r),a.appendChild(m);const b=e._dmCtxFilters||[];if(b.length){const i=document.createElement("div");i.style.cssText="border-top:1px solid var(--dm-border,#2a2a40);margin:.25rem 0;",a.appendChild(i),a.appendChild(f(`Clear all filters (${b.length})`,()=>{e._dmCtxFilters=[],g(e),x()}))}document.body.appendChild(a);const C=a.getBoundingClientRect();a.style.left=Math.min(t,window.innerWidth-C.width-8)+"px",a.style.top=Math.min(n,window.innerHeight-C.height-8)+"px",document.addEventListener("mousedown",E,!0),document.addEventListener("keydown",N,!0),window.addEventListener("scroll",S,!0),setTimeout(()=>r.focus(),0)}function I(t,n,e){if(t._dmTable){const s=n.closest("tbody tr");if(!s)return{entry:null,field:null};const m=n.closest("td"),r=s.querySelectorAll("td"),b=Array.from(r).map(c=>c.textContent.trim()),C=e.entries.find(c=>e.fields.every((y,p)=>String(c.data?.[y.name]??"").trim()===(b[p]??"").trim()))||null,i=m&&e.fields[m.cellIndex]||null;return{entry:C,field:i}}const d=n.closest("[data-entry-id]");if(!d)return{entry:null,field:null};const o=e.entries.find(s=>s.id===d.dataset.entryId)||null;if(!o)return{entry:null,field:null};const l=(n.textContent||"").trim().slice(0,80);let a=null;if(l){const s=e.fields.filter(m=>{const r=String(o.data?.[m.name]??"");return r&&(r.includes(l)||l.includes(r))});s.length===1&&(a=s[0])}return{entry:o,field:a}}document.addEventListener("contextmenu",t=>{const n=t.target.closest("[data-ctx], [data-collection-table]");if(!n)return;const e=v(n);if(!e||!e.fields?.length)return;t.preventDefault();const{entry:d,field:o}=I(n,t.target,e);z(t.clientX,t.clientY,n,d,o)})})();
@@ -1 +1 @@
1
- const targets=document.querySelectorAll("[data-form]");targets.length&&targets.forEach(initFormTarget);function showMessage(a,s,t){const e=a.querySelector(".fb-form-success, .fb-form-error");e&&e.remove();const u=document.createElement("div");u.className=t==="success"?"fb-form-success":"fb-form-error",u.textContent=s,a.appendChild(u)}function attachRuntimeLifecycle(a,s){if(a._formLogicRuntime=s,!a.parentNode||typeof MutationObserver>"u")return;const t=new MutationObserver(function(e){for(const u of e)for(const r of u.removedNodes)if(r===a||r.nodeType===1&&r.contains&&r.contains(a)){s.destroy(),t.disconnect();return}});t.observe(a.parentNode,{childList:!0,subtree:!1})}function buildBlueprintFromFields(a,s){const t={};return a.forEach(function(e){if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const u=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),r=e.type==="checkbox"?"boolean":e.type==="date"?"string":u?"chooser":e.type,m={...e.formConfig||{}};m.span==="full"&&s&&(m.span=s);const n={type:r,label:e.label||e.name,required:e.required||!1,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{helperText:e.helper},...e.tooltip&&{tooltip:e.tooltip},...m}};e.minLength!==void 0&&(n.minLength=e.minLength),e.maxLength!==void 0&&(n.maxLength=e.maxLength),e.min!==void 0&&(n.min=e.min),e.max!==void 0&&(n.max=e.max),u&&(e.variant&&(n.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(n.multiple=!0),e.density&&(n.density=e.density),e.columns&&(n.columns=e.columns),e.accent&&(n.accent=e.accent),e.accentStyle&&(n.accentStyle=e.accentStyle),e.glow&&(n.glow=!0),e.glowColour&&(n.glowColour=e.glowColour),e.shadow&&(n.shadow=e.shadow),e.shadowColour&&(n.shadowColour=e.shadowColour)),t[e.name]=n}),t}function buildInitialData(a){const s={};return a.forEach(function(t){if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function patchDateInputs(a,s){(s||[]).forEach(function(t){if(t.type!=="date"||!t.name)return;const e=a.querySelector('[name="'+t.name+'"]');e&&e.type!=="date"&&(e.type="date")})}function buildWizardSteps(a,s){const t=[];let e=[],u=s||"Step 1",r="";return a.forEach(function(m){m.type==="page-break"?(t.push({title:u,description:r,fieldGroup:e}),e=[],u=m.label||"Step "+(t.length+1),r=m.description||""):m.type!=="spacer"&&e.push(m)}),t.push({title:u,description:r,fieldGroup:e}),t}function injectHoneypot(a){const s=document.createElement("div");s.className="fb-form-honeypot",s.setAttribute("aria-hidden","true");const t=document.createElement("input");t.name="website",t.type="text",t.tabIndex=-1,t.autocomplete="url",t.placeholder="https://",s.appendChild(t);const e=document.createElement("input");e.name="_t",e.type="hidden",e.value=Date.now(),s.appendChild(e),a.appendChild(s)}function injectSpacers(a,s){const t=a.querySelector("form");if(!t)return;const e=Array.from(t.querySelectorAll(".form-group"));let u=0;s.forEach(function(r){if(r.type==="spacer"){const m=document.createElement("div");m.className="fb-spacer";const n=e[u];if(n)t.insertBefore(m,n);else{const l=t.querySelector('[type="submit"]');l?t.insertBefore(m,l):t.appendChild(m)}}else r.type!=="page-break"&&u++})}function submitForm(a,s,t,e,u){const r=u||e,m=r.querySelector('[name="website"]')?.value||"",n=r.querySelector('[name="_t"]')?.value||"",l=r?Array.from(r.querySelectorAll('input[type="file"]')):[],p=l.some(c=>c.files&&c.files.length>0);let i,o;if(p){const c=new FormData;for(const[h,f]of Object.entries(s||{}))f!=null&&(l.some(d=>d.name===h)||c.append(h,typeof f=="object"?JSON.stringify(f):String(f)));for(const h of l)for(const f of h.files)c.append(h.name,f,f.name);c.append("_hp",m),c.append("_t",n),i=c,o={}}else{const c=Object.assign({},s,{_hp:m,_t:n});i=JSON.stringify(c),o={"Content-Type":"application/json"}}return fetch("/api/forms/submit/"+a,{method:"POST",headers:o,body:i}).then(c=>c.json().then(h=>({ok:c.ok,body:h}))).then(c=>{c.ok&&c.body.ok?(e.textContent="",showMessage(e,c.body.message||t.successMessage||"Thank you.","success")):showMessage(e,c.body.error||"Something went wrong.","error")}).catch(()=>{showMessage(e,"Unable to submit. Please check your connection.","error")})}function renderManualForm(a,s,t,e,u){const r=document.createElement("form");r.noValidate=!0,s.forEach(function(n){const l=document.createElement("div");l.className="form-group",l.style.marginBottom="1.25rem";const p=document.createElement("label");if(p.className="form-label",p.textContent=n.label||n.name,n.required){const o=document.createElement("span");o.textContent=" *",o.style.color="#f87171",p.appendChild(o)}let i;n.type==="textarea"?(i=document.createElement("textarea"),i.rows=n.formConfig?.rows||4,i.className="form-input"):n.type==="select"?(i=document.createElement("select"),i.className="form-input",(n.options||[]).forEach(function(o){const c=document.createElement("option");c.value=typeof o=="string"?o:o.value??"",c.textContent=typeof o=="string"?o:o.label||c.value,i.appendChild(c)})):(i=document.createElement("input"),i.type=n.type||"text",i.className="form-input",n.placeholder&&(i.placeholder=n.placeholder),n.type==="file"&&(n.file?.accept&&(i.accept=n.file.accept),n.multiple&&(i.multiple=!0))),i.name=n.name,i.required=n.required||!1,l.appendChild(p),l.appendChild(i),r.appendChild(l)}),t.honeypot&&injectHoneypot(r);const m=document.createElement("button");m.type="submit",m.className="btn btn-primary",m.textContent=t.submitText||"Submit",r.appendChild(m),r.addEventListener("submit",function(n){n.preventDefault();const l={};if(s.forEach(function(p){const i=r.querySelector('[name="'+p.name+'"]');i&&(l[p.name]=i.value)}),window.FormLogicEngine&&u){const p=window.FormLogicEngine,i=[],o=[];if(s.forEach(function(c){if(p.evaluateFieldVisibility(c,l)==="hidden"){delete l[c.name];return}const f=p.evaluateFieldRequirement(c,l),d=l[c.name];f&&(!d||!String(d).trim())&&i.push(c.label||c.name);const y=p.validateField(c,d||"",l);y.length&&o.push(y[0].message)}),i.length||o.length){const c=[];i.length&&c.push("Required: "+i.join(", ")),o.length&&c.push(o.join("; ")),showMessage(a,c.join(". "),"error");return}}a.classList.add("fb-form-loading"),m.disabled=!0,submitForm(e,l,t,a,r).finally(function(){a.classList.remove("fb-form-loading"),m.disabled=!1})}),a.appendChild(r),window.FormLogicEngine&&u&&s.some(n=>n.logic)&&new window.FormLogicEngine.FormLogicRuntime(u,a).init()}function initFormTarget(a){const s=a.getAttribute("data-form");s&&fetch("/api/forms/"+s+"/public").then(t=>{if(!t.ok)throw new Error("Form not found: "+s);return t.json()}).then(t=>{const e=t.fields||[],u=t.settings||{},r=document.createElement("div");r.className="fb-form-wrapper",a.appendChild(r);const m=e.some(n=>n.type==="page-break");if(typeof Domma<"u"&&Domma.forms){const n=u.columns||1;if(m&&Domma.forms.wizard){const p=buildWizardSteps(e,t.title).map(function(o){return{title:o.title,description:o.description,fields:buildBlueprintFromFields(o.fieldGroup,n)}}),i=Domma.forms.wizard(r,{schema:{steps:p},onSubmit:function(o){return submitForm(s,o,u,r,null)}});Promise.resolve(i).then(function(){if(patchDateInputs(r,e),window.FormLogicEngine&&e.some(o=>o.logic)){const o=new window.FormLogicEngine.FormLogicRuntime(t,r);o.init(),attachRuntimeLifecycle(r,o)}if(u.honeypot){const o=r.querySelector("form");o&&injectHoneypot(o)}})}else if(Domma.forms.render){const l=buildBlueprintFromFields(e,n),p=buildInitialData(e),i=Domma.forms.render(r,l,p,{submitText:u.submitText||"Submit",layout:u.layout||"stacked",columns:n,onSubmit:function(o){return submitForm(s,o,u,r,null)}});Promise.resolve(i).then(function(){if(patchDateInputs(r,e),window.FormLogicEngine&&e.some(o=>o.logic)){const o=new window.FormLogicEngine.FormLogicRuntime(t,r);o.init(),attachRuntimeLifecycle(r,o)}if(e.some(o=>o.type==="spacer")&&injectSpacers(r,e),u.honeypot){const o=r.querySelector("form");o&&injectHoneypot(o)}})}}else renderManualForm(r,e.filter(n=>n.type!=="page-break"&&n.type!=="spacer"),u,s,t)}).catch(t=>{const e=document.createElement("p");e.textContent="Form unavailable.",e.style.cssText="color:#f87171;font-style:italic;",a.appendChild(e),console.warn("[forms]",t.message)})}
1
+ const v=document.querySelectorAll("[data-form]");v.length&&v.forEach(D);function y(i,o,t){const e=i.querySelector(".fb-form-success, .fb-form-error");e&&e.remove();const a=document.createElement("div");a.className=t==="success"?"fb-form-success":"fb-form-error",a.textContent=o,i.appendChild(a)}function E(i,o){if(i._formLogicRuntime=o,!i.parentNode||typeof MutationObserver>"u")return;const t=new MutationObserver(function(e){for(const a of e)for(const r of a.removedNodes)if(r===i||r.nodeType===1&&r.contains&&r.contains(i)){o.destroy(),t.disconnect();return}});t.observe(i.parentNode,{childList:!0,subtree:!1})}function w(i,o){const t={};return i.forEach(function(e){if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),r=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,l={...e.formConfig||{}};l.span==="full"&&o&&(l.span=o);const n={type:r,label:e.label||e.name,required:e.required||!1,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{helperText:e.helper},...e.tooltip&&{tooltip:e.tooltip},...l}};e.minLength!==void 0&&(n.minLength=e.minLength),e.maxLength!==void 0&&(n.maxLength=e.maxLength),e.min!==void 0&&(n.min=e.min),e.max!==void 0&&(n.max=e.max),a&&(e.variant&&(n.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(n.multiple=!0),e.density&&(n.density=e.density),e.columns&&(n.columns=e.columns),e.accent&&(n.accent=e.accent),e.accentStyle&&(n.accentStyle=e.accentStyle),e.glow&&(n.glow=!0),e.glowColour&&(n.glowColour=e.glowColour),e.shadow&&(n.shadow=e.shadow),e.shadowColour&&(n.shadowColour=e.shadowColour)),t[e.name]=n}),t}function j(i){const o={};return i.forEach(function(t){if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(o[t.name]=typeof e=="object"?e.value:e)}}),o}function C(i,o){(o||[]).forEach(function(t){if(t.type!=="date"||!t.name)return;const e=i.querySelector('[name="'+t.name+'"]');e&&e.type!=="date"&&(e.type="date")})}function N(i,o){const t=[];let e=[],a=o||"Step 1",r="";return i.forEach(function(l){l.type==="page-break"?(t.push({title:a,description:r,fieldGroup:e}),e=[],a=l.label||"Step "+(t.length+1),r=l.description||""):l.type!=="spacer"&&e.push(l)}),t.push({title:a,description:r,fieldGroup:e}),t}const k="padding:.45rem .7rem;border:1px solid var(--dm-border,#ccc);border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:.85em;flex-shrink:0;line-height:1.2;";function L(i){const o=document.createElement("div");o.style.cssText="position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;padding:20px;";const t=document.createElement("div");t.style.cssText="background:var(--dm-surface,#fff);color:var(--dm-text,#111);border:1px solid var(--dm-border,#ddd);border-radius:10px;width:100%;max-width:560px;max-height:75vh;display:flex;flex-direction:column;gap:.6rem;padding:1rem;box-shadow:0 12px 40px rgba(0,0,0,.35);";const e=document.createElement("div");e.style.cssText="display:flex;align-items:center;justify-content:space-between;gap:.5rem;";const a=document.createElement("strong");a.textContent=i;const r=document.createElement("button");r.type="button",r.textContent="\xD7",r.setAttribute("aria-label","Close"),r.style.cssText=k+"font-size:1rem;padding:.15rem .55rem;",e.appendChild(a),e.appendChild(r),t.appendChild(e);const l=document.createElement("div");l.style.cssText="display:flex;flex-direction:column;gap:.6rem;overflow:hidden;min-height:0;",t.appendChild(l),o.appendChild(t);const n=m=>{m.key==="Escape"&&s()};function s(){document.removeEventListener("keydown",n),o.remove()}return r.addEventListener("click",s),o.addEventListener("click",m=>{m.target===o&&s()}),document.addEventListener("keydown",n),document.body.appendChild(o),{body:l,close:s}}function A(i){const{body:o,close:t}=L("Choose an icon"),e=document.createElement("input");e.type="text",e.placeholder="Search icons\u2026",e.className="form-input",e.style.cssText="padding:.45rem .6rem;border:1px solid var(--dm-border,#ccc);border-radius:6px;",o.appendChild(e);const a=document.createElement("div");a.style.cssText="display:grid;grid-template-columns:repeat(auto-fill,minmax(46px,1fr));gap:4px;overflow-y:auto;min-height:0;",o.appendChild(a);const r=window.I&&typeof I.list=="function"?I.list():[],l=r.map(n=>{const s=document.createElement("button");s.type="button",s.title=n,s.dataset.iconName=n,s.style.cssText="display:flex;align-items:center;justify-content:center;padding:.55rem 0;border:1px solid transparent;border-radius:6px;background:transparent;color:inherit;cursor:pointer;";const m=document.createElement("span");return m.setAttribute("data-icon",n),m.setAttribute("data-icon-size","18"),s.appendChild(m),s.addEventListener("mouseenter",()=>{s.style.borderColor="var(--dm-border,#bbb)"}),s.addEventListener("mouseleave",()=>{s.style.borderColor="transparent"}),s.addEventListener("click",()=>{t(),i(n)}),a.appendChild(s),s});if(!r.length){const n=document.createElement("p");n.textContent="Icon engine unavailable \u2014 type an icon name into the field instead.",a.appendChild(n)}window.I?.scan?.(a),e.addEventListener("input",()=>{const n=e.value.trim().toLowerCase();l.forEach(s=>{s.style.display=!n||s.dataset.iconName.includes(n)?"":"none"})}),e.focus()}async function R(){const i=typeof S<"u"&&S.get?S.get("auth_token"):null;if(!i)return{error:"auth"};const o=a=>fetch("/api/media",{headers:{Authorization:"Bearer "+a}});let t;try{if(t=await o(i),t.status===401&&S.get("auth_refresh_token")){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:S.get("auth_refresh_token")})});if(a.ok){const{token:r}=await a.json();S.set("auth_token",r),t=await o(r)}}}catch{return{error:"load"}}if(!t.ok)return{error:t.status===401||t.status===403?"auth":"load"};const e=await t.json().catch(()=>[]);return{files:(Array.isArray(e)?e:[]).filter(a=>/\.(png|jpe?g|gif|webp|svg|avif)$/i.test(a.name||a.url||""))}}function _(i){const{body:o,close:t}=L("Choose an image"),e=document.createElement("p");e.textContent="Loading media library\u2026",e.style.cssText="margin:0;font-size:.85em;opacity:.7;",o.appendChild(e),R().then(({files:a,error:r})=>{if(r){e.textContent=r==="auth"?"Sign in with media access to browse the library \u2014 or paste an image URL into the field.":"Could not load the media library \u2014 paste an image URL into the field instead.";return}if(!a.length){e.textContent="No images in the media library yet.";return}e.remove();const l=document.createElement("div");l.style.cssText="display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;overflow-y:auto;min-height:0;",a.forEach(n=>{const s=document.createElement("button");s.type="button",s.title=n.name||n.url,s.style.cssText="display:flex;flex-direction:column;gap:.25rem;padding:.35rem;border:1px solid var(--dm-border,#ddd);border-radius:8px;background:transparent;color:inherit;cursor:pointer;min-width:0;";const m=document.createElement("img");m.src=n.url,m.alt=n.name||"",m.loading="lazy",m.style.cssText="width:100%;height:72px;object-fit:cover;border-radius:5px;";const u=document.createElement("span");u.textContent=n.name||n.url,u.style.cssText="font-size:.68em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",s.appendChild(m),s.appendChild(u),s.addEventListener("click",()=>{t(),i(n.url)}),l.appendChild(s)}),o.appendChild(l)})}function T(i,o){i.value=o,i.dispatchEvent(new Event("input",{bubbles:!0})),i.dispatchEvent(new Event("change",{bubbles:!0}))}function q(i){const o=document.createElement("div");return o.style.cssText="display:flex;align-items:center;gap:.5rem;",i.parentNode.insertBefore(o,i),o.appendChild(i),i.style.flex="1",i.style.minWidth="0",o}function F(i){const o=document.createElement("button");return o.type="button",o.textContent=i,o.style.cssText=k,o}function B(i){if(!window.I||typeof I.list!="function")return;const o=q(i),t=document.createElement("span");t.style.cssText="display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;flex-shrink:0;border:1px solid var(--dm-border,#ccc);border-radius:6px;";const e=()=>{t.textContent="";const r=i.value.trim();if(!r)return;const l=document.createElement("span");l.setAttribute("data-icon",r),l.setAttribute("data-icon-size","16"),t.appendChild(l),window.I?.scan?.(t)};i.addEventListener("input",e),e();const a=F("Browse");a.addEventListener("click",()=>A(r=>{T(i,r),e()})),o.appendChild(t),o.appendChild(a)}function z(i){const o=q(i),t=document.createElement("img");t.alt="",t.style.cssText="width:2rem;height:2rem;flex-shrink:0;object-fit:cover;border:1px solid var(--dm-border,#ccc);border-radius:6px;display:none;";const e=()=>{const r=i.value.trim();t.style.display=r?"":"none",r&&(t.src=r)};i.addEventListener("input",e),e();const a=F("Browse");a.addEventListener("click",()=>_(r=>{T(i,r),e()})),o.appendChild(t),o.appendChild(a)}export function enhancePickerFields(i,o){const t=typeof i=="string"?document.querySelector(i):i;t&&(o||[]).forEach(function(e){if(!e.name||e.type!=="icon"&&e.type!=="image")return;const a=t.querySelector('[name="'+e.name+'"]');!a||a.dataset.pickerEnhanced||(a.dataset.pickerEnhanced="1",e.type==="icon"?B(a):z(a))})}function g(i){const o=document.createElement("div");o.className="fb-form-honeypot",o.setAttribute("aria-hidden","true");const t=document.createElement("input");t.name="website",t.type="text",t.tabIndex=-1,t.autocomplete="url",t.placeholder="https://",o.appendChild(t);const e=document.createElement("input");e.name="_t",e.type="hidden",e.value=Date.now(),o.appendChild(e),i.appendChild(o)}function O(i,o){const t=i.querySelector("form");if(!t)return;const e=Array.from(t.querySelectorAll(".form-group"));let a=0;o.forEach(function(r){if(r.type==="spacer"){const l=document.createElement("div");l.className="fb-spacer";const n=e[a];if(n)t.insertBefore(l,n);else{const s=t.querySelector('[type="submit"]');s?t.insertBefore(l,s):t.appendChild(l)}}else r.type!=="page-break"&&a++})}function b(i,o,t,e,a){const r=a||e,l=r.querySelector('[name="website"]')?.value||"",n=r.querySelector('[name="_t"]')?.value||"",s=r?Array.from(r.querySelectorAll('input[type="file"]')):[],m=s.some(d=>d.files&&d.files.length>0);let u,c;if(m){const d=new FormData;for(const[h,p]of Object.entries(o||{}))p!=null&&(s.some(f=>f.name===h)||d.append(h,typeof p=="object"?JSON.stringify(p):String(p)));for(const h of s)for(const p of h.files)d.append(h.name,p,p.name);d.append("_hp",l),d.append("_t",n),u=d,c={}}else{const d=Object.assign({},o,{_hp:l,_t:n});u=JSON.stringify(d),c={"Content-Type":"application/json"}}return fetch("/api/forms/submit/"+i,{method:"POST",headers:c,body:u}).then(d=>d.json().then(h=>({ok:d.ok,body:h}))).then(d=>{d.ok&&d.body.ok?(e.textContent="",y(e,d.body.message||t.successMessage||"Thank you.","success")):y(e,d.body.error||"Something went wrong.","error")}).catch(()=>{y(e,"Unable to submit. Please check your connection.","error")})}function P(i,o,t,e,a){const r=document.createElement("form");r.noValidate=!0,o.forEach(function(n){const s=document.createElement("div");s.className="form-group",s.style.marginBottom="1.25rem";const m=document.createElement("label");if(m.className="form-label",m.textContent=n.label||n.name,n.required){const c=document.createElement("span");c.textContent=" *",c.style.color="#f87171",m.appendChild(c)}let u;n.type==="textarea"?(u=document.createElement("textarea"),u.rows=n.formConfig?.rows||4,u.className="form-input"):n.type==="select"?(u=document.createElement("select"),u.className="form-input",(n.options||[]).forEach(function(c){const d=document.createElement("option");d.value=typeof c=="string"?c:c.value??"",d.textContent=typeof c=="string"?c:c.label||d.value,u.appendChild(d)})):(u=document.createElement("input"),u.type=n.type==="icon"||n.type==="image"?"text":n.type||"text",u.className="form-input",n.placeholder&&(u.placeholder=n.placeholder),n.type==="file"&&(n.file?.accept&&(u.accept=n.file.accept),n.multiple&&(u.multiple=!0))),u.name=n.name,u.required=n.required||!1,s.appendChild(m),s.appendChild(u),r.appendChild(s)}),t.honeypot&&g(r);const l=document.createElement("button");l.type="submit",l.className="btn btn-primary",l.textContent=t.submitText||"Submit",r.appendChild(l),r.addEventListener("submit",function(n){n.preventDefault();const s={};if(o.forEach(function(m){const u=r.querySelector('[name="'+m.name+'"]');u&&(s[m.name]=u.value)}),window.FormLogicEngine&&a){const m=window.FormLogicEngine,u=[],c=[];if(o.forEach(function(d){if(m.evaluateFieldVisibility(d,s)==="hidden"){delete s[d.name];return}const p=m.evaluateFieldRequirement(d,s),f=s[d.name];p&&(!f||!String(f).trim())&&u.push(d.label||d.name);const x=m.validateField(d,f||"",s);x.length&&c.push(x[0].message)}),u.length||c.length){const d=[];u.length&&d.push("Required: "+u.join(", ")),c.length&&d.push(c.join("; ")),y(i,d.join(". "),"error");return}}i.classList.add("fb-form-loading"),l.disabled=!0,b(e,s,t,i,r).finally(function(){i.classList.remove("fb-form-loading"),l.disabled=!1})}),i.appendChild(r),enhancePickerFields(i,o),window.FormLogicEngine&&a&&o.some(n=>n.logic)&&new window.FormLogicEngine.FormLogicRuntime(a,i).init()}function D(i){const o=i.getAttribute("data-form");o&&fetch("/api/forms/"+o+"/public").then(t=>{if(!t.ok)throw new Error("Form not found: "+o);return t.json()}).then(t=>{const e=t.fields||[],a=t.settings||{},r=document.createElement("div");r.className="fb-form-wrapper",i.appendChild(r);const l=e.some(n=>n.type==="page-break");if(typeof Domma<"u"&&Domma.forms){const n=a.columns||1;if(l&&Domma.forms.wizard){const m=N(e,t.title).map(function(c){return{title:c.title,description:c.description,fields:w(c.fieldGroup,n)}}),u=Domma.forms.wizard(r,{schema:{steps:m},onSubmit:function(c){return b(o,c,a,r,null)}});Promise.resolve(u).then(function(){if(C(r,e),enhancePickerFields(r,e),window.FormLogicEngine&&e.some(c=>c.logic)){const c=new window.FormLogicEngine.FormLogicRuntime(t,r);c.init(),E(r,c)}if(a.honeypot){const c=r.querySelector("form");c&&g(c)}})}else if(Domma.forms.render){const s=w(e,n),m=j(e),u=Domma.forms.render(r,s,m,{submitText:a.submitText||"Submit",layout:a.layout||"stacked",columns:n,onSubmit:function(c){return b(o,c,a,r,null)}});Promise.resolve(u).then(function(){if(C(r,e),enhancePickerFields(r,e),window.FormLogicEngine&&e.some(c=>c.logic)){const c=new window.FormLogicEngine.FormLogicRuntime(t,r);c.init(),E(r,c)}if(e.some(c=>c.type==="spacer")&&O(r,e),a.honeypot){const c=r.querySelector("form");c&&g(c)}})}}else P(r,e.filter(n=>n.type!=="page-break"&&n.type!=="spacer"),a,o,t)}).catch(t=>{const e=document.createElement("p");e.textContent="Form unavailable.",e.style.cssText="color:#f87171;font-style:italic;",i.appendChild(e),console.warn("[forms]",t.message)})}
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{makeBadgeEl as _,applyColour as A,applyPill as D}from"/public/js/menu-decor.mjs";$(()=>{const C=window.__CMS_NAV__||{},y=window.__CMS_SITE__||{};if(y.autoTheme?.enabled){let o=function(e){const p=(e||"07:00").split(":");return+p[0]*60+(+p[1]||0)},a=function(){const e=new Date,p=e.getHours()*60+e.getMinutes();return p>=o(t.dayStart)&&p<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=y.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const b=$("#site-navbar");if(b.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const p=b.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");p.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=p.querySelector(".navbar-toggle"),h=p.querySelector(".navbar-collapse");if(d&&h){let i=null;const n=()=>{h.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?s():c()})}),h.addEventListener("click",u=>{u.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",u=>{u.key==="Escape"&&h.classList.contains("show")&&n()})}const m=e(C.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=m[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&D(i,s.pill);const u=_(s.badge);u&&i.appendChild(u)})}const v=$("#site-footer");if(v.length){let p=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return p(r).replace(/'/g,"&#39;")},h=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const l=d(r.url||"#"),g=p(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"";return`<li><a href="${l}">${g}</a>${k}</li>`},m=function(r,l){return(r||[]).forEach(g=>{g&&g.type==="separator"||(l.push(g),Array.isArray(g.items)&&g.items.length&&m(g.items,l))}),l};const t=y.social||{},o={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 a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${p(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(h).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(h).join("")}</ul></nav>`),y.footer){const r=Object.keys(o).filter(l=>t[l]);r.length&&(a+='<div class="footer-social">',r.forEach(l=>{const{label:g,svg:k}=o[l];a+=`<a href="${t[l]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",v.html(a);const i=[...m(e.primary,[]),...m(e.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((r,l)=>{const g=i[l];if(!g)return;g.colour&&A(r,g.colour);const k=_(g.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),u=v.get(0).querySelector(".footer-inner");if(u){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,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 g=document.createElement("label");g.className="form-switch footer-motion-switch",g.title="Reduce motion",g.appendChild(r),g.appendChild(l),u.appendChild(g)}}$("#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 t={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 o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),f.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),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(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});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(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,p=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,h=t.dataset.revealDirection==="rtl",m=Array.from(t.children),i={"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)"};m.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=h?m.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*p}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});m.forEach(c=>n.observe(c))}))}),f.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const p=e?.events?.click,{confirm:d,toast:h,alert:m,prompt:i,...n}=p||{};d||h||m||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let u=null;if(!(i&&(u=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),u===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),u!==null&&(n.setText&&r.text(u),n.setVal&&r.val(u),n.setAttr&&r.attr(n.setAttr,u))}n.href&&(window.location.href=n.href),h&&E.toast(h,{type:n.toastType||"success"}),m&&E.alert(m)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}f.length&&x(f.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(y=>{y.__ctaWired||(y.__ctaWired=!0,y.addEventListener("click",async()=>{const b=y.dataset.action,v=y.dataset.entry,w=y.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(y.childNodes).map(a=>a.cloneNode(!0));y.disabled=!0,y.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:v})});try{let a=await o(f);if(a.status===401){const p=S.get("auth_refresh_token");if(p){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:p})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),f=h,a=await o(f)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{y.disabled=!1,y.textContent="",t.forEach(a=>y.appendChild(a)),Domma.icons.scan(y)}}))})}(function(){const y=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!y.length)return;const b=S.get("auth_token");function v(f,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=f;return o.appendChild(a),o}function w(f,t){const o=document.createElement("template");o.innerHTML=t||"",f.replaceWith(o.content)}y.forEach(async f=>{const t=f.dataset.collectionAttrs;if(!t)return;if(!b){f.replaceChildren(v("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=b,e=await o(a);if(e.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:m}=await h.json();S.set("auth_token",m),a=m,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));f.replaceChildren(v(d||"Unable to load entries.",!1));return}const{html:p}=await e.json();w(f,p)}catch{f.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),x(document.body)},100)})(),(function(){const y=document.querySelectorAll("[data-collection-table]");!y.length||typeof T>"u"||!T.create||y.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:w,rows:f,search:t,sortable:o,exportable:a,pageSize:e,empty:p,ctaConfig:d}=v;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const u=document.createElement("span");u.dataset.icon=d.icon,s.appendChild(u),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),m=document.createElement("div");m.id=h,b.replaceChildren(m),T.create("#"+h,{data:f,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:p}),d&&x(m)})})(),(function(){const y=document.querySelectorAll("[data-form-inline]");if(!y.length||typeof F>"u")return;function b(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const p=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),d=e.type==="checkbox"?"boolean":e.type==="date"?"string":p?"chooser":e.type,h={...e.formConfig||{}};h.span==="full"&&o&&(h.span=o);const m={type:d,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...h}};p&&(e.variant&&(m.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(m.multiple=!0),e.density&&(m.density=e.density),e.columns&&(m.columns=e.columns),e.accent&&(m.accent=e.accent),e.accentStyle&&(m.accentStyle=e.accentStyle),e.glow&&(m.glow=!0),e.glowColour&&(m.glowColour=e.glowColour),e.shadow&&(m.shadow=e.shadow),e.shadowColour&&(m.shadowColour=e.shadowColour)),a[e.name]=m}),a}function v(t){const o={};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&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function f(t,o,a){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=a?"color:var(--dm-danger,#f87171);margin-top:.75rem;":"color:var(--dm-success,#4ade80);margin-top:.75rem;"}y.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},p=e.columns||1,d=e.layout||"stacked",h=a.some(c=>c.type==="page-break"),m=async c=>{try{const s=t.querySelector('[name="website"]'),u=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),u!==null&&(r._t=u.value);const l=await H.post(`/api/forms/submit/${o.slug}`,r);if(l&&l.error){f(t,l.error,!0);const g=new Error(l.error);throw g.formSubmitFailed=!0,g}if(l?.redirect){window.location.href=l.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);f(t,l?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||f(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const u=document.createElement("div");u.className="fb-form-honeypot",u.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",u.appendChild(r);const l=document.createElement("input");l.name="_t",l.type="hidden",l.value=Date.now(),u.appendChild(l),s.appendChild(u)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(u){for(const r of u)for(const l of r.removedNodes)if(l===t||l.nodeType===1&&l.contains&&l.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(h&&F.wizard){const c=[];let s=[],u=o.title||"Step 1",r="";a.forEach(g=>{g.type==="page-break"?(c.push({title:u,description:r,fields:b(s,p)}),s=[],u=g.label||`Step ${c.length+1}`,r=g.description||""):g.type!=="spacer"&&s.push(g)}),c.push({title:u,description:r,fields:b(s,p)});const l=F.wizard(t,{schema:{steps:c},onSubmit:m});Promise.resolve(l).then(function(){w(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,b(a,p),v(a),{submitText:e.submitText||"Submit",layout:d,columns:p,onSubmit:m});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
1
+ import{makeBadgeEl as _,applyColour as A,applyPill as I}from"/public/js/menu-decor.mjs";import{enhancePickerFields as D}from"/public/js/forms.js?v=20260722-pickers";$(()=>{const C=window.__CMS_NAV__||{},y=window.__CMS_SITE__||{};if(y.autoTheme?.enabled){let o=function(e){const l=(e||"07:00").split(":");return+l[0]*60+(+l[1]||0)},a=function(){const e=new Date,l=e.getHours()*60+e.getMinutes();return l>=o(t.dayStart)&&l<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=y.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const v=$("#site-navbar");if(v.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const l=v.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");l.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=l.querySelector(".navbar-toggle"),h=l.querySelector(".navbar-collapse");if(d&&h){let i=null;const n=()=>{h.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?s():c()})}),h.addEventListener("click",p=>{p.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",p=>{p.key==="Escape"&&h.classList.contains("show")&&n()})}const m=e(C.items||[]);l.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=m[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&I(i,s.pill);const p=_(s.badge);p&&i.appendChild(p)})}const b=$("#site-footer");if(b.length){let l=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return l(r).replace(/'/g,"&#39;")},h=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const u=d(r.url||"#"),g=l(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"";return`<li><a href="${u}">${g}</a>${k}</li>`},m=function(r,u){return(r||[]).forEach(g=>{g&&g.type==="separator"||(u.push(g),Array.isArray(g.items)&&g.items.length&&m(g.items,u))}),u};const t=y.social||{},o={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 a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${l(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(h).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(h).join("")}</ul></nav>`),y.footer){const r=Object.keys(o).filter(u=>t[u]);r.length&&(a+='<div class="footer-social">',r.forEach(u=>{const{label:g,svg:k}=o[u];a+=`<a href="${t[u]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",b.html(a);const i=[...m(e.primary,[]),...m(e.legal,[])];b.get(0).querySelectorAll(".footer-nav a").forEach((r,u)=>{const g=i[u];if(!g)return;g.colour&&A(r,g.colour);const k=_(g.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),p=b.get(0).querySelector(".footer-inner");if(p){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const u=document.createElement("span");u.className="form-switch-label",u.textContent="Reduce motion";const g=document.createElement("label");g.className="form-switch footer-motion-switch",g.title="Reduce motion",g.appendChild(r),g.appendChild(u),p.appendChild(g)}}$("#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 t={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 o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(o=>o.cloneNode(!0)),this._dmCarouselOpts=t),this._dmCarousel=Domma.elements.carousel(this,t)}),f.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),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 t=Array.from(this.querySelectorAll(".dm-progression-item")).map(e=>{const l={title:e.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:e.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return e.dataset.date&&(l.date=e.dataset.date),e.dataset.status&&(l.status=e.dataset.status),e.dataset.entryId&&(l._entryId=e.dataset.entryId),l}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",a=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...t.length?{items:t}:{}});t.length&&(this._dmCtx={instance:a,items:t})});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(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,l=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,h=t.dataset.revealDirection==="rtl",m=Array.from(t.children),i={"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)"};m.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=h?m.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*l}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});m.forEach(c=>n.observe(c))}))}),f.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const l=e?.events?.click,{confirm:d,toast:h,alert:m,prompt:i,...n}=l||{};d||h||m||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let p=null;if(!(i&&(p=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),p===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),p!==null&&(n.setText&&r.text(p),n.setVal&&r.val(p),n.setAttr&&r.attr(n.setAttr,p))}n.href&&(window.location.href=n.href),h&&E.toast(h,{type:n.toastType||"success"}),m&&E.alert(m)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}f.length&&x(f.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(y=>{y.__ctaWired||(y.__ctaWired=!0,y.addEventListener("click",async()=>{const v=y.dataset.action,b=y.dataset.entry,w=y.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(y.childNodes).map(a=>a.cloneNode(!0));y.disabled=!0,y.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${v}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:b})});try{let a=await o(f);if(a.status===401){const l=S.get("auth_refresh_token");if(l){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:l})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),f=h,a=await o(f)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{y.disabled=!1,y.textContent="",t.forEach(a=>y.appendChild(a)),Domma.icons.scan(y)}}))})}(function(){const y=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!y.length)return;const v=S.get("auth_token");function b(f,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=f;return o.appendChild(a),o}function w(f,t){const o=document.createElement("template");o.innerHTML=t||"",f.replaceWith(o.content)}y.forEach(async f=>{const t=f.dataset.collectionAttrs;if(!t)return;if(!v){f.replaceChildren(b("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=v,e=await o(a);if(e.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:m}=await h.json();S.set("auth_token",m),a=m,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));f.replaceChildren(b(d||"Unable to load entries.",!1));return}const{html:l}=await e.json();w(f,l)}catch{f.replaceChildren(b("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),x(document.body)},100)})(),(function(){const y=document.querySelectorAll("[data-collection-table]");!y.length||typeof T>"u"||!T.create||y.forEach(v=>{let b;try{b=JSON.parse(atob(v.dataset.payload))}catch{return}const{columns:w,rows:f,search:t,sortable:o,exportable:a,pageSize:e,empty:l,ctaConfig:d}=b;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const p=document.createElement("span");p.dataset.icon=d.icon,s.appendChild(p),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),m=document.createElement("div");m.id=h,v.replaceChildren(m),T.create("#"+h,{data:f,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:l}),d&&x(m),v._dmTable={columns:w,rows:f,render(i){T.create("#"+h,{data:i,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:l}),d&&x(m)}}})})(),(function(){const y=document.querySelectorAll("[data-form-inline]");if(!y.length||typeof F>"u")return;function v(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const l=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),d=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":l?"chooser":e.type,h={...e.formConfig||{}};h.span==="full"&&o&&(h.span=o);const m={type:d,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...h}};l&&(e.variant&&(m.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(m.multiple=!0),e.density&&(m.density=e.density),e.columns&&(m.columns=e.columns),e.accent&&(m.accent=e.accent),e.accentStyle&&(m.accentStyle=e.accentStyle),e.glow&&(m.glow=!0),e.glowColour&&(m.glowColour=e.glowColour),e.shadow&&(m.shadow=e.shadow),e.shadowColour&&(m.shadowColour=e.shadowColour)),a[e.name]=m}),a}function b(t){const o={};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&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function f(t,o,a){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=a?"color:var(--dm-danger,#f87171);margin-top:.75rem;":"color:var(--dm-success,#4ade80);margin-top:.75rem;"}y.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},l=e.columns||1,d=e.layout||"stacked",h=a.some(c=>c.type==="page-break"),m=async c=>{try{const s=t.querySelector('[name="website"]'),p=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),p!==null&&(r._t=p.value);const u=await H.post(`/api/forms/submit/${o.slug}`,r);if(u&&u.error){f(t,u.error,!0);const g=new Error(u.error);throw g.formSubmitFailed=!0,g}if(u?.redirect){window.location.href=u.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);f(t,u?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||f(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const p=document.createElement("div");p.className="fb-form-honeypot",p.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",p.appendChild(r);const u=document.createElement("input");u.name="_t",u.type="hidden",u.value=Date.now(),p.appendChild(u),s.appendChild(p)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(p){for(const r of p)for(const u of r.removedNodes)if(u===t||u.nodeType===1&&u.contains&&u.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(h&&F.wizard){const c=[];let s=[],p=o.title||"Step 1",r="";a.forEach(g=>{g.type==="page-break"?(c.push({title:p,description:r,fields:v(s,l)}),s=[],p=g.label||`Step ${c.length+1}`,r=g.description||""):g.type!=="spacer"&&s.push(g)}),c.push({title:p,description:r,fields:v(s,l)});const u=F.wizard(t,{schema:{steps:c},onSubmit:m});Promise.resolve(u).then(function(){w(t,a),D(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,v(a,l),b(a),{submitText:e.submitText||"Submit",layout:d,columns:l,onSubmit:m});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),D(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {getConfig, saveConfig} from '../../config.js';
8
8
  import {authenticate, requirePermission} from '../../middleware/auth.js';
9
- import nodemailer from 'nodemailer';
9
+ import {createTransport} from '../../services/email.js';
10
10
  import fs from 'fs/promises';
11
11
  import path from 'path';
12
12
  import {fileURLToPath} from 'url';
@@ -40,7 +40,12 @@ export async function settingsRoutes(fastify) {
40
40
  'theme', 'adminTheme', 'fontFamily', 'fontSize',
41
41
  'smtp', 'footer', 'analytics', 'social', 'locale',
42
42
  'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
43
- 'adminBrand'
43
+ 'adminBrand',
44
+ // Keys that legitimately live in site.json (written elsewhere — e.g.
45
+ // `brand` by the menus migration) and are echoed back by GET /settings.
46
+ // They must be accepted here or any client that round-trips the GET
47
+ // payload into a save gets a 400 for keys it never touched.
48
+ 'brand', 'url', 'baseTheme'
44
49
  ]);
45
50
  const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
46
51
  if (unknownKeys.length > 0) {
@@ -75,12 +80,9 @@ export async function settingsRoutes(fastify) {
75
80
  return reply.status(400).send({ error: 'SMTP is not configured. Save your SMTP settings first.' });
76
81
  }
77
82
 
78
- const transporter = nodemailer.createTransport({
79
- host: smtp.host,
80
- port: smtp.port || 587,
81
- secure: smtp.secure || false,
82
- auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
83
- });
83
+ // Shared transport factory — carries the self-signed-cert TLS relaxation.
84
+ // Do NOT build a raw nodemailer transport here; that regresses on engine sync.
85
+ const transporter = await createTransport(smtp);
84
86
 
85
87
  const to = request.body?.to || smtp.fromAddress;
86
88
  if (!to) {
@@ -89,19 +89,26 @@ export async function usersRoutes(fastify) {
89
89
  const target = await getUserById(id);
90
90
  if (!target) return reply.status(404).send({ error: 'User not found' });
91
91
 
92
- if (!canManageUser(actor, target)) {
92
+ // Self-edit is always allowed — the hierarchy guard only applies to
93
+ // OTHER users, otherwise nobody can edit a peer-level account (or
94
+ // even their own, since canManageUser is a strict level comparison).
95
+ // Role-escalation guards below still apply to self-edits.
96
+ const isSelf = actor.id === id;
97
+ if (!isSelf && !canManageUser(actor, target)) {
93
98
  return reply.code(403).send({ error: 'You cannot edit a user with that role' });
94
99
  }
95
100
 
96
101
  const updates = request.body || {};
97
- // Prevent role escalation beyond what the actor can manage
98
- if (updates.role && !canManageUser(actor, updates.role)) {
102
+ // Prevent role escalation beyond what the actor can manage.
103
+ // Keeping the target's existing role is never an escalation.
104
+ if (updates.role && updates.role !== target.role && !canManageUser(actor, updates.role)) {
99
105
  return reply.code(403).send({ error: 'You cannot assign that role' });
100
106
  }
101
- // Same check for any additionalRoles being assigned
107
+ // Same check for any additionalRoles being ADDED (already-held ones may be kept)
102
108
  if (Array.isArray(updates.additionalRoles)) {
109
+ const held = Array.isArray(target.additionalRoles) ? target.additionalRoles : [];
103
110
  for (const r of updates.additionalRoles) {
104
- if (!canManageUser(actor, r)) {
111
+ if (!held.includes(r) && !canManageUser(actor, r)) {
105
112
  return reply.code(403).send({ error: `You cannot assign the additional role "${r}"` });
106
113
  }
107
114
  }