domma-cms 0.72.0 → 0.74.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 +1 @@
1
- import{api as x}from"../api.js";import{populateThemeSelect as T}from"../lib/themes.js";const ce=[{value:"#site-navbar",label:"Navigation bar"},{value:"#site-footer",label:"Footer"},{value:".site-content",label:"Page content area"},{value:".page-body",label:"Page body (inside the container)"},{value:".site-sidebar",label:"Sidebar"},{value:".card",label:"Every card"},{value:".cms-form-embed",label:"Embedded forms"},{value:".cms-collection",label:"Collection displays"}];export const themeView={templateUrl:"/admin/js/templates/theme.html",async onMount(o){E.tabs(o.find("#theme-tabs").get(0));const K=E.loader(o.get(0),{type:"dots"});let d,V;try{[d,V]=await Promise.all([x.theme.get(),x.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){K.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}K.destroy();const z=V.tokens||[],B=new Map;async function _(e){if(!e)return null;if(B.has(e))return B.get(e);const t=await x.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return B.set(e,n),n}function ne(){const e=o.find("#theme-auto-enabled").prop("checked");return d.baseTheme||(e?S.val():f.val())||d.theme}const X=["--dm-card-bg","--dm-surface"],Y=[...X,"--dm-text","--dm-primary","--dm-background"];function H(e){const t=document.createElement("span");return t.style.cssText="display:inline-block;width:.7rem;height:.7rem;border-radius:2px;border:1px solid var(--dm-border);vertical-align:middle;margin-right:.25rem;",/^#[0-9a-f]{3,8}$/i.test(e)?t.style.background=e:t.style.background="transparent",t}async function D(e){const t=e.querySelector(".ov-theme-diff");if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);const n=e.querySelector(".ov-theme")?.value;if(!n)return;const s=ne();if(n===s){const r=document.createElement("p");r.className="form-hint text-muted",r.style.cssText="margin:.4rem 0 0;font-size:.8rem;",r.textContent="That is already the site theme, so this changes nothing on its own. Set a token below instead.",t.appendChild(r);return}const[a,l]=await Promise.all([_(s),_(n)]);if(!a||!l)return;const c=Object.keys(l).filter(r=>String(a[r]??"").toLowerCase()!==String(l[r]).toLowerCase()),i=X.filter(r=>c.includes(r)),m=document.createElement("div");m.style.cssText="margin-top:.5rem;padding:.5rem .6rem;border-radius:var(--dm-radius-md);background:var(--dm-surface-raised);font-size:.8rem;";const h=document.createElement("p");if(h.style.cssText="margin:0 0 .35rem;",h.textContent=`Changes ${c.length} of ${Object.keys(l).length} tokens here.`,m.appendChild(h),!i.length){const r=document.createElement("p");r.style.cssText="margin:0 0 .35rem;color:var(--dm-warning);",r.textContent="Both themes use the same surface colour, so this region keeps the fill it has now. Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.",m.appendChild(r)}const p=document.createElement("div");p.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[...Y.filter(r=>c.includes(r)),...c.filter(r=>!Y.includes(r))];for(const r of v.slice(0,6)){const u=document.createElement("span");u.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",u.appendChild(H(a[r]||""));const C=document.createElement("span");C.textContent=`${r} `,u.appendChild(C),u.appendChild(H(l[r])),p.appendChild(u)}if(v.length>6){const r=document.createElement("span");r.style.cssText="color:var(--dm-text-muted);",r.textContent=`+${v.length-6} more`,p.appendChild(r)}m.appendChild(p),t.appendChild(m)}let b=Array.isArray(d.overrides)?d.overrides.map(J):[];const f=o.find("#theme-frontend"),F=o.find("#theme-admin");T(f.get(0)),T(F.get(0),{includeAdmin:!0}),f.val(d.baseTheme||d.theme||"charcoal-dark"),F.val(d.adminTheme||"charcoal-dark"),d.baseTheme&&(f.prop("disabled",!0),o.find("#theme-auto-enabled").prop("disabled",!0),o.find("#theme-auto-roller-hint").show());const S=o.find("#theme-day"),L=o.find("#theme-night");T(S.get(0)),T(L.get(0));const y=d.autoTheme||{};S.val(y.dayTheme||"charcoal-light"),L.val(y.nightTheme||"charcoal-dark"),o.find("#theme-day-start").val(y.dayStart||"07:00"),o.find("#theme-night-start").val(y.nightStart||"19:00");const N=d.switcher||{},q=o.find("#theme-switcher-enabled"),oe=o.find("#theme-switcher-fields");q.prop("checked",N.enabled===!0),o.find("#theme-switcher-position").val(N.position||"right-center"),o.find("#theme-switcher-offset-x").val(N.offsetX??32),o.find("#theme-switcher-offset-y").val(N.offsetY??32);const U=()=>{oe.get(0).style.display=q.prop("checked")?"":"none"};U(),q.on("change",U);const W=o.find("#theme-auto-enabled"),G=o.find("#theme-auto-fields");d.baseTheme||(W.prop("checked",!!y.enabled),y.enabled&&(G.show(),f.prop("disabled",!0)),W.on("change",function(){G.toggle(this.checked),f.prop("disabled",this.checked)})),o.find("#theme-font-family").val(d.font?.family||"Roboto"),o.find("#theme-font-size").val(String(d.font?.size??16));function J(e){return{id:e.id||`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,label:e.label||"",enabled:e.enabled!==!1,scope:e.scope||"",theme:e.theme||"",tokens:{...e.tokens||{}},css:e.css||""}}const g=document.getElementById("theme-overrides"),se=document.getElementById("theme-overrides-empty");function P(){for(;g.firstChild;)g.removeChild(g.firstChild);se.style.display=b.length?"none":"",b.forEach((e,t)=>g.appendChild(ae(e,t))),Domma.icons.scan("#theme-overrides")}function ae(e,t){const n=document.createElement("div");n.className="card mb-3",n.dataset.i=String(t);const s=document.createElement("div");s.className="card-header";const a=document.createElement("h3");a.className="m-0",a.style.fontSize="1rem",a.textContent=e.label||e.scope||`Override ${t+1}`,s.appendChild(a);const l=document.createElement("div");l.style.cssText="display:flex;gap:.5rem;align-items:center;";const c=document.createElement("label");c.style.cssText="display:flex;align-items:center;gap:.35rem;font-size:.85rem;cursor:pointer;";const i=document.createElement("input");i.type="checkbox",i.className="form-check ov-enabled",i.checked=e.enabled,c.appendChild(i),c.appendChild(document.createTextNode("Enabled")),l.appendChild(c);const m=document.createElement("button");m.type="button",m.className="btn btn-ghost btn-sm ov-remove",m.textContent="Remove",l.appendChild(m),s.appendChild(l),n.appendChild(s);const h=document.createElement("div");h.className="card-body";const p=document.createElement("div");p.className="row mb-3",p.appendChild(M("col-4","Name",$("ov-label",e.label,"Navbar island"))),p.appendChild(M("col-8","Target selector",le(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(p);const v=document.createElement("div");v.className="row mb-3";const r=document.createElement("select");r.className="form-select ov-theme",T(r,{includeDefault:!0,defaultLabel:"Keep the site theme"}),r.value=e.theme||"",v.appendChild(M("col-6","Adopt a theme here",r,"Copies every token of that theme onto this region.")),h.appendChild(v);const u=document.createElement("div");u.className="ov-theme-diff",h.appendChild(u);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([re,de])=>{C.appendChild(Q(re,de))}),h.appendChild(Z("Token overrides",C));const A=document.createElement("button");A.type="button",A.className="btn btn-ghost btn-sm ov-add-token",A.textContent="+ Add token",h.appendChild(A);const w=document.createElement("details");w.style.marginTop="1rem";const R=document.createElement("summary");R.style.cssText="cursor:pointer;font-size:.85rem;",R.textContent="Custom CSS for this override",w.appendChild(R);const k=document.createElement("textarea");k.className="form-input ov-css",k.rows=4,k.placeholder="#site-navbar .brand { letter-spacing: .04em; }",k.value=e.css||"",k.style.marginTop=".5rem",w.appendChild(k);const I=document.createElement("p");return I.className="form-hint text-muted",I.style.cssText="font-size:.8rem;margin-top:.35rem;",I.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",w.appendChild(I),h.appendChild(w),n.appendChild(h),setTimeout(()=>D(n),0),n}function Q(e,t){const n=document.createElement("div");n.className="ov-token-row",n.style.cssText="display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;";const s=document.createElement("select");if(s.className="form-select ov-token-name",s.style.flex="1 1 auto",z.length){for(const i of z){const m=document.createElement("option");m.value=i,m.textContent=i,s.appendChild(m)}s.value=e||z[0]}else{const i=document.createElement("option");i.value=e||"",i.textContent=e||"(no tokens found)",s.appendChild(i)}const a=document.createElement("input");a.type="text",a.className="form-input ov-token-value",a.style.flex="0 0 150px",a.placeholder="#0096c7",a.value=t||"";const l=document.createElement("input");l.type="color",l.className="ov-token-swatch",l.style.cssText="flex:0 0 38px;height:34px;padding:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:none;cursor:pointer;",/^#[0-9a-f]{6}$/i.test(t||"")&&(l.value=t),l.addEventListener("input",()=>{a.value=l.value,a.dispatchEvent(new Event("input",{bubbles:!0}))});const c=document.createElement("button");return c.type="button",c.className="btn btn-ghost btn-sm ov-token-remove",c.textContent="\xD7",n.append(s,a,l,c),n}function M(e,t,n,s){const a=document.createElement("div");return a.className=e,a.appendChild(Z(t,n,s)),a}function Z(e,t,n){const s=document.createDocumentFragment(),a=document.createElement("label");if(a.className="form-label",a.textContent=e,s.appendChild(a),s.appendChild(t),n){const l=document.createElement("p");l.className="form-hint text-muted",l.style.cssText="margin-top:.35rem;font-size:.8rem;",l.textContent=n,s.appendChild(l)}return s}function $(e,t,n){const s=document.createElement("input");return s.type="text",s.className=`form-input ${e}`,s.value=t||"",s.placeholder=n||"",s}function le(e){const t=document.createElement("div"),n=$("ov-scope",e,"#site-navbar");if(n.setAttribute("list","theme-scope-options"),t.appendChild(n),!document.getElementById("theme-scope-options")){const s=document.createElement("datalist");s.id="theme-scope-options";for(const a of ce){const l=document.createElement("option");l.value=a.value,l.label=a.label,s.appendChild(l)}t.appendChild(s)}return t}function O(){const e=g.querySelectorAll(".card"),t=[];return e.forEach((n,s)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(l=>{const c=l.querySelector(".ov-token-name")?.value?.trim(),i=l.querySelector(".ov-token-value")?.value?.trim();c&&i&&(a[c]=i)}),t.push({id:b[s]?.id,label:n.querySelector(".ov-label")?.value?.trim()||"",enabled:!!n.querySelector(".ov-enabled")?.checked,scope:n.querySelector(".ov-scope")?.value?.trim()||"",theme:n.querySelector(".ov-theme")?.value||"",tokens:a,css:n.querySelector(".ov-css")?.value||""})}),b=t,t}o.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);O(),b.splice(e,1),P()}),o.on("change",".ov-theme",function(){D(this.closest(".card"))}),o.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{g.querySelectorAll(".card").forEach(D)}),o.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(Q("",""))}),o.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),o.find("#theme-add-override").on("click",()=>{O(),b.push(J({label:"",scope:"",enabled:!0})),P()}),P();async function j(){const e=document.getElementById("theme-css-preview"),t={...ee(),overrides:O()};try{const n=await x.theme.compile(t);e.textContent=n.css?.trim()||"(no overrides - nothing is injected)",e.style.color=""}catch(n){e.textContent=n.message||String(n),e.style.color="var(--dm-danger)"}}o.find("#theme-refresh-css").on("click",j);function ee(){const e=!d.baseTheme&&o.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:L.val()||"charcoal-dark",dayStart:o.find("#theme-day-start").val()||"07:00",nightStart:o.find("#theme-night-start").val()||"19:00"};return{theme:d.baseTheme?d.theme:e?t.dayTheme:f.val(),...d.baseTheme?{baseTheme:d.baseTheme}:{},adminTheme:F.val(),autoTheme:t,font:{family:o.find("#theme-font-family").val()||"Roboto",size:parseInt(o.find("#theme-font-size").val(),10)||16},switcher:{enabled:q.prop("checked"),position:o.find("#theme-switcher-position").val()||"right-center",offsetX:parseInt(o.find("#theme-switcher-offset-x").val(),10)||0,offsetY:parseInt(o.find("#theme-switcher-offset-y").val(),10)||0,themes:Array.isArray(d.switcher?.themes)?d.switcher.themes:[]}}}function te(e){const t=document.getElementById("theme-errors");for(;t.firstChild;)t.removeChild(t.firstChild);if(!e||!e.length){t.style.display="none";return}t.style.display="";for(const n of e){const s=document.createElement("div");s.textContent=n,t.appendChild(s)}t.scrollIntoView({behavior:"smooth",block:"nearest"})}o.find("#theme-save-btn").on("click",async function(){te(null);const e={...ee(),overrides:O()};this.disabled=!0;try{d=await x.theme.save(e),Domma.theme.set(e.adminTheme),E.toast("Theme saved.",{type:"success"}),await j()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];te(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await j()}};
1
+ import{api as w}from"../api.js";import{populateThemeSelect as A}from"../lib/themes.js";const he=[{value:"#site-navbar",label:"Navigation bar"},{value:"#site-footer",label:"Footer"},{value:".site-content",label:"Page content area"},{value:".page-body",label:"Page body (inside the container)"},{value:".site-sidebar",label:"Sidebar"},{value:".card",label:"Every card"},{value:".cms-form-embed",label:"Embedded forms"},{value:".cms-collection",label:"Collection displays"}];export const themeView={templateUrl:"/admin/js/templates/theme.html",async onMount(s){E.tabs(s.find("#theme-tabs").get(0));const X=E.loader(s.get(0),{type:"dots"});let m,q;try{[m,q]=await Promise.all([w.theme.get(),w.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){X.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}X.destroy();const D=q.tokens||[],Y=Array.isArray(q.custom)?q.custom:[];function x(e){if(!Y.length)return;const t=document.createElement("optgroup");t.label="Custom themes";for(const n of Y){const o=document.createElement("option");o.value=n.id,o.textContent=`${n.label} (on ${n.baseTheme})`,t.appendChild(o)}e.appendChild(t)}function T(e,t){const n=e.get?e.get(0):e;if(t&&![...n.options].some(o=>o.value===t)){const o=document.createElement("option");o.value=t,o.textContent=`${t} (not available)`,n.appendChild(o)}n.value=t}const M=new Map;async function H(e){if(!e)return null;if(M.has(e))return M.get(e);const t=await w.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return M.set(e,n),n}function ae(){return(s.find("#theme-auto-enabled").prop("checked")?S.val():f.val())||m.theme}const U=["--dm-card-bg","--dm-surface"],W=[...U,"--dm-text","--dm-primary","--dm-background"];function G(e){const t=document.createElement("span");return t.style.cssText="display:inline-block;width:.7rem;height:.7rem;border-radius:2px;border:1px solid var(--dm-border);vertical-align:middle;margin-right:.25rem;",/^#[0-9a-f]{3,8}$/i.test(e)?t.style.background=e:t.style.background="transparent",t}async function j(e){const t=e.querySelector(".ov-theme-diff");if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);const n=e.querySelector(".ov-theme")?.value;if(!n)return;const o=ae();if(n===o){const l=document.createElement("p");l.className="form-hint text-muted",l.style.cssText="margin:.4rem 0 0;font-size:.8rem;",l.textContent="That is already the site theme, so this changes nothing on its own. Set a token below instead.",t.appendChild(l);return}const[a,r]=await Promise.all([H(o),H(n)]);if(!a||!r)return;const c=Object.keys(r).filter(l=>String(a[l]??"").toLowerCase()!==String(r[l]).toLowerCase()),d=U.filter(l=>c.includes(l)),i=document.createElement("div");i.style.cssText="margin-top:.5rem;padding:.5rem .6rem;border-radius:var(--dm-radius-md);background:var(--dm-surface-raised);font-size:.8rem;";const h=document.createElement("p");if(h.style.cssText="margin:0 0 .35rem;",h.textContent=`Changes ${c.length} of ${Object.keys(r).length} tokens here.`,i.appendChild(h),!d.length){const l=document.createElement("p");l.style.cssText="margin:0 0 .35rem;color:var(--dm-warning);",l.textContent="Both themes use the same surface colour, so this region keeps the fill it has now. Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.",i.appendChild(l)}const p=document.createElement("div");p.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[...W.filter(l=>c.includes(l)),...c.filter(l=>!W.includes(l))];for(const l of v.slice(0,6)){const u=document.createElement("span");u.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",u.appendChild(G(a[l]||""));const C=document.createElement("span");C.textContent=`${l} `,u.appendChild(C),u.appendChild(G(r[l])),p.appendChild(u)}if(v.length>6){const l=document.createElement("span");l.style.cssText="color:var(--dm-text-muted);",l.textContent=`+${v.length-6} more`,p.appendChild(l)}i.appendChild(p),t.appendChild(i)}let b=Array.isArray(m.overrides)?m.overrides.map($):[];const f=s.find("#theme-frontend"),O=s.find("#theme-admin");A(f.get(0)),A(O.get(0),{includeAdmin:!0}),x(f.get(0)),x(O.get(0)),T(f,m.theme||"charcoal-dark"),T(O,m.adminTheme||"charcoal-dark");const S=s.find("#theme-day"),I=s.find("#theme-night");A(S.get(0)),A(I.get(0)),x(S.get(0)),x(I.get(0));const g=m.autoTheme||{};T(S,g.dayTheme||"charcoal-light"),T(I,g.nightTheme||"charcoal-dark"),s.find("#theme-day-start").val(g.dayStart||"07:00"),s.find("#theme-night-start").val(g.nightStart||"19:00");const z=m.switcher||{},B=s.find("#theme-switcher-enabled"),le=s.find("#theme-switcher-fields");B.prop("checked",z.enabled===!0),s.find("#theme-switcher-position").val(z.position||"right-center"),s.find("#theme-switcher-offset-x").val(z.offsetX??32),s.find("#theme-switcher-offset-y").val(z.offsetY??32);const J=()=>{le.get(0).style.display=B.prop("checked")?"":"none"};J(),B.on("change",J);const Q=s.find("#theme-auto-enabled"),Z=s.find("#theme-auto-fields");Q.prop("checked",!!g.enabled),g.enabled&&(Z.show(),f.prop("disabled",!0)),Q.on("change",function(){Z.toggle(this.checked),f.prop("disabled",this.checked)}),s.find("#theme-font-family").val(m.font?.family||"Roboto"),s.find("#theme-font-size").val(String(m.font?.size??16));function $(e){return{id:e.id||`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,label:e.label||"",enabled:e.enabled!==!1,scope:e.scope||"",theme:e.theme||"",tokens:{...e.tokens||{}},css:e.css||""}}const y=document.getElementById("theme-overrides"),re=document.getElementById("theme-overrides-empty");function R(){for(;y.firstChild;)y.removeChild(y.firstChild);re.style.display=b.length?"none":"",b.forEach((e,t)=>y.appendChild(ce(e,t))),Domma.icons.scan("#theme-overrides")}function ce(e,t){const n=document.createElement("div");n.className="card mb-3",n.dataset.i=String(t);const o=document.createElement("div");o.className="card-header";const a=document.createElement("h3");a.className="m-0",a.style.fontSize="1rem",a.textContent=e.label||e.scope||`Override ${t+1}`,o.appendChild(a);const r=document.createElement("div");r.style.cssText="display:flex;gap:.5rem;align-items:center;";const c=document.createElement("label");c.style.cssText="display:flex;align-items:center;gap:.35rem;font-size:.85rem;cursor:pointer;";const d=document.createElement("input");d.type="checkbox",d.className="form-check ov-enabled",d.checked=e.enabled,c.appendChild(d),c.appendChild(document.createTextNode("Enabled")),r.appendChild(c);const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm ov-remove",i.textContent="Remove",r.appendChild(i),o.appendChild(r),n.appendChild(o);const h=document.createElement("div");h.className="card-body";const p=document.createElement("div");p.className="row mb-3",p.appendChild(V("col-4","Name",ne("ov-label",e.label,"Navbar island"))),p.appendChild(V("col-8","Target selector",de(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(p);const v=document.createElement("div");v.className="row mb-3";const l=document.createElement("select");l.className="form-select ov-theme",A(l,{includeDefault:!0,defaultLabel:"Keep the site theme"}),x(l),T(l,e.theme||""),v.appendChild(V("col-6","Adopt a theme here",l,"Copies every token of that theme onto this region.")),h.appendChild(v);const u=document.createElement("div");u.className="ov-theme-diff",h.appendChild(u);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([ie,me])=>{C.appendChild(ee(ie,me))}),h.appendChild(te("Token overrides",C));const L=document.createElement("button");L.type="button",L.className="btn btn-ghost btn-sm ov-add-token",L.textContent="+ Add token",h.appendChild(L);const N=document.createElement("details");N.style.marginTop="1rem";const _=document.createElement("summary");_.style.cssText="cursor:pointer;font-size:.85rem;",_.textContent="Custom CSS for this override",N.appendChild(_);const k=document.createElement("textarea");k.className="form-input ov-css",k.rows=4,k.placeholder="#site-navbar .brand { letter-spacing: .04em; }",k.value=e.css||"",k.style.marginTop=".5rem",N.appendChild(k);const P=document.createElement("p");return P.className="form-hint text-muted",P.style.cssText="font-size:.8rem;margin-top:.35rem;",P.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",N.appendChild(P),h.appendChild(N),n.appendChild(h),setTimeout(()=>j(n),0),n}function ee(e,t){const n=document.createElement("div");n.className="ov-token-row",n.style.cssText="display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;";const o=document.createElement("select");if(o.className="form-select ov-token-name",o.style.flex="1 1 auto",D.length){for(const d of D){const i=document.createElement("option");i.value=d,i.textContent=d,o.appendChild(i)}o.value=e||D[0]}else{const d=document.createElement("option");d.value=e||"",d.textContent=e||"(no tokens found)",o.appendChild(d)}const a=document.createElement("input");a.type="text",a.className="form-input ov-token-value",a.style.flex="0 0 150px",a.placeholder="#0096c7",a.value=t||"";const r=document.createElement("input");r.type="color",r.className="ov-token-swatch",r.style.cssText="flex:0 0 38px;height:34px;padding:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:none;cursor:pointer;",/^#[0-9a-f]{6}$/i.test(t||"")&&(r.value=t),r.addEventListener("input",()=>{a.value=r.value,a.dispatchEvent(new Event("input",{bubbles:!0}))});const c=document.createElement("button");return c.type="button",c.className="btn btn-ghost btn-sm ov-token-remove",c.textContent="\xD7",n.append(o,a,r,c),n}function V(e,t,n,o){const a=document.createElement("div");return a.className=e,a.appendChild(te(t,n,o)),a}function te(e,t,n){const o=document.createDocumentFragment(),a=document.createElement("label");if(a.className="form-label",a.textContent=e,o.appendChild(a),o.appendChild(t),n){const r=document.createElement("p");r.className="form-hint text-muted",r.style.cssText="margin-top:.35rem;font-size:.8rem;",r.textContent=n,o.appendChild(r)}return o}function ne(e,t,n){const o=document.createElement("input");return o.type="text",o.className=`form-input ${e}`,o.value=t||"",o.placeholder=n||"",o}function de(e){const t=document.createElement("div"),n=ne("ov-scope",e,"#site-navbar");if(n.setAttribute("list","theme-scope-options"),t.appendChild(n),!document.getElementById("theme-scope-options")){const o=document.createElement("datalist");o.id="theme-scope-options";for(const a of he){const r=document.createElement("option");r.value=a.value,r.label=a.label,o.appendChild(r)}t.appendChild(o)}return t}function F(){const e=y.querySelectorAll(".card"),t=[];return e.forEach((n,o)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(r=>{const c=r.querySelector(".ov-token-name")?.value?.trim(),d=r.querySelector(".ov-token-value")?.value?.trim();c&&d&&(a[c]=d)}),t.push({id:b[o]?.id,label:n.querySelector(".ov-label")?.value?.trim()||"",enabled:!!n.querySelector(".ov-enabled")?.checked,scope:n.querySelector(".ov-scope")?.value?.trim()||"",theme:n.querySelector(".ov-theme")?.value||"",tokens:a,css:n.querySelector(".ov-css")?.value||""})}),b=t,t}s.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);F(),b.splice(e,1),R()}),s.on("change",".ov-theme",function(){j(this.closest(".card"))}),s.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{y.querySelectorAll(".card").forEach(j)}),s.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(ee("",""))}),s.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),s.find("#theme-add-override").on("click",()=>{F(),b.push($({label:"",scope:"",enabled:!0})),R()}),R();async function K(){const e=document.getElementById("theme-css-preview"),t={...oe(),overrides:F()};try{const n=await w.theme.compile(t);e.textContent=n.css?.trim()||"(no overrides - nothing is injected)",e.style.color=""}catch(n){e.textContent=n.message||String(n),e.style.color="var(--dm-danger)"}}s.find("#theme-refresh-css").on("click",K);function oe(){const e=s.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:I.val()||"charcoal-dark",dayStart:s.find("#theme-day-start").val()||"07:00",nightStart:s.find("#theme-night-start").val()||"19:00"};return{theme:e?t.dayTheme:f.val(),adminTheme:O.val(),autoTheme:t,font:{family:s.find("#theme-font-family").val()||"Roboto",size:parseInt(s.find("#theme-font-size").val(),10)||16},switcher:{enabled:B.prop("checked"),position:s.find("#theme-switcher-position").val()||"right-center",offsetX:parseInt(s.find("#theme-switcher-offset-x").val(),10)||0,offsetY:parseInt(s.find("#theme-switcher-offset-y").val(),10)||0,themes:Array.isArray(m.switcher?.themes)?m.switcher.themes:[]}}}function se(e){const t=document.getElementById("theme-errors");for(;t.firstChild;)t.removeChild(t.firstChild);if(!e||!e.length){t.style.display="none";return}t.style.display="";for(const n of e){const o=document.createElement("div");o.textContent=n,t.appendChild(o)}t.scrollIntoView({behavior:"smooth",block:"nearest"})}s.find("#theme-save-btn").on("click",async function(){se(null);const e={...oe(),overrides:F()};this.disabled=!0;try{m=await w.theme.save(e);const{refreshAdminTheme:n}=await import("../lib/admin-theme.js");await n(w),E.toast("Theme saved.",{type:"success"}),await K()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];se(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await K()}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.72.0",
3
+ "version": "0.74.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",
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{makeBadgeEl as N,applyColour as M,applyPill as B,applyFloat as R,applyHighlight as z,applyItemHighlight as P,itemHighlightAttrs as V,isCurrentItem as O,menuAlign as J}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{initFloatingMenus as W}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as j,markInvalidFields as G,showValidationPopup as X}from"/public/js/forms.js?v=20260821-wizard-grid";import{buildWizardSteps as Y}from"/public/js/form-steps.mjs";$(()=>{const b=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let e=function(a){const t=(a||"07:00").split(":");return+t[0]*60+(+t[1]||0)},n=function(){const a=new Date,t=a.getHours()*60+a.getMinutes();return t>=e(o.dayStart)&&t<e(o.nightStart)?o.dayTheme:o.nightTheme};const o=g.autoTheme;Domma.theme.set(n()),setInterval(()=>Domma.theme.set(n()),6e4)}const k=$("#site-navbar");if(k.length&&b.brand){let n=function(i,l=0){return Array.isArray(i)?i.filter(s=>!(s&&s.type==="spacer")).filter(s=>!(s&&s.type==="separator"&&l===0)).map(s=>s&&s.type==="separator"?{divider:!0}:Array.isArray(s?.items)?{...s,items:n(s.items,l+1)}:s):i},a=function(i){return Array.isArray(i)?i.filter(l=>!(l&&(l.type==="separator"||l.type==="spacer"))):[]};const o={...b.brand},e=o.size&&o.size!=="md"?` navbar-brand-${o.size}`:"";if(o.logo||o.icon||o.tagline){let i="";o.logo?i+=`<img src="${o.logo}" class="navbar-brand-logo" alt="${o.text||""}">`:o.icon&&(i+=`<span data-icon="${o.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),o.text&&(i+=`<span class="navbar-brand-text${e}">${o.text}</span>`),o.tagline&&(i+=`<small class="navbar-brand-tagline">${o.tagline}</small>`),o.html=i}else e&&o.text&&(o.html=`<span class="navbar-brand-text${e}">${o.text}</span>`);const t=b.position==="floating",d=b.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:o,items:n(b.items||[]),variant:b.variant||"dark",position:t?"static":b.position||"sticky",collapsible:!0,appearOnHover:!!b.appearOnHover}),Domma.icons.scan("#site-navbar");const h=k.get(0);h.classList.add(`dm-nav-align-${J(b)||"centre"}`),d&&k.get(0).classList.add("dm-nav-vertical",b.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(h,b),t&&(R(k.get(0),b),W(k.get(0).parentNode||document));const y=h.querySelector(".navbar-toggle"),f=h.querySelector(".navbar-collapse");if(y&&f){let i=null;const l=()=>{f.classList.contains("show")&&y.click()},s=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},r=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",l),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{f.classList.contains("show")?r():s()})}),f.addEventListener("click",c=>{c.target.closest("a")&&requestAnimationFrame(s)}),document.addEventListener("keydown",c=>{c.key==="Escape"&&f.classList.contains("show")&&l()})}const u=h.querySelector(".navbar-nav");if(u){const i=Array.from(u.children);let l=0;for(const s of b.items||[])if(!(s&&s.type==="separator")){if(s&&s.type==="spacer"){const r=document.createElement("li"),c=["xs","sm","md","lg","xl","flex"].includes(s.size)?s.size:"md";r.className=`dm-menu-spacer dm-menu-spacer--${c}`,r.setAttribute("aria-hidden","true"),u.insertBefore(r,i[l]||null);continue}l++}}const w=a(b.items||[]);h.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const l=w[parseInt(i.dataset.index,10)];if(!l)return;const s=i.dataset.subindex!=null,r=s&&Array.isArray(l.items)?l.items[parseInt(i.dataset.subindex,10)]:l;if(!r)return;r.colour&&M(i,r.colour),r.pill&&!s&&B(i,r.pill),P(i,r);const c=N(r.badge);c&&i.appendChild(c),O(r.url,location.pathname)&&(i.classList.add("active"),i.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let t=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return t(r).replace(/'/g,"&#39;")},h=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;const c=d(r.url||"#"),m=t(r.text||""),v=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"",I=V(r),L=[I.hover&&` data-menu-item-hover="${d(I.hover)}"`,I.active&&` data-menu-item-active="${d(I.active)}"`].filter(Boolean).join(""),x=I.accent?` style="--dm-menu-highlight:${d(I.accent)}"`:"";return`<li><a href="${c}"${L}${x}>${m}</a>${v}</li>`},y=function(r,c){return(r||[]).forEach(m=>{m&&(m.type==="separator"||m.type==="spacer")||(c.push(m),Array.isArray(m.items)&&m.items.length&&y(m.items,c))}),c};const o=g.social||{},e={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let n='<div class="footer-inner container">';const a=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(a.copyright&&(n+=`<p>${t(a.copyright)}</p>`),a.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary"><ul>${a.primary.map(h).join("")}</ul></nav>`),a.legal.length&&(n+=`<nav class="footer-nav footer-nav--legal"><ul>${a.legal.map(h).join("")}</ul></nav>`),g.footer){const r=Object.keys(e).filter(c=>o[c]);r.length&&(n+='<div class="footer-social">',r.forEach(c=>{const{label:m,svg:v}=e[c];n+=`<a href="${o[c]}" target="_blank" rel="noopener noreferrer" aria-label="${m}" class="footer-social-link">${v}</a>`}),n+="</div>")}n+="</div>",C.html(n);const f=[...y(a.primary,[]),...y(a.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,c)=>{const m=f[c];if(!m)return;m.colour&&M(r,m.colour);const v=N(m.badge);v&&r.appendChild(v),O(m.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:a.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:a.legalHighlight});const w=g.footer?.showMotionToggle!==!1,i=S.get("reduced_motion"),l=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),s=C.get(0).querySelector(".footer-inner");if(w&&s){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=l,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const c=document.createElement("span");c.className="form-switch-label",c.textContent="Reduce motion";const m=document.createElement("label");m.className="form-switch footer-motion-switch",m.title="Reduce motion",m.appendChild(r),m.appendChild(c),s.appendChild(m)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const o={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const e=parseInt(this.dataset.animationDuration,10);Number.isFinite(e)&&(o.animationDuration=e)}this.dataset.animationEasing&&(o.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(e=>e.cloneNode(!0)),this._dmCarouselOpts=o),this._dmCarousel=Domma.elements.carousel(this,o)}),p.find(".dm-countdown").each(function(){const o={autoStart:!0};this.dataset.to&&(o.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(o.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(o.format=this.dataset.format),Domma.elements.timer(this,o)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(a=>{const t={title:a.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:a.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return a.dataset.date&&(t.date=a.dataset.date),a.dataset.status&&(t.status=a.dataset.status),a.dataset.entryId&&(t._entryId=a.dataset.entryId),t}),e=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",n=Domma.elements.progression(this,{layout:e,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...o.length?{items:o}:{}});o.length&&(this._dmCtx={instance:n,items:o})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(o=>{const e=o.dataset.revealMode||"stagger",n=o.dataset.revealAnimation||"slide-up",a=parseInt(o.dataset.revealDuration,10)||400,t=parseInt(o.dataset.revealStagger,10)||60,d=parseInt(o.dataset.revealDelay,10)||0,h=o.dataset.revealDirection==="rtl",y=Array.from(o.children),f={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((u,w)=>{u.style.opacity="0",u.style.transform=f[n]||"",u.style.transition=`opacity ${a}ms ease, transform ${a}ms ease`;const i=h?y.length-1-w:w;u.style.transitionDelay=e==="stagger"?`${d+i*t}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(w=>{w.forEach(i=>{i.isIntersecting&&(i.target.offsetWidth,i.target.style.opacity="1",i.target.style.transform="none",u.unobserve(i.target))})},{threshold:.1});y.forEach(w=>u.observe(w))}))}),p.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,e=document.getElementById(o);if(!e)return;const n=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",n.setContent(e),n.open()})})}if(typeof $.setup=="function"){const o=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(e=>{try{const n=atob(e.dataset.config),a=JSON.parse(n);Object.assign(o,a)}catch{}}),Object.keys(o).length>0){const e={};for(const[n,a]of Object.entries(o)){const t=a?.events?.click,{confirm:d,toast:h,alert:y,prompt:f,...u}=t||{};d||h||y||f?($(n).on("click",async function(i){if(i.preventDefault(),d&&!await E.confirm(d))return;let l=null;if(!(f&&(l=await E.prompt(f,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),l===null))){if(u.target){const s=$(u.target);u.toggleClass&&s.toggleClass(u.toggleClass),u.addClass&&s.addClass(u.addClass),u.removeClass&&s.removeClass(u.removeClass),l!==null&&(u.setText&&s.text(l),u.setVal&&s.val(l),u.setAttr&&s.attr(u.setAttr,l))}u.href&&(window.location.href=u.href),h&&E.toast(h,{type:u.toastType||"success"}),y&&E.alert(y)}}),Object.keys(u).length&&(e[n]={...a,events:{...a.events,click:u}})):e[n]=a}$.setup(e)}}p.length&&D(p.get(0))});function D(b){b.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const k=g.dataset.action,C=g.dataset.entry,_=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(_&&!await E.confirm(_))return;const o=Array.from(g.childNodes).map(n=>n.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const e=n=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({entryId:C})});try{let n=await e(p);if(n.status===401){const t=S.get("auth_refresh_token");if(t){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),p=h,n=await e(p)}}}const a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||a.message||`Error ${n.status}`);E.toast(a.message||"Action completed.",{type:"success"})}catch(n){E.toast(n.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",o.forEach(n=>g.appendChild(n)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const k=S.get("auth_token");function C(p,o){const e=document.createElement("div");e.className="dm-collection-display dm-collection-empty";const n=document.createElement("p");if(o){n.appendChild(document.createTextNode("Please "));const a=document.createElement("a");a.href="/login",a.textContent="sign in",n.appendChild(a),n.appendChild(document.createTextNode(" to view your entries."))}else n.textContent=p;return e.appendChild(n),e}function _(p,o){const e=document.createElement("template");e.innerHTML=o||"",p.replaceWith(e.content)}g.forEach(async p=>{const o=p.dataset.collectionAttrs;if(!o)return;if(!k){p.replaceChildren(C("",!0));return}const e=n=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({attrs:o})});try{let n=k,a=await e(n);if(a.status===401){const d=S.get("auth_refresh_token");if(d){const h=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(h.ok){const{token:y}=await h.json();S.set("auth_token",y),n=y,a=await e(n)}}}if(!a.ok){const{error:d}=await a.json().catch(()=>({}));p.replaceChildren(C(d||"Unable to load entries.",!1));return}const{html:t}=await a.json();_(p,t)}catch{p.replaceChildren(C("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),D(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(k=>{let C;try{C=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:_,rows:p,search:o,sortable:e,exportable:n,pageSize:a,empty:t,ctaConfig:d,slug:h,exportCfg:y}=C;if(!_?.length)return;if(d){const w=_.findIndex(i=>i.key==="_cta");w!==-1&&(_[w]={key:"_cta",title:"",render:(i,l)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=l._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const r=document.createElement("span");r.dataset.icon=d.icon,s.appendChild(r),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const f="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=f,k.replaceChildren(u),T.create("#"+f,{data:p,columns:_,search:o,sortable:e,exportable:n,pageSize:a,emptyMessage:t}),d&&D(u),k._dmTable={columns:_,rows:p,slug:h,exportCfg:y,render(w){T.create("#"+f,{data:w,columns:_,search:o,sortable:e,exportable:n,pageSize:a,emptyMessage:t}),d&&D(u)}}})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function k(e){const n=e&&e.logic;return!!(n&&(n.visibility||n.requirement))}function C(e,n){const a={};return(e||[]).forEach(t=>{if(t.type==="page-break"||t.type==="spacer"||!t.name)return;const d=t.type==="chooser"||!!(t.variant&&["select","radio","checkbox-group"].includes(t.type)),h=t.type==="checkbox"?"boolean":t.type==="date"||t.type==="icon"||t.type==="image"?"string":d?"chooser":t.type,y={...t.formConfig||{}};y.span==="full"&&n&&(y.span=n);const f={type:h,required:k(t)?!1:t.required,label:t.label,options:t.options,formConfig:{...t.placeholder&&{placeholder:t.placeholder},...t.helper&&{hint:t.helper},...y}};d&&(t.variant&&(f.variant=t.variant),(t.multiple||t.type==="checkbox-group")&&(f.multiple=!0),t.density&&(f.density=t.density),t.columns&&(f.columns=t.columns),t.accent&&(f.accent=t.accent),t.accentStyle&&(f.accentStyle=t.accentStyle),t.glow&&(f.glow=!0),t.glowColour&&(f.glowColour=t.glowColour),t.shadow&&(f.shadow=t.shadow),t.shadowColour&&(f.shadowColour=t.shadowColour)),a[t.name]=f}),a}function _(e){const n={};return(e||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const t=(a.options||[])[0];t&&(n[a.name]=typeof t=="object"?t.value:t)}}),n}function p(e,n){(n||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const t=e.querySelector(`[name="${a.name}"]`);t&&t.type!=="date"&&(t.type="date")})}function o(e,n,a){let t=e.querySelector(".cms-form-message");t||(t=document.createElement("p"),t.className="cms-form-message",e.appendChild(t)),t.textContent=n,t.style.cssText=a?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}g.forEach(e=>{let n;try{n=JSON.parse(atob(e.dataset.formInline))}catch{return}const a=n.fields||[],t=n.settings||{},d=Number(t.columns)===3?3:2,h=t.layout||"grid",y=a.some(l=>l.type==="page-break"),f=async l=>{if(e.dataset.triggerBlocked==="true"){const s=e._formLogicRuntime,r=s&&s.submitBlock?s.submitBlock().reasons:[];o(e,r.join(" ")||"This form cannot be submitted yet.",!0);const c=new Error("Blocked by a form trigger");throw c.formSubmitFailed=!0,c}try{const s=e.querySelector(".fb-form-honeypot"),r=s?.querySelector('input[type="text"]')||null,c=s?.querySelector('[name="_t"]')||null,m=Object.assign({},l);delete m._t,s?.dataset.hpName&&delete m[s.dataset.hpName],r!==null&&(m._hp=r.value),c!==null&&(m._t=c.value);const v=await H.post(`/api/forms/submit/${n.slug}`,m);if(v&&v.error){o(e,v.error,!0);const I=new Error(v.error);throw I.formSubmitFailed=!0,I}if(v?.redirect){window.location.href=v.redirect;return}for(;e.firstChild;)e.removeChild(e.firstChild);o(e,v?.message||t.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||o(e,s.message||"Submission failed. Please try again.",!0),s}},u=Date.now();function w(l){const s=l.querySelector("form");if(!s||s.querySelector(".fb-form-honeypot"))return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const c=document.createElement("input");c.name=s.querySelector('[name="website"]')?"_website":"website",r.dataset.hpName=c.name,c.type="text",c.tabIndex=-1,c.autocomplete="url",c.placeholder="https://",r.appendChild(c);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=u,r.appendChild(m),s.appendChild(r)}function i(l){if(!window.FormLogicEngine||!a.some(r=>r.logic||Array.isArray(r.triggers)&&r.triggers.length))return null;const s=new window.FormLogicEngine.FormLogicRuntime(n,e,l?{baseValues:l}:void 0);if(s.init(),e._formLogicRuntime=s,e._formLogicObserved)return s;if(e._formLogicObserved=!0,e.parentNode&&typeof MutationObserver<"u"){const r=new MutationObserver(function(c){for(const m of c)for(const v of m.removedNodes)if(v===e||v.nodeType===1&&v.contains&&v.contains(e)){e._formLogicRuntime&&e._formLogicRuntime.destroy(),r.disconnect();return}});r.observe(e.parentNode,{childList:!0,subtree:!1}),e._formLogicObserver=r}return s}if(y&&F.wizard){const l=Y(a,n.title),s=l.map(x=>({title:x.title,description:x.description,fields:C(x.fields,d)})),r=l.map(x=>x.fields);let c=0,m=null,v=null;const I=x=>{const A=window.FormLogicEngine;if(!A||!A.validateFields)return;const q=A.validateFields(e,r[c]||[]);q.valid||(x.preventDefault(),x.stopImmediatePropagation(),G(e,q.missing),X(q))},L=()=>{const x=r[c]||a;p(e,x),j(e,x),t.honeypot!==!1&&w(e),m&&m.destroy(),m=i(v?.getStepData?v.getStepData():void 0),e.querySelectorAll(".wizard-next, .wizard-submit").forEach(A=>A.addEventListener("click",I,!0))};v=F.wizard(e,{schema:{steps:s},layout:h,columns:d,finishText:t.submitText||"Finish",onSubmit:f,onStepChange:x=>{c=x,setTimeout(L,0)}}),e.addEventListener("dm-form-trigger",x=>{if(x.detail?.type!=="goto-step"||!v?.goToStep)return;const A=Number(x.detail.action.step);!Number.isFinite(A)||A<0||A>=s.length||A!==c&&v.goToStep(A)}),Promise.resolve(v).then(L)}else if(F.render){const l=F.render(e,C(a,d),_(a),{submitText:t.submitText||"Submit",layout:h,columns:d,onSubmit:f});Promise.resolve(l).then(function(){if(h==="grid"&&t.submitSpan==="full"){const s=e.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}p(e,a),j(e,a),t.honeypot!==!1&&w(e),i()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
1
+ import{makeBadgeEl as N,applyColour as M,applyPill as B,applyFloat as R,applyHighlight as z,applyItemHighlight as P,itemHighlightAttrs as V,isCurrentItem as O,menuAlign as J}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{initFloatingMenus as W}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as j,markInvalidFields as G,showValidationPopup as X}from"/public/js/forms.js?v=20260821-wizard-grid";import{buildWizardSteps as Y}from"/public/js/form-steps.mjs";$(()=>{const b=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let t=function(e){const c=(e||"07:00").split(":");return+c[0]*60+(+c[1]||0)},o=function(){const e=new Date,c=e.getHours()*60+e.getMinutes();return c>=t(a.dayStart)&&c<t(a.nightStart)?a.dayTheme:a.nightTheme},n=function(e){const c=a.apply&&a.apply[e]||{theme:e,custom:""};Domma.theme.set(c.theme);for(const p of[...document.body.classList])p.startsWith("dm-custom-")&&document.body.classList.remove(p);c.custom&&document.body.classList.add(c.custom)};const a=g.autoTheme;n(o()),setInterval(()=>n(o()),6e4)}const k=$("#site-navbar");if(k.length&&b.brand){let o=function(i,d=0){return Array.isArray(i)?i.filter(s=>!(s&&s.type==="spacer")).filter(s=>!(s&&s.type==="separator"&&d===0)).map(s=>s&&s.type==="separator"?{divider:!0}:Array.isArray(s?.items)?{...s,items:o(s.items,d+1)}:s):i},n=function(i){return Array.isArray(i)?i.filter(d=>!(d&&(d.type==="separator"||d.type==="spacer"))):[]};const a={...b.brand},t=a.size&&a.size!=="md"?` navbar-brand-${a.size}`:"";if(a.logo||a.icon||a.tagline){let i="";a.logo?i+=`<img src="${a.logo}" class="navbar-brand-logo" alt="${a.text||""}">`:a.icon&&(i+=`<span data-icon="${a.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),a.text&&(i+=`<span class="navbar-brand-text${t}">${a.text}</span>`),a.tagline&&(i+=`<small class="navbar-brand-tagline">${a.tagline}</small>`),a.html=i}else t&&a.text&&(a.html=`<span class="navbar-brand-text${t}">${a.text}</span>`);const e=b.position==="floating",c=b.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:a,items:o(b.items||[]),variant:b.variant||"dark",position:e?"static":b.position||"sticky",collapsible:!0,appearOnHover:!!b.appearOnHover}),Domma.icons.scan("#site-navbar");const p=k.get(0);p.classList.add(`dm-nav-align-${J(b)||"centre"}`),c&&k.get(0).classList.add("dm-nav-vertical",b.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(p,b),e&&(R(k.get(0),b),W(k.get(0).parentNode||document));const y=p.querySelector(".navbar-toggle"),h=p.querySelector(".navbar-collapse");if(y&&h){let i=null;const d=()=>{h.classList.contains("show")&&y.click()},s=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},r=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",d),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?r():s()})}),h.addEventListener("click",l=>{l.target.closest("a")&&requestAnimationFrame(s)}),document.addEventListener("keydown",l=>{l.key==="Escape"&&h.classList.contains("show")&&d()})}const u=p.querySelector(".navbar-nav");if(u){const i=Array.from(u.children);let d=0;for(const s of b.items||[])if(!(s&&s.type==="separator")){if(s&&s.type==="spacer"){const r=document.createElement("li"),l=["xs","sm","md","lg","xl","flex"].includes(s.size)?s.size:"md";r.className=`dm-menu-spacer dm-menu-spacer--${l}`,r.setAttribute("aria-hidden","true"),u.insertBefore(r,i[d]||null);continue}d++}}const w=n(b.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const d=w[parseInt(i.dataset.index,10)];if(!d)return;const s=i.dataset.subindex!=null,r=s&&Array.isArray(d.items)?d.items[parseInt(i.dataset.subindex,10)]:d;if(!r)return;r.colour&&M(i,r.colour),r.pill&&!s&&B(i,r.pill),P(i,r);const l=N(r.badge);l&&i.appendChild(l),O(r.url,location.pathname)&&(i.classList.add("active"),i.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let e=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},c=function(r){return e(r).replace(/'/g,"&#39;")},p=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;const l=c(r.url||"#"),m=e(r.text||""),v=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(p).join("")}</ul>`:"",L=V(r),I=[L.hover&&` data-menu-item-hover="${c(L.hover)}"`,L.active&&` data-menu-item-active="${c(L.active)}"`].filter(Boolean).join(""),x=L.accent?` style="--dm-menu-highlight:${c(L.accent)}"`:"";return`<li><a href="${l}"${I}${x}>${m}</a>${v}</li>`},y=function(r,l){return(r||[]).forEach(m=>{m&&(m.type==="separator"||m.type==="spacer")||(l.push(m),Array.isArray(m.items)&&m.items.length&&y(m.items,l))}),l};const a=g.social||{},t={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let o='<div class="footer-inner container">';const n=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(n.copyright&&(o+=`<p>${e(n.copyright)}</p>`),n.primary.length&&(o+=`<nav class="footer-nav footer-nav--primary"><ul>${n.primary.map(p).join("")}</ul></nav>`),n.legal.length&&(o+=`<nav class="footer-nav footer-nav--legal"><ul>${n.legal.map(p).join("")}</ul></nav>`),g.footer){const r=Object.keys(t).filter(l=>a[l]);r.length&&(o+='<div class="footer-social">',r.forEach(l=>{const{label:m,svg:v}=t[l];o+=`<a href="${a[l]}" target="_blank" rel="noopener noreferrer" aria-label="${m}" class="footer-social-link">${v}</a>`}),o+="</div>")}o+="</div>",C.html(o);const h=[...y(n.primary,[]),...y(n.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,l)=>{const m=h[l];if(!m)return;m.colour&&M(r,m.colour);const v=N(m.badge);v&&r.appendChild(v),O(m.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:n.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:n.legalHighlight});const w=g.footer?.showMotionToggle!==!1,i=S.get("reduced_motion"),d=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),s=C.get(0).querySelector(".footer-inner");if(w&&s){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=d,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const l=document.createElement("span");l.className="form-switch-label",l.textContent="Reduce motion";const m=document.createElement("label");m.className="form-switch footer-motion-switch",m.title="Reduce motion",m.appendChild(r),m.appendChild(l),s.appendChild(m)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const f=$(".page-body");if(f.length){f.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),f.find(".tabs").each(function(){Domma.elements.tabs(this)}),f.find(".carousel").each(function(){const a={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const t=parseInt(this.dataset.animationDuration,10);Number.isFinite(t)&&(a.animationDuration=t)}this.dataset.animationEasing&&(a.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(t=>t.cloneNode(!0)),this._dmCarouselOpts=a),this._dmCarousel=Domma.elements.carousel(this,a)}),f.find(".dm-countdown").each(function(){const a={autoStart:!0};this.dataset.to&&(a.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(a.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(a.format=this.dataset.format),Domma.elements.timer(this,a)}),f.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),f.find(".dm-progression").each(function(){const a=Array.from(this.querySelectorAll(".dm-progression-item")).map(n=>{const e={title:n.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:n.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return n.dataset.date&&(e.date=n.dataset.date),n.dataset.status&&(e.status=n.dataset.status),n.dataset.entryId&&(e._entryId=n.dataset.entryId),e}),t=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",o=Domma.elements.progression(this,{layout:t,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...a.length?{items:a}:{}});a.length&&(this._dmCtx={instance:o,items:a})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(a=>{const t=a.dataset.revealMode||"stagger",o=a.dataset.revealAnimation||"slide-up",n=parseInt(a.dataset.revealDuration,10)||400,e=parseInt(a.dataset.revealStagger,10)||60,c=parseInt(a.dataset.revealDelay,10)||0,p=a.dataset.revealDirection==="rtl",y=Array.from(a.children),h={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((u,w)=>{u.style.opacity="0",u.style.transform=h[o]||"",u.style.transition=`opacity ${n}ms ease, transform ${n}ms ease`;const i=p?y.length-1-w:w;u.style.transitionDelay=t==="stagger"?`${c+i*e}ms`:`${c}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(w=>{w.forEach(i=>{i.isIntersecting&&(i.target.offsetWidth,i.target.style.opacity="1",i.target.style.transform="none",u.unobserve(i.target))})},{threshold:.1});y.forEach(w=>u.observe(w))}))}),f.find(".card[data-collapsible]").each(function(){const a=this.querySelector(".card-header");a&&a.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const a=this.dataset.soTarget,t=document.getElementById(a);if(!t)return;const o=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",o.setContent(t),o.open()})})}if(typeof $.setup=="function"){const a=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const o=atob(t.dataset.config),n=JSON.parse(o);Object.assign(a,n)}catch{}}),Object.keys(a).length>0){const t={};for(const[o,n]of Object.entries(a)){const e=n?.events?.click,{confirm:c,toast:p,alert:y,prompt:h,...u}=e||{};c||p||y||h?($(o).on("click",async function(i){if(i.preventDefault(),c&&!await E.confirm(c))return;let d=null;if(!(h&&(d=await E.prompt(h,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),d===null))){if(u.target){const s=$(u.target);u.toggleClass&&s.toggleClass(u.toggleClass),u.addClass&&s.addClass(u.addClass),u.removeClass&&s.removeClass(u.removeClass),d!==null&&(u.setText&&s.text(d),u.setVal&&s.val(d),u.setAttr&&s.attr(u.setAttr,d))}u.href&&(window.location.href=u.href),p&&E.toast(p,{type:u.toastType||"success"}),y&&E.alert(y)}}),Object.keys(u).length&&(t[o]={...n,events:{...n.events,click:u}})):t[o]=n}$.setup(t)}}f.length&&D(f.get(0))});function D(b){b.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const k=g.dataset.action,C=g.dataset.entry,_=g.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(_&&!await E.confirm(_))return;const a=Array.from(g.childNodes).map(o=>o.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const t=o=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({entryId:C})});try{let o=await t(f);if(o.status===401){const e=S.get("auth_refresh_token");if(e){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(c.ok){const{token:p}=await c.json();S.set("auth_token",p),f=p,o=await t(f)}}}const n=await o.json().catch(()=>({}));if(!o.ok)throw new Error(n.error||n.message||`Error ${o.status}`);E.toast(n.message||"Action completed.",{type:"success"})}catch(o){E.toast(o.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",a.forEach(o=>g.appendChild(o)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const k=S.get("auth_token");function C(f,a){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const o=document.createElement("p");if(a){o.appendChild(document.createTextNode("Please "));const n=document.createElement("a");n.href="/login",n.textContent="sign in",o.appendChild(n),o.appendChild(document.createTextNode(" to view your entries."))}else o.textContent=f;return t.appendChild(o),t}function _(f,a){const t=document.createElement("template");t.innerHTML=a||"",f.replaceWith(t.content)}g.forEach(async f=>{const a=f.dataset.collectionAttrs;if(!a)return;if(!k){f.replaceChildren(C("",!0));return}const t=o=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({attrs:a})});try{let o=k,n=await t(o);if(n.status===401){const c=S.get("auth_refresh_token");if(c){const p=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:c})});if(p.ok){const{token:y}=await p.json();S.set("auth_token",y),o=y,n=await t(o)}}}if(!n.ok){const{error:c}=await n.json().catch(()=>({}));f.replaceChildren(C(c||"Unable to load entries.",!1));return}const{html:e}=await n.json();_(f,e)}catch{f.replaceChildren(C("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),D(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(k=>{let C;try{C=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:_,rows:f,search:a,sortable:t,exportable:o,pageSize:n,empty:e,ctaConfig:c,slug:p,exportCfg:y}=C;if(!_?.length)return;if(c){const w=_.findIndex(i=>i.key==="_cta");w!==-1&&(_[w]={key:"_cta",title:"",render:(i,d)=>{const s=document.createElement("button");if(s.className=`btn btn-${c.style||"primary"} dm-cta-trigger`,s.dataset.action=c.action||"",s.dataset.entry=d._entryId||"",c.confirm&&(s.dataset.confirm=c.confirm),c.icon){const r=document.createElement("span");r.dataset.icon=c.icon,s.appendChild(r),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(c.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=h,k.replaceChildren(u),T.create("#"+h,{data:f,columns:_,search:a,sortable:t,exportable:o,pageSize:n,emptyMessage:e}),c&&D(u),k._dmTable={columns:_,rows:f,slug:p,exportCfg:y,render(w){T.create("#"+h,{data:w,columns:_,search:a,sortable:t,exportable:o,pageSize:n,emptyMessage:e}),c&&D(u)}}})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function k(t){const o=t&&t.logic;return!!(o&&(o.visibility||o.requirement))}function C(t,o){const n={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const c=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),p=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":c?"chooser":e.type,y={...e.formConfig||{}};y.span==="full"&&o&&(y.span=o);const h={type:p,required:k(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...y}};c&&(e.variant&&(h.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(h.multiple=!0),e.density&&(h.density=e.density),e.columns&&(h.columns=e.columns),e.accent&&(h.accent=e.accent),e.accentStyle&&(h.accentStyle=e.accentStyle),e.glow&&(h.glow=!0),e.glowColour&&(h.glowColour=e.glowColour),e.shadow&&(h.shadow=e.shadow),e.shadowColour&&(h.shadowColour=e.shadowColour)),n[e.name]=h}),n}function _(t){const o={};return(t||[]).forEach(n=>{if(!(!n.name||n.type==="page-break"||n.type==="spacer")&&(n.type==="select"||n.type==="multiselect")&&n.required){const e=(n.options||[])[0];e&&(o[n.name]=typeof e=="object"?e.value:e)}}),o}function f(t,o){(o||[]).forEach(n=>{if(n.type!=="date"||!n.name)return;const e=t.querySelector(`[name="${n.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function a(t,o,n){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=n?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const n=o.fields||[],e=o.settings||{},c=Number(e.columns)===3?3:2,p=e.layout||"grid",y=n.some(d=>d.type==="page-break"),h=async d=>{if(t.dataset.triggerBlocked==="true"){const s=t._formLogicRuntime,r=s&&s.submitBlock?s.submitBlock().reasons:[];a(t,r.join(" ")||"This form cannot be submitted yet.",!0);const l=new Error("Blocked by a form trigger");throw l.formSubmitFailed=!0,l}try{const s=t.querySelector(".fb-form-honeypot"),r=s?.querySelector('input[type="text"]')||null,l=s?.querySelector('[name="_t"]')||null,m=Object.assign({},d);delete m._t,s?.dataset.hpName&&delete m[s.dataset.hpName],r!==null&&(m._hp=r.value),l!==null&&(m._t=l.value);const v=await H.post(`/api/forms/submit/${o.slug}`,m);if(v&&v.error){a(t,v.error,!0);const L=new Error(v.error);throw L.formSubmitFailed=!0,L}if(v?.redirect){window.location.href=v.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);a(t,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||a(t,s.message||"Submission failed. Please try again.",!0),s}},u=Date.now();function w(d){const s=d.querySelector("form");if(!s||s.querySelector(".fb-form-honeypot"))return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const l=document.createElement("input");l.name=s.querySelector('[name="website"]')?"_website":"website",r.dataset.hpName=l.name,l.type="text",l.tabIndex=-1,l.autocomplete="url",l.placeholder="https://",r.appendChild(l);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=u,r.appendChild(m),s.appendChild(r)}function i(d){if(!window.FormLogicEngine||!n.some(r=>r.logic||Array.isArray(r.triggers)&&r.triggers.length))return null;const s=new window.FormLogicEngine.FormLogicRuntime(o,t,d?{baseValues:d}:void 0);if(s.init(),t._formLogicRuntime=s,t._formLogicObserved)return s;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const r=new MutationObserver(function(l){for(const m of l)for(const v of m.removedNodes)if(v===t||v.nodeType===1&&v.contains&&v.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),r.disconnect();return}});r.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=r}return s}if(y&&F.wizard){const d=Y(n,o.title),s=d.map(x=>({title:x.title,description:x.description,fields:C(x.fields,c)})),r=d.map(x=>x.fields);let l=0,m=null,v=null;const L=x=>{const A=window.FormLogicEngine;if(!A||!A.validateFields)return;const q=A.validateFields(t,r[l]||[]);q.valid||(x.preventDefault(),x.stopImmediatePropagation(),G(t,q.missing),X(q))},I=()=>{const x=r[l]||n;f(t,x),j(t,x),e.honeypot!==!1&&w(t),m&&m.destroy(),m=i(v?.getStepData?v.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(A=>A.addEventListener("click",L,!0))};v=F.wizard(t,{schema:{steps:s},layout:p,columns:c,finishText:e.submitText||"Finish",onSubmit:h,onStepChange:x=>{l=x,setTimeout(I,0)}}),t.addEventListener("dm-form-trigger",x=>{if(x.detail?.type!=="goto-step"||!v?.goToStep)return;const A=Number(x.detail.action.step);!Number.isFinite(A)||A<0||A>=s.length||A!==l&&v.goToStep(A)}),Promise.resolve(v).then(I)}else if(F.render){const d=F.render(t,C(n,c),_(n),{submitText:e.submitText||"Submit",layout:p,columns:c,onSubmit:h});Promise.resolve(d).then(function(){if(p==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}f(t,n),j(t,n),e.honeypot!==!1&&w(t),i()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
@@ -1 +1 @@
1
- (function(){"use strict";var a=window.__CMS_THEME_SWITCHER__;if(!a||!Array.isArray(a.themes)||!a.themes.length)return;var r=a.position||"right-center",d=r==="left-center"||r==="bottom-left"||r==="top-left",m=r==="bottom-left"||r==="bottom-right",u=r==="top-left"||r==="top-right",l=a.offsetX!=null?a.offsetX:32,c=a.offsetY!=null?a.offsetY:32,n=document.createElement("div");n.className="dm-theme-switcher"+(d?" dm-ts-left":"")+(u?" dm-ts-top":""),n.id="dm-theme-switcher",n.setAttribute("role","navigation"),n.setAttribute("aria-label","Theme switcher");var o=document.createElement("button");o.className="dm-ts-trigger",o.setAttribute("data-tooltip","Switch theme"),o.setAttribute("aria-haspopup","true"),o.setAttribute("aria-expanded","false");var f=360/a.themes.length;o.style.background="conic-gradient("+a.themes.map(function(e,t){return(e.from||"#888")+" "+(t*f).toFixed(2)+"deg "+((t+1)*f).toFixed(2)+"deg"}).join(", ")+")";var s=document.createElement("div");s.className="dm-ts-options",s.setAttribute("role","menu");var p=a.themes.map(function(e){var t=document.createElement("button");return t.className="dm-ts-dot",t.dataset.theme=e.id,t.setAttribute("data-tooltip",e.label),t.setAttribute("aria-label",e.label),t.setAttribute("role","menuitem"),t.style.background="linear-gradient(135deg, "+(e.from||"#888")+", "+(e.to||e.from||"#888")+")",s.appendChild(t),t});n.appendChild(o),n.appendChild(s),Object.assign(n.style,{right:d?"auto":l+"px",left:d?l+"px":"auto",top:u?c+"px":m?"auto":"50vh",bottom:m?c+"px":"auto",transform:u||m?"none":"translateY(-50%)"});function h(){document.body.appendChild(n)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h();function g(e,t){typeof Domma<"u"&&Domma.theme?e(Domma.theme):t>0&&setTimeout(function(){g(e,t-1)},100)}g(function(e){function t(){var i=e.get();p.forEach(function(b){b.classList.toggle("active",b.dataset.theme===i)})}p.forEach(function(i){i.addEventListener("click",function(){e.set(i.dataset.theme),t()})}),e.onChange(t),t(),o.addEventListener("click",function(){var i=o.getAttribute("aria-expanded")==="true";o.setAttribute("aria-expanded",String(!i))})},30)})();
1
+ (function(){"use strict";var a=window.__CMS_THEME_SWITCHER__;if(!a||!Array.isArray(a.themes)||!a.themes.length)return;var i=a.position||"right-center",m=i==="left-center"||i==="bottom-left"||i==="top-left",u=i==="bottom-left"||i==="bottom-right",l=i==="top-left"||i==="top-right",c=a.offsetX!=null?a.offsetX:32,f=a.offsetY!=null?a.offsetY:32,n=document.createElement("div");n.className="dm-theme-switcher"+(m?" dm-ts-left":"")+(l?" dm-ts-top":""),n.id="dm-theme-switcher",n.setAttribute("role","navigation"),n.setAttribute("aria-label","Theme switcher");var o=document.createElement("button");o.className="dm-ts-trigger",o.setAttribute("data-tooltip","Switch theme"),o.setAttribute("aria-haspopup","true"),o.setAttribute("aria-expanded","false");var p=360/a.themes.length;o.style.background="conic-gradient("+a.themes.map(function(e,t){return(e.from||"#888")+" "+(t*p).toFixed(2)+"deg "+((t+1)*p).toFixed(2)+"deg"}).join(", ")+")";var s=document.createElement("div");s.className="dm-ts-options",s.setAttribute("role","menu");var h=a.themes.map(function(e){var t=document.createElement("button");return t.className="dm-ts-dot",t.dataset.theme=e.id,t.setAttribute("data-tooltip",e.label),t.setAttribute("aria-label",e.label),t.setAttribute("role","menuitem"),t.style.background="linear-gradient(135deg, "+(e.from||"#888")+", "+(e.to||e.from||"#888")+")",s.appendChild(t),t});n.appendChild(o),n.appendChild(s),Object.assign(n.style,{right:m?"auto":c+"px",left:m?c+"px":"auto",top:l?f+"px":u?"auto":"50vh",bottom:u?f+"px":"auto",transform:l||u?"none":"translateY(-50%)"});function b(){document.body.appendChild(n)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",b):b();function g(e,t){typeof Domma<"u"&&Domma.theme?e(Domma.theme):t>0&&setTimeout(function(){g(e,t-1)},100)}g(function(e){function t(){var r=e.get();h.forEach(function(d){d.classList.toggle("active",d.dataset.theme===r)})}h.forEach(function(r){r.addEventListener("click",function(){e.set(r.dataset.theme),document.body.className=document.body.className.split(" ").filter(function(d){return d.indexOf("dm-custom-")!==0}).join(" "),t()})}),e.onChange(t),t(),o.addEventListener("click",function(){var r=o.getAttribute("aria-expanded")==="true";o.setAttribute("aria-expanded",String(!r))})},30)})();
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Projects API
3
3
  *
4
- * GET /api/projects - list visible projects
4
+ * GET /api/projects - list visible projects (?counts=1 adds per-type artefact counts)
5
+ * GET /api/projects/_count - the sidebar badge (the underscore can never be a slug)
5
6
  * GET /api/projects/:slug - full project
6
7
  * POST /api/projects - create
7
8
  * PUT /api/projects/:slug - edit (slug immutable)
@@ -19,9 +20,12 @@ import {
19
20
  CORE_PROJECT_SLUG,
20
21
  createProject,
21
22
  deleteProject,
23
+ emptyGroups,
24
+ getArtefactsByProject,
22
25
  getArtefactsForProject,
23
26
  getProject,
24
27
  listProjectsForUser,
28
+ projectsBadge,
25
29
  untagAllForProject,
26
30
  updateProject
27
31
  } from '../../services/projects.js';
@@ -44,9 +48,19 @@ export async function projectsRoutes(fastify, opts = {}) {
44
48
  const canDelete = {preHandler: [authenticate, requirePermission('projects', 'delete')]};
45
49
 
46
50
  fastify.get('/projects', canRead, async (request) => {
47
- return listProjectsForUser(request.user);
51
+ const list = await listProjectsForUser(request.user);
52
+ if (request.query?.counts !== '1') return list;
53
+ // One request for the Manage projects screen and the sidebar, and one pass over the artefacts.
54
+ const groups = await getArtefactsByProject().catch(() => ({}));
55
+ return list.map((p) => ({
56
+ ...p,
57
+ counts: Object.fromEntries(Object.entries(groups[p.slug] || emptyGroups()).map(([k, v]) => [k, v.length]))
58
+ }));
48
59
  });
49
60
 
61
+ // Registered before /projects/:slug; `_` is outside the slug alphabet, so no project can shadow it.
62
+ fastify.get('/projects/_count', canRead, async (request) => projectsBadge(await listProjectsForUser(request.user)));
63
+
50
64
  fastify.get('/projects/:slug', canRead, async (request, reply) => {
51
65
  const project = await getProject(request.params.slug);
52
66
  if (!project) return reply.status(404).send({error: 'Project not found'});
@@ -4,7 +4,10 @@
4
4
  * GET /api/theme - the theme config (themes, fonts, overrides)
5
5
  * PUT /api/theme - replace it
6
6
  * GET /api/theme/catalog - theme ids and --dm-* token names the shipped
7
- * stylesheet actually defines
7
+ * stylesheet actually defines, and the custom
8
+ * themes plugins provide
9
+ * GET /api/theme/admin - how to apply the admin's own theme (any signed-in
10
+ * user - everyone's admin needs one)
8
11
  * POST /api/theme/compile - compile a candidate config to CSS without saving,
9
12
  * so the admin previews exactly what the page gets
10
13
  *
@@ -16,11 +19,15 @@ import {
16
19
  requirePermission as defaultRequirePermission
17
20
  } from '../../middleware/auth.js';
18
21
  import {
22
+ compileCustomThemeCss,
19
23
  compileOverrideCss,
20
24
  getThemeTokenMap,
25
+ listCustomThemes,
26
+ resolveThemeClasses,
21
27
  listThemeIds,
22
28
  listThemeTokens,
23
29
  loadThemeConfig,
30
+ rememberedIds,
24
31
  saveThemeConfig,
25
32
  validateThemeConfig
26
33
  } from '../../services/themeSettings.js';
@@ -38,9 +45,21 @@ export async function themeRoutes(fastify, opts = {}) {
38
45
  // rather than from a hardcoded list that would drift from the CSS.
39
46
  fastify.get('/theme/catalog', canRead, async () => ({
40
47
  themes: listThemeIds(),
41
- tokens: listThemeTokens()
48
+ tokens: listThemeTokens(),
49
+ // Custom themes are listed apart: they are a base plus changes, and the
50
+ // public switcher cannot offer them.
51
+ custom: listCustomThemes().map(({id, label, baseTheme, owner}) => ({id, label, baseTheme, owner}))
42
52
  }));
43
53
 
54
+ // The admin applies its theme before anything else loads. A custom admin
55
+ // theme is its base, its class, and the CSS behind that class.
56
+ fastify.get('/theme/admin', {preHandler: [authenticate]}, async () => {
57
+ const cfg = await loadThemeConfig();
58
+ const id = cfg.adminTheme || 'charcoal-dark';
59
+ const {dommaTheme, customClass} = resolveThemeClasses(id, cfg);
60
+ return {theme: id, dommaTheme, customClass, css: customClass ? compileCustomThemeCss([id]) : ''};
61
+ });
62
+
44
63
  /*
45
64
  * One theme's token values. Fetched on demand rather than folded into
46
65
  * /catalog: 32 themes x ~245 tokens is a payload nobody needs on a page
@@ -62,7 +81,7 @@ export async function themeRoutes(fastify, opts = {}) {
62
81
 
63
82
  fastify.post('/theme/compile', canRead, async (request, reply) => {
64
83
  const candidate = request.body || {};
65
- const errors = validateThemeConfig(candidate);
84
+ const errors = validateThemeConfig(candidate, {remembered: rememberedIds(await loadThemeConfig())});
66
85
  if (errors.length) return reply.code(400).send({error: errors.join('; '), validation: errors});
67
86
  return {css: compileOverrideCss(candidate)};
68
87
  });
package/server/server.js CHANGED
@@ -525,6 +525,10 @@ registerSidebarItem({item: {source: 'Built in', text: 'Notes', url: '#/notes', i
525
525
  registerSidebarItem({item: {source: 'Built in', text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'}});
526
526
  registerSidebarItem({item: {source: 'Built in', text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'}});
527
527
  registerSidebarItem({item: {source: 'Built in', text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'}});
528
+ // Manage projects lives in the menu (the Projects folder, sidebar-migration.js);
529
+ // registering it here only adds its badge - the renderer merges a registered
530
+ // item into the menu node with the same url rather than adding a second one.
531
+ registerSidebarItem({folder: 'projects', item: {source: 'Built in', text: 'Manage projects', url: '#/projects', icon: 'folder', permission: 'projects', countUrl: '/api/projects/_count'}});
528
532
 
529
533
  await registerPlugins(app);
530
534
 
@@ -25,6 +25,7 @@ import {
25
25
  registerTransform
26
26
  } from './hooks.js';
27
27
  import {registerPluginResource, unregisterPluginResourcesByPlugin} from './permissionRegistry.js';
28
+ import {registerThemeProvider} from './themeSettings.js';
28
29
  import {classifyEntitlement, mayLoad} from './pluginEntitlement.js';
29
30
  import {createCollection, getCollection} from './collections.js';
30
31
  import * as defaultRolesService from './roles.js';
@@ -464,6 +465,9 @@ export async function registerPlugins(fastify) {
464
465
  hooks: {registerShortcode, registerSanitizeRules, registerTransform, registerMenuLocation,
465
466
  // Stamped with the plugin's name, so the Menus editor can say where a Tool came from.
466
467
  registerSidebarItem: (opts) => registerSidebarItem({...opts, item: {source: manifest.displayName || manifest.name, ...opts?.item}}),
468
+ // Custom themes the plugin offers (the Theme Roller) - bound to the
469
+ // plugin, so one plugin cannot replace another's. See themeSettings.js.
470
+ registerThemes: (provider) => registerThemeProvider(manifest.name, provider),
467
471
  on: hooks.on.bind(hooks), ...pluginRoleHooks(manifest.name)},
468
472
  settings,
469
473
  config: {}
@@ -108,8 +108,16 @@ export async function getProject(slug) {
108
108
  * @returns {Promise<object>} The persisted project record.
109
109
  * @throws {Error} If validation fails or the slug already exists.
110
110
  */
111
+ /**
112
+ * Slugs a NEW project cannot take: `#/projects/new` and `#/projects/edit/…`
113
+ * are admin addresses, so a project called either could never be opened.
114
+ * (Checked on create only - an existing record keeps working.)
115
+ */
116
+ export const RESERVED_PROJECT_SLUGS = ['new', 'edit'];
117
+
111
118
  export async function createProject(input) {
112
119
  const errors = validateProject(input);
120
+ if (RESERVED_PROJECT_SLUGS.includes(input?.slug)) errors.push(`"${input.slug}" is reserved - the admin uses it in its own addresses`);
113
121
  if (errors.length) throw new Error('Validation failed: ' + errors.join('; '));
114
122
  if (await getProject(input.slug)) {
115
123
  throw new Error(`Project "${input.slug}" already exists`);
@@ -397,23 +405,28 @@ export async function getProjectForPage(urlPath, explicitProject) {
397
405
  return best ?? coreFallback;
398
406
  }
399
407
 
408
+ /** The per-type buckets every project's artefacts are grouped into. */
409
+ export function emptyGroups() {
410
+ return {
411
+ pages: [], collections: [], forms: [], actions: [],
412
+ menus: [], blocks: [], components: [], views: [], roles: [], users: [], apis: []
413
+ };
414
+ }
415
+
400
416
  /**
401
- * Enumerate every artefact belonging to the given project. Returns a flat
402
- * grouping by type - each value is an array of artefact summary objects.
403
- * Used by the project detail page (counts + per-type tabs).
417
+ * Every artefact, grouped by the project it resolves to - ONE pass over each
418
+ * artefact type, however many projects there are. The Manage projects list
419
+ * (GET /api/projects?counts=1) needs every project's counts, and asking
420
+ * getArtefactsForProject once per project re-read every artefact N times.
404
421
  *
405
- * Membership is by resolution, not tag equality: artefacts without a
406
- * `meta.project` tag belong to the core project, so
407
- * `getArtefactsForProject('core')` enumerates everything unclaimed.
422
+ * Membership is by resolution, not tag equality: an artefact without a
423
+ * `meta.project` tag lands under core.
408
424
  *
409
- * @param {string} projectSlug
410
- * @returns {Promise<{pages:object[], collections:object[], forms:object[], actions:object[], menus:object[], blocks:object[], components:object[], views:object[], roles:object[], users:object[], apis:object[]}>}
425
+ * @returns {Promise<Record<string, ReturnType<typeof emptyGroups>>>} keyed by project slug; a project with nothing is absent
411
426
  */
412
- export async function getArtefactsForProject(projectSlug) {
413
- const out = {
414
- pages: [], collections: [], forms: [], actions: [],
415
- menus: [], blocks: [], components: [], views: [], roles: [], users: [], apis: []
416
- };
427
+ export async function getArtefactsByProject() {
428
+ const groups = {};
429
+ const out = (slug) => (groups[slug] ??= emptyGroups());
417
430
 
418
431
  // The three loops below re-read each artefact by slug; guard PER ITEM so a
419
432
  // single unreadable artefact (e.g. a form whose internal slug doesn't match
@@ -423,7 +436,7 @@ export async function getArtefactsForProject(projectSlug) {
423
436
  for (const m of await listMenus()) {
424
437
  try {
425
438
  const full = await getMenu(m.slug);
426
- if (resolveArtefactProject(full) === projectSlug) out.menus.push(m);
439
+ out(resolveArtefactProject(full)).menus.push(m);
427
440
  } catch { /* skip broken menu */ }
428
441
  }
429
442
  } catch { /* service unavailable */ }
@@ -433,7 +446,7 @@ export async function getArtefactsForProject(projectSlug) {
433
446
  for (const f of await listForms()) {
434
447
  try {
435
448
  const full = await readForm(f.slug);
436
- if (resolveArtefactProject(full) === projectSlug) out.forms.push(f);
449
+ out(resolveArtefactProject(full)).forms.push(f);
437
450
  } catch { /* skip broken form */ }
438
451
  }
439
452
  } catch { /* skip */ }
@@ -443,7 +456,7 @@ export async function getArtefactsForProject(projectSlug) {
443
456
  for (const c of await listCollections()) {
444
457
  try {
445
458
  const full = await getCollection(c.slug);
446
- if (resolveArtefactProject(full) === projectSlug) out.collections.push(c);
459
+ out(resolveArtefactProject(full)).collections.push(c);
447
460
  } catch { /* skip broken collection */ }
448
461
  }
449
462
  } catch { /* skip */ }
@@ -451,7 +464,7 @@ export async function getArtefactsForProject(projectSlug) {
451
464
  try {
452
465
  const {listActions} = await import('./actions.js');
453
466
  for (const a of await listActions()) {
454
- if (resolveArtefactProject(a) === projectSlug) out.actions.push(a);
467
+ out(resolveArtefactProject(a)).actions.push(a);
455
468
  }
456
469
  } catch { /* skip */ }
457
470
 
@@ -460,7 +473,7 @@ export async function getArtefactsForProject(projectSlug) {
460
473
  // per-block read is needed. (Blocks are keyed by `name`, not `slug`.)
461
474
  const {listBlocks} = await import('./blocks.js');
462
475
  for (const b of await listBlocks()) {
463
- if (resolveArtefactProject(b) === projectSlug) out.blocks.push(b);
476
+ out(resolveArtefactProject(b)).blocks.push(b);
464
477
  }
465
478
  } catch { /* skip */ }
466
479
 
@@ -469,28 +482,28 @@ export async function getArtefactsForProject(projectSlug) {
469
482
  // per-component read is needed. (Components are keyed by `name`.)
470
483
  const {listComponents} = await import('./components.js');
471
484
  for (const c of await listComponents()) {
472
- if (resolveArtefactProject(c) === projectSlug) out.components.push(c);
485
+ out(resolveArtefactProject(c)).components.push(c);
473
486
  }
474
487
  } catch { /* skip */ }
475
488
 
476
489
  try {
477
490
  const {listViews} = await import('./views.js');
478
491
  for (const v of await listViews()) {
479
- if (resolveArtefactProject(v) === projectSlug) out.views.push(v);
492
+ out(resolveArtefactProject(v)).views.push(v);
480
493
  }
481
494
  } catch { /* skip */ }
482
495
 
483
496
  try {
484
497
  const {getRoleMap} = await import('./roles.js');
485
498
  for (const role of getRoleMap().values()) {
486
- if (resolveArtefactProject(role) === projectSlug) out.roles.push(role);
499
+ out(resolveArtefactProject(role)).roles.push(role);
487
500
  }
488
501
  } catch { /* skip */ }
489
502
 
490
503
  try {
491
504
  const {listUsers} = await import('./users.js');
492
505
  for (const u of await listUsers()) {
493
- if (resolveArtefactProject(u) === projectSlug) out.users.push(u);
506
+ out(resolveArtefactProject(u)).users.push(u);
494
507
  }
495
508
  } catch { /* skip */ }
496
509
 
@@ -500,7 +513,7 @@ export async function getArtefactsForProject(projectSlug) {
500
513
  const url = p.url || p.urlPath || '/';
501
514
  const explicit = (p.project === undefined) ? undefined : p.project;
502
515
  const pageProject = await getProjectForPage(url, explicit);
503
- if (pageProject === projectSlug) out.pages.push(p);
516
+ out(pageProject).pages.push(p);
504
517
  }
505
518
  } catch { /* skip */ }
506
519
 
@@ -509,13 +522,27 @@ export async function getArtefactsForProject(projectSlug) {
509
522
  // field - no untagged-→core fallback applies, so match directly).
510
523
  const {entries} = await listEntries('api-endpoints', {limit: 0});
511
524
  for (const e of entries) {
512
- if (e.data?.project === projectSlug) {
513
- out.apis.push({id: e.id, name: e.data.name, path: e.data.path, collection: e.data.collection});
525
+ if (e.data?.project) {
526
+ out(e.data.project).apis.push({id: e.id, name: e.data.name, path: e.data.path, collection: e.data.collection});
514
527
  }
515
528
  }
516
529
  } catch { /* skip */ }
517
530
 
518
- return out;
531
+ return groups;
532
+ }
533
+
534
+ /**
535
+ * Enumerate every artefact belonging to the given project. Returns a flat
536
+ * grouping by type - each value is an array of artefact summary objects.
537
+ * Used by the project detail page (counts + per-type tabs).
538
+ *
539
+ * `getArtefactsForProject('core')` enumerates everything unclaimed.
540
+ *
541
+ * @param {string} projectSlug
542
+ * @returns {Promise<ReturnType<typeof emptyGroups>>}
543
+ */
544
+ export async function getArtefactsForProject(projectSlug) {
545
+ return (await getArtefactsByProject())[projectSlug] ?? emptyGroups();
519
546
  }
520
547
 
521
548
  /**
@@ -644,3 +671,36 @@ export async function untagAllForProject(projectSlug) {
644
671
 
645
672
  return counts;
646
673
  }
674
+
675
+ /**
676
+ * The sidebar badge on Manage projects (its countUrl, GET /api/projects/_count):
677
+ * how many projects this user sees - a count to glance at, not a warning -
678
+ * with the disabled ones called out. Plain text throughout: the
679
+ * popover sets it with textContent.
680
+ *
681
+ * @param {object[]} projects - as listProjectsForUser returns them
682
+ * @returns {{count: number, label: string, tone: string, details: object}}
683
+ */
684
+ export function projectsBadge(projects = []) {
685
+ const list = [...projects].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)
686
+ || String(a.name || a.slug).localeCompare(String(b.name || b.slug)));
687
+ const off = list.filter(p => p.enabled === false);
688
+ const n = list.length;
689
+ return {
690
+ count: n,
691
+ label: `${n} project${n === 1 ? '' : 's'}${off.length ? `, ${off.length} disabled` : ''}`,
692
+ tone: 'info',
693
+ details: {
694
+ title: 'Projects',
695
+ rows: [
696
+ {label: 'Enabled', value: String(n - off.length)},
697
+ {label: 'Disabled', value: String(off.length)}
698
+ ],
699
+ items: list.slice(0, 8).map(p => ({
700
+ text: String(p.name || p.slug),
701
+ meta: [p.slug, p.rootUrl, p.enabled === false ? 'disabled' : '', p.protected ? 'built in' : ''].filter(Boolean).join(' · ')
702
+ })),
703
+ empty: 'No projects yet.'
704
+ }
705
+ };
706
+ }